From 7a9821607bafcbb98cf059705aaab358d46e711c Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sun, 20 Jul 2025 15:35:21 -0400 Subject: [PATCH] Check for zeditor if zed binary is not found (#3680) Co-authored-by: Jacob Richman Co-authored-by: Scott Densmore --- packages/core/src/utils/editor.test.ts | 191 ++++++++++++++++++++----- packages/core/src/utils/editor.ts | 33 +++-- 2 files changed, 178 insertions(+), 46 deletions(-) diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts index 382e5e18..86274be2 100644 --- a/packages/core/src/utils/editor.test.ts +++ b/packages/core/src/utils/editor.test.ts @@ -52,56 +52,99 @@ describe('editor utils', () => { describe('checkHasEditorType', () => { const testCases: Array<{ editor: EditorType; - command: string; - win32Command: string; + commands: string[]; + win32Commands: string[]; }> = [ - { editor: 'vscode', command: 'code', win32Command: 'code.cmd' }, - { editor: 'vscodium', command: 'codium', win32Command: 'codium.cmd' }, - { editor: 'windsurf', command: 'windsurf', win32Command: 'windsurf' }, - { editor: 'cursor', command: 'cursor', win32Command: 'cursor' }, - { editor: 'vim', command: 'vim', win32Command: 'vim' }, - { editor: 'neovim', command: 'nvim', win32Command: 'nvim' }, - { editor: 'zed', command: 'zed', win32Command: 'zed' }, + { editor: 'vscode', commands: ['code'], win32Commands: ['code.cmd'] }, + { + editor: 'vscodium', + commands: ['codium'], + win32Commands: ['codium.cmd'], + }, + { + editor: 'windsurf', + commands: ['windsurf'], + win32Commands: ['windsurf'], + }, + { editor: 'cursor', commands: ['cursor'], win32Commands: ['cursor'] }, + { editor: 'vim', commands: ['vim'], win32Commands: ['vim'] }, + { editor: 'neovim', commands: ['nvim'], win32Commands: ['nvim'] }, + { editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] }, ]; - for (const { editor, command, win32Command } of testCases) { + for (const { editor, commands, win32Commands } of testCases) { describe(`${editor}`, () => { - it(`should return true if "${command}" command exists on non-windows`, () => { + // Non-windows tests + it(`should return true if first command "${commands[0]}" exists on non-windows`, () => { Object.defineProperty(process, 'platform', { value: 'linux' }); (execSync as Mock).mockReturnValue( - Buffer.from(`/usr/bin/${command}`), + Buffer.from(`/usr/bin/${commands[0]}`), ); expect(checkHasEditorType(editor)).toBe(true); - expect(execSync).toHaveBeenCalledWith(`command -v ${command}`, { + expect(execSync).toHaveBeenCalledWith(`command -v ${commands[0]}`, { stdio: 'ignore', }); }); - it(`should return false if "${command}" command does not exist on non-windows`, () => { + if (commands.length > 1) { + it(`should return true if first command doesn't exist but second command "${commands[1]}" exists on non-windows`, () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock) + .mockImplementationOnce(() => { + throw new Error(); // first command not found + }) + .mockReturnValueOnce(Buffer.from(`/usr/bin/${commands[1]}`)); // second command found + expect(checkHasEditorType(editor)).toBe(true); + expect(execSync).toHaveBeenCalledTimes(2); + }); + } + + it(`should return false if none of the commands exist on non-windows`, () => { Object.defineProperty(process, 'platform', { value: 'linux' }); (execSync as Mock).mockImplementation(() => { - throw new Error(); + throw new Error(); // all commands not found }); expect(checkHasEditorType(editor)).toBe(false); + expect(execSync).toHaveBeenCalledTimes(commands.length); }); - it(`should return true if "${win32Command}" command exists on windows`, () => { + // Windows tests + it(`should return true if first command "${win32Commands[0]}" exists on windows`, () => { Object.defineProperty(process, 'platform', { value: 'win32' }); (execSync as Mock).mockReturnValue( - Buffer.from(`C:\\Program Files\\...\\${win32Command}`), + Buffer.from(`C:\\Program Files\\...\\${win32Commands[0]}`), ); expect(checkHasEditorType(editor)).toBe(true); - expect(execSync).toHaveBeenCalledWith(`where.exe ${win32Command}`, { - stdio: 'ignore', - }); + expect(execSync).toHaveBeenCalledWith( + `where.exe ${win32Commands[0]}`, + { + stdio: 'ignore', + }, + ); }); - it(`should return false if "${win32Command}" command does not exist on windows`, () => { + if (win32Commands.length > 1) { + it(`should return true if first command doesn't exist but second command "${win32Commands[1]}" exists on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock) + .mockImplementationOnce(() => { + throw new Error(); // first command not found + }) + .mockReturnValueOnce( + Buffer.from(`C:\\Program Files\\...\\${win32Commands[1]}`), + ); // second command found + expect(checkHasEditorType(editor)).toBe(true); + expect(execSync).toHaveBeenCalledTimes(2); + }); + } + + it(`should return false if none of the commands exist on windows`, () => { Object.defineProperty(process, 'platform', { value: 'win32' }); (execSync as Mock).mockImplementation(() => { - throw new Error(); + throw new Error(); // all commands not found }); expect(checkHasEditorType(editor)).toBe(false); + expect(execSync).toHaveBeenCalledTimes(win32Commands.length); }); }); } @@ -110,31 +153,109 @@ describe('editor utils', () => { describe('getDiffCommand', () => { const guiEditors: Array<{ editor: EditorType; - command: string; - win32Command: string; + commands: string[]; + win32Commands: string[]; }> = [ - { editor: 'vscode', command: 'code', win32Command: 'code.cmd' }, - { editor: 'vscodium', command: 'codium', win32Command: 'codium.cmd' }, - { editor: 'windsurf', command: 'windsurf', win32Command: 'windsurf' }, - { editor: 'cursor', command: 'cursor', win32Command: 'cursor' }, - { editor: 'zed', command: 'zed', win32Command: 'zed' }, + { editor: 'vscode', commands: ['code'], win32Commands: ['code.cmd'] }, + { + editor: 'vscodium', + commands: ['codium'], + win32Commands: ['codium.cmd'], + }, + { + editor: 'windsurf', + commands: ['windsurf'], + win32Commands: ['windsurf'], + }, + { editor: 'cursor', commands: ['cursor'], win32Commands: ['cursor'] }, + { editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] }, ]; - for (const { editor, command, win32Command } of guiEditors) { - it(`should return the correct command for ${editor} on non-windows`, () => { + for (const { editor, commands, win32Commands } of guiEditors) { + // Non-windows tests + it(`should use first command "${commands[0]}" when it exists on non-windows`, () => { Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock).mockReturnValue( + Buffer.from(`/usr/bin/${commands[0]}`), + ); const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); expect(diffCommand).toEqual({ - command, + command: commands[0], args: ['--wait', '--diff', 'old.txt', 'new.txt'], }); }); - it(`should return the correct command for ${editor} on windows`, () => { - Object.defineProperty(process, 'platform', { value: 'win32' }); + if (commands.length > 1) { + it(`should use second command "${commands[1]}" when first doesn't exist on non-windows`, () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock) + .mockImplementationOnce(() => { + throw new Error(); // first command not found + }) + .mockReturnValueOnce(Buffer.from(`/usr/bin/${commands[1]}`)); // second command found + + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); + expect(diffCommand).toEqual({ + command: commands[1], + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); + }); + } + + it(`should fallback to last command "${commands[commands.length - 1]}" when none exist on non-windows`, () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock).mockImplementation(() => { + throw new Error(); // all commands not found + }); + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); expect(diffCommand).toEqual({ - command: win32Command, + command: commands[commands.length - 1], + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); + }); + + // Windows tests + it(`should use first command "${win32Commands[0]}" when it exists on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock).mockReturnValue( + Buffer.from(`C:\\Program Files\\...\\${win32Commands[0]}`), + ); + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); + expect(diffCommand).toEqual({ + command: win32Commands[0], + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); + }); + + if (win32Commands.length > 1) { + it(`should use second command "${win32Commands[1]}" when first doesn't exist on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock) + .mockImplementationOnce(() => { + throw new Error(); // first command not found + }) + .mockReturnValueOnce( + Buffer.from(`C:\\Program Files\\...\\${win32Commands[1]}`), + ); // second command found + + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); + expect(diffCommand).toEqual({ + command: win32Commands[1], + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); + }); + } + + it(`should fallback to last command "${win32Commands[win32Commands.length - 1]}" when none exist on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock).mockImplementation(() => { + throw new Error(); // all commands not found + }); + + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor); + expect(diffCommand).toEqual({ + command: win32Commands[win32Commands.length - 1], args: ['--wait', '--diff', 'old.txt', 'new.txt'], }); }); diff --git a/packages/core/src/utils/editor.ts b/packages/core/src/utils/editor.ts index 8d95a593..2d65d525 100644 --- a/packages/core/src/utils/editor.ts +++ b/packages/core/src/utils/editor.ts @@ -44,21 +44,28 @@ function commandExists(cmd: string): boolean { } } -const editorCommands: Record = { - vscode: { win32: 'code.cmd', default: 'code' }, - vscodium: { win32: 'codium.cmd', default: 'codium' }, - windsurf: { win32: 'windsurf', default: 'windsurf' }, - cursor: { win32: 'cursor', default: 'cursor' }, - vim: { win32: 'vim', default: 'vim' }, - neovim: { win32: 'nvim', default: 'nvim' }, - zed: { win32: 'zed', default: 'zed' }, +/** + * Editor command configurations for different platforms. + * Each editor can have multiple possible command names, listed in order of preference. + */ +const editorCommands: Record< + EditorType, + { win32: string[]; default: string[] } +> = { + vscode: { win32: ['code.cmd'], default: ['code'] }, + vscodium: { win32: ['codium.cmd'], default: ['codium'] }, + windsurf: { win32: ['windsurf'], default: ['windsurf'] }, + cursor: { win32: ['cursor'], default: ['cursor'] }, + vim: { win32: ['vim'], default: ['vim'] }, + neovim: { win32: ['nvim'], default: ['nvim'] }, + zed: { win32: ['zed'], default: ['zed', 'zeditor'] }, }; export function checkHasEditorType(editor: EditorType): boolean { const commandConfig = editorCommands[editor]; - const command = + const commands = process.platform === 'win32' ? commandConfig.win32 : commandConfig.default; - return commandExists(command); + return commands.some((cmd) => commandExists(cmd)); } export function allowEditorTypeInSandbox(editor: EditorType): boolean { @@ -92,8 +99,12 @@ export function getDiffCommand( return null; } const commandConfig = editorCommands[editor]; - const command = + const commands = process.platform === 'win32' ? commandConfig.win32 : commandConfig.default; + const command = + commands.slice(0, -1).find((cmd) => commandExists(cmd)) || + commands[commands.length - 1]; + switch (editor) { case 'vscode': case 'vscodium':