diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 6c7e1a75..845f2ff3 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -74,11 +74,11 @@ 7C135CU00000000000000002 /* ClosedTerminalUndoStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C135CU00000000000000001 /* ClosedTerminalUndoStore.swift */; }; NRSP0010A1B2C3D4E5F60719 /* ContentView+SidebarResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRSP0009A1B2C3D4E5F60719 /* ContentView+SidebarResizer.swift */; }; NRSP0008A1B2C3D4E5F60719 /* ColorSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRSP0007A1B2C3D4E5F60719 /* ColorSupport.swift */; }; - A5FF0005 /* ContentView+CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FF0015 /* ContentView+CommandPalette.swift */; }; - A2CV0000000000000000000C /* ContentView+CommandPaletteCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV0000000000000000000D /* ContentView+CommandPaletteCommands.swift */; }; - A2CV0000000000000000000A /* ContentView+CommandPaletteOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV0000000000000000000B /* ContentView+CommandPaletteOverlay.swift */; }; - A2CV00000000000000000008 /* ContentView+CommandPaletteFlows.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000009 /* ContentView+CommandPaletteFlows.swift */; }; - A2CV00000000000000000006 /* ContentView+CommandPaletteTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000007 /* ContentView+CommandPaletteTypes.swift */; }; + A5FF0005 /* CommandPaletteView+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FF0015 /* CommandPaletteView+Helpers.swift */; }; + A2CV0000000000000000000C /* CommandPaletteView+Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV0000000000000000000D /* CommandPaletteView+Commands.swift */; }; + A2CV0000000000000000000A /* CommandPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV0000000000000000000B /* CommandPaletteView.swift */; }; + A2CV00000000000000000008 /* CommandPaletteView+Flows.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000009 /* CommandPaletteView+Flows.swift */; }; + A2CV00000000000000000006 /* CommandPaletteView+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000007 /* CommandPaletteView+Types.swift */; }; A2CV00000000000000000004 /* NotificationBurstCoalescer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000005 /* NotificationBurstCoalescer.swift */; }; A2CV00000000000000000002 /* TabManagerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2CV00000000000000000003 /* TabManagerSettings.swift */; }; NRCV00000000000000000005 /* TabItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRCV00000000000000000006 /* TabItemView.swift */; }; @@ -87,6 +87,7 @@ NRCV00000000000000000007 /* SidebarDragDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRCV00000000000000000008 /* SidebarDragDrop.swift */; }; NRCV00000000000000000009 /* SidebarVisuals.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRCV00000000000000000010 /* SidebarVisuals.swift */; }; NRCV00000000000000000011 /* CommandPaletteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRCV00000000000000000012 /* CommandPaletteController.swift */; }; + CPVW00000000000000000001 /* CommandPaletteController+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CPVW00000000000000000002 /* CommandPaletteController+Focus.swift */; }; E62155868BB29FEB5DAAAF25 /* SidebarSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */; }; B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */; }; A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; @@ -481,11 +482,11 @@ 7C135CU00000000000000001 /* ClosedTerminalUndoStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedTerminalUndoStore.swift; sourceTree = ""; }; NRSP0009A1B2C3D4E5F60719 /* ContentView+SidebarResizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+SidebarResizer.swift"; sourceTree = ""; }; NRSP0007A1B2C3D4E5F60719 /* ColorSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSupport.swift; sourceTree = ""; }; - A5FF0015 /* ContentView+CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+CommandPalette.swift"; sourceTree = ""; }; - A2CV0000000000000000000D /* ContentView+CommandPaletteCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+CommandPaletteCommands.swift"; sourceTree = ""; }; - A2CV0000000000000000000B /* ContentView+CommandPaletteOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+CommandPaletteOverlay.swift"; sourceTree = ""; }; - A2CV00000000000000000009 /* ContentView+CommandPaletteFlows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+CommandPaletteFlows.swift"; sourceTree = ""; }; - A2CV00000000000000000007 /* ContentView+CommandPaletteTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+CommandPaletteTypes.swift"; sourceTree = ""; }; + A5FF0015 /* CommandPaletteView+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteView+Helpers.swift"; sourceTree = ""; }; + A2CV0000000000000000000D /* CommandPaletteView+Commands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteView+Commands.swift"; sourceTree = ""; }; + A2CV0000000000000000000B /* CommandPaletteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteView.swift; sourceTree = ""; }; + A2CV00000000000000000009 /* CommandPaletteView+Flows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteView+Flows.swift"; sourceTree = ""; }; + A2CV00000000000000000007 /* CommandPaletteView+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteView+Types.swift"; sourceTree = ""; }; A2CV00000000000000000005 /* NotificationBurstCoalescer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBurstCoalescer.swift; sourceTree = ""; }; A2CV00000000000000000003 /* TabManagerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSettings.swift; sourceTree = ""; }; NRCV00000000000000000006 /* TabItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemView.swift; sourceTree = ""; }; @@ -494,6 +495,7 @@ NRCV00000000000000000008 /* SidebarDragDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarDragDrop.swift; sourceTree = ""; }; NRCV00000000000000000010 /* SidebarVisuals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarVisuals.swift; sourceTree = ""; }; NRCV00000000000000000012 /* CommandPaletteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteController.swift; sourceTree = ""; }; + CPVW00000000000000000002 /* CommandPaletteController+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPaletteController+Focus.swift"; sourceTree = ""; }; 9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSelectionState.swift; sourceTree = ""; }; B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDragHandleView.swift; sourceTree = ""; }; A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; @@ -933,11 +935,11 @@ 7C135CU00000000000000001 /* ClosedTerminalUndoStore.swift */, NRSP0009A1B2C3D4E5F60719 /* ContentView+SidebarResizer.swift */, NRSP0007A1B2C3D4E5F60719 /* ColorSupport.swift */, - A5FF0015 /* ContentView+CommandPalette.swift */, - A2CV0000000000000000000D /* ContentView+CommandPaletteCommands.swift */, - A2CV0000000000000000000B /* ContentView+CommandPaletteOverlay.swift */, - A2CV00000000000000000009 /* ContentView+CommandPaletteFlows.swift */, - A2CV00000000000000000007 /* ContentView+CommandPaletteTypes.swift */, + A5FF0015 /* CommandPaletteView+Helpers.swift */, + A2CV0000000000000000000D /* CommandPaletteView+Commands.swift */, + A2CV0000000000000000000B /* CommandPaletteView.swift */, + A2CV00000000000000000009 /* CommandPaletteView+Flows.swift */, + A2CV00000000000000000007 /* CommandPaletteView+Types.swift */, A2CV00000000000000000005 /* NotificationBurstCoalescer.swift */, A2CV00000000000000000003 /* TabManagerSettings.swift */, NRCV00000000000000000006 /* TabItemView.swift */, @@ -946,6 +948,7 @@ NRCV00000000000000000008 /* SidebarDragDrop.swift */, NRCV00000000000000000010 /* SidebarVisuals.swift */, NRCV00000000000000000012 /* CommandPaletteController.swift */, + CPVW00000000000000000002 /* CommandPaletteController+Focus.swift */, 9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */, B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */, A50012F0 /* Backport.swift */, @@ -1481,11 +1484,11 @@ 7C135CU00000000000000002 /* ClosedTerminalUndoStore.swift in Sources */, NRSP0010A1B2C3D4E5F60719 /* ContentView+SidebarResizer.swift in Sources */, NRSP0008A1B2C3D4E5F60719 /* ColorSupport.swift in Sources */, - A5FF0005 /* ContentView+CommandPalette.swift in Sources */, - A2CV0000000000000000000C /* ContentView+CommandPaletteCommands.swift in Sources */, - A2CV0000000000000000000A /* ContentView+CommandPaletteOverlay.swift in Sources */, - A2CV00000000000000000008 /* ContentView+CommandPaletteFlows.swift in Sources */, - A2CV00000000000000000006 /* ContentView+CommandPaletteTypes.swift in Sources */, + A5FF0005 /* CommandPaletteView+Helpers.swift in Sources */, + A2CV0000000000000000000C /* CommandPaletteView+Commands.swift in Sources */, + A2CV0000000000000000000A /* CommandPaletteView.swift in Sources */, + A2CV00000000000000000008 /* CommandPaletteView+Flows.swift in Sources */, + A2CV00000000000000000006 /* CommandPaletteView+Types.swift in Sources */, A2CV00000000000000000004 /* NotificationBurstCoalescer.swift in Sources */, A2CV00000000000000000002 /* TabManagerSettings.swift in Sources */, NRCV00000000000000000005 /* TabItemView.swift in Sources */, @@ -1494,6 +1497,7 @@ NRCV00000000000000000007 /* SidebarDragDrop.swift in Sources */, NRCV00000000000000000009 /* SidebarVisuals.swift in Sources */, NRCV00000000000000000011 /* CommandPaletteController.swift in Sources */, + CPVW00000000000000000001 /* CommandPaletteController+Focus.swift in Sources */, E62155868BB29FEB5DAAAF25 /* SidebarSelectionState.swift in Sources */, B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */, A50012F1 /* Backport.swift in Sources */, diff --git a/Sources/CommandPaletteController+Focus.swift b/Sources/CommandPaletteController+Focus.swift new file mode 100644 index 00000000..4398b5cf --- /dev/null +++ b/Sources/CommandPaletteController+Focus.swift @@ -0,0 +1,99 @@ +// Palette behavior that ContentView drives as well as CommandPaletteView: +// restoring terminal focus after a dismissal, and the per-window debug state +// AppDelegate exposes to the socket. Neither touches @FocusState, so both live +// on the model. ContentView calls them from inside its own surface-focus +// handlers so the restore runs after the workspace handoff in the same +// notification callback. + +import AppKit + +@MainActor +extension CommandPaletteController { + func requestFocusRestore( + target: CommandPaletteView.CommandPaletteRestoreFocusTarget, + tabManager: TabManager, + observedWindow: NSWindow? + ) { + commandPalettePendingDismissFocusTarget = target + commandPaletteRestoreTimeoutWorkItem?.cancel() + let timeoutWork = DispatchWorkItem { + self.commandPalettePendingDismissFocusTarget = nil + self.commandPaletteRestoreTimeoutWorkItem = nil + } + commandPaletteRestoreTimeoutWorkItem = timeoutWork + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: timeoutWork) + attemptFocusRestoreIfNeeded(tabManager: tabManager, observedWindow: observedWindow) + } + + func attemptFocusRestoreIfNeeded(tabManager: TabManager, observedWindow: NSWindow?) { + guard !isCommandPalettePresented else { return } + guard let target = commandPalettePendingDismissFocusTarget else { return } + if let window = observedWindow, !window.isVisible { + commandPalettePendingDismissFocusTarget = nil + commandPaletteRestoreTimeoutWorkItem?.cancel() + commandPaletteRestoreTimeoutWorkItem = nil + return + } + guard tabManager.tabs.contains(where: { $0.id == target.workspaceId }) else { + commandPalettePendingDismissFocusTarget = nil + commandPaletteRestoreTimeoutWorkItem?.cancel() + commandPaletteRestoreTimeoutWorkItem = nil + return + } + + if let window = observedWindow, !window.isKeyWindow { + window.makeKeyAndOrderFront(nil) + } + tabManager.focusTab(target.workspaceId, surfaceId: target.panelId, suppressFlash: true) + + guard let context = CommandPaletteView.focusedPanelContext(in: tabManager), + context.workspace.id == target.workspaceId, + context.panelId == target.panelId else { + return + } + guard context.panel.restoreFocusIntent(target.intent) else { return } + commandPalettePendingDismissFocusTarget = nil + commandPaletteRestoreTimeoutWorkItem?.cancel() + commandPaletteRestoreTimeoutWorkItem = nil + } + + func syncDebugState(observedWindow: NSWindow?) { + guard let window = observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow else { return } + AppDelegate.shared?.setCommandPaletteVisible(isCommandPalettePresented, for: window) + let visibleResultCount = commandPaletteVisibleResults.count + let selectedIndex = isCommandPalettePresented ? selectedIndex(resultCount: visibleResultCount) : 0 + AppDelegate.shared?.setCommandPaletteSelectionIndex(selectedIndex, for: window) + AppDelegate.shared?.setCommandPaletteSnapshot(debugSnapshot(), for: window) + } + + private func debugSnapshot() -> CommandPaletteDebugSnapshot { + guard isCommandPalettePresented else { return .empty } + + let listScope = CommandPaletteView.commandPaletteListScope(for: commandPaletteQuery) + let mode: String + switch commandPaletteMode { + case .commands: + mode = listScope.rawValue + case .renameInput: + mode = "rename_input" + case .workspaceDescriptionInput: + mode = "workspace_description_input" + } + + let rows = Array(commandPaletteVisibleResults.prefix(100)).map { result in + CommandPaletteDebugResultRow( + commandId: result.command.id, + title: result.command.title, + shortcutHint: result.command.shortcutHint, + trailingLabel: CommandPaletteView.commandPaletteTrailingLabel(for: result.command)?.text, + score: result.score + ) + } + + return CommandPaletteDebugSnapshot( + query: CommandPaletteView.commandPaletteQueryForMatching(query: commandPaletteQuery, scope: listScope), + mode: mode, + results: rows + ) + } +} diff --git a/Sources/CommandPaletteController.swift b/Sources/CommandPaletteController.swift index 2d612e2e..f5ee8492 100644 --- a/Sources/CommandPaletteController.swift +++ b/Sources/CommandPaletteController.swift @@ -3,25 +3,24 @@ // CommandPaletteController owns every piece of palette state that is // exclusively used by the command palette (query, mode, // search corpus/results, rename/workspace-description drafts, focus-restore -// targets, usage history, etc.). ContentView holds a single -// `@StateObject private var commandPaletteController` and exposes each -// property to the palette code in the ContentView+CommandPalette*.swift -// extensions through thin computed proxies, so that code reads and writes -// unqualified names while storage lives on the controller. +// targets, usage history, etc.). ContentView creates it as a +// `@StateObject` (one per main window) and passes it to CommandPaletteView, +// whose CommandPaletteView*.swift extensions hold the palette's UI, commands +// and flows. The focus-restore and debug-state methods ContentView also +// calls live in CommandPaletteController+Focus.swift. // // The two @FocusState properties (isCommandPaletteSearchFocused, -// isCommandPaletteRenameFocused) stay on ContentView: @FocusState is a -// SwiftUI View-only property wrapper and cannot be hosted on an +// isCommandPaletteRenameFocused) live on CommandPaletteView: @FocusState is +// a SwiftUI View-only property wrapper and cannot be hosted on an // ObservableObject. // -// CommandPaletteMode, CommandPaletteRestoreFocusTarget, -// CommandPaletteTextSelectionBehavior, and CommandPaletteMultilineTextEditorRepresentable -// are internal nested types of ContentView so this controller (a different file) can -// reference them. CommandPaletteCommand, CommandPaletteSearchResult, -// CommandPaletteListScope, CommandPalettePendingActivation, and -// CommandPaletteUsageEntry are also internal nested types; referenced here -// via `ContentView.` qualification since nested-type name lookup requires it -// from outside the enclosing type's lexical scope. +// CommandPaletteMode, CommandPaletteRestoreFocusTarget, CommandPaletteCommand, +// CommandPaletteSearchResult, CommandPaletteListScope, +// CommandPalettePendingActivation, CommandPaletteTextSelectionBehavior, +// CommandPaletteUsageEntry and CommandPaletteMultilineTextEditorRepresentable +// are internal nested types of CommandPaletteView, referenced here via +// `CommandPaletteView.` qualification since nested-type name lookup requires +// it from outside the enclosing type's lexical scope. import AppKit import Combine @@ -144,39 +143,39 @@ final class CommandPaletteController: ObservableObject { static let windowLifecycle = CommandPaletteWindowLifecycle() @Published var isCommandPalettePresented = false @Published var commandPaletteQuery: String = "" - @Published var commandPaletteMode: ContentView.CommandPaletteMode = .commands + @Published var commandPaletteMode: CommandPaletteView.CommandPaletteMode = .commands @Published var commandPaletteRenameDraft: String = "" @Published var commandPaletteWorkspaceDescriptionDraft: String = "" - @Published var commandPaletteWorkspaceDescriptionHeight: CGFloat = ContentView.CommandPaletteMultilineTextEditorRepresentable.defaultMinimumHeight + @Published var commandPaletteWorkspaceDescriptionHeight: CGFloat = CommandPaletteView.CommandPaletteMultilineTextEditorRepresentable.defaultMinimumHeight @Published var commandPaletteSelectedResultIndex: Int = 0 @Published var commandPaletteSelectionAnchorCommandID: String? @Published var commandPaletteHoveredResultIndex: Int? @Published var commandPaletteScrollTargetIndex: Int? @Published var commandPaletteScrollTargetAnchor: UnitPoint? - @Published var commandPaletteRestoreFocusTarget: ContentView.CommandPaletteRestoreFocusTarget? + @Published var commandPaletteRestoreFocusTarget: CommandPaletteView.CommandPaletteRestoreFocusTarget? @Published var commandPaletteSearchCorpus: [CommandPaletteSearchCorpusEntry] = [] @Published var commandPaletteSearchCorpusByID: [String: CommandPaletteSearchCorpusEntry] = [:] - @Published var commandPaletteSearchCommandsByID: [String: ContentView.CommandPaletteCommand] = [:] - @Published var cachedCommandPaletteResults: [ContentView.CommandPaletteSearchResult] = [] - @Published var commandPaletteVisibleResults: [ContentView.CommandPaletteSearchResult] = [] - @Published var commandPaletteVisibleResultsScope: ContentView.CommandPaletteListScope? + @Published var commandPaletteSearchCommandsByID: [String: CommandPaletteView.CommandPaletteCommand] = [:] + @Published var cachedCommandPaletteResults: [CommandPaletteView.CommandPaletteSearchResult] = [] + @Published var commandPaletteVisibleResults: [CommandPaletteView.CommandPaletteSearchResult] = [] + @Published var commandPaletteVisibleResultsScope: CommandPaletteView.CommandPaletteListScope? @Published var commandPaletteVisibleResultsFingerprint: Int? - @Published var cachedCommandPaletteScope: ContentView.CommandPaletteListScope? + @Published var cachedCommandPaletteScope: CommandPaletteView.CommandPaletteListScope? @Published var cachedCommandPaletteFingerprint: Int? - @Published var commandPalettePendingDismissFocusTarget: ContentView.CommandPaletteRestoreFocusTarget? + @Published var commandPalettePendingDismissFocusTarget: CommandPaletteView.CommandPaletteRestoreFocusTarget? @Published var commandPaletteRestoreTimeoutWorkItem: DispatchWorkItem? - @Published var commandPalettePendingTextSelectionBehavior: ContentView.CommandPaletteTextSelectionBehavior? + @Published var commandPalettePendingTextSelectionBehavior: CommandPaletteView.CommandPaletteTextSelectionBehavior? @Published var commandPaletteSearchTask: Task? @Published var commandPaletteSearchRequestID: UInt64 = 0 @Published var commandPaletteResolvedSearchRequestID: UInt64 = 0 - @Published var commandPaletteResolvedSearchScope: ContentView.CommandPaletteListScope? + @Published var commandPaletteResolvedSearchScope: CommandPaletteView.CommandPaletteListScope? @Published var commandPaletteResolvedSearchFingerprint: Int? @Published var commandPaletteResolvedMatchingQuery = "" @Published var commandPaletteTerminalOpenTargetAvailability: Set = [] @Published var isCommandPaletteSearchPending = false - @Published var commandPalettePendingActivation: ContentView.CommandPalettePendingActivation? + @Published var commandPalettePendingActivation: CommandPaletteView.CommandPalettePendingActivation? @Published var commandPaletteResultsRevision: UInt64 = 0 - @Published var commandPaletteUsageHistoryByCommandId: [String: ContentView.CommandPaletteUsageEntry] = [:] + @Published var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteView.CommandPaletteUsageEntry] = [:] @AppStorage(CommandPaletteSwitcherSearchSettings.searchAllSurfacesKey) var commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces @Published var commandPaletteShouldFocusWorkspaceDescriptionEditor = false @@ -187,8 +186,8 @@ final class CommandPaletteController: ObservableObject { } func setVisibleResults( - _ results: [ContentView.CommandPaletteSearchResult], - scope: ContentView.CommandPaletteListScope, + _ results: [CommandPaletteView.CommandPaletteSearchResult], + scope: CommandPaletteView.CommandPaletteListScope, fingerprint: Int? ) { commandPaletteVisibleResults = results @@ -202,7 +201,7 @@ final class CommandPaletteController: ObservableObject { } func syncSelectionAnchor(resultIDs: [String]) { - commandPaletteSelectionAnchorCommandID = ContentView.commandPaletteSelectionAnchorCommandID( + commandPaletteSelectionAnchorCommandID = CommandPaletteView.commandPaletteSelectionAnchorCommandID( selectedIndex: commandPaletteSelectedResultIndex, resultIDs: resultIDs ) @@ -235,7 +234,7 @@ final class CommandPaletteController: ObservableObject { func beginPresentation( initialQuery: String, - restoreFocusTarget: ContentView.CommandPaletteRestoreFocusTarget?, + restoreFocusTarget: CommandPaletteView.CommandPaletteRestoreFocusTarget?, minimumEditorHeight: CGFloat ) { commandPaletteRestoreFocusTarget = restoreFocusTarget @@ -243,7 +242,7 @@ final class CommandPaletteController: ObservableObject { resetListState(initialQuery: initialQuery, minimumEditorHeight: minimumEditorHeight) } - func prepareDismissal(minimumEditorHeight: CGFloat) -> ContentView.CommandPaletteRestoreFocusTarget? { + func prepareDismissal(minimumEditorHeight: CGFloat) -> CommandPaletteView.CommandPaletteRestoreFocusTarget? { let focusTarget = commandPaletteRestoreFocusTarget cancelSearch() commandPaletteSearchRequestID &+= 1 @@ -270,9 +269,9 @@ final class CommandPaletteController: ObservableObject { return focusTarget } - func recordUsage(commandId: String, usedAt: TimeInterval) -> [String: ContentView.CommandPaletteUsageEntry] { + func recordUsage(commandId: String, usedAt: TimeInterval) -> [String: CommandPaletteView.CommandPaletteUsageEntry] { var entry = commandPaletteUsageHistoryByCommandId[commandId] - ?? ContentView.CommandPaletteUsageEntry(useCount: 0, lastUsedAt: 0) + ?? CommandPaletteView.CommandPaletteUsageEntry(useCount: 0, lastUsedAt: 0) entry.useCount += 1 entry.lastUsedAt = usedAt commandPaletteUsageHistoryByCommandId[commandId] = entry diff --git a/Sources/ContentView+CommandPaletteCommands.swift b/Sources/CommandPaletteView+Commands.swift similarity index 96% rename from Sources/ContentView+CommandPaletteCommands.swift rename to Sources/CommandPaletteView+Commands.swift index 1b9cae6f..c9469f74 100644 --- a/Sources/ContentView+CommandPaletteCommands.swift +++ b/Sources/CommandPaletteView+Commands.swift @@ -2,7 +2,7 @@ import AppKit import Bonsplit import SwiftUI -extension ContentView { +extension CommandPaletteView { func focusCommandPaletteSwitcherTarget( windowId: UUID, tabManager: TabManager, @@ -722,7 +722,7 @@ extension ContentView { AppDelegate.shared?.openNewMainWindow(nil) } registry.register(commandId: "palette.sendFeedback") { - presentFeedbackComposer() + onSendFeedback() } registry.register(commandId: "palette.reloadConfiguration") { GhosttyApp.shared.reloadConfiguration(source: "palette.reloadConfiguration") @@ -1001,7 +1001,46 @@ extension ContentView { } } + private func selectedWorkspaceIndex() -> Int? { + guard let workspace = tabManager.selectedWorkspace else { return nil } + return tabManager.tabs.firstIndex { $0.id == workspace.id } + } + + func moveSelectedWorkspace(by delta: Int) { + guard let workspace = tabManager.selectedWorkspace else { return } + tabManager.moveWorkspaceAmongSiblings(tabId: workspace.id, by: delta) + tabManager.selectWorkspace(workspace) + } + + private func closeWorkspaceIds(_ workspaceIds: [UUID], allowPinned: Bool) { + tabManager.closeWorkspacesWithConfirmation(workspaceIds, allowPinned: allowPinned) + } + + func closeOtherSelectedWorkspaces() { + guard let workspace = tabManager.selectedWorkspace else { return } + let workspaceIds = tabManager.tabs.compactMap { $0.id == workspace.id ? nil : $0.id } + closeWorkspaceIds(workspaceIds, allowPinned: true) + } + + func closeSelectedWorkspacesBelow() { + guard tabManager.selectedWorkspace != nil, + let anchorIndex = selectedWorkspaceIndex() else { return } + let workspaceIds = tabManager.tabs.suffix(from: anchorIndex + 1).map(\.id) + closeWorkspaceIds(workspaceIds, allowPinned: true) + } + + func closeSelectedWorkspacesAbove() { + guard tabManager.selectedWorkspace != nil, + let anchorIndex = selectedWorkspaceIndex() else { return } + let workspaceIds = tabManager.tabs.prefix(upTo: anchorIndex).map(\.id) + closeWorkspaceIds(workspaceIds, allowPinned: true) + } + var focusedPanelContext: (workspace: Workspace, panelId: UUID, panel: any Panel)? { + Self.focusedPanelContext(in: tabManager) + } + + static func focusedPanelContext(in tabManager: TabManager) -> (workspace: Workspace, panelId: UUID, panel: any Panel)? { guard let workspace = tabManager.selectedWorkspace, let panelId = workspace.focusedPanelId, let panel = workspace.panels[panelId] else { diff --git a/Sources/ContentView+CommandPaletteFlows.swift b/Sources/CommandPaletteView+Flows.swift similarity index 89% rename from Sources/ContentView+CommandPaletteFlows.swift rename to Sources/CommandPaletteView+Flows.swift index ae20638a..62803671 100644 --- a/Sources/ContentView+CommandPaletteFlows.swift +++ b/Sources/CommandPaletteView+Flows.swift @@ -2,9 +2,9 @@ import AppKit import Bonsplit import SwiftUI -extension ContentView { +extension CommandPaletteView { @ViewBuilder - func attachCommandPaletteFocusAndTabsHandlers(to view: some View) -> some View { + func attachCommandPaletteFocusHandlers(to view: some View) -> some View { view .onChange(of: isCommandPaletteSearchFocused) { _, focused in if focused { @@ -16,47 +16,26 @@ extension ContentView { attemptCommandPaletteTextSelectionIfNeeded() } } - .onReceive(tabManager.$tabs) { tabs in - let existingIds = Set(tabs.map { $0.id }) - if let retiringWorkspaceId, !existingIds.contains(retiringWorkspaceId) { - self.retiringWorkspaceId = nil - workspaceHandoffFallbackTask?.cancel() - workspaceHandoffFallbackTask = nil - } - if let previousSelectedWorkspaceId, !existingIds.contains(previousSelectedWorkspaceId) { - self.previousSelectedWorkspaceId = tabManager.selectedTabId - } - tabManager.pruneBackgroundWorkspaceLoads(existingIds: existingIds) - reconcileMountedWorkspaceIds(tabs: tabs) - selectedTabIds = selectedTabIds.filter { existingIds.contains($0) } - if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId { - selectedTabIds = [selectedId] - } - if let lastIndex = lastSidebarSelectionIndex, lastIndex >= tabs.count { - if let selectedId = tabManager.selectedTabId { - lastSidebarSelectionIndex = tabs.firstIndex { $0.id == selectedId } - } else { - lastSidebarSelectionIndex = nil - } - } - syncSidebarSelectedWorkspaceIds() - applyUITestSidebarSelectionIfNeeded(tabs: tabs) + .onReceive(NotificationCenter.default.publisher( + for: NSWindow.didBecomeKeyNotification, + object: observedWindow + )) { _ in + attemptCommandPaletteFocusRestoreIfNeeded() + attemptCommandPaletteTextSelectionIfNeeded() + } + .onReceive(NotificationCenter.default.publisher(for: NSText.didBeginEditingNotification)) { notification in + guard commandPaletteController.commandPalettePendingTextSelectionBehavior != nil else { return } + guard let editor = notification.object as? NSTextView, + editor.isFieldEditor else { return } + guard let observedWindow else { return } + guard editor.window === observedWindow else { return } + attemptCommandPaletteTextSelectionIfNeeded() } } @ViewBuilder func attachCommandPaletteRequestHandlers(to view: some View) -> some View { view - .onReceive(NotificationCenter.default.publisher(for: SidebarDragLifecycleNotification.stateDidChange)) { notification in - let tabId = SidebarDragLifecycleNotification.tabId(from: notification) - sidebarDraggedTabId = tabId - #if DEBUG - dlog( - "sidebar.dragState.content tab=\(debugShortWorkspaceId(tabId)) " + - "reason=\(SidebarDragLifecycleNotification.reason(from: notification))" - ) - #endif - } .onReceive(NotificationCenter.default.publisher(for: .commandPaletteToggleRequested)) { notification in let requestedWindow = notification.object as? NSWindow guard Self.shouldHandleCommandPaletteRequest( @@ -185,16 +164,6 @@ extension ContentView { ) else { return } _ = handleCommandPaletteRenameDeleteBackward(modifiers: []) } - .onReceive(NotificationCenter.default.publisher(for: .feedbackComposerRequested)) { notification in - let requestedWindow = notification.object as? NSWindow - guard Self.shouldHandleCommandPaletteRequest( - observedWindow: observedWindow, - requestedWindow: requestedWindow, - keyWindow: NSApp.keyWindow, - mainWindow: NSApp.mainWindow - ) else { return } - presentFeedbackComposer() - } } var commandPaletteListScope: CommandPaletteListScope { @@ -722,49 +691,8 @@ extension ContentView { #endif } - func presentFeedbackComposer() { - DispatchQueue.main.async { - NSWorkspace.shared.open(URL(string: "https://github.com/darkroomengineering/programa/issues")!) - } - } - func syncCommandPaletteDebugStateForObservedWindow() { - guard let window = observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow else { return } - AppDelegate.shared?.setCommandPaletteVisible(commandPaletteController.isCommandPalettePresented, for: window) - let visibleResultCount = commandPaletteController.commandPaletteVisibleResults.count - let selectedIndex = commandPaletteController.isCommandPalettePresented ? commandPaletteSelectedIndex(resultCount: visibleResultCount) : 0 - AppDelegate.shared?.setCommandPaletteSelectionIndex(selectedIndex, for: window) - AppDelegate.shared?.setCommandPaletteSnapshot(commandPaletteDebugSnapshot(), for: window) - } - - private func commandPaletteDebugSnapshot() -> CommandPaletteDebugSnapshot { - guard commandPaletteController.isCommandPalettePresented else { return .empty } - - let mode: String - switch commandPaletteController.commandPaletteMode { - case .commands: - mode = commandPaletteListScope.rawValue - case .renameInput: - mode = "rename_input" - case .workspaceDescriptionInput: - mode = "workspace_description_input" - } - - let rows = Array(commandPaletteController.commandPaletteVisibleResults.prefix(100)).map { result in - CommandPaletteDebugResultRow( - commandId: result.command.id, - title: result.command.title, - shortcutHint: result.command.shortcutHint, - trailingLabel: commandPaletteTrailingLabel(for: result.command)?.text, - score: result.score - ) - } - - return CommandPaletteDebugSnapshot( - query: commandPaletteQueryForMatching, - mode: mode, - results: rows - ) + commandPaletteController.syncDebugState(observedWindow: observedWindow) } private func presentCommandPalette(initialQuery: String) { @@ -922,47 +850,15 @@ extension ContentView { } private func requestCommandPaletteFocusRestore(target: CommandPaletteRestoreFocusTarget) { - commandPaletteController.commandPalettePendingDismissFocusTarget = target - commandPaletteController.commandPaletteRestoreTimeoutWorkItem?.cancel() - let timeoutWork = DispatchWorkItem { - commandPaletteController.commandPalettePendingDismissFocusTarget = nil - commandPaletteController.commandPaletteRestoreTimeoutWorkItem = nil - } - commandPaletteController.commandPaletteRestoreTimeoutWorkItem = timeoutWork - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: timeoutWork) - attemptCommandPaletteFocusRestoreIfNeeded() + commandPaletteController.requestFocusRestore( + target: target, + tabManager: tabManager, + observedWindow: observedWindow + ) } func attemptCommandPaletteFocusRestoreIfNeeded() { - guard !commandPaletteController.isCommandPalettePresented else { return } - guard let target = commandPaletteController.commandPalettePendingDismissFocusTarget else { return } - if let window = observedWindow, !window.isVisible { - commandPaletteController.commandPalettePendingDismissFocusTarget = nil - commandPaletteController.commandPaletteRestoreTimeoutWorkItem?.cancel() - commandPaletteController.commandPaletteRestoreTimeoutWorkItem = nil - return - } - guard tabManager.tabs.contains(where: { $0.id == target.workspaceId }) else { - commandPaletteController.commandPalettePendingDismissFocusTarget = nil - commandPaletteController.commandPaletteRestoreTimeoutWorkItem?.cancel() - commandPaletteController.commandPaletteRestoreTimeoutWorkItem = nil - return - } - - if let window = observedWindow, !window.isKeyWindow { - window.makeKeyAndOrderFront(nil) - } - tabManager.focusTab(target.workspaceId, surfaceId: target.panelId, suppressFlash: true) - - guard let context = focusedPanelContext, - context.workspace.id == target.workspaceId, - context.panelId == target.panelId else { - return - } - guard context.panel.restoreFocusIntent(target.intent) else { return } - commandPaletteController.commandPalettePendingDismissFocusTarget = nil - commandPaletteController.commandPaletteRestoreTimeoutWorkItem?.cancel() - commandPaletteController.commandPaletteRestoreTimeoutWorkItem = nil + commandPaletteController.attemptFocusRestoreIfNeeded(tabManager: tabManager, observedWindow: observedWindow) } #if DEBUG diff --git a/Sources/ContentView+CommandPalette.swift b/Sources/CommandPaletteView+Helpers.swift similarity index 99% rename from Sources/ContentView+CommandPalette.swift rename to Sources/CommandPaletteView+Helpers.swift index ade34d8f..816d5b01 100644 --- a/Sources/ContentView+CommandPalette.swift +++ b/Sources/CommandPaletteView+Helpers.swift @@ -1,13 +1,13 @@ // Command palette pure helpers: fuzzy-search corpus building, result // fingerprinting and diffing, shortcut resolution, switcher entries and -// context snapshots. None of them mutate `@State`. The overlay view, the -// command list and the @State-mutating flows live in the sibling -// ContentView+CommandPalette{Overlay,Commands,Flows}.swift files. +// context snapshots. None of them mutate palette state. The view and its +// overlay UI live in CommandPaletteView.swift; the command list and the +// state-mutating flows live in CommandPaletteView+{Commands,Flows}.swift. import AppKit import SwiftUI -extension ContentView { +extension CommandPaletteView { nonisolated static let commandPaletteCommandsPrefix = ">" static let commandPaletteVisiblePreviewResultLimit = 48 static let commandPaletteVisiblePreviewCandidateLimit = 192 diff --git a/Sources/ContentView+CommandPaletteTypes.swift b/Sources/CommandPaletteView+Types.swift similarity index 98% rename from Sources/ContentView+CommandPaletteTypes.swift rename to Sources/CommandPaletteView+Types.swift index 8885ef79..d227fc9e 100644 --- a/Sources/ContentView+CommandPaletteTypes.swift +++ b/Sources/CommandPaletteView+Types.swift @@ -1,6 +1,6 @@ import SwiftUI -extension ContentView { +extension CommandPaletteView { enum CommandPaletteMode { case commands case renameInput(CommandPaletteRenameTarget) @@ -160,7 +160,7 @@ extension ContentView { } func fingerprint() -> Int { - ContentView.commandPaletteContextFingerprint( + CommandPaletteView.commandPaletteContextFingerprint( boolValues: boolValues, stringValues: stringValues ) diff --git a/Sources/ContentView+CommandPaletteOverlay.swift b/Sources/CommandPaletteView.swift similarity index 95% rename from Sources/ContentView+CommandPaletteOverlay.swift rename to Sources/CommandPaletteView.swift index 4061ac9d..99fe27ee 100644 --- a/Sources/ContentView+CommandPaletteOverlay.swift +++ b/Sources/CommandPaletteView.swift @@ -2,7 +2,48 @@ import AppKit import Bonsplit import SwiftUI -extension ContentView { +/// The command palette of one main window. ContentView mounts it as a background so +/// its state (including the two `@FocusState`s) lives in the main window's view +/// graph and its handlers receive palette requests while the palette is hidden. +/// The visible UI (`commandPaletteOverlay`) renders in the window's AppKit overlay +/// layer: the overlay's hosting view does not observe anything, so this view pushes +/// a fresh root into `WindowCommandPaletteOverlayController` on every update. That is +/// why it observes every object the overlay content reads. The `onSendFeedback` +/// closure also makes SwiftUI re-evaluate this view whenever ContentView updates, +/// which refreshes the switcher for workspace changes no observed object publishes. +struct CommandPaletteView: View { + @ObservedObject var commandPaletteController: CommandPaletteController + @ObservedObject var updateViewModel: UpdateViewModel + let windowId: UUID + let observedWindow: NSWindow? + let onSendFeedback: () -> Void + @EnvironmentObject var tabManager: TabManager + @EnvironmentObject var notificationStore: TerminalNotificationStore + @EnvironmentObject var sidebarState: SidebarState + @EnvironmentObject var programaConfigStore: ProgramaConfigStore + @ObservedObject var programaLayoutStore = ProgramaLayoutStore.shared + @AppStorage(WorkspacePresentationModeSettings.modeKey) + var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue + @FocusState var isCommandPaletteSearchFocused: Bool + @FocusState var isCommandPaletteRenameFocused: Bool + + var isMinimalMode: Bool { + WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal + } + + var body: some View { + let handlers = attachCommandPaletteFocusHandlers(to: Color.clear.allowsHitTesting(false)) + return attachCommandPaletteRequestHandlers(to: handlers) + .background(WindowAccessor(dedupeByWindow: false) { window in + MainActor.assumeIsolated { + let overlayController = commandPaletteWindowOverlayController(for: window) + overlayController.update(rootView: AnyView(commandPaletteOverlay), isVisible: commandPaletteController.isCommandPalettePresented) + } + }) + } +} + +extension CommandPaletteView { var commandPaletteOverlay: some View { GeometryReader { proxy in let maxAllowedWidth = max(340, proxy.size.width - 260) @@ -114,7 +155,7 @@ extension ContentView { ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in let isSelected = index == selectedIndex let isHovered = commandPaletteController.commandPaletteHoveredResultIndex == index - let trailingLabel = commandPaletteTrailingLabel(for: result.command) + let trailingLabel = Self.commandPaletteTrailingLabel(for: result.command) let rowBackground: Color = isSelected ? programaAccentColor().opacity(0.12) : (isHovered ? Color.primary.opacity(0.08) : .clear) @@ -1267,7 +1308,7 @@ extension ContentView { } } - func commandPaletteTrailingLabel(for command: CommandPaletteCommand) -> CommandPaletteTrailingLabel? { + static func commandPaletteTrailingLabel(for command: CommandPaletteCommand) -> CommandPaletteTrailingLabel? { if let shortcutHint = command.shortcutHint { return CommandPaletteTrailingLabel(text: shortcutHint, style: .shortcut) } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index d57e2578..d6ff6315 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -9,11 +9,8 @@ struct ContentView: View { @ObservedObject var updateViewModel: UpdateViewModel let windowId: UUID @EnvironmentObject var tabManager: TabManager - @EnvironmentObject var notificationStore: TerminalNotificationStore @EnvironmentObject var sidebarState: SidebarState @EnvironmentObject var sidebarSelectionState: SidebarSelectionState - @EnvironmentObject var programaConfigStore: ProgramaConfigStore - @ObservedObject var programaLayoutStore = ProgramaLayoutStore.shared @State var sidebarWidth: CGFloat = 200 @State var hoveredResizerHandles: Set = [] @State var isResizerDragging = false @@ -43,9 +40,7 @@ struct ContentView: View { #endif @State var isSidebarResizerCursorActive = false @State var sidebarResizerCursorStabilizer: DispatchSourceTimer? - @StateObject var commandPaletteController = CommandPaletteController() - @FocusState var isCommandPaletteSearchFocused: Bool - @FocusState var isCommandPaletteRenameFocused: Bool + @StateObject private var commandPaletteController = CommandPaletteController() private var sidebarView: some View { VerticalTabsSidebar( @@ -402,9 +397,9 @@ struct ContentView: View { let step1 = attachWorkspaceLifecycleHandlers(to: baseLayout) let step2 = attachTerminalFocusHandlers(to: step1) - let step3 = attachCommandPaletteFocusAndTabsHandlers(to: step2) - let step4 = attachCommandPaletteRequestHandlers(to: step3) - let step5 = attachOverlayWindowAccessor(to: step4) + let step3 = attachWorkspaceTabsHandlers(to: step2) + let step4 = attachSidebarDragAndFeedbackHandlers(to: step3) + let step5 = attachCommandPalette(to: step4) let step6 = attachWindowGlassAndFullscreenHandlers(to: step5) let step7 = attachSidebarSyncHandlers(to: step6) let step8 = attachFinalLifecycleHandlers(to: step7) @@ -552,7 +547,7 @@ struct ContentView: View { guard let tabId = notification.userInfo?[GhosttyNotificationKey.tabId] as? UUID, tabId == tabManager.selectedTabId else { return } completeWorkspaceHandoffIfNeeded(focusedTabId: tabId, reason: "focus") - attemptCommandPaletteFocusRestoreIfNeeded() + commandPaletteController.attemptFocusRestoreIfNeeded(tabManager: tabManager, observedWindow: observedWindow) scheduleTitlebarTextRefresh() } .onChange(of: titlebarThemeGeneration) { oldValue, newValue in @@ -565,34 +560,84 @@ struct ContentView: View { guard let tabId = notification.userInfo?[GhosttyNotificationKey.tabId] as? UUID, tabId == tabManager.selectedTabId else { return } completeWorkspaceHandoffIfNeeded(focusedTabId: tabId, reason: "first_responder") - attemptCommandPaletteFocusRestoreIfNeeded() + commandPaletteController.attemptFocusRestoreIfNeeded(tabManager: tabManager, observedWindow: observedWindow) } - .onReceive(NotificationCenter.default.publisher( - for: NSWindow.didBecomeKeyNotification, - object: observedWindow - )) { _ in - attemptCommandPaletteFocusRestoreIfNeeded() - attemptCommandPaletteTextSelectionIfNeeded() + } + + @ViewBuilder + private func attachWorkspaceTabsHandlers(to view: some View) -> some View { + view + .onReceive(tabManager.$tabs) { tabs in + let existingIds = Set(tabs.map { $0.id }) + if let retiringWorkspaceId, !existingIds.contains(retiringWorkspaceId) { + self.retiringWorkspaceId = nil + workspaceHandoffFallbackTask?.cancel() + workspaceHandoffFallbackTask = nil + } + if let previousSelectedWorkspaceId, !existingIds.contains(previousSelectedWorkspaceId) { + self.previousSelectedWorkspaceId = tabManager.selectedTabId + } + tabManager.pruneBackgroundWorkspaceLoads(existingIds: existingIds) + reconcileMountedWorkspaceIds(tabs: tabs) + selectedTabIds = selectedTabIds.filter { existingIds.contains($0) } + if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId { + selectedTabIds = [selectedId] + } + if let lastIndex = lastSidebarSelectionIndex, lastIndex >= tabs.count { + if let selectedId = tabManager.selectedTabId { + lastSidebarSelectionIndex = tabs.firstIndex { $0.id == selectedId } + } else { + lastSidebarSelectionIndex = nil + } + } + syncSidebarSelectedWorkspaceIds() + applyUITestSidebarSelectionIfNeeded(tabs: tabs) + } + } + + @ViewBuilder + private func attachSidebarDragAndFeedbackHandlers(to view: some View) -> some View { + view + .onReceive(NotificationCenter.default.publisher(for: SidebarDragLifecycleNotification.stateDidChange)) { notification in + let tabId = SidebarDragLifecycleNotification.tabId(from: notification) + sidebarDraggedTabId = tabId + #if DEBUG + dlog( + "sidebar.dragState.content tab=\(debugShortWorkspaceId(tabId)) " + + "reason=\(SidebarDragLifecycleNotification.reason(from: notification))" + ) + #endif } - .onReceive(NotificationCenter.default.publisher(for: NSText.didBeginEditingNotification)) { notification in - guard commandPaletteController.commandPalettePendingTextSelectionBehavior != nil else { return } - guard let editor = notification.object as? NSTextView, - editor.isFieldEditor else { return } - guard let observedWindow else { return } - guard editor.window === observedWindow else { return } - attemptCommandPaletteTextSelectionIfNeeded() + .onReceive(NotificationCenter.default.publisher(for: .feedbackComposerRequested)) { notification in + let requestedWindow = notification.object as? NSWindow + guard CommandPaletteView.shouldHandleCommandPaletteRequest( + observedWindow: observedWindow, + requestedWindow: requestedWindow, + keyWindow: NSApp.keyWindow, + mainWindow: NSApp.mainWindow + ) else { return } + presentFeedbackComposer() } } @ViewBuilder - private func attachOverlayWindowAccessor(to view: some View) -> some View { + private func attachCommandPalette(to view: some View) -> some View { view - .background(WindowAccessor(dedupeByWindow: false) { window in - MainActor.assumeIsolated { - let overlayController = commandPaletteWindowOverlayController(for: window) - overlayController.update(rootView: AnyView(commandPaletteOverlay), isVisible: commandPaletteController.isCommandPalettePresented) - } - }) + .background( + CommandPaletteView( + commandPaletteController: commandPaletteController, + updateViewModel: updateViewModel, + windowId: windowId, + observedWindow: observedWindow, + onSendFeedback: presentFeedbackComposer + ) + ) + } + + func presentFeedbackComposer() { + DispatchQueue.main.async { + NSWorkspace.shared.open(URL(string: "https://github.com/darkroomengineering/programa/issues")!) + } } @ViewBuilder @@ -734,7 +779,7 @@ struct ContentView: View { observedWindow = window isFullScreen = window.styleMask.contains(.fullScreen) clampSidebarWidthIfNeeded(availableWidth: window.contentView?.bounds.width ?? window.contentLayoutRect.width) - syncCommandPaletteDebugStateForObservedWindow() + commandPaletteController.syncDebugState(observedWindow: window) installSidebarResizerPointerMonitorIfNeeded() updateSidebarResizerBandState() } @@ -1108,41 +1153,6 @@ struct ContentView: View { #endif } - private func selectedWorkspaceIndex() -> Int? { - guard let workspace = tabManager.selectedWorkspace else { return nil } - return tabManager.tabs.firstIndex { $0.id == workspace.id } - } - - func moveSelectedWorkspace(by delta: Int) { - guard let workspace = tabManager.selectedWorkspace else { return } - tabManager.moveWorkspaceAmongSiblings(tabId: workspace.id, by: delta) - tabManager.selectWorkspace(workspace) - } - - private func closeWorkspaceIds(_ workspaceIds: [UUID], allowPinned: Bool) { - tabManager.closeWorkspacesWithConfirmation(workspaceIds, allowPinned: allowPinned) - } - - func closeOtherSelectedWorkspaces() { - guard let workspace = tabManager.selectedWorkspace else { return } - let workspaceIds = tabManager.tabs.compactMap { $0.id == workspace.id ? nil : $0.id } - closeWorkspaceIds(workspaceIds, allowPinned: true) - } - - func closeSelectedWorkspacesBelow() { - guard tabManager.selectedWorkspace != nil, - let anchorIndex = selectedWorkspaceIndex() else { return } - let workspaceIds = tabManager.tabs.suffix(from: anchorIndex + 1).map(\.id) - closeWorkspaceIds(workspaceIds, allowPinned: true) - } - - func closeSelectedWorkspacesAbove() { - guard tabManager.selectedWorkspace != nil, - let anchorIndex = selectedWorkspaceIndex() else { return } - let workspaceIds = tabManager.tabs.prefix(upTo: anchorIndex).map(\.id) - closeWorkspaceIds(workspaceIds, allowPinned: true) - } - func syncSidebarSelectedWorkspaceIds() { tabManager.setSidebarSelectedWorkspaceIds(selectedTabIds) } diff --git a/programaTests/AppDelegateShortcutRoutingTests.swift b/programaTests/AppDelegateShortcutRoutingTests.swift index 9b05f639..81a2373a 100644 --- a/programaTests/AppDelegateShortcutRoutingTests.swift +++ b/programaTests/AppDelegateShortcutRoutingTests.swift @@ -5384,7 +5384,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { } /// Every main window built via `createMainWindow()` already hosts a real command - /// palette overlay container (installed by `ContentView`, tagged with + /// palette overlay container (installed by `CommandPaletteView`, tagged with /// `commandPaletteOverlayContainerIdentifier`) as a sibling of `contentView`, not /// nested inside it. Adding a second, decoy view with the same identifier under /// `contentView` does not shadow it -- the production tree walk diff --git a/programaTests/CommandPaletteSearchEngineTests.swift b/programaTests/CommandPaletteSearchEngineTests.swift index 59c923b7..43f2d43a 100644 --- a/programaTests/CommandPaletteSearchEngineTests.swift +++ b/programaTests/CommandPaletteSearchEngineTests.swift @@ -301,7 +301,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } let corpusByID = Dictionary(uniqueKeysWithValues: corpus.map { ($0.payload, $0) }) - let previewCommandIDs = ContentView.commandPaletteCommandPreviewMatchCommandIDsForTests( + let previewCommandIDs = CommandPaletteView.commandPaletteCommandPreviewMatchCommandIDsForTests( searchCorpus: corpus, candidateCommandIDs: ["command.find"], searchCorpusByID: corpusByID, @@ -370,7 +370,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { let resultIDs = ["command.0", "command.1", "command.2"] XCTAssertEqual( - ContentView.commandPaletteResolvedSelectionIndex( + CommandPaletteView.commandPaletteResolvedSelectionIndex( preferredCommandID: "command.2", fallbackSelectedIndex: 0, resultIDs: resultIDs @@ -378,7 +378,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { 2 ) XCTAssertEqual( - ContentView.commandPaletteResolvedSelectionIndex( + CommandPaletteView.commandPaletteResolvedSelectionIndex( preferredCommandID: "missing", fallbackSelectedIndex: 9, resultIDs: resultIDs @@ -386,7 +386,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { 2 ) XCTAssertEqual( - ContentView.commandPaletteResolvedSelectionIndex( + CommandPaletteView.commandPaletteResolvedSelectionIndex( preferredCommandID: nil, fallbackSelectedIndex: 1, resultIDs: [] @@ -399,7 +399,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { let resultIDs = ["command.0", "command.1", "command.2"] XCTAssertEqual( - ContentView.commandPaletteResolvedPendingActivation( + CommandPaletteView.commandPaletteResolvedPendingActivation( .selected(requestID: 41, fallbackSelectedIndex: 0, preferredCommandID: "command.2"), requestID: 41, resultIDs: resultIDs @@ -407,7 +407,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { .selected(index: 2) ) XCTAssertEqual( - ContentView.commandPaletteResolvedPendingActivation( + CommandPaletteView.commandPaletteResolvedPendingActivation( .command(requestID: 41, commandID: "command.1"), requestID: 41, resultIDs: resultIDs @@ -415,14 +415,14 @@ final class CommandPaletteSearchEngineTests: XCTestCase { .command(commandID: "command.1") ) XCTAssertNil( - ContentView.commandPaletteResolvedPendingActivation( + CommandPaletteView.commandPaletteResolvedPendingActivation( .command(requestID: 41, commandID: "missing"), requestID: 41, resultIDs: resultIDs ) ) XCTAssertNil( - ContentView.commandPaletteResolvedPendingActivation( + CommandPaletteView.commandPaletteResolvedPendingActivation( .selected(requestID: 40, fallbackSelectedIndex: 0, preferredCommandID: nil), requestID: 41, resultIDs: resultIDs @@ -432,13 +432,13 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testSelectionAnchorTracksVisiblePendingSelection() { let resultIDs = ["command.0", "command.1", "command.2"] - let visibleAnchor = ContentView.commandPaletteSelectionAnchorCommandID( + let visibleAnchor = CommandPaletteView.commandPaletteSelectionAnchorCommandID( selectedIndex: 2, resultIDs: resultIDs ) XCTAssertEqual( - ContentView.commandPaletteResolvedPendingActivation( + CommandPaletteView.commandPaletteResolvedPendingActivation( .selected( requestID: 41, fallbackSelectedIndex: 0, @@ -454,7 +454,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testPreviewCandidateCommandIDsAreBounded() { let resultIDs = (0..<500).map { "command.\($0)" } - let previewCandidateIDs = ContentView.commandPalettePreviewCandidateCommandIDs( + let previewCandidateIDs = CommandPaletteView.commandPalettePreviewCandidateCommandIDs( resultIDs: resultIDs, limit: 192 ) @@ -466,12 +466,12 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testSynchronousSeedRunsOnlyWhenScopeChanges() { XCTAssertTrue( - ContentView.commandPaletteShouldSynchronouslySeedResults( + CommandPaletteView.commandPaletteShouldSynchronouslySeedResults( hasVisibleResultsForScope: false ) ) XCTAssertFalse( - ContentView.commandPaletteShouldSynchronouslySeedResults( + CommandPaletteView.commandPaletteShouldSynchronouslySeedResults( hasVisibleResultsForScope: true ) ) @@ -479,7 +479,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testPendingEmptyStateIsNotPreservedWhileSearchIsStillPending() { XCTAssertFalse( - ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + CommandPaletteView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( isSearchPending: true, visibleResultsScopeMatches: true, resolvedSearchScopeMatches: true, @@ -493,7 +493,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testPendingEmptyStateIsPreservedForSameResolvedNoMatchQuery() { XCTAssertTrue( - ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + CommandPaletteView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( isSearchPending: true, visibleResultsScopeMatches: true, resolvedSearchScopeMatches: true, @@ -507,7 +507,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testPendingEmptyStateIsNotPreservedWhenQueryDoesNotRefineResolvedNoMatch() { XCTAssertFalse( - ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + CommandPaletteView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( isSearchPending: true, visibleResultsScopeMatches: true, resolvedSearchScopeMatches: true, @@ -521,7 +521,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testPendingEmptyStateIsNotPreservedWhenResolvedResultsMayBeStale() { XCTAssertFalse( - ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + CommandPaletteView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( isSearchPending: true, visibleResultsScopeMatches: true, resolvedSearchScopeMatches: true, @@ -532,7 +532,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) ) XCTAssertFalse( - ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + CommandPaletteView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( isSearchPending: true, visibleResultsScopeMatches: true, resolvedSearchScopeMatches: true, @@ -546,28 +546,28 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testVisibleResultsResetWhenQueryChangesCommandPaletteScope() { XCTAssertTrue( - ContentView.commandPaletteShouldResetVisibleResultsForQueryTransition( + CommandPaletteView.commandPaletteShouldResetVisibleResultsForQueryTransition( oldQuery: ">", newQuery: "", hasVisibleResults: true ) ) XCTAssertTrue( - ContentView.commandPaletteShouldResetVisibleResultsForQueryTransition( + CommandPaletteView.commandPaletteShouldResetVisibleResultsForQueryTransition( oldQuery: "", newQuery: ">", hasVisibleResults: true ) ) XCTAssertFalse( - ContentView.commandPaletteShouldResetVisibleResultsForQueryTransition( + CommandPaletteView.commandPaletteShouldResetVisibleResultsForQueryTransition( oldQuery: ">rename", newQuery: ">renam", hasVisibleResults: true ) ) XCTAssertFalse( - ContentView.commandPaletteShouldResetVisibleResultsForQueryTransition( + CommandPaletteView.commandPaletteShouldResetVisibleResultsForQueryTransition( oldQuery: ">", newQuery: "", hasVisibleResults: false @@ -576,7 +576,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } func testRefreshInputsPreferObservedQueryOverStaleState() { - let inputs = ContentView.commandPaletteRefreshInputsForTests( + let inputs = CommandPaletteView.commandPaletteRefreshInputsForTests( stateQuery: ">", observedQuery: "", searchAllSurfaces: true @@ -588,7 +588,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } func testRefreshInputsIncludeSurfacesOnlyForNonEmptySwitcherQuery() { - let switcherInputs = ContentView.commandPaletteRefreshInputsForTests( + let switcherInputs = CommandPaletteView.commandPaletteRefreshInputsForTests( stateQuery: "", observedQuery: " feature/search ", searchAllSurfaces: true @@ -597,7 +597,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { XCTAssertEqual(switcherInputs.matchingQuery, "feature/search") XCTAssertTrue(switcherInputs.includesSurfaces) - let commandInputs = ContentView.commandPaletteRefreshInputsForTests( + let commandInputs = CommandPaletteView.commandPaletteRefreshInputsForTests( stateQuery: "", observedQuery: ">feature/search", searchAllSurfaces: true @@ -606,7 +606,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { XCTAssertEqual(commandInputs.matchingQuery, "feature/search") XCTAssertFalse(commandInputs.includesSurfaces) - let workspaceOnlyInputs = ContentView.commandPaletteRefreshInputsForTests( + let workspaceOnlyInputs = CommandPaletteView.commandPaletteRefreshInputsForTests( stateQuery: "", observedQuery: "feature/search", searchAllSurfaces: false @@ -617,7 +617,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } func testCommandContextFingerprintTracksExactContextValues() { - let base = ContentView.commandPaletteContextFingerprint( + let base = CommandPaletteView.commandPaletteContextFingerprint( boolValues: [ "workspace.hasPullRequests": true, "panel.hasUnread": false, @@ -628,7 +628,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { "panel.name": "Main", ] ) - let unreadChanged = ContentView.commandPaletteContextFingerprint( + let unreadChanged = CommandPaletteView.commandPaletteContextFingerprint( boolValues: [ "workspace.hasPullRequests": true, "panel.hasUnread": true, @@ -639,7 +639,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase { "panel.name": "Main", ] ) - let renamed = ContentView.commandPaletteContextFingerprint( + let renamed = CommandPaletteView.commandPaletteContextFingerprint( boolValues: [ "workspace.hasPullRequests": true, "panel.hasUnread": false, @@ -658,14 +658,14 @@ final class CommandPaletteSearchEngineTests: XCTestCase { func testSwitcherFingerprintTracksMetadataValuesAtSameCardinality() { let windowID = UUID() let workspaceID = UUID() - let base = ContentView.commandPaletteSwitcherFingerprint( + let base = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: "Window 2", selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Alpha", metadata: CommandPaletteSwitcherSearchMetadata( @@ -679,14 +679,14 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) ] ) - let changedMetadata = ContentView.commandPaletteSwitcherFingerprint( + let changedMetadata = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: "Window 2", selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Alpha", metadata: CommandPaletteSwitcherSearchMetadata( @@ -700,14 +700,14 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) ] ) - let changedDisplayName = ContentView.commandPaletteSwitcherFingerprint( + let changedDisplayName = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: "Window 2", selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Beta", metadata: CommandPaletteSwitcherSearchMetadata( @@ -731,19 +731,19 @@ final class CommandPaletteSearchEngineTests: XCTestCase { let workspaceID = UUID() let surfaceID = UUID() - let base = ContentView.commandPaletteSwitcherFingerprint( + let base = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: nil, selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Alpha", metadata: CommandPaletteSwitcherSearchMetadata(), surfaces: [ - ContentView.CommandPaletteSwitcherFingerprintSurface( + CommandPaletteView.CommandPaletteSwitcherFingerprintSurface( id: surfaceID, displayName: "Terminal", kindLabel: "Terminal", @@ -759,19 +759,19 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) ] ) - let changedSurfaceMetadata = ContentView.commandPaletteSwitcherFingerprint( + let changedSurfaceMetadata = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: nil, selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Alpha", metadata: CommandPaletteSwitcherSearchMetadata(), surfaces: [ - ContentView.CommandPaletteSwitcherFingerprintSurface( + CommandPaletteView.CommandPaletteSwitcherFingerprintSurface( id: surfaceID, displayName: "Terminal", kindLabel: "Terminal", @@ -787,19 +787,19 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) ] ) - let changedSurfaceKind = ContentView.commandPaletteSwitcherFingerprint( + let changedSurfaceKind = CommandPaletteView.commandPaletteSwitcherFingerprint( windowContexts: [ - ContentView.CommandPaletteSwitcherFingerprintContext( + CommandPaletteView.CommandPaletteSwitcherFingerprintContext( windowId: windowID, windowLabel: nil, selectedWorkspaceId: workspaceID, workspaces: [ - ContentView.CommandPaletteSwitcherFingerprintWorkspace( + CommandPaletteView.CommandPaletteSwitcherFingerprintWorkspace( id: workspaceID, displayName: "Workspace Alpha", metadata: CommandPaletteSwitcherSearchMetadata(), surfaces: [ - ContentView.CommandPaletteSwitcherFingerprintSurface( + CommandPaletteView.CommandPaletteSwitcherFingerprintSurface( id: surfaceID, displayName: "Terminal", kindLabel: "Markdown", diff --git a/programaTests/ShortcutAndCommandPaletteTests.swift b/programaTests/ShortcutAndCommandPaletteTests.swift index 519d53de..698a6d71 100644 --- a/programaTests/ShortcutAndCommandPaletteTests.swift +++ b/programaTests/ShortcutAndCommandPaletteTests.swift @@ -367,7 +367,7 @@ final class CommandPaletteFocusStealerClassificationTests: XCTestCase { final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase { func testFirstEntryPinsToTopAnchor() { - let anchor = ContentView.commandPaletteScrollPositionAnchor( + let anchor = CommandPaletteView.commandPaletteScrollPositionAnchor( selectedIndex: 0, resultCount: 20 ) @@ -375,7 +375,7 @@ final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase { } func testLastEntryPinsToBottomAnchor() { - let anchor = ContentView.commandPaletteScrollPositionAnchor( + let anchor = CommandPaletteView.commandPaletteScrollPositionAnchor( selectedIndex: 19, resultCount: 20 ) @@ -383,7 +383,7 @@ final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase { } func testMiddleEntryUsesNilAnchorForMinimalScroll() { - let anchor = ContentView.commandPaletteScrollPositionAnchor( + let anchor = CommandPaletteView.commandPaletteScrollPositionAnchor( selectedIndex: 6, resultCount: 20 ) @@ -391,7 +391,7 @@ final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase { } func testEmptyResultsProduceNoAnchor() { - let anchor = ContentView.commandPaletteScrollPositionAnchor( + let anchor = CommandPaletteView.commandPaletteScrollPositionAnchor( selectedIndex: 0, resultCount: 0 ) diff --git a/programaTests/WorkspaceManualUnreadTests.swift b/programaTests/WorkspaceManualUnreadTests.swift index 7f2db329..52376104 100644 --- a/programaTests/WorkspaceManualUnreadTests.swift +++ b/programaTests/WorkspaceManualUnreadTests.swift @@ -338,7 +338,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { let windowB = makeWindow() XCTAssertTrue( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: windowA, requestedWindow: windowA, keyWindow: windowA, @@ -346,7 +346,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { ) ) XCTAssertFalse( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: windowB, requestedWindow: windowA, keyWindow: windowA, @@ -360,7 +360,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { let other = makeWindow() XCTAssertTrue( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: key, requestedWindow: nil, keyWindow: key, @@ -368,7 +368,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { ) ) XCTAssertFalse( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: other, requestedWindow: nil, keyWindow: key, @@ -382,7 +382,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { let other = makeWindow() XCTAssertTrue( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: main, requestedWindow: nil, keyWindow: nil, @@ -390,7 +390,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { ) ) XCTAssertFalse( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: other, requestedWindow: nil, keyWindow: nil, @@ -401,7 +401,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { func testNoObservedWindowNeverHandlesRequest() { XCTAssertFalse( - ContentView.shouldHandleCommandPaletteRequest( + CommandPaletteView.shouldHandleCommandPaletteRequest( observedWindow: nil, requestedWindow: makeWindow(), keyWindow: makeWindow(), @@ -414,7 +414,7 @@ final class CommandPaletteRequestRoutingTests: XCTestCase { final class CommandPaletteBackNavigationTests: XCTestCase { func testBackspaceOnEmptyRenameInputReturnsToCommandList() { XCTAssertTrue( - ContentView.commandPaletteShouldPopRenameInputOnDelete( + CommandPaletteView.commandPaletteShouldPopRenameInputOnDelete( renameDraft: "", modifiers: [] ) @@ -423,7 +423,7 @@ final class CommandPaletteBackNavigationTests: XCTestCase { func testBackspaceWithRenameTextDoesNotReturnToCommandList() { XCTAssertFalse( - ContentView.commandPaletteShouldPopRenameInputOnDelete( + CommandPaletteView.commandPaletteShouldPopRenameInputOnDelete( renameDraft: "Terminal 1", modifiers: [] ) @@ -432,13 +432,13 @@ final class CommandPaletteBackNavigationTests: XCTestCase { func testModifiedBackspaceDoesNotReturnToCommandList() { XCTAssertFalse( - ContentView.commandPaletteShouldPopRenameInputOnDelete( + CommandPaletteView.commandPaletteShouldPopRenameInputOnDelete( renameDraft: "", modifiers: [.control] ) ) XCTAssertFalse( - ContentView.commandPaletteShouldPopRenameInputOnDelete( + CommandPaletteView.commandPaletteShouldPopRenameInputOnDelete( renameDraft: "", modifiers: [.command] )