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