mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
fix(editor): open WSL workspaces in VS Code remote
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { resolveCliCommandMock } = vi.hoisted(() => ({
|
||||
resolveCliCommandMock: vi.fn((command: string) => command)
|
||||
}))
|
||||
|
||||
vi.mock('./codex-cli/command', () => ({
|
||||
resolveCliCommand: resolveCliCommandMock
|
||||
}))
|
||||
|
||||
import { getCmdExePath } from './win32-utils'
|
||||
import { resolveExternalEditorLaunchSpec } from './external-editor-launch'
|
||||
|
||||
describe('resolveExternalEditorLaunchSpec', () => {
|
||||
beforeEach(() => {
|
||||
resolveCliCommandMock.mockReset()
|
||||
resolveCliCommandMock.mockImplementation((command: string) => command)
|
||||
})
|
||||
|
||||
it('keeps simple CLI commands on the executable launch path', () => {
|
||||
const spec = resolveExternalEditorLaunchSpec('cursor', '/tmp/workspace', {
|
||||
platform: 'darwin'
|
||||
@@ -121,4 +135,113 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
spawnArgs: ['/d', '/s', '/c', 'nvim --clean C:\\workspaces\\orca']
|
||||
})
|
||||
})
|
||||
|
||||
it('opens modern WSL UNC workspaces in the matching VS Code remote', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code', '\\\\wsl.localhost\\Ubuntu\\home\\aliuq\\project', {
|
||||
platform: 'win32'
|
||||
}).spawnArgs
|
||||
).toEqual(['--remote', 'wsl+Ubuntu', '/home/aliuq/project'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'legacy WSL UNC workspace',
|
||||
'\\\\wsl$\\Debian\\home\\ada\\project',
|
||||
'wsl+Debian',
|
||||
'/home/ada/project'
|
||||
],
|
||||
['modern WSL distro root', '\\\\wsl.localhost\\Ubuntu', 'wsl+Ubuntu', '/'],
|
||||
['legacy WSL distro root', '\\\\wsl$\\Debian', 'wsl+Debian', '/']
|
||||
])('opens a %s in the matching VS Code remote', (_label, pathValue, authority, linuxPath) => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code', pathValue, { platform: 'win32' }).spawnArgs
|
||||
).toEqual(['--remote', authority, linuxPath])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'C:\\Program Files\\Microsoft VS Code\\Code.exe',
|
||||
'C:\\Tools\\CODE.CMD',
|
||||
'C:\\Tools\\code.bat'
|
||||
])('recognizes the direct Windows VS Code launcher %s', (editorCommand) => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec(
|
||||
editorCommand,
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\ada\\project',
|
||||
{ platform: 'win32' }
|
||||
).spawnArgs
|
||||
).toEqual(['--remote', 'wsl+Ubuntu', '/home/ada/project'])
|
||||
})
|
||||
|
||||
it.each(['C:\\Tools\\CODE.CMD', 'C:\\Tools\\code.bat'])(
|
||||
'recognizes the resolved Windows VS Code launcher %s',
|
||||
(resolvedCommand) => {
|
||||
resolveCliCommandMock.mockReturnValueOnce(resolvedCommand)
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code', '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project', {
|
||||
platform: 'win32'
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: resolvedCommand,
|
||||
spawnArgs: ['--remote', 'wsl+Ubuntu', '/home/ada/project']
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves spaces in WSL distro and folder arguments', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec(
|
||||
'code',
|
||||
'\\\\wsl.localhost\\Ubuntu Preview\\home\\Ada Lovelace\\project',
|
||||
{ platform: 'win32' }
|
||||
).spawnArgs
|
||||
).toEqual(['--remote', 'wsl+Ubuntu Preview', '/home/Ada Lovelace/project'])
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux'] as const)('keeps WSL-looking paths local on %s', (platform) => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
expect(resolveExternalEditorLaunchSpec('code', pathValue, { platform }).spawnArgs).toEqual([
|
||||
pathValue
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['C:\\workspaces\\orca', '\\\\server\\share\\project'])(
|
||||
'keeps the non-WSL Windows path %s local',
|
||||
(pathValue) => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code', pathValue, { platform: 'win32' }).spawnArgs
|
||||
).toEqual([pathValue])
|
||||
}
|
||||
)
|
||||
|
||||
it('does not add VS Code remote arguments to other editors', () => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('C:\\Tools\\cursor.exe', pathValue, {
|
||||
platform: 'win32'
|
||||
}).spawnArgs
|
||||
).toEqual(['--new-window', pathValue])
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('C:\\Tools\\codium.exe', pathValue, {
|
||||
platform: 'win32'
|
||||
}).spawnArgs
|
||||
).toEqual([pathValue])
|
||||
})
|
||||
|
||||
it('does not rewrite compound VS Code commands', () => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code --reuse-window', pathValue, { platform: 'win32' })
|
||||
).toEqual({
|
||||
kind: 'shell',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: getCmdExePath(),
|
||||
spawnArgs: ['/d', '/s', '/c', `code --reuse-window ${pathValue}`]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { basename, posix, win32 } from 'node:path'
|
||||
import { parseWslUncPath } from '../shared/wsl-paths'
|
||||
import { resolveCliCommand } from './codex-cli/command'
|
||||
import { getCmdExePath } from './win32-utils'
|
||||
|
||||
@@ -106,12 +107,23 @@ function shouldShowWindowsConsole(
|
||||
return platform === 'win32' && WINDOWS_CONSOLE_EDITORS.has(getLauncherBaseName(command, options))
|
||||
}
|
||||
|
||||
function buildExecutableArgs(editorCommand: string, pathValue: string): string[] {
|
||||
function buildExecutableArgs(
|
||||
editorCommand: string,
|
||||
pathValue: string,
|
||||
platform: NodeJS.Platform
|
||||
): string[] {
|
||||
if (getLauncherBaseName(editorCommand) === 'cursor') {
|
||||
// Why: Cursor can route bare folder launches through the last active
|
||||
// workbench. A new window keeps "Open in Cursor" scoped to this worktree.
|
||||
return ['--new-window', pathValue]
|
||||
}
|
||||
if (platform === 'win32' && getLauncherBaseName(editorCommand) === 'code') {
|
||||
const wslPath = parseWslUncPath(pathValue)
|
||||
if (wslPath) {
|
||||
// Why: VS Code otherwise treats a WSL UNC path as a local Windows folder.
|
||||
return ['--remote', `wsl+${wslPath.distro}`, wslPath.linuxPath]
|
||||
}
|
||||
}
|
||||
return [pathValue]
|
||||
}
|
||||
|
||||
@@ -156,7 +168,7 @@ export function resolveExternalEditorLaunchSpec(
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: !shouldShowWindowsConsole(editorCommand, platform),
|
||||
spawnCmd: editorCommand,
|
||||
spawnArgs: buildExecutableArgs(editorCommand, pathValue)
|
||||
spawnArgs: buildExecutableArgs(editorCommand, pathValue, platform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +181,6 @@ export function resolveExternalEditorLaunchSpec(
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: !shouldShowWindowsConsole(editorCommand, platform),
|
||||
spawnCmd: editorCommand,
|
||||
spawnArgs: buildExecutableArgs(editorCommand, pathValue)
|
||||
spawnArgs: buildExecutableArgs(editorCommand, pathValue, platform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,28 @@ describe('registerShellHandlers', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('forwards WSL remote arguments with spaces through the Windows launcher shim', async () => {
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
const workspacePath = '\\\\wsl.localhost\\Ubuntu Preview\\home\\Ada Lovelace\\project'
|
||||
const codeShim = 'C:\\Tools\\CODE.CMD'
|
||||
resolveCliCommandMock.mockReturnValueOnce(codeShim)
|
||||
const handler = getHandler('shell:openInExternalEditor')
|
||||
|
||||
try {
|
||||
await expect(handler({}, workspacePath, 'code')).resolves.toEqual({ ok: true })
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(codeShim, [
|
||||
'--remote',
|
||||
'wsl+Ubuntu Preview',
|
||||
'/home/Ada Lovelace/project'
|
||||
])
|
||||
} finally {
|
||||
if (platformDescriptor) {
|
||||
Object.defineProperty(process, 'platform', platformDescriptor)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the Windows console for NeoVim executable launchers on Windows', async () => {
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getCmdExePath,
|
||||
getSpawnArgsForWindows,
|
||||
isPermissionError,
|
||||
isWindowsBatchScript,
|
||||
@@ -65,6 +66,26 @@ describe('getSpawnArgsForWindows', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves VS Code WSL remote arguments with spaces through .cmd launchers', () => {
|
||||
withPlatform('win32', () => {
|
||||
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows('C:\\tools\\code.cmd', [
|
||||
'--remote',
|
||||
'wsl+Ubuntu Preview',
|
||||
'/home/Ada Lovelace/project'
|
||||
])
|
||||
|
||||
expect(spawnCmd).toBe(getCmdExePath())
|
||||
expect(spawnArgs).toEqual([
|
||||
'/d',
|
||||
'/c',
|
||||
'C:\\tools\\code.cmd',
|
||||
'--remote',
|
||||
'wsl+Ubuntu Preview',
|
||||
'/home/Ada Lovelace/project'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('passes .exe through unchanged on win32', () => {
|
||||
withPlatform('win32', () => {
|
||||
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows('C:\\tools\\codex.exe', ['login'])
|
||||
|
||||
Reference in New Issue
Block a user