tasks_ui.rs

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