tasks_ui.rs

  1use std::collections::HashMap;
  2use std::path::Path;
  3
  4use ::settings::Settings;
  5use editor::Editor;
  6use gpui::{App, AppContext as _, Context, Entity, Task, Window};
  7use modal::{TaskOverrides, TasksModal};
  8use project::{Location, TaskContexts, Worktree, WorktreeId};
  9use task::{RevealTarget, TaskContext, TaskId, TaskVariables, VariableName};
 10use workspace::tasks::schedule_task;
 11use workspace::{tasks::schedule_resolved_task, Workspace};
 12
 13mod modal;
 14mod settings;
 15
 16pub use modal::{Rerun, Spawn};
 17
 18pub fn init(cx: &mut App) {
 19    settings::TaskSettings::register(cx);
 20    cx.observe_new(
 21        |workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>| {
 22            workspace
 23                .register_action(spawn_task_or_modal)
 24                .register_action(move |workspace, action: &modal::Rerun, window, cx| {
 25                    if let Some((task_source_kind, mut last_scheduled_task)) = workspace
 26                        .project()
 27                        .read(cx)
 28                        .task_store()
 29                        .read(cx)
 30                        .task_inventory()
 31                        .and_then(|inventory| {
 32                            inventory.read(cx).last_scheduled_task(
 33                                action
 34                                    .task_id
 35                                    .as_ref()
 36                                    .map(|id| TaskId(id.clone()))
 37                                    .as_ref(),
 38                            )
 39                        })
 40                    {
 41                        if action.reevaluate_context {
 42                            let mut original_task = last_scheduled_task.original_task().clone();
 43                            if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 44                                original_task.allow_concurrent_runs = allow_concurrent_runs;
 45                            }
 46                            if let Some(use_new_terminal) = action.use_new_terminal {
 47                                original_task.use_new_terminal = use_new_terminal;
 48                            }
 49                            let task_contexts = task_contexts(workspace, window, cx);
 50                            cx.spawn_in(window, |workspace, mut cx| async move {
 51                                let task_contexts = task_contexts.await;
 52                                workspace
 53                                    .update_in(&mut cx, |workspace, window, cx| {
 54                                        if let Some(task_context) = task_contexts.active_context() {
 55                                            schedule_task(
 56                                                workspace,
 57                                                task_source_kind,
 58                                                &original_task,
 59                                                task_context,
 60                                                false,
 61                                                cx,
 62                                            )
 63                                        } else {
 64                                            toggle_modal(workspace, None, window, cx).detach();
 65                                        }
 66                                    })
 67                                    .ok()
 68                            })
 69                            .detach()
 70                        } else {
 71                            if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
 72                                if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 73                                    resolved.allow_concurrent_runs = allow_concurrent_runs;
 74                                }
 75                                if let Some(use_new_terminal) = action.use_new_terminal {
 76                                    resolved.use_new_terminal = use_new_terminal;
 77                                }
 78                            }
 79
 80                            schedule_resolved_task(
 81                                workspace,
 82                                task_source_kind,
 83                                last_scheduled_task,
 84                                false,
 85                                cx,
 86                            );
 87                        }
 88                    } else {
 89                        toggle_modal(workspace, None, window, cx).detach();
 90                    };
 91                });
 92        },
 93    )
 94    .detach();
 95}
 96
 97fn spawn_task_or_modal(
 98    workspace: &mut Workspace,
 99    action: &Spawn,
100    window: &mut Window,
101    cx: &mut Context<Workspace>,
102) {
103    match action {
104        Spawn::ByName {
105            task_name,
106            reveal_target,
107        } => {
108            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
109                reveal_target: Some(reveal_target),
110            });
111            spawn_task_with_name(task_name.clone(), overrides, window, cx).detach_and_log_err(cx)
112        }
113        Spawn::ViaModal { reveal_target } => {
114            toggle_modal(workspace, *reveal_target, window, cx).detach()
115        }
116    }
117}
118
119fn toggle_modal(
120    workspace: &mut Workspace,
121    reveal_target: Option<RevealTarget>,
122    window: &mut Window,
123    cx: &mut Context<Workspace>,
124) -> Task<()> {
125    let task_store = workspace.project().read(cx).task_store().clone();
126    let workspace_handle = workspace.weak_handle();
127    let can_open_modal = workspace.project().update(cx, |project, cx| {
128        project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
129    });
130    if can_open_modal {
131        let task_contexts = task_contexts(workspace, window, cx);
132        cx.spawn_in(window, |workspace, mut cx| async move {
133            let task_contexts = task_contexts.await;
134            workspace
135                .update_in(&mut cx, |workspace, window, cx| {
136                    workspace.toggle_modal(window, cx, |window, cx| {
137                        TasksModal::new(
138                            task_store.clone(),
139                            task_contexts,
140                            reveal_target.map(|target| TaskOverrides {
141                                reveal_target: Some(target),
142                            }),
143                            workspace_handle,
144                            window,
145                            cx,
146                        )
147                    })
148                })
149                .ok();
150        })
151    } else {
152        Task::ready(())
153    }
154}
155
156fn spawn_task_with_name(
157    name: String,
158    overrides: Option<TaskOverrides>,
159    window: &mut Window,
160    cx: &mut Context<Workspace>,
161) -> Task<anyhow::Result<()>> {
162    cx.spawn_in(window, |workspace, mut cx| async move {
163        let task_contexts = workspace.update_in(&mut cx, |workspace, window, cx| {
164            task_contexts(workspace, window, cx)
165        })?;
166        let task_contexts = task_contexts.await;
167        let tasks = workspace.update(&mut cx, |workspace, cx| {
168            let Some(task_inventory) = workspace
169                .project()
170                .read(cx)
171                .task_store()
172                .read(cx)
173                .task_inventory()
174                .cloned()
175            else {
176                return Vec::new();
177            };
178            let (worktree, location) = active_item_selection_properties(workspace, cx);
179            let (file, language) = location
180                .map(|location| {
181                    let buffer = location.buffer.read(cx);
182                    (
183                        buffer.file().cloned(),
184                        buffer.language_at(location.range.start),
185                    )
186                })
187                .unwrap_or_default();
188            task_inventory
189                .read(cx)
190                .list_tasks(file, language, worktree, cx)
191        })?;
192
193        let did_spawn = workspace
194            .update(&mut cx, |workspace, cx| {
195                let (task_source_kind, mut target_task) =
196                    tasks.into_iter().find(|(_, task)| task.label == name)?;
197                if let Some(overrides) = &overrides {
198                    if let Some(target_override) = overrides.reveal_target {
199                        target_task.reveal_target = target_override;
200                    }
201                }
202                let default_context = TaskContext::default();
203                let active_context = task_contexts.active_context().unwrap_or(&default_context);
204                schedule_task(
205                    workspace,
206                    task_source_kind,
207                    &target_task,
208                    active_context,
209                    false,
210                    cx,
211                );
212                Some(())
213            })?
214            .is_some();
215        if !did_spawn {
216            workspace
217                .update_in(&mut cx, |workspace, window, cx| {
218                    spawn_task_or_modal(
219                        workspace,
220                        &Spawn::ViaModal {
221                            reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
222                        },
223                        window,
224                        cx,
225                    );
226                })
227                .ok();
228        }
229
230        Ok(())
231    })
232}
233
234fn active_item_selection_properties(
235    workspace: &Workspace,
236    cx: &mut App,
237) -> (Option<WorktreeId>, Option<Location>) {
238    let active_item = workspace.active_item(cx);
239    let worktree_id = active_item
240        .as_ref()
241        .and_then(|item| item.project_path(cx))
242        .map(|path| path.worktree_id)
243        .filter(|worktree_id| {
244            workspace
245                .project()
246                .read(cx)
247                .worktree_for_id(*worktree_id, cx)
248                .map_or(false, |worktree| is_visible_directory(&worktree, cx))
249        });
250    let location = active_item
251        .and_then(|active_item| active_item.act_as::<Editor>(cx))
252        .and_then(|editor| {
253            editor.update(cx, |editor, cx| {
254                let selection = editor.selections.newest_anchor();
255                let multi_buffer = editor.buffer().clone();
256                let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
257                let (buffer_snapshot, buffer_offset) =
258                    multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
259                let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
260                let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
261                Some(Location {
262                    buffer,
263                    range: buffer_anchor..buffer_anchor,
264                })
265            })
266        });
267    (worktree_id, location)
268}
269
270fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Task<TaskContexts> {
271    let active_item = workspace.active_item(cx);
272    let active_worktree = active_item
273        .as_ref()
274        .and_then(|item| item.project_path(cx))
275        .map(|project_path| project_path.worktree_id)
276        .filter(|worktree_id| {
277            workspace
278                .project()
279                .read(cx)
280                .worktree_for_id(*worktree_id, cx)
281                .map_or(false, |worktree| is_visible_directory(&worktree, cx))
282        });
283
284    let editor_context_task =
285        active_item
286            .and_then(|item| item.act_as::<Editor>(cx))
287            .map(|active_editor| {
288                active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
289            });
290
291    let mut worktree_abs_paths = workspace
292        .worktrees(cx)
293        .filter(|worktree| is_visible_directory(worktree, cx))
294        .map(|worktree| {
295            let worktree = worktree.read(cx);
296            (worktree.id(), worktree.abs_path())
297        })
298        .collect::<HashMap<_, _>>();
299
300    cx.background_spawn(async move {
301        let mut task_contexts = TaskContexts::default();
302
303        if let Some(editor_context_task) = editor_context_task {
304            if let Some(editor_context) = editor_context_task.await {
305                task_contexts.active_item_context = Some((active_worktree, editor_context));
306            }
307        }
308
309        if let Some(active_worktree) = active_worktree {
310            if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) {
311                task_contexts.active_worktree_context =
312                    Some((active_worktree, worktree_context(&active_worktree_abs_path)));
313            }
314        } else if worktree_abs_paths.len() == 1 {
315            task_contexts.active_worktree_context = worktree_abs_paths
316                .drain()
317                .next()
318                .map(|(id, abs_path)| (id, worktree_context(&abs_path)));
319        }
320
321        task_contexts.other_worktree_contexts.extend(
322            worktree_abs_paths
323                .into_iter()
324                .map(|(id, abs_path)| (id, worktree_context(&abs_path))),
325        );
326        task_contexts
327    })
328}
329
330fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool {
331    let worktree = worktree.read(cx);
332    worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir())
333}
334
335fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
336    let mut task_variables = TaskVariables::default();
337    task_variables.insert(
338        VariableName::WorktreeRoot,
339        worktree_abs_path.to_string_lossy().to_string(),
340    );
341    TaskContext {
342        cwd: Some(worktree_abs_path.to_path_buf()),
343        task_variables,
344        project_env: HashMap::default(),
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use std::{collections::HashMap, sync::Arc};
351
352    use editor::Editor;
353    use gpui::TestAppContext;
354    use language::{Language, LanguageConfig};
355    use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
356    use serde_json::json;
357    use task::{TaskContext, TaskVariables, VariableName};
358    use ui::VisualContext;
359    use util::{path, separator};
360    use workspace::{AppState, Workspace};
361
362    use crate::task_contexts;
363
364    #[gpui::test]
365    async fn test_default_language_context(cx: &mut TestAppContext) {
366        init_test(cx);
367        let fs = FakeFs::new(cx.executor());
368        fs.insert_tree(
369            path!("/dir"),
370            json!({
371                ".zed": {
372                    "tasks.json": r#"[
373                            {
374                                "label": "example task",
375                                "command": "echo",
376                                "args": ["4"]
377                            },
378                            {
379                                "label": "another one",
380                                "command": "echo",
381                                "args": ["55"]
382                            },
383                        ]"#,
384                },
385                "a.ts": "function this_is_a_test() { }",
386                "rust": {
387                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
388                }
389
390            }),
391        )
392        .await;
393        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
394        let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
395        let rust_language = Arc::new(
396            Language::new(
397                LanguageConfig::default(),
398                Some(tree_sitter_rust::LANGUAGE.into()),
399            )
400            .with_outline_query(
401                r#"(function_item
402            "fn" @context
403            name: (_) @name) @item"#,
404            )
405            .unwrap()
406            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
407                worktree_store.clone(),
408            )))),
409        );
410
411        let typescript_language = Arc::new(
412            Language::new(
413                LanguageConfig::default(),
414                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
415            )
416            .with_outline_query(
417                r#"(function_declaration
418                    "async"? @context
419                    "function" @context
420                    name: (_) @name
421                    parameters: (formal_parameters
422                        "(" @context
423                        ")" @context)) @item"#,
424            )
425            .unwrap()
426            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
427                worktree_store.clone(),
428            )))),
429        );
430
431        let worktree_id = project.update(cx, |project, cx| {
432            project.worktrees(cx).next().unwrap().read(cx).id()
433        });
434        let (workspace, cx) =
435            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
436
437        let buffer1 = workspace
438            .update(cx, |this, cx| {
439                this.project()
440                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
441            })
442            .await
443            .unwrap();
444        buffer1.update(cx, |this, cx| {
445            this.set_language(Some(typescript_language), cx)
446        });
447        let editor1 = cx.new_window_entity(|window, cx| {
448            Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
449        });
450        let buffer2 = workspace
451            .update(cx, |this, cx| {
452                this.project().update(cx, |this, cx| {
453                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
454                })
455            })
456            .await
457            .unwrap();
458        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
459        let editor2 = cx
460            .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
461
462        let first_context = workspace
463            .update_in(cx, |workspace, window, cx| {
464                workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
465                workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
466                assert_eq!(
467                    workspace.active_item(cx).unwrap().item_id(),
468                    editor2.entity_id()
469                );
470                task_contexts(workspace, window, cx)
471            })
472            .await;
473
474        assert_eq!(
475            first_context
476                .active_context()
477                .expect("Should have an active context"),
478            &TaskContext {
479                cwd: Some(path!("/dir").into()),
480                task_variables: TaskVariables::from_iter([
481                    (VariableName::File, path!("/dir/rust/b.rs").into()),
482                    (VariableName::Filename, "b.rs".into()),
483                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
484                    (VariableName::Dirname, path!("/dir/rust").into()),
485                    (VariableName::Stem, "b".into()),
486                    (VariableName::WorktreeRoot, path!("/dir").into()),
487                    (VariableName::Row, "1".into()),
488                    (VariableName::Column, "1".into()),
489                ]),
490                project_env: HashMap::default(),
491            }
492        );
493
494        // And now, let's select an identifier.
495        editor2.update_in(cx, |editor, window, cx| {
496            editor.change_selections(None, window, cx, |selections| {
497                selections.select_ranges([14..18])
498            })
499        });
500
501        assert_eq!(
502            workspace
503                .update_in(cx, |workspace, window, cx| {
504                    task_contexts(workspace, window, cx)
505                })
506                .await
507                .active_context()
508                .expect("Should have an active context"),
509            &TaskContext {
510                cwd: Some(path!("/dir").into()),
511                task_variables: TaskVariables::from_iter([
512                    (VariableName::File, path!("/dir/rust/b.rs").into()),
513                    (VariableName::Filename, "b.rs".into()),
514                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
515                    (VariableName::Dirname, path!("/dir/rust").into()),
516                    (VariableName::Stem, "b".into()),
517                    (VariableName::WorktreeRoot, path!("/dir").into()),
518                    (VariableName::Row, "1".into()),
519                    (VariableName::Column, "15".into()),
520                    (VariableName::SelectedText, "is_i".into()),
521                    (VariableName::Symbol, "this_is_a_rust_file".into()),
522                ]),
523                project_env: HashMap::default(),
524            }
525        );
526
527        assert_eq!(
528            workspace
529                .update_in(cx, |workspace, window, cx| {
530                    // Now, let's switch the active item to .ts file.
531                    workspace.activate_item(&editor1, true, true, window, cx);
532                    task_contexts(workspace, window, cx)
533                })
534                .await
535                .active_context()
536                .expect("Should have an active context"),
537            &TaskContext {
538                cwd: Some(path!("/dir").into()),
539                task_variables: TaskVariables::from_iter([
540                    (VariableName::File, path!("/dir/a.ts").into()),
541                    (VariableName::Filename, "a.ts".into()),
542                    (VariableName::RelativeFile, "a.ts".into()),
543                    (VariableName::Dirname, path!("/dir").into()),
544                    (VariableName::Stem, "a".into()),
545                    (VariableName::WorktreeRoot, path!("/dir").into()),
546                    (VariableName::Row, "1".into()),
547                    (VariableName::Column, "1".into()),
548                    (VariableName::Symbol, "this_is_a_test".into()),
549                ]),
550                project_env: HashMap::default(),
551            }
552        );
553    }
554
555    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
556        cx.update(|cx| {
557            let state = AppState::test(cx);
558            language::init(cx);
559            crate::init(cx);
560            editor::init(cx);
561            workspace::init_settings(cx);
562            Project::init_settings(cx);
563            TaskStore::init(None);
564            state
565        })
566    }
567}