diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 8444849b5b..361fab7559 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; + 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(), + ); + } } - 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 3003bc746c..4c545bb911 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 c6e51f930a..962a3363f1 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 b727d1365b..aa9289aea7 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()) + 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 + } } - 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 32e9add67c..a53a4d33e1 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 5a536c0927..2738d74b57 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()),