diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index e6cc357bf6..635715c0be 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -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] fn test_template_variables_resolution() { let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1")); diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 41b24dee32..80c5dcf557 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -361,7 +361,7 @@ pub fn task_contexts( cx: &mut App, ) -> Task { let active_item = workspace.active_item(cx); - let active_worktree = active_item + let active_item_worktree = active_item .as_ref() .and_then(|item| item.project_path(cx)) .map(|project_path| project_path.worktree_id) @@ -371,21 +371,25 @@ pub fn task_contexts( .read(cx) .worktree_for_id(*worktree_id, 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::(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)) }); - let location = active_editor.as_ref().and_then(|editor| { + let location = active_project_editor.as_ref().and_then(|editor| { editor.update(cx, |editor, cx| { let selection = editor.selections.newest_anchor(); 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() .map(|active_editor| { active_editor.update(cx, |editor, cx| editor.lsp_task_sources(false, false, cx)) }) .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); snapshot .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 && 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 { @@ -491,6 +496,53 @@ mod tests { 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] async fn test_default_language_context(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/util/src/shell_builder.rs b/crates/util/src/shell_builder.rs index 76ff3923e6..870bf85caf 100644 --- a/crates/util/src/shell_builder.rs +++ b/crates/util/src/shell_builder.rs @@ -350,4 +350,38 @@ mod test { assert_eq!(program, "fish"); 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}^\"\""), + ] + ); + } }