tasks_ui.rs

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