Skip to content

Commit cfbca15

Browse files
Add ⌘R and ⌘. to Start and Stop Tasks, Add Tasks Menu (#2081)
### Description - Adds two key commands - Command R and Command . to start and stop tasks. - Adds a new Tasks menu item. Subitems: - Run {Selected Task} - Runs the selected task, disabled when no selected task. - Stop {Selected Task} - Stops the selected task, disabled when no task is running. - Show {Selected Task} Output - Navigates to the selected task output - this could maybe? be in the Navigate menu. *Looking for feedback here.* - Chose Task... - Submenu is a list of the user's tasks. Selects a task. If the user has no tasks, has a single "Create Task" item. - Manage Tasks - Opens the workspace's task settings. ### Related Issues * Thought there was an open issue but there isn't, whoops. ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots Demo with running a task, stopping it, navigating to it's output. > Note that there's a few bugs in this demo that are fixed by #2080. Such as overlapping task status and task activities appearing in all workspaces. https://github.com/user-attachments/assets/602dedf2-3626-4ea1-a04b-9d7a9945a458
1 parent f991614 commit cfbca15

File tree

3 files changed

+133
-13
lines changed

3 files changed

+133
-13
lines changed

CodeEdit/Features/WindowCommands/CodeEditCommands.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct CodeEditCommands: Commands {
1717
ViewCommands()
1818
FindCommands()
1919
NavigateCommands()
20+
TasksCommands()
2021
if sourceControlIsEnabled { SourceControlCommands() }
2122
EditorCommands()
2223
ExtensionCommands()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// TasksCommands.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 7/8/25.
6+
//
7+
8+
import SwiftUI
9+
import Combine
10+
11+
struct TasksCommands: Commands {
12+
@UpdatingWindowController var windowController: CodeEditWindowController?
13+
14+
var taskManager: TaskManager? {
15+
windowController?.workspace?.taskManager
16+
}
17+
18+
@State private var activeTaskStatus: CETaskStatus = .notRunning
19+
@State private var taskManagerListener: AnyCancellable?
20+
@State private var statusListener: AnyCancellable?
21+
22+
var body: some Commands {
23+
CommandMenu("Tasks") {
24+
let selectedTaskName: String = if let selectedTask = taskManager?.selectedTask {
25+
"\"" + selectedTask.name + "\""
26+
} else {
27+
"(No Selected Task)"
28+
}
29+
30+
Button("Run \(selectedTaskName)", systemImage: "play.fill") {
31+
taskManager?.executeActiveTask()
32+
showOutput()
33+
}
34+
.keyboardShortcut("R")
35+
.disabled(taskManager?.selectedTaskID == nil)
36+
37+
Button("Stop \(selectedTaskName)", systemImage: "stop.fill") {
38+
taskManager?.terminateActiveTask()
39+
}
40+
.keyboardShortcut(".")
41+
.onChange(of: windowController) { _ in
42+
taskManagerListener = taskManager?.objectWillChange.sink {
43+
updateStatusListener()
44+
}
45+
}
46+
.disabled(activeTaskStatus != .running)
47+
48+
Button("Show \(selectedTaskName) Output") {
49+
showOutput()
50+
}
51+
// Disable when there's no output yet
52+
.disabled(taskManager?.activeTasks[taskManager?.selectedTaskID ?? UUID()] == nil)
53+
54+
Divider()
55+
56+
Menu {
57+
if let taskManager {
58+
ForEach(taskManager.availableTasks) { task in
59+
Button(task.name) {
60+
taskManager.selectedTaskID = task.id
61+
}
62+
}
63+
}
64+
65+
if taskManager?.availableTasks.isEmpty ?? true {
66+
Button("Create Tasks") {
67+
openSettings()
68+
}
69+
}
70+
} label: {
71+
Text("Choose Task...")
72+
}
73+
.disabled(taskManager?.availableTasks.isEmpty == true)
74+
75+
Button("Manage Tasks...") {
76+
openSettings()
77+
}
78+
.disabled(windowController == nil)
79+
}
80+
}
81+
82+
/// Update the ``statusListener`` to listen to a potentially new active task.
83+
private func updateStatusListener() {
84+
statusListener?.cancel()
85+
guard let taskManager else { return }
86+
87+
activeTaskStatus = taskManager.activeTasks[taskManager.selectedTaskID ?? UUID()]?.status ?? .notRunning
88+
guard let id = taskManager.selectedTaskID else { return }
89+
90+
statusListener = taskManager.activeTasks[id]?.$status.sink { newValue in
91+
activeTaskStatus = newValue
92+
}
93+
}
94+
95+
private func showOutput() {
96+
guard let utilityAreaModel = windowController?.workspace?.utilityAreaModel else {
97+
return
98+
}
99+
if utilityAreaModel.isCollapsed {
100+
// Open the utility area
101+
utilityAreaModel.isCollapsed.toggle()
102+
}
103+
utilityAreaModel.selectedTab = .debugConsole // Switch to the correct tab
104+
taskManager?.taskShowingOutput = taskManager?.selectedTaskID // Switch to the selected task
105+
}
106+
107+
private func openSettings() {
108+
NSApp.sendAction(
109+
#selector(CodeEditWindowController.openWorkspaceSettings(_:)),
110+
to: windowController,
111+
from: nil
112+
)
113+
}
114+
}

CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ struct UpdatingWindowController: DynamicProperty {
3636
class WindowControllerBox: ObservableObject {
3737
public private(set) weak var controller: CodeEditWindowController?
3838

39-
private var objectWillChangeCancellable: AnyCancellable?
40-
private var utilityAreaCancellable: AnyCancellable? // ``ViewCommands`` needs this.
41-
private var windowCancellable: AnyCancellable?
42-
private var activeEditorCancellable: AnyCancellable?
39+
private var windowCancellable: AnyCancellable? // Needs to stick around between window changes.
40+
private var cancellables: Set<AnyCancellable> = []
4341

4442
init() {
4543
windowCancellable = NSApp.publisher(for: \.keyWindow).receive(on: RunLoop.main).sink { [weak self] window in
@@ -50,25 +48,32 @@ struct UpdatingWindowController: DynamicProperty {
5048
}
5149

5250
func setNewController(_ controller: CodeEditWindowController?) {
53-
objectWillChangeCancellable?.cancel()
54-
objectWillChangeCancellable = nil
55-
utilityAreaCancellable?.cancel()
56-
utilityAreaCancellable = nil
57-
activeEditorCancellable?.cancel()
58-
activeEditorCancellable = nil
51+
cancellables.forEach { $0.cancel() }
52+
cancellables.removeAll()
5953

6054
self.controller = controller
6155

62-
objectWillChangeCancellable = controller?.objectWillChange.sink { [weak self] in
56+
controller?.objectWillChange.sink { [weak self] in
6357
self?.objectWillChange.send()
6458
}
65-
utilityAreaCancellable = controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in
59+
.store(in: &cancellables)
60+
61+
controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in
6662
self?.objectWillChange.send()
6763
}
64+
.store(in: &cancellables)
65+
6866
let activeEditor = controller?.workspace?.editorManager?.activeEditor
69-
activeEditorCancellable = activeEditor?.objectWillChange.sink { [weak self] in
67+
activeEditor?.objectWillChange.sink { [weak self] in
68+
self?.objectWillChange.send()
69+
}
70+
.store(in: &cancellables)
71+
72+
controller?.workspace?.taskManager?.objectWillChange.sink { [weak self] in
7073
self?.objectWillChange.send()
7174
}
75+
.store(in: &cancellables)
76+
7277
self.objectWillChange.send()
7378
}
7479
}

0 commit comments

Comments
 (0)