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:
Kirill Bulatov 2025-02-26 22:30:31 +02:00 committed by GitHub
parent d2b49de0e4
commit b5a1ae6526
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 390 additions and 164 deletions

View file

@ -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