1use std::process::ExitStatus;
2
3use anyhow::Result;
4use gpui::{AppContext, Context, Entity, Task};
5use language::Buffer;
6use project::{TaskSourceKind, WorktreeId};
7use remote::ConnectionState;
8use task::{
9 DebugScenario, ResolvedTask, SaveStrategy, SharedTaskContext, SpawnInTerminal, TaskContext,
10 TaskTemplate,
11};
12use ui::Window;
13use util::TryFutureExt;
14
15use crate::{SaveIntent, Toast, Workspace, notifications::NotificationId};
16
17impl Workspace {
18 pub fn schedule_task(
19 self: &mut Workspace,
20 task_source_kind: TaskSourceKind,
21 task_to_resolve: &TaskTemplate,
22 task_cx: &TaskContext,
23 omit_history: bool,
24 window: &mut Window,
25 cx: &mut Context<Self>,
26 ) {
27 match self.project.read(cx).remote_connection_state(cx) {
28 None | Some(ConnectionState::Connected) => {}
29 Some(
30 ConnectionState::Connecting
31 | ConnectionState::Disconnected
32 | ConnectionState::HeartbeatMissed
33 | ConnectionState::Reconnecting,
34 ) => {
35 log::warn!("Cannot schedule tasks when disconnected from a remote host");
36 return;
37 }
38 }
39
40 if let Some(spawn_in_terminal) =
41 task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
42 {
43 self.schedule_resolved_task(
44 task_source_kind,
45 spawn_in_terminal,
46 omit_history,
47 window,
48 cx,
49 );
50 }
51 }
52
53 pub fn schedule_resolved_task(
54 self: &mut Workspace,
55 task_source_kind: TaskSourceKind,
56 resolved_task: ResolvedTask,
57 omit_history: bool,
58 window: &mut Window,
59 cx: &mut Context<Workspace>,
60 ) {
61 let spawn_in_terminal = resolved_task.resolved.clone();
62 if !omit_history {
63 if let Some(debugger_provider) = self.debugger_provider.as_ref() {
64 debugger_provider.task_scheduled(cx);
65 }
66
67 self.project().update(cx, |project, cx| {
68 if let Some(task_inventory) =
69 project.task_store().read(cx).task_inventory().cloned()
70 {
71 task_inventory.update(cx, |inventory, _| {
72 inventory.task_scheduled(task_source_kind, resolved_task);
73 })
74 }
75 });
76 }
77
78 if self.terminal_provider.is_some() {
79 let task = cx.spawn_in(window, async move |workspace, cx| {
80 let save_action = match spawn_in_terminal.save {
81 SaveStrategy::All => {
82 let save_all = workspace.update_in(cx, |workspace, window, cx| {
83 let task = workspace.save_all_internal(SaveIntent::SaveAll, window, cx);
84 // Match the type of the other arm by ignoring the bool value returned
85 cx.background_spawn(async { task.await.map(|_| ()) })
86 });
87 save_all.ok()
88 }
89 SaveStrategy::Current => {
90 let save_current = workspace.update_in(cx, |workspace, window, cx| {
91 workspace.save_active_item(SaveIntent::SaveAll, window, cx)
92 });
93 save_current.ok()
94 }
95 SaveStrategy::None => None,
96 };
97 if let Some(save_action) = save_action {
98 save_action.log_err().await;
99 }
100
101 let spawn_task = workspace.update_in(cx, |workspace, window, cx| {
102 workspace
103 .terminal_provider
104 .as_ref()
105 .map(|terminal_provider| {
106 terminal_provider.spawn(spawn_in_terminal, window, cx)
107 })
108 });
109 if let Some(spawn_task) = spawn_task.ok().flatten() {
110 let res = cx.background_spawn(spawn_task).await;
111 match res {
112 Some(Ok(status)) => {
113 if status.success() {
114 log::debug!("Task spawn succeeded");
115 } else {
116 log::debug!("Task spawn failed, code: {:?}", status.code());
117 }
118 }
119 Some(Err(e)) => {
120 log::error!("Task spawn failed: {e:#}");
121 _ = workspace.update(cx, |w, cx| {
122 let id = NotificationId::unique::<ResolvedTask>();
123 w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx);
124 })
125 }
126 None => log::debug!("Task spawn got cancelled"),
127 };
128 }
129 });
130 self.scheduled_tasks.push(task);
131 }
132 }
133
134 pub fn start_debug_session(
135 &mut self,
136 scenario: DebugScenario,
137 task_context: SharedTaskContext,
138 active_buffer: Option<Entity<Buffer>>,
139 worktree_id: Option<WorktreeId>,
140 window: &mut Window,
141 cx: &mut Context<Self>,
142 ) {
143 if let Some(provider) = self.debugger_provider.as_mut() {
144 provider.start_session(
145 scenario,
146 task_context,
147 active_buffer,
148 worktree_id,
149 window,
150 cx,
151 )
152 }
153 }
154
155 pub fn spawn_in_terminal(
156 self: &mut Workspace,
157 spawn_in_terminal: SpawnInTerminal,
158 window: &mut Window,
159 cx: &mut Context<Workspace>,
160 ) -> Task<Option<Result<ExitStatus>>> {
161 if let Some(terminal_provider) = self.terminal_provider.as_ref() {
162 terminal_provider.spawn(spawn_in_terminal, window, cx)
163 } else {
164 Task::ready(None)
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::{
173 TerminalProvider,
174 item::test::{TestItem, TestProjectItem},
175 register_serializable_item,
176 };
177 use gpui::{App, TestAppContext};
178 use parking_lot::Mutex;
179 use project::{FakeFs, Project, TaskSourceKind};
180 use serde_json::json;
181 use std::sync::Arc;
182 use task::TaskTemplate;
183
184 struct Fixture {
185 workspace: Entity<Workspace>,
186 item: Entity<TestItem>,
187 task: ResolvedTask,
188 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
189 }
190
191 #[gpui::test]
192 async fn test_schedule_resolved_task_save_all(cx: &mut TestAppContext) {
193 let (fixture, cx) = create_fixture(cx, SaveStrategy::All).await;
194 fixture.workspace.update_in(cx, |workspace, window, cx| {
195 workspace.schedule_resolved_task(
196 TaskSourceKind::UserInput,
197 fixture.task,
198 false,
199 window,
200 cx,
201 );
202 });
203 cx.executor().run_until_parked();
204
205 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
206 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
207 }
208
209 #[gpui::test]
210 async fn test_schedule_resolved_task_save_current(cx: &mut TestAppContext) {
211 let (fixture, cx) = create_fixture(cx, SaveStrategy::Current).await;
212 // Add a second inactive dirty item
213 let inactive = add_test_item(&fixture.workspace, "file2.txt", false, cx);
214 fixture.workspace.update_in(cx, |workspace, window, cx| {
215 workspace.schedule_resolved_task(
216 TaskSourceKind::UserInput,
217 fixture.task,
218 false,
219 window,
220 cx,
221 );
222 });
223 cx.executor().run_until_parked();
224
225 // The active item (fixture.item) should be saved
226 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
227 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
228 // The inactive item should not be saved
229 assert!(cx.read(|cx| inactive.read(cx).is_dirty));
230 }
231
232 #[gpui::test]
233 async fn test_schedule_resolved_task_save_none(cx: &mut TestAppContext) {
234 let (fixture, cx) = create_fixture(cx, SaveStrategy::None).await;
235 fixture.workspace.update_in(cx, |workspace, window, cx| {
236 workspace.schedule_resolved_task(
237 TaskSourceKind::UserInput,
238 fixture.task,
239 false,
240 window,
241 cx,
242 );
243 });
244 cx.executor().run_until_parked();
245
246 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(true));
247 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
248 }
249
250 async fn create_fixture(
251 cx: &mut TestAppContext,
252 save_strategy: SaveStrategy,
253 ) -> (Fixture, &mut gpui::VisualTestContext) {
254 cx.update(|cx| {
255 let settings_store = settings::SettingsStore::test(cx);
256 cx.set_global(settings_store);
257 theme::init(theme::LoadThemes::JustBase, cx);
258 register_serializable_item::<TestItem>(cx);
259 });
260 let fs = FakeFs::new(cx.executor());
261 fs.insert_tree("/root", json!({ "file.txt": "dirty" }))
262 .await;
263 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
264 let (workspace, cx) =
265 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
266
267 // Add a dirty item to the workspace
268 let item = add_test_item(&workspace, "file.txt", true, cx);
269
270 let template = TaskTemplate {
271 label: "test".to_string(),
272 command: "echo".to_string(),
273 save: save_strategy,
274 ..Default::default()
275 };
276 let task = template
277 .resolve_task("test", &task::TaskContext::default())
278 .unwrap();
279 let dirty_before_spawn: Arc<Mutex<Option<bool>>> = Arc::default();
280 let terminal_provider = Box::new(TestTerminalProvider {
281 item: item.clone(),
282 dirty_before_spawn: dirty_before_spawn.clone(),
283 });
284 workspace.update(cx, |workspace, _| {
285 workspace.terminal_provider = Some(terminal_provider);
286 });
287 let fixture = Fixture {
288 workspace,
289 item,
290 task,
291 dirty_before_spawn,
292 };
293 (fixture, cx)
294 }
295
296 fn add_test_item(
297 workspace: &Entity<Workspace>,
298 name: &str,
299 active: bool,
300 cx: &mut gpui::VisualTestContext,
301 ) -> Entity<TestItem> {
302 let item = cx.new(|cx| {
303 TestItem::new(cx)
304 .with_dirty(true)
305 .with_project_items(&[TestProjectItem::new(1, name, cx)])
306 });
307 workspace.update_in(cx, |workspace, window, cx| {
308 let pane = workspace.active_pane().clone();
309 workspace.add_item(pane, Box::new(item.clone()), None, true, active, window, cx);
310 });
311 item
312 }
313
314 struct TestTerminalProvider {
315 item: Entity<TestItem>,
316 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
317 }
318
319 impl TerminalProvider for TestTerminalProvider {
320 fn spawn(
321 &self,
322 _task: task::SpawnInTerminal,
323 _window: &mut ui::Window,
324 cx: &mut App,
325 ) -> Task<Option<Result<ExitStatus>>> {
326 *self.dirty_before_spawn.lock() = Some(cx.read_entity(&self.item, |e, _| e.is_dirty));
327 Task::ready(Some(Ok(ExitStatus::default())))
328 }
329 }
330}