tasks.rs

  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}