Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c2898a5
Unify Files and Find in right sidebar
austinywang Jul 14, 2026
0352030
Add regressions for unified Files and Find state
austinywang Jul 14, 2026
0ffbde4
Preserve unified Files and Find activation state
austinywang Jul 14, 2026
9fe8aa0
Add regressions for unified activation synchronization
austinywang Jul 14, 2026
9bab30e
Synchronize unified file explorer activation
austinywang Jul 14, 2026
683a28e
Add regressions for unified search handoff
austinywang Jul 14, 2026
a145c38
Pause hidden file search work
austinywang Jul 14, 2026
93a5129
Add regressions for no-focus and search accessibility
austinywang Jul 14, 2026
93705d6
Preserve no-focus and search accessibility
austinywang Jul 14, 2026
e8c5c8f
Add regression for preserved completed search
austinywang Jul 14, 2026
79af32e
Reuse completed unified search snapshots
austinywang Jul 14, 2026
9dab4a3
Add regression for typed unified search activation
austinywang Jul 14, 2026
b39ee55
Synchronize typed unified search activation
austinywang Jul 14, 2026
3d22f6b
Restore unified search test defaults
austinywang Jul 14, 2026
9a0f161
Avoid repeated unified search focus publication
austinywang Jul 14, 2026
4ba429d
Add regression for visible no-focus responder
austinywang Jul 14, 2026
f815a17
Cover authoritative no-focus refocus target
austinywang Jul 14, 2026
fd30f4b
Keep no-focus explorer responder visible
austinywang Jul 14, 2026
68fc515
Add regressions for grouped results and hidden chrome
austinywang Jul 14, 2026
02cf47b
Harden unified search grouping and hidden layout
austinywang Jul 14, 2026
b299b74
Fix unified explorer project file identity
austinywang Jul 14, 2026
0ff9d0d
Add regression for regrouped search selection
austinywang Jul 14, 2026
3a0e78e
Preserve regrouped search selection identity
austinywang Jul 14, 2026
d8ae997
Unify file and content search in one pane
austinywang Jul 14, 2026
25abfb3
Merge remote-tracking branch 'origin/main' into issue-8011-unified-fi…
austinywang Jul 14, 2026
fa85d97
Fix unified search tests and cover nested expansion
austinywang Jul 14, 2026
8464c27
Fix unified search focus and filtering regressions
austinywang Jul 14, 2026
614c3ec
Route the Files shortcut to the file viewer
austinywang Jul 14, 2026
c653a35
Debounce name filtering and stabilize result menus
austinywang Jul 14, 2026
b848b29
Add regression for original unified explorer chrome
austinywang Jul 14, 2026
a7a4304
Restore the original Files and Finder presentations
austinywang Jul 14, 2026
172d2c7
Add regressions for unified explorer focus and navigation
austinywang Jul 14, 2026
4a76a93
Fix unified explorer focus and filtered disclosure
austinywang Jul 14, 2026
436503c
Add regression for hidden Files filter refresh
austinywang Jul 14, 2026
81c64d7
Defer hidden Files filter refreshes
austinywang Jul 14, 2026
8fc3372
Add regressions for unified explorer mode and filtering
austinywang Jul 14, 2026
75d4bb8
Cover filtered metadata refresh stability
austinywang Jul 14, 2026
7f95034
Unify explorer mode and filter update ownership
austinywang Jul 14, 2026
e3861a8
Isolate unified explorer test preferences
austinywang Jul 14, 2026
cb99759
Cover pending filter double-click routing
austinywang Jul 14, 2026
697bae6
Move filename filtering off the main actor
austinywang Jul 14, 2026
0e1c13f
Cover passive unified explorer mode changes
austinywang Jul 14, 2026
57e3ad2
Cover hidden filename filter cancellation
austinywang Jul 14, 2026
324d8d1
Bound unified explorer filter lifecycle
austinywang Jul 14, 2026
c714c2a
Cover real arrow events during filename filtering
austinywang Jul 14, 2026
70240ff
Bound filename filter projection work
austinywang Jul 14, 2026
e5b929d
Cover unified explorer state reconciliation
austinywang Jul 14, 2026
30cbd1d
Reconcile unified explorer filter state
austinywang Jul 14, 2026
f73ff28
Cover filtered expansion style reloads
austinywang Jul 14, 2026
ef12447
Reuse in-flight filename filter work
austinywang Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -105209,104 +105209,104 @@
}
}
},
"fileExplorer.search.empty": {
"fileExplorer.search.failed": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Type to search"
"value": "Search failed: %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "入力して検索"
"value": "検索に失敗しました: %@"
}
}
}
},
"fileExplorer.search.failed": {
"fileExplorer.search.limit": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Search failed: %@"
"value": "First %d matches"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "検索に失敗しました: %@"
"value": "最初の%d件"
}
}
}
},
"fileExplorer.search.limit": {
"fileExplorer.search.matches": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "First %d matches"
"value": "%d matches"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "最初の%d件"
"value": "%d件"
}
}
}
},
"fileExplorer.search.matches": {
"fileExplorer.search.noMatches": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%d matches"
"value": "No matches"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%d件"
"value": "一致なし"
}
}
}
},
"fileExplorer.search.noMatches": {
"fileExplorer.search.placeholder": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "No matches"
"value": "Find in files"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "一致なし"
"value": "ファイル内を検索"
}
}
}
},
"fileExplorer.search.placeholder": {
"fileExplorer.search.result.accessibilityLabel": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Search files"
"value": "%@: line %lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ファイルを検索"
"value": "%@: %lld行目"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7056,7 +7056,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
@discardableResult
func toggleRightSidebarKeyboardFocusInActiveMainWindow(preferredWindow: NSWindow? = nil) -> Bool {
let context = preferredRegisteredMainWindowContext(preferredWindow: preferredWindow)

guard let context else {
#if DEBUG
dlog(
Expand All @@ -7078,7 +7077,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
if let window {
mainWindowVisibilityController.focusForInWindowCommand(window, reason: .rightSidebarToggle)
}
let result = context.keyboardFocusCoordinator.toggleRightSidebarOrTerminalFocus()
// The primary Cmd-Shift-E route is the Files shortcut; Find has its own customizable action.
let result = context.keyboardFocusCoordinator.toggleRightSidebarOrTerminalFocus(mode: .files)
#if DEBUG
let afterResponder = window?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
dlog(
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContentView+RightSidebarCommandPalette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ extension ContentView {
{ _ in value }
}

return RightSidebarMode.availableModes().map { mode in
return RightSidebarMode.availableActivationModes().map { mode in
let title = mode.shortcutAction?.label ?? mode.label
return CommandPaletteCommandContribution(
commandId: Self.commandPaletteRightSidebarModeCommandID(mode),
Expand Down Expand Up @@ -224,7 +224,7 @@ extension ContentView {
private static func commandPaletteRightSidebarModeShortcutAction(
forCommandID commandID: String
) -> KeyboardShortcutSettings.Action? {
guard let mode = RightSidebarMode.availableModes().first(where: { mode in
guard let mode = RightSidebarMode.availableActivationModes().first(where: { mode in
Self.commandPaletteRightSidebarModeCommandID(mode) == commandID
}) else {
return nil
Expand Down
177 changes: 177 additions & 0 deletions Sources/FileExplorerContainerView+SearchResults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import AppKit

extension FileExplorerContainerView: NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate, NSMenuDelegate {
func moveSearchSelection(by delta: Int, focusResults: Bool) {
guard !searchSnapshot.results.isEmpty else { return }
let currentRow = searchResultsView.selectedRow >= 0
? searchResultsView.selectedRow
: (delta >= 0 ? -1 : searchSnapshot.results.count)
let targetRow = min(max(currentRow + delta, 0), searchSnapshot.results.count - 1)
searchResultsView.selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false)
searchResultsView.scrollRowToVisible(targetRow)
if focusResults, let window { _ = window.makeFirstResponder(searchResultsView) }
}

@MainActor
func openSelectedSearchResult() {
let row = searchResultsView.selectedRow
guard row >= 0, row < searchSnapshot.results.count else { return }
let path = searchSnapshot.results[row].path
guard coordinator.store.provider is LocalFileExplorerProvider else {
coordinator.onOpenFilePreview(path)
return
}
performFileExplorerFileOpen(path: path, onOpenFilePreview: coordinator.onOpenFilePreview)
}

func controlTextDidChange(_ notification: Notification) {
guard notification.object as? NSTextField === searchField else { return }
searchFieldTextDidChange()
}

func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
guard control === searchField, !textView.hasMarkedText() else { return false }
if let event = NSApp.currentEvent, searchField.handleOpenSelectionShortcut(event) { return true }
return handleSearchFieldCommand(commandSelector, textView: textView)
}

func numberOfRows(in tableView: NSTableView) -> Int { searchSnapshot.results.count }

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard row >= 0, row < searchSnapshot.results.count else { return nil }
let result = searchSnapshot.results[row]
let startsFileGroup = row == 0 || searchSnapshot.results[row - 1].path != result.path
let identifier = NSUserInterfaceItemIdentifier("FileSearchResultCell")
let cellView = tableView.makeView(withIdentifier: identifier, owner: nil) as? FileExplorerSearchResultCellView
?? FileExplorerSearchResultCellView(identifier: identifier)
cellView.configure(with: result, startsFileGroup: startsFileGroup)
return cellView
}

func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard tableView === searchResultsView, row >= 0, row < searchSnapshot.results.count else {
return FileExplorerSearchResultCellView.preferredRowHeight
}
let startsFileGroup = row == 0 || searchSnapshot.results[row - 1].path != searchSnapshot.results[row].path
return FileExplorerSearchResultCellView.preferredRowHeight(startsFileGroup: startsFileGroup)
}

func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {
guard tableView === searchResultsView, row >= 0, row < searchSnapshot.results.count else { return nil }
let result = searchSnapshot.results[row]
return FilePreviewDragPasteboardWriter(
filePath: result.path,
displayTitle: (result.relativePath as NSString).lastPathComponent
)
}

func tableView(
_ tableView: NSTableView,
draggingSession session: NSDraggingSession,
endedAt screenPoint: NSPoint,
operation: NSDragOperation
) {
guard tableView === searchResultsView else { return }
FilePreviewDragPasteboardWriter.discardRegisteredDrag(from: NSPasteboard(name: .drag))
}

func menuNeedsUpdate(_ menu: NSMenu) {
guard let searchMenu = searchResultsView.menu, menu === searchMenu else { return }
menu.removeAllItems()
let clickedRow = searchResultsView.clickedRow
let row = clickedRow >= 0 ? clickedRow : searchResultsView.selectedRow
guard row >= 0, row < searchSnapshot.results.count else { return }
if clickedRow >= 0 && !searchResultsView.selectedRowIndexes.contains(clickedRow) {
searchResultsView.selectRowIndexes(IndexSet(integer: clickedRow), byExtendingSelection: false)
}
let menuSelection = FileExplorerSearchMenuSelection(
clickedResult: searchSnapshot.results[row],
selectedResults: searchResultsView.selectedRowIndexes.compactMap {
searchSnapshot.results.indices.contains($0) ? searchSnapshot.results[$0] : nil
}
)

let openInCmuxItem = NSMenuItem(
title: String(localized: "fileExplorer.contextMenu.openInCmux", defaultValue: "Open in cmux"),
action: #selector(contextMenuOpenSearchResultInCmux(_:)),
keyEquivalent: ""
)
openInCmuxItem.target = self
openInCmuxItem.representedObject = menuSelection
menu.addItem(openInCmuxItem)

FileExplorerExternalOpenMenuItems(
fileURL: URL(fileURLWithPath: searchSnapshot.results[row].path),
target: self,
action: #selector(contextMenuOpenSearchResultExternally(_:))
).add(to: menu)

let revealItem = NSMenuItem(
title: FileExternalOpenText.revealInFinder,
action: #selector(contextMenuRevealSearchResultInFinder(_:)),
keyEquivalent: ""
)
revealItem.target = self
revealItem.representedObject = menuSelection
menu.addItem(revealItem)
menu.addItem(.separator())
menu.addFileExplorerInsertPathItems(
target: self,
representedObject: menuSelection,
insertAction: #selector(contextMenuInsertSearchResultPath(_:)),
insertRelativeAction: #selector(contextMenuInsertSearchResultRelativePath(_:))
)

for (title, action) in [
(String(localized: "fileExplorer.contextMenu.copyPath", defaultValue: "Copy Path"), #selector(contextMenuCopySearchResultPath(_:))),
(String(localized: "fileExplorer.contextMenu.copyRelativePath", defaultValue: "Copy Relative Path"), #selector(contextMenuCopySearchResultRelativePath(_:))),
] {
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
item.target = self
item.representedObject = menuSelection
menu.addItem(item)
}
}

func scrollSearchFieldEditorToInsertionPoint() {
guard let editor = searchField.currentEditor() else { return }
let selection = editor.selectedRange
let textLength = (editor.string as NSString).length
editor.scrollRangeToVisible(NSRange(location: min(selection.location + selection.length, textLength), length: 0))
}

private func searchResult(forMenuItem sender: NSMenuItem) -> FileSearchResult? {
(sender.representedObject as? FileExplorerSearchMenuSelection)?.clickedResult
}

@objc func openSelectedSearchResultFromTable(_ sender: NSTableView) {
openSelectedSearchResult()
}

@objc private func contextMenuOpenSearchResultInCmux(_ sender: NSMenuItem) {
guard let result = searchResult(forMenuItem: sender) else { return }
coordinator.onOpenFilePreview(result.path)
}

@objc private func contextMenuOpenSearchResultExternally(_ sender: NSMenuItem) {
guard let request = sender.representedObject as? FileExplorerExternalOpenRequest else { return }
FileExternalOpenAction.open(fileURL: request.fileURL, applicationURL: request.applicationURL)
}

@objc private func contextMenuRevealSearchResultInFinder(_ sender: NSMenuItem) {
guard let result = searchResult(forMenuItem: sender) else { return }
FileExternalOpenAction.revealInFinder(fileURL: URL(fileURLWithPath: result.path))
}

@objc private func contextMenuCopySearchResultPath(_ sender: NSMenuItem) {
guard let result = searchResult(forMenuItem: sender) else { return }
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(result.path, forType: .string)
}

@objc private func contextMenuCopySearchResultRelativePath(_ sender: NSMenuItem) {
guard let result = searchResult(forMenuItem: sender) else { return }
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(result.relativePath, forType: .string)
}
}
Loading
Loading