diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index d7359d7a65..55bc5600ee 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -254,34 +254,6 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, - "vue.server.hybridMode": { - "type": [ - "boolean", - "string" - ], - "default": "auto", - "enum": [ - "auto", - "typeScriptPluginOnly", - true, - false - ], - "enumDescriptions": [ - "Automatically detect and enable TypeScript Plugin/Hybrid Mode in a safe environment.", - "Only enable Vue TypeScript Plugin but disable Hybrid Mode.", - "Enable TypeScript Plugin/Hybrid Mode.", - "Disable TypeScript Plugin/Hybrid Mode." - ], - "description": "Vue language server only handles CSS and HTML language support, and tsserver takes over TS language support via TS plugin." - }, - "vue.server.compatibleExtensions": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "Set compatible extensions to skip automatic detection of Hybrid Mode." - }, "vue.server.includeLanguages": { "type": "array", "items": { @@ -291,14 +263,6 @@ "vue" ] }, - "vue.server.maxOldSpaceSize": { - "type": [ - "number", - "null" - ], - "default": null, - "description": "Set --max-old-space-size option on server process. If you have problem on frequently \"Request textDocument/** failed.\" error, try setting higher memory(MB) on it." - }, "vue.doctor.status": { "type": "boolean", "default": true, @@ -330,11 +294,6 @@ "customBlocks" ] }, - "vue.updateImportsOnFileMove.enabled": { - "type": "boolean", - "default": true, - "description": "Enabled update imports on file move." - }, "vue.codeActions.enabled": { "type": "boolean", "default": true, @@ -472,47 +431,37 @@ "title": "Split - `)).items.map(item => item.label) - ).toMatchInlineSnapshot(` + let vFoo!: FunctionDirective; + + `)).items.map(item => item.label) + ).toMatchInlineSnapshot(` [ "attr", "prop" ] `); - }); - - it('$event argument', async () => { - await requestCompletionItem('fixture.vue', 'vue', ``, 'event'); - }); +}); - it(' - `, 'foo'); - }); +test(' + `, 'foo'); +}); - - `, 'default'); - }); + }; + + `, 'default'); +}); - it('#2454', async () => { - await requestCompletionItem('fixture.vue', 'vue', ` - +test.skip('#2454', async () => { + await requestCompletionItemToVueServer('fixture.vue', 'vue', ` + - - `, 'v-loading'); - }); + + `, 'v-loading'); +}); - it('#2511', async () => { - await prepareDocument('tsconfigProject/component-for-auto-import.vue', 'vue', ``); - expect( - (await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - - `, 'ComponentForAutoImport')).textEdit - ).toMatchInlineSnapshot(` +test.skip('#2511', async () => { + await prepareDocument('tsconfigProject/component-for-auto-import.vue', 'vue', ``); + expect( + (await requestCompletionItemToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + `, 'ComponentForAutoImport')) + ).toMatchInlineSnapshot(` { "newText": "import componentForAutoImport$1 from './component-for-auto-import.vue';", "range": { @@ -289,56 +282,52 @@ describe('Completions', async () => { }, } `); - }); - - it('#3658', async () => { - await requestCompletionItem('fixture.vue', 'vue', ` - - `, 'foo'); - }); +}); - it('#4639', async () => { - await requestCompletionItem('fixture.vue', 'vue', ` - - `, 'capture'); - }); +test('#3658', async () => { + await requestCompletionItemToTsServer('fixture.vue', 'vue', ` + + `, 'foo'); +}); - it('Alias path', async () => { - await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - - `, 'empty.vue'); - }); +test('#4639', async () => { + await requestCompletionItemToVueServer('fixture.vue', 'vue', ` + + `, 'capture'); +}); - it('Relative path', async () => { - await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - - `, 'empty.vue'); - }); +test('Alias path', async () => { + await requestCompletionItemToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + `, 'empty.vue'); +}); - it('Component auto import', async () => { - await prepareDocument('tsconfigProject/ComponentForAutoImport.vue', 'vue', ``); - expect( - (await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - +test('Relative path', async () => { + await requestCompletionItemToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + `, 'empty.vue'); +}); - - `, 'ComponentForAutoImport')) - ).toMatchInlineSnapshot(` +test.skip('Component auto import', async () => { + expect( + (await requestCompletionItemToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + `, 'Empty')) + ).toMatchInlineSnapshot(` { "additionalTextEdits": [ { @@ -391,153 +380,180 @@ describe('Completions', async () => { }, } `); - }); +}); - it('core#8811', async () => { - await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - + }; + + + `, ':-foo-bar'); +}); + +test('#4796', async () => { + expect( + (await requestCompletionItemToVueServer('tsconfigProject/fixture.vue', 'vue', ` - `, ':-foo-bar'); - }); - it('#4796', async () => { - expect( - (await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - + + `, ':msg')) + ).toMatchInlineSnapshot(` + { + "documentation": { + "kind": "markdown", + "value": "The message to display", + }, + "insertTextFormat": 1, + "kind": 5, + "label": ":msg", + "sortText": ":msg", + "textEdit": { + "newText": ":msg="$1"", + "range": { + "end": { + "character": 20, + "line": 2, + }, + "start": { + "character": 16, + "line": 2, + }, + }, + }, + } + `); +}); - - `, ':msg')) - ).toMatchInlineSnapshot(` - { - "documentation": { - "kind": "markdown", - "value": "The message to display", - }, - "insertTextFormat": 1, - "kind": 5, - "label": ":msg", - "sortText": ":msg", - "textEdit": { - "newText": ":msg="$1"", - "range": { - "end": { - "character": 21, - "line": 2, - }, - "start": { - "character": 17, - "line": 2, - }, - }, - }, - } - `); - }); +test('Auto insert defines', async () => { + expect( + (await requestCompletionItemToVueServer('tsconfigProject/fixture.vue', 'vue', ` + + `, 'props')) + ).toMatchInlineSnapshot(` + { + "additionalTextEdits": [ + { + "newText": "const props = ", + "range": { + "end": { + "character": 3, + "line": 2, + }, + "start": { + "character": 3, + "line": 2, + }, + }, + }, + ], + "commitCharacters": [ + ".", + ",", + ";", + ], + "kind": 6, + "label": "props", + } + `); +}); - it('Auto insert defines', async () => { - expect( - (await requestCompletionItem('tsconfigProject/fixture.vue', 'vue', ` - - `, 'props')) - ).toMatchInlineSnapshot(` - { - "additionalTextEdits": [ - { - "newText": "const props = ", - "range": { - "end": { - "character": 4, - "line": 2, - }, - "start": { - "character": 4, - "line": 2, - }, - }, - }, - ], - "commitCharacters": [ - ".", - ",", - ";", - ], - "kind": 6, - "label": "props", - } - `); - }); +const openedDocuments: TextDocument[] = []; - const openedDocuments: TextDocument[] = []; +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); + } + openedDocuments.length = 0; +}); - afterEach(async () => { +async function requestCompletionItemToVueServer(fileName: string, languageId: string, content: string, itemLabel: string) { + const completions = await requestCompletionListToVueServer(fileName, languageId, content); + let completion = completions.items.find(item => item.label === itemLabel); + expect(completion).toBeDefined(); + if (completion!.data) { const server = await getLanguageServer(); - for (const document of openedDocuments) { - await server.closeTextDocument(document.uri); - } - openedDocuments.length = 0; - }); - - async function requestCompletionItem(fileName: string, languageId: string, content: string, itemLabel: string) { - const completions = await requestCompletionList(fileName, languageId, content); - let completion = completions.items.find(item => item.label === itemLabel); + completion = await server.vueserver.sendCompletionResolveRequest(completion!); expect(completion).toBeDefined(); - if (completion!.data) { - const server = await getLanguageServer(); - completion = await server.sendCompletionResolveRequest(completion!); - expect(completion).toBeDefined(); - } - return completion!; } + return completion!; +} + +async function requestCompletionListToVueServer(fileName: string, languageId: string, content: string) { + const offset = content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + content = content.slice(0, offset) + content.slice(offset + 1); + + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const position = document.positionAt(offset); + const completions = await server.vueserver.sendCompletionRequest(document.uri, position); + expect(completions).toBeDefined(); + + return completions!; +} + +async function requestCompletionItemToTsServer(fileName: string, languageId: string, content: string, itemLabel: string) { + const completions = await requestCompletionListToTsServer(fileName, languageId, content); + let completion = completions.find((item: any) => item.name === itemLabel); + expect(completion).toBeDefined(); + return completion!; +} + +async function requestCompletionListToTsServer(fileName: string, languageId: string, content: string) { + const offset = content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + content = content.slice(0, offset) + content.slice(offset + 1); + + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const res = await server.tsserver.message({ + seq: server.nextSeq(), + command: 'completions', + arguments: { + file: URI.parse(document.uri).fsPath, + position: offset, + }, + }); + expect(res.success).toBe(true); - async function requestCompletionList(fileName: string, languageId: string, content: string) { - const offset = content.indexOf('|'); - expect(offset).toBeGreaterThanOrEqual(0); - content = content.slice(0, offset) + content.slice(offset + 1); - - const server = await getLanguageServer(); - let document = await prepareDocument(fileName, languageId, content); - - const position = document.positionAt(offset); - const completions = await server.sendCompletionRequest(document.uri, position); - expect(completions).toBeDefined(); - - return completions!; - } + return res.body; +} - async function prepareDocument(fileName: string, languageId: string, content: string) { - const server = await getLanguageServer(); - const uri = URI.file(`${testWorkspacePath}/${fileName}`); - const document = await server.openInMemoryDocument(uri.toString(), languageId, content); - if (openedDocuments.every(d => d.uri !== document.uri)) { - openedDocuments.push(document); - } - return document; +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); } -}); + return document; +} diff --git a/packages/language-server/tests/definitions.spec.ts b/packages/language-server/tests/definitions.spec.ts index 39b57b36e8..1b6d834c65 100644 --- a/packages/language-server/tests/definitions.spec.ts +++ b/packages/language-server/tests/definitions.spec.ts @@ -1,149 +1,152 @@ -import { Location, TextDocument } from '@volar/language-server'; -import { afterEach, describe, expect, it } from 'vitest'; +import { TextDocument } from '@volar/language-server'; +import { afterEach, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; import { getLanguageServer, testWorkspacePath } from './server.js'; -describe('Definitions', async () => { - - it('TS to vue', async () => { - expect( - await requestDefinition('tsconfigProject/fixture1.ts', 'typescript', `import C|omponent from './empty.vue';`) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 0, - "line": 0, - }, - "start": { - "character": 0, - "line": 0, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/empty.vue", - }, - ] - `); - expect( - await requestDefinition('tsconfigProject/fixture2.ts', 'typescript', `import Component from '|./empty.vue';`) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 0, - "line": 0, - }, - "start": { - "character": 0, - "line": 0, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/empty.vue", - }, - ] - `); - }); +test('TS to vue', async () => { + expect( + await requestDefinition('tsconfigProject/fixture1.ts', 'typescript', `import C|omponent from './empty.vue';`) + ).toMatchInlineSnapshot(` + [ + { + "end": { + "line": 1, + "offset": 1, + }, + "file": "\${testWorkspacePath}/tsconfigProject/empty.vue", + "start": { + "line": 1, + "offset": 1, + }, + }, + ] + `); + expect( + await requestDefinition('tsconfigProject/fixture2.ts', 'typescript', `import Component from '|./empty.vue';`) + ).toMatchInlineSnapshot(` + [ + { + "end": { + "line": 1, + "offset": 1, + }, + "file": "\${testWorkspacePath}/tsconfigProject/empty.vue", + "start": { + "line": 1, + "offset": 1, + }, + }, + ] + `); +}); - it('Alias path', async () => { - await prepareDocument('tsconfigProject/foo.ts', 'typescript', `export const foo = 'foo';`); - expect( - await requestDefinition('tsconfigProject/fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 25, - "line": 0, - }, - "start": { - "character": 0, - "line": 0, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/foo.ts", - }, - ] - `); - }); +test('Alias path', async () => { + await prepareDocument('tsconfigProject/foo.ts', 'typescript', `export const foo = 'foo';`); + expect( + await requestDefinition('tsconfigProject/fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + [ + { + "contextEnd": { + "line": 1, + "offset": 26, + }, + "contextStart": { + "line": 1, + "offset": 1, + }, + "end": { + "line": 1, + "offset": 17, + }, + "file": "\${testWorkspacePath}/tsconfigProject/foo.ts", + "start": { + "line": 1, + "offset": 14, + }, + }, + ] + `); +}); - it('#2600', async () => { - await prepareDocument('tsconfigProject/foo.vue', 'vue', ` - +test('#2600', async () => { + await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + - + `); + expect( + await requestDefinition('tsconfigProject/fixture.vue', 'vue', ` + - `); - expect( - await requestDefinition('tsconfigProject/fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 0, - "line": 0, - }, - "start": { - "character": 0, - "line": 0, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/foo.vue", - }, - ] - `); - }); - - const openedDocuments: TextDocument[] = []; + `) + ).toMatchInlineSnapshot(` + [ + { + "end": { + "line": 1, + "offset": 1, + }, + "file": "\${testWorkspacePath}/tsconfigProject/foo.vue", + "start": { + "line": 1, + "offset": 1, + }, + }, + ] + `); +}); - afterEach(async () => { - const server = await getLanguageServer(); - for (const document of openedDocuments) { - await server.closeTextDocument(document.uri); - } - openedDocuments.length = 0; - }); +const openedDocuments: TextDocument[] = []; - async function requestDefinition(fileName: string, languageId: string, content: string) { - const offset = content.indexOf('|'); - expect(offset).toBeGreaterThanOrEqual(0); - content = content.slice(0, offset) + content.slice(offset + 1); +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); + } + openedDocuments.length = 0; +}); - const server = await getLanguageServer(); - let document = await prepareDocument(fileName, languageId, content); +async function requestDefinition(fileName: string, languageId: string, content: string) { + const offset = content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + content = content.slice(0, offset) + content.slice(offset + 1); - const position = document.positionAt(offset); - const definition = await server.sendDefinitionRequest(document.uri, position) as Location[] | null; - expect(definition).toBeDefined(); + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); - for (const loc of definition!) { - loc.uri = 'file://${testWorkspacePath}' + loc.uri.slice(URI.file(testWorkspacePath).toString().length); - } + const res = await server.tsserver.message({ + seq: server.nextSeq(), + command: 'definition', + arguments: { + file: URI.parse(document.uri).fsPath, + position: offset, + }, + }); + expect(res.success).toBe(true); - return definition!; + for (const ref of res.body) { + ref.file = '${testWorkspacePath}' + ref.file.slice(testWorkspacePath.length); } - async function prepareDocument(fileName: string, languageId: string, content: string) { - const server = await getLanguageServer(); - const uri = URI.file(`${testWorkspacePath}/${fileName}`); - const document = await server.openInMemoryDocument(uri.toString(), languageId, content); - if (openedDocuments.every(d => d.uri !== document.uri)) { - openedDocuments.push(document); - } - return document; + return res.body; +} + +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); } -}); + return document; +} diff --git a/packages/language-server/tests/inlayHints.spec.ts b/packages/language-server/tests/inlayHints.spec.ts index 718b374fdd..2d878b223a 100644 --- a/packages/language-server/tests/inlayHints.spec.ts +++ b/packages/language-server/tests/inlayHints.spec.ts @@ -1,247 +1,245 @@ import { TextDocument } from '@volar/language-server'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; import { getLanguageServer, testWorkspacePath } from './server.js'; -describe('Definitions', async () => { - - it('Inline handler leading', async () => { - expect( - await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` - - - - `) - ).toMatchInlineSnapshot(` - " - - - - " - `); - }); - - it('Missing props', async () => { - prepareDocument('tsconfigProject/foo.vue', 'vue', ` +test('Inline handler leading', async () => { + expect( + await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` - `); - expect( - await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` - - - - `) - ).toMatchInlineSnapshot(` - " - - - - " - `); - }); - - it('Options wrapper', async () => { - expect( - await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - " - - " - `); - }); - - it('Destructured props', async () => { - expect( - await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` - + + + " + `); +}); - interface foo extends (typeof foo) { +test('Missing props', async () => { + expect( + await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` + - function func(foo) { } - - class cls { - foo: string = foo; - constructor(foo) { } - } - - for (const char of foo) { } - - try { } catch (foo) { } - - watch(() => foo, (foo) => { - console.log(foo, bar, props.baz); - }); - - `) - ).toMatchInlineSnapshot(` - " - - " - `); - }); - - it('#4720', async () => { - expect( - await requestInlayHintsResult('fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - " - - " - `); - }); - - it('#4855', async () => { - expect( - await requestInlayHintsResult('fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - " - - " - `); - }); - - const openedDocuments: TextDocument[] = []; - - afterEach(async () => { - const server = await getLanguageServer(); - for (const document of openedDocuments) { - await server.closeTextDocument(document.uri); - } - openedDocuments.length = 0; - }); - - async function requestInlayHintsResult(fileName: string, languageId: string, content: string) { - const server = await getLanguageServer(); - let document = await prepareDocument(fileName, languageId, content); - - const inlayHints = await server.sendInlayHintRequest(document.uri, { start: document.positionAt(0), end: document.positionAt(content.length) }); - expect(inlayHints).toBeDefined(); - expect(inlayHints!.length).greaterThan(0); - - let text = document.getText(); - for (const hint of inlayHints!.sort((a, b) => document.offsetAt(b.position) - document.offsetAt(a.position))) { - const offset = document.offsetAt(hint.position); - text = text.slice(0, offset) + '/* ' + hint.label + ' */' + text.slice(offset); - } - - return text; - } + + `) + ).toMatchInlineSnapshot(` + " + + + + " + `); +}); + +test('Options wrapper', async () => { + expect( + await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + " + + " + `); +}); + +test('Destructured props', async () => { + expect( + await requestInlayHintsResult('tsconfigProject/fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + " + + " + `); +}); + +test('#4720', async () => { + expect( + await requestInlayHintsResult('fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + " + + " + `); +}); + +test('#4855', async () => { + expect( + await requestInlayHintsResult('fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + " + + " + `); +}); + +const openedDocuments: TextDocument[] = []; + +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); } + openedDocuments.length = 0; }); + +async function requestInlayHintsResult(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const inlayHints = await server.vueserver.sendInlayHintRequest(document.uri, { start: document.positionAt(0), end: document.positionAt(content.length) }); + expect(inlayHints).toBeDefined(); + expect(inlayHints!.length).greaterThan(0); + + let text = document.getText(); + for (const hint of inlayHints!.sort((a, b) => document.offsetAt(b.position) - document.offsetAt(a.position))) { + const offset = document.offsetAt(hint.position); + text = text.slice(0, offset) + '/* ' + hint.label + ' */' + text.slice(offset); + } + + return text; +} + +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); + } + return document; +} diff --git a/packages/language-server/tests/references.spec.ts b/packages/language-server/tests/references.spec.ts index f32cfdd005..946a5a7ac1 100644 --- a/packages/language-server/tests/references.spec.ts +++ b/packages/language-server/tests/references.spec.ts @@ -1,187 +1,241 @@ import { TextDocument } from '@volar/language-server'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; import { getLanguageServer, testWorkspacePath } from './server.js'; -describe('Definitions', async () => { - - it('Default slot', async () => { - await prepareDocument('tsconfigProject/foo.vue', 'vue', ` - +test('Default slot', async () => { + await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + + + `); + expect( + await requestReferences('tsconfigProject/fixture.vue', 'vue', ` - `); - expect( - await requestReferences('tsconfigProject/fixture.vue', 'vue', ` - - `) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 16, - "line": 7, - }, - "start": { - "character": 5, - "line": 7, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/foo.vue", - }, - { - "range": { - "end": { - "character": 10, - "line": 2, - }, - "start": { - "character": 6, - "line": 2, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/fixture.vue", - }, - ] - `); - }); + `) + ).toMatchInlineSnapshot(` + { + "body": { + "refs": [ + { + "end": { + "line": 3, + "offset": 10, + }, + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "isDefinition": true, + "isWriteAccess": false, + "lineText": " ", + "start": { + "line": 3, + "offset": 6, + }, + }, + { + "end": { + "line": 8, + "offset": 16, + }, + "file": "\${testWorkspacePath}/tsconfigProject/foo.vue", + "isDefinition": false, + "isWriteAccess": false, + "lineText": "
", + "start": { + "line": 8, + "offset": 5, + }, + }, + ], + "symbolDisplayString": "(property) default?: (props: typeof __VLS_1) => any", + "symbolName": "slot", + "symbolStartOffset": 6, + }, + "command": "references", + "request_seq": 84, + "seq": 0, + "success": true, + "type": "response", + } + `); +}); + +test('Named slot', async () => { + await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + - it('Named slot', async () => { - await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + + `); + expect( + await requestReferences('tsconfigProject/fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + { + "body": { + "refs": [ + { + "end": { + "line": 3, + "offset": 19, + }, + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "isDefinition": true, + "isWriteAccess": false, + "lineText": " ", + "start": { + "line": 3, + "offset": 16, + }, + }, + { + "end": { + "line": 7, + "offset": 17, + }, + "file": "\${testWorkspacePath}/tsconfigProject/foo.vue", + "isDefinition": false, + "isWriteAccess": false, + "lineText": " ", + "start": { + "line": 7, + "offset": 14, + }, + }, + ], + "symbolDisplayString": "(property) foo?: (props: typeof __VLS_1) => any", + "symbolName": "foo", + "symbolStartOffset": 16, + }, + "command": "references", + "request_seq": 89, + "seq": 0, + "success": true, + "type": "response", + } + `); +}); + +test('v-bind shorthand', async () => { + expect( + await requestReferences('tsconfigProject/fixture.vue', 'vue', ` - `); - expect( - await requestReferences('tsconfigProject/fixture.vue', 'vue', ` - `) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 17, - "line": 6, - }, - "start": { - "character": 14, - "line": 6, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/foo.vue", - }, - { - "range": { - "end": { - "character": 19, - "line": 2, - }, - "start": { - "character": 16, - "line": 2, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/fixture.vue", - }, - ] - `); - }); - - it('v-bind shorthand', async () => { - expect( - await requestReferences('tsconfigProject/fixture.vue', 'vue', ` - - - - `) - ).toMatchInlineSnapshot(` - [ - { - "range": { - "end": { - "character": 14, - "line": 6, - }, - "start": { - "character": 11, - "line": 6, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/fixture.vue", - }, - { - "range": { - "end": { - "character": 13, - "line": 2, - }, - "start": { - "character": 10, - "line": 2, - }, - }, - "uri": "file://\${testWorkspacePath}/tsconfigProject/fixture.vue", - }, - ] - `); - }); - - const openedDocuments: TextDocument[] = []; - - afterEach(async () => { - const server = await getLanguageServer(); - for (const document of openedDocuments) { - await server.closeTextDocument(document.uri); + ).toMatchInlineSnapshot(` + { + "body": { + "refs": [ + { + "contextEnd": { + "line": 3, + "offset": 18, + }, + "contextStart": { + "line": 3, + "offset": 4, + }, + "end": { + "line": 3, + "offset": 13, + }, + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "isDefinition": true, + "isWriteAccess": true, + "lineText": " const foo = 1;", + "start": { + "line": 3, + "offset": 10, + }, + }, + { + "end": { + "line": 7, + "offset": 14, + }, + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "isDefinition": false, + "isWriteAccess": false, + "lineText": " ", + "start": { + "line": 7, + "offset": 11, + }, + }, + ], + "symbolDisplayString": "const foo: 1", + "symbolName": "foo", + "symbolStartOffset": 10, + }, + "command": "references", + "request_seq": 93, + "seq": 0, + "success": true, + "type": "response", } - openedDocuments.length = 0; - }); + `); +}); + +const openedDocuments: TextDocument[] = []; - async function requestReferences(fileName: string, languageId: string, content: string) { - const offset = content.indexOf('|'); - expect(offset).toBeGreaterThanOrEqual(0); - content = content.slice(0, offset) + content.slice(offset + 1); +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); + } + openedDocuments.length = 0; +}); - const server = await getLanguageServer(); - let document = await prepareDocument(fileName, languageId, content); +async function requestReferences(fileName: string, languageId: string, content: string) { + const offset = content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + content = content.slice(0, offset) + content.slice(offset + 1); - const position = document.positionAt(offset); - const references = await server.sendReferencesRequest(document.uri, position, { includeDeclaration: false }); - expect(references).toBeDefined(); + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); - for (const loc of references!) { - loc.uri = 'file://${testWorkspacePath}' + loc.uri.slice(URI.file(testWorkspacePath).toString().length); - } + const res = await server.tsserver.message({ + seq: server.nextSeq(), + command: 'references', + arguments: { + file: URI.parse(document.uri).fsPath, + position: offset, + includeDeclaration: false, + }, + }); + expect(res.success).toBe(true); - return references!; + for (const ref of res!.body.refs) { + ref.file = '${testWorkspacePath}' + ref.file.slice(testWorkspacePath.length); } - async function prepareDocument(fileName: string, languageId: string, content: string) { - const server = await getLanguageServer(); - const uri = URI.file(`${testWorkspacePath}/${fileName}`); - const document = await server.openInMemoryDocument(uri.toString(), languageId, content); - if (openedDocuments.every(d => d.uri !== document.uri)) { - openedDocuments.push(document); - } - return document; + return res!; +} + +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); } -}); + return document; +} diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index 1a81801da8..e59da6be8e 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -1,14 +1,12 @@ import { TextDocument } from '@volar/language-server'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; import { getLanguageServer, testWorkspacePath } from './server.js'; -describe('Renaming', async () => { - - it('#2410', async () => { - expect( - await requestRename('fixture.vue', 'vue', ``, 'h2') - ).toMatchInlineSnapshot(` +test('#2410', async () => { + expect( + await requestRenameToVueServer('fixture.vue', 'vue', ``, 'h2') + ).toMatchInlineSnapshot(` { "changes": { "file://\${testWorkspacePath}/fixture.vue": [ @@ -42,9 +40,9 @@ describe('Renaming', async () => { }, } `); - expect( - await requestRename('fixture.vue', 'vue', ``, 'h2') - ).toMatchInlineSnapshot(` + expect( + await requestRenameToVueServer('fixture.vue', 'vue', ``, 'h2') + ).toMatchInlineSnapshot(` { "changes": { "file://\${testWorkspacePath}/fixture.vue": [ @@ -78,932 +76,1160 @@ describe('Renaming', async () => { }, } `); - }); +}); - it('CSS', async () => { - expect( - await requestRename('fixture.vue', 'vue', ` - - - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 28, - "line": 2, - }, - "start": { - "character": 25, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 8, - "line": 7, - }, - "start": { - "character": 5, - "line": 7, - }, - }, - }, - ], - }, - } - `); - expect( - await requestRename('fixture.vue', 'vue', ` - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 20, - "line": 2, - }, - "start": { - "character": 17, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 8, - "line": 6, - }, - "start": { - "character": 5, - "line": 6, - }, - }, - }, - ], - }, - } - `); - expect( - await requestRename('fixture.vue', 'vue', ` - - - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 28, - "line": 7, - }, - "start": { - "character": 25, - "line": 7, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 13, - "line": 2, - }, - "start": { - "character": 10, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 35, - "line": 11, - }, - "start": { - "character": 32, - "line": 11, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 29, - "line": 11, - }, - "start": { - "character": 26, - "line": 11, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 35, - "line": 10, - }, - "start": { - "character": 32, - "line": 10, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 29, - "line": 10, - }, - "start": { - "character": 26, - "line": 10, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 29, - "line": 9, - }, - "start": { - "character": 26, - "line": 9, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 29, - "line": 8, - }, - "start": { - "character": 26, - "line": 8, - }, - }, - }, - ], - }, - } - `); - }); +test('CSS', async () => { + expect( + await requestRenameToTsServer('fixture.vue', 'vue', ` + - it('Component props', async () => { - await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "'foo'", + "fullDisplayName": "'foo'", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 28, + }, + "start": { + "line": 3, + "offset": 25, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 28, + }, + "start": { + "line": 3, + "offset": 25, + }, + }, + { + "end": { + "line": 8, + "offset": 8, + }, + "start": { + "line": 8, + "offset": 5, + }, + }, + ], + }, + ], + } + `); + expect( + await requestRenameToTsServer('fixture.vue', 'vue', ` + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "'foo'", + "fullDisplayName": "__type.'foo'", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 20, + }, + "start": { + "line": 3, + "offset": 17, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 20, + }, + "start": { + "line": 3, + "offset": 17, + }, + }, + { + "end": { + "line": 7, + "offset": 8, + }, + "start": { + "line": 7, + "offset": 5, + }, + }, + ], + }, + ], + } + `); + expect( + await requestRenameToTsServer('fixture.vue', 'vue', ` - `); - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'cccDdd') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "cccDdd", - "range": { - "end": { - "character": 24, - "line": 6, - }, - "start": { - "character": 18, - "line": 6, - }, - }, - }, - { - "newText": "cccDdd", - "range": { - "end": { - "character": 14, - "line": 2, - }, - "start": { - "character": 8, - "line": 2, - }, - }, - }, - ], - "file://\${testWorkspacePath}/tsconfigProject/foo.vue": [ - { - "newText": "cccDdd", - "range": { - "end": { - "character": 17, - "line": 3, - }, - "start": { - "character": 11, - "line": 3, - }, - }, - }, - { - "newText": "ccc-ddd", - "range": { - "end": { - "character": 18, - "line": 2, - }, - "start": { - "character": 11, - "line": 2, - }, - }, - }, - ], - }, - } - `); - }); - it('Component type props', async () => { - await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 8, + "offset": 28, + }, + "start": { + "line": 8, + "offset": 25, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/fixture.vue", + "locs": [ + { + "contextEnd": { + "line": 3, + "offset": 18, + }, + "contextStart": { + "line": 3, + "offset": 4, + }, + "end": { + "line": 3, + "offset": 13, + }, + "start": { + "line": 3, + "offset": 10, + }, + }, + { + "end": { + "line": 12, + "offset": 35, + }, + "start": { + "line": 12, + "offset": 32, + }, + }, + { + "end": { + "line": 12, + "offset": 29, + }, + "start": { + "line": 12, + "offset": 26, + }, + }, + { + "end": { + "line": 11, + "offset": 35, + }, + "start": { + "line": 11, + "offset": 32, + }, + }, + { + "end": { + "line": 11, + "offset": 29, + }, + "start": { + "line": 11, + "offset": 26, + }, + }, + { + "end": { + "line": 10, + "offset": 29, + }, + "start": { + "line": 10, + "offset": 26, + }, + }, + { + "end": { + "line": 9, + "offset": 29, + }, + "start": { + "line": 9, + "offset": 26, + }, + }, + { + "end": { + "line": 8, + "offset": 28, + }, + "start": { + "line": 8, + "offset": 25, + }, + }, + ], + }, + ], + } + `); +}); + +test('Component props', async () => { + await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + + + + `); + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` - `); - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'cccDdd') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "cccDdd", - "range": { - "end": { - "character": 14, - "line": 2, - }, - "start": { - "character": 8, - "line": 2, - }, - }, - }, - { - "newText": "cccDdd", - "range": { - "end": { - "character": 24, - "line": 6, - }, - "start": { - "character": 18, - "line": 6, - }, - }, - }, - ], - "file://\${testWorkspacePath}/tsconfigProject/foo.vue": [ - { - "newText": "cccDdd", - "range": { - "end": { - "character": 17, - "line": 3, - }, - "start": { - "character": 11, - "line": 3, - }, - }, - }, - { - "newText": "ccc-ddd", - "range": { - "end": { - "character": 18, - "line": 2, - }, - "start": { - "character": 11, - "line": 2, - }, - }, - }, - ], - }, - } - `); - }); + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "aaaBbb", + "fullDisplayName": "__object.aaaBbb", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 7, + "offset": 24, + }, + "start": { + "line": 7, + "offset": 18, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "contextEnd": { + "line": 7, + "offset": 32, + }, + "contextStart": { + "line": 7, + "offset": 18, + }, + "end": { + "line": 7, + "offset": 24, + }, + "start": { + "line": 7, + "offset": 18, + }, + }, + { + "end": { + "line": 3, + "offset": 14, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + ], + }, + { + "file": "\${testWorkspacePath}/tsconfigProject/foo.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 17, + }, + "start": { + "line": 4, + "offset": 11, + }, + }, + { + "end": { + "line": 3, + "offset": 18, + }, + "start": { + "line": 3, + "offset": 11, + }, + }, + ], + }, + ], + } + `); +}); - it('Component dynamic props', async () => { - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 13, - "line": 6, - }, - "start": { - "character": 10, - "line": 6, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 15, - "line": 2, - }, - "start": { - "character": 12, - "line": 2, - }, - }, - }, - ], - }, - } - `); - }); +test('Component type props', async () => { + await prepareDocument('tsconfigProject/foo.vue', 'vue', ` + - it('Component returns', async () => { - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 11, - "line": 2, - }, - "start": { - "character": 8, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 10, - "line": 11, - }, - "start": { - "character": 7, - "line": 11, - }, - }, - }, - ], - }, - } - `); - }); + + `); + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + - it(' - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 13, - "line": 6, - }, - "start": { - "character": 10, - "line": 6, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 11, - "line": 2, - }, - "start": { - "character": 8, - "line": 2, - }, - }, - }, - ], - }, - } - `); - }); + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "aaaBbb", + "fullDisplayName": "__type.aaaBbb", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 7, + "offset": 24, + }, + "start": { + "line": 7, + "offset": 18, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 14, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + { + "contextEnd": { + "line": 7, + "offset": 32, + }, + "contextStart": { + "line": 7, + "offset": 18, + }, + "end": { + "line": 7, + "offset": 24, + }, + "start": { + "line": 7, + "offset": 18, + }, + }, + ], + }, + { + "file": "\${testWorkspacePath}/tsconfigProject/foo.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 17, + }, + "start": { + "line": 4, + "offset": 11, + }, + }, + { + "end": { + "line": 3, + "offset": 18, + }, + "start": { + "line": 3, + "offset": 11, + }, + }, + ], + }, + ], + } + `); +}); - it('Component tags', async () => { - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'CcDd') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "cc-dd", - "range": { - "end": { - "character": 19, - "line": 3, - }, - "start": { - "character": 14, - "line": 3, - }, - }, - }, - { - "newText": "cc-dd", - "range": { - "end": { - "character": 11, - "line": 3, - }, - "start": { - "character": 6, - "line": 3, - }, - }, - }, - { - "newText": "CcDd", - "range": { - "end": { - "character": 17, - "line": 2, - }, - "start": { - "character": 13, - "line": 2, - }, - }, - }, - { - "newText": "CcDd", - "range": { - "end": { - "character": 10, - "line": 2, - }, - "start": { - "character": 6, - "line": 2, - }, - }, - }, - { - "newText": "CcDd", - "range": { - "end": { - "character": 15, - "line": 7, - }, - "start": { - "character": 11, - "line": 7, - }, - }, - }, - ], - }, - } - `); - }); +test('Component dynamic props', async () => { + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + - it('#4673', async () => { - expect( - await requestRename('fixture.vue', 'vue', ` - - - - - - - - `, 'stylus') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/fixture.vue": [ - { - "newText": "stylus", - "range": { - "end": { - "character": 22, - "line": 8, - }, - "start": { - "character": 18, - "line": 8, - }, - }, - }, - { - "newText": "stylus", - "range": { - "end": { - "character": 23, - "line": 15, - }, - "start": { - "character": 19, - "line": 15, - }, - }, - }, - { - "newText": "stylus", - "range": { - "end": { - "character": 40, - "line": 4, - }, - "start": { - "character": 36, - "line": 4, - }, - }, - }, - ], - }, - } - `); - }); + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 15, + }, + "start": { + "line": 3, + "offset": 12, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "contextEnd": { + "line": 7, + "offset": 22, + }, + "contextStart": { + "line": 7, + "offset": 4, + }, + "end": { + "line": 7, + "offset": 13, + }, + "start": { + "line": 7, + "offset": 10, + }, + }, + { + "end": { + "line": 3, + "offset": 15, + }, + "start": { + "line": 3, + "offset": 12, + }, + }, + ], + }, + ], + } + `); +}); - it('Scoped Classes', async () => { - expect( - await requestRename('fixture.vue', 'vue', ` - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 23, - "line": 4, - }, - "start": { - "character": 20, - "line": 4, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 32, - "line": 3, - }, - "start": { - "character": 29, - "line": 3, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 23, - "line": 3, - }, - "start": { - "character": 20, - "line": 3, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 22, - "line": 2, - }, - "start": { - "character": 19, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 8, - "line": 7, - }, - "start": { - "character": 5, - "line": 7, - }, - }, - }, - ], - }, - } - `); - }); +test('Component returns', async () => { + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + - it('Ref', async () => { - expect( - await requestRename('tsconfigProject/fixture.vue', 'vue', ` - - - - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 16, - "line": 2, - }, - "start": { - "character": 13, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 13, - "line": 7, - }, - "start": { - "character": 10, - "line": 7, - }, - }, - }, - ], - }, - } - `); - }); + - `, 'bar') - ).toMatchInlineSnapshot(` - { - "changes": { - "file://\${testWorkspacePath}/tsconfigProject/fixture.vue": [ - { - "newText": "bar", - "range": { - "end": { - "character": 16, - "line": 2, - }, - "start": { - "character": 13, - "line": 2, - }, - }, - }, - { - "newText": "bar", - "range": { - "end": { - "character": 34, - "line": 7, - }, - "start": { - "character": 31, - "line": 7, - }, - }, - }, - ], - }, - } - `); - }); + export default defineComponent({ + setup() { + return { + foo: 1, + }; + }, + }); + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 11, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 11, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + { + "contextEnd": { + "line": 12, + "offset": 13, + }, + "contextStart": { + "line": 12, + "offset": 7, + }, + "end": { + "line": 12, + "offset": 10, + }, + "start": { + "line": 12, + "offset": 7, + }, + }, + ], + }, + ], + } + `); +}); - const openedDocuments: TextDocument[] = []; +test(' + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 11, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "contextEnd": { + "line": 7, + "offset": 18, + }, + "contextStart": { + "line": 7, + "offset": 4, + }, + "end": { + "line": 7, + "offset": 13, + }, + "start": { + "line": 7, + "offset": 10, + }, + }, + { + "end": { + "line": 3, + "offset": 11, + }, + "start": { + "line": 3, + "offset": 8, + }, + }, + ], + }, + ], } - openedDocuments.length = 0; - }); + `); +}); + +test('Component tags', async () => { + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "AaBb", + "fullDisplayName": "AaBb", + "kind": "alias", + "kindModifiers": "export", + "triggerSpan": { + "end": { + "line": 8, + "offset": 15, + }, + "start": { + "line": 8, + "offset": 11, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 19, + }, + "start": { + "line": 4, + "offset": 14, + }, + }, + { + "end": { + "line": 4, + "offset": 11, + }, + "start": { + "line": 4, + "offset": 6, + }, + }, + { + "end": { + "line": 3, + "offset": 17, + }, + "start": { + "line": 3, + "offset": 13, + }, + }, + { + "end": { + "line": 3, + "offset": 10, + }, + "start": { + "line": 3, + "offset": 6, + }, + }, + { + "contextEnd": { + "line": 8, + "offset": 35, + }, + "contextStart": { + "line": 8, + "offset": 4, + }, + "end": { + "line": 8, + "offset": 15, + }, + "start": { + "line": 8, + "offset": 11, + }, + }, + ], + }, + ], + } + `); +}); - async function requestRename(fileName: string, languageId: string, _content: string, newName: string) { - const offset = _content.indexOf('|'); - expect(offset).toBeGreaterThanOrEqual(0); - const content = _content.slice(0, offset) + _content.slice(offset + 1); +test('#4673', async () => { + expect( + await requestRenameToTsServer('fixture.vue', 'vue', ` + - const server = await getLanguageServer(); - let document = await prepareDocument(fileName, languageId, content); + - const position = document.positionAt(offset); - const edit = await server.sendRenameRequest(document.uri, position, newName); - expect(edit?.changes).toBeDefined(); + - for (const [uri, edits] of Object.entries(edit!.changes!)) { - delete edit!.changes![uri]; - edit!.changes!['file://${testWorkspacePath}' + uri.slice(URI.file(testWorkspacePath).toString().length)] = edits; + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "styl", + "fullDisplayName": "__type.styl", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 9, + "offset": 22, + }, + "start": { + "line": 9, + "offset": 18, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/fixture.vue", + "locs": [ + { + "end": { + "line": 9, + "offset": 22, + }, + "start": { + "line": 9, + "offset": 18, + }, + }, + { + "end": { + "line": 16, + "offset": 23, + }, + "start": { + "line": 16, + "offset": 19, + }, + }, + { + "end": { + "line": 5, + "offset": 40, + }, + "start": { + "line": 5, + "offset": 36, + }, + }, + ], + }, + ], } + `); +}); - return edit; - } +test('Scoped Classes', async () => { + expect( + await requestRenameToTsServer('fixture.vue', 'vue', ` + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "'foo'", + "fullDisplayName": "__type.'foo'", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 22, + }, + "start": { + "line": 3, + "offset": 19, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/fixture.vue", + "locs": [ + { + "end": { + "line": 5, + "offset": 23, + }, + "start": { + "line": 5, + "offset": 20, + }, + }, + { + "end": { + "line": 4, + "offset": 32, + }, + "start": { + "line": 4, + "offset": 29, + }, + }, + { + "end": { + "line": 4, + "offset": 23, + }, + "start": { + "line": 4, + "offset": 20, + }, + }, + { + "end": { + "line": 3, + "offset": 22, + }, + "start": { + "line": 3, + "offset": 19, + }, + }, + { + "end": { + "line": 8, + "offset": 8, + }, + "start": { + "line": 8, + "offset": 5, + }, + }, + ], + }, + ], + } + `); +}); + +test('Ref', async () => { + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "const", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 8, + "offset": 13, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 16, + }, + "start": { + "line": 3, + "offset": 13, + }, + }, + { + "contextEnd": { + "line": 8, + "offset": 22, + }, + "contextStart": { + "line": 8, + "offset": 4, + }, + "end": { + "line": 8, + "offset": 13, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, + ], + }, + ], + } + `); +}); - async function prepareDocument(fileName: string, languageId: string, content: string) { - const server = await getLanguageServer(); - const uri = URI.file(`${testWorkspacePath}/${fileName}`); - const document = await server.openInMemoryDocument(uri.toString(), languageId, content); - if (openedDocuments.every(d => d.uri !== document.uri)) { - openedDocuments.push(document); +test('Template Ref', async () => { + expect( + await requestRenameToTsServer('tsconfigProject/fixture.vue', 'vue', ` + + + + `) + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "__type.foo", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 8, + "offset": 34, + }, + "start": { + "line": 8, + "offset": 31, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 3, + "offset": 16, + }, + "start": { + "line": 3, + "offset": 13, + }, + }, + { + "end": { + "line": 8, + "offset": 34, + }, + "start": { + "line": 8, + "offset": 31, + }, + }, + ], + }, + ], } - return document; + `); +}); + +const openedDocuments: TextDocument[] = []; + +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); } + openedDocuments.length = 0; }); + +async function requestRenameToVueServer(fileName: string, languageId: string, _content: string, newName: string) { + const offset = _content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + const content = _content.slice(0, offset) + _content.slice(offset + 1); + + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const position = document.positionAt(offset); + const edit = await server.vueserver.sendRenameRequest(document.uri, position, newName); + expect(edit?.changes).toBeDefined(); + + for (const [uri, edits] of Object.entries(edit!.changes!)) { + delete edit!.changes![uri]; + edit!.changes!['file://${testWorkspacePath}' + uri.slice(URI.file(testWorkspacePath).toString().length)] = edits; + } + + return edit; +} + +async function requestRenameToTsServer(fileName: string, languageId: string, _content: string) { + const offset = _content.indexOf('|'); + expect(offset).toBeGreaterThanOrEqual(0); + const content = _content.slice(0, offset) + _content.slice(offset + 1); + + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const res = await server.tsserver.message({ + seq: server.nextSeq(), + command: 'rename', + arguments: { + file: URI.parse(document.uri).fsPath, + position: offset, + findInStrings: false, + findInComments: false, + }, + }); + + expect(res!.success).toBe(true); + + for (const loc of res!.body.locs) { + loc.file = '${testWorkspacePath}' + loc.file.slice(testWorkspacePath.length); + } + + return res.body; +} + +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); + } + return document; +} diff --git a/packages/language-server/tests/server.ts b/packages/language-server/tests/server.ts index fbce2d8992..8736b51adf 100644 --- a/packages/language-server/tests/server.ts +++ b/packages/language-server/tests/server.ts @@ -1,15 +1,38 @@ -import { ConfigurationRequest, PublishDiagnosticsNotification } from '@volar/language-server'; +import { launchServer } from '@typescript/server-harness'; +import { ConfigurationRequest, PublishDiagnosticsNotification, TextDocument } from '@volar/language-server'; import type { LanguageServerHandle } from '@volar/test-utils'; import { startLanguageServer } from '@volar/test-utils'; import * as path from 'node:path'; import { URI } from 'vscode-uri'; let serverHandle: LanguageServerHandle | undefined; +let tsserver: import('@typescript/server-harness').Server; +let seq = 1; export const testWorkspacePath = path.resolve(__dirname, '../../../test-workspace'); -export async function getLanguageServer(): Promise { +export async function getLanguageServer(): Promise<{ + vueserver: LanguageServerHandle; + tsserver: import('@typescript/server-harness').Server; + nextSeq: () => number; + open: (uri: string, languageId: string, content: string) => Promise; + close: (uri: string) => Promise; +}> { if (!serverHandle) { + tsserver = launchServer( + path.join(__dirname, '..', '..', '..', 'node_modules', 'typescript', 'lib', 'tsserver.js'), + [ + '--disableAutomaticTypingAcquisition', + '--globalPlugins', '@vue/typescript-plugin', + '--suppressDiagnosticEvents', + // '--logVerbosity', 'verbose', + // '--logFile', path.join(__dirname, 'tsserver.log'), + ] + ); + + tsserver.on('exit', code => console.log(code ? `Exited with code ${code}` : `Terminated`)); + // tsserver.on('event', e => console.log(e)); + serverHandle = startLanguageServer(require.resolve('../bin/vue-language-server.js'), testWorkspacePath); serverHandle.connection.onNotification(PublishDiagnosticsNotification.type, () => { }); serverHandle.connection.onRequest(ConfigurationRequest.type, ({ items }) => { @@ -26,10 +49,6 @@ export async function getLanguageServer(): Promise { { typescript: { tsdk: path.dirname(require.resolve('typescript/lib/typescript.js')), - disableAutoImportCache: true, - }, - vue: { - hybridMode: false, }, }, { @@ -39,5 +58,53 @@ export async function getLanguageServer(): Promise { } ); } - return serverHandle; + return { + vueserver: serverHandle, + tsserver: tsserver, + nextSeq: () => seq++, + open: async (uri: string, languageId: string, content: string) => { + const res = await tsserver.message({ + seq: seq++, + type: 'request', + command: 'updateOpen', + arguments: { + changedFiles: [], + closedFiles: [], + openFiles: [ + { + file: URI.parse(uri).fsPath, + fileContent: content, + projectRootPath: path.resolve(testWorkspacePath, './tsconfigProject'), + plugins: ['@vue/typescript-plugin'], + } + ] + } + }); + if (!res.success) { + throw new Error(res.body); + } + + // Wait for the named pipe server ready + // TODO: remove this when named pipe logic is removed + await new Promise(resolve => setTimeout(resolve, 2000)); + + return await serverHandle!.openInMemoryDocument(uri, languageId, content); + }, + close: async (uri: string) => { + const res = await tsserver.message({ + seq: seq++, + type: 'request', + command: 'updateOpen', + arguments: { + changedFiles: [], + closedFiles: [URI.parse(uri).fsPath], + openFiles: [] + } + }); + if (!res.success) { + throw new Error(res.body); + } + await serverHandle!.closeTextDocument(uri); + }, + }; } diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 6c7925f83b..c50f8c61c7 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -50,16 +50,10 @@ declare module '@volar/language-service' { } } -export function getFullLanguageServicePlugins( - ts: typeof import('typescript'), - { disableAutoImportCache }: { disableAutoImportCache?: boolean; } = {} -) { +export function getFullLanguageServicePlugins(ts: typeof import('typescript')) { const plugins: LanguageServicePlugin[] = [ - ...createTypeScriptPlugins(ts, { disableAutoImportCache }), - ...getCommonLanguageServicePlugins( - ts, - getTsPluginClientForLSP - ) + ...createTypeScriptPlugins(ts), + ...getCommonLanguageServicePlugins(ts, getTsPluginClientForLSP), ]; for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 03a766fd22..f464c0ba85 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -28,7 +28,6 @@ class NamedPipeServer { path: string; connecting = false; projectInfo?: ProjectInfo; - containsFileCache = new Map>(); componentNamesAndProps = new FileMap< Record >(false); @@ -39,17 +38,7 @@ class NamedPipeServer { containsFile(fileName: string) { if (this.projectInfo) { - if (!this.containsFileCache.has(fileName)) { - this.containsFileCache.set(fileName, (async () => { - const res = await this.sendRequest('containsFile', fileName); - if (typeof res !== 'boolean') { - // If the request fails, delete the cache - this.containsFileCache.delete(fileName); - } - return res; - })()); - } - return this.containsFileCache.get(fileName); + return this.sendRequest('containsFile', fileName); } } @@ -82,7 +71,6 @@ class NamedPipeServer { if (projectInfo) { console.log('TSServer project ready:', projectInfo.name); this.projectInfo = projectInfo; - this.containsFileCache.clear(); onServerReady.forEach(cb => cb()); } else { this.close(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3891ba3c6..957e324b34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: packages/language-server: dependencies: + '@typescript/server-harness': + specifier: latest + version: 0.3.5 '@volar/language-core': specifier: ~2.4.11 version: 2.4.11 @@ -1207,6 +1210,9 @@ packages: resolution: {integrity: sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/server-harness@0.3.5': + resolution: {integrity: sha512-YT9oe27zm7HdGXYad5SZrdJzVe9eavG3F6YplsWvAraowGtuDeY7FHPVuQPtQj6GxG097Us4JDkA8n5I4iQovQ==} + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -4740,6 +4746,8 @@ snapshots: '@typescript-eslint/types': 8.19.0 eslint-visitor-keys: 4.2.0 + '@typescript/server-harness@0.3.5': {} + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 diff --git a/test-workspace/tsconfigProject/fixture.vue b/test-workspace/tsconfigProject/fixture.vue new file mode 100644 index 0000000000..e69de29bb2