diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index 89f4d41cfc45..c3ae7214f8b4 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -2,36 +2,36 @@ # Format: max_linesrelative path # Reduce counts as files shrink. CI fails if tracked files exceed this budget. 34475 CLI/cmux.swift -17604 Sources/AppDelegate.swift -15933 Sources/ContentView.swift +17753 Sources/AppDelegate.swift +15986 Sources/ContentView.swift 14104 Sources/TerminalController.swift -12658 Sources/Workspace.swift +12832 Sources/Workspace.swift 12115 cmuxTests/AppDelegateShortcutRoutingTests.swift 11833 Sources/GhosttyTerminalView.swift -11413 Sources/Panels/BrowserPanel.swift +11493 Sources/Panels/BrowserPanel.swift 9331 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift -7953 Sources/Panels/BrowserPanelView.swift -7356 cmuxTests/WorkspaceUnitTests.swift +7957 Sources/Panels/BrowserPanelView.swift +7434 cmuxTests/WorkspaceUnitTests.swift 7221 cmuxTests/WorkspaceRemoteConnectionTests.swift 6317 cmuxTests/SessionPersistenceTests.swift +6290 Sources/TabManager.swift 6223 cmuxTests/GhosttyConfigTests.swift -6172 Sources/TabManager.swift 6153 CLI/cmux_open.swift 6074 Sources/TextBoxInput.swift 5925 cmuxTests/TerminalAndGhosttyTests.swift 5566 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift 5526 cmuxTests/BrowserConfigTests.swift -4516 Sources/cmuxApp.swift +4521 Sources/cmuxApp.swift 4467 Sources/Panels/FilePreviewPanel.swift -4401 cmuxTests/BrowserPanelTests.swift +4413 cmuxTests/BrowserPanelTests.swift 4225 Sources/BrowserWindowPortal.swift +4001 cmuxTests/TabManagerUnitTests.swift 3937 Sources/Feed/FeedPanelView.swift -3926 cmuxTests/TabManagerUnitTests.swift 3896 cmuxTests/WindowAndDragTests.swift 3734 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift 3699 cmuxTests/CLIGenericHookPersistenceTests.swift -3397 Sources/CmuxConfig.swift -3361 cmuxTests/TabManagerSessionSnapshotTests.swift +3434 Sources/CmuxConfig.swift +3392 cmuxTests/TabManagerSessionSnapshotTests.swift 3055 Sources/Update/UpdateTitlebarAccessory.swift 2878 Sources/SessionIndexView.swift 2871 cmuxTests/CMUXOpenCommandTests.swift @@ -63,11 +63,11 @@ 1497 cmuxTests/OmnibarAndToolsTests.swift 1496 cmuxUITests/MultiWindowNotificationsUITests.swift 1446 Sources/FileExplorerStore.swift +1441 cmuxTests/AppDelegateIssue2907RoutingTests.swift 1426 Sources/VaultAgentProcessScanner.swift 1412 Sources/RemoteTmuxControlConnection.swift 1384 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift 1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift -1373 cmuxTests/AppDelegateIssue2907RoutingTests.swift 1366 Sources/Feed/FeedButtonStyleDebugWindowController.swift 1362 Sources/CMUXInstalledExtensionSidebarHostView.swift 1292 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift @@ -88,7 +88,7 @@ 1021 cmuxUITests/TerminalCmdClickUITests.swift 1006 cmuxTests/CmuxSSHURLRequestTests.swift 1000 cmuxTests/CmuxTopSnapshotScopeTests.swift -951 Sources/App/TerminalDirectoryOpenSupport.swift +958 Sources/App/TerminalDirectoryOpenSupport.swift 947 Sources/TerminalNotificationPolicy.swift 945 Sources/SessionIndexRegisteredAgents.swift 937 Sources/TextBoxMentionIndexStore.swift @@ -105,11 +105,11 @@ 859 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift 847 cmuxTests/AgentSessionAutoResumeSettingsTests.swift 845 cmuxTests/SSHStartupSignalLifecycleTests.swift -830 Sources/TaskManagerTypes.swift +833 Sources/TaskManagerTypes.swift 825 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift +818 Sources/WorkspaceContentView.swift 810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift -802 Sources/WorkspaceContentView.swift -797 Sources/ClosedItemHistory.swift +799 Sources/ClosedItemHistory.swift 779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift 774 cmuxUITests/BrowserFixtureInteractionUITests.swift 773 Sources/MainWindowFocusController.swift @@ -124,10 +124,10 @@ 746 Sources/App/MenuBarExtraController.swift 738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift 736 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift +728 Sources/TaskManagerSnapshot.swift 726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift 725 Sources/RightSidebarPanelView.swift 722 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift -716 Sources/TaskManagerSnapshot.swift 715 Sources/AppleScriptSupport.swift 710 Sources/TerminalSSHSessionDetector.swift 707 CLI/CMUXCLI+AgentHookDefinitions.swift @@ -151,8 +151,8 @@ 653 Packages/macOS/CmuxBrowserImport/Sources/CmuxBrowserImport/Detection/BrowserInstalledBrowserDetector.swift 650 Sources/Panels/MarkdownRemoteImageLoader.swift 649 Sources/CmuxTopSnapshot.swift +648 Sources/TerminalController+ControlPaneContext.swift 641 cmuxTests/CommandPaletteNucleoFFITests.swift -636 Sources/TerminalController+ControlPaneContext.swift 630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift 621 cmuxTests/FinderFileDropRegressionTests.swift 621 cmuxUITests/RightSidebarChromeHeightUITests.swift diff --git a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift index 3ccd6d276cd3..1562a17915fd 100644 --- a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift +++ b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift @@ -104,6 +104,8 @@ struct SurfaceInsight: Identifiable { return "terminal" case .browser: return "globe" + case .codeEditor: + return "curlybraces.square" case .markdown: return "doc.text" case .filePreview: diff --git a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift index 42bb6c8d149c..7043443f8bdc 100644 --- a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift +++ b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift @@ -165,6 +165,8 @@ struct TabsVisibleSidebarView: View { return "terminal" case .browser: return "globe" + case .codeEditor: + return "curlybraces.square" case .markdown: return "doc.text" case .filePreview: diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarPaneV1.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarPaneV1.swift index 20b19388b375..f17814d88ae3 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarPaneV1.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarPaneV1.swift @@ -145,8 +145,9 @@ extension ControlCommandCoordinator { return "ERROR: TabManager not available" } - // Parse arguments: --type=terminal|browser --direction=left|right|up|down --url=... + // Parse arguments: --type=terminal|browser|editor --direction=left|right|up|down --url=... var isBrowser = false + var isCodeEditor = false var direction: ControlSidebarSplitDirectionV1 = .right var urlRaw: String? = nil var url: URL? = nil @@ -157,7 +158,9 @@ extension ControlCommandCoordinator { let partStr = String(part) if partStr.hasPrefix("--type=") { let typeStr = String(partStr.dropFirst(7)) - isBrowser = typeStr == "browser" + let normalizedType = normalizedToken(typeStr) + isBrowser = normalizedType == "browser" + isCodeEditor = ["editor", "code", "codeeditor", "vscode", "vscodeinline"].contains(normalizedType) } else if partStr.hasPrefix("--direction=") { let dirStr = String(partStr.dropFirst(12)) if let parsed = ControlSidebarSplitDirectionV1.parse(dirStr) { @@ -175,12 +178,13 @@ extension ControlCommandCoordinator { if invalidDirection { return "ERROR: Invalid direction. Use left, right, up, or down." } - if isBrowser, !(browserPanelContext?.controlBrowserPanelAvailabilityEnabled() ?? true) { + if (isBrowser || isCodeEditor), !(browserPanelContext?.controlBrowserPanelAvailabilityEnabled() ?? true) { return browserPanelOpenExternallyWhenDisabled(rawURL: urlRaw, url: url) } switch sidebarContext?.controlSidebarCreatePaneSplit( isBrowser: isBrowser, + isCodeEditor: isCodeEditor, orientationIsHorizontal: direction.isHorizontal, insertFirst: direction.insertFirst, url: url @@ -204,8 +208,9 @@ extension ControlCommandCoordinator { return "ERROR: TabManager not available" } - // Parse arguments: --type=terminal|browser --pane= --url=... + // Parse arguments: --type=terminal|browser|editor --pane= --url=... var isBrowser = false + var isCodeEditor = false var paneArg: String? = nil var urlRaw: String? = nil var url: URL? = nil @@ -215,7 +220,9 @@ extension ControlCommandCoordinator { let partStr = String(part) if partStr.hasPrefix("--type=") { let typeStr = String(partStr.dropFirst(7)) - isBrowser = typeStr == "browser" + let normalizedType = normalizedToken(typeStr) + isBrowser = normalizedType == "browser" + isCodeEditor = ["editor", "code", "codeeditor", "vscode", "vscodeinline"].contains(normalizedType) } else if partStr.hasPrefix("--pane=") { paneArg = String(partStr.dropFirst(7)) } else if partStr.hasPrefix("--url=") { @@ -224,11 +231,16 @@ extension ControlCommandCoordinator { url = urlRaw.flatMap { URL(string: $0) } } } - if isBrowser, !(browserPanelContext?.controlBrowserPanelAvailabilityEnabled() ?? true) { + if (isBrowser || isCodeEditor), !(browserPanelContext?.controlBrowserPanelAvailabilityEnabled() ?? true) { return browserPanelOpenExternallyWhenDisabled(rawURL: urlRaw, url: url) } - switch sidebarContext?.controlSidebarNewSurface(isBrowser: isBrowser, paneArg: paneArg, url: url) ?? .noTabSelected { + switch sidebarContext?.controlSidebarNewSurface( + isBrowser: isBrowser, + isCodeEditor: isCodeEditor, + paneArg: paneArg, + url: url + ) ?? .noTabSelected { case .noTabSelected, .failed: return "ERROR: Failed to create tab" case .paneNotFound: diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarContext.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarContext.swift index 35406d8c7606..9e371b6de8ef 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarContext.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarContext.swift @@ -255,6 +255,7 @@ public protocol ControlSidebarContext: AnyObject { /// that from failure. func controlSidebarCreatePaneSplit( isBrowser: Bool, + isCodeEditor: Bool, orientationIsHorizontal: Bool, insertFirst: Bool, url: URL? @@ -262,7 +263,12 @@ public protocol ControlSidebarContext: AnyObject { /// Creates a `new_surface` in a pane (UUID-or-index argument, focused /// pane otherwise). - func controlSidebarNewSurface(isBrowser: Bool, paneArg: String?, url: URL?) -> ControlSidebarNewSurfaceResolution + func controlSidebarNewSurface( + isBrowser: Bool, + isCodeEditor: Bool, + paneArg: String?, + url: URL? + ) -> ControlSidebarNewSurfaceResolution /// Closes a surface (`close_surface`; empty argument = focused surface). func controlSidebarCloseSurface(surfaceArg: String?) -> ControlSidebarCloseSurfaceResolution diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs+SidebarBrowser.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs+SidebarBrowser.swift index a3421c44b16e..d1e2849ca3a8 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs+SidebarBrowser.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs+SidebarBrowser.swift @@ -191,12 +191,18 @@ extension ControlSidebarContext { func controlSidebarCreatePaneSplit( isBrowser: Bool, + isCodeEditor: Bool, orientationIsHorizontal: Bool, insertFirst: Bool, url: URL? ) -> ControlSidebarPaneSplitResolution { .failed } - func controlSidebarNewSurface(isBrowser: Bool, paneArg: String?, url: URL?) -> ControlSidebarNewSurfaceResolution { + func controlSidebarNewSurface( + isBrowser: Bool, + isCodeEditor: Bool, + paneArg: String?, + url: URL? + ) -> ControlSidebarNewSurfaceResolution { .noTabSelected } diff --git a/Packages/macOS/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift b/Packages/macOS/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift index 2676a6079760..529edf8fb338 100644 --- a/Packages/macOS/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift +++ b/Packages/macOS/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift @@ -6,6 +6,8 @@ public enum CmuxSidebarSurfaceKind: String, Codable, CaseIterable, Equatable, Se case terminal /// A browser surface. case browser + /// A code editor surface. + case codeEditor /// A markdown preview surface. case markdown /// A file preview surface. diff --git a/Packages/macOS/CmuxWorkspaceCore/Sources/CmuxWorkspaceCore/Values/SurfaceKind.swift b/Packages/macOS/CmuxWorkspaceCore/Sources/CmuxWorkspaceCore/Values/SurfaceKind.swift index a269b99b6496..1cb0aef79064 100644 --- a/Packages/macOS/CmuxWorkspaceCore/Sources/CmuxWorkspaceCore/Values/SurfaceKind.swift +++ b/Packages/macOS/CmuxWorkspaceCore/Sources/CmuxWorkspaceCore/Values/SurfaceKind.swift @@ -19,6 +19,8 @@ public struct SurfaceKind: RawRepresentable, Hashable, Sendable { public static let terminal = SurfaceKind(rawValue: "terminal") /// A browser pane. public static let browser = SurfaceKind(rawValue: "browser") + /// A code editor pane. + public static let codeEditor = SurfaceKind(rawValue: "editor") /// A markdown preview pane. public static let markdown = SurfaceKind(rawValue: "markdown") /// A file (Quick Look style) preview pane. diff --git a/Packages/macOS/CmuxWorkspaceCore/Tests/CmuxWorkspaceCoreTests/WorkspaceCoreValueTests.swift b/Packages/macOS/CmuxWorkspaceCore/Tests/CmuxWorkspaceCoreTests/WorkspaceCoreValueTests.swift index c0b986162954..e842dc079048 100644 --- a/Packages/macOS/CmuxWorkspaceCore/Tests/CmuxWorkspaceCoreTests/WorkspaceCoreValueTests.swift +++ b/Packages/macOS/CmuxWorkspaceCore/Tests/CmuxWorkspaceCoreTests/WorkspaceCoreValueTests.swift @@ -7,6 +7,7 @@ import Testing @Test func kindStringsAreFrozenWireValues() { #expect(SurfaceKind.terminal.rawValue == "terminal") #expect(SurfaceKind.browser.rawValue == "browser") + #expect(SurfaceKind.codeEditor.rawValue == "editor") #expect(SurfaceKind.markdown.rawValue == "markdown") #expect(SurfaceKind.filePreview.rawValue == "filePreview") #expect(SurfaceKind.rightSidebarTool.rawValue == "rightSidebarTool") diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 95309aaca27f..4366f6e98039 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -203147,6 +203147,1897 @@ } } } + }, + "mobile.pairing.testflight.prompt": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Don't have it yet?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "まだお持ちでないですか?" + } + } + } + }, + "mobile.pairing.testflight.link": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Download via TestFlight" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "TestFlight でダウンロード" + } + } + } + }, + "codeEditor.newTab": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Uređivač koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Edytor kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Редактор кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Редактор коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器" + } + } + } + }, + "command.codeEditorSplitDown.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تخطيط محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Raspored uređivača koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditorlayout" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor-Layout" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor Layout" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Diseño del editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disposition de l’éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Layout editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタレイアウト" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្លង់កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기 레이아웃" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditoroppsett" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Układ edytora kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Layout do editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Макет редактора кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เค้าโครงตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici düzeni" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Макет редактора коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器布局" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器版面配置" + } + } + } + }, + "command.codeEditorSplitDown.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تقسيم محرر الأكواد للأسفل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Podijeli uređivač koda dolje" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Opdel kodeeditor ned" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor nach unten teilen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Split Code Editor Down" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dividir editor de código abajo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Scinder l’éditeur de code vers le bas" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dividi editor di codice in basso" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタを下に分割" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បំបែកកម្មវិធីកែសម្រួលកូដចុះក្រោម" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기 아래로 분할" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Del kodeeditor ned" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Podziel edytor kodu w dół" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dividir editor de código abaixo" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Разделить редактор кода вниз" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แบ่งตัวแก้ไขโค้ดลง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyiciyi aşağı böl" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розділити редактор коду вниз" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "向下拆分代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "向下分割程式碼編輯器" + } + } + } + }, + "command.codeEditorSplitRight.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تخطيط محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Raspored uređivača koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditorlayout" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor-Layout" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor Layout" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Diseño del editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disposition de l’éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Layout editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタレイアウト" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្លង់កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기 레이아웃" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditoroppsett" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Układ edytora kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Layout do editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Макет редактора кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เค้าโครงตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici düzeni" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Макет редактора коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器布局" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器版面配置" + } + } + } + }, + "command.codeEditorSplitRight.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تقسيم محرر الأكواد إلى اليمين" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Podijeli uređivač koda desno" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Opdel kodeeditor til højre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor nach rechts teilen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Split Code Editor Right" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dividir editor de código a la derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Scinder l’éditeur de code à droite" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dividi editor di codice a destra" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタを右に分割" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បំបែកកម្មវិធីកែសម្រួលកូដទៅស្តាំ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기 오른쪽 분할" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Del kodeeditor til høyre" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Podziel edytor kodu w prawo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dividir editor de código à direita" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Разделить редактор кода вправо" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แบ่งตัวแก้ไขโค้ดไปทางขวา" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyiciyi sağa böl" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розділити редактор коду праворуч" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "向右拆分代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "向右分割程式碼編輯器" + } + } + } + }, + "command.newCodeEditorTab.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبويب" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kartica" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Fane" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tab" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Tab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pestaña" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Onglet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scheda" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "タブ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "탭" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fane" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Karta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aba" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Вкладка" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แท็บ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sekme" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вкладка" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "标签页" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "分頁" + } + } + } + }, + "command.newCodeEditorTab.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبويب جديد (محرر الأكواد)" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nova kartica (uređivač koda)" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ny fane (kodeeditor)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Tab (Code-Editor)" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "New Tab (Code Editor)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nueva pestaña (editor de código)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvel onglet (éditeur de code)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuova scheda (editor di codice)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規タブ(コードエディタ)" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំងថ្មី (កម្មវិធីកែសម្រួលកូដ)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 탭(코드 편집기)" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Ny fane (kodeeditor)" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nowa karta (edytor kodu)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nova aba (editor de código)" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Новая вкладка (редактор кода)" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แท็บใหม่ (ตัวแก้ไขโค้ด)" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni sekme (kod düzenleyici)" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Нова вкладка (редактор коду)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建标签页(代码编辑器)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增分頁(程式碼編輯器)" + } + } + } + }, + "commandPalette.kind.codeEditor": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Uređivač koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Edytor kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Редактор кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Редактор коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器" + } + } + } + }, + "emptyPane.browser": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "المتصفح" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Preglednik" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Browser" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Browser" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Browser" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Navegador" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Navigateur" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Browser" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "브라우저" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Nettleser" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przeglądarka" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Navegador" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Браузер" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เบราว์เซอร์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Tarayıcı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Браузер" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "浏览器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "瀏覽器" + } + } + } + }, + "emptyPane.codeEditor": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Uređivač koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Edytor kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Редактор кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Редактор коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器" + } + } + } + }, + "emptyPane.terminal": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "الطرفية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Terminale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ターミナル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "터미널" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Терминал" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เทอร์มินัล" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Термінал" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "终端" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "終端機" + } + } + } + }, + "emptyPane.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لوحة فارغة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prazan panel" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Tomt panel" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leeres Panel" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Empty Panel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Panel vacío" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Panneau vide" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pannello vuoto" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "空のパネル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "빈 패널" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Tomt panel" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Pusty panel" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Painel vazio" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Пустая панель" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แผงว่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Boş panel" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Порожня панель" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "空面板" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "空面板" + } + } + } + }, + "menu.history.recentlyClosed.panel.codeEditor": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Uređivač koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Edytor kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Редактор кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Редактор коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器" + } + } + } + }, + "menu.file.newCodeEditorTab": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبويب محرر أكواد جديد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nova kartica uređivača koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ny kodeeditorfane" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Code-Editor-Tab" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "New Code Editor Tab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nueva pestaña de editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvel onglet d’éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuova scheda editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新しいコードエディタタブ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំងកម្មវិធីកែសម្រួលកូដថ្មី" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 코드 편집기 탭" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Ny kodeeditorfane" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nowa karta edytora kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nova aba de editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Новая вкладка редактора кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แท็บตัวแก้ไขโค้ดใหม่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni kod düzenleyici sekmesi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Нова вкладка редактора коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建代码编辑器标签页" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增程式碼編輯器分頁" + } + } + } + }, + "taskManager.row.surfaceType.codeEditor": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "محرر الأكواد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Uređivač koda" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Code-Editor" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Code Editor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur de code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editor di codice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コードエディタ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "코드 편집기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kodeeditor" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Edytor kodu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editor de código" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Редактор кода" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวแก้ไขโค้ด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kod düzenleyici" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Редактор коду" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代码编辑器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "程式碼編輯器" + } + } + } } } } diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 6c757ca3dff6..12943a6f81d9 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -561,6 +561,8 @@ final class VSCodeServeWebController { guard self.activeLaunchGeneration == launchGeneration else { return } self.isLaunching = false self.activeLaunchGeneration = nil + let completions = self.takePendingCompletions(for: launchGeneration) + DispatchQueue.main.async { completions.forEach { $0(nil) } } } return } @@ -586,6 +588,8 @@ final class VSCodeServeWebController { if let process = launchResult?.process, process.isRunning { process.terminate() } + let completions = self.takePendingCompletions(for: launchGeneration) + DispatchQueue.main.async { completions.forEach { $0(nil) } } return } @@ -599,25 +603,28 @@ final class VSCodeServeWebController { self.serveWebURL = nil } - var completions: [(URL?) -> Void] = [] - var remaining: [(generation: UInt64, completion: (URL?) -> Void)] = [] - for pending in self.pendingCompletions { - if pending.generation == launchGeneration { - completions.append(pending.completion) - } else { - remaining.append(pending) - } - } - self.pendingCompletions = remaining + let completions = self.takePendingCompletions(for: launchGeneration) let resolvedURL = self.serveWebURL - DispatchQueue.main.async { - completions.forEach { $0(resolvedURL) } - } + DispatchQueue.main.async { completions.forEach { $0(resolvedURL) } } } } } } + private func takePendingCompletions(for generation: UInt64) -> [(URL?) -> Void] { + let completions = pendingCompletions.filter { $0.generation == generation }.map(\.completion) + pendingCompletions.removeAll { $0.generation == generation } + return completions + } + + func ensureServeWebURL(vscodeApplicationURL: URL) async -> URL? { + await withCheckedContinuation { continuation in + ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + continuation.resume(returning: serveWebURL) + } + } + } + func stop() { let (processes, tokenFileURLs, completions): ([Process], [URL], [(URL?) -> Void]) = queue.sync { self.lifecycleGeneration &+= 1 diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 8f80f0d62afe..00e405d6b59e 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7536,12 +7536,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return false } - let targetWorkspaceId = targetTabManager.selectedWorkspace?.id - ?? targetTabManager.tabs.first?.id - ?? targetTabManager.addWorkspace(select: true).id + let targetWorkspace = targetTabManager.selectedWorkspace + ?? targetTabManager.tabs.first + ?? targetTabManager.addWorkspace(select: true) + let targetWorkspaceId = targetWorkspace.id let normalizedDirectoryURL = directoryURL.standardizedFileURL - VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + Task { @MainActor [weak targetTabManager] in + let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( + vscodeApplicationURL: vscodeApplicationURL + ) + guard !Task.isCancelled, let targetTabManager else { return } guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, @@ -7551,7 +7556,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } - guard targetTabManager.openBrowser( + guard targetTabManager.openCodeEditor( inWorkspace: targetWorkspaceId, url: openFolderURL, preferSplitRight: true @@ -7560,10 +7565,150 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } } - return true } + @discardableResult + func openCodeEditor( + tabManager preferredTabManager: TabManager? = nil, + url requestedURL: URL? = nil, + preferSplitRight: Bool = false, + splitDirection: SplitDirection? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + let targetTabManager = preferredTabManager + ?? preferredMainWindowContextForWorkspaceCreation(debugSource: "codeEditor.open.target")?.tabManager + guard let targetTabManager else { + NSSound.beep() + return nil + } + + let targetWorkspace = targetTabManager.selectedWorkspace + ?? targetTabManager.tabs.first + ?? targetTabManager.addWorkspace(select: true) + let targetWorkspaceId = targetWorkspace.id + + func openCodeEditorPanel(url: URL?) -> UUID? { + if let splitDirection { + if targetTabManager.selectedTabId != targetWorkspaceId { + targetTabManager.selectWorkspace(targetWorkspace) + } + return targetTabManager.createCodeEditorSplit(direction: splitDirection, url: url) + } + return targetTabManager.openCodeEditor( + inWorkspace: targetWorkspaceId, + url: url, + preferSplitRight: preferSplitRight, + insertAtEnd: insertAtEnd + ) + } + + if let requestedURL { + return openCodeEditorPanel(url: requestedURL) + } + + func openDefaultCodeEditor() -> UUID? { + openCodeEditorPanel(url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL) + } + + guard let directoryPath = Self.initialCodeEditorDirectoryPath(for: targetWorkspace), + let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL() else { + return openDefaultCodeEditor() + } + + guard let panelId = openDefaultCodeEditor() else { + NSSound.beep() + return nil + } + + Task { @MainActor [weak targetTabManager] in + let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( + vscodeApplicationURL: vscodeApplicationURL + ) + guard !Task.isCancelled, let targetTabManager else { return } + guard let serveWebURL, + let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( + baseWebUIURL: serveWebURL, + directoryPath: directoryPath + ) else { + if Self.discardProvisionalDefaultCodeEditor( + tabManager: targetTabManager, + workspaceId: targetWorkspaceId, + panelId: panelId + ) { + NSSound.beep() + } + return + } + + Self.provisionalDefaultCodeEditor( + tabManager: targetTabManager, + workspaceId: targetWorkspaceId, + panelId: panelId + )? + .navigate(to: openFolderURL) + } + return panelId + } + + static func initialCodeEditorDirectoryPath(for workspace: Workspace) -> String? { + workspace.resolvedWorkingDirectory() + } + + @discardableResult + static func discardProvisionalDefaultCodeEditor( + tabManager: TabManager, + workspaceId: UUID, + panelId: UUID + ) -> Bool { + guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { + return false + } + return discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId) + } + + @discardableResult + static func discardProvisionalDefaultCodeEditor(in workspace: Workspace, panelId: UUID) -> Bool { + guard provisionalDefaultCodeEditor(in: workspace, panelId: panelId) != nil else { + return false + } + return workspace.closePanel(panelId, force: true) + } + + static func provisionalDefaultCodeEditor( + tabManager: TabManager, + workspaceId: UUID, + panelId: UUID + ) -> BrowserPanel? { + guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { + return nil + } + return provisionalDefaultCodeEditor(in: workspace, panelId: panelId) + } + + static func provisionalDefaultCodeEditor(in workspace: Workspace, panelId: UUID) -> BrowserPanel? { + guard let panel = workspace.browserPanel(for: panelId), + shouldDiscardProvisionalDefaultCodeEditor(panel) else { + return nil + } + return panel + } + + static func shouldDiscardProvisionalDefaultCodeEditor(_ panel: BrowserPanel) -> Bool { + guard panel.surfaceRole == .codeEditor else { + return false + } + return isCodeEditorDefaultInitialURL(panel.currentURL) + } + + static func isCodeEditorDefaultInitialURL(_ url: URL?) -> Bool { + guard let url, + let defaultURL = BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL else { + return false + } + return url.absoluteString == defaultURL.absoluteString + } + func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) { guard TerminalDirectoryOpenTarget.vscodeInline.isAvailable() else { NSSound.beep() @@ -14989,6 +15134,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } onExecuted?() return true + case .newCodeEditor: + guard openCodeEditor(tabManager: context.tabManager, insertAtEnd: true) != nil else { + return false + } + onExecuted?() + return true case .splitRight: if shouldSuppressSplitShortcutForTransientTerminalFocusState( direction: .right, diff --git a/Sources/Canvas/WorkspaceCanvasHostView.swift b/Sources/Canvas/WorkspaceCanvasHostView.swift index d5c413325aab..8a97dbe93a9c 100644 --- a/Sources/Canvas/WorkspaceCanvasHostView.swift +++ b/Sources/Canvas/WorkspaceCanvasHostView.swift @@ -68,6 +68,7 @@ struct WorkspaceCanvasHostView: View { switch panelType { case .terminal: return "terminal" case .browser: return "globe" + case .codeEditor: return "curlybraces.square" case .markdown: return "doc.richtext" case .filePreview: return "doc.text.magnifyingglass" case .rightSidebarTool: return "sidebar.right" diff --git a/Sources/ClosedItemHistory.swift b/Sources/ClosedItemHistory.swift index a7ef517a06df..e7baad4900bd 100644 --- a/Sources/ClosedItemHistory.swift +++ b/Sources/ClosedItemHistory.swift @@ -711,6 +711,8 @@ final class ClosedItemHistoryStore: ObservableObject { return String(localized: "menu.history.recentlyClosed.panel.terminal", defaultValue: "Terminal") case .browser: return String(localized: "menu.history.recentlyClosed.panel.browser", defaultValue: "Browser") + case .codeEditor: + return String(localized: "menu.history.recentlyClosed.panel.codeEditor", defaultValue: "Code Editor") case .markdown: return String(localized: "menu.history.recentlyClosed.panel.markdown", defaultValue: "Markdown") case .filePreview: diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift index d3f36a782cf8..3bf483520cab 100644 --- a/Sources/CmuxConfig.swift +++ b/Sources/CmuxConfig.swift @@ -1173,12 +1173,14 @@ struct CmuxSurfaceTabBarButton: Codable, Sendable, Hashable, Identifiable { static let newTerminal = actionReference(CmuxSurfaceTabBarBuiltInAction.newTerminal.configID) static let newBrowser = actionReference(CmuxSurfaceTabBarBuiltInAction.newBrowser.configID) + static let newCodeEditor = actionReference(CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID) static let splitRight = actionReference(CmuxSurfaceTabBarBuiltInAction.splitRight.configID) static let splitDown = actionReference(CmuxSurfaceTabBarBuiltInAction.splitDown.configID) static let defaults: [CmuxSurfaceTabBarButton] = [ .newTerminal, .newBrowser, + .newCodeEditor, .splitRight, .splitDown ] @@ -1563,6 +1565,9 @@ struct CmuxResolvedConfigAction: Identifiable, Sendable, Hashable { case .newBrowser: title = String(localized: "command.newBrowserTab.title", defaultValue: "New Browser Tab") keywords = ["new", "browser", "tab", "surface"] + case .newCodeEditor: + title = String(localized: "command.newCodeEditorTab.title", defaultValue: "New Tab (Code Editor)") + keywords = ["new", "code", "editor", "tab", "surface", "vscode"] case .splitRight: title = String(localized: "command.terminalSplitRight.title", defaultValue: "Split Right") keywords = ["terminal", "split", "right"] @@ -1817,7 +1822,38 @@ struct CmuxSurfaceDefinition: Codable, Sendable { enum CmuxSurfaceType: String, Codable, Sendable { case terminal case browser + case codeEditor = "editor" case project + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + let normalized = rawValue + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "-", with: "") + .replacingOccurrences(of: "_", with: "") + .lowercased() + switch normalized { + case Self.terminal.rawValue: + self = .terminal + case Self.browser.rawValue: + self = .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": + self = .codeEditor + case Self.project.rawValue: + self = .project + default: + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unknown surface type: \(rawValue)" + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } struct CmuxResolvedCommand: Sendable { @@ -2254,6 +2290,7 @@ final class CmuxConfigStore: ObservableObject { }) ?? [ .builtIn(.newTerminal), .builtIn(.newBrowser), + .builtIn(.newCodeEditor), .builtIn(.splitRight), .builtIn(.splitDown) ] diff --git a/Sources/CmuxLifecycleEventPublishing.swift b/Sources/CmuxLifecycleEventPublishing.swift index bfa568049c74..b24b9bca50a1 100644 --- a/Sources/CmuxLifecycleEventPublishing.swift +++ b/Sources/CmuxLifecycleEventPublishing.swift @@ -209,6 +209,8 @@ extension Workspace { return "terminal" case .browser: return "browser" + case .codeEditor: + return "editor" case .markdown: return "markdown" case .filePreview: diff --git a/Sources/CmuxSurfaceTabBarBuiltInAction.swift b/Sources/CmuxSurfaceTabBarBuiltInAction.swift index cc7586e14ea8..cdb031173001 100644 --- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift +++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift @@ -6,6 +6,7 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha case cloudVM = "cmux.cloudvm" case newTerminal = "cmux.newTerminal" case newBrowser = "cmux.newBrowser" + case newCodeEditor = "cmux.newCodeEditor" case splitRight = "cmux.splitRight" case splitDown = "cmux.splitDown" @@ -21,6 +22,8 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha self = .newTerminal case "cmux.newBrowser", "newBrowser": self = .newBrowser + case "cmux.newCodeEditor", "cmux.newEditor", "newCodeEditor", "newEditor", "codeEditor", "editor": + self = .newCodeEditor case "cmux.splitRight", "splitRight": self = .splitRight case "cmux.splitDown", "splitDown": @@ -44,6 +47,8 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha return "terminal" case .newBrowser: return "globe" + case .newCodeEditor: + return "curlybraces.square" case .splitRight: return "square.split.2x1" case .splitDown: @@ -53,7 +58,7 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha var bonsplitAction: BonsplitConfiguration.SplitActionButton.Action? { switch self { - case .newWorkspace, .cloudVM: + case .newWorkspace, .cloudVM, .newCodeEditor: return nil case .newTerminal: return .newTerminal diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 68cd17653a67..8161b233ca56 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -5499,6 +5499,8 @@ struct ContentView: View { return String(localized: "commandPalette.kind.terminal", defaultValue: "Terminal") case .browser: return String(localized: "commandPalette.kind.browser", defaultValue: "Browser") + case .codeEditor: + return String(localized: "commandPalette.kind.codeEditor", defaultValue: "Code Editor") case .markdown: return String(localized: "commandPalette.kind.markdown", defaultValue: "Markdown") case .filePreview: @@ -5520,6 +5522,8 @@ struct ContentView: View { return ["terminal", "shell", "console"] case .browser: return ["browser", "web", "page"] + case .codeEditor: + return ["code", "editor", "vscode", "vs", "cursor"] case .markdown: return ["markdown", "note", "preview"] case .filePreview: @@ -6048,6 +6052,8 @@ struct ContentView: View { return CmuxSurfaceTabBarBuiltInAction.newTerminal.configID case "palette.newBrowserTab": return CmuxSurfaceTabBarBuiltInAction.newBrowser.configID + case "palette.newCodeEditorTab": + return CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID case "palette.terminalSplitRight": return CmuxSurfaceTabBarBuiltInAction.splitRight.configID case "palette.terminalSplitDown": @@ -6412,6 +6418,15 @@ struct ContentView: View { when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } ) ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.newCodeEditorTab", + title: constant(String(localized: "command.newCodeEditorTab.title", defaultValue: "New Tab (Code Editor)")), + subtitle: constant(String(localized: "command.newCodeEditorTab.subtitle", defaultValue: "Tab")), + keywords: ["new", "code", "editor", "tab", "vscode", "vs", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) contributions.append( CommandPaletteCommandContribution( commandId: "palette.closeTab", @@ -7123,6 +7138,24 @@ struct ContentView: View { } ) ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.codeEditorSplitRight", + title: constant(String(localized: "command.codeEditorSplitRight.title", defaultValue: "Split Code Editor Right")), + subtitle: constant(String(localized: "command.codeEditorSplitRight.subtitle", defaultValue: "Code Editor Layout")), + keywords: ["code", "editor", "split", "right", "vscode", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.codeEditorSplitDown", + title: constant(String(localized: "command.codeEditorSplitDown.title", defaultValue: "Split Code Editor Down")), + subtitle: constant(String(localized: "command.codeEditorSplitDown.subtitle", defaultValue: "Code Editor Layout")), + keywords: ["code", "editor", "split", "down", "vscode", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) contributions.append( CommandPaletteCommandContribution( commandId: "palette.browserDuplicateRight", @@ -7610,6 +7643,12 @@ struct ContentView: View { _ = AppDelegate.shared?.openBrowserAndFocusAddressBar() } } + registry.register(commandId: "palette.newCodeEditorTab") { + if executeConfiguredAction(id: CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID) { + return + } + _ = AppDelegate.shared?.openCodeEditor(tabManager: tabManager) + } registry.register(commandId: "palette.closeTab") { tabManager.closeCurrentPanelWithConfirmation() } @@ -7981,6 +8020,18 @@ struct ContentView: View { registry.register(commandId: "palette.browserSplitDown") { _ = tabManager.createBrowserSplit(direction: .down) } + registry.register(commandId: "palette.codeEditorSplitRight") { + guard AppDelegate.shared?.openCodeEditor(tabManager: tabManager, splitDirection: .right) != nil else { + NSSound.beep() + return + } + } + registry.register(commandId: "palette.codeEditorSplitDown") { + guard AppDelegate.shared?.openCodeEditor(tabManager: tabManager, splitDirection: .down) != nil else { + NSSound.beep() + return + } + } registry.register(commandId: "palette.browserDuplicateRight") { let url = tabManager.focusedBrowserPanel?.preferredURLStringForOmnibar().flatMap(URL.init(string:)) _ = tabManager.createBrowserSplit(direction: .right, url: url) @@ -10971,6 +11022,8 @@ struct VerticalTabsSidebar: View { return .terminal case .browser: return .browser + case .codeEditor: + return .codeEditor case .markdown: return .markdown case .filePreview: diff --git a/Sources/KeyboardShortcutContext.swift b/Sources/KeyboardShortcutContext.swift index 6053b66cb1e3..8c53975b09a6 100644 --- a/Sources/KeyboardShortcutContext.swift +++ b/Sources/KeyboardShortcutContext.swift @@ -320,6 +320,7 @@ extension AppDelegate { for manager in shortcutCandidateTabManagers() { for workspace in manager.tabs { if let panel = workspace.browserPanel(for: panelId) { + guard panel.panelType == .browser else { continue } return panel } } @@ -332,6 +333,7 @@ extension AppDelegate { for workspace in manager.tabs { for panel in workspace.panels.values { guard let browserPanel = panel as? BrowserPanel, + browserPanel.panelType == .browser, browserPanel.webView === webView else { continue } diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift index d63f6dab98a8..223de3662a5a 100644 --- a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -258,7 +258,8 @@ extension TabManager { private func browserOpenTabSuggestionSeedSnapshots() -> [BrowserOpenTabSuggestionSnapshot] { tabs.flatMap { workspace in workspace.panels.compactMap { _, panel in - guard let browserPanel = panel as? BrowserPanel else { return nil } + guard let browserPanel = panel as? BrowserPanel, + browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions else { return nil } return BrowserOpenTabSuggestionSnapshot( workspaceId: workspace.id, panelId: browserPanel.id, @@ -272,6 +273,10 @@ extension TabManager { extension Workspace { func publishBrowserOpenTabSuggestion(for browserPanel: BrowserPanel) { + guard browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions else { + owningTabManager?.removeBrowserOpenTabSuggestion(panelId: browserPanel.id) + return + } guard let snapshot = BrowserOpenTabSuggestionSnapshot( workspaceId: id, panelId: browserPanel.id, diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 39b6208e7eca..3eef3ce0b41b 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2464,8 +2464,66 @@ final class BrowserPanel: Panel, ObservableObject { })() """ + enum SurfaceRole: String, Codable, Sendable { + case browser + case codeEditor + + var panelType: PanelType { + switch self { + case .browser: + return .browser + case .codeEditor: + return .codeEditor + } + } + + var displayIcon: String { + switch self { + case .browser: + return "globe" + case .codeEditor: + return "curlybraces.square" + } + } + + var defaultDisplayTitle: String { + switch self { + case .browser: + return String(localized: "browser.newTab", defaultValue: "New tab") + case .codeEditor: + return String(localized: "codeEditor.newTab", defaultValue: "Code Editor") + } + } + + var defaultInitialURL: URL? { + switch self { + case .browser: + return nil + case .codeEditor: + return URL(string: "https://vscode.dev/") + } + } + + var showsBrowserChrome: Bool { + self == .browser + } + + var recordsBrowserHistory: Bool { + self == .browser + } + + var contributesToBrowserOpenTabSuggestions: Bool { + self == .browser + } + + var contributesToGlobalBrowserSearch: Bool { + self == .browser + } + } + let id: UUID - let panelType: PanelType = .browser + let surfaceRole: SurfaceRole + let panelType: PanelType /// The workspace ID this panel belongs to private(set) var workspaceId: UUID @@ -2824,9 +2882,14 @@ final class BrowserPanel: Panel, ObservableObject { return pageTitle } if let url = currentURL { + if surfaceRole == .codeEditor, + let defaultURL = SurfaceRole.codeEditor.defaultInitialURL, + url.absoluteString == defaultURL.absoluteString { + return surfaceRole.defaultDisplayTitle + } return url.host ?? url.absoluteString } - return String(localized: "browser.newTab", defaultValue: "New tab") + return surfaceRole.defaultDisplayTitle } var profileDisplayName: String { @@ -3262,7 +3325,7 @@ final class BrowserPanel: Panel, ObservableObject { } var displayIcon: String? { - "globe" + surfaceRole.displayIcon } var isDirty: Bool { @@ -3442,7 +3505,9 @@ final class BrowserPanel: Panel, ObservableObject { self.publishCommittedURL(from: webView) self.applyMuteState(to: webView, reason: "navigationFinish") self.realignRestoredSessionHistoryToLiveCurrentIfPossible() - boundHistoryStore.recordVisit(url: webView.url, title: webView.title) + if self.surfaceRole.recordsBrowserHistory { + boundHistoryStore.recordVisit(url: webView.url, title: webView.title) + } self.refreshFavicon(from: webView) // Keep find-in-page open through load completion and refresh matches for the new DOM. self.restoreFindStateAfterNavigation(replaySearch: true) @@ -3479,7 +3544,9 @@ final class BrowserPanel: Panel, ObservableObject { currentURL = Self.remoteProxyDisplayURL(for: webView.url) navigationDelegate?.lastAttemptedURL = nil refreshBackgroundAppearance() - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } private func isCurrentWebView(_ candidate: WKWebView, instanceID: UUID? = nil) -> Bool { @@ -3566,13 +3633,15 @@ final class BrowserPanel: Panel, ObservableObject { init( workspaceId: UUID, + surfaceRole: SurfaceRole = .browser, profileID: UUID? = nil, initialURL: URL? = nil, initialRequest: URLRequest? = nil, renderInitialNavigation: Bool = true, preloadInitialNavigationInBackground: Bool = false, bypassInsecureHTTPHostOnce: String? = nil, - omnibarVisible: Bool = true, + useSurfaceRoleDefaultInitialURL: Bool = true, + omnibarVisible: Bool? = nil, transparentBackground: Bool = false, proxyEndpoint: BrowserProxyEndpoint? = nil, bypassRemoteProxy: Bool = false, @@ -3582,7 +3651,13 @@ final class BrowserPanel: Panel, ObservableObject { // Register fallback defaults and normalize legacy/out-of-range settings once // per process, before any setting is read below or by the SwiftUI view. Self.bootstrapBrowserDefaultsIfNeeded() + let resolvedInitialURL = initialURL + ?? (useSurfaceRoleDefaultInitialURL ? surfaceRole.defaultInitialURL : nil) + let resolvedOmnibarVisible = surfaceRole.showsBrowserChrome + && (omnibarVisible ?? surfaceRole.showsBrowserChrome) self.id = UUID() + self.surfaceRole = surfaceRole + self.panelType = surfaceRole.panelType self.workspaceId = workspaceId let requestedProfileID = profileID ?? BrowserProfileStore.shared.effectiveLastUsedProfileID let resolvedProfileID = BrowserProfileStore.shared.profileDefinition(id: requestedProfileID) != nil @@ -3596,7 +3671,7 @@ final class BrowserPanel: Panel, ObservableObject { self.usesRemoteWorkspaceProxy = isRemoteWorkspace && !bypassRemoteProxy self.browserThemeMode = BrowserThemeSettings.mode() self.shouldPreloadInitialNavigationInBackground = preloadInitialNavigationInBackground - self.isOmnibarVisible = omnibarVisible + self.isOmnibarVisible = resolvedOmnibarVisible self.usesTransparentBackground = transparentBackground self.websiteDataStore = isRemoteWorkspace ? WKWebsiteDataStore(forIdentifier: remoteWebsiteDataStoreIdentifier ?? workspaceId) @@ -3749,7 +3824,7 @@ final class BrowserPanel: Panel, ObservableObject { recordTypedNavigation: false ) } - } else if let url = initialURL { + } else if let url = resolvedInitialURL { hiddenWebViewDiscardManager.updateRestoredSessionRenderIntent(nil) currentURL = url shouldRenderWebView = renderInitialNavigation @@ -4182,7 +4257,7 @@ final class BrowserPanel: Panel, ObservableObject { let diffURL = CmuxDiffViewerURLSchemeHandler.diffViewerURL(token: token, requestPath: requestPath) { hiddenWebViewDiscardManager.updateRestoredSessionRenderIntent(snapshot.shouldRenderWebView) setMuted(snapshot.isMuted) - setOmnibarVisible(snapshot.omnibarVisible ?? false) + setOmnibarVisible(surfaceRole.showsBrowserChrome && (snapshot.omnibarVisible ?? false)) currentURL = diffURL let shouldRenderRestoredWebView = snapshot.shouldRenderWebView && BrowserAvailabilitySettings.isEnabled() shouldRenderWebView = shouldRenderRestoredWebView @@ -4202,7 +4277,7 @@ final class BrowserPanel: Panel, ObservableObject { let shouldRenderRestoredWebView = snapshot.shouldRenderWebView && BrowserAvailabilitySettings.isEnabled() hiddenWebViewDiscardManager.updateRestoredSessionRenderIntent(snapshot.shouldRenderWebView) setMuted(snapshot.isMuted) - setOmnibarVisible(snapshot.omnibarVisible ?? true) + setOmnibarVisible(surfaceRole.showsBrowserChrome && (snapshot.omnibarVisible ?? true)) restoreSessionNavigationHistory( backHistoryURLStrings: snapshot.backHistoryURLStrings ?? [], @@ -4290,7 +4365,9 @@ final class BrowserPanel: Panel, ObservableObject { guard !self.isMainFrameProvisionalNavigationActive else { return } self.currentURL = Self.remoteProxyDisplayURL(for: observedURL) self.refreshBackgroundAppearance() - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if self.surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } } webViewObservers.append(urlObserver) @@ -4305,7 +4382,9 @@ final class BrowserPanel: Panel, ObservableObject { let trimmed = (webView.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } self.pageTitle = trimmed - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if self.surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } } webViewObservers.append(titleObserver) @@ -5858,7 +5937,8 @@ extension BrowserPanel { initialRequest: seed.initialRequest, focus: true, preferredProfileID: profileID, - bypassInsecureHTTPHostOnce: seed.bypassInsecureHTTPHostOnce + bypassInsecureHTTPHostOnce: seed.bypassInsecureHTTPHostOnce, + surfaceRole: surfaceRole ) else { #if DEBUG cmuxDebugLog("browser.newTab.open.abort panel=\(id.uuidString.prefix(5)) reason=newPanelFailed") diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 21688edc9b44..f92906e41c59 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -617,7 +617,7 @@ struct BrowserPanelView: View { } private var hasVisibleOmnibarSuggestions: Bool { - panel.isOmnibarVisible && addressBarFocused && hasActionableOmnibarSuggestions && omnibarPillFrame.width > 0 + shouldShowBrowserChrome && addressBarFocused && hasActionableOmnibarSuggestions && omnibarPillFrame.width > 0 } private var shouldRenderOmnibarSuggestionsInPortal: Bool { @@ -1158,12 +1158,16 @@ struct BrowserPanelView: View { @ViewBuilder private var omnibarHeaderView: some View { - if panel.isOmnibarVisible { + if shouldShowBrowserChrome { addressBar .fixedSize(horizontal: false, vertical: true) } } + private var shouldShowBrowserChrome: Bool { + panel.surfaceRole.showsBrowserChrome && panel.isOmnibarVisible + } + private var browserPanelBaseView: some View { // Layering contract: browser find UI is mounted in the portal-hosted AppKit // container. Rendering it here can hide it behind the portal-hosted WKWebView. @@ -1883,7 +1887,7 @@ struct BrowserPanelView: View { ) }, omnibarSuggestions: portalOmnibarSuggestions, - paneTopChromeHeight: panel.isOmnibarVisible ? addressBarHeight : 0 + paneTopChromeHeight: shouldShowBrowserChrome ? addressBarHeight : 0 ) .accessibilityIdentifier("BrowserWebViewSurface") // Keep the host stable for normal pane churn, but force a remount when @@ -2421,7 +2425,7 @@ struct BrowserPanelView: View { } private func autoFocusOmnibarIfBlank() { - guard panel.isOmnibarVisible else { + guard shouldShowBrowserChrome else { #if DEBUG logBrowserFocusState(event: "addressBarFocus.autoFocus.skip", detail: "reason=omnibar_hidden") #endif diff --git a/Sources/Panels/Panel.swift b/Sources/Panels/Panel.swift index a344302d3ef9..470f224bf5e5 100644 --- a/Sources/Panels/Panel.swift +++ b/Sources/Panels/Panel.swift @@ -6,6 +6,7 @@ import AppKit public enum PanelType: String, Codable, Sendable { case terminal case browser + case codeEditor = "editor" case markdown case filePreview = "filepreview" case rightSidebarTool @@ -13,25 +14,44 @@ public enum PanelType: String, Codable, Sendable { case project case extensionBrowser + public init?(externalValue rawValue: String) { + let normalized = rawValue + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "-", with: "") + .replacingOccurrences(of: "_", with: "") + .replacingOccurrences(of: " ", with: "") + .lowercased() + switch normalized { + case Self.terminal.rawValue: + self = .terminal + case Self.browser.rawValue: + self = .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": + self = .codeEditor + case Self.markdown.rawValue: + self = .markdown + case Self.filePreview.rawValue: + self = .filePreview + case "rightsidebartool": + self = .rightSidebarTool + case "agentsession": + self = .agentSession + case Self.project.rawValue: + self = .project + case "extensionbrowser": + self = .extensionBrowser + default: + return nil + } + } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) - if let type = Self(rawValue: rawValue) { + if let type = Self(externalValue: rawValue) { self = type return } - if rawValue.lowercased() == Self.filePreview.rawValue { - self = .filePreview - return - } - if rawValue.lowercased() == Self.rightSidebarTool.rawValue.lowercased() { - self = .rightSidebarTool - return - } - if rawValue.lowercased() == Self.agentSession.rawValue.lowercased() { - self = .agentSession - return - } throw DecodingError.dataCorruptedError( in: container, debugDescription: "Unknown panel type: \(rawValue)" diff --git a/Sources/Panels/PanelContentView.swift b/Sources/Panels/PanelContentView.swift index 2016d4d70189..c9218fe737b9 100644 --- a/Sources/Panels/PanelContentView.swift +++ b/Sources/Panels/PanelContentView.swift @@ -50,7 +50,7 @@ struct PanelContentView: View { onTriggerFlash: onTriggerFlash ) } - case .browser: + case .browser, .codeEditor: if let browserPanel = panel as? BrowserPanel { BrowserPanelView( panel: browserPanel, @@ -139,7 +139,7 @@ struct PanelContentView: View { switch panel.panelType { case .markdown, .filePreview, .rightSidebarTool, .agentSession, .project, .extensionBrowser: return true - case .terminal, .browser: + case .terminal, .browser, .codeEditor: return false } } diff --git a/Sources/Search/GlobalSearchDocuments.swift b/Sources/Search/GlobalSearchDocuments.swift index 3aff52a45eaf..af919823b958 100644 --- a/Sources/Search/GlobalSearchDocuments.swift +++ b/Sources/Search/GlobalSearchDocuments.swift @@ -34,7 +34,7 @@ enum GlobalSearchDocuments { kind = .browser case .markdown: kind = .markdown - case .terminal, .filePreview, .rightSidebarTool, .agentSession, .project, .extensionBrowser: + case .terminal, .codeEditor, .filePreview, .rightSidebarTool, .agentSession, .project, .extensionBrowser: kind = .title } diff --git a/Sources/Search/GlobalSearchPanelCaptureManager.swift b/Sources/Search/GlobalSearchPanelCaptureManager.swift index 41e5d1343eca..4362cbe7ab98 100644 --- a/Sources/Search/GlobalSearchPanelCaptureManager.swift +++ b/Sources/Search/GlobalSearchPanelCaptureManager.swift @@ -36,12 +36,18 @@ final class GlobalSearchPanelCaptureManager { #endif } } - } else if let browserPanel = context.panel as? BrowserPanel { + } else if let browserPanel = context.panel as? BrowserPanel, + browserPanel.surfaceRole.contributesToGlobalBrowserSearch { captureBrowserPanel(browserPanel) } } func captureBrowserPanel(_ panel: BrowserPanel) { + guard panel.surfaceRole.contributesToGlobalBrowserSearch else { + cancelBrowserCapture(forPanelID: panel.id) + return + } + let panelID = panel.id let taskID = UUID() cancelPanelPurge(panelID) @@ -202,6 +208,7 @@ final class GlobalSearchPanelCaptureManager { } private func indexBrowserPanel(_ panel: BrowserPanel) async { + guard panel.surfaceRole.contributesToGlobalBrowserSearch else { return } guard let context = AppDelegate.shared?.globalSearchContext( forPanelID: panel.id, preferredWorkspaceID: panel.workspaceId diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index f21ee16d6e4f..0361db0b73bf 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -2915,8 +2915,10 @@ class TabManager: ObservableObject { /// Returns the focused panel if it's a BrowserPanel, nil otherwise var focusedBrowserPanel: BrowserPanel? { guard let tab = selectedWorkspace, - let panelId = tab.focusedPanelId else { return nil } - return tab.panels[panelId] as? BrowserPanel + let panelId = tab.focusedPanelId, + let panel = tab.panels[panelId] as? BrowserPanel, + panel.panelType == .browser else { return nil } + return panel } /// Returns the focused panel if it's a MarkdownPanel showing the rendered @@ -3696,6 +3698,23 @@ class TabManager: ObservableObject { ) } + /// Create a new code editor split from the currently focused panel. + @discardableResult + func createCodeEditorSplit(direction: SplitDirection, url: URL? = nil) -> UUID? { + guard let selectedTabId, + let tab = tabs.first(where: { $0.id == selectedTabId }), + let focusedPanelId = tab.focusedPanelId else { return nil } + tab.clearSplitZoom() + return newBrowserSplit( + tabId: selectedTabId, + fromPanelId: focusedPanelId, + orientation: direction.orientation, + insertFirst: direction.insertFirst, + url: url, + surfaceRole: .codeEditor + ) + } + /// Refresh Bonsplit right-side action button tooltips for all workspaces. func refreshSplitButtonTooltips() { for workspace in tabs { @@ -3894,6 +3913,7 @@ class TabManager: ObservableObject { url: URL? = nil, preferredProfileID: UUID? = nil, focus: Bool = true, + surfaceRole: BrowserPanel.SurfaceRole = .browser, initialDividerPosition: CGFloat? = nil ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } @@ -3905,40 +3925,80 @@ class TabManager: ObservableObject { url: url, preferredProfileID: preferredProfileID, focus: focus, + surfaceRole: surfaceRole, initialDividerPosition: initialDividerPosition )?.id } + func newCodeEditorSplit( + tabId: UUID, + fromPanelId: UUID, + orientation: SplitOrientation, + insertFirst: Bool = false, + url: URL? = nil, + preferredProfileID: UUID? = nil, + focus: Bool = true, + initialDividerPosition: CGFloat? = nil + ) -> UUID? { + newBrowserSplit( + tabId: tabId, + fromPanelId: fromPanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + preferredProfileID: preferredProfileID, + focus: focus, + surfaceRole: .codeEditor, + initialDividerPosition: initialDividerPosition + ) + } + /// Create a new browser surface in a pane func newBrowserSurface( tabId: UUID, inPane paneId: PaneID, url: URL? = nil, - preferredProfileID: UUID? = nil + preferredProfileID: UUID? = nil, + surfaceRole: BrowserPanel.SurfaceRole = .browser ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil } return tab.newBrowserSurface( inPane: paneId, url: url, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole )?.id } + func newCodeEditorSurface( + tabId: UUID, + inPane paneId: PaneID, + url: URL? = nil, + preferredProfileID: UUID? = nil + ) -> UUID? { + newBrowserSurface( + tabId: tabId, + inPane: paneId, + url: url, + preferredProfileID: preferredProfileID, + surfaceRole: .codeEditor + ) + } + /// Get a browser panel by ID func browserPanel(tabId: UUID, panelId: UUID) -> BrowserPanel? { guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil } return tab.browserPanel(for: panelId) } - /// Open a browser in a specific workspace, optionally preferring a split-right layout. - @discardableResult - func openBrowser( + private func openBrowserSurfaceRole( + _ surfaceRole: BrowserPanel.SurfaceRole, inWorkspace tabId: UUID, - url: URL? = nil, - preferSplitRight: Bool = false, - preferredProfileID: UUID? = nil, - insertAtEnd: Bool = false + url: URL?, + preferSplitRight: Bool, + preferredProfileID: UUID?, + insertAtEnd: Bool ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } guard let workspace = tabs.first(where: { $0.id == tabId }) else { return nil } @@ -3948,15 +4008,16 @@ class TabManager: ObservableObject { if preferSplitRight { if let targetPaneId = workspace.topRightBrowserReusePane(), - let browserPanel = workspace.newBrowserSurface( + let panel = workspace.newBrowserSurface( inPane: targetPaneId, url: url, focus: true, insertAtEnd: insertAtEnd, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole ) { - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id } let splitSourcePanelId: UUID? = { @@ -3975,30 +4036,70 @@ class TabManager: ObservableObject { }() if let splitSourcePanelId, - let browserPanel = workspace.newBrowserSplit( + let panel = workspace.newBrowserSplit( from: splitSourcePanelId, orientation: .horizontal, url: url, preferredProfileID: preferredProfileID, - focus: true + focus: true, + surfaceRole: surfaceRole ) { - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id } } guard let paneId = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first, - let browserPanel = workspace.newBrowserSurface( + let panel = workspace.newBrowserSurface( inPane: paneId, url: url, focus: true, insertAtEnd: insertAtEnd, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole ) else { return nil } - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id + } + + /// Open a browser in a specific workspace, optionally preferring a split-right layout. + @discardableResult + func openBrowser( + inWorkspace tabId: UUID, + url: URL? = nil, + preferSplitRight: Bool = false, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + openBrowserSurfaceRole( + .browser, + inWorkspace: tabId, + url: url, + preferSplitRight: preferSplitRight, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) + } + + /// Open a code editor in a specific workspace, optionally preferring a split-right layout. + @discardableResult + func openCodeEditor( + inWorkspace tabId: UUID, + url: URL? = nil, + preferSplitRight: Bool = false, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + openBrowserSurfaceRole( + .codeEditor, + inWorkspace: tabId, + url: url, + preferSplitRight: preferSplitRight, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) } /// Open a browser in the currently focused pane (as a new surface) @@ -4018,6 +4119,23 @@ class TabManager: ObservableObject { ) } + /// Open a code editor in the currently focused pane (as a new surface). + @discardableResult + func openCodeEditor( + url: URL? = nil, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + guard let tabId = selectedTabId else { return nil } + return openCodeEditor( + inWorkspace: tabId, + url: url, + preferSplitRight: false, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) + } + /// Reopen the most recently closed browser panel (Cmd+Shift+T). /// No-op when no browser panel restore snapshot is available. @discardableResult diff --git a/Sources/TaskManagerSnapshot.swift b/Sources/TaskManagerSnapshot.swift index b85e302d0fff..3045ea077812 100644 --- a/Sources/TaskManagerSnapshot.swift +++ b/Sources/TaskManagerSnapshot.swift @@ -446,6 +446,16 @@ struct CmuxTaskManagerSnapshot { let title = nonEmptyString(surface["title"]) ?? displayHandle(surface) let surfaceId = uuid(surface["id"]) let terminalSurfaceId = type == "terminal" ? surfaceId : nil + let surfaceKind: CmuxTaskManagerRow.Kind = { + switch type { + case "browser": + return .browserSurface + case "editor", "code_editor", "codeeditor": + return .codeEditorSurface + default: + return .terminalSurface + } + }() var detailParts = [surfaceTypeLabel(type)] if bool(surface["selected"]) { detailParts.append(String(localized: "taskManager.row.selected", defaultValue: "Selected")) @@ -458,7 +468,7 @@ struct CmuxTaskManagerSnapshot { } rows.append(row( surface, - kind: type == "browser" ? .browserSurface : .terminalSurface, + kind: surfaceKind, level: 3, title: title, detail: detailParts.joined(separator: " / "), @@ -475,7 +485,7 @@ struct CmuxTaskManagerSnapshot { } } let processes = surface["processes"] as? [[String: Any]] ?? [] - let context = rowID(surface, kind: type == "browser" ? .browserSurface : .terminalSurface) + let context = rowID(surface, kind: surfaceKind) for process in processes { appendProcess( process, @@ -655,6 +665,8 @@ struct CmuxTaskManagerSnapshot { switch type { case "browser": return String(localized: "taskManager.row.surfaceType.browser", defaultValue: "Browser") + case "editor", "code_editor", "codeeditor": + return String(localized: "taskManager.row.surfaceType.codeEditor", defaultValue: "Code Editor") case "terminal": return String(localized: "taskManager.row.surfaceType.terminal", defaultValue: "Terminal") case "unknown", "": diff --git a/Sources/TaskManagerTypes.swift b/Sources/TaskManagerTypes.swift index f3818d4bc07c..e1ee4f4f436b 100644 --- a/Sources/TaskManagerTypes.swift +++ b/Sources/TaskManagerTypes.swift @@ -10,6 +10,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case pane case terminalSurface case browserSurface + case codeEditorSurface case webview case process case programAggregate @@ -24,6 +25,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case .pane: return "square.split.2x1" case .terminalSurface: return "terminal" case .browserSurface: return "globe" + case .codeEditorSurface: return "curlybraces.square" case .webview: return "network" case .process: return "gearshape" case .programAggregate: return "gearshape.2" @@ -40,6 +42,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case .pane: return .secondary case .terminalSurface: return .green case .browserSurface: return .blue + case .codeEditorSurface: return .teal case .webview: return .purple case .process: return .secondary case .programAggregate: return .accentColor diff --git a/Sources/TerminalController+ControlPaneContext.swift b/Sources/TerminalController+ControlPaneContext.swift index 7e824098069b..8a232b653ad0 100644 --- a/Sources/TerminalController+ControlPaneContext.swift +++ b/Sources/TerminalController+ControlPaneContext.swift @@ -188,7 +188,7 @@ extension TerminalController: ControlPaneContext { return .agentSessionRejected(typeRawValue: panelType.rawValue) } let url = inputs.urlRaw.flatMap { URL(string: $0) } - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return browserDisabledCreateResolution(rawURL: inputs.urlRaw, url: url, tabManager: tabManager) } @@ -239,6 +239,16 @@ extension TerminalController: ControlPaneContext { creationPolicy: .automationPreload, initialDividerPosition: initialDividerPosition.map { CGFloat($0) } )?.id + } else if panelType == .codeEditor { + newPanelId = ws.newCodeEditorSplit( + from: sourcePanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload, + initialDividerPosition: initialDividerPosition.map { CGFloat($0) } + )?.id } else { switch ws.newTerminalSplitOutcome( from: sourcePanelId, @@ -287,6 +297,8 @@ extension TerminalController: ControlPaneContext { return .terminal case "browser": return .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": + return .codeEditor case "markdown": return .markdown case "filepreview": diff --git a/Sources/TerminalController+ControlSidebarContext3.swift b/Sources/TerminalController+ControlSidebarContext3.swift index a970fef6e714..3e2c051c3f7f 100644 --- a/Sources/TerminalController+ControlSidebarContext3.swift +++ b/Sources/TerminalController+ControlSidebarContext3.swift @@ -174,6 +174,7 @@ extension TerminalController { func controlSidebarCreatePaneSplit( isBrowser: Bool, + isCodeEditor: Bool, orientationIsHorizontal: Bool, insertFirst: Bool, url: URL? @@ -200,6 +201,19 @@ extension TerminalController { } return .created(id) } + if isCodeEditor { + guard let id = tab.newCodeEditorSplit( + from: focusedPanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id else { + return .failed + } + return .created(id) + } if tab.isRemoteTmuxMirror, insertFirst { // Routed tmux `split-window` cannot insert before the target // pane; reject before mutating the remote session. @@ -222,7 +236,12 @@ extension TerminalController { // MARK: - New / close surface - func controlSidebarNewSurface(isBrowser: Bool, paneArg: String?, url: URL?) -> ControlSidebarNewSurfaceResolution { + func controlSidebarNewSurface( + isBrowser: Bool, + isCodeEditor: Bool, + paneArg: String?, + url: URL? + ) -> ControlSidebarNewSurfaceResolution { let focus = Self.socketCommandAllowsInAppFocusMutations() guard let tabManager, let tabId = tabManager.selectedTabId, @@ -260,6 +279,17 @@ extension TerminalController { } return .created(id) } + if isCodeEditor { + guard let id = tab.newCodeEditorSurface( + inPane: targetPaneId, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id else { + return .failed + } + return .created(id) + } switch tab.newTerminalSurfaceOutcome( inPane: targetPaneId, focus: focus, diff --git a/Sources/TerminalController+ControlSurfaceContext2.swift b/Sources/TerminalController+ControlSurfaceContext2.swift index 8818c09a7586..a9b050f1f9ae 100644 --- a/Sources/TerminalController+ControlSurfaceContext2.swift +++ b/Sources/TerminalController+ControlSurfaceContext2.swift @@ -109,7 +109,7 @@ extension TerminalController { return .agentSessionRejected(typeRawValue: panelType.rawValue) } let url = inputs.urlRaw.flatMap { URL(string: $0) } - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return .browserDisabled(surfaceBrowserDisabledOutcome( rawURL: inputs.urlRaw, url: url, @@ -167,6 +167,16 @@ extension TerminalController { creationPolicy: .automationPreload, initialDividerPosition: dividerPosition )?.id + } else if panelType == .codeEditor { + newId = ws.newCodeEditorSplit( + from: targetSurfaceId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload, + initialDividerPosition: dividerPosition + )?.id } else { switch ws.newTerminalSplitOutcome( from: targetSurfaceId, @@ -303,7 +313,7 @@ extension TerminalController { } let url = inputs.urlRaw.flatMap { URL(string: $0) } - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return .browserDisabled(surfaceBrowserDisabledOutcome( rawURL: inputs.urlRaw, url: url, @@ -349,6 +359,13 @@ extension TerminalController { focus: focus, creationPolicy: .automationPreload )?.id + } else if panelType == .codeEditor { + newPanelId = ws.newCodeEditorSurface( + inPane: paneId, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id } else if panelType == .agentSession { newPanelId = ws.newAgentSessionSurface( inPane: paneId, @@ -451,6 +468,7 @@ extension TerminalController { switch v2NormalizedToken(raw) { case "terminal": return .terminal case "browser": return .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": return .codeEditor case "markdown": return .markdown case "filepreview": return .filePreview case "rightsidebartool": return .rightSidebarTool diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index a734a352c076..b76a0b3b10d3 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -10150,8 +10150,8 @@ class TerminalController { Split & surface commands: new_split [panel] - Split panel (left/right/up/down) drag_surface_to_split - Move surface into a new split (drag-to-edge) - new_pane [--type=terminal|browser] [--direction=left|right|up|down] [--url=...] - new_surface [--type=terminal|browser] [--pane=] [--url=...] + new_pane [--type=terminal|browser|editor] [--direction=left|right|up|down] [--url=...] + new_surface [--type=terminal|browser|editor] [--pane=] [--url=...] list_surfaces [workspace] - List surfaces for workspace (current if omitted) list_panes - List all panes with IDs list_pane_surfaces [--pane=] - List surfaces in pane diff --git a/Sources/TerminalControllerV2ParamParsingSupport.swift b/Sources/TerminalControllerV2ParamParsingSupport.swift index bd82a81ea159..bf2e7be29f3e 100644 --- a/Sources/TerminalControllerV2ParamParsingSupport.swift +++ b/Sources/TerminalControllerV2ParamParsingSupport.swift @@ -183,22 +183,7 @@ extension TerminalController { nonisolated func v2PanelType(_ params: [String: Any], _ key: String) -> PanelType? { guard let s = v2String(params, key) else { return nil } - switch v2NormalizedToken(s) { - case "terminal": - return .terminal - case "browser": - return .browser - case "markdown": - return .markdown - case "filepreview": - return .filePreview - case "rightsidebartool": - return .rightSidebarTool - case "agentsession": - return .agentSession - default: - return nil - } + return PanelType(externalValue: s) } nonisolated func v2NormalizedToken(_ raw: String) -> String { diff --git a/Sources/TerminalPaneDropTargetView.swift b/Sources/TerminalPaneDropTargetView.swift index d898bf806685..c12d21dbf673 100644 --- a/Sources/TerminalPaneDropTargetView.swift +++ b/Sources/TerminalPaneDropTargetView.swift @@ -311,7 +311,7 @@ final class PaneDropTargetView: NSView { switch panel.panelType { case .terminal: return .terminal - case .browser: + case .browser, .codeEditor: return nil case .filePreview: guard let filePreviewPanel = panel as? FilePreviewPanel, diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index e3e3a1d3aaf7..5c088eb4fef0 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -529,7 +529,7 @@ extension Workspace { rightSidebarToolSnapshot = nil agentSessionSnapshot = nil projectSnapshot = nil - case .browser: + case .browser, .codeEditor: guard let browserPanel = panel as? BrowserPanel else { return nil } guard browserPanel.shouldPersistSessionSnapshot() else { return nil } terminalSnapshot = nil @@ -1354,6 +1354,18 @@ extension Workspace { } applySessionPanelMetadata(snapshot, toPanelId: browserPanel.id) return browserPanel.id + case .codeEditor: + guard let editorPanel = newCodeEditorSurface( + inPane: paneId, + url: nil, + focus: false, + preferredProfileID: snapshot.browser?.profileID, + creationPolicy: .restoration + ) else { + return nil + } + applySessionPanelMetadata(snapshot, toPanelId: editorPanel.id) + return editorPanel.id case .markdown: guard let filePath = snapshot.markdown?.filePath, let markdownPanel = newMarkdownSurface( @@ -1699,6 +1711,19 @@ extension Workspace { if surface.focus == true { focusPanelId = panel.id } } + case .codeEditor: + let url = surface.url.flatMap { URL(string: $0) } + if let panel = newCodeEditorSurface( + inPane: paneId, + url: url ?? BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + focus: false, + creationPolicy: .restoration + ) { + _ = closePanel(panelId, force: true) + if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } + if surface.focus == true { focusPanelId = panel.id } + } + case .project: if let panel = newProjectSurface( inPane: paneId, @@ -1744,6 +1769,18 @@ extension Workspace { if surface.focus == true { focusPanelId = panel.id } } + case .codeEditor: + let url = surface.url.flatMap { URL(string: $0) } + if let panel = newCodeEditorSurface( + inPane: paneId, + url: url ?? BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + focus: false, + creationPolicy: .restoration + ) { + if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } + if surface.focus == true { focusPanelId = panel.id } + } + case .project: if let panel = newProjectSurface( inPane: paneId, @@ -3506,7 +3543,9 @@ final class Workspace: Identifiable, ObservableObject { guard let self = self, let browserPanel = browserPanel, let tabId = self.surfaceIdFromPanelId(browserPanel.id) else { return } - self.publishBrowserOpenTabSuggestion(for: browserPanel) + if browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions { + self.publishBrowserOpenTabSuggestion(for: browserPanel) + } guard let existing = self.bonsplitController.tab(tabId) else { return } let nextTitle = browserPanel.displayTitle if self.panelTitles[browserPanel.id] != nextTitle { @@ -3528,7 +3567,9 @@ final class Workspace: Identifiable, ObservableObject { ) } panelSubscriptions[browserPanel.id] = subscription - publishBrowserOpenTabSuggestion(for: browserPanel) + if browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions { + publishBrowserOpenTabSuggestion(for: browserPanel) + } setPreferredBrowserProfileID(browserPanel.profileID) } @@ -3569,6 +3610,47 @@ final class Workspace: Identifiable, ObservableObject { return BrowserProfileStore.shared.effectiveLastUsedProfileID } + private func browserSurfaceKind(for role: BrowserPanel.SurfaceRole) -> String { + switch role { + case .browser: + return SurfaceKind.browser.rawValue + case .codeEditor: + return SurfaceKind.codeEditor.rawValue + } + } + + private func browserLifecycleKind(for role: BrowserPanel.SurfaceRole) -> String { + switch role { + case .browser: + return "browser" + case .codeEditor: + return "editor" + } + } + + private func browserLifecycleOrigin(for role: BrowserPanel.SurfaceRole, suffix: String) -> String { + switch role { + case .browser: + return "browser_\(suffix)" + case .codeEditor: + return "editor_\(suffix)" + } + } + + private func initialURLForBrowserRole( + _ role: BrowserPanel.SurfaceRole, + requestedURL: URL?, + creationPolicy: BrowserPanelCreationPolicy + ) -> URL? { + if let requestedURL { + return requestedURL + } + guard creationPolicy != .restoration else { + return nil + } + return role.defaultInitialURL + } + private func installMarkdownPanelSubscription(_ markdownPanel: MarkdownPanel) { let subscription = Publishers.CombineLatest( markdownPanel.$displayTitle.removeDuplicates(), @@ -3736,6 +3818,8 @@ final class Workspace: Identifiable, ObservableObject { return SurfaceKind.terminal.rawValue case .browser: return SurfaceKind.browser.rawValue + case .codeEditor: + return SurfaceKind.codeEditor.rawValue case .markdown: return SurfaceKind.markdown.rawValue case .filePreview: @@ -7463,6 +7547,7 @@ final class Workspace: Identifiable, ObservableObject { url: URL? = nil, preferredProfileID: UUID? = nil, focus: Bool = true, + surfaceRole: BrowserPanel.SurfaceRole = .browser, creationPolicy: BrowserPanelCreationPolicy = .userInitiated, omnibarVisible: Bool = true, transparentBackground: Bool = false, @@ -7474,8 +7559,8 @@ final class Workspace: Identifiable, ObservableObject { if isRemoteTmuxMirror { return nil } let browserEnabled = BrowserAvailabilitySettings.isEnabled() guard browserEnabled || creationPolicy.permitsCreationWhenBrowserDisabled else { - if let url { - _ = NSWorkspace.shared.open(url) + if let externalURL = initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy) { + _ = NSWorkspace.shared.open(externalURL) } return nil } @@ -7492,18 +7577,21 @@ final class Workspace: Identifiable, ObservableObject { } guard let paneId = sourcePaneId else { return nil } + let effectiveOmnibarVisible = surfaceRole.showsBrowserChrome && omnibarVisible // Create browser panel let browserPanel = BrowserPanel( workspaceId: id, + surfaceRole: surfaceRole, profileID: resolvedNewBrowserProfileID( preferredProfileID: preferredProfileID, sourcePanelId: panelId ), - initialURL: url, + initialURL: initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy), renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, - omnibarVisible: omnibarVisible, + useSurfaceRoleDefaultInitialURL: creationPolicy != .restoration, + omnibarVisible: effectiveOmnibarVisible, transparentBackground: transparentBackground, proxyEndpoint: remoteProxyEndpoint, bypassRemoteProxy: bypassRemoteProxy, @@ -7518,7 +7606,7 @@ final class Workspace: Identifiable, ObservableObject { let newTab = Bonsplit.Tab( title: browserPanel.displayTitle, icon: browserPanel.displayIcon, - kind: SurfaceKind.browser.rawValue, + kind: browserSurfaceKind(for: surfaceRole), isDirty: browserPanel.isDirty, isLoading: browserPanel.isLoading, isAudioMuted: browserPanel.isMuted, @@ -7539,7 +7627,15 @@ final class Workspace: Identifiable, ObservableObject { } applyInitialSplitDividerPosition(initialDividerPosition, sourcePaneId: paneId, newPaneId: newPaneId) setPreferredBrowserProfileID(browserPanel.profileID) - publishCmuxSplitCreated(newPaneId, sourcePaneId: paneId, orientation: orientation, surfaceId: browserPanel.id, kind: "browser", origin: "browser_split", focused: focus) + publishCmuxSplitCreated( + newPaneId, + sourcePaneId: paneId, + orientation: orientation, + surfaceId: browserPanel.id, + kind: browserLifecycleKind(for: surfaceRole), + origin: browserLifecycleOrigin(for: surfaceRole, suffix: "split"), + focused: focus + ) // See newTerminalSplit: suppress old view's becomeFirstResponder during reparenting. let previousHostedView = focusedTerminalPanel?.hostedView @@ -7577,6 +7673,7 @@ final class Workspace: Identifiable, ObservableObject { insertAtEnd: Bool = false, preferredProfileID: UUID? = nil, bypassInsecureHTTPHostOnce: String? = nil, + surfaceRole: BrowserPanel.SurfaceRole = .browser, creationPolicy: BrowserPanelCreationPolicy = .userInitiated, omnibarVisible: Bool = true, transparentBackground: Bool = false, @@ -7589,7 +7686,8 @@ final class Workspace: Identifiable, ObservableObject { if isRemoteTmuxMirror { return nil } let browserEnabled = BrowserAvailabilitySettings.isEnabled() guard browserEnabled || creationPolicy.permitsCreationWhenBrowserDisabled else { - if let externalURL = url ?? initialRequest?.url { + if let externalURL = initialRequest?.url + ?? initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy) { _ = NSWorkspace.shared.open(externalURL) } return nil @@ -7599,19 +7697,22 @@ final class Workspace: Identifiable, ObservableObject { let sourcePanelId = effectiveSelectedPanelId(inPane: paneId) let previousFocusedPanelId = focusedPanelId let previousHostedView = focusedTerminalPanel?.hostedView + let effectiveOmnibarVisible = surfaceRole.showsBrowserChrome && omnibarVisible let browserPanel = BrowserPanel( workspaceId: id, + surfaceRole: surfaceRole, profileID: resolvedNewBrowserProfileID( preferredProfileID: preferredProfileID, sourcePanelId: sourcePanelId ), - initialURL: url, + initialURL: initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy), initialRequest: initialRequest, renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, bypassInsecureHTTPHostOnce: bypassInsecureHTTPHostOnce, - omnibarVisible: omnibarVisible, + useSurfaceRoleDefaultInitialURL: creationPolicy != .restoration, + omnibarVisible: effectiveOmnibarVisible, transparentBackground: transparentBackground, proxyEndpoint: remoteProxyEndpoint, bypassRemoteProxy: bypassRemoteProxy, @@ -7625,7 +7726,7 @@ final class Workspace: Identifiable, ObservableObject { guard let newTabId = bonsplitController.createTab( title: browserPanel.displayTitle, icon: browserPanel.displayIcon, - kind: SurfaceKind.browser.rawValue, + kind: browserSurfaceKind(for: surfaceRole), isDirty: browserPanel.isDirty, isLoading: browserPanel.isLoading, isAudioMuted: browserPanel.isMuted, @@ -7645,7 +7746,13 @@ final class Workspace: Identifiable, ObservableObject { let targetIndex = max(0, bonsplitController.tabs(inPane: paneId).count - 1) _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) } - publishCmuxSurfaceCreated(browserPanel.id, paneId: paneId, kind: "browser", origin: "browser_tab", focused: shouldFocusNewTab) + publishCmuxSurfaceCreated( + browserPanel.id, + paneId: paneId, + kind: browserLifecycleKind(for: surfaceRole), + origin: browserLifecycleOrigin(for: surfaceRole, suffix: "tab"), + focused: shouldFocusNewTab + ) // Match terminal behavior: enforce deterministic selection + focus. if shouldFocusNewTab { @@ -7670,6 +7777,67 @@ final class Workspace: Identifiable, ObservableObject { return browserPanel } + @discardableResult + func newCodeEditorSplit( + from panelId: UUID, + orientation: SplitOrientation, + insertFirst: Bool = false, + url: URL? = nil, + preferredProfileID: UUID? = nil, + focus: Bool = true, + creationPolicy: BrowserPanelCreationPolicy = .userInitiated, + initialDividerPosition: CGFloat? = nil + ) -> BrowserPanel? { + newBrowserSplit( + from: panelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + preferredProfileID: preferredProfileID, + focus: focus, + surfaceRole: .codeEditor, + creationPolicy: creationPolicy, + omnibarVisible: false, + initialDividerPosition: initialDividerPosition + ) + } + + @discardableResult + func newCodeEditorSurface( + inPane paneId: PaneID, + url: URL? = nil, + focus: Bool? = nil, + insertAtEnd: Bool = false, + preferredProfileID: UUID? = nil, + creationPolicy: BrowserPanelCreationPolicy = .userInitiated + ) -> BrowserPanel? { + newBrowserSurface( + inPane: paneId, + url: url, + focus: focus, + insertAtEnd: insertAtEnd, + preferredProfileID: preferredProfileID, + surfaceRole: .codeEditor, + creationPolicy: creationPolicy, + omnibarVisible: false + ) + } + + @discardableResult + func openCodeEditorFromPane(_ pane: PaneID) -> UUID? { + if let owningTabManager, owningTabManager.selectedTabId != id { + owningTabManager.selectWorkspace(self) + } + bonsplitController.focusPane(pane) + if let selectedTab = bonsplitController.selectedTab(inPane: pane) { + applyTabSelection(tabId: selectedTab.id, inPane: pane) + } + if let panelId = AppDelegate.shared?.openCodeEditor(tabManager: owningTabManager) { + return panelId + } + return newCodeEditorSurface(inPane: pane, focus: true)?.id + } + /// Creates a sidebar extension browser tab in the requested pane and returns its panel. /// /// - Parameters: @@ -8576,6 +8744,7 @@ final class Workspace: Identifiable, ObservableObject { } guard let panelId = panelIdFromSurfaceId(tab.id), let browserPanel = browserPanel(for: panelId), + browserPanel.panelType == .browser, let tabIndex = bonsplitController.tabs(inPane: pane).firstIndex(where: { $0.id == tab.id }) else { pendingClosedBrowserRestoreSnapshots.removeValue(forKey: tab.id) return @@ -9274,6 +9443,7 @@ final class Workspace: Identifiable, ObservableObject { trigger: FocusPanelTrigger ) { guard trigger == .standard else { return } + guard browserPanel.surfaceRole.showsBrowserChrome else { return } guard !isCommandPaletteVisibleForWorkspaceWindow() else { return } guard !browserPanel.shouldSuppressOmnibarAutofocus() else { return } guard browserPanel.isShowingNewTabPage || browserPanel.preferredURLStringForOmnibar() == nil else { return } @@ -12363,6 +12533,8 @@ extension Workspace: BonsplitDelegate { preferredWindow: presentingWindow, debugSource: "surfaceTabBar.cloudVM" ) + case .newCodeEditor: + _ = openCodeEditorFromPane(pane) case .newTerminal, .newBrowser, .splitRight, .splitDown: break } @@ -12446,6 +12618,8 @@ extension Workspace: BonsplitDelegate { _ = newTerminalSurface(inPane: pane, inheritWorkingDirectoryFallback: true) case "browser": _ = newBrowserSurface(inPane: pane) + case SurfaceKind.codeEditor.rawValue, "codeEditor", "code_editor": + _ = openCodeEditorFromPane(pane) default: _ = newTerminalSurface(inPane: pane, inheritWorkingDirectoryFallback: true) } diff --git a/Sources/WorkspaceContentView.swift b/Sources/WorkspaceContentView.swift index 98796ee1ba8b..23510fab9080 100644 --- a/Sources/WorkspaceContentView.swift +++ b/Sources/WorkspaceContentView.swift @@ -717,6 +717,13 @@ struct EmptyPanelView: View { _ = workspace.newBrowserSurface(inPane: paneId) } + private func createCodeEditor() { + #if DEBUG + cmuxDebugLog("emptyPane.newCodeEditor pane=\(paneId.id.uuidString.prefix(5))") + #endif + _ = workspace.openCodeEditorFromPane(paneId) + } + private var newSurfaceShortcut: StoredShortcut { let _ = keyboardShortcutSettingsObserver.revision return KeyboardShortcutSettings.shortcut(for: .newSurface) @@ -731,10 +738,10 @@ struct EmptyPanelView: View { private func emptyPaneActionButton( title: String, systemImage: String, - shortcut: StoredShortcut, + shortcut: StoredShortcut?, action: @escaping () -> Void ) -> some View { - if let key = shortcut.keyEquivalent { + if let shortcut, let key = shortcut.keyEquivalent { Button(action: action) { HStack(spacing: 10) { Label(title, systemImage: systemImage) @@ -747,7 +754,9 @@ struct EmptyPanelView: View { Button(action: action) { HStack(spacing: 10) { Label(title, systemImage: systemImage) - ShortcutHint(text: shortcut.displayString) + if let shortcut { + ShortcutHint(text: shortcut.displayString) + } } } .buttonStyle(.borderedProminent) @@ -760,24 +769,31 @@ struct EmptyPanelView: View { .font(.system(size: 48)) .foregroundStyle(.tertiary) - Text("Empty Panel") + Text(String(localized: "emptyPane.title", defaultValue: "Empty Panel")) .font(.headline) .foregroundStyle(.secondary) HStack(spacing: 12) { emptyPaneActionButton( - title: "Terminal", + title: String(localized: "emptyPane.terminal", defaultValue: "Terminal"), systemImage: "terminal.fill", shortcut: newSurfaceShortcut, action: createTerminal ) emptyPaneActionButton( - title: "Browser", + title: String(localized: "emptyPane.browser", defaultValue: "Browser"), systemImage: "globe", shortcut: openBrowserShortcut, action: createBrowser ) + + emptyPaneActionButton( + title: String(localized: "emptyPane.codeEditor", defaultValue: "Code Editor"), + systemImage: "curlybraces.square", + shortcut: nil, + action: createCodeEditor + ) } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 3a3833de3e68..fe5542e946f7 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -736,6 +736,11 @@ struct cmuxApp: App { } } + Button(String(localized: "menu.file.newCodeEditorTab", defaultValue: "New Code Editor Tab")) { + _ = AppDelegate.shared?.openCodeEditor(tabManager: activeTabManager) + } + .disabled(BrowserAvailabilitySettings.isDisabled()) + splitCommandButton(title: String(localized: "menu.file.openFolder", defaultValue: "Open Folder…"), shortcut: menuShortcut(for: .openFolder)) { AppDelegate.shared?.showOpenFolderPanel() } diff --git a/cmuxTests/AppDelegateIssue2907RoutingTests.swift b/cmuxTests/AppDelegateIssue2907RoutingTests.swift index 503e70f57f25..82a7fbc44c1e 100644 --- a/cmuxTests/AppDelegateIssue2907RoutingTests.swift +++ b/cmuxTests/AppDelegateIssue2907RoutingTests.swift @@ -94,6 +94,74 @@ final class AppDelegateIssue2907RoutingTests: XCTestCase { ) } + func testSystemTreeSerializesEditorSurfaceURLAndWebViewMetadata() throws { + _ = NSApplication.shared + let previousAppDelegate = AppDelegate.shared + let previousBrowserDisabled = UserDefaults.standard.object(forKey: BrowserAvailabilitySettings.disabledKey) + let app = AppDelegate() + defer { + if let previousBrowserDisabled { + UserDefaults.standard.set(previousBrowserDisabled, forKey: BrowserAvailabilitySettings.disabledKey) + } else { + UserDefaults.standard.removeObject(forKey: BrowserAvailabilitySettings.disabledKey) + } + TerminalController.shared.setActiveTabManager(nil) + AppDelegate.shared = previousAppDelegate + } + BrowserAvailabilitySettings.setDisabled(false) + + let windowId = UUID() + let window = makeMainWindow(id: windowId) + defer { + app.unregisterMainWindowContextForTesting(windowId: windowId) + window.orderOut(nil) + } + + let manager = TabManager(autoWelcomeIfNeeded: false) + app.registerMainWindow( + window, + windowId: windowId, + tabManager: manager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState() + ) + TerminalController.shared.setActiveTabManager(manager) + + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let editorURL = try XCTUnwrap(URL(string: "https://vscode.dev/")) + let editorPanel = try XCTUnwrap(workspace.newCodeEditorSurface( + inPane: paneId, + url: editorURL, + focus: true + )) + + let tree = try v2Result(method: "system.tree") + let windows = try XCTUnwrap(tree["windows"] as? [[String: Any]]) + var editorSurface: [String: Any]? + for windowPayload in windows { + let workspaces = windowPayload["workspaces"] as? [[String: Any]] ?? [] + for workspacePayload in workspaces { + let panes = workspacePayload["panes"] as? [[String: Any]] ?? [] + for panePayload in panes { + let surfaces = panePayload["surfaces"] as? [[String: Any]] ?? [] + if let surface = surfaces.first(where: { ($0["id"] as? String) == editorPanel.id.uuidString }) { + editorSurface = surface + } + } + } + } + + let surface = try XCTUnwrap(editorSurface) + XCTAssertEqual(surface["type"] as? String, "editor") + XCTAssertEqual(surface["url"] as? String, editorURL.absoluteString) + let webviews = try XCTUnwrap(surface["webviews"] as? [[String: Any]]) + let webview = try XCTUnwrap(webviews.first) + XCTAssertEqual(webview["surface_id"] as? String, editorPanel.id.uuidString) + XCTAssertEqual(webview["url"] as? String, editorURL.absoluteString) + } + func testWorkspaceReorderManyRoutesByWorkspaceOwnerWhenWindowIsOmitted() throws { let previousAppDelegate = AppDelegate.shared let app = AppDelegate() diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index 792544863cd9..7df7a8b17f6f 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -1069,6 +1069,18 @@ final class BrowserPanelReactGrabBridgeTests: XCTestCase { XCTAssertEqual(panel.preferredFocusIntent, .addressBar) } + func testCodeEditorRoleDefaultsInitialURLAndHidesOmnibar() { + let panel = BrowserPanel( + workspaceId: UUID(), + surfaceRole: .codeEditor, + renderInitialNavigation: false + ) + defer { panel.close() } + + XCTAssertEqual(panel.currentURL?.absoluteString, "https://vscode.dev/") + XCTAssertFalse(panel.isOmnibarVisible) + } + func testCopySuccessPostsPastebackNotificationAndClearsPendingTarget() throws { let workspaceId = UUID() let terminalId = UUID() diff --git a/cmuxTests/TabManagerSessionSnapshotTests.swift b/cmuxTests/TabManagerSessionSnapshotTests.swift index 145345e5be36..2dbfe3f5925c 100644 --- a/cmuxTests/TabManagerSessionSnapshotTests.swift +++ b/cmuxTests/TabManagerSessionSnapshotTests.swift @@ -1735,6 +1735,37 @@ final class TabManagerSessionSnapshotTests: XCTestCase { XCTAssertNotNil(manager.selectedTabId) } + func testSessionSnapshotRestoresCodeEditorPanelTypeAndURL() { + let manager = TabManager() + guard let workspace = manager.selectedWorkspace, + let paneId = workspace.bonsplitController.focusedPaneId, + let editorURL = URL(string: "https://vscode.dev/"), + let editorPanel = workspace.newCodeEditorSurface(inPane: paneId, url: editorURL, focus: true) else { + XCTFail("Expected code editor setup to succeed") + return + } + + let snapshot = manager.sessionSnapshot(includeScrollback: false) + guard let panelSnapshot = snapshot.workspaces.first?.panels.first(where: { $0.id == editorPanel.id }) else { + XCTFail("Expected code editor panel snapshot") + return + } + XCTAssertEqual(panelSnapshot.type, .codeEditor) + XCTAssertEqual(panelSnapshot.browser?.urlString, editorURL.absoluteString) + + let restored = TabManager() + restored.restoreSessionSnapshot(snapshot) + guard let restoredWorkspace = restored.selectedWorkspace, + let restoredEditor = restoredWorkspace.panels.values.compactMap({ $0 as? BrowserPanel }).first(where: { $0.panelType == .codeEditor }) else { + XCTFail("Expected restored code editor panel") + return + } + + XCTAssertEqual(restoredEditor.surfaceRole, .codeEditor) + XCTAssertFalse(restoredEditor.isOmnibarVisible) + XCTAssertEqual(restoredEditor.currentURL?.absoluteString, editorURL.absoluteString) + } + func testRestoredPersistentSSHBrowserOnlyWorkspaceAutoConnectsWithoutForegroundAuthTerminal() { let browserPanelId = UUID() let browserOnlySnapshot = Self.persistentSSHWorkspaceSnapshot( diff --git a/cmuxTests/TabManagerUnitTests.swift b/cmuxTests/TabManagerUnitTests.swift index cbfbfe2ae711..7d00fcc93fe4 100644 --- a/cmuxTests/TabManagerUnitTests.swift +++ b/cmuxTests/TabManagerUnitTests.swift @@ -12,6 +12,7 @@ import CmuxSidebarGit import CmuxSidebar import CmuxTerminal import CmuxSettings +import CmuxWorkspaceCore #if canImport(cmux_DEV) @testable import cmux_DEV @@ -2542,6 +2543,80 @@ final class TabManagerSurfaceCreationTests: XCTestCase { XCTAssertEqual(workspace.focusedPanelId, browserPanelId, "Expected opened browser surface to be focused") } + func testOpenCodeEditorCreatesFirstClassEditorSurface() { + let manager = TabManager() + guard let workspace = manager.selectedWorkspace, + let paneId = workspace.bonsplitController.focusedPaneId else { + XCTFail("Expected focused workspace and pane") + return + } + + _ = workspace.newTerminalSurface(inPane: paneId, focus: false) + + guard let editorPanelId = manager.openCodeEditor(insertAtEnd: true), + let editorPanel = workspace.browserPanel(for: editorPanelId) else { + XCTFail("Expected code editor panel to be created") + return + } + + XCTAssertEqual(editorPanel.panelType, .codeEditor) + XCTAssertEqual(editorPanel.surfaceRole, .codeEditor) + XCTAssertFalse(editorPanel.isOmnibarVisible) + XCTAssertEqual(editorPanel.currentURL?.absoluteString, "https://vscode.dev/") + XCTAssertNil(manager.focusedBrowserPanel, "Code editor surfaces should not be treated as browser shortcut targets") + + let tabs = workspace.bonsplitController.tabs(inPane: paneId) + guard let lastSurface = tabs.last else { + XCTFail("Expected at least one surface in pane") + return + } + XCTAssertEqual(workspace.panelIdFromSurfaceId(lastSurface.id), editorPanelId) + XCTAssertEqual(lastSurface.kind, SurfaceKind.codeEditor) + } + + func testOpenCodeEditorWithSplitDirectionCreatesDirectedEditorSplit() throws { + let appDelegate = AppDelegate() + let manager = TabManager() + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let sourcePaneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let sourcePanel = try XCTUnwrap( + workspace.newBrowserSurface(inPane: sourcePaneId, focus: true) + ) + workspace.currentDirectory = "" + workspace.panelDirectories.removeAll() + let initialPaneCount = workspace.bonsplitController.allPaneIds.count + + let editorPanelId = try XCTUnwrap( + appDelegate.openCodeEditor(tabManager: manager, splitDirection: .down) + ) + let editorPanel = try XCTUnwrap(workspace.browserPanel(for: editorPanelId)) + + XCTAssertEqual(editorPanel.panelType, .codeEditor) + XCTAssertEqual(editorPanel.surfaceRole, .codeEditor) + XCTAssertEqual( + editorPanel.currentURL?.absoluteString, + BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL?.absoluteString + ) + XCTAssertEqual(workspace.bonsplitController.allPaneIds.count, initialPaneCount + 1) + XCTAssertEqual(workspace.focusedPanelId, editorPanelId) + + guard case .split(let root) = workspace.bonsplitController.treeSnapshot() else { + XCTFail("Expected code editor open with split direction to split the workspace root") + return + } + XCTAssertEqual(root.orientation, "vertical") + + let expectedSourcePaneId = try XCTUnwrap(workspace.paneId(forPanelId: sourcePanel.id)).id.uuidString + let expectedEditorPaneId = try XCTUnwrap(workspace.paneId(forPanelId: editorPanelId)).id.uuidString + guard case .pane(let firstPane) = root.first, + case .pane(let secondPane) = root.second else { + XCTFail("Expected split children to be panes") + return + } + XCTAssertEqual(firstPane.id, expectedSourcePaneId) + XCTAssertEqual(secondPane.id, expectedEditorPaneId) + } + func testToggleOmnibarFocusedBrowserIsSurfaceSpecific() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 0fda3b7f229f..30b6640cd022 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -4587,6 +4587,84 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { ) } + func testInitialCodeEditorDirectoryUsesFocusedPanelDirectoryWhenWorkspaceDirectoryIsEmpty() { + let workspace = Workspace() + guard let focusedPanelId = workspace.focusedPanelId else { + XCTFail("Expected focused panel in new workspace") + return + } + + let focusedDirectory = "/tmp/cmux-focused-editor-cwd" + workspace.currentDirectory = "" + workspace.panelDirectories[focusedPanelId] = focusedDirectory + + XCTAssertEqual(workspace.resolvedWorkingDirectory(), focusedDirectory) + XCTAssertEqual(AppDelegate.initialCodeEditorDirectoryPath(for: workspace), focusedDirectory) + } + + func testInitialCodeEditorDirectoryUsesRequestedTerminalDirectoryBeforeLiveReport() { + let workspace = Workspace() + guard let paneId = workspace.bonsplitController.focusedPaneId else { + XCTFail("Expected focused pane in new workspace") + return + } + + let requestedDirectory = "/tmp/cmux-requested-editor-cwd" + guard let requestedPanel = workspace.newTerminalSurface( + inPane: paneId, + focus: true, + workingDirectory: requestedDirectory + ) else { + XCTFail("Expected terminal panel with requested cwd") + return + } + + workspace.currentDirectory = "" + workspace.panelDirectories.removeValue(forKey: requestedPanel.id) + + XCTAssertEqual(workspace.resolvedWorkingDirectory(), requestedDirectory) + XCTAssertEqual(AppDelegate.initialCodeEditorDirectoryPath(for: workspace), requestedDirectory) + } + + func testDiscardProvisionalDefaultCodeEditorClosesUntouchedFallbackPanel() throws { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let panel = try XCTUnwrap( + workspace.newCodeEditorSurface( + inPane: paneId, + url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + focus: true, + creationPolicy: .restoration + ) + ) + let panelId = panel.id + + XCTAssertTrue(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertEqual(AppDelegate.provisionalDefaultCodeEditor(in: workspace, panelId: panelId)?.id, panelId) + XCTAssertTrue(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) + XCTAssertNil(workspace.panels[panelId]) + } + + func testDiscardProvisionalDefaultCodeEditorPreservesNavigatedPanel() throws { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let folderURL = try XCTUnwrap(URL(string: "http://127.0.0.1:5555/?tkn=test&folder=/tmp/cmux")) + let panel = try XCTUnwrap( + workspace.newCodeEditorSurface( + inPane: paneId, + url: folderURL, + focus: true, + creationPolicy: .restoration + ) + ) + let panelId = panel.id + + XCTAssertFalse(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertNil(AppDelegate.provisionalDefaultCodeEditor(in: workspace, panelId: panelId)) + XCTAssertFalse(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) + XCTAssertNotNil(workspace.panels[panelId]) + } + func testNewTerminalSplitSkipsFreedInheritedSurfacePointer() throws { #if DEBUG let workspace = Workspace()