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}