tasks_ui.rs

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