diff --git a/packages/language-server/tests/__snapshots__/completions.spec.ts.snap b/packages/language-server/tests/__snapshots__/completions.spec.ts.snap
deleted file mode 100644
index f2b3dd1bf6..0000000000
--- a/packages/language-server/tests/__snapshots__/completions.spec.ts.snap
+++ /dev/null
@@ -1,124 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`Completions > #2454 1`] = `
-"
-
-
-
-
-
- "
-`;
-
-exports[`Completions > #2511 1`] = `
-"
-
- "
-`;
-
-exports[`Completions > #3658 1`] = `
-"
-
-
-
- {{ foo }}
-
-
-
- "
-`;
-
-exports[`Completions > #4639 1`] = `
-"
-
-
-
- "
-`;
-
-exports[`Completions > $event argument 1`] = `""`;
-
-exports[`Completions >
- "
-`;
-
-exports[`Completions > Alias path 1`] = `
-"
-
- "
-`;
-
-exports[`Completions > Component auto import 1`] = `
-"
-
-
-
-
-
- "
-`;
-
-exports[`Completions > Directives 1`] = `""`;
-
-exports[`Completions > Directives 2`] = `""`;
-
-exports[`Completions > Directives 3`] = `""`;
-
-exports[`Completions > Directives 4`] = `""`;
-
-exports[`Completions > Relative path 1`] = `
-"
-
- "
-`;
-
-exports[`Completions > Slot name 1`] = `
-"
-
-
-
-
-
-
-
- "
-`;
-
-exports[`Completions > core#8811 1`] = `
-"
-
-
-
-
-
- "
-`;
diff --git a/packages/language-server/tests/__snapshots__/definitions.spec.ts.snap b/packages/language-server/tests/__snapshots__/definitions.spec.ts.snap
new file mode 100644
index 0000000000..ff893647c0
--- /dev/null
+++ b/packages/language-server/tests/__snapshots__/definitions.spec.ts.snap
@@ -0,0 +1,61 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Definitions > #2600 1`] = `"tsconfigProject/foo.vue"`;
+
+exports[`Definitions > #2600 2`] = `
+{
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+}
+`;
+
+exports[`Definitions > Alias path 1`] = `"tsconfigProject/foo.ts"`;
+
+exports[`Definitions > Alias path 2`] = `
+{
+ "end": {
+ "character": 25,
+ "line": 0,
+ },
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+}
+`;
+
+exports[`Definitions > TS to vue 1`] = `"tsconfigProject/empty.vue"`;
+
+exports[`Definitions > TS to vue 2`] = `
+{
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+}
+`;
+
+exports[`Definitions > TS to vue 3`] = `"tsconfigProject/empty.vue"`;
+
+exports[`Definitions > TS to vue 4`] = `
+{
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+}
+`;
diff --git a/packages/language-server/tests/definitions.spec.ts b/packages/language-server/tests/definitions.spec.ts
new file mode 100644
index 0000000000..0121718366
--- /dev/null
+++ b/packages/language-server/tests/definitions.spec.ts
@@ -0,0 +1,89 @@
+import { Location, TextDocument } from '@volar/language-server';
+import * as path from 'path';
+import { afterEach, describe, expect, it } from 'vitest';
+import { URI } from 'vscode-uri';
+import { getLanguageServer, testWorkspacePath } from './server.js';
+
+describe('Definitions', async () => {
+
+ it('TS to vue', async () => {
+ await ensureGlobalTypesHolder('tsconfigProject');
+ await assertDefinition('tsconfigProject/fixture1.ts', 'typescript', `import C|omponent from './empty.vue';`);
+ await assertDefinition('tsconfigProject/fixture2.ts', 'typescript', `import Component from '|./empty.vue';`);
+ });
+
+ it('Alias path', async () => {
+ await ensureGlobalTypesHolder('tsconfigProject');
+ await openDocument('tsconfigProject/foo.ts', 'typescript', `export const foo = 'foo';`);
+ await assertDefinition('tsconfigProject/fixture.vue', 'vue', `
+
+ `);
+ });
+
+ it('#2600', async () => {
+ await ensureGlobalTypesHolder('tsconfigProject');
+ await openDocument('tsconfigProject/foo.vue', 'vue', `
+
+ {{ msg }}
+
+
+
+ `);
+ await assertDefinition('tsconfigProject/fixture.vue', 'vue', `
+
+ `);
+ });
+
+ const openedDocuments: TextDocument[] = [];
+
+ afterEach(async () => {
+ const server = await getLanguageServer();
+ for (const document of openedDocuments) {
+ await server.closeTextDocument(document.uri);
+ }
+ openedDocuments.length = 0;
+ });
+
+ /**
+ * @deprecated Remove this when #4717 fixed.
+ */
+ async function ensureGlobalTypesHolder(folderName: string) {
+ const document = await openDocument(`${folderName}/globalTypesHolder.vue`, 'vue', '');
+ const server = await getLanguageServer();
+ await server.sendDocumentDiagnosticRequest(document.uri);
+ }
+
+ async function assertDefinition(fileName: string, languageId: string, content: string) {
+ const offset = content.indexOf('|');
+ content = content.slice(0, offset) + content.slice(offset + 1);
+
+ const server = await getLanguageServer();
+ let document = await openDocument(fileName, languageId, content);
+
+ const position = document.positionAt(offset);
+ const definition = await server.sendDefinitionRequest(document.uri, position) as Location[] | null;
+ expect(definition).toBeDefined();
+ expect(definition!.length).greaterThan(0);
+
+ for (const loc of definition!) {
+ expect(path.relative(testWorkspacePath, URI.parse(loc.uri).fsPath)).toMatchSnapshot();
+ expect(loc.range).toMatchSnapshot();
+ }
+ }
+
+ async function openDocument(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;
+ }
+});
diff --git a/packages/language-service/tests/complete.ts b/packages/language-service/tests/complete.ts
deleted file mode 100644
index 582287949d..0000000000
--- a/packages/language-service/tests/complete.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import * as fs from 'fs';
-import * as path from 'path';
-import { describe, expect, it } from 'vitest';
-import type * as vscode from 'vscode-languageserver-protocol';
-import { TextDocument } from 'vscode-languageserver-textdocument';
-import { tester } from './utils/createTester';
-import { fileNameToUri } from './utils/mockEnv';
-
-const baseDir = path.resolve(__dirname, '../../../test-workspace/language-service/complete');
-const testDirs = fs.readdirSync(baseDir);
-const getLineText = (text: string, line: number) => text.replace(/\r\n/g, '\n').split('\n')[line];
-
-for (const dirName of testDirs) {
-
- describe(`complete: ${dirName}`, async () => {
-
- const dir = path.join(baseDir, dirName);
- const inputFiles = readFiles(path.join(dir, 'input'));
- const outputFiles = readFiles(path.join(dir, 'output'));
-
- for (const file in inputFiles) {
-
- const filePath = path.join(dir, 'input', file);
- const uri = fileNameToUri(filePath);
- const fileText = inputFiles[file];
- const document = TextDocument.create('', '', 0, fileText);
- const actions = findCompleteActions(fileText);
-
- const expectedFileText = outputFiles[file];
-
- for (const action of actions) {
-
- const position = document.positionAt(action.offset);
-
- position.line--;
-
- const location = `${filePath}:${position.line + 1}:${position.character + 1}`;
-
- it(`${location} => ${action.label}`, async () => {
-
- expect(expectedFileText).toBeDefined();
-
- let complete = await tester.languageService.getCompletionItems(
- uri,
- position,
- { triggerKind: 1 satisfies typeof vscode.CompletionTriggerKind.Invoked }
- );
-
- if (!complete.items.length) {
- // fix #2511 test case, it's a bug of TS 5.3
- complete = await tester.languageService.getCompletionItems(
- uri,
- position,
- { triggerKind: 1 satisfies typeof vscode.CompletionTriggerKind.Invoked }
- );
- }
-
- let item = complete.items.find(item => item.label === action.label)!;
-
- expect(item).toBeDefined();
-
- item = await tester.languageService.resolveCompletionItem(item);
-
- let edits: vscode.TextEdit[] = [];
-
- if (item.textEdit) {
- if ('replace' in item.textEdit) {
- edits.push({ range: item.textEdit.replace, newText: item.textEdit.newText });
- }
- else {
- edits.push(item.textEdit);
- }
- }
- else {
- edits.push({ range: { start: position, end: position }, newText: item.insertText ?? item.label });
- }
-
- if (item.additionalTextEdits) {
- edits = edits.concat(item.additionalTextEdits);
- }
-
- let result = TextDocument.applyEdits(TextDocument.create('', '', 0, fileText), edits);
-
- result = result.replace(/\$0/g, '').replace(/\$1/g, '');
-
- expect(getLineText(result, position.line)).toBe(getLineText(expectedFileText, position.line));
- });
- }
- }
- });
-}
-
-function readFiles(dir: string) {
-
- const filesText: Record = {};
- const files = fs.readdirSync(dir);
-
- for (const file of files) {
- const filePath = path.join(dir, file);
- filesText[file] = fs.readFileSync(filePath, 'utf8');
- }
-
- return filesText;
-}
-
-function findCompleteActions(text: string) {
-
- return [...text.matchAll(/(\^*)complete:\s*([\S]*)/g)].map(flag => {
-
- const offset = flag.index;
- const label = flag[2];
-
- return {
- offset,
- label,
- };
- });
-}
diff --git a/packages/language-service/tests/findDefinition.ts b/packages/language-service/tests/findDefinition.ts
deleted file mode 100644
index c359d44e3b..0000000000
--- a/packages/language-service/tests/findDefinition.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as fs from 'fs';
-import * as path from 'path';
-import { describe, expect, it } from 'vitest';
-import { TextDocument } from 'vscode-languageserver-textdocument';
-import { tester } from './utils/createTester';
-import { fileNameToUri } from './utils/mockEnv';
-
-const baseDir = path.resolve(__dirname, '../../../test-workspace/language-service/find-definition');
-const testDirs = fs.readdirSync(baseDir);
-
-for (const dirName of testDirs) {
-
- describe(`find definition: ${dirName}`, async () => {
-
- const dir = path.join(baseDir, dirName);
- const inputFiles = readFiles(dir);
-
- for (const file in inputFiles) {
-
- const filePath = path.join(dir, file);
- const uri = fileNameToUri(filePath);
- const fileText = inputFiles[file];
- const document = TextDocument.create('', '', 0, fileText);
- const actions = findActions(fileText);
-
- for (const action of actions) {
-
- const position = document.positionAt(action.offset);
-
- position.line--;
-
- const targetFile = path.resolve(dir, action.targetFile);
- const targetDocument = TextDocument.create('', '', 0, fs.readFileSync(targetFile, 'utf8'));
-
- it(`${filePath}:${position.line + 1}:${position.character + 1} => ${targetFile}:${action.targeRange.start}`, async () => {
-
- const locations = await tester.languageService.getDefinition(
- uri,
- position
- );
-
- expect(locations).toBeDefined();
-
- const location = locations?.find(loc =>
- loc.targetUri === fileNameToUri(targetFile).toString()
- && targetDocument.offsetAt(loc.targetSelectionRange.start) === action.targeRange.start
- && targetDocument.offsetAt(loc.targetSelectionRange.end) === action.targeRange.end
- );
-
- if (!location) {
- console.log(JSON.stringify(locations, null, 2));
- console.log(action.targeRange);
- }
-
- expect(location).toBeDefined();
- });
- }
- }
- });
-}
-
-function readFiles(dir: string) {
-
- const filesText: Record = {};
- const files = fs.readdirSync(dir);
-
- for (const file of files) {
- const filePath = path.join(dir, file);
- filesText[file] = fs.readFileSync(filePath, 'utf8');
- }
-
- return filesText;
-}
-
-const definitionReg = /(\^*)definition:\s*([\S]*),\s*([\S]*),\s*([\S]*)/g;
-
-function findActions(text: string) {
-
- return [...text.matchAll(definitionReg)].map(flag => {
-
- const offset = flag.index;
- const targetFile = flag[2];
- const targeRange = {
- start: Number(flag[3]),
- end: Number(flag[4]),
- };
-
- return {
- offset,
- targetFile,
- targeRange,
- };
- });
-}
diff --git a/test-workspace/language-service/find-definition/#2600/entry.vue b/test-workspace/language-service/find-definition/#2600/entry.vue
deleted file mode 100644
index a39313dd73..0000000000
--- a/test-workspace/language-service/find-definition/#2600/entry.vue
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/test-workspace/language-service/find-definition/#2600/foo.vue b/test-workspace/language-service/find-definition/#2600/foo.vue
deleted file mode 100644
index 35fd4030f5..0000000000
--- a/test-workspace/language-service/find-definition/#2600/foo.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-
- {{ msg }}
-
-
-
diff --git a/test-workspace/language-service/find-definition/alias-path/entry.vue b/test-workspace/language-service/find-definition/alias-path/entry.vue
deleted file mode 100644
index 0df2ffefbb..0000000000
--- a/test-workspace/language-service/find-definition/alias-path/entry.vue
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/test-workspace/language-service/find-definition/alias-path/foo.ts b/test-workspace/language-service/find-definition/alias-path/foo.ts
deleted file mode 100644
index 3329a7d972..0000000000
--- a/test-workspace/language-service/find-definition/alias-path/foo.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const foo = 'foo';
diff --git a/test-workspace/language-service/find-definition/ts-to-vue/component.vue b/test-workspace/language-service/find-definition/ts-to-vue/component.vue
deleted file mode 100644
index 6b4c19a058..0000000000
--- a/test-workspace/language-service/find-definition/ts-to-vue/component.vue
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test-workspace/language-service/find-definition/ts-to-vue/entry.ts b/test-workspace/language-service/find-definition/ts-to-vue/entry.ts
deleted file mode 100644
index d3e3c899c0..0000000000
--- a/test-workspace/language-service/find-definition/ts-to-vue/entry.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import Component from './component.vue';
- // ^definition: ./component.vue, 0, 0
-import Component from './component.vue';
- // ^definition: ./component.vue, 0, 0