Use active worktree's task sources (#25784)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/25605

Previous PR made global tasks with `ZED_WORKTREE_ROOT` available for
"nothing open" scenario, this PR also gets all related worktree task
templates, using the centralized `TextContexts`' active worktree
detection.

Release Notes:

- N/A

Change summary

crates/project/src/project_tests.rs  |  20 ++--
crates/project/src/task_inventory.rs | 111 +++++++++++++++++++----------
crates/tasks_ui/src/modal.rs         |  19 +---
crates/tasks_ui/src/tasks_ui.rs      |  99 ++++++++++----------------
4 files changed, 125 insertions(+), 124 deletions(-)

Detailed changes

crates/project/src/project_tests.rs 🔗

@@ -233,7 +233,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
 
     let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
     let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
-    let task_contexts = TaskContexts::default();
 
     cx.executor().run_until_parked();
     let worktree_id = cx.update(|cx| {
@@ -241,6 +240,10 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             project.worktrees(cx).next().unwrap().read(cx).id()
         })
     });
+
+    let mut task_contexts = TaskContexts::default();
+    task_contexts.active_worktree_context = Some((worktree_id, TaskContext::default()));
+
     let topmost_local_task_source_kind = TaskSourceKind::Worktree {
         id: worktree_id,
         directory_in_worktree: PathBuf::from(".zed"),
@@ -265,7 +268,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             assert_eq!(settings_a.tab_size.get(), 8);
             assert_eq!(settings_b.tab_size.get(), 2);
 
-            get_all_tasks(&project, Some(worktree_id), &task_contexts, cx)
+            get_all_tasks(&project, &task_contexts, cx)
         })
         .into_iter()
         .map(|(source_kind, task)| {
@@ -305,7 +308,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
     );
 
     let (_, resolved_task) = cx
-        .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_contexts, cx))
+        .update(|cx| get_all_tasks(&project, &task_contexts, cx))
         .into_iter()
         .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind)
         .expect("should have one global task");
@@ -343,7 +346,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
     cx.run_until_parked();
 
     let all_tasks = cx
-        .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_contexts, cx))
+        .update(|cx| get_all_tasks(&project, &task_contexts, cx))
         .into_iter()
         .map(|(source_kind, task)| {
             let resolved = task.resolved.unwrap();
@@ -433,9 +436,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
     let active_non_worktree_item_tasks = cx.update(|cx| {
         get_all_tasks(
             &project,
-            Some(worktree_id),
             &TaskContexts {
-                active_item_context: Some((Some(worktree_id), TaskContext::default())),
+                active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
                 active_worktree_context: None,
                 other_worktree_contexts: Vec::new(),
             },
@@ -450,9 +452,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
     let active_worktree_tasks = cx.update(|cx| {
         get_all_tasks(
             &project,
-            Some(worktree_id),
             &TaskContexts {
-                active_item_context: Some((Some(worktree_id), TaskContext::default())),
+                active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
                 active_worktree_context: Some((worktree_id, {
                     let mut worktree_context = TaskContext::default();
                     worktree_context
@@ -6139,7 +6140,6 @@ fn tsx_lang() -> Arc<Language> {
 
 fn get_all_tasks(
     project: &Entity<Project>,
-    worktree_id: Option<WorktreeId>,
     task_contexts: &TaskContexts,
     cx: &mut App,
 ) -> Vec<(TaskSourceKind, ResolvedTask)> {
@@ -6150,7 +6150,7 @@ fn get_all_tasks(
             .task_inventory()
             .unwrap()
             .read(cx)
-            .used_and_current_resolved_tasks(worktree_id, None, task_contexts, cx)
+            .used_and_current_resolved_tasks(task_contexts, cx)
     });
     old.extend(new);
     old

crates/project/src/task_inventory.rs 🔗

@@ -62,7 +62,7 @@ pub enum TaskSourceKind {
 pub struct TaskContexts {
     /// A context, related to the currently opened item.
     /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree.
-    pub active_item_context: Option<(Option<WorktreeId>, TaskContext)>,
+    pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>,
     /// A worktree that corresponds to the active item, or the only worktree in the workspace.
     pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
     /// If there are multiple worktrees in the workspace, all non-active ones are included here.
@@ -73,13 +73,31 @@ impl TaskContexts {
     pub fn active_context(&self) -> Option<&TaskContext> {
         self.active_item_context
             .as_ref()
-            .map(|(_, context)| context)
+            .map(|(_, _, context)| context)
             .or_else(|| {
                 self.active_worktree_context
                     .as_ref()
                     .map(|(_, context)| context)
             })
     }
+
+    pub fn location(&self) -> Option<&Location> {
+        self.active_item_context
+            .as_ref()
+            .and_then(|(_, location, _)| location.as_ref())
+    }
+
+    pub fn worktree(&self) -> Option<WorktreeId> {
+        self.active_item_context
+            .as_ref()
+            .and_then(|(worktree_id, _, _)| worktree_id.as_ref())
+            .or_else(|| {
+                self.active_worktree_context
+                    .as_ref()
+                    .map(|(worktree_id, _)| worktree_id)
+            })
+            .copied()
+    }
 }
 
 impl TaskSourceKind {
@@ -138,23 +156,20 @@ impl Inventory {
     /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
     pub fn used_and_current_resolved_tasks(
         &self,
-        worktree: Option<WorktreeId>,
-        location: Option<Location>,
         task_contexts: &TaskContexts,
         cx: &App,
     ) -> (
         Vec<(TaskSourceKind, ResolvedTask)>,
         Vec<(TaskSourceKind, ResolvedTask)>,
     ) {
+        let worktree = task_contexts.worktree();
+        let location = task_contexts.location();
         let language = location
-            .as_ref()
             .and_then(|location| location.buffer.read(cx).language_at(location.range.start));
         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
             name: language.name().into(),
         });
-        let file = location
-            .as_ref()
-            .and_then(|location| location.buffer.read(cx).file().cloned());
+        let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
 
         let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
         let mut lru_score = 0_u32;
@@ -199,29 +214,28 @@ impl Inventory {
             .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
             .into_iter()
             .flat_map(|tasks| tasks.0.into_iter())
-            .flat_map(|task| Some((task_source_kind.clone()?, task)))
-            .chain(global_tasks);
+            .flat_map(|task| Some((task_source_kind.clone()?, task)));
         let worktree_tasks = self
             .worktree_templates_from_settings(worktree)
-            .chain(language_tasks);
+            .chain(language_tasks)
+            .chain(global_tasks);
 
-        let new_resolved_tasks =
-            worktree_tasks
-                .flat_map(|(kind, task)| {
-                    let id_base = kind.to_id_base();
+        let new_resolved_tasks = worktree_tasks
+            .flat_map(|(kind, task)| {
+                let id_base = kind.to_id_base();
+                if let TaskSourceKind::Worktree { id, .. } = &kind {
                     None.or_else(|| {
-                        let (_, item_context) = task_contexts.active_item_context.as_ref().filter(
-                            |(worktree_id, _)| worktree.is_none() || worktree == *worktree_id,
-                        )?;
+                        let (_, _, item_context) = task_contexts
+                            .active_item_context
+                            .as_ref()
+                            .filter(|(worktree_id, _, _)| Some(id) == worktree_id.as_ref())?;
                         task.resolve_task(&id_base, item_context)
                     })
                     .or_else(|| {
                         let (_, worktree_context) = task_contexts
                             .active_worktree_context
                             .as_ref()
-                            .filter(|(worktree_id, _)| {
-                                worktree.is_none() || worktree == Some(*worktree_id)
-                            })?;
+                            .filter(|(worktree_id, _)| id == worktree_id)?;
                         task.resolve_task(&id_base, worktree_context)
                     })
                     .or_else(|| {
@@ -236,24 +250,35 @@ impl Inventory {
                             None
                         }
                     })
-                    .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
-                    .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
-                })
-                .filter(|(_, resolved_task, _)| {
-                    match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
-                        hash_map::Entry::Occupied(mut o) => {
-                            // Allow new tasks with the same label, if their context is different
-                            o.get_mut().insert(resolved_task.id.clone())
-                        }
-                        hash_map::Entry::Vacant(v) => {
-                            v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
-                            true
-                        }
+                } else {
+                    None.or_else(|| {
+                        let (_, _, item_context) = task_contexts.active_item_context.as_ref()?;
+                        task.resolve_task(&id_base, item_context)
+                    })
+                    .or_else(|| {
+                        let (_, worktree_context) =
+                            task_contexts.active_worktree_context.as_ref()?;
+                        task.resolve_task(&id_base, worktree_context)
+                    })
+                }
+                .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
+                .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
+            })
+            .filter(|(_, resolved_task, _)| {
+                match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
+                    hash_map::Entry::Occupied(mut o) => {
+                        // Allow new tasks with the same label, if their context is different
+                        o.get_mut().insert(resolved_task.id.clone())
                     }
-                })
-                .sorted_unstable_by(task_lru_comparator)
-                .map(|(kind, task, _)| (kind, task))
-                .collect::<Vec<_>>();
+                    hash_map::Entry::Vacant(v) => {
+                        v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
+                        true
+                    }
+                }
+            })
+            .sorted_unstable_by(task_lru_comparator)
+            .map(|(kind, task, _)| (kind, task))
+            .collect::<Vec<_>>();
 
         (previously_spawned_tasks, new_resolved_tasks)
     }
@@ -915,7 +940,10 @@ mod tests {
         cx: &mut TestAppContext,
     ) -> Vec<String> {
         let (used, current) = inventory.update(cx, |inventory, cx| {
-            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContexts::default(), cx)
+            let mut task_contexts = TaskContexts::default();
+            task_contexts.active_worktree_context =
+                worktree.map(|worktree| (worktree, TaskContext::default()));
+            inventory.used_and_current_resolved_tasks(&task_contexts, cx)
         });
         used.into_iter()
             .chain(current)
@@ -944,7 +972,10 @@ mod tests {
         cx: &mut TestAppContext,
     ) -> Vec<(TaskSourceKind, String)> {
         let (used, current) = inventory.update(cx, |inventory, cx| {
-            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContexts::default(), cx)
+            let mut task_contexts = TaskContexts::default();
+            task_contexts.active_worktree_context =
+                worktree.map(|worktree| (worktree, TaskContext::default()));
+            inventory.used_and_current_resolved_tasks(&task_contexts, cx)
         });
         let mut all = used;
         all.extend(current);

crates/tasks_ui/src/modal.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use crate::{active_item_selection_properties, TaskContexts};
+use crate::TaskContexts;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     rems, Action, AnyElement, App, AppContext as _, Context, DismissEvent, Entity, EventEmitter,
@@ -209,13 +209,6 @@ impl PickerDelegate for TasksModalDelegate {
                     match &mut picker.delegate.candidates {
                         Some(candidates) => string_match_candidates(candidates.iter()),
                         None => {
-                            let Ok((worktree, location)) =
-                                picker.delegate.workspace.update(cx, |workspace, cx| {
-                                    active_item_selection_properties(workspace, cx)
-                                })
-                            else {
-                                return Vec::new();
-                            };
                             let Some(task_inventory) = picker
                                 .delegate
                                 .task_store
@@ -228,8 +221,6 @@ impl PickerDelegate for TasksModalDelegate {
 
                             let (used, current) =
                                 task_inventory.read(cx).used_and_current_resolved_tasks(
-                                    worktree,
-                                    location,
                                     &picker.delegate.task_contexts,
                                     cx,
                                 );
@@ -655,8 +646,8 @@ mod tests {
         );
         assert_eq!(
             task_names(&tasks_picker, cx),
-            Vec::<String>::new(),
-            "With no global tasks and no open item, no tasks should be listed"
+            vec!["another one", "example task"],
+            "With no global tasks and no open item, a single worktree should be used and its tasks listed"
         );
         drop(tasks_picker);
 
@@ -815,8 +806,8 @@ mod tests {
         let tasks_picker = open_spawn_tasks(&workspace, cx);
         assert_eq!(
             task_names(&tasks_picker, cx),
-            Vec::<String>::new(),
-            "Should list no file or worktree context-dependent when no file is open"
+            vec![concat!("opened now: ", path!("/dir")).to_string()],
+            "When no file is open for a single worktree, should autodetect all worktree-related tasks"
         );
         tasks_picker.update(cx, |_, cx| {
             cx.emit(DismissEvent);

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -5,7 +5,7 @@ use ::settings::Settings;
 use editor::Editor;
 use gpui::{App, AppContext as _, Context, Entity, Task, Window};
 use modal::{TaskOverrides, TasksModal};
-use project::{Location, TaskContexts, Worktree, WorktreeId};
+use project::{Location, TaskContexts, Worktree};
 use task::{RevealTarget, TaskContext, TaskId, TaskVariables, VariableName};
 use workspace::tasks::schedule_task;
 use workspace::{tasks::schedule_resolved_task, Workspace};
@@ -49,20 +49,19 @@ pub fn init(cx: &mut App) {
                             let task_contexts = task_contexts(workspace, window, cx);
                             cx.spawn_in(window, |workspace, mut cx| async move {
                                 let task_contexts = task_contexts.await;
+                                let default_context = TaskContext::default();
                                 workspace
-                                    .update_in(&mut cx, |workspace, window, cx| {
-                                        if let Some(task_context) = task_contexts.active_context() {
-                                            schedule_task(
-                                                workspace,
-                                                task_source_kind,
-                                                &original_task,
-                                                task_context,
-                                                false,
-                                                cx,
-                                            )
-                                        } else {
-                                            toggle_modal(workspace, None, window, cx).detach();
-                                        }
+                                    .update_in(&mut cx, |workspace, _, cx| {
+                                        schedule_task(
+                                            workspace,
+                                            task_source_kind,
+                                            &original_task,
+                                            task_contexts
+                                                .active_context()
+                                                .unwrap_or(&default_context),
+                                            false,
+                                            cx,
+                                        )
                                     })
                                     .ok()
                             })
@@ -175,8 +174,8 @@ fn spawn_task_with_name(
             else {
                 return Vec::new();
             };
-            let (worktree, location) = active_item_selection_properties(workspace, cx);
-            let (file, language) = location
+            let (file, language) = task_contexts
+                .location()
                 .map(|location| {
                     let buffer = location.buffer.read(cx);
                     (
@@ -187,7 +186,7 @@ fn spawn_task_with_name(
                 .unwrap_or_default();
             task_inventory
                 .read(cx)
-                .list_tasks(file, language, worktree, cx)
+                .list_tasks(file, language, task_contexts.worktree(), cx)
         })?;
 
         let did_spawn = workspace
@@ -231,42 +230,6 @@ fn spawn_task_with_name(
     })
 }
 
-fn active_item_selection_properties(
-    workspace: &Workspace,
-    cx: &mut App,
-) -> (Option<WorktreeId>, Option<Location>) {
-    let active_item = workspace.active_item(cx);
-    let worktree_id = active_item
-        .as_ref()
-        .and_then(|item| item.project_path(cx))
-        .map(|path| path.worktree_id)
-        .filter(|worktree_id| {
-            workspace
-                .project()
-                .read(cx)
-                .worktree_for_id(*worktree_id, cx)
-                .map_or(false, |worktree| is_visible_directory(&worktree, cx))
-        });
-    let location = active_item
-        .and_then(|active_item| active_item.act_as::<Editor>(cx))
-        .and_then(|editor| {
-            editor.update(cx, |editor, cx| {
-                let selection = editor.selections.newest_anchor();
-                let multi_buffer = editor.buffer().clone();
-                let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
-                let (buffer_snapshot, buffer_offset) =
-                    multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
-                let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
-                let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
-                Some(Location {
-                    buffer,
-                    range: buffer_anchor..buffer_anchor,
-                })
-            })
-        });
-    (worktree_id, location)
-}
-
 fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Task<TaskContexts> {
     let active_item = workspace.active_item(cx);
     let active_worktree = active_item
@@ -281,12 +244,27 @@ fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Ta
                 .map_or(false, |worktree| is_visible_directory(&worktree, cx))
         });
 
-    let editor_context_task =
-        active_item
-            .and_then(|item| item.act_as::<Editor>(cx))
-            .map(|active_editor| {
-                active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
-            });
+    let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
+
+    let editor_context_task = active_editor.as_ref().map(|active_editor| {
+        active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
+    });
+
+    let location = active_editor.as_ref().and_then(|editor| {
+        editor.update(cx, |editor, cx| {
+            let selection = editor.selections.newest_anchor();
+            let multi_buffer = editor.buffer().clone();
+            let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
+            let (buffer_snapshot, buffer_offset) =
+                multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
+            let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
+            let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
+            Some(Location {
+                buffer,
+                range: buffer_anchor..buffer_anchor,
+            })
+        })
+    });
 
     let mut worktree_abs_paths = workspace
         .worktrees(cx)
@@ -302,7 +280,8 @@ fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Ta
 
         if let Some(editor_context_task) = editor_context_task {
             if let Some(editor_context) = editor_context_task.await {
-                task_contexts.active_item_context = Some((active_worktree, editor_context));
+                task_contexts.active_item_context =
+                    Some((active_worktree, location, editor_context));
             }
         }