Fix Windows task path handling for worktree variables with spaces (#55727)
## Summary Fixes task variable resolution and Windows shell argument handling when `$ZED_WORKTREE_ROOT` contains spaces or when tasks are spawned while the global `tasks.json` buffer is focused. ## Problem `$ZED_WORKTREE_ROOT` could resolve from the active editor item even when that item was outside every visible project worktree, such as the global `tasks.json` file under the Zed config directory. On Windows, paths with spaces also need to remain one logical argument when terminal shell commands are constructed. ## Changes - Only let the active editor item provide the active task context when it belongs to a visible project worktree. - Fall back to the selected or first visible project worktree when the active editor item is external/global. - Preserve project-file-specific task variables for normal project buffers. - Add regression coverage for `$ZED_WORKTREE_ROOT` in `args` and `cwd` with synthetic Windows-style paths containing spaces. - Add regression coverage for PowerShell and cmd shell command construction so a spaced path remains quoted as one shell argument. ## Testing Ran on Windows with Rust 1.95.0, Visual Studio Build Tools 2022, and Windows SDK 10.0.26100.0: - `cargo fmt --all --check` - `cargo test -p util shell_builder::test::windows_powershell_preserves_spaced_arg_as_single_shell_argument -- --exact` - `cargo test -p util shell_builder::test::windows_cmd_preserves_spaced_arg_as_single_shell_argument -- --exact` - `cargo test -p task task_template::tests::test_worktree_root_with_spaces_stays_atomic_in_args_and_cwd -- --exact` - `cargo test -p tasks_ui tests::test_non_project_active_editor_uses_visible_worktree_context -- --exact` - `cargo test -p task` - `cargo test -p tasks_ui` - `cargo test -p workspace` Release Notes: - Fix Windows task path handling for worktree variables with spaces - [GPUI] N/A Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
1 parent
56ae3f4293
commit
a0f008b755
3 files changed
+138
-12
No files matched your search
@@ -626,6 +626,46 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worktree_root_with_spaces_stays_atomic_in_args_and_cwd() {
|
||||||
|
let worktree_root = r"C:\worktrees\Godot Projects\sample-game";
|
||||||
|
let task = TaskTemplate {
|
||||||
|
label: "Run Godot Game".to_string(),
|
||||||
|
command: "godot".to_string(),
|
||||||
|
args: vec![
|
||||||
|
"--path".to_string(),
|
||||||
|
VariableName::WorktreeRoot.template_value(),
|
||||||
|
"scenes/main_menu.tscn".to_string(),
|
||||||
|
],
|
||||||
|
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||||
|
..TaskTemplate::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = task
|
||||||
|
.resolve_task(
|
||||||
|
TEST_ID_BASE,
|
||||||
|
&TaskContext {
|
||||||
|
cwd: None,
|
||||||
|
task_variables: TaskVariables::from_iter([(
|
||||||
|
VariableName::WorktreeRoot,
|
||||||
|
worktree_root.to_string(),
|
||||||
|
)]),
|
||||||
|
project_env: HashMap::default(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("task should resolve with worktree root variable");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolved.resolved.args,
|
||||||
|
vec![
|
||||||
|
"--path".to_string(),
|
||||||
|
worktree_root.to_string(),
|
||||||
|
"scenes/main_menu.tscn".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(resolved.resolved.cwd, Some(PathBuf::from(worktree_root)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_template_variables_resolution() {
|
fn test_template_variables_resolution() {
|
||||||
let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1"));
|
let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1"));
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ pub fn task_contexts(
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<TaskContexts> {
|
) -> Task<TaskContexts> {
|
||||||
let active_item = workspace.active_item(cx);
|
let active_item = workspace.active_item(cx);
|
||||||
let active_worktree = active_item
|
let active_item_worktree = active_item
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|item| item.project_path(cx))
|
.and_then(|item| item.project_path(cx))
|
||||||
.map(|project_path| project_path.worktree_id)
|
.map(|project_path| project_path.worktree_id)
|
||||||
@@ -371,21 +371,25 @@ pub fn task_contexts(
|
|||||||
.read(cx)
|
.read(cx)
|
||||||
.worktree_for_id(*worktree_id, cx)
|
.worktree_for_id(*worktree_id, cx)
|
||||||
.is_some_and(|worktree| is_visible_directory(&worktree, cx))
|
.is_some_and(|worktree| is_visible_directory(&worktree, cx))
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
workspace
|
|
||||||
.visible_worktrees(cx)
|
|
||||||
.next()
|
|
||||||
.map(|tree| tree.read(cx).id())
|
|
||||||
});
|
});
|
||||||
|
let active_worktree = active_item_worktree.or_else(|| {
|
||||||
|
workspace
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.map(|tree| tree.read(cx).id())
|
||||||
|
});
|
||||||
|
|
||||||
let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
|
let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
|
||||||
|
let active_project_editor = active_item_worktree
|
||||||
|
.is_some()
|
||||||
|
.then(|| active_editor.clone())
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let editor_context_task = active_editor.as_ref().map(|active_editor| {
|
let editor_context_task = active_project_editor.as_ref().map(|active_editor| {
|
||||||
active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
|
active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
let location = active_editor.as_ref().and_then(|editor| {
|
let location = active_project_editor.as_ref().and_then(|editor| {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
let selection = editor.selections.newest_anchor();
|
let selection = editor.selections.newest_anchor();
|
||||||
let multi_buffer = editor.buffer().clone();
|
let multi_buffer = editor.buffer().clone();
|
||||||
@@ -401,14 +405,14 @@ pub fn task_contexts(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let lsp_task_sources = active_editor
|
let lsp_task_sources = active_project_editor
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|active_editor| {
|
.map(|active_editor| {
|
||||||
active_editor.update(cx, |editor, cx| editor.lsp_task_sources(false, false, cx))
|
active_editor.update(cx, |editor, cx| editor.lsp_task_sources(false, false, cx))
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let latest_selection = active_editor.as_ref().and_then(|active_editor| {
|
let latest_selection = active_project_editor.as_ref().and_then(|active_editor| {
|
||||||
let snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
|
let snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||||
snapshot
|
snapshot
|
||||||
.anchor_to_buffer_anchor(active_editor.read(cx).selections.newest_anchor().head())
|
.anchor_to_buffer_anchor(active_editor.read(cx).selections.newest_anchor().head())
|
||||||
@@ -433,7 +437,8 @@ pub fn task_contexts(
|
|||||||
if let Some(editor_context_task) = editor_context_task
|
if let Some(editor_context_task) = editor_context_task
|
||||||
&& let Some(editor_context) = editor_context_task.await
|
&& let Some(editor_context) = editor_context_task.await
|
||||||
{
|
{
|
||||||
task_contexts.active_item_context = Some((active_worktree, location, editor_context));
|
task_contexts.active_item_context =
|
||||||
|
Some((active_item_worktree, location, editor_context));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(active_worktree) = active_worktree {
|
if let Some(active_worktree) = active_worktree {
|
||||||
@@ -491,6 +496,53 @@ mod tests {
|
|||||||
|
|
||||||
use crate::task_contexts;
|
use crate::task_contexts;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_non_project_active_editor_uses_visible_worktree_context(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let worktree_root = path!("/worktrees/Godot Projects/sample-game");
|
||||||
|
fs.insert_tree(
|
||||||
|
worktree_root,
|
||||||
|
json!({
|
||||||
|
".zed": {
|
||||||
|
"tasks.json": "[]",
|
||||||
|
},
|
||||||
|
"scenes": {
|
||||||
|
"main_menu.tscn": "",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, [worktree_root.as_ref()], cx).await;
|
||||||
|
let (multi_workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
|
||||||
|
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
|
||||||
|
let global_tasks_editor = cx.new_window_entity(|window, cx| Editor::multi_line(window, cx));
|
||||||
|
|
||||||
|
let contexts = workspace
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
workspace.add_item_to_center(Box::new(global_tasks_editor), window, cx);
|
||||||
|
task_contexts(workspace, window, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(contexts.active_item_context.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
contexts
|
||||||
|
.active_context()
|
||||||
|
.expect("visible worktree should provide an active task context"),
|
||||||
|
&TaskContext {
|
||||||
|
cwd: Some(worktree_root.into()),
|
||||||
|
task_variables: TaskVariables::from_iter([(
|
||||||
|
VariableName::WorktreeRoot,
|
||||||
|
worktree_root.into(),
|
||||||
|
)]),
|
||||||
|
project_env: HashMap::default(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_default_language_context(cx: &mut TestAppContext) {
|
async fn test_default_language_context(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|||||||
@@ -350,4 +350,38 @@ mod test {
|
|||||||
assert_eq!(program, "fish");
|
assert_eq!(program, "fish");
|
||||||
assert_eq!(args, vec!["-i", "-c", "echo oo"]);
|
assert_eq!(args, vec!["-i", "-c", "echo oo"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_powershell_preserves_spaced_arg_as_single_shell_argument() {
|
||||||
|
let worktree_root = r"C:\worktrees\Godot Projects\sample-game";
|
||||||
|
let shell = Shell::Program("powershell".to_owned());
|
||||||
|
|
||||||
|
let (program, args) = ShellBuilder::new(&shell, true)
|
||||||
|
.build(Some("echo".into()), &[worktree_root.to_string()]);
|
||||||
|
|
||||||
|
assert_eq!(program, "powershell");
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec!["-C".to_string(), format!("echo '{worktree_root}'")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_cmd_preserves_spaced_arg_as_single_shell_argument() {
|
||||||
|
let worktree_root = r"C:\worktrees\Godot Projects\sample-game";
|
||||||
|
let shell = Shell::Program("cmd".to_owned());
|
||||||
|
|
||||||
|
let (program, args) = ShellBuilder::new(&shell, true)
|
||||||
|
.build(Some("echo".into()), &[worktree_root.to_string()]);
|
||||||
|
|
||||||
|
assert_eq!(program, "cmd");
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
"/S".to_string(),
|
||||||
|
"/C".to_string(),
|
||||||
|
format!("\"echo ^\"{worktree_root}^\"\""),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user