Skip to content

Commit c887249

Browse files
Clean Up File Management (#1969)
### Description #### App changes: - Creating a new file always selects it in the project navigator and selects or opens it in a tab. - Creating a new folder always selects it. - Renaming a file always selects it. - Renaming a file no longer sometimes adds a trailing period to the file name. - File system updates no longer clear the project navigator's selection. - No longer attempts to save folders and files whose name was not changed when the name field is submitted. - Fixes a case where the navigator filter was empty but files were still being filtered. #### Code changes: - Cleans up the file management API for the `CEWorkspaceFileManager`. - Each method now throws, and if it creates a file or folder it returns it. - Errors are logged by the file manager's own logger before being thrown, for better debugging output and standardization across the code base. - Introduced a `FileManagerError` enum that has meaningful error description and resolution messages. This type is now used in the file management methods where it makes sense. The type is suitable for presenting in an `NSAlert`. - Standardizes up how the workspace is notified of new/moved/copied files. Changes include the project navigator and file inspector. - Always notifies the project navigator and other listeners using the `workspace.listenerModel`, as well as opening a tab if it's not a folder. - If file errors are thrown, presents an `NSAlert` with the error information. - Removed a `DispatchQueue.async` call in the project navigator that caused a new selection received by the project navigator to be overwritten, causing a flashing selection when new files were created and subsequently selected, which also resulted in incorrect selections. - Moved `String.isValidFilename` to its own file, and added checks for more invalid cases. - Added a flag to the `DirectoryEventStream` to receive more timely notifications. - Added a check to see if the project navigator filter is empty before displaying filtered files. #### Tests: - Added UI tests to the project navigator. - Added unit tests for the `isValidFilename` extension. - Added a UI testing document. - Uncommented the directory changes listener test after adding the new flag to the directory event stream. ### Related Issues - Closes #1934 - Closes #1966 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots <details><summary>On main</summary> <p> https://github.com/user-attachments/assets/f2404f06-aa59-4486-95c2-8cf9432254b2 https://github.com/user-attachments/assets/af365adf-0704-475f-bd1e-347f66855114 </p> </details> https://github.com/user-attachments/assets/4c3b7a3b-3c30-45fa-b7f3-6e428c10641e https://github.com/user-attachments/assets/02967b41-409c-4bbc-bd86-0d12469852d6
1 parent 6d16c90 commit c887249

23 files changed

+721
-251
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@
480480
6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */; };
481481
6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */; };
482482
6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; };
483+
6CDAFDDD2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDAFDDC2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift */; };
484+
6CDAFDDF2D35DADD002B2D47 /* String+ValidFileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDAFDDE2D35DADD002B2D47 /* String+ValidFileName.swift */; };
483485
6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE21E802C643D8F0031B056 /* CETerminalView.swift */; };
484486
6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE21E862C650D2C0031B056 /* SwiftTerm */; };
485487
6CE622692A2A174A0013085C /* InspectorTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE622682A2A174A0013085C /* InspectorTab.swift */; };
@@ -488,6 +490,8 @@
488490
6CED16E42A3E660D000EC962 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CED16E32A3E660D000EC962 /* String+Lines.swift */; };
489491
6CFBA54B2C4E168A00E3A914 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFBA54A2C4E168A00E3A914 /* App.swift */; };
490492
6CFBA54D2C4E16C900E3A914 /* WindowCloseCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFBA54C2C4E16C900E3A914 /* WindowCloseCommandTests.swift */; };
493+
6CFC0C3C2D381D2000F09CD0 /* ProjectNavigatorFileManagementUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFC0C3B2D381D2000F09CD0 /* ProjectNavigatorFileManagementUITests.swift */; };
494+
6CFC0C3E2D382B3F00F09CD0 /* UI TESTING.md in Resources */ = {isa = PBXBuildFile; fileRef = 6CFC0C3D2D382B3900F09CD0 /* UI TESTING.md */; };
491495
6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967329BEBCC300182D6F /* FindCommands.swift */; };
492496
6CFF967629BEBCD900182D6F /* FileCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967529BEBCD900182D6F /* FileCommands.swift */; };
493497
6CFF967829BEBCF600182D6F /* MainCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF967729BEBCF600182D6F /* MainCommands.swift */; };
@@ -1161,13 +1165,17 @@
11611165
6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageIdentifier+CodeLanguage.swift"; sourceTree = "<group>"; };
11621166
6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentTests.swift"; sourceTree = "<group>"; };
11631167
6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = "<group>"; };
1168+
6CDAFDDC2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFileManager+Error.swift"; sourceTree = "<group>"; };
1169+
6CDAFDDE2D35DADD002B2D47 /* String+ValidFileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ValidFileName.swift"; sourceTree = "<group>"; };
11641170
6CE21E802C643D8F0031B056 /* CETerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETerminalView.swift; sourceTree = "<group>"; };
11651171
6CE622682A2A174A0013085C /* InspectorTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorTab.swift; sourceTree = "<group>"; };
11661172
6CE6226A2A2A1C730013085C /* UtilityAreaTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTab.swift; sourceTree = "<group>"; };
11671173
6CE6226D2A2A1CDE0013085C /* NavigatorTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorTab.swift; sourceTree = "<group>"; };
11681174
6CED16E32A3E660D000EC962 /* String+Lines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Lines.swift"; sourceTree = "<group>"; };
11691175
6CFBA54A2C4E168A00E3A914 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
11701176
6CFBA54C2C4E16C900E3A914 /* WindowCloseCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCloseCommandTests.swift; sourceTree = "<group>"; };
1177+
6CFC0C3B2D381D2000F09CD0 /* ProjectNavigatorFileManagementUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectNavigatorFileManagementUITests.swift; sourceTree = "<group>"; };
1178+
6CFC0C3D2D382B3900F09CD0 /* UI TESTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "UI TESTING.md"; sourceTree = "<group>"; };
11711179
6CFF967329BEBCC300182D6F /* FindCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindCommands.swift; sourceTree = "<group>"; };
11721180
6CFF967529BEBCD900182D6F /* FileCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCommands.swift; sourceTree = "<group>"; };
11731181
6CFF967729BEBCF600182D6F /* MainCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCommands.swift; sourceTree = "<group>"; };
@@ -2425,6 +2433,7 @@
24252433
58A2E40629C3975D005CB615 /* CEWorkspaceFileIcon.swift */,
24262434
58710158298EB80000951BA4 /* CEWorkspaceFileManager.swift */,
24272435
77EF6C0C2C60E23400984B69 /* CEWorkspaceFileManager+DirectoryEvents.swift */,
2436+
6CDAFDDC2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift */,
24282437
6CB52DC82AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift */,
24292438
6C049A362A49E2DB00D42923 /* DirectoryEventStream.swift */,
24302439
);
@@ -2502,18 +2511,18 @@
25022511
children = (
25032512
588847672992AAB800996D95 /* Array */,
25042513
5831E3C72933E7F700D5A6D2 /* Bundle */,
2505-
5831E3C62933E7E600D5A6D2 /* Color */,
25062514
669A504F2C380BFD00304CD8 /* Collection */,
2515+
5831E3C62933E7E600D5A6D2 /* Color */,
25072516
5831E3C82933E80500D5A6D2 /* Date */,
25082517
6CB94D002C9F1CF900E8651C /* LanguageIdentifier */,
25092518
6C82D6C429C0129E00495C54 /* NSApplication */,
25102519
5831E3D02934036D00D5A6D2 /* NSTableView */,
25112520
77A01E922BCA9C0400F0EA38 /* NSWindow */,
2512-
6CB94CFF2C9F1CB600E8651C /* TextView */,
2513-
77EF6C042C57DE4B00984B69 /* URL */,
25142521
58D01C8B293167DC00C5B6B4 /* String */,
25152522
5831E3CB2933E89A00D5A6D2 /* SwiftTerm */,
25162523
6CBD1BC42978DE3E006639D5 /* Text */,
2524+
6CB94CFF2C9F1CB600E8651C /* TextView */,
2525+
77EF6C042C57DE4B00984B69 /* URL */,
25172526
6CD26C752C8EA80000ADBA38 /* URL */,
25182527
5831E3CA2933E86F00D5A6D2 /* View */,
25192528
);
@@ -2532,6 +2541,7 @@
25322541
D7E201AD27E8B3C000CB86D0 /* String+Ranges.swift */,
25332542
58D01C8D293167DC00C5B6B4 /* String+RemoveOccurrences.swift */,
25342543
58D01C8C293167DC00C5B6B4 /* String+SHA256.swift */,
2544+
6CDAFDDE2D35DADD002B2D47 /* String+ValidFileName.swift */,
25352545
);
25362546
path = String;
25372547
sourceTree = "<group>";
@@ -3031,6 +3041,7 @@
30313041
6C96191C2C3F27E3009733CE /* ProjectNavigator */ = {
30323042
isa = PBXGroup;
30333043
children = (
3044+
6CFC0C3B2D381D2000F09CD0 /* ProjectNavigatorFileManagementUITests.swift */,
30343045
6C96191B2C3F27E3009733CE /* ProjectNavigatorUITests.swift */,
30353046
);
30363047
path = ProjectNavigator;
@@ -3056,6 +3067,7 @@
30563067
6C96191F2C3F27E3009733CE /* CodeEditUITests */ = {
30573068
isa = PBXGroup;
30583069
children = (
3070+
6CFC0C3D2D382B3900F09CD0 /* UI TESTING.md */,
30593071
6CFBA54A2C4E168A00E3A914 /* App.swift */,
30603072
6C510CB62D2E462D006EBE85 /* Extensions */,
30613073
6C96191E2C3F27E3009733CE /* Features */,
@@ -3977,6 +3989,7 @@
39773989
isa = PBXResourcesBuildPhase;
39783990
buildActionMask = 2147483647;
39793991
files = (
3992+
6CFC0C3E2D382B3F00F09CD0 /* UI TESTING.md in Resources */,
39803993
);
39813994
runOnlyForDeploymentPostprocessing = 0;
39823995
};
@@ -4073,6 +4086,7 @@
40734086
EC0870F72A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift in Sources */,
40744087
B60718202B0C6CE7009CDAB4 /* GitStashEntry.swift in Sources */,
40754088
6CAAF69429BCD78600A1F48A /* (null) in Sources */,
4089+
6CDAFDDF2D35DADD002B2D47 /* String+ValidFileName.swift in Sources */,
40764090
3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */,
40774091
6C82D6C629C012AD00495C54 /* NSApp+openWindow.swift in Sources */,
40784092
6C14CEB028777D3C001468FE /* FindNavigatorListViewController.swift in Sources */,
@@ -4136,6 +4150,7 @@
41364150
D7012EE827E757850001E1EF /* FindNavigatorView.swift in Sources */,
41374151
58A5DF8029325B5A00D1BD5D /* GitClient.swift in Sources */,
41384152
D7E201AE27E8B3C000CB86D0 /* String+Ranges.swift in Sources */,
4153+
6CDAFDDD2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift in Sources */,
41394154
6CE6226E2A2A1CDE0013085C /* NavigatorTab.swift in Sources */,
41404155
041FC6AD2AE437CE00C1F65A /* SourceControlNewBranchView.swift in Sources */,
41414156
77A01E432BBC3A2800F0EA38 /* CETask.swift in Sources */,
@@ -4632,6 +4647,7 @@
46324647
6CFBA54D2C4E16C900E3A914 /* WindowCloseCommandTests.swift in Sources */,
46334648
6C9619222C3F27F1009733CE /* Query.swift in Sources */,
46344649
6C07383B2D284ECA0025CBE3 /* TasksMenuUITests.swift in Sources */,
4650+
6CFC0C3C2D381D2000F09CD0 /* ProjectNavigatorFileManagementUITests.swift in Sources */,
46354651
6C9619202C3F27E3009733CE /* ProjectNavigatorUITests.swift in Sources */,
46364652
);
46374653
runOnlyForDeploymentPostprocessing = 0;

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,15 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
253253
}
254254

255255
func validateFileName(for newName: String) -> Bool {
256-
guard newName != labelFileName() else { return true }
257-
258-
guard !newName.isEmpty && newName.isValidFilename &&
256+
// Name must be: new, nonempty, valid characters, and not exist in the filesystem.
257+
guard newName != labelFileName() &&
258+
!newName.isEmpty &&
259+
newName.isValidFilename &&
259260
!FileManager.default.fileExists(
260261
atPath: self.url.deletingLastPathComponent().appendingPathComponent(newName).path
261-
)
262-
else { return false }
262+
) else {
263+
return false
264+
}
263265

264266
return true
265267
}

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ extension CEWorkspaceFileManager {
1919
var files: Set<CEWorkspaceFile> = []
2020
for event in events {
2121
// Event returns file/folder that was changed, but in tree we need to update it's parent
22-
let parentUrl = "/" + event.path.split(separator: "/").dropLast().joined(separator: "/")
23-
// Find all folders pointing to the parent's file url.
24-
let fileItems = self.flattenedFileItems.filter({
25-
$0.value.resolvedURL.path == parentUrl
26-
}).map { $0.value }
22+
guard let parentUrl = URL(string: event.path, relativeTo: self.folderUrl)?.deletingLastPathComponent(),
23+
let parentFileItem = self.flattenedFileItems[parentUrl.path] else {
24+
continue
25+
}
2726

2827
switch event.eventType {
2928
case .changeInDirectory, .itemChangedOwner, .itemModified:
@@ -33,15 +32,13 @@ extension CEWorkspaceFileManager {
3332
// TODO: #1880 - Handle workspace root changing.
3433
continue
3534
case .itemCreated, .itemCloned, .itemRemoved, .itemRenamed:
36-
for fileItem in fileItems {
37-
do {
38-
try self.rebuildFiles(fromItem: fileItem)
39-
} catch {
40-
// swiftlint:disable:next line_length
41-
self.logger.error("Failed to rebuild files for event: \(event.eventType.rawValue), path: \(event.path, privacy: .sensitive)")
42-
}
43-
files.insert(fileItem)
35+
do {
36+
try self.rebuildFiles(fromItem: parentFileItem)
37+
} catch {
38+
// swiftlint:disable:next line_length
39+
self.logger.error("Failed to rebuild files for event: \(event.eventType.rawValue), path: \(event.path, privacy: .sensitive)")
4440
}
41+
files.insert(parentFileItem)
4542
}
4643
}
4744
if !files.isEmpty {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// CEWorkspaceFileManager+Error.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 1/13/25.
6+
//
7+
8+
import Foundation
9+
10+
extension CEWorkspaceFileManager {
11+
/// Localized errors related to actions in the file manager.
12+
/// These errors are suitable for presentation using `NSAlert(error:)`.
13+
enum FileManagerError: LocalizedError {
14+
case fileNotFound
15+
case fileNotIndexed
16+
case originFileNotFound
17+
case destinationFileExists
18+
case invalidFileName
19+
20+
var errorDescription: String? {
21+
switch self {
22+
case .fileNotFound:
23+
return "File not found"
24+
case .fileNotIndexed:
25+
return "File not found in CodeEdit"
26+
case .originFileNotFound:
27+
return "Failed to find origin file"
28+
case .destinationFileExists:
29+
return "Destination already exists"
30+
case .invalidFileName:
31+
return "Invalid file name"
32+
}
33+
}
34+
35+
var recoverySuggestion: String? {
36+
switch self {
37+
case .fileNotIndexed:
38+
return "Reopen the workspace to reindex the file system."
39+
case .fileNotFound, .originFileNotFound:
40+
return "The file may have moved during the operation, try again."
41+
case .destinationFileExists:
42+
return "Use a different file name or remove the conflicting file."
43+
case .invalidFileName:
44+
return "File names must not contain the : character and be less than 256 characters."
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)