From b5a1ae65266d2cb6363775c2212d4a56e9adfb78 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 26 Feb 2025 22:30:31 +0200 Subject: [PATCH] Improve Zed tasks' `ZED_WORKTREE_ROOT` fallbacks (#25605) Closes https://github.com/zed-industries/zed/issues/22912 Reworks the task context infrastructure so that it's possible to have multiple contexts at the same time, and stores all possible worktree context there. Task UI code is now falling back to the "active" worktree context, if active item's context did not produce a resolved task. Current code does not produce meaningful results for projects with multiple worktrees to avoid ambiguity and design changes: instead of resolving tasks per worktree context available, extra worktree context is only used when resolving tasks from the same worktree. Release Notes: - Improved Zed tasks' `ZED_WORKTREE_ROOT` fallbacks --- crates/editor/src/tasks.rs | 142 ++++++++++----------- crates/project/src/project.rs | 2 +- crates/project/src/project_tests.rs | 104 ++++++++++++++-- crates/project/src/task_inventory.rs | 109 ++++++++++++----- crates/tasks_ui/src/modal.rs | 21 ++-- crates/tasks_ui/src/tasks_ui.rs | 176 +++++++++++++++++++++------ 6 files changed, 390 insertions(+), 164 deletions(-) diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 8444849b5b4121a1c7f115cd25ad956fe69504d5..361fab75593204c1c812715b12098d95138f4565 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -1,93 +1,73 @@ use crate::Editor; -use gpui::{App, AppContext as _, Task as AsyncTask, Window}; +use gpui::{App, Task, Window}; use project::Location; use task::{TaskContext, TaskVariables, VariableName}; use text::{ToOffset, ToPoint}; -use workspace::Workspace; -fn task_context_with_editor( - editor: &mut Editor, - window: &mut Window, - cx: &mut App, -) -> AsyncTask> { - let Some(project) = editor.project.clone() else { - return AsyncTask::ready(None); - }; - let (selection, buffer, editor_snapshot) = { - let selection = editor.selections.newest_adjusted(cx); - let Some((buffer, _)) = editor - .buffer() - .read(cx) - .point_to_buffer_offset(selection.start, cx) - else { - return AsyncTask::ready(None); +impl Editor { + pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task> { + let Some(project) = self.project.clone() else { + return Task::ready(None); }; - let snapshot = editor.snapshot(window, cx); - (selection, buffer, snapshot) - }; - let selection_range = selection.range(); - let start = editor_snapshot - .display_snapshot - .buffer_snapshot - .anchor_after(selection_range.start) - .text_anchor; - let end = editor_snapshot - .display_snapshot - .buffer_snapshot - .anchor_after(selection_range.end) - .text_anchor; - let location = Location { - buffer, - range: start..end, - }; - let captured_variables = { - let mut variables = TaskVariables::default(); - let buffer = location.buffer.read(cx); - let buffer_id = buffer.remote_id(); - let snapshot = buffer.snapshot(); - let starting_point = location.range.start.to_point(&snapshot); - let starting_offset = starting_point.to_offset(&snapshot); - for (_, tasks) in editor - .tasks - .range((buffer_id, 0)..(buffer_id, starting_point.row + 1)) - { - if !tasks - .context_range - .contains(&crate::BufferOffset(starting_offset)) + let (selection, buffer, editor_snapshot) = { + let selection = self.selections.newest_adjusted(cx); + let Some((buffer, _)) = self + .buffer() + .read(cx) + .point_to_buffer_offset(selection.start, cx) + else { + return Task::ready(None); + }; + let snapshot = self.snapshot(window, cx); + (selection, buffer, snapshot) + }; + let selection_range = selection.range(); + let start = editor_snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.start) + .text_anchor; + let end = editor_snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.end) + .text_anchor; + let location = Location { + buffer, + range: start..end, + }; + let captured_variables = { + let mut variables = TaskVariables::default(); + let buffer = location.buffer.read(cx); + let buffer_id = buffer.remote_id(); + let snapshot = buffer.snapshot(); + let starting_point = location.range.start.to_point(&snapshot); + let starting_offset = starting_point.to_offset(&snapshot); + for (_, tasks) in self + .tasks + .range((buffer_id, 0)..(buffer_id, starting_point.row + 1)) { - continue; - } - for (capture_name, value) in tasks.extra_variables.iter() { - variables.insert( - VariableName::Custom(capture_name.to_owned().into()), - value.clone(), - ); + if !tasks + .context_range + .contains(&crate::BufferOffset(starting_offset)) + { + continue; + } + for (capture_name, value) in tasks.extra_variables.iter() { + variables.insert( + VariableName::Custom(capture_name.to_owned().into()), + value.clone(), + ); + } } - } - variables - }; + variables + }; - project.update(cx, |project, cx| { - project.task_store().update(cx, |task_store, cx| { - task_store.task_context_for_location(captured_variables, location, cx) + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_variables, location, cx) + }) }) - }) -} - -pub fn task_context( - workspace: &Workspace, - window: &mut Window, - cx: &mut App, -) -> AsyncTask { - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return AsyncTask::ready(TaskContext::default()); - }; - editor.update(cx, |editor, cx| { - let context_task = task_context_with_editor(editor, window, cx); - cx.background_spawn(async move { context_task.await.unwrap_or_default() }) - }) + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3003bc746c113ad49ade40f96136eab5aa6a241e..4c545bb91146678f3dc6ec31cbd782170079bcf2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -107,7 +107,7 @@ pub use language::Location; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use task_inventory::{ - BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind, + BasicContextProvider, ContextProviderWithTasks, Inventory, TaskContexts, TaskSourceKind, }; pub use worktree::{ Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c6e51f930ae88fc91f79ad0d3ebe4a04436b7614..962a3363f16e419779f52d75d4143b1896d15e45 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{Event, *}; +use crate::{task_inventory::TaskContexts, Event, *}; use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus}; use fs::FakeFs; use futures::{future, StreamExt}; @@ -233,7 +233,7 @@ 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_context = TaskContext::default(); + let task_contexts = TaskContexts::default(); cx.executor().run_until_parked(); let worktree_id = cx.update(|cx| { @@ -265,7 +265,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_context, cx) + get_all_tasks(&project, Some(worktree_id), &task_contexts, cx) }) .into_iter() .map(|(source_kind, task)| { @@ -305,7 +305,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_context, cx)) + .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_contexts, cx)) .into_iter() .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind) .expect("should have one global task"); @@ -343,7 +343,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_context, cx)) + .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_contexts, cx)) .into_iter() .map(|(source_kind, task)| { let resolved = task.resolved.unwrap(); @@ -398,6 +398,96 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ); } +#[gpui::test] +async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx); + TaskStore::init(None); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + ".zed": { + "tasks.json": r#"[{ + "label": "test worktree root", + "command": "echo $ZED_WORKTREE_ROOT" + }]"#, + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let _worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + cx.executor().run_until_parked(); + let worktree_id = cx.update(|cx| { + project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + 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_worktree_context: None, + other_worktree_contexts: Vec::new(), + }, + cx, + ) + }); + assert!( + active_non_worktree_item_tasks.is_empty(), + "A task can not be resolved with context with no ZED_WORKTREE_ROOT data" + ); + + 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_worktree_context: Some((worktree_id, { + let mut worktree_context = TaskContext::default(); + worktree_context + .task_variables + .insert(task::VariableName::WorktreeRoot, "/dir".to_string()); + worktree_context + })), + other_worktree_contexts: Vec::new(), + }, + cx, + ) + }); + assert_eq!( + active_worktree_tasks + .into_iter() + .map(|(source_kind, task)| { + let resolved = task.resolved.unwrap(); + (source_kind, resolved.command) + }) + .collect::>(), + vec![( + TaskSourceKind::Worktree { + id: worktree_id, + directory_in_worktree: PathBuf::from(separator!(".zed")), + id_base: if cfg!(windows) { + "local worktree tasks from directory \".zed\"".into() + } else { + "local worktree tasks from directory \".zed\"".into() + }, + }, + "echo /dir".to_string(), + )] + ); +} + #[gpui::test] async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -6050,7 +6140,7 @@ fn tsx_lang() -> Arc { fn get_all_tasks( project: &Entity, worktree_id: Option, - task_context: &TaskContext, + task_contexts: &TaskContexts, cx: &mut App, ) -> Vec<(TaskSourceKind, ResolvedTask)> { let (mut old, new) = project.update(cx, |project, cx| { @@ -6060,7 +6150,7 @@ fn get_all_tasks( .task_inventory() .unwrap() .read(cx) - .used_and_current_resolved_tasks(worktree_id, None, task_context, cx) + .used_and_current_resolved_tasks(worktree_id, None, task_contexts, cx) }); old.extend(new); old diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index b727d1365b6ca8d1aeb94cc4332375d8d96678b1..aa9289aea77c445905633b8e99f7e3342c85de5f 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -56,6 +56,32 @@ pub enum TaskSourceKind { Language { name: SharedString }, } +/// A collection of task contexts, derived from the current state of the workspace. +/// Only contains worktrees that are visible and with their root being a directory. +#[derive(Debug, Default)] +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)>, + /// 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. + pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>, +} + +impl TaskContexts { + pub fn active_context(&self) -> Option<&TaskContext> { + self.active_item_context + .as_ref() + .map(|(_, context)| context) + .or_else(|| { + self.active_worktree_context + .as_ref() + .map(|(_, context)| context) + }) + } +} + impl TaskSourceKind { pub fn to_id_base(&self) -> String { match self { @@ -106,7 +132,7 @@ impl Inventory { .collect() } - /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given. + /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given. /// Joins the new resolutions with the resolved tasks that were used (spawned) before, /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first. /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks. @@ -114,7 +140,7 @@ impl Inventory { &self, worktree: Option, location: Option, - task_context: &TaskContext, + task_contexts: &TaskContexts, cx: &App, ) -> ( Vec<(TaskSourceKind, ResolvedTask)>, @@ -179,30 +205,55 @@ impl Inventory { .worktree_templates_from_settings(worktree) .chain(language_tasks); - let new_resolved_tasks = worktree_tasks - .filter_map(|(kind, task)| { - let id_base = kind.to_id_base(); - Some(( - kind, - task.resolve_task(&id_base, task_context)?, - 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 + let new_resolved_tasks = + worktree_tasks + .flat_map(|(kind, task)| { + let id_base = kind.to_id_base(); + None.or_else(|| { + let (_, item_context) = task_contexts.active_item_context.as_ref().filter( + |(worktree_id, _)| worktree.is_none() || worktree == *worktree_id, + )?; + 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) + })?; + task.resolve_task(&id_base, worktree_context) + }) + .or_else(|| { + if let TaskSourceKind::Worktree { id, .. } = &kind { + let worktree_context = task_contexts + .other_worktree_contexts + .iter() + .find(|(worktree_id, _)| worktree_id == id) + .map(|(_, context)| context)?; + task.resolve_task(&id_base, worktree_context) + } else { + 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 + } } - } - }) - .sorted_unstable_by(task_lru_comparator) - .map(|(kind, task, _)| (kind, task)) - .collect::>(); + }) + .sorted_unstable_by(task_lru_comparator) + .map(|(kind, task, _)| (kind, task)) + .collect::>(); (previously_spawned_tasks, new_resolved_tasks) } @@ -497,9 +548,9 @@ impl ContextProvider for BasicContextProvider { self.worktree_store .read(cx) .worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).root_dir()) + .and_then(|worktree| worktree.read(cx).root_dir()) }); - if let Some(Some(worktree_path)) = worktree_root_dir { + if let Some(worktree_path) = worktree_root_dir { task_variables.insert( VariableName::WorktreeRoot, worktree_path.to_sanitized_string(), @@ -864,7 +915,7 @@ mod tests { cx: &mut TestAppContext, ) -> Vec { let (used, current) = inventory.update(cx, |inventory, cx| { - inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) + inventory.used_and_current_resolved_tasks(worktree, None, &TaskContexts::default(), cx) }); used.into_iter() .chain(current) @@ -893,7 +944,7 @@ mod tests { cx: &mut TestAppContext, ) -> Vec<(TaskSourceKind, String)> { let (used, current) = inventory.update(cx, |inventory, cx| { - inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) + inventory.used_and_current_resolved_tasks(worktree, None, &TaskContexts::default(), 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 32e9add67c38c54bc02e7f42a5d367ca62a8ef1c..a53a4d33e13808ddcdfc00b4bdf2a9c0ae16d373 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; +use crate::{active_item_selection_properties, TaskContexts}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ rems, Action, AnyElement, App, AppContext as _, Context, DismissEvent, Entity, EventEmitter, @@ -30,7 +30,7 @@ pub(crate) struct TasksModalDelegate { selected_index: usize, workspace: WeakEntity, prompt: String, - task_context: TaskContext, + task_contexts: TaskContexts, placeholder_text: Arc, } @@ -44,7 +44,7 @@ pub(crate) struct TaskOverrides { impl TasksModalDelegate { fn new( task_store: Entity, - task_context: TaskContext, + task_contexts: TaskContexts, task_overrides: Option, workspace: WeakEntity, ) -> Self { @@ -65,7 +65,7 @@ impl TasksModalDelegate { divider_index: None, selected_index: 0, prompt: String::default(), - task_context, + task_contexts, task_overrides, placeholder_text, } @@ -76,6 +76,11 @@ impl TasksModalDelegate { return None; } + let default_context = TaskContext::default(); + let active_context = self + .task_contexts + .active_context() + .unwrap_or(&default_context); let source_kind = TaskSourceKind::UserInput; let id_base = source_kind.to_id_base(); let mut new_oneshot = TaskTemplate { @@ -91,7 +96,7 @@ impl TasksModalDelegate { } Some(( source_kind, - new_oneshot.resolve_task(&id_base, &self.task_context)?, + new_oneshot.resolve_task(&id_base, active_context)?, )) } @@ -122,7 +127,7 @@ pub(crate) struct TasksModal { impl TasksModal { pub(crate) fn new( task_store: Entity, - task_context: TaskContext, + task_contexts: TaskContexts, task_overrides: Option, workspace: WeakEntity, window: &mut Window, @@ -130,7 +135,7 @@ impl TasksModal { ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( - TasksModalDelegate::new(task_store, task_context, task_overrides, workspace), + TasksModalDelegate::new(task_store, task_contexts, task_overrides, workspace), window, cx, ) @@ -225,7 +230,7 @@ impl PickerDelegate for TasksModalDelegate { task_inventory.read(cx).used_and_current_resolved_tasks( worktree, location, - &picker.delegate.task_context, + &picker.delegate.task_contexts, cx, ); picker.delegate.last_used_candidate_index = if used.is_empty() { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 5a536c09270aa230b60137c6da8abfc886cceac0..2738d74b57a1a2d4d94c1cea3d78446b23de6f7d 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -1,9 +1,12 @@ +use std::collections::HashMap; +use std::path::Path; + use ::settings::Settings; -use editor::{tasks::task_context, Editor}; -use gpui::{App, Context, Task as AsyncTask, Window}; +use editor::Editor; +use gpui::{App, AppContext as _, Context, Entity, Task, Window}; use modal::{TaskOverrides, TasksModal}; -use project::{Location, WorktreeId}; -use task::{RevealTarget, TaskId}; +use project::{Location, TaskContexts, Worktree, WorktreeId}; +use task::{RevealTarget, TaskContext, TaskId, TaskVariables, VariableName}; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -43,19 +46,23 @@ pub fn init(cx: &mut App) { if let Some(use_new_terminal) = action.use_new_terminal { original_task.use_new_terminal = use_new_terminal; } - let context_task = task_context(workspace, window, cx); + let task_contexts = task_contexts(workspace, window, cx); cx.spawn_in(window, |workspace, mut cx| async move { - let task_context = context_task.await; + let task_contexts = task_contexts.await; workspace - .update(&mut cx, |workspace, cx| { - schedule_task( - workspace, - task_source_kind, - &original_task, - &task_context, - false, - cx, - ) + .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(); + } }) .ok() }) @@ -114,22 +121,22 @@ fn toggle_modal( reveal_target: Option, window: &mut Window, cx: &mut Context, -) -> AsyncTask<()> { +) -> Task<()> { let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); let can_open_modal = workspace.project().update(cx, |project, cx| { project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh() }); if can_open_modal { - let context_task = task_context(workspace, window, cx); + let task_contexts = task_contexts(workspace, window, cx); cx.spawn_in(window, |workspace, mut cx| async move { - let task_context = context_task.await; + let task_contexts = task_contexts.await; workspace .update_in(&mut cx, |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { TasksModal::new( task_store.clone(), - task_context, + task_contexts, reveal_target.map(|target| TaskOverrides { reveal_target: Some(target), }), @@ -142,7 +149,7 @@ fn toggle_modal( .ok(); }) } else { - AsyncTask::ready(()) + Task::ready(()) } } @@ -151,12 +158,12 @@ fn spawn_task_with_name( overrides: Option, window: &mut Window, cx: &mut Context, -) -> AsyncTask> { +) -> Task> { cx.spawn_in(window, |workspace, mut cx| async move { - let context_task = workspace.update_in(&mut cx, |workspace, window, cx| { - task_context(workspace, window, cx) + let task_contexts = workspace.update_in(&mut cx, |workspace, window, cx| { + task_contexts(workspace, window, cx) })?; - let task_context = context_task.await; + let task_contexts = task_contexts.await; let tasks = workspace.update(&mut cx, |workspace, cx| { let Some(task_inventory) = workspace .project() @@ -192,11 +199,13 @@ fn spawn_task_with_name( target_task.reveal_target = target_override; } } + let default_context = TaskContext::default(); + let active_context = task_contexts.active_context().unwrap_or(&default_context); schedule_task( workspace, task_source_kind, &target_task, - &task_context, + active_context, false, cx, ); @@ -230,7 +239,14 @@ fn active_item_selection_properties( let worktree_id = active_item .as_ref() .and_then(|item| item.project_path(cx)) - .map(|path| path.worktree_id); + .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| { @@ -251,6 +267,84 @@ fn active_item_selection_properties( (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 + .as_ref() + .and_then(|item| item.project_path(cx)) + .map(|project_path| project_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 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 mut worktree_abs_paths = workspace + .worktrees(cx) + .filter(|worktree| is_visible_directory(worktree, cx)) + .map(|worktree| { + let worktree = worktree.read(cx); + (worktree.id(), worktree.abs_path()) + }) + .collect::>(); + + cx.background_spawn(async move { + let mut task_contexts = TaskContexts::default(); + + 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)); + } + } + + if let Some(active_worktree) = active_worktree { + if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) { + task_contexts.active_worktree_context = + Some((active_worktree, worktree_context(&active_worktree_abs_path))); + } + } else if worktree_abs_paths.len() == 1 { + task_contexts.active_worktree_context = worktree_abs_paths + .drain() + .next() + .map(|(id, abs_path)| (id, worktree_context(&abs_path))); + } + + task_contexts.other_worktree_contexts.extend( + worktree_abs_paths + .into_iter() + .map(|(id, abs_path)| (id, worktree_context(&abs_path))), + ); + task_contexts + }) +} + +fn is_visible_directory(worktree: &Entity, cx: &App) -> bool { + let worktree = worktree.read(cx); + worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir()) +} + +fn worktree_context(worktree_abs_path: &Path) -> TaskContext { + let mut task_variables = TaskVariables::default(); + task_variables.insert( + VariableName::WorktreeRoot, + worktree_abs_path.to_string_lossy().to_string(), + ); + TaskContext { + cwd: Some(worktree_abs_path.to_path_buf()), + task_variables, + project_env: HashMap::default(), + } +} + #[cfg(test)] mod tests { use std::{collections::HashMap, sync::Arc}; @@ -265,7 +359,7 @@ mod tests { use util::{path, separator}; use workspace::{AppState, Workspace}; - use crate::task_context; + use crate::task_contexts; #[gpui::test] async fn test_default_language_context(cx: &mut TestAppContext) { @@ -325,8 +419,8 @@ mod tests { "function" @context name: (_) @name parameters: (formal_parameters - "(" @context - ")" @context)) @item"#, + "(" @context + ")" @context)) @item"#, ) .unwrap() .with_context_provider(Some(Arc::new(BasicContextProvider::new( @@ -373,13 +467,15 @@ mod tests { workspace.active_item(cx).unwrap().item_id(), editor2.entity_id() ); - task_context(workspace, window, cx) + task_contexts(workspace, window, cx) }) .await; assert_eq!( - first_context, - TaskContext { + first_context + .active_context() + .expect("Should have an active context"), + &TaskContext { cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ (VariableName::File, path!("/dir/rust/b.rs").into()), @@ -405,10 +501,12 @@ mod tests { assert_eq!( workspace .update_in(cx, |workspace, window, cx| { - task_context(workspace, window, cx) + task_contexts(workspace, window, cx) }) - .await, - TaskContext { + .await + .active_context() + .expect("Should have an active context"), + &TaskContext { cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ (VariableName::File, path!("/dir/rust/b.rs").into()), @@ -431,10 +529,12 @@ mod tests { .update_in(cx, |workspace, window, cx| { // Now, let's switch the active item to .ts file. workspace.activate_item(&editor1, true, true, window, cx); - task_context(workspace, window, cx) + task_contexts(workspace, window, cx) }) - .await, - TaskContext { + .await + .active_context() + .expect("Should have an active context"), + &TaskContext { cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ (VariableName::File, path!("/dir/a.ts").into()),