fix(lsp): preserve execution-world URI semantics
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import type { LspLocation } from '@deepseek-ai/dsh-lsp'
|
||||
|
||||
const WS = resolve('/home/u/proj')
|
||||
const WS_URI = pathToFileURL(WS).href
|
||||
|
||||
function loc(uri: string, line: number, character = 0): LspLocation {
|
||||
return { uri, range: { start: { line, character }, end: { line, character: character + 1 } } }
|
||||
@@ -48,64 +49,84 @@ describe('parseLspArgs', () => {
|
||||
describe('renderUri', () => {
|
||||
it('relativizes a file: URI inside the workspace with forward slashes', () => {
|
||||
const uri = pathToFileURL(join(WS, 'src', 'a.ts')).href
|
||||
expect(renderUri(uri, WS)).toBe('src/a.ts')
|
||||
expect(renderUri(uri, WS_URI)).toBe('src/a.ts')
|
||||
})
|
||||
|
||||
it('returns an absolute path for a file: URI outside the workspace', () => {
|
||||
const outside = resolve(WS, '..', 'other', 'lib', 'b.ts')
|
||||
const uri = pathToFileURL(outside).href
|
||||
expect(renderUri(uri, WS)).toBe(outside)
|
||||
expect(renderUri(uri, WS_URI)).toBe(outside)
|
||||
})
|
||||
|
||||
it('renders the workspace root itself as "."', () => {
|
||||
expect(renderUri(pathToFileURL(WS).href, WS)).toBe('.')
|
||||
expect(renderUri(WS_URI, WS_URI)).toBe('.')
|
||||
})
|
||||
|
||||
it('keeps an in-workspace path whose first segment starts with dots relative', () => {
|
||||
// `..generated` is a real in-workspace dir, not a parent escape; only a `..` segment is external.
|
||||
const uri = pathToFileURL(join(WS, '..generated', 'a.ts')).href
|
||||
expect(renderUri(uri, WS)).toBe('..generated/a.ts')
|
||||
expect(renderUri(uri, WS_URI)).toBe('..generated/a.ts')
|
||||
})
|
||||
|
||||
it('relativizes Windows execution-world URIs on a non-Windows host', () => {
|
||||
expect(renderUri('file:///C:/WORKSPACE/src/a.ts', 'file:///c:/workspace')).toBe('src/a.ts')
|
||||
expect(renderUri('file:///D:/lib/b.ts', 'file:///C:/workspace')).toBe('D:/lib/b.ts')
|
||||
})
|
||||
|
||||
it('renders remote file authorities without host path conversion', () => {
|
||||
expect(renderUri('file://server/share/workspace/a.ts', 'file://server/share/workspace')).toBe('a.ts')
|
||||
expect(renderUri('file://other/share/b.ts', 'file://server/share/workspace')).toBe('//other/share/b.ts')
|
||||
expect(renderUri('file:///a.ts', 'file://server/')).toBe('/a.ts')
|
||||
})
|
||||
|
||||
it('keeps malformed or mismatched URI coordinates verbatim', () => {
|
||||
expect(renderUri('file://[', WS_URI)).toBe('file://[')
|
||||
expect(renderUri('file:///a.ts', 'https://example.com/workspace')).toBe('file:///a.ts')
|
||||
expect(renderUri('file:///a.ts', 'file:///bad%ZZ')).toBe('file:///a.ts')
|
||||
expect(renderUri('file:///bad%5Cpath', WS_URI)).toBe('file:///bad%5Cpath')
|
||||
expect(renderUri('file:///short', 'file:///short/deeper')).toBe('/short')
|
||||
expect(renderUri('file:///', 'file:///C:/workspace')).toBe('/')
|
||||
})
|
||||
|
||||
it('keeps a non-file URI verbatim', () => {
|
||||
expect(renderUri('untitled:Untitled-1', WS)).toBe('untitled:Untitled-1')
|
||||
expect(renderUri('jdt://contents/Foo.class', WS)).toBe('jdt://contents/Foo.class')
|
||||
expect(renderUri('untitled:Untitled-1', WS_URI)).toBe('untitled:Untitled-1')
|
||||
expect(renderUri('jdt://contents/Foo.class', WS_URI)).toBe('jdt://contents/Foo.class')
|
||||
})
|
||||
|
||||
it('keeps a malformed file: URI verbatim when it cannot be parsed to a path', () => {
|
||||
// An encoded path separator is invalid on every platform and must remain verbatim.
|
||||
expect(renderUri('file:///bad%2Fpath', WS)).toBe('file:///bad%2Fpath')
|
||||
expect(renderUri('file:///bad%2Fpath', WS_URI)).toBe('file:///bad%2Fpath')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatLocations', () => {
|
||||
it('renders a no-result line for an empty list', () => {
|
||||
expect(formatLocations([], WS, DEFAULT_MAX_LOCATIONS, DEFAULT_MAX_RESULT_CHARS)).toBe('No results.')
|
||||
expect(formatLocations([], WS_URI, DEFAULT_MAX_LOCATIONS, DEFAULT_MAX_RESULT_CHARS)).toBe('No results.')
|
||||
})
|
||||
|
||||
it('renders one-based path:line:character grouped by file', () => {
|
||||
const a = pathToFileURL(join(WS, 'a.ts')).href
|
||||
const text = formatLocations([loc(a, 0, 0), loc(a, 4, 2)], WS, DEFAULT_MAX_LOCATIONS, DEFAULT_MAX_RESULT_CHARS)
|
||||
const text = formatLocations([loc(a, 0, 0), loc(a, 4, 2)], WS_URI, DEFAULT_MAX_LOCATIONS, DEFAULT_MAX_RESULT_CHARS)
|
||||
expect(text).toBe('a.ts:1:1\na.ts:5:3')
|
||||
})
|
||||
|
||||
it('caps at maxLocations and marks the omission', () => {
|
||||
const a = pathToFileURL(join(WS, 'a.ts')).href
|
||||
const many = Array.from({ length: 5 }, (_, i) => loc(a, i))
|
||||
const text = formatLocations(many, WS, 2, DEFAULT_MAX_RESULT_CHARS)
|
||||
const text = formatLocations(many, WS_URI, 2, DEFAULT_MAX_RESULT_CHARS)
|
||||
expect(text).toContain('a.ts:1:1')
|
||||
expect(text).toContain('3 more locations omitted (limit 2).')
|
||||
})
|
||||
|
||||
it('uses the singular omission marker for exactly one extra', () => {
|
||||
const a = pathToFileURL(join(WS, 'a.ts')).href
|
||||
const text = formatLocations([loc(a, 0), loc(a, 1)], WS, 1, DEFAULT_MAX_RESULT_CHARS)
|
||||
const text = formatLocations([loc(a, 0), loc(a, 1)], WS_URI, 1, DEFAULT_MAX_RESULT_CHARS)
|
||||
expect(text).toContain('1 more location omitted (limit 1).')
|
||||
})
|
||||
|
||||
it('caps the complete location text even when one URI is enormous', () => {
|
||||
const maxResultChars = 80
|
||||
const text = formatLocations([loc(`custom:${'x'.repeat(1_000_000)}`, 0)], WS, 1, maxResultChars)
|
||||
const text = formatLocations([loc(`custom:${'x'.repeat(1_000_000)}`, 0)], WS_URI, 1, maxResultChars)
|
||||
expect(text).toHaveLength(maxResultChars)
|
||||
expect(text).toContain('locations truncated')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user