From 18754615c0a53bc963aa3f3cc89c9168efcd1536 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 6 Dec 2025 14:50:44 +0800 Subject: [PATCH 1/7] feat(language-core): cache virtual code by URI scheme To prevent git diff buffers overwrite snapshots --- packages/language-core/lib/languagePlugin.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/language-core/lib/languagePlugin.ts b/packages/language-core/lib/languagePlugin.ts index c2b8d805b3..4876161173 100644 --- a/packages/language-core/lib/languagePlugin.ts +++ b/packages/language-core/lib/languagePlugin.ts @@ -60,7 +60,8 @@ export function createVueLanguagePlugin( createVirtualCode(scriptId, languageId, snapshot) { const fileName = asFileName(scriptId); if (plugins.some(plugin => plugin.isValidFile?.(fileName, languageId))) { - const code = fileRegistry.get(fileName); + const codeKey = getFileRegistryKey(scriptId, fileName); + const code = fileRegistry.get(codeKey); if (code) { code.update(snapshot); return code; @@ -74,7 +75,7 @@ export function createVueLanguagePlugin( plugins, ts, ); - fileRegistry.set(fileName, code); + fileRegistry.set(codeKey, code); return code; } } @@ -85,7 +86,7 @@ export function createVueLanguagePlugin( }, disposeVirtualCode(scriptId) { const fileName = asFileName(scriptId); - fileRegistry.delete(fileName); + fileRegistry.delete(getFileRegistryKey(scriptId, fileName)); }, typescript: { extraFileExtensions: getAllExtensions(vueCompilerOptions) @@ -125,3 +126,13 @@ export function getAllExtensions(options: VueCompilerOptions) { ] as const).flatMap(key => options[key])), ]; } + +function getFileRegistryKey(scriptId: T, fileName: string) { + if (typeof scriptId === 'object' && scriptId) { + const scheme = (scriptId as { scheme?: string }).scheme; + if (scheme && scheme !== 'file') { + return `${scheme}:${fileName}`; + } + } + return fileName; +} From b7ae07c64a5f87ecc36107a4d09184da0561007d Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 6 Dec 2025 15:59:41 +0800 Subject: [PATCH 2/7] feat: force send `UpdateOpen` command when document did open --- packages/language-server/lib/server.ts | 17 +++++++++++++++++ packages/typescript-plugin/index.ts | 3 +++ 2 files changed, 20 insertions(+) diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index 60ae5b91d4..dd179c1d2d 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -175,6 +175,23 @@ export function startServer(ts: typeof import('typescript')) { version: packageJson.version, }; + server.documents.onDidOpen(async ({ document }) => { + const uri = URI.parse(document.uri); + return sendTsServerRequest( + '_vue:' + ts.server.protocol.CommandTypes.UpdateOpen, + { + openFiles: [ + { + file: uri.fsPath.replace(/\\/g, '/'), + fileContent: document.getText(), + }, + ], + changedFiles: [], + closedFiles: [], + } satisfies ts.server.protocol.UpdateOpenRequestArgs, + ); + }); + return result; async function sendTsServerRequest(command: string, args: any): Promise { diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index c082cce47a..8fec9bb0c9 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -102,6 +102,9 @@ export = createLanguageServicePlugin( session.addProtocolHandler('_vue:quickinfo', request => { return handlers.get('quickinfo')!(request); }); + session.addProtocolHandler('_vue:updateOpen', request => { + return handlers.get('updateOpen')!(request); + }); session.addProtocolHandler( '_vue:collectExtractProps', request => { From 02044becf987995b9179c991167c60f8b64141f4 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 6 Dec 2025 16:29:46 +0800 Subject: [PATCH 3/7] fix: force update opened file if when non-file scheme open --- packages/language-server/lib/server.ts | 43 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index dd179c1d2d..98f3fbea04 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -175,21 +175,25 @@ export function startServer(ts: typeof import('typescript')) { version: packageJson.version, }; + // #5572 server.documents.onDidOpen(async ({ document }) => { - const uri = URI.parse(document.uri); - return sendTsServerRequest( - '_vue:' + ts.server.protocol.CommandTypes.UpdateOpen, - { - openFiles: [ - { - file: uri.fsPath.replace(/\\/g, '/'), - fileContent: document.getText(), - }, - ], - changedFiles: [], - closedFiles: [], - } satisfies ts.server.protocol.UpdateOpenRequestArgs, - ); + const fileDocument = findFileDocumentForUpdate(document.uri); + if (fileDocument) { + const uri = URI.parse(fileDocument.uri); + await sendTsServerRequest( + '_vue:' + ts.server.protocol.CommandTypes.UpdateOpen, + { + openFiles: [ + { + file: uri.fsPath.replace(/\\/g, '/'), + fileContent: fileDocument.getText(), + }, + ], + changedFiles: [], + closedFiles: [], + } satisfies ts.server.protocol.UpdateOpenRequestArgs, + ); + } }); return result; @@ -236,6 +240,17 @@ export function startServer(ts: typeof import('typescript')) { {}, ); } + + function findFileDocumentForUpdate(docUri: string) { + const uri = URI.parse(docUri); + if (uri.scheme === 'file') { + return; + } + return server.documents.all().find(document => { + const documentUri = URI.parse(document.uri); + return documentUri.scheme === 'file' && documentUri.fsPath === uri.fsPath; + }); + } }); connection.onInitialized(server.initialized); From 258b35d6c6f84bbf52cbd79f4dc9a0005cefd3b3 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 6 Dec 2025 19:14:16 +0800 Subject: [PATCH 4/7] test: add semantic tokens test --- .../tests/semanticTokens.spec.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/language-server/tests/semanticTokens.spec.ts diff --git a/packages/language-server/tests/semanticTokens.spec.ts b/packages/language-server/tests/semanticTokens.spec.ts new file mode 100644 index 0000000000..8dc0c841cf --- /dev/null +++ b/packages/language-server/tests/semanticTokens.spec.ts @@ -0,0 +1,49 @@ +import type { TextDocument } from '@volar/language-server'; +import { afterEach, expect, test } from 'vitest'; +import { URI } from 'vscode-uri'; +import { getLanguageServer, testWorkspacePath } from './server.js'; + +const openedDocuments: TextDocument[] = []; + +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); + } + openedDocuments.length = 0; +}); + +test('#5572 semantic tokens stay in sync when a git view is opened', async () => { + const server = await getLanguageServer(); + const fileContent = ` + + `; + const fileUri = URI.file(`${testWorkspacePath}/semanticTokens.vue`); + + const document = await prepareDocument(fileUri, 'vue', fileContent); + const tokensBefore = (await server.vueserver.sendSemanticTokensRequest(document.uri))!.data; + + // simlulate open git diff view + const gitUri = URI.from({ scheme: 'git', path: fileUri.path }); + await prepareDocument(gitUri, 'vue', fileContent.replace('foo', 'fooooooo')); + + const tokensAfter = (await server.vueserver.sendSemanticTokensRequest(document.uri))!.data; + + expect(tokensAfter).toEqual(tokensBefore); +}); + +async function prepareDocument(uriOrFileName: string | URI, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = typeof uriOrFileName === 'string' + ? URI.file(`${testWorkspacePath}/${uriOrFileName}`) + : uriOrFileName; + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); + } + return document; +} From 7046c760e9018a139434433dd176b5d642198e3c Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 6 Dec 2025 22:33:44 +0800 Subject: [PATCH 5/7] Update server.ts --- packages/language-server/tests/server.ts | 66 +++++++++++++----------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/language-server/tests/server.ts b/packages/language-server/tests/server.ts index 060c7b529d..e9f13ae0e9 100644 --- a/packages/language-server/tests/server.ts +++ b/packages/language-server/tests/server.ts @@ -69,40 +69,44 @@ export async function getLanguageServer(): Promise<{ 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, - }, - ], - }, - }); - if (!res.success) { - throw new Error(res.body); + open: async (uri, languageId, content) => { + if (uri.startsWith('file://')) { + const res = await tsserver.message({ + seq: seq++, + type: 'request', + command: 'updateOpen', + arguments: { + changedFiles: [], + closedFiles: [], + openFiles: [ + { + file: URI.parse(uri).fsPath, + fileContent: content, + }, + ], + }, + }); + if (!res.success) { + throw new Error(res.body); + } } 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); + close: async uri => { + if (uri.startsWith('file://')) { + 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); }, From d55f09addda1ddec35ee925263e7aca5183bb0b3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 6 Dec 2025 23:00:26 +0800 Subject: [PATCH 6/7] Update languagePlugin.ts --- packages/language-core/lib/languagePlugin.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/language-core/lib/languagePlugin.ts b/packages/language-core/lib/languagePlugin.ts index 4876161173..5b33fb1a25 100644 --- a/packages/language-core/lib/languagePlugin.ts +++ b/packages/language-core/lib/languagePlugin.ts @@ -60,8 +60,7 @@ export function createVueLanguagePlugin( createVirtualCode(scriptId, languageId, snapshot) { const fileName = asFileName(scriptId); if (plugins.some(plugin => plugin.isValidFile?.(fileName, languageId))) { - const codeKey = getFileRegistryKey(scriptId, fileName); - const code = fileRegistry.get(codeKey); + const code = fileRegistry.get(String(scriptId)); if (code) { code.update(snapshot); return code; @@ -75,7 +74,7 @@ export function createVueLanguagePlugin( plugins, ts, ); - fileRegistry.set(codeKey, code); + fileRegistry.set(String(scriptId), code); return code; } } @@ -85,8 +84,7 @@ export function createVueLanguagePlugin( return code; }, disposeVirtualCode(scriptId) { - const fileName = asFileName(scriptId); - fileRegistry.delete(getFileRegistryKey(scriptId, fileName)); + fileRegistry.delete(String(scriptId)); }, typescript: { extraFileExtensions: getAllExtensions(vueCompilerOptions) @@ -126,13 +124,3 @@ export function getAllExtensions(options: VueCompilerOptions) { ] as const).flatMap(key => options[key])), ]; } - -function getFileRegistryKey(scriptId: T, fileName: string) { - if (typeof scriptId === 'object' && scriptId) { - const scheme = (scriptId as { scheme?: string }).scheme; - if (scheme && scheme !== 'file') { - return `${scheme}:${fileName}`; - } - } - return fileName; -} From 410da2a97397d57a7e274281ffe76e27ed352687 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 6 Dec 2025 23:26:24 +0800 Subject: [PATCH 7/7] chore: revert force call `updateOpen` --- packages/language-server/lib/server.ts | 32 -------------------------- packages/typescript-plugin/index.ts | 3 --- 2 files changed, 35 deletions(-) diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index 98f3fbea04..60ae5b91d4 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -175,27 +175,6 @@ export function startServer(ts: typeof import('typescript')) { version: packageJson.version, }; - // #5572 - server.documents.onDidOpen(async ({ document }) => { - const fileDocument = findFileDocumentForUpdate(document.uri); - if (fileDocument) { - const uri = URI.parse(fileDocument.uri); - await sendTsServerRequest( - '_vue:' + ts.server.protocol.CommandTypes.UpdateOpen, - { - openFiles: [ - { - file: uri.fsPath.replace(/\\/g, '/'), - fileContent: fileDocument.getText(), - }, - ], - changedFiles: [], - closedFiles: [], - } satisfies ts.server.protocol.UpdateOpenRequestArgs, - ); - } - }); - return result; async function sendTsServerRequest(command: string, args: any): Promise { @@ -240,17 +219,6 @@ export function startServer(ts: typeof import('typescript')) { {}, ); } - - function findFileDocumentForUpdate(docUri: string) { - const uri = URI.parse(docUri); - if (uri.scheme === 'file') { - return; - } - return server.documents.all().find(document => { - const documentUri = URI.parse(document.uri); - return documentUri.scheme === 'file' && documentUri.fsPath === uri.fsPath; - }); - } }); connection.onInitialized(server.initialized); diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 8fec9bb0c9..c082cce47a 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -102,9 +102,6 @@ export = createLanguageServicePlugin( session.addProtocolHandler('_vue:quickinfo', request => { return handlers.get('quickinfo')!(request); }); - session.addProtocolHandler('_vue:updateOpen', request => { - return handlers.get('updateOpen')!(request); - }); session.addProtocolHandler( '_vue:collectExtractProps', request => {