tasks_ui.rs

  1use std::collections::HashMap;
  2use std::path::Path;
  3
  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 mut worktree_abs_paths = workspace
317        .worktrees(cx)
318        .filter(|worktree| is_visible_directory(worktree, cx))
319        .map(|worktree| {
320            let worktree = worktree.read(cx);
321            (worktree.id(), worktree.abs_path())
322        })
323        .collect::<HashMap<_, _>>();
324
325    cx.background_spawn(async move {
326        let mut task_contexts = TaskContexts::default();
327
328        if let Some(editor_context_task) = editor_context_task {
329            if let Some(editor_context) = editor_context_task.await {
330                task_contexts.active_item_context =
331                    Some((active_worktree, location, editor_context));
332            }
333        }
334
335        if let Some(active_worktree) = active_worktree {
336            if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) {
337                task_contexts.active_worktree_context =
338                    Some((active_worktree, worktree_context(&active_worktree_abs_path)));
339            }
340        } else if worktree_abs_paths.len() == 1 {
341            task_contexts.active_worktree_context = worktree_abs_paths
342                .drain()
343                .next()
344                .map(|(id, abs_path)| (id, worktree_context(&abs_path)));
345        }
346
347        task_contexts.other_worktree_contexts.extend(
348            worktree_abs_paths
349                .into_iter()
350                .map(|(id, abs_path)| (id, worktree_context(&abs_path))),
351        );
352        task_contexts
353    })
354}
355
356fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool {
357    let worktree = worktree.read(cx);
358    worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir())
359}
360
361fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
362    let mut task_variables = TaskVariables::default();
363    task_variables.insert(
364        VariableName::WorktreeRoot,
365        worktree_abs_path.to_string_lossy().to_string(),
366    );
367    TaskContext {
368        cwd: Some(worktree_abs_path.to_path_buf()),
369        task_variables,
370        project_env: HashMap::default(),
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use std::{collections::HashMap, sync::Arc};
377
378    use editor::Editor;
379    use gpui::TestAppContext;
380    use language::{Language, LanguageConfig};
381    use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore};
382    use serde_json::json;
383    use task::{TaskContext, TaskVariables, VariableName};
384    use ui::VisualContext;
385    use util::{path, separator};
386    use workspace::{AppState, Workspace};
387
388    use crate::task_contexts;
389
390    #[gpui::test]
391    async fn test_default_language_context(cx: &mut TestAppContext) {
392        init_test(cx);
393        let fs = FakeFs::new(cx.executor());
394        fs.insert_tree(
395            path!("/dir"),
396            json!({
397                ".zed": {
398                    "tasks.json": r#"[
399                            {
400                                "label": "example task",
401                                "command": "echo",
402                                "args": ["4"]
403                            },
404                            {
405                                "label": "another one",
406                                "command": "echo",
407                                "args": ["55"]
408                            },
409                        ]"#,
410                },
411                "a.ts": "function this_is_a_test() { }",
412                "rust": {
413                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
414                }
415
416            }),
417        )
418        .await;
419        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
420        let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
421        let rust_language = Arc::new(
422            Language::new(
423                LanguageConfig::default(),
424                Some(tree_sitter_rust::LANGUAGE.into()),
425            )
426            .with_outline_query(
427                r#"(function_item
428            "fn" @context
429            name: (_) @name) @item"#,
430            )
431            .unwrap()
432            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
433                worktree_store.clone(),
434            )))),
435        );
436
437        let typescript_language = Arc::new(
438            Language::new(
439                LanguageConfig::default(),
440                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
441            )
442            .with_outline_query(
443                r#"(function_declaration
444                    "async"? @context
445                    "function" @context
446                    name: (_) @name
447                    parameters: (formal_parameters
448                        "(" @context
449                        ")" @context)) @item"#,
450            )
451            .unwrap()
452            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
453                worktree_store.clone(),
454            )))),
455        );
456
457        let worktree_id = project.update(cx, |project, cx| {
458            project.worktrees(cx).next().unwrap().read(cx).id()
459        });
460        let (workspace, cx) =
461            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
462
463        let buffer1 = workspace
464            .update(cx, |this, cx| {
465                this.project()
466                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
467            })
468            .await
469            .unwrap();
470        buffer1.update(cx, |this, cx| {
471            this.set_language(Some(typescript_language), cx)
472        });
473        let editor1 = cx.new_window_entity(|window, cx| {
474            Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
475        });
476        let buffer2 = workspace
477            .update(cx, |this, cx| {
478                this.project().update(cx, |this, cx| {
479                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
480                })
481            })
482            .await
483            .unwrap();
484        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
485        let editor2 = cx
486            .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
487
488        let first_context = workspace
489            .update_in(cx, |workspace, window, cx| {
490                workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
491                workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
492                assert_eq!(
493                    workspace.active_item(cx).unwrap().item_id(),
494                    editor2.entity_id()
495                );
496                task_contexts(workspace, window, cx)
497            })
498            .await;
499
500        assert_eq!(
501            first_context
502                .active_context()
503                .expect("Should have an active context"),
504            &TaskContext {
505                cwd: Some(path!("/dir").into()),
506                task_variables: TaskVariables::from_iter([
507                    (VariableName::File, path!("/dir/rust/b.rs").into()),
508                    (VariableName::Filename, "b.rs".into()),
509                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
510                    (VariableName::Dirname, path!("/dir/rust").into()),
511                    (VariableName::Stem, "b".into()),
512                    (VariableName::WorktreeRoot, path!("/dir").into()),
513                    (VariableName::Row, "1".into()),
514                    (VariableName::Column, "1".into()),
515                ]),
516                project_env: HashMap::default(),
517            }
518        );
519
520        // And now, let's select an identifier.
521        editor2.update_in(cx, |editor, window, cx| {
522            editor.change_selections(None, window, cx, |selections| {
523                selections.select_ranges([14..18])
524            })
525        });
526
527        assert_eq!(
528            workspace
529                .update_in(cx, |workspace, window, cx| {
530                    task_contexts(workspace, window, cx)
531                })
532                .await
533                .active_context()
534                .expect("Should have an active context"),
535            &TaskContext {
536                cwd: Some(path!("/dir").into()),
537                task_variables: TaskVariables::from_iter([
538                    (VariableName::File, path!("/dir/rust/b.rs").into()),
539                    (VariableName::Filename, "b.rs".into()),
540                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
541                    (VariableName::Dirname, path!("/dir/rust").into()),
542                    (VariableName::Stem, "b".into()),
543                    (VariableName::WorktreeRoot, path!("/dir").into()),
544                    (VariableName::Row, "1".into()),
545                    (VariableName::Column, "15".into()),
546                    (VariableName::SelectedText, "is_i".into()),
547                    (VariableName::Symbol, "this_is_a_rust_file".into()),
548                ]),
549                project_env: HashMap::default(),
550            }
551        );
552
553        assert_eq!(
554            workspace
555                .update_in(cx, |workspace, window, cx| {
556                    // Now, let's switch the active item to .ts file.
557                    workspace.activate_item(&editor1, true, true, window, cx);
558                    task_contexts(workspace, window, cx)
559                })
560                .await
561                .active_context()
562                .expect("Should have an active context"),
563            &TaskContext {
564                cwd: Some(path!("/dir").into()),
565                task_variables: TaskVariables::from_iter([
566                    (VariableName::File, path!("/dir/a.ts").into()),
567                    (VariableName::Filename, "a.ts".into()),
568                    (VariableName::RelativeFile, "a.ts".into()),
569                    (VariableName::Dirname, path!("/dir").into()),
570                    (VariableName::Stem, "a".into()),
571                    (VariableName::WorktreeRoot, path!("/dir").into()),
572                    (VariableName::Row, "1".into()),
573                    (VariableName::Column, "1".into()),
574                    (VariableName::Symbol, "this_is_a_test".into()),
575                ]),
576                project_env: HashMap::default(),
577            }
578        );
579    }
580
581    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
582        cx.update(|cx| {
583            let state = AppState::test(cx);
584            language::init(cx);
585            crate::init(cx);
586            editor::init(cx);
587            workspace::init_settings(cx);
588            Project::init_settings(cx);
589            TaskStore::init(None);
590            state
591        })
592    }
593}