From aa30305cdce7773c87789305721870464dceded4 Mon Sep 17 00:00:00 2001 From: Abe M Date: Fri, 25 Jul 2025 00:57:47 -0700 Subject: [PATCH 1/5] Added phantom files and file renaming focus --- .../CEWorkspace/Models/CEWorkspaceFile.swift | 3 + .../ProjectNavigatorMenuActions.swift | 37 +++++++--- .../ProjectNavigatorTableViewCell.swift | 71 +++++++++++++++++-- ...ViewController+NSOutlineViewDelegate.swift | 6 +- 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index ce3a4d7c94..f7774d8065 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -164,6 +164,9 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor FileIcon.iconColor(fileType: type) } + /// Indicates whether the file is phantom (not yet created on disk) + var isPhantom: Bool = false + init( id: String, url: URL, diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 1aa65af926..6b55f3c782 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -81,20 +81,35 @@ extension ProjectNavigatorMenu { try? process.run() } - // TODO: allow custom file names /// Action that creates a new untitled file @objc func newFile() { guard let item else { return } - do { - if let newFile = try workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) { - workspace?.listenerModel.highlightedFileItem = newFile - workspace?.editorManager?.openTab(item: newFile) + let phantomFile = CEWorkspaceFile( + id: UUID().uuidString, + url: item.url.appendingPathComponent("Untitled"), + changeType: nil, + staged: false + ) + phantomFile.isPhantom = true + phantomFile.parent = item + + // Add phantom file to parent's children temporarily for display + if let workspace = workspace, + let fileManager = workspace.workspaceFileManager { + _ = fileManager.childrenOfFile(item) + fileManager.flattenedFileItems[phantomFile.id] = phantomFile + if fileManager.childrenMap[item.id] == nil { + fileManager.childrenMap[item.id] = [] } - } catch { - let alert = NSAlert(error: error) - alert.addButton(withTitle: "Dismiss") - alert.runModal() + fileManager.childrenMap[item.id]?.append(phantomFile.id) + } + + workspace?.listenerModel.highlightedFileItem = phantomFile + sender.outlineView.reloadData() + + DispatchQueue.main.async { + self.renameFile() } } @@ -103,11 +118,11 @@ extension ProjectNavigatorMenu { func renameFile() { guard let newFile = workspace?.listenerModel.highlightedFileItem else { return } let row = sender.outlineView.row(forItem: newFile) - guard row > 0, + guard row >= 0, let cell = sender.outlineView.view( atColumn: 0, row: row, - makeIfNecessary: false + makeIfNecessary: true ) as? ProjectNavigatorTableViewCell else { return } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 82db7b1649..a9e34b2758 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -56,15 +56,72 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { override func controlTextDidEndEditing(_ obj: Notification) { guard let fileItem else { return } - textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed - if fileItem.validateFileName(for: textField?.stringValue ?? "") { - let destinationURL = fileItem.url - .deletingLastPathComponent() - .appending(path: textField?.stringValue ?? "") - delegate?.moveFile(file: fileItem, to: destinationURL) + + if fileItem.isPhantom { + handlePhantomFileCompletion(fileItem: fileItem, wasCancelled: false) } else { - textField?.stringValue = fileItem.labelFileName() + textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed + if fileItem.validateFileName(for: textField?.stringValue ?? "") { + let destinationURL = fileItem.url + .deletingLastPathComponent() + .appending(path: textField?.stringValue ?? "") + delegate?.moveFile(file: fileItem, to: destinationURL) + } else { + textField?.stringValue = fileItem.labelFileName() + } } delegate?.cellDidFinishEditing() } + + private func handlePhantomFileCompletion(fileItem: CEWorkspaceFile, wasCancelled: Bool) { + if wasCancelled { + if let workspace = delegate as? ProjectNavigatorViewController, + let workspaceFileManager = workspace.workspace?.workspaceFileManager { + removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) + } + return + } + + let newName = textField?.stringValue ?? "" + if !newName.isEmpty && newName.isValidFilename { + if let workspace = delegate as? ProjectNavigatorViewController, + let workspaceFileManager = workspace.workspace?.workspaceFileManager, + let parent = fileItem.parent { + do { + let newFile = try workspaceFileManager.addFile( + fileName: newName, + toFile: parent + ) + + removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) + workspace.workspace?.listenerModel.highlightedFileItem = newFile + workspace.workspace?.editorManager?.openTab(item: newFile) + + } catch { + let alert = NSAlert(error: error) + alert.addButton(withTitle: "Dismiss") + alert.runModal() + removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) + } + } + } else { + if let workspace = delegate as? ProjectNavigatorViewController, + let workspaceFileManager = workspace.workspace?.workspaceFileManager { + removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) + } + } + } + + private func removePhantomFile(fileItem: CEWorkspaceFile, fileManager: CEWorkspaceFileManager) { + fileManager.flattenedFileItems.removeValue(forKey: fileItem.id) + + if let parent = fileItem.parent, + let childrenIds = fileManager.childrenMap[parent.id] { + fileManager.childrenMap[parent.id] = childrenIds.filter { $0 != fileItem.id } + } + + if let workspace = delegate as? ProjectNavigatorViewController { + workspace.outlineView.reloadData() + } + } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 9256c3e3e1..9d9115e4c1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -44,7 +44,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } - if !item.isFolder && shouldSendSelectionUpdate { + if !item.isFolder && !item.isPhantom && shouldSendSelectionUpdate { shouldSendSelectionUpdate = false if workspace?.editorManager?.activeEditor.selectedTab?.file != item { workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) @@ -131,6 +131,10 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) shouldSendSelectionUpdate = true + if fileItem.isPhantom { + return + } + if row < 0 { let alert = NSAlert() alert.messageText = NSLocalizedString( From 890117f6f0d4b8752441a7c3e373e01e46223eab Mon Sep 17 00:00:00 2001 From: Abe M Date: Fri, 25 Jul 2025 02:44:51 -0700 Subject: [PATCH 2/5] Added folder renaming --- .../CEWorkspace/Models/CEWorkspaceFile.swift | 2 +- ...EWorkspaceFileManager+FileManagement.swift | 43 +---------- .../ProjectNavigatorMenuActions.swift | 74 +++++++++---------- .../ProjectNavigatorTableViewCell.swift | 26 ++++--- 4 files changed, 55 insertions(+), 90 deletions(-) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index f7774d8065..fa85a09ae5 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -116,7 +116,7 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor /// Returns a boolean that is true if the resource represented by this object is a directory. lazy var isFolder: Bool = { - resolvedURL.isFolder + isPhantom ? resolvedURL.hasDirectoryPath : resolvedURL.isFolder }() /// Returns a boolean that is true if the contents of the directory at this path are diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift index 82989fbffc..94f5785f31 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift @@ -65,28 +65,14 @@ extension CEWorkspaceFileManager { useExtension: String? = nil, contents: Data? = nil ) throws -> CEWorkspaceFile { - // check the folder for other files, and see what the most common file extension is do { - var fileExtension: String - if fileName.contains(".") { - // If we already have a file extension in the name, don't add another one - fileExtension = "" - } else { - fileExtension = useExtension ?? findCommonFileExtension(for: file) - - // Don't add a . if the extension is empty, but add it if it's missing. - if !fileExtension.isEmpty && !fileExtension.starts(with: ".") { - fileExtension = "." + fileExtension - } - } - - var fileUrl = file.nearestFolder.appending(path: "\(fileName)\(fileExtension)") + var fileUrl = file.nearestFolder.appending(path: "\(fileName)") // If a file/folder with the same name exists, add a number to the end. var fileNumber = 0 while fileManager.fileExists(atPath: fileUrl.path) { fileNumber += 1 fileUrl = fileUrl.deletingLastPathComponent() - .appending(path: "\(fileName)\(fileNumber)\(fileExtension)") + .appending(path: "\(fileName)\(fileNumber)") } guard fileUrl.fileName.isValidFilename else { @@ -117,31 +103,6 @@ extension CEWorkspaceFileManager { } } - /// Finds a common file extension in the same directory as a file. Defaults to `txt` if no better alternatives - /// are found. - /// - Parameter file: The file to use to determine a common extension. - /// - Returns: The suggested file extension. - private func findCommonFileExtension(for file: CEWorkspaceFile) -> String { - var fileExtensions: [String: Int] = ["": 0] - - for child in ( - file.isFolder ? file.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self) - : file.parent?.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self) - ) ?? [] - where !child.isFolder { - // if the file extension was present before, add it now - let childFileName = child.fileName(typeHidden: false) - if let index = childFileName.lastIndex(of: ".") { - let childFileExtension = ".\(childFileName.suffix(from: index).dropFirst())" - fileExtensions[childFileExtension] = (fileExtensions[childFileExtension] ?? 0) + 1 - } else { - fileExtensions[""] = (fileExtensions[""] ?? 0) + 1 - } - } - - return fileExtensions.max(by: { $0.value < $1.value })?.key ?? "txt" - } - /// This function deletes the item or folder from the current project by moving to Trash /// - Parameters: /// - file: The file or folder to delete diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 6b55f3c782..3c9c3644b1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -84,33 +84,7 @@ extension ProjectNavigatorMenu { /// Action that creates a new untitled file @objc func newFile() { - guard let item else { return } - let phantomFile = CEWorkspaceFile( - id: UUID().uuidString, - url: item.url.appendingPathComponent("Untitled"), - changeType: nil, - staged: false - ) - phantomFile.isPhantom = true - phantomFile.parent = item - - // Add phantom file to parent's children temporarily for display - if let workspace = workspace, - let fileManager = workspace.workspaceFileManager { - _ = fileManager.childrenOfFile(item) - fileManager.flattenedFileItems[phantomFile.id] = phantomFile - if fileManager.childrenMap[item.id] == nil { - fileManager.childrenMap[item.id] = [] - } - fileManager.childrenMap[item.id]?.append(phantomFile.id) - } - - workspace?.listenerModel.highlightedFileItem = phantomFile - sender.outlineView.reloadData() - - DispatchQueue.main.async { - self.renameFile() - } + createAndAddPhantomFile(isFolder: false) } /// Opens the rename file dialogue on the cell this was presented from. @@ -154,20 +128,10 @@ extension ProjectNavigatorMenu { } } - // TODO: allow custom folder names /// Action that creates a new untitled folder @objc func newFolder() { - guard let item else { return } - do { - if let newFolder = try workspace?.workspaceFileManager?.addFolder(folderName: "untitled", toFile: item) { - workspace?.listenerModel.highlightedFileItem = newFolder - } - } catch { - let alert = NSAlert(error: error) - alert.addButton(withTitle: "Dismiss") - alert.runModal() - } + createAndAddPhantomFile(isFolder: true) } /// Creates a new folder with the items selected. @@ -299,6 +263,40 @@ extension ProjectNavigatorMenu { NSPasteboard.general.setString(paths, forType: .string) } + private func createAndAddPhantomFile(isFolder: Bool) { + guard let item else { return } + let phantomFile = CEWorkspaceFile( + id: UUID().uuidString, + url: item.url + .appending( + path: isFolder ? "New Folder" : "Untitled", + directoryHint: isFolder ? .isDirectory : .notDirectory + ), + changeType: nil, + staged: false + ) + phantomFile.isPhantom = true + phantomFile.parent = item + + // Add phantom file to parent's children temporarily for display + if let workspace = workspace, + let fileManager = workspace.workspaceFileManager { + _ = fileManager.childrenOfFile(item) + fileManager.flattenedFileItems[phantomFile.id] = phantomFile + if fileManager.childrenMap[item.id] == nil { + fileManager.childrenMap[item.id] = [] + } + fileManager.childrenMap[item.id]?.append(phantomFile.id) + } + + workspace?.listenerModel.highlightedFileItem = phantomFile + sender.outlineView.reloadData() + + DispatchQueue.main.async { + self.renameFile() + } + } + private func reloadData() { sender.outlineView.reloadData() sender.filteredContentChildren.removeAll() diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index a9e34b2758..a26e6cdb26 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -88,21 +88,27 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { let workspaceFileManager = workspace.workspace?.workspaceFileManager, let parent = fileItem.parent { do { - let newFile = try workspaceFileManager.addFile( - fileName: newName, - toFile: parent - ) - - removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) - workspace.workspace?.listenerModel.highlightedFileItem = newFile - workspace.workspace?.editorManager?.openTab(item: newFile) - + if fileItem.isFolder { + let newFolder = try workspaceFileManager.addFolder( + folderName: newName, + toFile: parent + ) + workspace.workspace?.listenerModel.highlightedFileItem = newFolder + } else { + let newFile = try workspaceFileManager.addFile( + fileName: newName, + toFile: parent + ) + workspace.workspace?.listenerModel.highlightedFileItem = newFile + workspace.workspace?.editorManager?.openTab(item: newFile) + } } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") alert.runModal() - removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) } + + removePhantomFile(fileItem: fileItem, fileManager: workspaceFileManager) } } else { if let workspace = delegate as? ProjectNavigatorViewController, From a09d8d120474bc200df32ff423254b33c61554d4 Mon Sep 17 00:00:00 2001 From: Abe M Date: Fri, 25 Jul 2025 11:47:45 -0700 Subject: [PATCH 3/5] Added cancel operation check, fix bug --- .../ProjectNavigatorMenuActions.swift | 7 ++---- .../ProjectNavigatorTableViewCell.swift | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 3c9c3644b1..e66b7698c7 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -119,7 +119,7 @@ extension ProjectNavigatorMenu { ) { workspace?.listenerModel.highlightedFileItem = newFile workspace?.editorManager?.openTab(item: newFile) - renameFile() + self.renameFile() } } catch { let alert = NSAlert(error: error) @@ -291,10 +291,7 @@ extension ProjectNavigatorMenu { workspace?.listenerModel.highlightedFileItem = phantomFile sender.outlineView.reloadData() - - DispatchQueue.main.async { - self.renameFile() - } + self.renameFile() } private func reloadData() { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index a26e6cdb26..e1883b9888 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -58,7 +58,10 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { guard let fileItem else { return } if fileItem.isPhantom { - handlePhantomFileCompletion(fileItem: fileItem, wasCancelled: false) + DispatchQueue.main.async { [weak fileItem, weak self] in + guard let fileItem, let self = self else { return } + self.handlePhantomFileCompletion(fileItem: fileItem, wasCancelled: false) + } } else { textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed if fileItem.validateFileName(for: textField?.stringValue ?? "") { @@ -130,4 +133,22 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { workspace.outlineView.reloadData() } } + + /// Capture a cancel operation (escape key) to remove a phantom file that we are currently renaming + func control( + _ control: NSControl, + textView: NSTextView, + doCommandBy commandSelector: Selector + ) -> Bool { + guard let fileItem, fileItem.isPhantom else { return false } + + if commandSelector == #selector(NSResponder.cancelOperation(_:)) { + DispatchQueue.main.async { [weak fileItem, weak self] in + guard let fileItem, let self = self else { return } + self.handlePhantomFileCompletion(fileItem: fileItem, wasCancelled: true) + } + } + + return false + } } From 2493c660eee5b59572be9bf90ebe90f77db88ceb Mon Sep 17 00:00:00 2001 From: Abe M Date: Sat, 26 Jul 2025 03:56:40 -0700 Subject: [PATCH 4/5] Fixed renaming behavior for pasted file --- .../CEWorkspace/Models/CEWorkspaceFile.swift | 6 +-- .../CEWorkspace/Models/PhantomFile.swift | 12 ++++++ .../ProjectNavigatorMenuActions.swift | 39 +++++++------------ .../ProjectNavigatorTableViewCell.swift | 9 +++-- ...ViewController+NSOutlineViewDelegate.swift | 4 +- 5 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/Models/PhantomFile.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index fa85a09ae5..fa20bf37ee 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -116,7 +116,7 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor /// Returns a boolean that is true if the resource represented by this object is a directory. lazy var isFolder: Bool = { - isPhantom ? resolvedURL.hasDirectoryPath : resolvedURL.isFolder + phantomFile != nil ? resolvedURL.hasDirectoryPath : resolvedURL.isFolder }() /// Returns a boolean that is true if the contents of the directory at this path are @@ -164,8 +164,8 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor FileIcon.iconColor(fileType: type) } - /// Indicates whether the file is phantom (not yet created on disk) - var isPhantom: Bool = false + /// Holds information about the phantom file + var phantomFile: PhantomFile? init( id: String, diff --git a/CodeEdit/Features/CEWorkspace/Models/PhantomFile.swift b/CodeEdit/Features/CEWorkspace/Models/PhantomFile.swift new file mode 100644 index 0000000000..d6112ea488 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/PhantomFile.swift @@ -0,0 +1,12 @@ +// +// PhantomFile.swift +// CodeEdit +// +// Created by Abe Malla on 7/25/25. +// + +/// Represents a file that doesn't exist on disk +enum PhantomFile { + case empty + case pasteboardContent +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index e66b7698c7..69a4b58a6e 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -107,25 +107,14 @@ extension ProjectNavigatorMenu { /// Action that creates a new file with clipboard content @objc func newFileFromClipboard() { - guard let item else { return } - do { - let clipBoardContent = NSPasteboard.general.string(forType: .string)?.data(using: .utf8) - if let clipBoardContent, !clipBoardContent.isEmpty, let newFile = try workspace? - .workspaceFileManager? - .addFile( - fileName: "untitled", - toFile: item, - contents: clipBoardContent - ) { - workspace?.listenerModel.highlightedFileItem = newFile - workspace?.editorManager?.openTab(item: newFile) - self.renameFile() - } - } catch { - let alert = NSAlert(error: error) - alert.addButton(withTitle: "Dismiss") - alert.runModal() + guard item != nil else { return } + let clipBoardContent = NSPasteboard.general.string(forType: .string)?.data(using: .utf8) + + guard let clipBoardContent, !clipBoardContent.isEmpty else { + return } + + createAndAddPhantomFile(isFolder: false, usePasteboardContent: true) } /// Action that creates a new untitled folder @@ -263,9 +252,9 @@ extension ProjectNavigatorMenu { NSPasteboard.general.setString(paths, forType: .string) } - private func createAndAddPhantomFile(isFolder: Bool) { + private func createAndAddPhantomFile(isFolder: Bool, usePasteboardContent: Bool = false) { guard let item else { return } - let phantomFile = CEWorkspaceFile( + let file = CEWorkspaceFile( id: UUID().uuidString, url: item.url .appending( @@ -275,21 +264,21 @@ extension ProjectNavigatorMenu { changeType: nil, staged: false ) - phantomFile.isPhantom = true - phantomFile.parent = item + file.phantomFile = usePasteboardContent ? .pasteboardContent : .empty + file.parent = item // Add phantom file to parent's children temporarily for display if let workspace = workspace, let fileManager = workspace.workspaceFileManager { _ = fileManager.childrenOfFile(item) - fileManager.flattenedFileItems[phantomFile.id] = phantomFile + fileManager.flattenedFileItems[file.id] = file if fileManager.childrenMap[item.id] == nil { fileManager.childrenMap[item.id] = [] } - fileManager.childrenMap[item.id]?.append(phantomFile.id) + fileManager.childrenMap[item.id]?.append(file.id) } - workspace?.listenerModel.highlightedFileItem = phantomFile + workspace?.listenerModel.highlightedFileItem = file sender.outlineView.reloadData() self.renameFile() } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index e1883b9888..91a7d7212d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -57,7 +57,7 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { override func controlTextDidEndEditing(_ obj: Notification) { guard let fileItem else { return } - if fileItem.isPhantom { + if fileItem.phantomFile != nil { DispatchQueue.main.async { [weak fileItem, weak self] in guard let fileItem, let self = self else { return } self.handlePhantomFileCompletion(fileItem: fileItem, wasCancelled: false) @@ -100,7 +100,10 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { } else { let newFile = try workspaceFileManager.addFile( fileName: newName, - toFile: parent + toFile: parent, + contents: fileItem.phantomFile == PhantomFile.pasteboardContent + ? NSPasteboard.general.string(forType: .string)?.data(using: .utf8) + : nil ) workspace.workspace?.listenerModel.highlightedFileItem = newFile workspace.workspace?.editorManager?.openTab(item: newFile) @@ -140,7 +143,7 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { textView: NSTextView, doCommandBy commandSelector: Selector ) -> Bool { - guard let fileItem, fileItem.isPhantom else { return false } + guard let fileItem, fileItem.phantomFile != nil else { return false } if commandSelector == #selector(NSResponder.cancelOperation(_:)) { DispatchQueue.main.async { [weak fileItem, weak self] in diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 9d9115e4c1..9ee8d38fc2 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -44,7 +44,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { guard let item = outlineView.item(atRow: selectedIndex) as? CEWorkspaceFile else { return } - if !item.isFolder && !item.isPhantom && shouldSendSelectionUpdate { + if !item.isFolder && item.phantomFile == nil && shouldSendSelectionUpdate { shouldSendSelectionUpdate = false if workspace?.editorManager?.activeEditor.selectedTab?.file != item { workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) @@ -131,7 +131,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) shouldSendSelectionUpdate = true - if fileItem.isPhantom { + if fileItem.phantomFile != nil { return } From 7cf8bc6e6c66664e7dc69e6b2672827685c3aff6 Mon Sep 17 00:00:00 2001 From: Abe M Date: Sat, 26 Jul 2025 04:04:39 -0700 Subject: [PATCH 5/5] Fix tests --- .../CEWorkspaceFileManager+FileManagement.swift | 17 +++++++++++++++-- .../CEWorkspaceFileManagerTests.swift | 10 ---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift index 94f5785f31..30d7d0c8d1 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift @@ -66,13 +66,26 @@ extension CEWorkspaceFileManager { contents: Data? = nil ) throws -> CEWorkspaceFile { do { - var fileUrl = file.nearestFolder.appending(path: "\(fileName)") + var fileExtension: String + if fileName.contains(".") { + // If we already have a file extension in the name, don't add another one + fileExtension = "" + } else { + fileExtension = useExtension ?? "" + + // Don't add a . if the extension is empty, but add it if it's missing. + if !fileExtension.isEmpty && !fileExtension.starts(with: ".") { + fileExtension = "." + fileExtension + } + } + + var fileUrl = file.nearestFolder.appending(path: "\(fileName)\(fileExtension)") // If a file/folder with the same name exists, add a number to the end. var fileNumber = 0 while fileManager.fileExists(atPath: fileUrl.path) { fileNumber += 1 fileUrl = fileUrl.deletingLastPathComponent() - .appending(path: "\(fileName)\(fileNumber)") + .appending(path: "\(fileName)\(fileNumber)\(fileExtension)") } guard fileUrl.fileName.isValidFilename else { diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 2fb01159fd..5d2bd0aa74 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -174,16 +174,6 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { // See #1966 XCTAssertEqual(file.name, "Test File.txt") - // Test the automatic file extension stuff - file = try fileManager.addFile( - fileName: "Test File Extension", - toFile: fileManager.workspaceItem, - useExtension: nil - ) - - // Should detect '.txt' with the previous file in the same directory. - XCTAssertEqual(file.name, "Test File Extension.txt") - // Test explicit file extension with both . and no period at the beginning of the given extension. file = try fileManager.addFile( fileName: "Explicit File Extension",