feat(external-tools): force tools to open in new windows where needed
Adds a helper that forces selected tools to launch in a new window instead of reusing the current one. This is applied to code editors, diff/merge, and terminal launches, aligning behavior across platforms. Presets and defaults are updated to pass new-window or equivalent flags, and tests verify the new behavior for common tools. - Update code editors to always use a new window when opened - Normalize launch flags for Windows terminals and diff tools - Add tests covering new-window behavior for common tools
This commit is contained in:
@@ -405,8 +405,10 @@ fn known_tools() -> Vec<ToolSpec> {
|
||||
tool(
|
||||
"git-bash",
|
||||
"Git Bash",
|
||||
&["bash.exe"],
|
||||
&["git-bash.exe", "bash.exe"],
|
||||
&[
|
||||
r"%PROGRAMFILES%\Git\git-bash.exe",
|
||||
r"%LOCALAPPDATA%\Programs\Git\git-bash.exe",
|
||||
r"%PROGRAMFILES%\Git\bin\bash.exe",
|
||||
r"%LOCALAPPDATA%\Programs\Git\bin\bash.exe",
|
||||
],
|
||||
@@ -1350,6 +1352,101 @@ fn expand_args(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn command_in_new_window(mut command: ExternalToolCommand) -> ExternalToolCommand {
|
||||
let executable = Path::new(command.program.trim())
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(command.program.trim())
|
||||
.to_ascii_lowercase();
|
||||
|
||||
let remove_flags = |args: &mut Vec<String>, flags: &[&str]| {
|
||||
args.retain(|arg| !flags.iter().any(|flag| arg.eq_ignore_ascii_case(flag)));
|
||||
};
|
||||
let prepend_flag = |args: &mut Vec<String>, flag: &str| {
|
||||
if !args.iter().any(|arg| arg.eq_ignore_ascii_case(flag)) {
|
||||
args.insert(0, flag.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(
|
||||
executable.as_str(),
|
||||
"code"
|
||||
| "code.exe"
|
||||
| "code-insiders"
|
||||
| "code-insiders.exe"
|
||||
| "code - insiders.exe"
|
||||
| "cursor"
|
||||
| "cursor.exe"
|
||||
| "windsurf"
|
||||
| "windsurf.exe"
|
||||
| "vscodium"
|
||||
| "vscodium.exe"
|
||||
| "codium"
|
||||
| "codium.exe"
|
||||
) {
|
||||
remove_flags(&mut command.args, &["--reuse-window", "-r"]);
|
||||
prepend_flag(&mut command.args, "--new-window");
|
||||
} else if matches!(executable.as_str(), "zed" | "zed.exe") {
|
||||
remove_flags(&mut command.args, &["--reuse", "-r"]);
|
||||
prepend_flag(&mut command.args, "--new");
|
||||
} else if matches!(
|
||||
executable.as_str(),
|
||||
"subl" | "subl.exe" | "sublime_text" | "sublime_text.exe"
|
||||
) {
|
||||
prepend_flag(&mut command.args, "--new-window");
|
||||
} else if executable == "notepad++.exe" {
|
||||
prepend_flag(&mut command.args, "-multiInst");
|
||||
} else if executable == "kate" || executable == "kate.exe" {
|
||||
prepend_flag(&mut command.args, "--new");
|
||||
} else if executable == "geany" || executable == "geany.exe" {
|
||||
prepend_flag(&mut command.args, "--new-instance");
|
||||
} else if executable == "wt" || executable == "wt.exe" {
|
||||
let mut index = 0;
|
||||
while index < command.args.len() {
|
||||
let is_window = command.args[index].eq_ignore_ascii_case("-w")
|
||||
|| command.args[index].eq_ignore_ascii_case("--window")
|
||||
|| command.args[index]
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("--window=");
|
||||
if is_window {
|
||||
let has_separate_value = !command.args[index].contains('=');
|
||||
command.args.remove(index);
|
||||
if has_separate_value && index < command.args.len() {
|
||||
command.args.remove(index);
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
command
|
||||
.args
|
||||
.splice(0..0, ["-w".to_string(), "new".to_string()]);
|
||||
} else if executable == "explorer" || executable == "explorer.exe" {
|
||||
prepend_flag(&mut command.args, "/n,");
|
||||
} else if matches!(
|
||||
executable.as_str(),
|
||||
"totalcmd" | "totalcmd.exe" | "totalcmd64" | "totalcmd64.exe"
|
||||
) {
|
||||
command
|
||||
.args
|
||||
.retain(|arg| !arg.to_ascii_lowercase().starts_with("/o"));
|
||||
prepend_flag(&mut command.args, "/N");
|
||||
} else if executable == "open" {
|
||||
prepend_flag(&mut command.args, "-n");
|
||||
} else if executable.starts_with("bcomp") {
|
||||
remove_flags(&mut command.args, &["/solo", "-solo"]);
|
||||
#[cfg(windows)]
|
||||
prepend_flag(&mut command.args, "/solo");
|
||||
#[cfg(not(windows))]
|
||||
prepend_flag(&mut command.args, "-solo");
|
||||
} else if executable == "winmergeu" || executable == "winmergeu.exe" {
|
||||
remove_flags(&mut command.args, &["/s", "/sw", "-s", "-sw"]);
|
||||
prepend_flag(&mut command.args, "/s-");
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
fn tool_values(repo: &Path, file: Option<&Path>) -> BTreeMap<&'static str, String> {
|
||||
let mut values = BTreeMap::new();
|
||||
values.insert("repo", repo.to_string_lossy().into_owned());
|
||||
@@ -1371,11 +1468,10 @@ fn spawn_tool(
|
||||
repo: &Path,
|
||||
values: BTreeMap<&str, String>,
|
||||
) -> Result<(), String> {
|
||||
let command = command_in_new_window(command);
|
||||
let args = expand_args(&command, &values)?;
|
||||
let mut process = Command::new(command.program.trim());
|
||||
process.args(args).current_dir(repo);
|
||||
#[cfg(windows)]
|
||||
process.creation_flags(CREATE_NO_WINDOW);
|
||||
process
|
||||
.spawn()
|
||||
.map_err(|error| format!("Could not launch external tool: {error}"))?;
|
||||
@@ -1388,6 +1484,7 @@ fn run_tool(
|
||||
values: BTreeMap<&str, String>,
|
||||
kind: ExternalToolRunKind,
|
||||
) -> Result<(), String> {
|
||||
let command = command_in_new_window(command);
|
||||
let args = expand_args(&command, &values)?;
|
||||
let program = command.program.trim().to_owned();
|
||||
let mut process = Command::new(command.program.trim());
|
||||
@@ -1580,6 +1677,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forces_code_family_tools_into_a_new_window() {
|
||||
let command = command_in_new_window(ExternalToolCommand {
|
||||
program: "C:/Program Files/Microsoft VS Code/Code.exe".into(),
|
||||
args: vec![
|
||||
"--reuse-window".into(),
|
||||
"--diff".into(),
|
||||
"{left}".into(),
|
||||
"{right}".into(),
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
command.args,
|
||||
vec!["--new-window", "--diff", "{left}", "{right}"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forces_windows_terminal_to_use_a_fresh_window() {
|
||||
let command = command_in_new_window(ExternalToolCommand {
|
||||
program: "wt.exe".into(),
|
||||
args: vec![
|
||||
"--window".into(),
|
||||
"last".into(),
|
||||
"-d".into(),
|
||||
"{repo}".into(),
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(command.args, vec!["-w", "new", "-d", "{repo}"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forces_single_instance_compare_and_file_tools_into_new_windows() {
|
||||
let beyond = command_in_new_window(ExternalToolCommand {
|
||||
program: "BCompare.exe".into(),
|
||||
args: vec!["/readonly".into(), "{left}".into(), "{right}".into()],
|
||||
});
|
||||
let winmerge = command_in_new_window(ExternalToolCommand {
|
||||
program: "WinMergeU.exe".into(),
|
||||
args: vec!["/s".into(), "{left}".into(), "{right}".into()],
|
||||
});
|
||||
let total_commander = command_in_new_window(ExternalToolCommand {
|
||||
program: "TOTALCMD64.EXE".into(),
|
||||
args: vec!["/O".into(), "/T".into(), "{repo}".into()],
|
||||
});
|
||||
|
||||
#[cfg(windows)]
|
||||
assert_eq!(beyond.args[0], "/solo");
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(beyond.args[0], "-solo");
|
||||
assert_eq!(winmerge.args, vec!["/s-", "{left}", "{right}"]);
|
||||
assert_eq!(total_commander.args, vec!["/N", "/T", "{repo}"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_placeholder() {
|
||||
let command = ExternalToolCommand {
|
||||
|
||||
Reference in New Issue
Block a user