diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index ef5714559..e2f1ba7e6 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 000000000..5ea47e6fc --- /dev/null +++ b/CodeEdit/Features/WindowCommands/TasksCommands.swift @@ -0,0 +1,114 @@ +// +// TasksCommands.swift +// CodeEdit +// +// Created by Khan Winter on 7/8/25. +// + +import SwiftUI +import Combine + +struct TasksCommands: Commands { + @UpdatingWindowController var windowController: CodeEditWindowController? + + var taskManager: TaskManager? { + windowController?.workspace?.taskManager + } + + @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 = taskManager?.selectedTask { + "\"" + selectedTask.name + "\"" + } else { + "(No Selected Task)" + } + + Button("Run \(selectedTaskName)", systemImage: "play.fill") { + taskManager?.executeActiveTask() + showOutput() + } + .keyboardShortcut("R") + .disabled(taskManager?.selectedTaskID == nil) + + 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() + } + // Disable when there's no output yet + .disabled(taskManager?.activeTasks[taskManager?.selectedTaskID ?? UUID()] == nil) + + Divider() + + Menu { + if let taskManager { + ForEach(taskManager.availableTasks) { task in + Button(task.name) { + taskManager.selectedTaskID = task.id + } + } + } + + if taskManager?.availableTasks.isEmpty ?? true { + Button("Create Tasks") { + openSettings() + } + } + } label: { + Text("Choose Task...") + } + .disabled(taskManager?.availableTasks.isEmpty == true) + + Button("Manage Tasks...") { + 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/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 288db12e6..ecd717e11 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() } }