From df7beb421746fdf511b1cdcc8fa1714c968126b6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 28 Feb 2025 00:57:59 +0200 Subject: [PATCH] Use active worktree's task sources (#25784) 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 --- 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(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 962a3363f1..8086ebbf98 100644 --- a/crates/project/src/project_tests.rs +++ b/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 { fn get_all_tasks( project: &Entity, - worktree_id: Option, 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 diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index aa9289aea7..b1705242a9 100644 --- a/crates/project/src/task_inventory.rs +++ b/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, TaskContext)>, + pub active_item_context: Option<(Option, Option, 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 { + 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, - location: Option, 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::>::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::>(); + 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::>(); (previously_spawned_tasks, new_resolved_tasks) } @@ -915,7 +940,10 @@ mod tests { cx: &mut TestAppContext, ) -> Vec { 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); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index a53a4d33e1..a512bd448b 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/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::::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::::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); diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 2738d74b57..f2f53957bd 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/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, Option) { - 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::(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 { 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::(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::(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)); } }