From 9d4bbf2ad0358968b1d31152c11a33ce413c7dc4 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:41:25 -0500 Subject: [PATCH 1/2] =?UTF-8?q?Add=20`=E2=8C=98R`=20and=20`=E2=8C=98.`=20t?= =?UTF-8?q?o=20Start=20and=20Stop=20Tasks,=20Add=20Tasks=20Menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeEdit/Features/Tasks/TaskManager.swift | 2 +- .../Tasks/Views/StopTaskToolbarButton.swift | 2 +- .../WindowCommands/CodeEditCommands.swift | 1 + .../WindowCommands/TasksCommands.swift | 66 +++++++++++++++++++ .../WindowControllerPropertyWrapper.swift | 31 +++++---- .../Features/Tasks/TaskManagerTests.swift | 2 +- 6 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 CodeEdit/Features/WindowCommands/TasksCommands.swift diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift index aeb96e2deb..bc6eb70282 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -93,7 +93,7 @@ class TaskManager: ObservableObject { } } - func terminateActiveTask() { + func terminateSelectedTask() { guard let taskID = selectedTaskID else { return } diff --git a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift index 559dbf0b77..9c76bbd580 100644 --- a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift @@ -24,7 +24,7 @@ struct StopTaskToolbarButton: View { HStack { if let currentSelectedStatus, currentSelectedStatus == .running { Button { - taskManager.terminateActiveTask() + taskManager.terminateSelectedTask() } label: { Label("Stop", systemImage: "stop.fill") .labelStyle(.iconOnly) diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index ef5714559a..e2f1ba7e69 100644 --- a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift +++ b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift @@ -17,6 +17,7 @@ struct CodeEditCommands: Commands { ViewCommands() FindCommands() NavigateCommands() + TasksCommands() if sourceControlIsEnabled { SourceControlCommands() } ExtensionCommands() WindowCommands() diff --git a/CodeEdit/Features/WindowCommands/TasksCommands.swift b/CodeEdit/Features/WindowCommands/TasksCommands.swift new file mode 100644 index 0000000000..c8ef10f1e6 --- /dev/null +++ b/CodeEdit/Features/WindowCommands/TasksCommands.swift @@ -0,0 +1,66 @@ +// +// TasksCommands.swift +// CodeEdit +// +// Created by Khan Winter on 7/8/25. +// + +import SwiftUI + +struct TasksCommands: Commands { + @UpdatingWindowController var windowController: CodeEditWindowController? + + var taskManager: TaskManager? { + windowController?.workspace?.taskManager + } + + var selectedTask: CETask? { + taskManager?.availableTasks.first(where: { $0.id == taskManager?.selectedTaskID }) + } + + var body: some Commands { + CommandMenu("Tasks") { + let selectedTaskName: String? = if let selectedTask { + "\"" + selectedTask.name + "\"" + } else { + nil + } + + Button("Run \(selectedTaskName ?? "(No Selected Task)")") { + taskManager?.executeActiveTask() + } + .keyboardShortcut("r", modifiers: .command) + .disabled(taskManager?.selectedTaskID == nil) + + Button("Stop \(selectedTaskName ?? "(No Selected Task)")") { + taskManager?.terminateSelectedTask() + } + .keyboardShortcut(".", modifiers: .command) + .disabled(taskManager?.activeTasks.isEmpty == true) + + Divider() + + Menu { + if let taskManager { + ForEach(taskManager.availableTasks) { task in + Button(task.name) { + taskManager.selectedTaskID = task.id + } + } + } + } label: { + Text("Choose Task...") + } + .disabled(taskManager?.availableTasks.isEmpty == true) + + Button("Manage Tasks...") { + NSApp.sendAction( + #selector(CodeEditWindowController.openWorkspaceSettings(_:)), + to: windowController, + from: nil + ) + } + .disabled(windowController == nil) + } + } +} diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 288db12e63..ecd717e111 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -36,10 +36,8 @@ struct UpdatingWindowController: DynamicProperty { class WindowControllerBox: ObservableObject { public private(set) weak var controller: CodeEditWindowController? - private var objectWillChangeCancellable: AnyCancellable? - private var utilityAreaCancellable: AnyCancellable? // ``ViewCommands`` needs this. - private var windowCancellable: AnyCancellable? - private var activeEditorCancellable: AnyCancellable? + private var windowCancellable: AnyCancellable? // Needs to stick around between window changes. + private var cancellables: Set = [] init() { windowCancellable = NSApp.publisher(for: \.keyWindow).receive(on: RunLoop.main).sink { [weak self] window in @@ -50,25 +48,32 @@ struct UpdatingWindowController: DynamicProperty { } func setNewController(_ controller: CodeEditWindowController?) { - objectWillChangeCancellable?.cancel() - objectWillChangeCancellable = nil - utilityAreaCancellable?.cancel() - utilityAreaCancellable = nil - activeEditorCancellable?.cancel() - activeEditorCancellable = nil + cancellables.forEach { $0.cancel() } + cancellables.removeAll() self.controller = controller - objectWillChangeCancellable = controller?.objectWillChange.sink { [weak self] in + controller?.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } - utilityAreaCancellable = controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in + .store(in: &cancellables) + + controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + let activeEditor = controller?.workspace?.editorManager?.activeEditor - activeEditorCancellable = activeEditor?.objectWillChange.sink { [weak self] in + activeEditor?.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + controller?.workspace?.taskManager?.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + self.objectWillChange.send() } } diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index 3b1abbf23c..0c7a2def7a 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -59,7 +59,7 @@ final class TaskManagerTests: XCTestCase { let testExpectation1 = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { XCTAssertEqual(self.taskManager.taskStatus(taskID: task.id), .running) - self.taskManager.terminateActiveTask() + self.taskManager.terminateSelectedTask() testExpectation1.fulfill() } From 9d4760c79d092842e6f3c4037694afb30306cf91 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:15:48 -0500 Subject: [PATCH 2/2] Fix Up Commands --- CodeEdit/Features/Tasks/TaskManager.swift | 2 +- .../Tasks/Views/StopTaskToolbarButton.swift | 2 +- .../WindowCommands/TasksCommands.swift | 80 +++++++++++++++---- .../Features/Tasks/TaskManagerTests.swift | 2 +- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift index bc6eb70282..aeb96e2deb 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -93,7 +93,7 @@ class TaskManager: ObservableObject { } } - func terminateSelectedTask() { + func terminateActiveTask() { guard let taskID = selectedTaskID else { return } diff --git a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift index 9c76bbd580..559dbf0b77 100644 --- a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift @@ -24,7 +24,7 @@ struct StopTaskToolbarButton: View { HStack { if let currentSelectedStatus, currentSelectedStatus == .running { Button { - taskManager.terminateSelectedTask() + taskManager.terminateActiveTask() } label: { Label("Stop", systemImage: "stop.fill") .labelStyle(.iconOnly) diff --git a/CodeEdit/Features/WindowCommands/TasksCommands.swift b/CodeEdit/Features/WindowCommands/TasksCommands.swift index c8ef10f1e6..5ea47e6fc5 100644 --- a/CodeEdit/Features/WindowCommands/TasksCommands.swift +++ b/CodeEdit/Features/WindowCommands/TasksCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Combine struct TasksCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? @@ -14,29 +15,41 @@ struct TasksCommands: Commands { windowController?.workspace?.taskManager } - var selectedTask: CETask? { - taskManager?.availableTasks.first(where: { $0.id == taskManager?.selectedTaskID }) - } + @State private var activeTaskStatus: CETaskStatus = .notRunning + @State private var taskManagerListener: AnyCancellable? + @State private var statusListener: AnyCancellable? var body: some Commands { CommandMenu("Tasks") { - let selectedTaskName: String? = if let selectedTask { + let selectedTaskName: String = if let selectedTask = taskManager?.selectedTask { "\"" + selectedTask.name + "\"" } else { - nil + "(No Selected Task)" } - Button("Run \(selectedTaskName ?? "(No Selected Task)")") { + Button("Run \(selectedTaskName)", systemImage: "play.fill") { taskManager?.executeActiveTask() + showOutput() } - .keyboardShortcut("r", modifiers: .command) + .keyboardShortcut("R") .disabled(taskManager?.selectedTaskID == nil) - Button("Stop \(selectedTaskName ?? "(No Selected Task)")") { - taskManager?.terminateSelectedTask() + Button("Stop \(selectedTaskName)", systemImage: "stop.fill") { + taskManager?.terminateActiveTask() + } + .keyboardShortcut(".") + .onChange(of: windowController) { _ in + taskManagerListener = taskManager?.objectWillChange.sink { + updateStatusListener() + } + } + .disabled(activeTaskStatus != .running) + + Button("Show \(selectedTaskName) Output") { + showOutput() } - .keyboardShortcut(".", modifiers: .command) - .disabled(taskManager?.activeTasks.isEmpty == true) + // Disable when there's no output yet + .disabled(taskManager?.activeTasks[taskManager?.selectedTaskID ?? UUID()] == nil) Divider() @@ -48,19 +61,54 @@ struct TasksCommands: Commands { } } } + + if taskManager?.availableTasks.isEmpty ?? true { + Button("Create Tasks") { + openSettings() + } + } } label: { Text("Choose Task...") } .disabled(taskManager?.availableTasks.isEmpty == true) Button("Manage Tasks...") { - NSApp.sendAction( - #selector(CodeEditWindowController.openWorkspaceSettings(_:)), - to: windowController, - from: nil - ) + openSettings() } .disabled(windowController == nil) } } + + /// Update the ``statusListener`` to listen to a potentially new active task. + private func updateStatusListener() { + statusListener?.cancel() + guard let taskManager else { return } + + activeTaskStatus = taskManager.activeTasks[taskManager.selectedTaskID ?? UUID()]?.status ?? .notRunning + guard let id = taskManager.selectedTaskID else { return } + + statusListener = taskManager.activeTasks[id]?.$status.sink { newValue in + activeTaskStatus = newValue + } + } + + private func showOutput() { + guard let utilityAreaModel = windowController?.workspace?.utilityAreaModel else { + return + } + if utilityAreaModel.isCollapsed { + // Open the utility area + utilityAreaModel.isCollapsed.toggle() + } + utilityAreaModel.selectedTab = .debugConsole // Switch to the correct tab + taskManager?.taskShowingOutput = taskManager?.selectedTaskID // Switch to the selected task + } + + private func openSettings() { + NSApp.sendAction( + #selector(CodeEditWindowController.openWorkspaceSettings(_:)), + to: windowController, + from: nil + ) + } } diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index 0c7a2def7a..3b1abbf23c 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -59,7 +59,7 @@ final class TaskManagerTests: XCTestCase { let testExpectation1 = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { XCTAssertEqual(self.taskManager.taskStatus(taskID: task.id), .running) - self.taskManager.terminateSelectedTask() + self.taskManager.terminateActiveTask() testExpectation1.fulfill() }