From 858068bfbfd47f028b0e499f3f9380294f6ae97f Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:10:36 -0600 Subject: [PATCH 1/4] Fix Terminal Tabs --- CodeEdit.xcodeproj/project.pbxproj | 4 ++ .../UtilityAreaTerminalPicker.swift | 42 +++++++++++++++++++ .../UtilityAreaTerminalTab.swift | 11 ++++- .../UtilityAreaTerminalView.swift | 30 ------------- .../ViewModels/UtilityAreaViewModel.swift | 2 +- 5 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 1d9d1aae26..da722c1953 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -465,6 +465,7 @@ 6CC3D1FB2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */; }; 6CC3D1FD2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */; }; 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; + 6CCEE7F52D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CCEE7F42D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift */; }; 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */; }; @@ -1142,6 +1143,7 @@ 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextView+SemanticTokenRangeProvider.swift"; sourceTree = ""; }; 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapRangeProvider.swift; sourceTree = ""; }; 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; + 6CCEE7F42D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalPicker.swift; sourceTree = ""; }; 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileMap.swift; sourceTree = ""; }; @@ -3441,6 +3443,7 @@ isa = PBXGroup; children = ( B62AEDB22A1FD95B009A9F52 /* UtilityAreaTerminalView.swift */, + 6CCEE7F42D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift */, B62AEDBB2A210DBB009A9F52 /* UtilityAreaTerminalTab.swift */, 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */, ); @@ -4519,6 +4522,7 @@ 613899BC2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift in Sources */, 611192002B08CCD700D4459B /* SearchIndexer+Memory.swift in Sources */, 587B9E8129301D8F00AC7927 /* PublicKey.swift in Sources */, + 6CCEE7F52D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift in Sources */, 611191FE2B08CCD200D4459B /* SearchIndexer+File.swift in Sources */, B69D3EE52C5F54B3005CF43A /* TasksPopoverMenuItem.swift in Sources */, 669A50532C380C8E00304CD8 /* Collection+subscript_safe.swift in Sources */, diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift new file mode 100644 index 0000000000..8e241ab386 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift @@ -0,0 +1,42 @@ +// +// UtilityAreaTerminalPicker.swift +// CodeEdit +// +// Created by Khan Winter on 1/6/25. +// + +import SwiftUI + +struct UtilityAreaTerminalPicker: View { + @Binding var selectedIDs: Set + var terminals: [UtilityAreaTerminal] + + var selectedID: Binding { + Binding( + get: { + selectedIDs.first + }, + set: { newValue in + if let selectedID = newValue { + selectedIDs = [selectedID] + } + } + ) + } + + var body: some View { + Picker("Terminal Tab", selection: selectedID) { + ForEach(terminals, id: \.self.id) { terminal in + Text(terminal.title) + .tag(terminal.id) + } + + if terminals.isEmpty { + Text("No Open Terminals") + } + } + .labelsHidden() + .controlSize(.small) + .buttonStyle(.borderless) + } +} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift index c7a310c93f..57401759db 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift @@ -51,8 +51,15 @@ struct UtilityAreaTerminalTab: View { Button("Rename...") { isFocused = true } - Button("Kill Terminal") { - if isSelected { removeTerminals([terminal.id]) } + + if selectedIDs.contains(terminal.id) && selectedIDs.count > 1 { + Button("Kill Terminals") { + removeTerminals(selectedIDs) + } + } else { + Button("Kill Terminal") { + removeTerminals([terminal.id]) + } } } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 9d6aba9d63..f4c9dab1f3 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -179,33 +179,3 @@ struct UtilityAreaTerminalView: View { } } } - -struct UtilityAreaTerminalPicker: View { - @Binding var selectedIDs: Set - var terminals: [UtilityAreaTerminal] - - var selectedID: Binding { - Binding( - get: { - selectedIDs.first - }, - set: { newValue in - if let selectedID = newValue { - selectedIDs = [selectedID] - } - } - ) - } - - var body: some View { - Picker("Terminal Tab", selection: selectedID) { - ForEach(terminals, id: \.self.id) { terminal in - Text(terminal.title) - .tag(terminal.id as UUID?) - } - } - .labelsHidden() - .controlSize(.small) - .buttonStyle(.borderless) - } -} diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 47143a1801..2b9fd36027 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -34,7 +34,7 @@ class UtilityAreaViewModel: ObservableObject { @Published var tabViewModel = UtilityAreaTabViewModel() func removeTerminals(_ ids: Set) { - for (idx, terminal) in terminals.lazy.reversed().enumerated() + for (idx, terminal) in terminals.enumerated().reversed() where ids.contains(terminal.id) { TerminalCache.shared.removeCachedView(terminal.id) terminals.remove(at: idx) From 6e66ab74e0f8150752e2becb995a3f1a27270b0e Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:15:22 -0600 Subject: [PATCH 2/4] Add Tests --- CodeEdit.xcodeproj/project.pbxproj | 19 ++- .../Features/SplitView/Views/SplitView.swift | 1 + .../UtilityAreaTerminalSidebar.swift | 14 ++- .../UtilityAreaTerminalTab.swift | 1 + .../UtilityAreaTerminalView.swift | 8 +- .../ViewModels/UtilityAreaViewModel.swift | 100 +++++++++------ .../UtilityArea/Views/UtilityAreaView.swift | 2 + CodeEdit/WorkspaceView.swift | 2 + .../TerminalEmulator/Shell/ShellTests.swift | 0 .../ShellIntegrationTests.swift | 0 .../UtilityAreaViewModelTests.swift | 114 ++++++++++++++++++ 11 files changed, 209 insertions(+), 52 deletions(-) rename CodeEditTests/Features/{ActivityViewer => }/TerminalEmulator/Shell/ShellTests.swift (100%) rename CodeEditTests/Features/{ActivityViewer => }/TerminalEmulator/ShellIntegrationTests.swift (100%) create mode 100644 CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index da722c1953..a7a64ceb48 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -402,6 +402,7 @@ 6C48D8F72972E5F300D6D205 /* WindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */; }; 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */; }; 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; }; + 6C510CB02D2E3547006EBE85 /* UtilityAreaViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C510CAF2D2E3547006EBE85 /* UtilityAreaViewModelTests.swift */; }; 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; @@ -1094,6 +1095,7 @@ 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+Window.swift"; sourceTree = ""; }; 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalSidebar.swift; sourceTree = ""; }; + 6C510CAF2D2E3547006EBE85 /* UtilityAreaViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaViewModelTests.swift; sourceTree = ""; }; 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ContentInsets.swift"; sourceTree = ""; }; 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionActivatorView.swift; sourceTree = ""; }; @@ -2066,6 +2068,7 @@ 613899BD2B6E70E200A5CAF6 /* Search */, 61FB03A92C3C1FC4001B3671 /* Tasks */, 6141CF392C3DA4180073BC9F /* TerminalEmulator */, + 6C510CAE2D2E351E006EBE85 /* UtilityArea */, ); path = Features; sourceTree = ""; @@ -2673,8 +2676,7 @@ 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */, 61FB03AA2C3C1FD5001B3671 /* Shell */, ); - name = TerminalEmulator; - path = ActivityViewer/TerminalEmulator; + path = TerminalEmulator; sourceTree = ""; }; 617DB3CE2C25AF5B00B58BFE /* ActivityViewer */ = { @@ -2950,6 +2952,14 @@ path = Environment; sourceTree = ""; }; + 6C510CAE2D2E351E006EBE85 /* UtilityArea */ = { + isa = PBXGroup; + children = ( + 6C510CAF2D2E3547006EBE85 /* UtilityAreaViewModelTests.swift */, + ); + path = UtilityArea; + sourceTree = ""; + }; 6C6BD6ED29CD123000235D17 /* Extensions */ = { isa = PBXGroup; children = ( @@ -3442,10 +3452,10 @@ B676606A2AA973A500CD56B0 /* TerminalUtility */ = { isa = PBXGroup; children = ( - B62AEDB22A1FD95B009A9F52 /* UtilityAreaTerminalView.swift */, 6CCEE7F42D2C91F700B2B854 /* UtilityAreaTerminalPicker.swift */, - B62AEDBB2A210DBB009A9F52 /* UtilityAreaTerminalTab.swift */, 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */, + B62AEDBB2A210DBB009A9F52 /* UtilityAreaTerminalTab.swift */, + B62AEDB22A1FD95B009A9F52 /* UtilityAreaTerminalView.swift */, ); path = TerminalUtility; sourceTree = ""; @@ -4555,6 +4565,7 @@ 583E528C29361B39001AB554 /* CodeEditUITests.swift in Sources */, 6C7D6D462C9092EC00B69EE0 /* BufferingServerConnection.swift in Sources */, 613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */, + 6C510CB02D2E3547006EBE85 /* UtilityAreaViewModelTests.swift in Sources */, 617DB3DF2C25E13800B58BFE /* TaskNotificationHandlerTests.swift in Sources */, 775566502C27FD1B001E7A4D /* CodeFileDocument+UTTypeTests.swift in Sources */, 587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */, diff --git a/CodeEdit/Features/SplitView/Views/SplitView.swift b/CodeEdit/Features/SplitView/Views/SplitView.swift index e16d45e421..12b752f4fb 100644 --- a/CodeEdit/Features/SplitView/Views/SplitView.swift +++ b/CodeEdit/Features/SplitView/Views/SplitView.swift @@ -25,5 +25,6 @@ struct SplitView: View { } } ._trait(SplitViewControllerLayoutValueKey.self, viewController) + .accessibilityElement(children: .contain) } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 6f1b0817d6..1c61d8bb41 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -26,7 +26,7 @@ struct UtilityAreaTerminalSidebar: View { .listRowSeparator(.hidden) } .onMove { [weak utilityAreaViewModel] (source, destination) in - utilityAreaViewModel?.moveItems(from: source, to: destination) + utilityAreaViewModel?.reorderTerminals(from: source, to: destination) } } .focusedObject(utilityAreaViewModel) @@ -34,29 +34,29 @@ struct UtilityAreaTerminalSidebar: View { .accentColor(.secondary) .contextMenu { Button("New Terminal") { - utilityAreaViewModel.addTerminal(workspace: workspace) + utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } Menu("New Terminal With Profile") { Button("Default") { - utilityAreaViewModel.addTerminal(workspace: workspace) + utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } Divider() ForEach(Shell.allCases, id: \.self) { shell in Button(shell.rawValue) { - utilityAreaViewModel.addTerminal(shell: shell, workspace: workspace) + utilityAreaViewModel.addTerminal(shell: shell, rootURL: workspace.fileURL) } } } } .onChange(of: utilityAreaViewModel.terminals) { newValue in if newValue.isEmpty { - utilityAreaViewModel.addTerminal(workspace: workspace) + utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } } .paneToolbar { PaneToolbarSection { Button { - utilityAreaViewModel.addTerminal(workspace: workspace) + utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } label: { Image(systemName: "plus") } @@ -70,6 +70,8 @@ struct UtilityAreaTerminalSidebar: View { } Spacer() } + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminals") } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift index 57401759db..2ee68eb733 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift @@ -46,6 +46,7 @@ struct UtilityAreaTerminalTab: View { } } icon: { Image(systemName: "terminal") + .accessibilityHidden(true) } .contextMenu { Button("Rename...") { diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index f4c9dab1f3..998ed620b7 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -138,7 +138,7 @@ struct UtilityAreaTerminalView: View { guard let terminal = getSelectedTerminal() else { return } - utilityAreaViewModel.addTerminal(shell: nil, workspace: workspace, replacing: terminal.id) + utilityAreaViewModel.replaceTerminal(terminal.id) } label: { Image(systemName: "trash") } @@ -161,7 +161,11 @@ struct UtilityAreaTerminalView: View { UtilityAreaTerminalSidebar() } .onAppear { - utilityAreaViewModel.initializeTerminals(workspace) + guard let workspaceURL = workspace.fileURL else { + assertionFailure("Workspace does not have a file URL.") + return + } + utilityAreaViewModel.initializeTerminals(workspaceURL: workspaceURL) } } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 2b9fd36027..cd6fdf2b30 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -33,15 +33,7 @@ class UtilityAreaViewModel: ObservableObject { /// The tab bar view model for UtilityAreaTabView @Published var tabViewModel = UtilityAreaTabViewModel() - func removeTerminals(_ ids: Set) { - for (idx, terminal) in terminals.enumerated().reversed() - where ids.contains(terminal.id) { - TerminalCache.shared.removeCachedView(terminal.id) - terminals.remove(at: idx) - } - - selectedTerminals = [terminals.last?.id ?? UUID()] - } + // MARK: - State Restoration func restoreFromState(_ workspace: WorkspaceDocument) { isCollapsed = workspace.getFromWorkspaceState(.utilityAreaCollapsed) as? Bool ?? false @@ -62,6 +54,25 @@ class UtilityAreaViewModel: ObservableObject { // MARK: - Terminal Management + /// Removes all terminals included in the given set and selects a new terminal if the selection was modified. + /// The new selection is either the same selection minus the ids removed, or if that's empty the last terminal. + /// - Parameter ids: A set of all terminal ids to remove. + func removeTerminals(_ ids: Set) { + for (idx, terminal) in terminals.enumerated().reversed() + where ids.contains(terminal.id) { + TerminalCache.shared.removeCachedView(terminal.id) + terminals.remove(at: idx) + } + + var newSelection = selectedTerminals.subtracting(ids) + + if newSelection.isEmpty, let terminal = terminals.last { + newSelection = [terminal.id] + } + + selectedTerminals = newSelection + } + /// Update a terminal's title. /// - Parameters: /// - id: The id of the terminal to update. @@ -81,56 +92,65 @@ class UtilityAreaViewModel: ObservableObject { /// Create a new terminal if there are no existing terminals. /// Will not perform any action if terminals exist in the ``terminals`` array. - /// - Parameter workspace: The workspace to use to find the default path. - func initializeTerminals(_ workspace: WorkspaceDocument) { + /// - Parameter workspaceURL: The base url of the workspace, to initialize terminals.l + func initializeTerminals(workspaceURL: URL) { guard terminals.isEmpty else { return } - addTerminal(shell: nil, workspace: workspace) + addTerminal(rootURL: workspaceURL) } - /// Add a new terminal to the workspace and selects it. Optionally replaces an existing terminal - /// - /// Terminals being replaced will have the `SIGKILL` signal sent to the running shell. The new terminal will - /// inherit the same `url` and `shell` parameters from the old one, in case they were specified. - /// + /// Add a new terminal to the workspace and selects it. /// - Parameters: /// - shell: The shell to use, `nil` if auto-detect the default shell. - /// - workspace: The workspace to use to find the default path. - /// - replacing: The ID of a terminal to replace with a new terminal. If left `nil`, will ignore. - func addTerminal(shell: Shell? = nil, workspace: WorkspaceDocument, replacing: UUID? = nil) { + /// - rootURL: The url to start the new terminal at. If left `nil` defaults to the user's home directory. + func addTerminal(shell: Shell? = nil, rootURL: URL?) { let id = UUID() - if let replacing, let index = terminals.firstIndex(where: { $0.id == replacing }) { - let url = terminals[index].url - let shell = terminals[index].shell - if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { - kill(shellPid, SIGKILL) - } - terminals[index] = UtilityAreaTerminal( + terminals.append( + UtilityAreaTerminal( id: id, - url: url, - title: "terminal", + url: rootURL ?? URL(filePath: "~/"), + title: shell?.rawValue ?? "terminal", shell: shell ) - TerminalCache.shared.removeCachedView(replacing) - } else { - terminals.append( - UtilityAreaTerminal( - id: id, - url: workspace.workspaceFileManager?.folderUrl ?? URL(filePath: "/"), - title: "terminal", - shell: shell - ) - ) + ) + + selectedTerminals = [id] + } + + /// Replaces the terminal with a given ID, killing the shell and restarting it at the same directory. + /// + /// Terminals being replaced will have the `SIGKILL` signal sent to the running shell. The new terminal will + /// inherit the same `url` and `shell` parameters from the old one. + /// - Parameter replacing: The ID of a terminal to replace with a new terminal. + func replaceTerminal(_ replacing: UUID) { + guard let index = terminals.firstIndex(where: { $0.id == replacing }) else { + return } + let id = UUID() + let url = terminals[index].url + let shell = terminals[index].shell + if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { + kill(shellPid, SIGKILL) + } + + terminals[index] = UtilityAreaTerminal( + id: id, + url: url, + title: shell?.rawValue ?? "terminal", + shell: shell + ) + TerminalCache.shared.removeCachedView(replacing) + selectedTerminals = [id] + return } /// Reorders terminals in the ``utilityAreaViewModel``. /// - Parameters: /// - source: The source indices. /// - destination: The destination indices. - func moveItems(from source: IndexSet, to destination: Int) { + func reorderTerminals(from source: IndexSet, to destination: Int) { terminals.move(fromOffsets: source, toOffset: destination) } } diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift b/CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift index a89e1e665c..d0032f5e7c 100644 --- a/CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift +++ b/CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift @@ -41,5 +41,7 @@ struct UtilityAreaView: View { .overlay(Color(nsColor: colorScheme == .dark ? .black : .clear)) } } + .accessibilityElement(children: .contain) + .accessibilityLabel("Utility Area") } } diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 5327ffc010..3a7cf0ddff 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -82,6 +82,7 @@ struct WorkspaceView: View { } } } + .accessibilityHidden(true) } .edgesIgnoringSafeArea(.top) .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -100,6 +101,7 @@ struct WorkspaceView: View { } .offset(y: utilityAreaViewModel.isMaximized ? 0 : editorsHeight - statusbarHeight) } + .accessibilityElement(children: .contain) } .onChange(of: focusedEditor) { newValue in /// update active tab group only if the new one is not the same with it. diff --git a/CodeEditTests/Features/ActivityViewer/TerminalEmulator/Shell/ShellTests.swift b/CodeEditTests/Features/TerminalEmulator/Shell/ShellTests.swift similarity index 100% rename from CodeEditTests/Features/ActivityViewer/TerminalEmulator/Shell/ShellTests.swift rename to CodeEditTests/Features/TerminalEmulator/Shell/ShellTests.swift diff --git a/CodeEditTests/Features/ActivityViewer/TerminalEmulator/ShellIntegrationTests.swift b/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift similarity index 100% rename from CodeEditTests/Features/ActivityViewer/TerminalEmulator/ShellIntegrationTests.swift rename to CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift diff --git a/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift new file mode 100644 index 0000000000..23f0bd1968 --- /dev/null +++ b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift @@ -0,0 +1,114 @@ +// +// UtilityAreaViewModelTests.swift +// CodeEditTests +// +// Created by Khan Winter on 1/7/25. +// + +import XCTest +@testable import CodeEdit + +final class UtilityAreaViewModelTests: XCTestCase { + var model: UtilityAreaViewModel! + let rootURL = URL(filePath: "~/")! + + override func setUp() { + model = UtilityAreaViewModel() + model.terminals = [ + UtilityAreaTerminal(id: UUID(), url: rootURL, title: "Terminal 1", shell: .bash), + UtilityAreaTerminal(id: UUID(), url: rootURL, title: "Terminal 2", shell: .zsh), + UtilityAreaTerminal(id: UUID(), url: rootURL, title: "Terminal 3", shell: nil), + UtilityAreaTerminal(id: UUID(), url: rootURL, title: "Terminal 4", shell: .bash), + UtilityAreaTerminal(id: UUID(), url: rootURL, title: "Terminal 5", shell: .zsh) + ] + } + + func testRemoveLastTerminal() { + let originalTerminals = model.terminals.map { $0.id } + model.removeTerminals(Set([originalTerminals[4]])) + XCTAssertEqual(model.terminals.count, 4) + XCTAssertEqual(Array(originalTerminals[0..<4]), model.terminals.map({ $0.id })) + } + + func testRemoveMiddleTerminal() { + let originalTerminals = model.terminals.map { $0.id } + model.removeTerminals(Set([originalTerminals[2]])) + XCTAssertEqual(model.terminals.count, 4) + XCTAssertEqual( + Array(originalTerminals[0..<2]) + Array(originalTerminals[3..<5]), + model.terminals.map({ $0.id }) + ) + } + + func testRemoveFirstTerminal() { + let originalTerminals = model.terminals.map { $0.id } + model.removeTerminals(Set([originalTerminals[0]])) + XCTAssertEqual(model.terminals.count, 4) + XCTAssertEqual(Array(originalTerminals[1..<5]), model.terminals.map({ $0.id })) + } + + func testRemoveAllTerminals() { + let originalTerminals = model.terminals.map { $0.id } + model.removeTerminals(Set(originalTerminals)) + XCTAssertEqual(model.terminals, []) + } + + // Skipping this test. The semantics of updating terminal titles needs work. + func _testUpdateTerminalTitle() { + model.updateTerminal(model.terminals[0].id, title: "Custom Title") + XCTAssertFalse(model.terminals[0].customTitle) // This feels wrong, but it's how this view model is set up. + XCTAssertEqual(model.terminals[0].title, "Custom Title") + + model.updateTerminal(model.terminals[0].id, title: nil) + XCTAssertFalse(model.terminals[0].customTitle) + // Should stay the same title, just disables the custom title. + XCTAssertEqual(model.terminals[0].title, "Custom Title") + } + + func testAddTerminal() { + model.addTerminal(shell: nil, rootURL: rootURL) + XCTAssertEqual(model.terminals.count, 6) + XCTAssertEqual(model.terminals[5].shell, nil) + XCTAssertEqual(model.terminals[5].title, "terminal") + XCTAssertFalse(model.terminals[5].customTitle) + XCTAssertEqual(model.terminals[5].url, rootURL) + } + + func testAddTerminalCustomShell() { + model.addTerminal(shell: .bash, rootURL: rootURL) + XCTAssertEqual(model.terminals.count, 6) + XCTAssertEqual(model.terminals[5].shell, .bash) + XCTAssertEqual(model.terminals[5].title, Shell.bash.rawValue) + XCTAssertFalse(model.terminals[5].customTitle) + XCTAssertEqual(model.terminals[5].url, rootURL) + + model.addTerminal(shell: .zsh, rootURL: rootURL) + XCTAssertEqual(model.terminals.count, 7) + XCTAssertEqual(model.terminals[6].shell, .zsh) + XCTAssertEqual(model.terminals[6].title, Shell.zsh.rawValue) + XCTAssertFalse(model.terminals[6].customTitle) + XCTAssertEqual(model.terminals[6].url, rootURL) + } + + func testReplaceTerminal() { + let terminalToReplace = model.terminals[2] + let shell = terminalToReplace.shell + model.replaceTerminal(terminalToReplace.id) + XCTAssertNotEqual(model.terminals[2].id, terminalToReplace.id) + XCTAssertEqual(model.terminals[2].shell, terminalToReplace.shell) + XCTAssertEqual(model.terminals[2].url, terminalToReplace.url) + } + + func testInitializeTerminals() { + let terminals = model.terminals + model.initializeTerminals(workspaceURL: rootURL) + XCTAssertEqual(terminals, model.terminals) // Should not modify if terminals exist + + // Remove all terminals so it can do something + model.removeTerminals(Set(model.terminals.map { $0.id })) + XCTAssertEqual(model.terminals.count, 0, "Model did not delete all terminals") + + model.initializeTerminals(workspaceURL: rootURL) + XCTAssertEqual(model.terminals.count, 1) + } +} From f870bf06abfdc147ceaf81bf1ffe18d9ce707b13 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:20:46 -0600 Subject: [PATCH 3/4] Fix Tests Not Compiling --- .../xcschemes/OpenWithCodeEdit.xcscheme | 97 +++++++++++++++++++ ...kspaceDocument+SearchState+FindTests.swift | 6 +- .../UtilityAreaViewModelTests.swift | 3 +- 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 CodeEdit.xcodeproj/xcshareddata/xcschemes/OpenWithCodeEdit.xcscheme diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/OpenWithCodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/OpenWithCodeEdit.xcscheme new file mode 100644 index 0000000000..2ef8c219e1 --- /dev/null +++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/OpenWithCodeEdit.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index b002063905..71434f2efa 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -55,8 +55,10 @@ final class FindTests: XCTestCase { files = fileURLs.map { CEWorkspaceFile(url: $0) } - files[1].parent = CEWorkspaceFile(url: folder1) - files[2].parent = CEWorkspaceFile(url: folder2) + let parent1 = CEWorkspaceFile(url: folder1) + let parent2 = CEWorkspaceFile(url: folder2) + files[1].parent = parent1 + files[2].parent = parent2 await mockWorkspace.searchState?.addProjectToIndex() diff --git a/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift index 23f0bd1968..5406cb58f4 100644 --- a/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift +++ b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift @@ -10,7 +10,7 @@ import XCTest final class UtilityAreaViewModelTests: XCTestCase { var model: UtilityAreaViewModel! - let rootURL = URL(filePath: "~/")! + let rootURL: URL = URL(filePath: "~/") override func setUp() { model = UtilityAreaViewModel() @@ -92,7 +92,6 @@ final class UtilityAreaViewModelTests: XCTestCase { func testReplaceTerminal() { let terminalToReplace = model.terminals[2] - let shell = terminalToReplace.shell model.replaceTerminal(terminalToReplace.id) XCTAssertNotEqual(model.terminals[2].id, terminalToReplace.id) XCTAssertEqual(model.terminals[2].shell, terminalToReplace.shell) From 05633551572dc09b709f687c80f2aa99eaa6d6ff Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:14:37 -0600 Subject: [PATCH 4/4] Fix Project File --- CodeEdit.xcodeproj/project.pbxproj | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 549be0b0d2..87e5d8aff8 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -404,6 +404,7 @@ 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */; }; 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; }; 6C510CB82D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C510CB72D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift */; }; + 6C510CBC2D2ECD68006EBE85 /* UtilityAreaViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C510CBA2D2ECD68006EBE85 /* UtilityAreaViewModelTests.swift */; }; 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; @@ -1099,6 +1100,7 @@ 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalSidebar.swift; sourceTree = ""; }; 6C510CB72D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUITest+waitForNonExistence.swift"; sourceTree = ""; }; + 6C510CBA2D2ECD68006EBE85 /* UtilityAreaViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaViewModelTests.swift; sourceTree = ""; }; 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ContentInsets.swift"; sourceTree = ""; }; 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionActivatorView.swift; sourceTree = ""; }; @@ -2072,7 +2074,7 @@ 613899BD2B6E70E200A5CAF6 /* Search */, 61FB03A92C3C1FC4001B3671 /* Tasks */, 6141CF392C3DA4180073BC9F /* TerminalEmulator */, - 6C510CAE2D2E351E006EBE85 /* UtilityArea */, + 6C510CBB2D2ECD68006EBE85 /* UtilityArea */, ); path = Features; sourceTree = ""; @@ -2981,6 +2983,14 @@ path = Extensions; sourceTree = ""; }; + 6C510CBB2D2ECD68006EBE85 /* UtilityArea */ = { + isa = PBXGroup; + children = ( + 6C510CBA2D2ECD68006EBE85 /* UtilityAreaViewModelTests.swift */, + ); + path = UtilityArea; + sourceTree = ""; + }; 6C6BD6ED29CD123000235D17 /* Extensions */ = { isa = PBXGroup; children = ( @@ -4589,7 +4599,7 @@ 583E528C29361B39001AB554 /* CodeEditUITests.swift in Sources */, 6C7D6D462C9092EC00B69EE0 /* BufferingServerConnection.swift in Sources */, 613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */, - 6C510CB02D2E3547006EBE85 /* UtilityAreaViewModelTests.swift in Sources */, + 6C510CBC2D2ECD68006EBE85 /* UtilityAreaViewModelTests.swift in Sources */, 617DB3DF2C25E13800B58BFE /* TaskNotificationHandlerTests.swift in Sources */, 775566502C27FD1B001E7A4D /* CodeFileDocument+UTTypeTests.swift in Sources */, 587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */,