Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 11 additions & 10 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
6317 cmuxTests/SessionPersistenceTests.swift
6299 cmuxTests/GhosttyConfigTests.swift
6153 CLI/cmux_open.swift
6074 Sources/TabManager.swift
6116 Sources/TabManager.swift
6074 Sources/TextBoxInput.swift
5925 cmuxTests/TerminalAndGhosttyTests.swift
5522 cmuxTests/BrowserConfigTests.swift
4921 Sources/cmuxApp.swift
5113 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
4460 Sources/Panels/FilePreviewPanel.swift
4921 Sources/cmuxApp.swift
4502 Sources/Panels/FilePreviewPanel.swift
4400 cmuxTests/BrowserPanelTests.swift
4227 Sources/BrowserWindowPortal.swift
3937 Sources/Feed/FeedPanelView.swift
Expand All @@ -32,7 +32,7 @@
3664 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3397 Sources/CmuxConfig.swift
3331 cmuxTests/TabManagerSessionSnapshotTests.swift
3204 Sources/Update/UpdateTitlebarAccessory.swift
3200 Sources/Update/UpdateTitlebarAccessory.swift
2878 Sources/SessionIndexView.swift
2871 cmuxTests/CMUXOpenCommandTests.swift
2573 Sources/KeyboardShortcutSettings.swift
Expand All @@ -59,11 +59,11 @@
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1574 cmuxTests/MarkdownPanelTests.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1498 cmuxTests/OmnibarAndToolsTests.swift
1497 cmuxTests/OmnibarAndToolsTests.swift
1496 cmuxUITests/MultiWindowNotificationsUITests.swift
1446 Sources/FileExplorerStore.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1382 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1373 cmuxTests/AppDelegateIssue2907RoutingTests.swift
1366 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1362 Sources/CMUXInstalledExtensionSidebarHostView.swift
Expand Down Expand Up @@ -92,9 +92,9 @@
937 Sources/TextBoxMentionIndexStore.swift
934 Sources/App/ShortcutRoutingSupport.swift
926 Sources/DockPanelView.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
919 Packages/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
918 cmuxTests/WorkspaceGroupTests.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
905 Sources/CmuxSSHURLRequest.swift
901 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
893 Sources/WorkspaceContentView.swift
Expand All @@ -117,13 +117,14 @@
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
746 Sources/App/MenuBarExtraController.swift
738 Packages/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
738 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
736 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
716 Sources/TaskManagerSnapshot.swift
715 Packages/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
715 Sources/AppleScriptSupport.swift
710 Sources/TerminalSSHSessionDetector.swift
706 CLI/CMUXCLI+Config.swift
707 CLI/CMUXCLI+AgentHookDefinitions.swift
706 CLI/CMUXCLI+Config.swift
699 Sources/RightSidebarPanelView.swift
699 cmuxTests/TerminalNotificationClearAllTests.swift
698 cmuxTests/RestorableAgentHookProviderResumeTests.swift
Expand Down Expand Up @@ -187,8 +188,8 @@
528 cmuxTests/CLINotifyProcessTestSupport.swift
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
523 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
523 Sources/Panels/MarkdownPanel.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
519 Packages/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-two-column-cockpit-sidebar.swift
Expand Down
35 changes: 35 additions & 0 deletions Sources/Panels/AgentSessionPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,38 @@ final class AgentSessionPanel: Panel {
_ = reason
}
}


// MARK: - Find support

extension AgentSessionPanel: FindablePanel {
/// Shows the web view's find panel.
@discardableResult
func startFind() -> Bool {
sendFindPanelAction(.showFindInterface)
}

/// Jumps to the next match in the web view.
func findNext() {
_ = sendFindPanelAction(.nextMatch)
}

/// Jumps to the previous match in the web view.
func findPrevious() {
_ = sendFindPanelAction(.previousMatch)
}

/// Hiding the web view find panel is not supported by `WKWebView`.
func hideFind() {}

/// Sends a find action to the agent session web view.
@discardableResult
private func sendFindPanelAction(_ action: NSTextFinder.Action) -> Bool {
guard let webView = rendererSession.webView else { return false }
return NSApp.sendAction(
NSSelectorFromString("performFindPanelAction:"),
to: webView,
from: action.menuItemSender
)
}
}
5 changes: 5 additions & 0 deletions Sources/Panels/AgentSessionWebRendererSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ final class AgentSessionWebRendererSession {
func close() {
ownedCoordinator.close()
}

/// The underlying web view, exposed so find actions can be dispatched to it.
var webView: AgentSessionWebView? {
ownedCoordinator.webView
}
}
42 changes: 42 additions & 0 deletions Sources/Panels/FilePreviewPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4458,3 +4458,45 @@ private final class FilePreviewPointerObserverView: NSView {
nil
}
}


// MARK: - Find in text mode

extension FilePreviewPanel: FindablePanel {
/// Text-mode previews support "Use Selection for Find" when text is selected.
var hasSelectionForFind: Bool {
previewMode == .text && (textView?.selectedRange.length ?? 0) > 0
}

/// Shows the AppKit find bar when the panel is in text preview mode.
@discardableResult
func startFind() -> Bool {
guard previewMode == .text, let textView else { return false }
textView.performTextFinderAction(NSTextFinder.Action.showFindInterface.menuItemSender)
return true
}

/// Jumps to the next match in the text preview.
func findNext() {
guard previewMode == .text, let textView else { return }
textView.performTextFinderAction(NSTextFinder.Action.nextMatch.menuItemSender)
}

/// Jumps to the previous match in the text preview.
func findPrevious() {
guard previewMode == .text, let textView else { return }
textView.performTextFinderAction(NSTextFinder.Action.previousMatch.menuItemSender)
}

/// Hides the AppKit find bar in the text preview.
func hideFind() {
guard previewMode == .text, let textView else { return }
textView.performTextFinderAction(NSTextFinder.Action.hideFindInterface.menuItemSender)
}

/// Uses the current text selection as the find needle.
func useSelectionForFind() {
guard previewMode == .text, let textView else { return }
textView.performTextFinderAction(NSTextFinder.Action.setSearchString.menuItemSender)
}
}
71 changes: 71 additions & 0 deletions Sources/Panels/MarkdownPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,74 @@ final class MarkdownPanel: Panel, ObservableObject, FilePreviewTextEditingPanel
}
}
}


// MARK: - Find in panel

extension MarkdownPanel: FindablePanel {
/// Text-edit mode supports "Use Selection for Find" when text is selected.
var hasSelectionForFind: Bool {
displayMode == .text && (textView?.selectedRange.length ?? 0) > 0
}

/// Shows the find UI for the current display mode.
@discardableResult
func startFind() -> Bool {
switch displayMode {
case .text:
guard let textView else { return false }
textView.performTextFinderAction(NSTextFinder.Action.showFindInterface.menuItemSender)
return true
case .preview:
return sendFindPanelAction(.showFindInterface)
}
}

/// Jumps to the next match in the current display mode.
func findNext() {
switch displayMode {
case .text:
textView?.performTextFinderAction(NSTextFinder.Action.nextMatch.menuItemSender)
case .preview:
_ = sendFindPanelAction(.nextMatch)
}
}

/// Jumps to the previous match in the current display mode.
func findPrevious() {
switch displayMode {
case .text:
textView?.performTextFinderAction(NSTextFinder.Action.previousMatch.menuItemSender)
case .preview:
_ = sendFindPanelAction(.previousMatch)
}
}

/// Hides the find UI. Preview mode is a no-op because `WKWebView` does not
/// expose a hide action (`NSFindPanelAction` only defines values 1-10).
func hideFind() {
switch displayMode {
case .text:
textView?.performTextFinderAction(NSTextFinder.Action.hideFindInterface.menuItemSender)
case .preview:
break
}
}

/// Uses the current text selection as the find needle in text-edit mode.
func useSelectionForFind() {
guard displayMode == .text, let textView else { return }
textView.performTextFinderAction(NSTextFinder.Action.setSearchString.menuItemSender)
}

/// Sends a find action to the preview `WKWebView`.
@discardableResult
private func sendFindPanelAction(_ action: NSTextFinder.Action) -> Bool {
guard let webView = rendererSession.webView else { return false }
return NSApp.sendAction(
NSSelectorFromString("performFindPanelAction:"),
to: webView,
from: action.menuItemSender
)
}
}
5 changes: 5 additions & 0 deletions Sources/Panels/MarkdownWebSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ final class MarkdownRendererSession {
func renderedText() async -> String? {
await ownedCoordinator.renderedText()
}

/// The underlying web view, exposed so find actions can be dispatched to it.
var webView: MarkdownWebView? {
ownedCoordinator.webView
}
}

extension NSColor {
Expand Down
46 changes: 46 additions & 0 deletions Sources/Panels/Panel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,49 @@ extension Panel {
triggerFlash(reason: .navigation)
}
}


// MARK: - Find support

/// Capability surfaced by panels that can participate in the global `Cmd+F`
/// find flow. `TabManager` routes find actions to the focused panel only if it
/// conforms to this protocol.
@MainActor
protocol FindablePanel: AnyObject {
/// Whether the panel has a text selection that can be used as the find needle.
var hasSelectionForFind: Bool { get }

/// Opens the find UI. Returns `true` if the panel handled the request.
@discardableResult
func startFind() -> Bool

/// Jumps to the next search result in the panel.
func findNext()

/// Jumps to the previous search result in the panel.
func findPrevious()

/// Closes or hides the panel's find UI.
func hideFind()

/// Uses the current selection as the find needle (Cmd+E / "Use Selection for Find").
func useSelectionForFind()
}

extension FindablePanel {
/// Most panels do not support selection-based find.
var hasSelectionForFind: Bool { false }

/// Most panels do not support seeding the find query from the current selection.
func useSelectionForFind() {}
}

extension NSTextFinder.Action {
/// Returns an `NSMenuItem` whose tag matches this action, suitable for
/// sending to `performTextFinderAction(_:)` or `performFindPanelAction:`.
var menuItemSender: NSMenuItem {
let item = NSMenuItem()
item.tag = rawValue
return item
}
}
10 changes: 10 additions & 0 deletions Sources/Panels/ProjectBuildSettingsTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ struct ProjectBuildSettingsTabView: View {
@ObservedObject var panel: ProjectPanel
let model: ProjectModel

/// Drives focus into the settings filter text field when requested.
@FocusState private var focus: ProjectPanelSearchFocus?

var body: some View {
let computedRows = rows
VStack(alignment: .leading, spacing: 0) {
Expand All @@ -21,6 +24,12 @@ struct ProjectBuildSettingsTabView: View {
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onChange(of: panel.focusState.request) { _, newValue in
if newValue == .settings {
focus = .settings
panel.focusState.request = nil
}
}
}

@ViewBuilder
Expand All @@ -37,6 +46,7 @@ struct ProjectBuildSettingsTabView: View {
TextField("Filter settings", text: $panel.settingsSearchText)
.textFieldStyle(.plain)
.font(.system(size: 12))
.focused($focus, equals: .settings)
Toggle("Customized only", isOn: $panel.settingsCustomizedOnly)
.toggleStyle(.checkbox)
.font(.system(size: 11))
Expand Down
10 changes: 10 additions & 0 deletions Sources/Panels/ProjectFilesTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ struct ProjectFilesTabView: View {
@ObservedObject var panel: ProjectPanel
let model: ProjectModel

/// Drives focus into the files filter text field when requested.
@FocusState private var focus: ProjectPanelSearchFocus?

var body: some View {
let rows = flattenedRows
VStack(alignment: .leading, spacing: 0) {
Expand All @@ -36,6 +39,12 @@ struct ProjectFilesTabView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.onChange(of: panel.focusState.request) { _, newValue in
if newValue == .files {
focus = .files
panel.focusState.request = nil
}
}
}

@ViewBuilder
Expand All @@ -46,6 +55,7 @@ struct ProjectFilesTabView: View {
TextField("Filter files (e.g. AppDelegate)", text: $panel.filesSearchText)
.textFieldStyle(.plain)
.font(.system(size: 12))
.focused($focus, equals: .files)
if !panel.filesSearchText.isEmpty {
Button {
panel.filesSearchText = ""
Expand Down
Loading