1use std::process::ExitStatus;
2
3use anyhow::Result;
4use collections::HashSet;
5use gpui::{AppContext, AsyncWindowContext, Context, Entity, Task, WeakEntity};
6use language::Buffer;
7use project::{TaskSourceKind, WorktreeId};
8use remote::ConnectionState;
9use task::{
10 DebugScenario, ResolvedTask, SaveStrategy, SharedTaskContext, SpawnInTerminal, TaskContext,
11 TaskHook, TaskTemplate, TaskVariables, VariableName,
12};
13use ui::Window;
14use util::TryFutureExt;
15
16use crate::{SaveIntent, Toast, Workspace, notifications::NotificationId};
17
18impl Workspace {
19 pub fn schedule_task(
20 self: &mut Workspace,
21 task_source_kind: TaskSourceKind,
22 task_to_resolve: &TaskTemplate,
23 task_cx: &TaskContext,
24 omit_history: bool,
25 window: &mut Window,
26 cx: &mut Context<Self>,
27 ) {
28 match self.project.read(cx).remote_connection_state(cx) {
29 None | Some(ConnectionState::Connected) => {}
30 Some(
31 ConnectionState::Connecting
32 | ConnectionState::Disconnected
33 | ConnectionState::HeartbeatMissed
34 | ConnectionState::Reconnecting,
35 ) => {
36 log::warn!("Cannot schedule tasks when disconnected from a remote host");
37 return;
38 }
39 }
40
41 if let Some(spawn_in_terminal) =
42 task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
43 {
44 self.schedule_resolved_task(
45 task_source_kind,
46 spawn_in_terminal,
47 omit_history,
48 window,
49 cx,
50 );
51 }
52 }
53
54 pub fn schedule_resolved_task(
55 self: &mut Workspace,
56 task_source_kind: TaskSourceKind,
57 resolved_task: ResolvedTask,
58 omit_history: bool,
59 window: &mut Window,
60 cx: &mut Context<Workspace>,
61 ) {
62 let spawn_in_terminal = resolved_task.resolved.clone();
63 if !omit_history {
64 if let Some(debugger_provider) = self.debugger_provider.as_ref() {
65 debugger_provider.task_scheduled(cx);
66 }
67
68 self.project().update(cx, |project, cx| {
69 if let Some(task_inventory) =
70 project.task_store().read(cx).task_inventory().cloned()
71 {
72 task_inventory.update(cx, |inventory, _| {
73 inventory.task_scheduled(task_source_kind, resolved_task);
74 })
75 }
76 });
77 }
78
79 if self.terminal_provider.is_some() {
80 let task = cx.spawn_in(window, async move |workspace, cx| {
81 Self::save_for_task(&workspace, spawn_in_terminal.save, cx).await;
82
83 let spawn_task = workspace.update_in(cx, |workspace, window, cx| {
84 workspace
85 .terminal_provider
86 .as_ref()
87 .map(|terminal_provider| {
88 terminal_provider.spawn(spawn_in_terminal, window, cx)
89 })
90 });
91 if let Some(spawn_task) = spawn_task.ok().flatten() {
92 let res = cx.background_spawn(spawn_task).await;
93 match res {
94 Some(Ok(status)) => {
95 if status.success() {
96 log::debug!("Task spawn succeeded");
97 } else {
98 log::debug!("Task spawn failed, code: {:?}", status.code());
99 }
100 }
101 Some(Err(e)) => {
102 log::error!("Task spawn failed: {e:#}");
103 _ = workspace.update(cx, |w, cx| {
104 let id = NotificationId::unique::<ResolvedTask>();
105 w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx);
106 })
107 }
108 None => log::debug!("Task spawn got cancelled"),
109 };
110 }
111 });
112 self.scheduled_tasks.push(task);
113 }
114 }
115
116 pub async fn save_for_task(
117 workspace: &WeakEntity<Self>,
118 save_strategy: SaveStrategy,
119 cx: &mut AsyncWindowContext,
120 ) {
121 let save_action = match save_strategy {
122 SaveStrategy::All => {
123 let save_all = workspace.update_in(cx, |workspace, window, cx| {
124 let task = workspace.save_all_internal(SaveIntent::SaveAll, window, cx);
125 cx.background_spawn(async { task.await.map(|_| ()) })
126 });
127 save_all.ok()
128 }
129 SaveStrategy::Current => {
130 let save_current = workspace.update_in(cx, |workspace, window, cx| {
131 workspace.save_active_item(SaveIntent::SaveAll, window, cx)
132 });
133 save_current.ok()
134 }
135 SaveStrategy::None => None,
136 };
137 if let Some(save_action) = save_action {
138 save_action.log_err().await;
139 }
140 }
141
142 pub fn start_debug_session(
143 &mut self,
144 scenario: DebugScenario,
145 task_context: SharedTaskContext,
146 active_buffer: Option<Entity<Buffer>>,
147 worktree_id: Option<WorktreeId>,
148 window: &mut Window,
149 cx: &mut Context<Self>,
150 ) {
151 if let Some(provider) = self.debugger_provider.as_mut() {
152 provider.start_session(
153 scenario,
154 task_context,
155 active_buffer,
156 worktree_id,
157 window,
158 cx,
159 )
160 }
161 }
162
163 pub fn spawn_in_terminal(
164 self: &mut Workspace,
165 spawn_in_terminal: SpawnInTerminal,
166 window: &mut Window,
167 cx: &mut Context<Workspace>,
168 ) -> Task<Option<Result<ExitStatus>>> {
169 if let Some(terminal_provider) = self.terminal_provider.as_ref() {
170 terminal_provider.spawn(spawn_in_terminal, window, cx)
171 } else {
172 Task::ready(None)
173 }
174 }
175
176 pub fn run_create_worktree_tasks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
177 let project = self.project().clone();
178 let hooks = HashSet::from_iter([TaskHook::CreateWorktree]);
179
180 let worktree_tasks: Vec<(WorktreeId, TaskContext, Vec<TaskTemplate>)> = {
181 let project = project.read(cx);
182 let task_store = project.task_store();
183 let Some(inventory) = task_store.read(cx).task_inventory().cloned() else {
184 return;
185 };
186
187 let git_store = project.git_store().read(cx);
188
189 let mut worktree_tasks = Vec::new();
190 for worktree in project.worktrees(cx) {
191 let worktree = worktree.read(cx);
192 let worktree_id = worktree.id();
193 let worktree_abs_path = worktree.abs_path();
194
195 let templates: Vec<TaskTemplate> = inventory
196 .read(cx)
197 .templates_with_hooks(&hooks, worktree_id)
198 .into_iter()
199 .map(|(_, template)| template)
200 .collect();
201
202 if templates.is_empty() {
203 continue;
204 }
205
206 let mut task_variables = TaskVariables::default();
207 task_variables.insert(
208 VariableName::WorktreeRoot,
209 worktree_abs_path.to_string_lossy().into_owned(),
210 );
211
212 if let Some(path) = git_store.original_repo_path_for_worktree(worktree_id, cx) {
213 task_variables.insert(
214 VariableName::MainGitWorktree,
215 path.to_string_lossy().into_owned(),
216 );
217 }
218
219 let task_context = TaskContext {
220 cwd: Some(worktree_abs_path.to_path_buf()),
221 task_variables,
222 project_env: Default::default(),
223 };
224
225 worktree_tasks.push((worktree_id, task_context, templates));
226 }
227 worktree_tasks
228 };
229
230 if worktree_tasks.is_empty() {
231 return;
232 }
233
234 let task = cx.spawn_in(window, async move |workspace, cx| {
235 let mut tasks = Vec::new();
236 for (worktree_id, task_context, templates) in worktree_tasks {
237 let id_base = format!("worktree_setup_{worktree_id}");
238
239 tasks.push(cx.spawn({
240 let workspace = workspace.clone();
241 async move |cx| {
242 for task_template in templates {
243 let Some(resolved) =
244 task_template.resolve_task(&id_base, &task_context)
245 else {
246 continue;
247 };
248
249 let status = workspace.update_in(cx, |workspace, window, cx| {
250 workspace.spawn_in_terminal(resolved.resolved, window, cx)
251 })?;
252
253 if let Some(result) = status.await {
254 match result {
255 Ok(exit_status) if !exit_status.success() => {
256 log::error!(
257 "Git worktree setup task failed with status: {:?}",
258 exit_status.code()
259 );
260 break;
261 }
262 Err(error) => {
263 log::error!("Git worktree setup task error: {error:#}");
264 break;
265 }
266 _ => {}
267 }
268 }
269 }
270 anyhow::Ok(())
271 }
272 }));
273 }
274
275 futures::future::join_all(tasks).await;
276 anyhow::Ok(())
277 });
278 task.detach_and_log_err(cx);
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::{
286 TerminalProvider,
287 item::test::{TestItem, TestProjectItem},
288 register_serializable_item,
289 };
290 use gpui::{App, TestAppContext};
291 use parking_lot::Mutex;
292 use project::{FakeFs, Project, TaskSourceKind};
293 use serde_json::json;
294 use std::sync::Arc;
295 use task::TaskTemplate;
296
297 struct Fixture {
298 workspace: Entity<Workspace>,
299 item: Entity<TestItem>,
300 task: ResolvedTask,
301 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
302 }
303
304 #[gpui::test]
305 async fn test_schedule_resolved_task_save_all(cx: &mut TestAppContext) {
306 let (fixture, cx) = create_fixture(cx, SaveStrategy::All).await;
307 fixture.workspace.update_in(cx, |workspace, window, cx| {
308 workspace.schedule_resolved_task(
309 TaskSourceKind::UserInput,
310 fixture.task,
311 false,
312 window,
313 cx,
314 );
315 });
316 cx.executor().run_until_parked();
317
318 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
319 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
320 }
321
322 #[gpui::test]
323 async fn test_schedule_resolved_task_save_current(cx: &mut TestAppContext) {
324 let (fixture, cx) = create_fixture(cx, SaveStrategy::Current).await;
325 // Add a second inactive dirty item
326 let inactive = add_test_item(&fixture.workspace, "file2.txt", false, cx);
327 fixture.workspace.update_in(cx, |workspace, window, cx| {
328 workspace.schedule_resolved_task(
329 TaskSourceKind::UserInput,
330 fixture.task,
331 false,
332 window,
333 cx,
334 );
335 });
336 cx.executor().run_until_parked();
337
338 // The active item (fixture.item) should be saved
339 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
340 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
341 // The inactive item should not be saved
342 assert!(cx.read(|cx| inactive.read(cx).is_dirty));
343 }
344
345 #[gpui::test]
346 async fn test_schedule_resolved_task_save_none(cx: &mut TestAppContext) {
347 let (fixture, cx) = create_fixture(cx, SaveStrategy::None).await;
348 fixture.workspace.update_in(cx, |workspace, window, cx| {
349 workspace.schedule_resolved_task(
350 TaskSourceKind::UserInput,
351 fixture.task,
352 false,
353 window,
354 cx,
355 );
356 });
357 cx.executor().run_until_parked();
358
359 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(true));
360 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
361 }
362
363 async fn create_fixture(
364 cx: &mut TestAppContext,
365 save_strategy: SaveStrategy,
366 ) -> (Fixture, &mut gpui::VisualTestContext) {
367 cx.update(|cx| {
368 let settings_store = settings::SettingsStore::test(cx);
369 cx.set_global(settings_store);
370 theme_settings::init(theme::LoadThemes::JustBase, cx);
371 register_serializable_item::<TestItem>(cx);
372 });
373 let fs = FakeFs::new(cx.executor());
374 fs.insert_tree("/root", json!({ "file.txt": "dirty" }))
375 .await;
376 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
377 let (workspace, cx) =
378 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
379
380 // Add a dirty item to the workspace
381 let item = add_test_item(&workspace, "file.txt", true, cx);
382
383 let template = TaskTemplate {
384 label: "test".to_string(),
385 command: "echo".to_string(),
386 save: save_strategy,
387 ..Default::default()
388 };
389 let task = template
390 .resolve_task("test", &task::TaskContext::default())
391 .unwrap();
392 let dirty_before_spawn: Arc<Mutex<Option<bool>>> = Arc::default();
393 let terminal_provider = Box::new(TestTerminalProvider {
394 item: item.clone(),
395 dirty_before_spawn: dirty_before_spawn.clone(),
396 });
397 workspace.update(cx, |workspace, _| {
398 workspace.terminal_provider = Some(terminal_provider);
399 });
400 let fixture = Fixture {
401 workspace,
402 item,
403 task,
404 dirty_before_spawn,
405 };
406 (fixture, cx)
407 }
408
409 fn add_test_item(
410 workspace: &Entity<Workspace>,
411 name: &str,
412 active: bool,
413 cx: &mut gpui::VisualTestContext,
414 ) -> Entity<TestItem> {
415 let item = cx.new(|cx| {
416 TestItem::new(cx)
417 .with_dirty(true)
418 .with_project_items(&[TestProjectItem::new(1, name, cx)])
419 });
420 workspace.update_in(cx, |workspace, window, cx| {
421 let pane = workspace.active_pane().clone();
422 workspace.add_item(pane, Box::new(item.clone()), None, true, active, window, cx);
423 });
424 item
425 }
426
427 #[gpui::test]
428 async fn test_save_for_task_all(cx: &mut TestAppContext) {
429 let (fixture, cx) = create_fixture(cx, SaveStrategy::All).await;
430 let workspace = fixture.workspace.downgrade();
431 cx.run_until_parked();
432
433 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
434 fixture.workspace.update_in(cx, |_workspace, window, cx| {
435 cx.spawn_in(window, {
436 let workspace = workspace.clone();
437 async move |_this, cx| {
438 Workspace::save_for_task(&workspace, SaveStrategy::All, cx).await;
439 }
440 })
441 .detach();
442 });
443 cx.run_until_parked();
444 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
445 }
446
447 #[gpui::test]
448 async fn test_save_for_task_none(cx: &mut TestAppContext) {
449 let (fixture, cx) = create_fixture(cx, SaveStrategy::None).await;
450 let workspace = fixture.workspace.downgrade();
451 cx.run_until_parked();
452
453 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
454 fixture.workspace.update_in(cx, |_workspace, window, cx| {
455 cx.spawn_in(window, {
456 let workspace = workspace.clone();
457 async move |_this, cx| {
458 Workspace::save_for_task(&workspace, SaveStrategy::None, cx).await;
459 }
460 })
461 .detach();
462 });
463 cx.run_until_parked();
464 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
465 }
466
467 #[gpui::test]
468 async fn test_save_for_task_current(cx: &mut TestAppContext) {
469 let (fixture, cx) = create_fixture(cx, SaveStrategy::Current).await;
470 let inactive = add_test_item(&fixture.workspace, "file2.txt", false, cx);
471 let workspace = fixture.workspace.downgrade();
472 cx.run_until_parked();
473
474 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
475 assert!(cx.read(|cx| inactive.read(cx).is_dirty));
476 fixture.workspace.update_in(cx, |_workspace, window, cx| {
477 cx.spawn_in(window, {
478 let workspace = workspace.clone();
479 async move |_this, cx| {
480 Workspace::save_for_task(&workspace, SaveStrategy::Current, cx).await;
481 }
482 })
483 .detach();
484 });
485 cx.run_until_parked();
486 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
487 assert!(cx.read(|cx| inactive.read(cx).is_dirty));
488 }
489
490 struct TestTerminalProvider {
491 item: Entity<TestItem>,
492 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
493 }
494
495 impl TerminalProvider for TestTerminalProvider {
496 fn spawn(
497 &self,
498 _task: task::SpawnInTerminal,
499 _window: &mut ui::Window,
500 cx: &mut App,
501 ) -> Task<Option<Result<ExitStatus>>> {
502 *self.dirty_before_spawn.lock() = Some(cx.read_entity(&self.item, |e, _| e.is_dirty));
503 Task::ready(Some(Ok(ExitStatus::default())))
504 }
505 }
506}