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
289 lines
18 KiB
TypeScript
289 lines
18 KiB
TypeScript
import type {
|
|
DetectedExternalTool,
|
|
ExternalToolKind,
|
|
ExternalToolSetting,
|
|
ExternalToolsSettings,
|
|
ToolOpenMode,
|
|
} from "./types";
|
|
|
|
export type { ExternalToolKind } from "./types";
|
|
|
|
export interface ExternalToolPreset {
|
|
id: string;
|
|
label: string;
|
|
program: string;
|
|
args: string[];
|
|
builtIn?: boolean;
|
|
}
|
|
|
|
type ToolIdentity = Pick<ExternalToolPreset, "id" | "label" | "program">;
|
|
|
|
const userAgent = navigator.userAgent;
|
|
export const externalToolsPlatform = userAgent.includes("Windows")
|
|
? "windows"
|
|
: userAgent.includes("Mac")
|
|
? "macos"
|
|
: "linux";
|
|
|
|
const codeFamily: ToolIdentity[] = externalToolsPlatform === "windows"
|
|
? [
|
|
{ id: "vscode", label: "Visual Studio Code", program: "Code.exe" },
|
|
{ id: "vscode-insiders", label: "Visual Studio Code Insiders", program: "Code - Insiders.exe" },
|
|
{ id: "cursor", label: "Cursor", program: "Cursor.exe" },
|
|
{ id: "windsurf", label: "Windsurf", program: "Windsurf.exe" },
|
|
{ id: "vscodium", label: "VSCodium", program: "VSCodium.exe" },
|
|
]
|
|
: [
|
|
{ id: "vscode", label: "Visual Studio Code", program: "code" },
|
|
{ id: "vscode-insiders", label: "Visual Studio Code Insiders", program: "code-insiders" },
|
|
{ id: "cursor", label: "Cursor", program: "cursor" },
|
|
{ id: "windsurf", label: "Windsurf", program: "windsurf" },
|
|
{ id: "vscodium", label: "VSCodium", program: "codium" },
|
|
];
|
|
|
|
const jetBrains: ToolIdentity[] = [
|
|
{ id: "intellij-idea", label: "IntelliJ IDEA", program: externalToolsPlatform === "windows" ? "idea64.exe" : "idea" },
|
|
{ id: "webstorm", label: "WebStorm", program: externalToolsPlatform === "windows" ? "webstorm64.exe" : "webstorm" },
|
|
{ id: "pycharm", label: "PyCharm", program: externalToolsPlatform === "windows" ? "pycharm64.exe" : "pycharm" },
|
|
{ id: "phpstorm", label: "PhpStorm", program: externalToolsPlatform === "windows" ? "phpstorm64.exe" : "phpstorm" },
|
|
{ id: "rider", label: "JetBrains Rider", program: externalToolsPlatform === "windows" ? "rider64.exe" : "rider" },
|
|
{ id: "clion", label: "CLion", program: externalToolsPlatform === "windows" ? "clion64.exe" : "clion" },
|
|
{ id: "rustrover", label: "RustRover", program: externalToolsPlatform === "windows" ? "rustrover64.exe" : "rustrover" },
|
|
{ id: "goland", label: "GoLand", program: externalToolsPlatform === "windows" ? "goland64.exe" : "goland" },
|
|
];
|
|
|
|
const editor = (tool: ToolIdentity, args = ["{file}"]): ExternalToolPreset => ({ ...tool, args });
|
|
const codeDiff = (tool: ToolIdentity): ExternalToolPreset => ({ ...tool, args: ["--new-window", "--wait", "--diff", "{left}", "{right}"] });
|
|
const codeMerge = (tool: ToolIdentity): ExternalToolPreset => ({ ...tool, args: ["--new-window", "--wait", "--merge", "{ours}", "{theirs}", "{base}", "{result}"] });
|
|
const jetBrainsDiff = (tool: ToolIdentity): ExternalToolPreset => ({ ...tool, args: ["diff", "{left}", "{right}"] });
|
|
const jetBrainsMerge = (tool: ToolIdentity): ExternalToolPreset => ({ ...tool, args: ["merge", "{ours}", "{theirs}", "{base}", "{result}"] });
|
|
|
|
const commonEditors: ExternalToolPreset[] = [
|
|
...codeFamily.map((item) => editor(item, ["--new-window", "{file}"])),
|
|
editor({ id: "zed", label: "Zed", program: externalToolsPlatform === "windows" ? "zed.exe" : "zed" }, ["--new", "{file}"]),
|
|
editor({ id: "sublime-text", label: "Sublime Text", program: externalToolsPlatform === "windows" ? "subl.exe" : "subl" }, ["--new-window", "{file}"]),
|
|
...jetBrains.map((item) => editor(item)),
|
|
editor({ id: "neovim", label: "Neovim", program: externalToolsPlatform === "windows" ? "nvim.exe" : "nvim" }),
|
|
editor({ id: "vim", label: "Vim", program: externalToolsPlatform === "windows" ? "gvim.exe" : "gvim" }),
|
|
editor({ id: "emacs", label: "Emacs", program: externalToolsPlatform === "windows" ? "runemacs.exe" : "emacs" }),
|
|
];
|
|
|
|
const platformEditors: ExternalToolPreset[] = externalToolsPlatform === "windows"
|
|
? [
|
|
editor({ id: "notepad-plus-plus", label: "Notepad++", program: "notepad++.exe" }, ["-multiInst", "{file}"]),
|
|
{ ...editor({ id: "notepad", label: "Windows Notepad", program: "notepad.exe" }), builtIn: true },
|
|
]
|
|
: externalToolsPlatform === "macos"
|
|
? [
|
|
editor({ id: "nova", label: "Nova", program: "nova" }),
|
|
editor({ id: "textmate", label: "TextMate", program: "mate" }),
|
|
editor({ id: "bbedit", label: "BBEdit", program: "bbedit" }),
|
|
editor({ id: "xcode", label: "Xcode", program: "xed" }),
|
|
]
|
|
: [
|
|
editor({ id: "lapce", label: "Lapce", program: "lapce" }),
|
|
editor({ id: "kate", label: "Kate", program: "kate" }, ["--new", "{file}"]),
|
|
editor({ id: "gedit", label: "GNOME Text Editor", program: "gnome-text-editor" }),
|
|
editor({ id: "geany", label: "Geany", program: "geany" }, ["--new-instance", "{file}"]),
|
|
editor({ id: "helix", label: "Helix", program: "hx" }),
|
|
];
|
|
|
|
const dedicatedDiff: ExternalToolPreset[] = externalToolsPlatform === "windows"
|
|
? [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "BCompare.exe", args: ["/solo", "/readonly", "{left}", "{right}"] },
|
|
{ id: "winmerge", label: "WinMerge", program: "WinMergeU.exe", args: ["/s-", "/u", "/e", "/wl", "/wr", "{left}", "{right}"] },
|
|
{ id: "meld", label: "Meld", program: "meld.exe", args: ["--wait", "{left}", "{right}"] },
|
|
{ id: "kdiff3", label: "KDiff3", program: "kdiff3.exe", args: ["{left}", "{right}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge.exe", args: ["{left}", "{right}"] },
|
|
{ id: "araxis-merge", label: "Araxis Merge", program: "Compare.exe", args: ["/wait", "{left}", "{right}"] },
|
|
{ id: "tortoisegitmerge", label: "TortoiseGitMerge", program: "TortoiseGitMerge.exe", args: ["/base:{left}", "/mine:{right}"] },
|
|
]
|
|
: externalToolsPlatform === "macos"
|
|
? [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "bcompare", args: ["-solo", "{left}", "{right}"] },
|
|
{ id: "kaleidoscope", label: "Kaleidoscope", program: "ksdiff", args: ["--wait", "{left}", "{right}"] },
|
|
{ id: "araxis-merge", label: "Araxis Merge", program: "compare", args: ["-wait", "{left}", "{right}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge", args: ["{left}", "{right}"] },
|
|
{ id: "opendiff", label: "FileMerge", program: "opendiff", args: ["{left}", "{right}"] },
|
|
]
|
|
: [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "bcompare", args: ["-solo", "{left}", "{right}"] },
|
|
{ id: "meld", label: "Meld", program: "meld", args: ["--wait", "{left}", "{right}"] },
|
|
{ id: "kdiff3", label: "KDiff3", program: "kdiff3", args: ["{left}", "{right}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge", args: ["{left}", "{right}"] },
|
|
{ id: "kompare", label: "Kompare", program: "kompare", args: ["{left}", "{right}"] },
|
|
];
|
|
|
|
const dedicatedMerge: ExternalToolPreset[] = externalToolsPlatform === "windows"
|
|
? [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "BCompare.exe", args: ["/solo", "{ours}", "{theirs}", "{base}", "/mergeoutput={result}"] },
|
|
{ id: "winmerge", label: "WinMerge", program: "WinMergeU.exe", args: ["/s-", "/u", "/e", "{ours}", "{theirs}", "{base}", "/o", "{result}"] },
|
|
{ id: "meld", label: "Meld", program: "meld.exe", args: ["--wait", "--auto-merge", "{ours}", "{base}", "{theirs}", "--output={result}"] },
|
|
{ id: "kdiff3", label: "KDiff3", program: "kdiff3.exe", args: ["{base}", "{ours}", "{theirs}", "-o", "{result}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge.exe", args: ["{base}", "{theirs}", "{ours}", "{result}"] },
|
|
{ id: "araxis-merge", label: "Araxis Merge", program: "Compare.exe", args: ["/wait", "{ours}", "{base}", "{theirs}", "{result}"] },
|
|
{ id: "tortoisegitmerge", label: "TortoiseGitMerge", program: "TortoiseGitMerge.exe", args: ["/base:{base}", "/theirs:{theirs}", "/mine:{ours}", "/merged:{result}"] },
|
|
]
|
|
: externalToolsPlatform === "macos"
|
|
? [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "bcompare", args: ["-solo", "{ours}", "{theirs}", "{base}", "/mergeoutput={result}"] },
|
|
{ id: "kaleidoscope", label: "Kaleidoscope", program: "ksdiff", args: ["--merge", "--output", "{result}", "{base}", "{ours}", "{theirs}"] },
|
|
{ id: "araxis-merge", label: "Araxis Merge", program: "compare", args: ["-wait", "{ours}", "{base}", "{theirs}", "{result}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge", args: ["{base}", "{theirs}", "{ours}", "{result}"] },
|
|
{ id: "opendiff", label: "FileMerge", program: "opendiff", args: ["{ours}", "{theirs}", "-ancestor", "{base}", "-merge", "{result}"] },
|
|
]
|
|
: [
|
|
{ id: "beyond-compare", label: "Beyond Compare", program: "bcompare", args: ["-solo", "{ours}", "{theirs}", "{base}", "/mergeoutput={result}"] },
|
|
{ id: "meld", label: "Meld", program: "meld", args: ["--wait", "--auto-merge", "{ours}", "{base}", "{theirs}", "--output={result}"] },
|
|
{ id: "kdiff3", label: "KDiff3", program: "kdiff3", args: ["{base}", "{ours}", "{theirs}", "-o", "{result}"] },
|
|
{ id: "p4merge", label: "P4Merge", program: "p4merge", args: ["{base}", "{theirs}", "{ours}", "{result}"] },
|
|
];
|
|
|
|
const terminalPresets: ExternalToolPreset[] = externalToolsPlatform === "windows"
|
|
? [
|
|
{ id: "windows-terminal", label: "Windows Terminal", program: "wt.exe", args: ["-w", "new", "-d", "{repo}"] },
|
|
{ id: "powershell", label: "PowerShell 7", program: "pwsh.exe", args: ["-NoExit", "-WorkingDirectory", "{repo}"] },
|
|
{ id: "windows-powershell", label: "Windows PowerShell", program: "powershell.exe", args: ["-NoExit"], builtIn: true },
|
|
{ id: "git-bash", label: "Git Bash", program: "git-bash.exe", args: ["--login", "-i"] },
|
|
{ id: "cmd", label: "Command Prompt", program: "cmd.exe", args: ["/K"], builtIn: true },
|
|
{ id: "wezterm", label: "WezTerm", program: "wezterm-gui.exe", args: ["start", "--cwd", "{repo}"] },
|
|
{ id: "alacritty", label: "Alacritty", program: "alacritty.exe", args: ["--working-directory", "{repo}"] },
|
|
{ id: "kitty-terminal", label: "kitty", program: "kitty.exe", args: ["--directory", "{repo}"] },
|
|
]
|
|
: externalToolsPlatform === "macos"
|
|
? [
|
|
{ id: "terminal", label: "Terminal", program: "open", args: ["-n", "-a", "Terminal", "{repo}"], builtIn: true },
|
|
{ id: "iterm2", label: "iTerm2", program: "open", args: ["-n", "-a", "iTerm", "{repo}"] },
|
|
{ id: "warp", label: "Warp", program: "open", args: ["-n", "-a", "Warp", "{repo}"] },
|
|
{ id: "wezterm", label: "WezTerm", program: "wezterm", args: ["start", "--cwd", "{repo}"] },
|
|
{ id: "alacritty", label: "Alacritty", program: "alacritty", args: ["--working-directory", "{repo}"] },
|
|
]
|
|
: [
|
|
{ id: "x-terminal", label: "System terminal", program: "x-terminal-emulator", args: ["--working-directory={repo}"] },
|
|
{ id: "gnome-terminal", label: "GNOME Terminal", program: "gnome-terminal", args: ["--working-directory={repo}"] },
|
|
{ id: "konsole", label: "Konsole", program: "konsole", args: ["--workdir", "{repo}"] },
|
|
{ id: "kitty-terminal", label: "kitty", program: "kitty", args: ["--directory", "{repo}"] },
|
|
{ id: "wezterm", label: "WezTerm", program: "wezterm", args: ["start", "--cwd", "{repo}"] },
|
|
{ id: "alacritty", label: "Alacritty", program: "alacritty", args: ["--working-directory", "{repo}"] },
|
|
{ id: "xfce-terminal", label: "Xfce Terminal", program: "xfce4-terminal", args: ["--working-directory={repo}"] },
|
|
{ id: "tilix", label: "Tilix", program: "tilix", args: ["--working-directory={repo}"] },
|
|
];
|
|
|
|
const fileManagerPresets: ExternalToolPreset[] = externalToolsPlatform === "windows"
|
|
? [
|
|
{ id: "explorer", label: "Windows Explorer", program: "explorer.exe", args: ["/n,", "{repo}"], builtIn: true },
|
|
{ id: "total-commander", label: "Total Commander", program: "TOTALCMD64.EXE", args: ["/N", "/T", "{repo}"] },
|
|
{ id: "directory-opus", label: "Directory Opus", program: "dopus.exe", args: ["{repo}"] },
|
|
{ id: "double-commander", label: "Double Commander", program: "doublecmd.exe", args: ["{repo}"] },
|
|
{ id: "freecommander", label: "FreeCommander XE", program: "FreeCommander.exe", args: ["/L={repo}"] },
|
|
{ id: "xyplorer", label: "XYplorer", program: "XYplorer.exe", args: ["{repo}"] },
|
|
]
|
|
: externalToolsPlatform === "macos"
|
|
? [
|
|
{ id: "finder", label: "Finder", program: "open", args: ["-n", "{repo}"], builtIn: true },
|
|
{ id: "forklift", label: "ForkLift", program: "forklift", args: ["{repo}"] },
|
|
{ id: "path-finder", label: "Path Finder", program: "Path Finder", args: ["{repo}"] },
|
|
]
|
|
: [
|
|
{ id: "system-file-manager", label: "System file manager", program: "xdg-open", args: ["{repo}"], builtIn: true },
|
|
{ id: "nautilus", label: "GNOME Files", program: "nautilus", args: ["{repo}"] },
|
|
{ id: "dolphin", label: "Dolphin", program: "dolphin", args: ["{repo}"] },
|
|
{ id: "thunar", label: "Thunar", program: "thunar", args: ["{repo}"] },
|
|
{ id: "nemo", label: "Nemo", program: "nemo", args: ["{repo}"] },
|
|
{ id: "pcmanfm", label: "PCManFM", program: "pcmanfm", args: ["{repo}"] },
|
|
{ id: "double-commander", label: "Double Commander", program: "doublecmd", args: ["{repo}"] },
|
|
];
|
|
|
|
export const externalToolPresets: Record<ExternalToolKind, ExternalToolPreset[]> = {
|
|
editor: [...commonEditors, ...platformEditors],
|
|
diff: [...dedicatedDiff, ...codeFamily.map(codeDiff), { id: "zed", label: "Zed", program: externalToolsPlatform === "windows" ? "zed.exe" : "zed", args: ["--new", "--diff", "{left}", "{right}"] }, ...jetBrains.map(jetBrainsDiff)],
|
|
merge: [...dedicatedMerge, ...codeFamily.map(codeMerge), ...jetBrains.map(jetBrainsMerge)],
|
|
terminal: terminalPresets,
|
|
fileManager: fileManagerPresets,
|
|
};
|
|
|
|
function detectedPreset(kind: ExternalToolKind, id: string, detectedTools: DetectedExternalTool[]): DetectedExternalTool | undefined {
|
|
return detectedTools.find((tool) => tool.id === id && tool.kinds.includes(kind));
|
|
}
|
|
|
|
export function isExternalToolPresetAvailable(kind: ExternalToolKind, preset: ExternalToolPreset, detectedTools: DetectedExternalTool[]): boolean {
|
|
return preset.builtIn === true || detectedPreset(kind, preset.id, detectedTools) != null;
|
|
}
|
|
|
|
export function applyExternalToolPreset(kind: ExternalToolKind, id: string, detectedTools: DetectedExternalTool[] = []): ExternalToolSetting {
|
|
const value = externalToolPresets[kind].find((item) => item.id === id) ?? externalToolPresets[kind][0];
|
|
const detected = detectedPreset(kind, value.id, detectedTools);
|
|
return { preset: value.id, program: detected?.program ?? value.program, args: [...value.args] };
|
|
}
|
|
|
|
function preferredPreset(kind: ExternalToolKind, detectedTools: DetectedExternalTool[]): ExternalToolPreset {
|
|
return externalToolPresets[kind].find((item) => isExternalToolPresetAvailable(kind, item, detectedTools))
|
|
?? externalToolPresets[kind][0];
|
|
}
|
|
|
|
export function defaultExternalToolsSettings(detectedTools: DetectedExternalTool[] = []): ExternalToolsSettings {
|
|
return {
|
|
editor: applyExternalToolPreset("editor", preferredPreset("editor", detectedTools).id, detectedTools),
|
|
diff: applyExternalToolPreset("diff", preferredPreset("diff", detectedTools).id, detectedTools),
|
|
merge: applyExternalToolPreset("merge", preferredPreset("merge", detectedTools).id, detectedTools),
|
|
terminal: applyExternalToolPreset("terminal", preferredPreset("terminal", detectedTools).id, detectedTools),
|
|
fileManager: applyExternalToolPreset("fileManager", preferredPreset("fileManager", detectedTools).id, detectedTools),
|
|
diffOpenMode: "gitty",
|
|
mergeOpenMode: "gitty",
|
|
};
|
|
}
|
|
|
|
function normaliseSetting(kind: ExternalToolKind, value: unknown, fallback: ExternalToolSetting): ExternalToolSetting {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return fallback;
|
|
const candidate = value as Partial<ExternalToolSetting>;
|
|
const program = typeof candidate.program === "string" && candidate.program.trim() ? candidate.program : fallback.program;
|
|
const storedArgs = Array.isArray(candidate.args)
|
|
? candidate.args.filter((item): item is string => typeof item === "string").slice(0, 64)
|
|
: fallback.args;
|
|
const preset = typeof candidate.preset === "string" && candidate.preset ? candidate.preset : fallback.preset;
|
|
// Preset arguments are application-owned and can be upgraded safely. Editing either
|
|
// the executable or arguments in Settings changes the preset to "custom", which keeps
|
|
// genuine user commands untouched.
|
|
const currentPreset = preset === "custom"
|
|
? undefined
|
|
: externalToolPresets[kind].find((item) => item.id === preset);
|
|
const args = currentPreset ? [...currentPreset.args] : storedArgs;
|
|
return { preset, program, args };
|
|
}
|
|
|
|
export function normaliseExternalToolsSettings(value: unknown): ExternalToolsSettings {
|
|
const defaults = defaultExternalToolsSettings();
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return defaults;
|
|
const candidate = value as Partial<ExternalToolsSettings>;
|
|
return {
|
|
editor: normaliseSetting("editor", candidate.editor, defaults.editor),
|
|
diff: normaliseSetting("diff", candidate.diff, defaults.diff),
|
|
merge: normaliseSetting("merge", candidate.merge, defaults.merge),
|
|
terminal: normaliseSetting("terminal", candidate.terminal, defaults.terminal),
|
|
fileManager: normaliseSetting("fileManager", candidate.fileManager, defaults.fileManager),
|
|
diffOpenMode: normaliseOpenMode(candidate.diffOpenMode, defaults.diffOpenMode),
|
|
mergeOpenMode: normaliseOpenMode(candidate.mergeOpenMode, defaults.mergeOpenMode),
|
|
};
|
|
}
|
|
|
|
function normaliseOpenMode(value: unknown, fallback: ToolOpenMode): ToolOpenMode {
|
|
return value === "gitty" || value === "external" ? value : fallback;
|
|
}
|
|
|
|
export function resolveDetectedExternalToolPrograms(settings: ExternalToolsSettings, detectedTools: DetectedExternalTool[]): ExternalToolsSettings {
|
|
const resolved = structuredClone(settings);
|
|
for (const kind of ["editor", "diff", "merge", "terminal", "fileManager"] as ExternalToolKind[]) {
|
|
if (resolved[kind].preset === "custom") continue;
|
|
const detected = detectedPreset(kind, resolved[kind].preset, detectedTools);
|
|
if (detected) resolved[kind].program = detected.program;
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
export function externalToolDisplayName(kind: ExternalToolKind, setting: ExternalToolSetting, detectedTools: DetectedExternalTool[] = []): string {
|
|
if (setting.preset === "custom") return setting.program.split(/[\\/]/).pop() || setting.program;
|
|
return detectedPreset(kind, setting.preset, detectedTools)?.label
|
|
?? externalToolPresets[kind].find((item) => item.id === setting.preset)?.label
|
|
?? setting.program;
|
|
}
|