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
This commit is contained in:
parent
d2b49de0e4
commit
b5a1ae6526
6 changed files with 390 additions and 164 deletions
|
@ -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,
|
||||
|
|
|
@ -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<_>>(),
|
||||
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<Language> {
|
|||
fn get_all_tasks(
|
||||
project: &Entity<Project>,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
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
|
||||
|
|
|
@ -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<WorktreeId>, 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<WorktreeId>,
|
||||
location: Option<Location>,
|
||||
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::<Vec<_>>();
|
||||
})
|
||||
.sorted_unstable_by(task_lru_comparator)
|
||||
.map(|(kind, task, _)| (kind, task))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(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<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)
|
||||
});
|
||||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue