tasks.rs

  1use std::process::ExitStatus;
  2
  3use anyhow::Result;
  4use collections::HashSet;
  5use gpui::{AppContext, Context, Entity, Task};
  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                let save_action = match spawn_in_terminal.save {
 82                    SaveStrategy::All => {
 83                        let save_all = workspace.update_in(cx, |workspace, window, cx| {
 84                            let task = workspace.save_all_internal(SaveIntent::SaveAll, window, cx);
 85                            // Match the type of the other arm by ignoring the bool value returned
 86                            cx.background_spawn(async { task.await.map(|_| ()) })
 87                        });
 88                        save_all.ok()
 89                    }
 90                    SaveStrategy::Current => {
 91                        let save_current = workspace.update_in(cx, |workspace, window, cx| {
 92                            workspace.save_active_item(SaveIntent::SaveAll, window, cx)
 93                        });
 94                        save_current.ok()
 95                    }
 96                    SaveStrategy::None => None,
 97                };
 98                if let Some(save_action) = save_action {
 99                    save_action.log_err().await;
100                }
101
102                let spawn_task = workspace.update_in(cx, |workspace, window, cx| {
103                    workspace
104                        .terminal_provider
105                        .as_ref()
106                        .map(|terminal_provider| {
107                            terminal_provider.spawn(spawn_in_terminal, window, cx)
108                        })
109                });
110                if let Some(spawn_task) = spawn_task.ok().flatten() {
111                    let res = cx.background_spawn(spawn_task).await;
112                    match res {
113                        Some(Ok(status)) => {
114                            if status.success() {
115                                log::debug!("Task spawn succeeded");
116                            } else {
117                                log::debug!("Task spawn failed, code: {:?}", status.code());
118                            }
119                        }
120                        Some(Err(e)) => {
121                            log::error!("Task spawn failed: {e:#}");
122                            _ = workspace.update(cx, |w, cx| {
123                                let id = NotificationId::unique::<ResolvedTask>();
124                                w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx);
125                            })
126                        }
127                        None => log::debug!("Task spawn got cancelled"),
128                    };
129                }
130            });
131            self.scheduled_tasks.push(task);
132        }
133    }
134
135    pub fn start_debug_session(
136        &mut self,
137        scenario: DebugScenario,
138        task_context: SharedTaskContext,
139        active_buffer: Option<Entity<Buffer>>,
140        worktree_id: Option<WorktreeId>,
141        window: &mut Window,
142        cx: &mut Context<Self>,
143    ) {
144        if let Some(provider) = self.debugger_provider.as_mut() {
145            provider.start_session(
146                scenario,
147                task_context,
148                active_buffer,
149                worktree_id,
150                window,
151                cx,
152            )
153        }
154    }
155
156    pub fn spawn_in_terminal(
157        self: &mut Workspace,
158        spawn_in_terminal: SpawnInTerminal,
159        window: &mut Window,
160        cx: &mut Context<Workspace>,
161    ) -> Task<Option<Result<ExitStatus>>> {
162        if let Some(terminal_provider) = self.terminal_provider.as_ref() {
163            terminal_provider.spawn(spawn_in_terminal, window, cx)
164        } else {
165            Task::ready(None)
166        }
167    }
168
169    pub fn run_create_worktree_tasks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
170        let project = self.project().clone();
171        let hooks = HashSet::from_iter([TaskHook::CreateWorktree]);
172
173        let worktree_tasks: Vec<(WorktreeId, TaskContext, Vec<TaskTemplate>)> = {
174            let project = project.read(cx);
175            let task_store = project.task_store();
176            let Some(inventory) = task_store.read(cx).task_inventory().cloned() else {
177                return;
178            };
179
180            let git_store = project.git_store().read(cx);
181
182            let mut worktree_tasks = Vec::new();
183            for worktree in project.worktrees(cx) {
184                let worktree = worktree.read(cx);
185                let worktree_id = worktree.id();
186                let worktree_abs_path = worktree.abs_path();
187
188                let templates: Vec<TaskTemplate> = inventory
189                    .read(cx)
190                    .templates_with_hooks(&hooks, worktree_id)
191                    .into_iter()
192                    .map(|(_, template)| template)
193                    .collect();
194
195                if templates.is_empty() {
196                    continue;
197                }
198
199                let mut task_variables = TaskVariables::default();
200                task_variables.insert(
201                    VariableName::WorktreeRoot,
202                    worktree_abs_path.to_string_lossy().into_owned(),
203                );
204
205                if let Some(path) = git_store.original_repo_path_for_worktree(worktree_id, cx) {
206                    task_variables.insert(
207                        VariableName::MainGitWorktree,
208                        path.to_string_lossy().into_owned(),
209                    );
210                }
211
212                let task_context = TaskContext {
213                    cwd: Some(worktree_abs_path.to_path_buf()),
214                    task_variables,
215                    project_env: Default::default(),
216                };
217
218                worktree_tasks.push((worktree_id, task_context, templates));
219            }
220            worktree_tasks
221        };
222
223        if worktree_tasks.is_empty() {
224            return;
225        }
226
227        let task = cx.spawn_in(window, async move |workspace, cx| {
228            let mut tasks = Vec::new();
229            for (worktree_id, task_context, templates) in worktree_tasks {
230                let id_base = format!("worktree_setup_{worktree_id}");
231
232                tasks.push(cx.spawn({
233                    let workspace = workspace.clone();
234                    async move |cx| {
235                        for task_template in templates {
236                            let Some(resolved) =
237                                task_template.resolve_task(&id_base, &task_context)
238                            else {
239                                continue;
240                            };
241
242                            let status = workspace.update_in(cx, |workspace, window, cx| {
243                                workspace.spawn_in_terminal(resolved.resolved, window, cx)
244                            })?;
245
246                            if let Some(result) = status.await {
247                                match result {
248                                    Ok(exit_status) if !exit_status.success() => {
249                                        log::error!(
250                                            "Git worktree setup task failed with status: {:?}",
251                                            exit_status.code()
252                                        );
253                                        break;
254                                    }
255                                    Err(error) => {
256                                        log::error!("Git worktree setup task error: {error:#}");
257                                        break;
258                                    }
259                                    _ => {}
260                                }
261                            }
262                        }
263                        anyhow::Ok(())
264                    }
265                }));
266            }
267
268            futures::future::join_all(tasks).await;
269            anyhow::Ok(())
270        });
271        task.detach_and_log_err(cx);
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::{
279        TerminalProvider,
280        item::test::{TestItem, TestProjectItem},
281        register_serializable_item,
282    };
283    use gpui::{App, TestAppContext};
284    use parking_lot::Mutex;
285    use project::{FakeFs, Project, TaskSourceKind};
286    use serde_json::json;
287    use std::sync::Arc;
288    use task::TaskTemplate;
289
290    struct Fixture {
291        workspace: Entity<Workspace>,
292        item: Entity<TestItem>,
293        task: ResolvedTask,
294        dirty_before_spawn: Arc<Mutex<Option<bool>>>,
295    }
296
297    #[gpui::test]
298    async fn test_schedule_resolved_task_save_all(cx: &mut TestAppContext) {
299        let (fixture, cx) = create_fixture(cx, SaveStrategy::All).await;
300        fixture.workspace.update_in(cx, |workspace, window, cx| {
301            workspace.schedule_resolved_task(
302                TaskSourceKind::UserInput,
303                fixture.task,
304                false,
305                window,
306                cx,
307            );
308        });
309        cx.executor().run_until_parked();
310
311        assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
312        assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
313    }
314
315    #[gpui::test]
316    async fn test_schedule_resolved_task_save_current(cx: &mut TestAppContext) {
317        let (fixture, cx) = create_fixture(cx, SaveStrategy::Current).await;
318        // Add a second inactive dirty item
319        let inactive = add_test_item(&fixture.workspace, "file2.txt", false, cx);
320        fixture.workspace.update_in(cx, |workspace, window, cx| {
321            workspace.schedule_resolved_task(
322                TaskSourceKind::UserInput,
323                fixture.task,
324                false,
325                window,
326                cx,
327            );
328        });
329        cx.executor().run_until_parked();
330
331        // The active item (fixture.item) should be saved
332        assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
333        assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
334        // The inactive item should not be saved
335        assert!(cx.read(|cx| inactive.read(cx).is_dirty));
336    }
337
338    #[gpui::test]
339    async fn test_schedule_resolved_task_save_none(cx: &mut TestAppContext) {
340        let (fixture, cx) = create_fixture(cx, SaveStrategy::None).await;
341        fixture.workspace.update_in(cx, |workspace, window, cx| {
342            workspace.schedule_resolved_task(
343                TaskSourceKind::UserInput,
344                fixture.task,
345                false,
346                window,
347                cx,
348            );
349        });
350        cx.executor().run_until_parked();
351
352        assert_eq!(*fixture.dirty_before_spawn.lock(), Some(true));
353        assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
354    }
355
356    async fn create_fixture(
357        cx: &mut TestAppContext,
358        save_strategy: SaveStrategy,
359    ) -> (Fixture, &mut gpui::VisualTestContext) {
360        cx.update(|cx| {
361            let settings_store = settings::SettingsStore::test(cx);
362            cx.set_global(settings_store);
363            theme_settings::init(theme::LoadThemes::JustBase, cx);
364            register_serializable_item::<TestItem>(cx);
365        });
366        let fs = FakeFs::new(cx.executor());
367        fs.insert_tree("/root", json!({ "file.txt": "dirty" }))
368            .await;
369        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
370        let (workspace, cx) =
371            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
372
373        // Add a dirty item to the workspace
374        let item = add_test_item(&workspace, "file.txt", true, cx);
375
376        let template = TaskTemplate {
377            label: "test".to_string(),
378            command: "echo".to_string(),
379            save: save_strategy,
380            ..Default::default()
381        };
382        let task = template
383            .resolve_task("test", &task::TaskContext::default())
384            .unwrap();
385        let dirty_before_spawn: Arc<Mutex<Option<bool>>> = Arc::default();
386        let terminal_provider = Box::new(TestTerminalProvider {
387            item: item.clone(),
388            dirty_before_spawn: dirty_before_spawn.clone(),
389        });
390        workspace.update(cx, |workspace, _| {
391            workspace.terminal_provider = Some(terminal_provider);
392        });
393        let fixture = Fixture {
394            workspace,
395            item,
396            task,
397            dirty_before_spawn,
398        };
399        (fixture, cx)
400    }
401
402    fn add_test_item(
403        workspace: &Entity<Workspace>,
404        name: &str,
405        active: bool,
406        cx: &mut gpui::VisualTestContext,
407    ) -> Entity<TestItem> {
408        let item = cx.new(|cx| {
409            TestItem::new(cx)
410                .with_dirty(true)
411                .with_project_items(&[TestProjectItem::new(1, name, cx)])
412        });
413        workspace.update_in(cx, |workspace, window, cx| {
414            let pane = workspace.active_pane().clone();
415            workspace.add_item(pane, Box::new(item.clone()), None, true, active, window, cx);
416        });
417        item
418    }
419
420    struct TestTerminalProvider {
421        item: Entity<TestItem>,
422        dirty_before_spawn: Arc<Mutex<Option<bool>>>,
423    }
424
425    impl TerminalProvider for TestTerminalProvider {
426        fn spawn(
427            &self,
428            _task: task::SpawnInTerminal,
429            _window: &mut ui::Window,
430            cx: &mut App,
431        ) -> Task<Option<Result<ExitStatus>>> {
432            *self.dirty_before_spawn.lock() = Some(cx.read_entity(&self.item, |e, _| e.is_dirty));
433            Task::ready(Some(Ok(ExitStatus::default())))
434        }
435    }
436}