From 4dbda3b828d0c312c30df5b3866b352ed9082a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 00:54:19 -0700 Subject: [PATCH 1/9] Initial search, saving expanded items, added "No Filter Results" label. --- .../WorkspaceDocument/WorkspaceDocument.swift | 1 + .../ProjectNavigatorOutlineView.swift | 1 + ...ewController+NSOutlineViewDataSource.swift | 22 ++++- ...ViewController+NSOutlineViewDelegate.swift | 18 +++- .../ProjectNavigatorViewController.swift | 98 ++++++++++++++++++- .../ProjectNavigatorToolbarBottom.swift | 15 +-- 6 files changed, 137 insertions(+), 18 deletions(-) diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift index 6af153faee..cded2770ef 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift @@ -14,6 +14,7 @@ import LanguageServerProtocol @objc(WorkspaceDocument) final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { @Published var sortFoldersOnTop: Bool = true + @Published var filter: String = "" private var workspaceState: [String: Any] { get { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 49a23985ff..3d63adcabf 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -36,6 +36,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { nsViewController.hiddenFileExtensions = prefs.preferences.general.hiddenFileExtensions /// if the window becomes active from background, it will restore the selection to outline view. nsViewController.updateSelection(itemID: workspace.editorManager?.activeEditor.selectedTab?.file.id) + nsViewController.filter = workspace.filter return } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 2b7fc81d77..8349347604 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -8,17 +8,31 @@ import AppKit extension ProjectNavigatorViewController: NSOutlineViewDataSource { + /// Retrieves the children of a given item for the outline view, applying the current filter if necessary. + private func getOutlineViewItems(for item: CEWorkspaceFile) -> [CEWorkspaceFile] { + if let cachedChildren = filteredContentChildren[item] { + return cachedChildren + } + + if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + let filteredChildren = children.filter { fileSearchMatches(filter, for: $0) } + filteredContentChildren[item] = filteredChildren + return filteredChildren + } + + return [] + } + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { if let item = item as? CEWorkspaceFile { - return item.isFolder ? workspace?.workspaceFileManager?.childrenOfFile(item)?.count ?? 0 : 0 + return getOutlineViewItems(for: item).count } return content.count } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - if let item = item as? CEWorkspaceFile, - let children = workspace?.workspaceFileManager?.childrenOfFile(item) { - return children[index] + if let item = item as? CEWorkspaceFile { + return getOutlineViewItems(for: item)[index] } return content[index] } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 53d9298a6c..9455a6e8de 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -49,8 +49,14 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } func outlineViewItemDidExpand(_ notification: Notification) { - guard let id = workspace?.editorManager?.activeEditor.selectedTab?.file.id, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true), + /// Save expanded items' state to restore when finish filtering. + guard let workspace else { return } + if workspace.filter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + expandedItems.insert(item) + } + + guard let id = workspace.editorManager?.activeEditor.selectedTab?.file.id, + let item = workspace.workspaceFileManager?.getFile(id, createIfNotFound: true), /// update outline selection only if the parent of selected item match with expanded item item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return @@ -61,7 +67,13 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } } - func outlineViewItemDidCollapse(_ notification: Notification) {} + func outlineViewItemDidCollapse(_ notification: Notification) { + /// Save expanded items' state to restore when finish filtering. + guard let workspace else { return } + if workspace.filter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + expandedItems.remove(item) + } + } func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { guard let id = object as? CEWorkspaceFile.ID, diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 4ffd7321eb..4fd2833ed2 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -21,6 +21,7 @@ final class ProjectNavigatorViewController: NSViewController { var scrollView: NSScrollView! var outlineView: NSOutlineView! + var noResultsLabel: NSTextField! /// Gets the folder structure /// @@ -31,6 +32,14 @@ final class ProjectNavigatorViewController: NSViewController { return [root] } + var filteredContentChildren: [CEWorkspaceFile: [CEWorkspaceFile]] = [:] + var expandedItems: Set = [] + var filter: String = "" { + didSet { + handleFilterChange() + } + } + weak var workspace: WorkspaceDocument? var iconColor: SettingsData.FileIconStyle = .color { @@ -94,6 +103,27 @@ final class ProjectNavigatorViewController: NSViewController { scrollView.autohidesScrollers = true outlineView.expandItem(outlineView.item(atRow: 0)) + + /// Get autosave expanded items. + for row in 0.. Bool { + guard !filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true } + + if item.name.localizedLowercase.contains(filter.localizedLowercase) { + saveAllContentChildren(for: item) + return true + } + + if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + return children.contains { fileSearchMatches(filter, for: $0) } + } + + return false + } + + /// Saves all children of a given folder item to the filtered content cache, this is specially useful when the name of a folder matches the search. + /// Just like in Xcode, this shows all the content of the folder. + private func saveAllContentChildren(for item: CEWorkspaceFile) { + guard item.isFolder, filteredContentChildren[item] == nil else { return } + + if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + filteredContentChildren[item] = children + for child in children.filter({ $0.isFolder }) { + saveAllContentChildren(for: child) + } + } + } + + /// Restores the expanded state of items in the outline view based on previously expanded items when finish searching. + private func restoreExpandedState() { + let copy = expandedItems + outlineView.collapseItem(outlineView.item(atRow: 0), collapseChildren: true) + + for item in copy { + expandParentsRecursively(of: item) + outlineView.expandItem(item) + } + + expandedItems = copy + } + + /// Recursively expands all parent items of a given item in the outline view. + /// The order of the items may get lost in the `expandedItems` set. + /// This means that a children item might be expanded before its parent, causing it not to really expand. + private func expandParentsRecursively(of item: CEWorkspaceFile) { + if let parent = item.parent { + expandParentsRecursively(of: parent) + outlineView.expandItem(parent) + } + } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index d9d7bb2fd4..f1a5a4d482 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -17,7 +17,6 @@ struct ProjectNavigatorToolbarBottom: View { @EnvironmentObject var workspace: WorkspaceDocument @EnvironmentObject var editorManager: EditorManager - @State var filter: String = "" @State var recentsFilter: Bool = false @State var sourceControlFilter: Bool = false @@ -26,7 +25,7 @@ struct ProjectNavigatorToolbarBottom: View { addNewFileButton PaneTextField( "Filter", - text: $filter, + text: $workspace.filter, leadingAccessories: { FilterDropDownIconButton(menu: { Button { @@ -34,10 +33,10 @@ struct ProjectNavigatorToolbarBottom: View { } label: { Text(workspace.sortFoldersOnTop ? "Alphabetically" : "Folders on top") } - }, isOn: !filter.isEmpty) + }, isOn: !workspace.filter.isEmpty) .padding(.leading, 4) .foregroundStyle( - filter.isEmpty + workspace.filter.isEmpty ? Color(nsColor: .secondaryLabelColor) : Color(nsColor: .controlAccentColor) ) @@ -58,12 +57,8 @@ struct ProjectNavigatorToolbarBottom: View { .padding(.trailing, 2.5) }, clearable: true, - hasValue: !filter.isEmpty || recentsFilter || sourceControlFilter + hasValue: !workspace.filter.isEmpty || recentsFilter || sourceControlFilter ) - // .onChange(of: filter, perform: { - // TODO: Filter Workspace Files - // workspace.filter = $0 - // }) } .padding(.horizontal, 5) .frame(height: 28, alignment: .center) @@ -129,7 +124,7 @@ struct ProjectNavigatorToolbarBottom: View { /// when the user clears the filter. private var clearFilterButton: some View { Button { - filter = "" + workspace.filter = "" NSApp.keyWindow?.makeFirstResponder(nil) } label: { Image(systemName: "xmark.circle.fill") From 5748644a73c089829b9ea549f095ab16e78e1347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 01:23:26 -0700 Subject: [PATCH 2/9] Fixed SwiftLint issues. --- .../OutlineView/ProjectNavigatorViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 4fd2833ed2..8a37385fc1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -217,7 +217,8 @@ final class ProjectNavigatorViewController: NSViewController { return false } - /// Saves all children of a given folder item to the filtered content cache, this is specially useful when the name of a folder matches the search. + /// Saves all children of a given folder item to the filtered content cache. + /// This is specially useful when the name of a folder matches the search. /// Just like in Xcode, this shows all the content of the folder. private func saveAllContentChildren(for item: CEWorkspaceFile) { guard item.isFolder, filteredContentChildren[item] == nil else { return } @@ -230,7 +231,7 @@ final class ProjectNavigatorViewController: NSViewController { } } - /// Restores the expanded state of items in the outline view based on previously expanded items when finish searching. + /// Restores the expanded state of items when finish searching. private func restoreExpandedState() { let copy = expandedItems outlineView.collapseItem(outlineView.item(atRow: 0), collapseChildren: true) From 01c9b32236d122c7db9f2346d3a8333c9a4c40ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 01:30:56 -0700 Subject: [PATCH 3/9] Added bold text when searching --- .../FindNavigatorListViewController.swift | 3 ++- .../OutlineView/FileSystemTableViewCell.swift | 23 ++++++++++++++++--- .../OutlineView/StandardTableViewCell.swift | 5 ++-- .../ProjectNavigatorTableViewCell.swift | 5 ++-- ...ViewController+NSOutlineViewDelegate.swift | 9 ++++++-- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index f469de2ee7..a6acd28a3d 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -177,7 +177,8 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let view = ProjectNavigatorTableViewCell( frame: frameRect, item: (item as? SearchResultModel)?.file, - isEditable: false + isEditable: false, + workspace: workspace ) // We're using a medium label for file names b/c it makes it easier to // distinguish quickly which results are from which files. diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 6f17630896..90ab4fa5f5 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -22,8 +22,8 @@ class FileSystemTableViewCell: StandardTableViewCell { /// - frameRect: The frame of the cell. /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. - init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true) { - super.init(frame: frameRect, isEditable: isEditable) + init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, workspace: WorkspaceDocument?) { + super.init(frame: frameRect, isEditable: isEditable, workspace: workspace) if let item = item { addIcon(item: item) @@ -40,7 +40,24 @@ class FileSystemTableViewCell: StandardTableViewCell { fileItem = item imageView?.image = item.nsIcon imageView?.contentTintColor = color(for: item) - textField?.stringValue = item.labelFileName() + + let fileName = item.labelFileName() + textField?.stringValue = fileName + + // Apply bold style if the filename matches the workspace filter + if let filter = workspace?.filter, fileName.localizedLowercase.contains(filter.localizedLowercase) { + let attributedString = NSMutableAttributedString(string: fileName) + let range = NSString(string: fileName).range(of: filter, options: .caseInsensitive) + attributedString.addAttribute( + .font, + value: NSFont.boldSystemFont(ofSize: textField?.font?.pointSize ?? 12), + range: range + ) + textField?.attributedStringValue = attributedString + } else { + // Reset to normal font if no match + textField?.attributedStringValue = NSAttributedString(string: fileName) + } } func addModel() { diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift index bad932d153..2067e17c5b 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift @@ -10,7 +10,7 @@ import SwiftUI class StandardTableViewCell: NSTableCellView { weak var secondaryLabel: NSTextField? - weak var workspace: WorkspaceDocument? + var workspace: WorkspaceDocument? var secondaryLabelRightAligned: Bool = true { didSet { @@ -24,9 +24,10 @@ class StandardTableViewCell: NSTableCellView { /// - frameRect: The frame of the cell. /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. - init(frame frameRect: NSRect, isEditable: Bool = true) { + init(frame frameRect: NSRect, isEditable: Bool = true, workspace: WorkspaceDocument?) { super.init(frame: frameRect) setupViews(frame: frameRect, isEditable: isEditable) + self.workspace = workspace } // Default init, assumes isEditable to be false diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index bd64211705..6994d94f24 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -26,9 +26,10 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, - delegate: OutlineTableViewCellDelegate? = nil + delegate: OutlineTableViewCellDelegate? = nil, + workspace: WorkspaceDocument? ) { - super.init(frame: frameRect, item: item, isEditable: isEditable) + super.init(frame: frameRect, item: item, isEditable: isEditable, workspace: workspace) self.textField?.setAccessibilityIdentifier("ProjectNavigatorTableViewCell-\(item?.name ?? "")") self.delegate = delegate } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 9455a6e8de..17de629236 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -24,8 +24,13 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { guard let tableColumn else { return nil } let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight) - - return ProjectNavigatorTableViewCell(frame: frameRect, item: item as? CEWorkspaceFile, delegate: self) + let cell = ProjectNavigatorTableViewCell( + frame: frameRect, + item: item as? CEWorkspaceFile, + delegate: self, + workspace: workspace + ) + return cell } func outlineViewSelectionDidChange(_ notification: Notification) { From 0ce7f130943b91d21e5b5449b5efaf985f990233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 09:55:32 -0700 Subject: [PATCH 4/9] Fixed a possible crash. --- .../OutlineView/ProjectNavigatorViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 8a37385fc1..d6c2a24f2d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -198,7 +198,9 @@ final class ProjectNavigatorViewController: NSViewController { outlineView.expandItem(outlineView.item(atRow: 0), expandChildren: true) } - noResultsLabel.isHidden = !(filteredContentChildren[content[0]]?.isEmpty ?? false) + if let root = content.first(where: { $0.isRoot }), let children = filteredContentChildren[root] { + noResultsLabel.isHidden = !children.isEmpty + } } /// Checks if the given filter matches the name of the item or any of its children. From 8c036507b3d9ee2e5b988c138b1c0be14e6eaa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 10:43:30 -0700 Subject: [PATCH 5/9] Added primary and secondary label colors for search. --- .../OutlineView/FileSystemTableViewCell.swift | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 90ab4fa5f5..478e3121c0 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -42,21 +42,49 @@ class FileSystemTableViewCell: StandardTableViewCell { imageView?.contentTintColor = color(for: item) let fileName = item.labelFileName() - textField?.stringValue = fileName // Apply bold style if the filename matches the workspace filter - if let filter = workspace?.filter, fileName.localizedLowercase.contains(filter.localizedLowercase) { + if let filter = workspace?.filter, !filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let attributedString = NSMutableAttributedString(string: fileName) + + // Check if the filename contains the filter text let range = NSString(string: fileName).range(of: filter, options: .caseInsensitive) - attributedString.addAttribute( - .font, - value: NSFont.boldSystemFont(ofSize: textField?.font?.pointSize ?? 12), - range: range - ) + if range.location != NSNotFound { + // Set the label color to secondary + attributedString.addAttribute( + .foregroundColor, + value: NSColor.secondaryLabelColor, + range: NSRange(location: 0, length: attributedString.length) + ) + + // If the filter text matches, bold the matching text and set primary label color + attributedString.addAttributes( + [ + .font: NSFont.boldSystemFont(ofSize: textField?.font?.pointSize ?? 12), + .foregroundColor: NSColor.textColor + ], + range: range + ) + } else { + // If no match, apply primary label color for parent folder, + // or secondary label color for a non-matching file + attributedString.addAttribute( + .foregroundColor, + value: item.isFolder ? NSColor.labelColor : NSColor.secondaryLabelColor, + range: NSRange(location: 0, length: attributedString.length) + ) + } + textField?.attributedStringValue = attributedString } else { - // Reset to normal font if no match - textField?.attributedStringValue = NSAttributedString(string: fileName) + // If no filter is applied, reset to normal font and primary label color + textField?.attributedStringValue = NSAttributedString( + string: fileName, + attributes: [ + .font: NSFont.systemFont(ofSize: textField?.font?.pointSize ?? 12), + .foregroundColor: NSColor.labelColor + ] + ) } } From b9371a6ef12d4181a7e1776bbe946c0877f520c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Thu, 3 Oct 2024 10:47:35 -0700 Subject: [PATCH 6/9] When there's no filter results, the root folder is hidden. --- .../OutlineView/ProjectNavigatorViewController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index d6c2a24f2d..10e6969839 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -199,7 +199,12 @@ final class ProjectNavigatorViewController: NSViewController { } if let root = content.first(where: { $0.isRoot }), let children = filteredContentChildren[root] { - noResultsLabel.isHidden = !children.isEmpty + if children.isEmpty { + noResultsLabel.isHidden = false + outlineView.hideRows(at: IndexSet(integer: 0)) + } else { + noResultsLabel.isHidden = true + } } } From 4bce33aa9bd8f5b3b35e1ba21ce9e8eb0617047b Mon Sep 17 00:00:00 2001 From: Leonardo <83844690+LeonardoLarranaga@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:34:03 -0700 Subject: [PATCH 7/9] Update CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift Co-authored-by: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> --- .../NavigatorArea/OutlineView/FileSystemTableViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 478e3121c0..82eac1aeb1 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -61,7 +61,7 @@ class FileSystemTableViewCell: StandardTableViewCell { attributedString.addAttributes( [ .font: NSFont.boldSystemFont(ofSize: textField?.font?.pointSize ?? 12), - .foregroundColor: NSColor.textColor +.foregroundColor: NSColor.labelColor ], range: range ) From abab0b1b943316dd9906aa9d778dc69009fe0f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Sun, 6 Oct 2024 14:52:31 -0700 Subject: [PATCH 8/9] Resolved some conversations - Changed `workspace.filter` to `workspace.navigatorFilter` and added its respective comment. - Returned `StandardTableViewCell` to a weak reference of the workspace object. - Replaced the navigators own filter string with the workspace one and added throttle to the filter change. - When closing the window, if there is a search going on, all the items don't get saved expanded anymore. --- .../WorkspaceDocument/WorkspaceDocument.swift | 3 ++- .../OutlineView/FileSystemTableViewCell.swift | 2 +- .../OutlineView/StandardTableViewCell.swift | 2 +- .../OutlineView/ProjectNavigatorOutlineView.swift | 5 ++++- ...atorViewController+NSOutlineViewDataSource.swift | 2 +- ...igatorViewController+NSOutlineViewDelegate.swift | 4 ++-- .../ProjectNavigatorViewController.swift | 13 ++++++------- .../ProjectNavigatorToolbarBottom.swift | 10 +++++----- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift index cded2770ef..aaac6e235a 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift @@ -14,7 +14,8 @@ import LanguageServerProtocol @objc(WorkspaceDocument) final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { @Published var sortFoldersOnTop: Bool = true - @Published var filter: String = "" + /// A string used to filter the displayed files and folders in the project navigator area based on user input. + @Published var navigatorFilter: String = "" private var workspaceState: [String: Any] { get { diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 82eac1aeb1..9a382c7564 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -44,7 +44,7 @@ class FileSystemTableViewCell: StandardTableViewCell { let fileName = item.labelFileName() // Apply bold style if the filename matches the workspace filter - if let filter = workspace?.filter, !filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let filter = workspace?.navigatorFilter, !filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let attributedString = NSMutableAttributedString(string: fileName) // Check if the filename contains the filter text diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift index 2067e17c5b..f4f186d31d 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift @@ -10,7 +10,7 @@ import SwiftUI class StandardTableViewCell: NSTableCellView { weak var secondaryLabel: NSTextField? - var workspace: WorkspaceDocument? + weak var workspace: WorkspaceDocument? var secondaryLabelRightAligned: Bool = true { didSet { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 3d63adcabf..cb94566631 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -36,7 +36,6 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { nsViewController.hiddenFileExtensions = prefs.preferences.general.hiddenFileExtensions /// if the window becomes active from background, it will restore the selection to outline view. nsViewController.updateSelection(itemID: workspace.editorManager?.activeEditor.selectedTab?.file.id) - nsViewController.filter = workspace.filter return } @@ -62,6 +61,10 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self?.controller?.updateSelection(itemID: itemID) } .store(in: &cancellables) + workspace.$navigatorFilter + .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) + .sink { [weak self] _ in self?.controller?.handleFilterChange() } + .store(in: &cancellables) } var cancellables: Set = [] diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 8349347604..cdf4cd80e7 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -15,7 +15,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { } if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { - let filteredChildren = children.filter { fileSearchMatches(filter, for: $0) } + let filteredChildren = children.filter { fileSearchMatches(workspace?.navigatorFilter ?? "", for: $0) } filteredContentChildren[item] = filteredChildren return filteredChildren } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 17de629236..d3b5d8bf89 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -56,7 +56,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidExpand(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.filter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + if workspace.navigatorFilter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.insert(item) } @@ -75,7 +75,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidCollapse(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.filter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + if workspace.navigatorFilter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.remove(item) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 10e6969839..973bbdde40 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -34,11 +34,6 @@ final class ProjectNavigatorViewController: NSViewController { var filteredContentChildren: [CEWorkspaceFile: [CEWorkspaceFile]] = [:] var expandedItems: Set = [] - var filter: String = "" { - didSet { - handleFilterChange() - } - } weak var workspace: WorkspaceDocument? @@ -186,14 +181,18 @@ final class ProjectNavigatorViewController: NSViewController { } } - private func handleFilterChange() { + func handleFilterChange() { filteredContentChildren.removeAll() outlineView.reloadData() + guard let workspace else { return } + /// If the filter is empty, show all items and restore the expanded state. - if filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if workspace.navigatorFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { restoreExpandedState() + outlineView.autosaveExpandedItems = true } else { + outlineView.autosaveExpandedItems = false /// Expand all items for search. outlineView.expandItem(outlineView.item(atRow: 0), expandChildren: true) } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index f1a5a4d482..8f61a449b5 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -25,7 +25,7 @@ struct ProjectNavigatorToolbarBottom: View { addNewFileButton PaneTextField( "Filter", - text: $workspace.filter, + text: $workspace.navigatorFilter, leadingAccessories: { FilterDropDownIconButton(menu: { Button { @@ -33,10 +33,10 @@ struct ProjectNavigatorToolbarBottom: View { } label: { Text(workspace.sortFoldersOnTop ? "Alphabetically" : "Folders on top") } - }, isOn: !workspace.filter.isEmpty) + }, isOn: !workspace.navigatorFilter.isEmpty) .padding(.leading, 4) .foregroundStyle( - workspace.filter.isEmpty + workspace.navigatorFilter.isEmpty ? Color(nsColor: .secondaryLabelColor) : Color(nsColor: .controlAccentColor) ) @@ -57,7 +57,7 @@ struct ProjectNavigatorToolbarBottom: View { .padding(.trailing, 2.5) }, clearable: true, - hasValue: !workspace.filter.isEmpty || recentsFilter || sourceControlFilter + hasValue: !workspace.navigatorFilter.isEmpty || recentsFilter || sourceControlFilter ) } .padding(.horizontal, 5) @@ -124,7 +124,7 @@ struct ProjectNavigatorToolbarBottom: View { /// when the user clears the filter. private var clearFilterButton: some View { Button { - workspace.filter = "" + workspace.navigatorFilter = "" NSApp.keyWindow?.makeFirstResponder(nil) } label: { Image(systemName: "xmark.circle.fill") From 6e743db464a602de3a98e32a2f77297a67c2ad23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Larra=C3=B1aga?= Date: Sun, 6 Oct 2024 15:20:43 -0700 Subject: [PATCH 9/9] Replaced the workspace filter with an optional string parameter in the cell class. --- .../FindNavigatorListViewController.swift | 3 +-- .../OutlineView/FileSystemTableViewCell.swift | 17 +++++++++++++---- .../OutlineView/StandardTableViewCell.swift | 4 ++-- .../ProjectNavigatorTableViewCell.swift | 6 ++++-- ...orViewController+NSOutlineViewDelegate.swift | 2 +- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index a6acd28a3d..f469de2ee7 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -177,8 +177,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let view = ProjectNavigatorTableViewCell( frame: frameRect, item: (item as? SearchResultModel)?.file, - isEditable: false, - workspace: workspace + isEditable: false ) // We're using a medium label for file names b/c it makes it easier to // distinguish quickly which results are from which files. diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 9a382c7564..2388532bc5 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -15,6 +15,7 @@ class FileSystemTableViewCell: StandardTableViewCell { var changeLabelSmallWidth: NSLayoutConstraint! private let prefs = Settings.shared.preferences.general + private var navigatorFilter: String? /// Initializes the `OutlineTableViewCell` with an `icon` and `label` /// Both the icon and label will be colored, and sized based on the user's preferences. @@ -22,8 +23,11 @@ class FileSystemTableViewCell: StandardTableViewCell { /// - frameRect: The frame of the cell. /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. - init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, workspace: WorkspaceDocument?) { - super.init(frame: frameRect, isEditable: isEditable, workspace: workspace) + /// - navigatorFilter: An optional string use to filter the navigator area. + /// (Used for bolding and changing primary/secondary color). + init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, navigatorFilter: String? = nil) { + super.init(frame: frameRect, isEditable: isEditable) + self.navigatorFilter = navigatorFilter if let item = item { addIcon(item: item) @@ -43,12 +47,17 @@ class FileSystemTableViewCell: StandardTableViewCell { let fileName = item.labelFileName() + guard let navigatorFilter else { + textField?.stringValue = fileName + return + } + // Apply bold style if the filename matches the workspace filter - if let filter = workspace?.navigatorFilter, !filter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if !navigatorFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let attributedString = NSMutableAttributedString(string: fileName) // Check if the filename contains the filter text - let range = NSString(string: fileName).range(of: filter, options: .caseInsensitive) + let range = NSString(string: fileName).range(of: navigatorFilter, options: .caseInsensitive) if range.location != NSNotFound { // Set the label color to secondary attributedString.addAttribute( diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift index f4f186d31d..3f1becd8d4 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift @@ -24,10 +24,10 @@ class StandardTableViewCell: NSTableCellView { /// - frameRect: The frame of the cell. /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. - init(frame frameRect: NSRect, isEditable: Bool = true, workspace: WorkspaceDocument?) { + init(frame frameRect: NSRect, isEditable: Bool = true) { super.init(frame: frameRect) setupViews(frame: frameRect, isEditable: isEditable) - self.workspace = workspace + } // Default init, assumes isEditable to be false diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 6994d94f24..e6a2ac3775 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -22,14 +22,16 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { /// - frameRect: The frame of the cell. /// - item: The file item the cell represents. /// - isEditable: Set to true if the user should be able to edit the file name. + /// - navigatorFilter: An optional string use to filter the navigator area. + /// (Used for bolding and changing primary/secondary color). init( frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, delegate: OutlineTableViewCellDelegate? = nil, - workspace: WorkspaceDocument? + navigatorFilter: String? = nil ) { - super.init(frame: frameRect, item: item, isEditable: isEditable, workspace: workspace) + super.init(frame: frameRect, item: item, isEditable: isEditable, navigatorFilter: navigatorFilter) self.textField?.setAccessibilityIdentifier("ProjectNavigatorTableViewCell-\(item?.name ?? "")") self.delegate = delegate } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index d3b5d8bf89..bac0ddae3b 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -28,7 +28,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { frame: frameRect, item: item as? CEWorkspaceFile, delegate: self, - workspace: workspace + navigatorFilter: workspace?.navigatorFilter ) return cell }