diff --git a/packages/lsp/src/server/pyright.ts b/packages/lsp/src/server/pyright.ts index 30183ad..da32e07 100644 --- a/packages/lsp/src/server/pyright.ts +++ b/packages/lsp/src/server/pyright.ts @@ -4,8 +4,11 @@ import type { LSPServerInfo } from './types' import { spawn } from 'node:child_process' +import process from 'node:process' import { attachLSPProcessHandlers, log, nearestRoot } from './utils' +export const PYRIGHT_VERSION = '1.1.414' + export const PyrightServer: LSPServerInfo = { id: 'pyright', extensions: ['.py', '.pyi'], @@ -16,20 +19,21 @@ export const PyrightServer: LSPServerInfo = { 'pyrightconfig.json', ]), async spawn(root) { - const pyright = Bun.which('pyright-langserver') + // Read PATH now rather than the copy Bun captured at startup + const pyright = Bun.which('pyright-langserver', { PATH: process.env.PATH ?? '' }) if (!pyright) { - // Try via bunx/npx - try { - const proc = spawn('bunx', ['pyright-langserver', '--stdio'], { - cwd: root, - }) - attachLSPProcessHandlers(proc, 'pyright') - return { process: proc } - } - catch (err) { - log.error({ err }, 'Failed to spawn pyright-langserver via bunx') + // pyright-langserver is a bin of the pyright package, not a package itself. + // spawn reports a missing bunx only as an async 'error' event, so check for it first. + const bunx = Bun.which('bunx', { PATH: process.env.PATH ?? '' }) + if (!bunx) { + log.warn({ root }, 'pyright not found. Install with: npm install pyright') return undefined } + const proc = spawn(bunx, ['-p', `pyright@${PYRIGHT_VERSION}`, 'pyright-langserver', '--stdio'], { + cwd: root, + }) + attachLSPProcessHandlers(proc, 'pyright') + return { process: proc } } const proc = spawn(pyright, ['--stdio'], { diff --git a/packages/lsp/test/unit/server.test.ts b/packages/lsp/test/unit/server.test.ts index e4c95fc..2d1f830 100644 --- a/packages/lsp/test/unit/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -595,6 +595,36 @@ describe.skipIf(process.platform === 'win32')('BiomeServer fallback', () => { }) }) +describe.skipIf(process.platform === 'win32')('PyrightServer fallback', () => { + test('falls back to bunx with a pinned pyright when pyright-langserver is not installed', async () => { + const invoked = await spawnServerWith(PyrightServer, { bunx: recorder('bunx') }) + expect(invoked).toBe('bunx -p pyright@1.1.414 pyright-langserver --stdio') + }) + + test('prefers pyright-langserver on PATH over bunx', async () => { + const invoked = await spawnServerWith(PyrightServer, { 'pyright-langserver': recorder('path'), 'bunx': recorder('bunx') }) + expect(invoked).toBe('path --stdio') + }) + + test('returns undefined with the install hint when bunx is missing', async () => { + const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lsp-fallback-empty-')) + const savedPath = process.env.PATH + const warn = spyOn(log, 'warn') + try { + process.env.PATH = emptyDir + const handle = await PyrightServer.spawn(emptyDir) + process.env.PATH = savedPath + expect(handle).toBeUndefined() + expect(warn).toHaveBeenCalledWith(expect.anything(), 'pyright not found. Install with: npm install pyright') + } + finally { + warn.mockRestore() + process.env.PATH = savedPath + await fs.rm(emptyDir, { recursive: true, force: true }) + } + }) +}) + describe('VueServer', () => { test('has correct id', () => { expect(VueServer.id).toBe('vue')