From 6fddeffa5d21c44f16d60b0ea5deddc2b8cec11b Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 01:19:06 +0800 Subject: [PATCH 01/16] feat: incremental ts script snapshot --- packages/vue-component-meta/src/index.ts | 8 +- packages/vue-language-core/src/lsContext.ts | 50 ++-- packages/vue-language-core/src/sourceFile.ts | 43 ++-- packages/vue-language-server/src/common.ts | 7 +- .../src/features/customFeatures.ts | 2 - .../src/features/documentFeatures.ts | 5 +- packages/vue-language-server/src/project.ts | 73 +++--- packages/vue-language-server/src/projects.ts | 31 ++- packages/vue-language-server/src/snapshots.ts | 237 ++++++++++++++++++ packages/vue-language-server/src/utils.ts | 19 -- .../src/documentFeatures/format.ts | 4 +- .../src/documentService.ts | 4 +- tsconfig.base.json | 2 +- 13 files changed, 349 insertions(+), 136 deletions(-) create mode 100644 packages/vue-language-server/src/snapshots.ts delete mode 100644 packages/vue-language-server/src/utils.ts diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts index 167f9d400..8de2c0251 100644 --- a/packages/vue-component-meta/src/index.ts +++ b/packages/vue-component-meta/src/index.ts @@ -175,7 +175,7 @@ export function createComponentMetaChecker(tsconfigPath: string, checkerOptions: const snapshot = host.getScriptSnapshot(componentPath)!; const vueDefaults = componentPath.endsWith('.vue') && exportName === 'default' - ? readVueComponentDefaultProps(core, snapshot.getText(0, snapshot.getLength()), printer) + ? readVueComponentDefaultProps(core, snapshot, printer) : {}; const tsDefaults = !componentPath.endsWith('.vue') ? readTsComponentDefaultProps( componentPath.substring(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx @@ -458,7 +458,7 @@ function createSchemaResolvers(typeChecker: ts.TypeChecker, symbolNode: ts.Expre }; } -function readVueComponentDefaultProps(core: vue.LanguageContext, vueFileText: string, printer: ts.Printer | undefined) { +function readVueComponentDefaultProps(core: vue.LanguageContext, vueFileScript: ts.IScriptSnapshot, printer: ts.Printer | undefined) { let result: Record = {}; scriptSetupWorker(); @@ -468,7 +468,7 @@ function readVueComponentDefaultProps(core: vue.LanguageContext, vueFileText: st function scriptSetupWorker() { - const vueSourceFile = vue.createSourceFile('/tmp.vue', vueFileText, {}, ts, core.plugins); + const vueSourceFile = vue.createSourceFile('/tmp.vue', vueFileScript, {}, ts, core.plugins); const descriptor = vueSourceFile.sfc; const scriptSetupRanges = descriptor.scriptSetupAst ? parseScriptSetupRanges(ts, descriptor.scriptSetupAst) : undefined; @@ -520,7 +520,7 @@ function readVueComponentDefaultProps(core: vue.LanguageContext, vueFileText: st function scriptWorker() { - const vueSourceFile = vue.createSourceFile('/tmp.vue', vueFileText, {}, ts, core.plugins); + const vueSourceFile = vue.createSourceFile('/tmp.vue', vueFileScript, {}, ts, core.plugins); const descriptor = vueSourceFile.sfc; if (descriptor.script) { diff --git a/packages/vue-language-core/src/lsContext.ts b/packages/vue-language-core/src/lsContext.ts index 538a7a9d8..11b606bf7 100644 --- a/packages/vue-language-core/src/lsContext.ts +++ b/packages/vue-language-core/src/lsContext.ts @@ -2,7 +2,7 @@ import { posix as path } from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { LanguageServiceHost, VueCompilerOptions } from './types'; import * as localTypes from './utils/localTypes'; -import { createSourceFile, EmbeddedFile, VueLanguagePlugin } from './sourceFile'; +import { createSourceFile, EmbeddedFile, SourceFile, VueLanguagePlugin } from './sourceFile'; import { createDocumentRegistry } from './documentRegistry'; import * as useHtmlFilePlugin from './plugins/file-html'; @@ -93,6 +93,7 @@ export function createLanguageContext( const sharedTypesScript = ts.ScriptSnapshot.fromString(localTypes.getTypesCode(vueCompilerOptions.target)); const scriptSnapshots = new Map(); const fileVersions = new WeakMap(); + const vueFileVersions = new WeakMap(); const _tsHost: Partial = { fileExists: host.fileExists ? fileName => { @@ -117,7 +118,7 @@ export function createLanguageContext( if (scriptSnapshot) { documentRegistry.set(vueFileName, createSourceFile( vueFileName, - scriptSnapshot.getText(0, scriptSnapshot.getLength()), + scriptSnapshot, vueCompilerOptions, ts, plugins, @@ -209,16 +210,17 @@ export function createLanguageContext( // .vue for (const vueFile of documentRegistry.getAll()) { - const newSnapshot = host.getScriptSnapshot(vueFile.fileName); - if (!newSnapshot) { - // delete - fileNamesToRemove.push(vueFile.fileName); - } - else { - // update - if (vueFile.text !== newSnapshot.getText(0, newSnapshot.getLength())) { + const newVersion = host.getScriptVersion(vueFile.fileName); + if (vueFileVersions.get(vueFile) !== newVersion) { + vueFileVersions.set(vueFile, newVersion); + if (host.getScriptSnapshot(vueFile.fileName)) { + // update fileNamesToUpdate.push(vueFile.fileName); } + else { + // delete + fileNamesToRemove.push(vueFile.fileName); + } } } @@ -230,19 +232,18 @@ export function createLanguageContext( } // .ts / .js / .d.ts / .json ... - for (const tsFileVersion of tsFileVersions) { - if (!tsFileNames.has(tsFileVersion[0]) && !host.getScriptSnapshot(tsFileVersion[0])) { - // delete - tsFileVersions.delete(tsFileVersion[0]); - tsFileUpdated = true; - } - else { - // update - const newVersion = host.getScriptVersion(tsFileVersion[0]); - if (tsFileVersion[1] !== newVersion) { - tsFileVersions.set(tsFileVersion[0], newVersion); - tsFileUpdated = true; + for (const [oldTsFileName, oldTsFileVersion] of [...tsFileVersions]) { + const newVersion = host.getScriptVersion(oldTsFileName); + if (oldTsFileVersion !== newVersion) { + if (!tsFileNames.has(oldTsFileName) && !host.getScriptSnapshot(oldTsFileName)) { + // delete + tsFileVersions.delete(oldTsFileName); } + else { + // update + tsFileVersions.set(oldTsFileName, newVersion); + } + tsFileUpdated = true; } } @@ -272,12 +273,11 @@ export function createLanguageContext( } const sourceFile = documentRegistry.get(fileName); - const scriptText = scriptSnapshot.getText(0, scriptSnapshot.getLength()); if (!sourceFile) { documentRegistry.set(fileName, createSourceFile( fileName, - scriptText, + scriptSnapshot, vueCompilerOptions, ts, plugins, @@ -297,7 +297,7 @@ export function createLanguageContext( } } - sourceFile.text = scriptText; + sourceFile.update(scriptSnapshot); if (!tsFileUpdated) { for (const embedded of sourceFile.allEmbeddeds) { diff --git a/packages/vue-language-core/src/sourceFile.ts b/packages/vue-language-core/src/sourceFile.ts index d774d8167..ca479fcfd 100644 --- a/packages/vue-language-core/src/sourceFile.ts +++ b/packages/vue-language-core/src/sourceFile.ts @@ -82,12 +82,14 @@ export interface EmbeddedFile { export function createSourceFile( fileName: string, - _content: string, + scriptSnapshot: ts.IScriptSnapshot, vueCompilerOptions: VueCompilerOptions, ts: typeof import('typescript/lib/tsserverlibrary'), plugins: ReturnType[], ) { + let lastScriptSnapshot: ts.IScriptSnapshot | undefined; + // refs const fileContent = ref(''); const sfc = reactive({ @@ -147,7 +149,6 @@ export function createSourceFile( errors.push(err); } - if (ast || errors.length) { return { errors, @@ -321,16 +322,14 @@ export function createSourceFile( } }); - update(_content); + update(scriptSnapshot); return { fileName, get text() { return fileContent.value; }, - set text(value) { - update(value); - }, + update, get compiledSFCTemplate() { return compiledSFCTemplate.value; }, @@ -399,12 +398,16 @@ export function createSourceFile( } return range; } - function update(newContent: string) { + function update(newScriptSnapshot: ts.IScriptSnapshot) { - if (fileContent.value === newContent) - return; + const change = lastScriptSnapshot ? newScriptSnapshot.getChangeRange(lastScriptSnapshot) : undefined; + lastScriptSnapshot = newScriptSnapshot; + + if (change) { + // TODO + } - fileContent.value = newContent; + fileContent.value = newScriptSnapshot.getText(0, newScriptSnapshot.getLength()); // TODO: wait for https://github.com/vuejs/core/pull/5912 if (parsedSfc.value) { @@ -419,8 +422,8 @@ export function createSourceFile( const newData: Sfc['template'] | null = block ? { tag: 'template', - start: newContent.substring(0, block.loc.start.offset).lastIndexOf('<'), - end: block.loc.end.offset + newContent.substring(block.loc.end.offset).indexOf('>') + 1, + start: fileContent.value.substring(0, block.loc.start.offset).lastIndexOf('<'), + end: block.loc.end.offset + fileContent.value.substring(block.loc.end.offset).indexOf('>') + 1, startTagEnd: block.loc.start.offset, endTagStart: block.loc.end.offset, content: block.content, @@ -438,8 +441,8 @@ export function createSourceFile( const newData: Sfc['script'] | null = block ? { tag: 'script', - start: newContent.substring(0, block.loc.start.offset).lastIndexOf('<'), - end: block.loc.end.offset + newContent.substring(block.loc.end.offset).indexOf('>') + 1, + start: fileContent.value.substring(0, block.loc.start.offset).lastIndexOf('<'), + end: block.loc.end.offset + fileContent.value.substring(block.loc.end.offset).indexOf('>') + 1, startTagEnd: block.loc.start.offset, endTagStart: block.loc.end.offset, content: block.content, @@ -458,8 +461,8 @@ export function createSourceFile( const newData: Sfc['scriptSetup'] | null = block ? { tag: 'scriptSetup', - start: newContent.substring(0, block.loc.start.offset).lastIndexOf('<'), - end: block.loc.end.offset + newContent.substring(block.loc.end.offset).indexOf('>') + 1, + start: fileContent.value.substring(0, block.loc.start.offset).lastIndexOf('<'), + end: block.loc.end.offset + fileContent.value.substring(block.loc.end.offset).indexOf('>') + 1, startTagEnd: block.loc.start.offset, endTagStart: block.loc.end.offset, content: block.content, @@ -479,8 +482,8 @@ export function createSourceFile( const block = blocks[i]; const newData: Sfc['styles'][number] = { tag: 'style', - start: newContent.substring(0, block.loc.start.offset).lastIndexOf('<'), - end: block.loc.end.offset + newContent.substring(block.loc.end.offset).indexOf('>') + 1, + start: fileContent.value.substring(0, block.loc.start.offset).lastIndexOf('<'), + end: block.loc.end.offset + fileContent.value.substring(block.loc.end.offset).indexOf('>') + 1, startTagEnd: block.loc.start.offset, endTagStart: block.loc.end.offset, content: block.content, @@ -506,8 +509,8 @@ export function createSourceFile( const block = blocks[i]; const newData: Sfc['customBlocks'][number] = { tag: 'customBlock', - start: newContent.substring(0, block.loc.start.offset).lastIndexOf('<'), - end: block.loc.end.offset + newContent.substring(block.loc.end.offset).indexOf('>') + 1, + start: fileContent.value.substring(0, block.loc.start.offset).lastIndexOf('<'), + end: block.loc.end.offset + fileContent.value.substring(block.loc.end.offset).indexOf('>') + 1, startTagEnd: block.loc.start.offset, endTagStart: block.loc.end.offset, content: block.content, diff --git a/packages/vue-language-server/src/common.ts b/packages/vue-language-server/src/common.ts index cabd62a76..6d027a9ba 100644 --- a/packages/vue-language-server/src/common.ts +++ b/packages/vue-language-server/src/common.ts @@ -1,5 +1,4 @@ import * as shared from '@volar/shared'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import * as vue from '@volar/vue-language-service'; @@ -7,6 +6,7 @@ import { createLsConfigs } from './configHost'; import { getInferredCompilerOptions } from './inferredCompilerOptions'; import { createProjects } from './projects'; import type { FileSystemProvider } from 'vscode-html-languageservice'; +import { createSnapshots } from './snapshots'; export interface RuntimeEnvironment { loadTypescript: (initOptions: shared.ServerInitializationOptions) => typeof import('typescript/lib/tsserverlibrary'), @@ -95,7 +95,7 @@ export function createLanguageServer( params.capabilities, ); - (await import('./features/customFeatures')).register(connection, documents, projects); + (await import('./features/customFeatures')).register(connection, projects); (await import('./features/languageFeatures')).register(connection, projects, options.languageFeatures, params); (await import('./registers/registerlanguageFeatures')).register(options.languageFeatures!, vue.getSemanticTokenLegend(), result.capabilities, languageConfigs); } @@ -109,8 +109,7 @@ export function createLanguageServer( }); connection.listen(); - const documents = new vscode.TextDocuments(TextDocument); - documents.listen(connection); + const documents = createSnapshots(connection); } export function loadCustomPlugins(dir: string) { diff --git a/packages/vue-language-server/src/features/customFeatures.ts b/packages/vue-language-server/src/features/customFeatures.ts index 936b5be3b..ac6486306 100644 --- a/packages/vue-language-server/src/features/customFeatures.ts +++ b/packages/vue-language-server/src/features/customFeatures.ts @@ -1,13 +1,11 @@ import * as shared from '@volar/shared'; import * as path from 'upath'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver'; import type { Projects } from '../projects'; import * as vue from '@volar/vue-language-core'; export function register( connection: vscode.Connection, - documents: vscode.TextDocuments, projects: Projects, ) { connection.onRequest(shared.D3Request.type, async handler => { diff --git a/packages/vue-language-server/src/features/documentFeatures.ts b/packages/vue-language-server/src/features/documentFeatures.ts index 9ca1ce446..74973ea5e 100644 --- a/packages/vue-language-server/src/features/documentFeatures.ts +++ b/packages/vue-language-server/src/features/documentFeatures.ts @@ -2,10 +2,11 @@ import * as shared from '@volar/shared'; import type * as vscode from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as vue from '@volar/vue-language-service'; +import { createSnapshots } from '../snapshots'; export function register( connection: vscode.Connection, - documents: vscode.TextDocuments, + documents: ReturnType, vueDs: vue.DocumentService, allowedLanguageIds: string[] = [ 'vue', @@ -67,7 +68,7 @@ export function register( }); function worker(uri: string, cb: (document: TextDocument) => T) { - const document = documents.get(uri); + const document = documents.data.uriGet(uri)?.getDocument(); if (document && allowedLanguageIds.includes(document.languageId)) { return cb(document); } diff --git a/packages/vue-language-server/src/project.ts b/packages/vue-language-server/src/project.ts index afc84e462..2876f50c3 100644 --- a/packages/vue-language-server/src/project.ts +++ b/packages/vue-language-server/src/project.ts @@ -1,11 +1,10 @@ import * as shared from '@volar/shared'; import * as vue from '@volar/vue-language-service'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver'; import type { createLsConfigs } from './configHost'; -import { getDocumentSafely } from './utils'; import { LanguageConfigs, loadCustomPlugins, RuntimeEnvironment } from './common'; +import { createSnapshots } from './snapshots'; export interface Project extends ReturnType { } @@ -18,7 +17,7 @@ export async function createProject( rootPath: string, tsConfig: string | ts.CompilerOptions, tsLocalized: ts.MapLike | undefined, - documents: vscode.TextDocuments, + documents: ReturnType, connection: vscode.Connection, lsConfigs: ReturnType | undefined, ) { @@ -120,13 +119,7 @@ export async function createProject( typeRootVersion++; // TODO: check changed in node_modules? } } - async function onDocumentUpdated(document: TextDocument) { - - const script = scripts.uriGet(document.uri); - if (script) { - script.version = document.version; - } - + async function onDocumentUpdated() { projectVersion++; } function createLanguageServiceHost() { @@ -169,29 +162,44 @@ export async function createProject( return host; function getScriptVersion(fileName: string) { + + const doc = documents.data.fsPathGet(fileName); + if (doc) { + return doc.version.toString(); + } + return scripts.fsPathGet(fileName)?.version.toString() ?? ''; } function getScriptSnapshot(fileName: string) { + + const doc = documents.data.fsPathGet(fileName); + if (doc) { + return doc.getSnapshot(); + } + const script = scripts.fsPathGet(fileName); if (script && script.snapshotVersion === script.version) { return script.snapshot; } - const text = getScriptText(documents, fileName, projectSys); - if (text !== undefined) { - const snapshot = ts.ScriptSnapshot.fromString(text); - if (script) { - script.snapshot = snapshot; - script.snapshotVersion = script.version; - } - else { - scripts.fsPathSet(fileName, { - version: -1, - fileName: fileName, - snapshot: snapshot, - snapshotVersion: -1, - }); + + if (projectSys.fileExists(fileName)) { + const text = projectSys.readFile(fileName, 'utf8'); + if (text !== undefined) { + const snapshot = ts.ScriptSnapshot.fromString(text); + if (script) { + script.snapshot = snapshot; + script.snapshotVersion = script.version; + } + else { + scripts.fsPathSet(fileName, { + version: -1, + fileName: fileName, + snapshot: snapshot, + snapshotVersion: -1, + }); + } + return snapshot; } - return snapshot; } } } @@ -225,18 +233,3 @@ export async function createProject( } } } - -export function getScriptText( - documents: vscode.TextDocuments, - fileName: string, - sys: typeof import('typescript/lib/tsserverlibrary')['sys'], -) { - const uri = shared.fsPathToUri(fileName); - const doc = getDocumentSafely(documents, uri); - if (doc) { - return doc.getText(); - } - if (sys.fileExists(fileName)) { - return sys.readFile(fileName, 'utf8'); - } -} diff --git a/packages/vue-language-server/src/projects.ts b/packages/vue-language-server/src/projects.ts index 2f1ee388e..63620ecf6 100644 --- a/packages/vue-language-server/src/projects.ts +++ b/packages/vue-language-server/src/projects.ts @@ -1,12 +1,11 @@ import * as shared from '@volar/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as path from 'upath'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver'; import { createProject, Project } from './project'; import type { createLsConfigs } from './configHost'; -import { getDocumentSafely } from './utils'; import { LanguageConfigs, RuntimeEnvironment } from './common'; +import { createSnapshots } from './snapshots'; export interface Projects extends ReturnType { } @@ -19,7 +18,7 @@ export function createProjects( ts: typeof import('typescript/lib/tsserverlibrary'), tsLocalized: ts.MapLike | undefined, options: shared.ServerInitializationOptions, - documents: vscode.TextDocuments, + documents: ReturnType, connection: vscode.Connection, lsConfigs: ReturnType | undefined, getInferredCompilerOptions: () => Promise, @@ -51,31 +50,31 @@ export function createProjects( )); } - documents.onDidOpen(async change => { + documents.onDidOpen(params => { lastOpenDoc = { - uri: change.document.uri, + uri: params.textDocument.uri, time: Date.now(), }; }); - documents.onDidChangeContent(async change => { + documents.onDidChangeContent(async params => { const req = ++documentUpdatedReq; - await waitForOnDidChangeWatchedFiles(change.document.uri); + await waitForOnDidChangeWatchedFiles(params.textDocument.uri); for (const workspace of workspaces.values()) { const projects = [...workspace.projects.values(), workspace.getInferredProjectDontCreate()].filter(shared.notEmpty); for (const project of projects) { - (await project).onDocumentUpdated(change.document); + (await project).onDocumentUpdated(); } } if (req === documentUpdatedReq) { - updateDiagnostics(change.document.uri); + updateDiagnostics(params.textDocument.uri); } }); - documents.onDidClose(change => { - connection.sendDiagnostics({ uri: change.document.uri, diagnostics: [] }); + documents.onDidClose(params => { + connection.sendDiagnostics({ uri: params.textDocument.uri, diagnostics: [] }); }); connection.onDidChangeWatchedFiles(onDidChangeWatchedFiles); @@ -187,10 +186,10 @@ export function createProjects( return _isCancel; }; - const changeDocs = docUri ? [getDocumentSafely(documents, docUri)].filter(shared.notEmpty) : []; - const otherDocs = documents.all().filter(doc => doc.uri !== docUri); + const changeDoc = docUri ? documents.data.uriGet(docUri) : undefined; + const otherDocs = [...documents.data.values()].filter(doc => doc !== changeDoc); - for (const changeDoc of changeDocs) { + if (changeDoc) { await shared.sleep(delay ?? 200); @@ -258,7 +257,7 @@ export function createProjects( } } function clearDiagnostics() { - for (const doc of documents.all()) { + for (const doc of documents.data.values()) { connection.sendDiagnostics({ uri: doc.uri, diagnostics: [] }); } } @@ -271,7 +270,7 @@ function createWorkspace( ts: typeof import('typescript/lib/tsserverlibrary'), tsLocalized: ts.MapLike | undefined, options: shared.ServerInitializationOptions, - documents: vscode.TextDocuments, + documents: ReturnType, connection: vscode.Connection, lsConfigs: ReturnType | undefined, getInferredCompilerOptions: () => Promise, diff --git a/packages/vue-language-server/src/snapshots.ts b/packages/vue-language-server/src/snapshots.ts new file mode 100644 index 000000000..4830e152f --- /dev/null +++ b/packages/vue-language-server/src/snapshots.ts @@ -0,0 +1,237 @@ +import { TextDocument } from 'vscode-languageserver-textdocument'; +import * as vscode from 'vscode-languageserver'; +import * as shared from '@volar/shared'; +import type * as ts from 'typescript/lib/tsserverlibrary'; + +interface IncrementalScriptSnapshotVersion { + // if set, it mean this change is applyed to document + changeRange: ts.TextChangeRange | undefined, + version: number, + contentChanges: { + range: vscode.Range; + text: string; + }[], + snapshot: WeakRef | undefined, +} + +class IncrementalScriptSnapshot { + + private document: TextDocument; + private snapshot: ts.IScriptSnapshot; + uri: string; + versions: IncrementalScriptSnapshotVersion[]; + + constructor(uri: string, languageId: string, version: number, text: string) { + this.uri = uri; + this.document = TextDocument.create(uri, languageId, version, text); + this.snapshot = { + getLength: () => text.length, + getText: (start, end) => text.substring(start, end), + getChangeRange: () => undefined, + }; + this.versions = []; + } + + get version() { + if (this.versions.length) { + return this.versions[this.versions.length - 1].version; + } + return this.document.version; + } + + update(params: vscode.DidChangeTextDocumentParams) { + TextDocument.update(this.document, params.contentChanges, params.textDocument.version); + this.versions.length = 0; + } + + getSnapshot() { + + if (this.versions.length) { + this.clearUnReferenceVersions(); + + const lastChange = this.versions[this.versions.length - 1]; + if (!lastChange.snapshot) { + this.applyVersionToRootDocument(lastChange.version, false); + const text = this.document.getText(); + const cache = new WeakMap(); + const snapshot: ts.IScriptSnapshot = { + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: (oldSnapshot) => { + if (!cache.has(oldSnapshot)) { + const start = oldSnapshot === this.snapshot ? 0 : (this.versions.findIndex(change => change.snapshot?.deref() === oldSnapshot) + 1); + const end = this.versions.indexOf(lastChange) + 1; + if (start >= 0 && end >= 0) { + const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); + const result = combineContinuousChangeRanges(changeRanges); + cache.set(oldSnapshot, result); + } + else { + cache.set(oldSnapshot, undefined); + } + } + return cache.get(oldSnapshot); + }, + }; + lastChange.snapshot = new WeakRef(snapshot); + } + + return lastChange.snapshot.deref()!; + } + + return this.snapshot; + } + + getDocument() { + + if (this.versions.length) { + this.clearUnReferenceVersions(); + + const lastChange = this.versions[this.versions.length - 1]; + if (!lastChange.changeRange) { + this.applyVersionToRootDocument(lastChange.version, false); + } + } + + return this.document; + } + + clearUnReferenceVersions() { + let versionToApply: number | undefined; + for (let i = 0; i < this.versions.length - 1; i++) { + const change = this.versions[i]; + if (!change.snapshot?.deref()) { + versionToApply = change.version; + } + else { + break; + } + } + if (versionToApply !== undefined) { + this.applyVersionToRootDocument(versionToApply, true); + } + } + + applyVersionToRootDocument(version: number, removeBeforeVersions: boolean) { + let removeEnd = -1; + for (let i = 0; i < this.versions.length; i++) { + const change = this.versions[i]; + if (change.version > version) { + break; + } + if (!change.changeRange) { + const changeRanges: ts.TextChangeRange[] = change.contentChanges.map(edit => ({ + span: { + start: this.document.offsetAt(edit.range.start), + length: this.document.offsetAt(edit.range.end) - this.document.offsetAt(edit.range.start), + }, + newLength: edit.text.length, + })); + change.changeRange = combineMultiLineChangeRanges(changeRanges); + TextDocument.update(this.document, change.contentChanges, change.version); + } + removeEnd = i + 1; + } + if (removeBeforeVersions && removeEnd >= 1) { + this.versions.splice(0, removeEnd); + } + } +} + +function combineContinuousChangeRanges(changeRanges: ts.TextChangeRange[]) { + if (changeRanges.length === 1) { + return changeRanges[0]; + } + const changeRange: ts.TextChangeRange = { + span: { + start: changeRanges[0].span.start, + length: changeRanges[0].span.length, + }, + newLength: changeRanges[0].newLength, + }; + for (let i = 1; i < changeRanges.length; i++) { + const nextChangeRange = changeRanges[i]; + if (nextChangeRange.span.start === changeRange.span.start + changeRange.newLength) { // is continuous input + changeRange.span.length += nextChangeRange.span.length; + changeRange.newLength += nextChangeRange.newLength; + } + else { + return; + } + } + return changeRange; +} + +function combineMultiLineChangeRanges(changeRanges: ts.TextChangeRange[]) { + if (changeRanges.length === 1) { + return changeRanges[0]; + } + const firstChangeRange = changeRanges.sort((a, b) => a.span.start - b.span.start)[0]; + const lastChangeRange = changeRanges.sort((a, b) => b.span.start - a.span.start)[0]; + const fullStart = firstChangeRange.span.start; + const fullEnd = lastChangeRange.span.start + lastChangeRange.span.length; + let newLength = fullEnd - fullStart; + for (const changeRange of changeRanges) { + newLength = newLength - changeRange.span.length + changeRange.newLength; + } + const lastChange: ts.TextChangeRange = { + span: { + start: firstChangeRange.span.start, + length: lastChangeRange.span.start + lastChangeRange.span.length - firstChangeRange.span.start, + }, + newLength, + }; + return lastChange; +} + +export function createSnapshots(connection: vscode.Connection) { + + const snapshots = shared.createPathMap(); + const onDidOpens: ((params: vscode.DidOpenTextDocumentParams) => void)[] = []; + const onDidChangeContents: ((params: vscode.DidChangeTextDocumentParams) => void)[] = []; + const onDidCloses: ((params: vscode.DidCloseTextDocumentParams) => void)[] = []; + + connection.onDidOpenTextDocument(params => { + snapshots.uriSet(params.textDocument.uri, new IncrementalScriptSnapshot( + params.textDocument.uri, + params.textDocument.languageId, + params.textDocument.version, + params.textDocument.text, + )); + for (const cb of onDidOpens) { + cb(params); + } + }); + connection.onDidChangeTextDocument(params => { + const incrementalSnapshot = snapshots.uriGet(params.textDocument.uri); + if (incrementalSnapshot) { + if (params.contentChanges.every(vscode.TextDocumentContentChangeEvent.isIncremental)) { + incrementalSnapshot.versions.push({ + changeRange: undefined, + contentChanges: params.contentChanges, + version: params.textDocument.version, + snapshot: undefined, + }); + } + else { + incrementalSnapshot.update(params); + } + } + for (const cb of onDidChangeContents) { + cb(params); + } + }); + connection.onDidCloseTextDocument(params => { + snapshots.uriDelete(params.textDocument.uri); + for (const cb of onDidCloses) { + cb(params); + } + }); + + return { + data: snapshots, + onDidOpen: (cb: typeof onDidOpens[number]) => onDidOpens.push(cb), + onDidChangeContent: (cb: typeof onDidChangeContents[number]) => onDidChangeContents.push(cb), + onDidClose: (cb: typeof onDidCloses[number]) => onDidCloses.push(cb), + }; +} diff --git a/packages/vue-language-server/src/utils.ts b/packages/vue-language-server/src/utils.ts deleted file mode 100644 index 336a2152e..000000000 --- a/packages/vue-language-server/src/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type * as vscode from 'vscode-languageserver'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; -import * as shared from '@volar/shared'; - -export function getDocumentSafely(documents: vscode.TextDocuments, uri: string) { - - const normalizeUri = shared.normalizeUri(uri); - const document = documents.get(uri) ?? documents.get(normalizeUri); - - if (document) { - return document; - } - - for (const document of documents.all()) { - if (shared.normalizeUri(document.uri).toLowerCase() === normalizeUri.toLowerCase()) { - return document; - } - } -} diff --git a/packages/vue-language-service/src/documentFeatures/format.ts b/packages/vue-language-service/src/documentFeatures/format.ts index 480d0c568..ccd5e8296 100644 --- a/packages/vue-language-service/src/documentFeatures/format.ts +++ b/packages/vue-language-service/src/documentFeatures/format.ts @@ -6,6 +6,8 @@ import { EmbeddedDocumentSourceMap, VueDocument } from '../vueDocuments'; export function register(context: DocumentServiceRuntimeContext) { + const ts = context.typescript; + return async ( document: TextDocument, options: vscode.FormattingOptions, @@ -155,7 +157,7 @@ export function register(context: DocumentServiceRuntimeContext) { function tryUpdateVueDocument() { if (vueDocument) { - vueDocument.file.text = document.getText(); + vueDocument.file.update(ts.ScriptSnapshot.fromString(document.getText())); } } diff --git a/packages/vue-language-service/src/documentService.ts b/packages/vue-language-service/src/documentService.ts index 685c64dfc..1b2cfeb89 100644 --- a/packages/vue-language-service/src/documentService.ts +++ b/packages/vue-language-service/src/documentService.ts @@ -123,14 +123,14 @@ export function getDocumentService( if (vueDoc) { - vueDoc.file.text = document.getText(); + vueDoc.file.update(ts.ScriptSnapshot.fromString(document.getText())); return vueDoc; } const vueFile = vue.createSourceFile( '/untitled.' + shared.languageIdToSyntax(document.languageId), - document.getText(), + ts.ScriptSnapshot.fromString(document.getText()), {}, context.typescript, vuePlugins, diff --git a/tsconfig.base.json b/tsconfig.base.json index 09d340b79..93e6e1463 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -3,7 +3,7 @@ "target": "ES2016", "lib": [ "WebWorker", - "ES2020" + "ES2021", ], "module": "commonjs", "moduleResolution": "node", From cda4d52ed19e133811f987f0a159a58fc540166d Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 12:36:01 +0800 Subject: [PATCH 02/16] feat: test combineContinuousChangeRanges, combineMultiLineChangeRanges --- packages/vue-language-server/src/snapshots.ts | 13 ++- .../combineContinuousChangeRanges.spec.ts | 89 +++++++++++++++++++ .../combineMultiLineChangeRanges.spec.ts | 26 ++++++ 3 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts create mode 100644 packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts diff --git a/packages/vue-language-server/src/snapshots.ts b/packages/vue-language-server/src/snapshots.ts index 4830e152f..eda770199 100644 --- a/packages/vue-language-server/src/snapshots.ts +++ b/packages/vue-language-server/src/snapshots.ts @@ -63,7 +63,7 @@ class IncrementalScriptSnapshot { const end = this.versions.indexOf(lastChange) + 1; if (start >= 0 && end >= 0) { const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); - const result = combineContinuousChangeRanges(changeRanges); + const result = combineContinuousChangeRanges.apply(changeRanges); cache.set(oldSnapshot, result); } else { @@ -127,7 +127,7 @@ class IncrementalScriptSnapshot { }, newLength: edit.text.length, })); - change.changeRange = combineMultiLineChangeRanges(changeRanges); + change.changeRange = combineMultiLineChangeRanges.apply(changeRanges); TextDocument.update(this.document, change.contentChanges, change.version); } removeEnd = i + 1; @@ -138,7 +138,7 @@ class IncrementalScriptSnapshot { } } -function combineContinuousChangeRanges(changeRanges: ts.TextChangeRange[]) { +export function combineContinuousChangeRanges(...changeRanges: ts.TextChangeRange[]) { if (changeRanges.length === 1) { return changeRanges[0]; } @@ -162,7 +162,7 @@ function combineContinuousChangeRanges(changeRanges: ts.TextChangeRange[]) { return changeRange; } -function combineMultiLineChangeRanges(changeRanges: ts.TextChangeRange[]) { +export function combineMultiLineChangeRanges(...changeRanges: ts.TextChangeRange[]) { if (changeRanges.length === 1) { return changeRanges[0]; } @@ -170,10 +170,7 @@ function combineMultiLineChangeRanges(changeRanges: ts.TextChangeRange[]) { const lastChangeRange = changeRanges.sort((a, b) => b.span.start - a.span.start)[0]; const fullStart = firstChangeRange.span.start; const fullEnd = lastChangeRange.span.start + lastChangeRange.span.length; - let newLength = fullEnd - fullStart; - for (const changeRange of changeRanges) { - newLength = newLength - changeRange.span.length + changeRange.newLength; - } + const newLength = fullEnd - fullStart + (lastChangeRange.newLength - lastChangeRange.span.length); const lastChange: ts.TextChangeRange = { span: { start: firstChangeRange.span.start, diff --git a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts new file mode 100644 index 000000000..5ea5a437a --- /dev/null +++ b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { combineContinuousChangeRanges } from '../out/snapshots'; + +describe(`Test combineContinuousChangeRanges()`, () => { + + it(`12345 -> a12345 -> ab12345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 1, length: 0, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 0, }, newLength: 2 }); + }); + + it(`12345 -> a12345 -> bb12345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 2 }, + )).toEqual({ span: { start: 0, length: 0, }, newLength: 2 }); + }); + + it(`12345 -> 2345 -> b2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 1, }, newLength: 0 }, + { span: { start: 0, length: 0, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 1, }, newLength: 1 }); + }); + + it(`12345 -> a12345 -> b12345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 0, }, newLength: 1 }); + }); + + it(`12345 -> a12345 -> bb2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 2, }, newLength: 2 }, + )).toEqual({ span: { start: 0, length: 1, }, newLength: 2 }); + }); + + it(`12345 -> a2345 -> b2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 1, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 1, }, newLength: 1 }); + }); + + it(`12345 -> 1a2345 -> ba2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 1, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 2, }, newLength: 2 }); + }); + + it(`12345 -> 1a2345 -> bb2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 1, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 2, }, newLength: 2 }, + )).toEqual({ span: { start: 0, length: 2, }, newLength: 2 }); + }); + + it(`12345 -> 1a2345 -> bba2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 1, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 2 }, + )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); + }); + + it(`12345 -> 12a45 -> 1bbb5`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 2, length: 1, }, newLength: 1 }, + { span: { start: 1, length: 3, }, newLength: 3 }, + )).toEqual({ span: { start: 1, length: 3, }, newLength: 3 }); + }); + + it(`12345 -> a12345 -> a1b2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 2, length: 0, }, newLength: 1 }, + )).toBeUndefined(); + }); + + it(`12345 -> 12a345 -> b2a345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 2, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 1 }, + )).toBeUndefined; + }); +}); diff --git a/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts b/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts new file mode 100644 index 000000000..9f52d8205 --- /dev/null +++ b/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { combineMultiLineChangeRanges } from '../out/snapshots'; + +describe(`Test combineMultiLineChangeRanges()`, () => { + + it(`a`, () => { + expect(combineMultiLineChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 5, length: 0, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 5, }, newLength: 6 }); + }); + + it(`b`, () => { + expect(combineMultiLineChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 5, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 6, }, newLength: 6 }); + }); + + it(`c`, () => { + expect(combineMultiLineChangeRanges( + { span: { start: 0, length: 1, }, newLength: 1 }, + { span: { start: 5, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 6, }, newLength: 6 }); + }); +}); From fe61d76524f927d73b2545cb88f77a4f0ec5913c Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 12:56:59 +0800 Subject: [PATCH 03/16] chore: toBeUndefined() --- .../tests/combineContinuousChangeRanges.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts index 5ea5a437a..fef809986 100644 --- a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts +++ b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts @@ -84,6 +84,6 @@ describe(`Test combineContinuousChangeRanges()`, () => { expect(combineContinuousChangeRanges( { span: { start: 2, length: 0, }, newLength: 1 }, { span: { start: 0, length: 1, }, newLength: 1 }, - )).toBeUndefined; + )).toBeUndefined(); }); }); From ece09140774e6a7113de8f5005c4db30e301be3f Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 13:15:16 +0800 Subject: [PATCH 04/16] Update combineContinuousChangeRanges.spec.ts --- .../tests/combineContinuousChangeRanges.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts index fef809986..bc00af8db 100644 --- a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts +++ b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts @@ -77,13 +77,13 @@ describe(`Test combineContinuousChangeRanges()`, () => { expect(combineContinuousChangeRanges( { span: { start: 0, length: 0, }, newLength: 1 }, { span: { start: 2, length: 0, }, newLength: 1 }, - )).toBeUndefined(); + )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); }); it(`12345 -> 12a345 -> b2a345`, () => { expect(combineContinuousChangeRanges( { span: { start: 2, length: 0, }, newLength: 1 }, { span: { start: 0, length: 1, }, newLength: 1 }, - )).toBeUndefined(); + )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); }); }); From b0b68bd2612d3439457a72db5b6b284c28e2a6fc Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 19:44:00 +0800 Subject: [PATCH 05/16] fix: correction test cases --- .../combineContinuousChangeRanges.spec.ts | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts index bc00af8db..ea8457226 100644 --- a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts +++ b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts @@ -10,6 +10,14 @@ describe(`Test combineContinuousChangeRanges()`, () => { )).toEqual({ span: { start: 0, length: 0, }, newLength: 2 }); }); + it(`12345 -> a12345 -> ab12345 -> abc12345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 1, length: 0, }, newLength: 1 }, + { span: { start: 2, length: 0, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 0, }, newLength: 3 }); + }); + it(`12345 -> a12345 -> bb12345`, () => { expect(combineContinuousChangeRanges( { span: { start: 0, length: 0, }, newLength: 1 }, @@ -49,21 +57,21 @@ describe(`Test combineContinuousChangeRanges()`, () => { expect(combineContinuousChangeRanges( { span: { start: 1, length: 0, }, newLength: 1 }, { span: { start: 0, length: 1, }, newLength: 1 }, - )).toEqual({ span: { start: 0, length: 2, }, newLength: 2 }); + )).toEqual({ span: { start: 0, length: 1, }, newLength: 2 }); }); it(`12345 -> 1a2345 -> bb2345`, () => { expect(combineContinuousChangeRanges( { span: { start: 1, length: 0, }, newLength: 1 }, { span: { start: 0, length: 2, }, newLength: 2 }, - )).toEqual({ span: { start: 0, length: 2, }, newLength: 2 }); + )).toEqual({ span: { start: 0, length: 1, }, newLength: 2 }); }); it(`12345 -> 1a2345 -> bba2345`, () => { expect(combineContinuousChangeRanges( { span: { start: 1, length: 0, }, newLength: 1 }, { span: { start: 0, length: 1, }, newLength: 2 }, - )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); + )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); }); it(`12345 -> 12a45 -> 1bbb5`, () => { @@ -73,17 +81,18 @@ describe(`Test combineContinuousChangeRanges()`, () => { )).toEqual({ span: { start: 1, length: 3, }, newLength: 3 }); }); - it(`12345 -> a12345 -> a1b2345`, () => { - expect(combineContinuousChangeRanges( - { span: { start: 0, length: 0, }, newLength: 1 }, - { span: { start: 2, length: 0, }, newLength: 1 }, - )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); - }); + // TODO + // it(`12345 -> a12345 -> a1b2345`, () => { + // expect(combineContinuousChangeRanges( + // { span: { start: 0, length: 0, }, newLength: 1 }, + // { span: { start: 2, length: 0, }, newLength: 1 }, + // )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); + // }); - it(`12345 -> 12a345 -> b2a345`, () => { - expect(combineContinuousChangeRanges( - { span: { start: 2, length: 0, }, newLength: 1 }, - { span: { start: 0, length: 1, }, newLength: 1 }, - )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); - }); + // it(`12345 -> 12a345 -> b2a345`, () => { + // expect(combineContinuousChangeRanges( + // { span: { start: 2, length: 0, }, newLength: 1 }, + // { span: { start: 0, length: 1, }, newLength: 1 }, + // )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); + // }); }); From f6061d261bb28898b14d71d3e6d160d940e07b0b Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 20:57:47 +0800 Subject: [PATCH 06/16] feat: implement combineContinuousChangeRanges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: 引证 <3074608+browsnet@users.noreply.github.com> --- packages/vue-language-server/src/snapshots.ts | 37 +++++++++++-------- .../combineContinuousChangeRanges.spec.ts | 25 ++++++------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/vue-language-server/src/snapshots.ts b/packages/vue-language-server/src/snapshots.ts index eda770199..083ad7413 100644 --- a/packages/vue-language-server/src/snapshots.ts +++ b/packages/vue-language-server/src/snapshots.ts @@ -63,7 +63,7 @@ class IncrementalScriptSnapshot { const end = this.versions.indexOf(lastChange) + 1; if (start >= 0 && end >= 0) { const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); - const result = combineContinuousChangeRanges.apply(changeRanges); + const result = combineContinuousChangeRanges.apply(null, changeRanges); cache.set(oldSnapshot, result); } else { @@ -127,7 +127,7 @@ class IncrementalScriptSnapshot { }, newLength: edit.text.length, })); - change.changeRange = combineMultiLineChangeRanges.apply(changeRanges); + change.changeRange = combineMultiLineChangeRanges.apply(null, changeRanges); TextDocument.update(this.document, change.contentChanges, change.version); } removeEnd = i + 1; @@ -142,26 +142,31 @@ export function combineContinuousChangeRanges(...changeRanges: ts.TextChangeRang if (changeRanges.length === 1) { return changeRanges[0]; } - const changeRange: ts.TextChangeRange = { - span: { - start: changeRanges[0].span.start, - length: changeRanges[0].span.length, - }, - newLength: changeRanges[0].newLength, - }; + let changeRange: ts.TextChangeRange = changeRanges[0]; for (let i = 1; i < changeRanges.length; i++) { const nextChangeRange = changeRanges[i]; - if (nextChangeRange.span.start === changeRange.span.start + changeRange.newLength) { // is continuous input - changeRange.span.length += nextChangeRange.span.length; - changeRange.newLength += nextChangeRange.newLength; - } - else { - return; - } + changeRange = _combineContinuousChangeRanges(changeRange, nextChangeRange); } return changeRange; } +// https://tsplay.dev/w6Paym - @browsnet +function _combineContinuousChangeRanges(a: ts.TextChangeRange, b: ts.TextChangeRange): ts.TextChangeRange { + const aStart = a.span.start; + const aEnd = a.span.start + a.span.length; + const aDiff = a.newLength - a.span.length; + const changeBegin = aStart + Math.min(a.span.length, a.newLength); + const rollback = (start: number) => start > changeBegin ? start - aDiff : start; + const bStart = rollback(b.span.start); + const bEnd = rollback(b.span.start + b.span.length); + const bDiff = b.newLength - b.span.length; + const start = Math.min(aStart, bStart); + const end = Math.max(aEnd, bEnd); + const length = end - start; + const newLength = aDiff + bDiff + length; + return { span: { start, length }, newLength }; +} + export function combineMultiLineChangeRanges(...changeRanges: ts.TextChangeRange[]) { if (changeRanges.length === 1) { return changeRanges[0]; diff --git a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts index ea8457226..c4039de89 100644 --- a/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts +++ b/packages/vue-language-server/tests/combineContinuousChangeRanges.spec.ts @@ -81,18 +81,17 @@ describe(`Test combineContinuousChangeRanges()`, () => { )).toEqual({ span: { start: 1, length: 3, }, newLength: 3 }); }); - // TODO - // it(`12345 -> a12345 -> a1b2345`, () => { - // expect(combineContinuousChangeRanges( - // { span: { start: 0, length: 0, }, newLength: 1 }, - // { span: { start: 2, length: 0, }, newLength: 1 }, - // )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); - // }); + it(`12345 -> a12345 -> a1b2345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 0, length: 0, }, newLength: 1 }, + { span: { start: 2, length: 0, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 1, }, newLength: 3 }); + }); - // it(`12345 -> 12a345 -> b2a345`, () => { - // expect(combineContinuousChangeRanges( - // { span: { start: 2, length: 0, }, newLength: 1 }, - // { span: { start: 0, length: 1, }, newLength: 1 }, - // )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); - // }); + it(`12345 -> 12a345 -> b2a345`, () => { + expect(combineContinuousChangeRanges( + { span: { start: 2, length: 0, }, newLength: 1 }, + { span: { start: 0, length: 1, }, newLength: 1 }, + )).toEqual({ span: { start: 0, length: 2, }, newLength: 3 }); + }); }); From f454377f92a84ce75d53967b6bbcdb709159c8d9 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 14 Aug 2022 21:05:05 +0800 Subject: [PATCH 07/16] fix: cannot update diagnostics on open document --- packages/vue-language-server/src/projects.ts | 31 ++++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/vue-language-server/src/projects.ts b/packages/vue-language-server/src/projects.ts index 63620ecf6..2064aa5f7 100644 --- a/packages/vue-language-server/src/projects.ts +++ b/packages/vue-language-server/src/projects.ts @@ -55,12 +55,27 @@ export function createProjects( uri: params.textDocument.uri, time: Date.now(), }; + onDidChangeContent(params.textDocument.uri); }); documents.onDidChangeContent(async params => { + onDidChangeContent(params.textDocument.uri); + }); + documents.onDidClose(params => { + connection.sendDiagnostics({ uri: params.textDocument.uri, diagnostics: [] }); + }); + connection.onDidChangeWatchedFiles(onDidChangeWatchedFiles); + + return { + workspaces, + getProject, + reloadProject, + }; + + async function onDidChangeContent(uri: string) { const req = ++documentUpdatedReq; - await waitForOnDidChangeWatchedFiles(params.textDocument.uri); + await waitForOnDidChangeWatchedFiles(uri); for (const workspace of workspaces.values()) { const projects = [...workspace.projects.values(), workspace.getInferredProjectDontCreate()].filter(shared.notEmpty); @@ -70,19 +85,9 @@ export function createProjects( } if (req === documentUpdatedReq) { - updateDiagnostics(params.textDocument.uri); + updateDiagnostics(uri); } - }); - documents.onDidClose(params => { - connection.sendDiagnostics({ uri: params.textDocument.uri, diagnostics: [] }); - }); - connection.onDidChangeWatchedFiles(onDidChangeWatchedFiles); - - return { - workspaces, - getProject, - reloadProject, - }; + } async function reloadProject(uri: string) { From 53fc84a04ff8719e7f35073a41f76cbb6d4201f8 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 04:27:08 +0800 Subject: [PATCH 08/16] fix: getChangeRange not working with initial snapshot --- packages/vue-language-server/src/snapshots.ts | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/packages/vue-language-server/src/snapshots.ts b/packages/vue-language-server/src/snapshots.ts index 083ad7413..1f2bae026 100644 --- a/packages/vue-language-server/src/snapshots.ts +++ b/packages/vue-language-server/src/snapshots.ts @@ -17,19 +17,26 @@ interface IncrementalScriptSnapshotVersion { class IncrementalScriptSnapshot { private document: TextDocument; - private snapshot: ts.IScriptSnapshot; uri: string; versions: IncrementalScriptSnapshotVersion[]; constructor(uri: string, languageId: string, version: number, text: string) { this.uri = uri; - this.document = TextDocument.create(uri, languageId, version, text); - this.snapshot = { - getLength: () => text.length, - getText: (start, end) => text.substring(start, end), - getChangeRange: () => undefined, - }; - this.versions = []; + this.document = TextDocument.create(uri, languageId, version - 1, ''); + this.versions = [ + { + changeRange: undefined, + version, + contentChanges: [{ + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + text, + }], + snapshot: undefined, + } + ]; } get version() { @@ -46,51 +53,45 @@ class IncrementalScriptSnapshot { getSnapshot() { - if (this.versions.length) { - this.clearUnReferenceVersions(); - - const lastChange = this.versions[this.versions.length - 1]; - if (!lastChange.snapshot) { - this.applyVersionToRootDocument(lastChange.version, false); - const text = this.document.getText(); - const cache = new WeakMap(); - const snapshot: ts.IScriptSnapshot = { - getText: (start, end) => text.substring(start, end), - getLength: () => text.length, - getChangeRange: (oldSnapshot) => { - if (!cache.has(oldSnapshot)) { - const start = oldSnapshot === this.snapshot ? 0 : (this.versions.findIndex(change => change.snapshot?.deref() === oldSnapshot) + 1); - const end = this.versions.indexOf(lastChange) + 1; - if (start >= 0 && end >= 0) { - const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); - const result = combineContinuousChangeRanges.apply(null, changeRanges); - cache.set(oldSnapshot, result); - } - else { - cache.set(oldSnapshot, undefined); - } + this.clearUnReferenceVersions(); + + const lastChange = this.versions[this.versions.length - 1]; + if (!lastChange.snapshot) { + this.applyVersionToRootDocument(lastChange.version, false); + const text = this.document.getText(); + const cache = new WeakMap(); + const snapshot: ts.IScriptSnapshot = { + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: (oldSnapshot) => { + if (!cache.has(oldSnapshot)) { + const start = this.versions.findIndex(change => change.snapshot?.deref() === oldSnapshot) + 1; + const end = this.versions.indexOf(lastChange) + 1; + if (start >= 0 && end >= 0) { + const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); + const result = combineContinuousChangeRanges.apply(null, changeRanges); + cache.set(oldSnapshot, result); } - return cache.get(oldSnapshot); - }, - }; - lastChange.snapshot = new WeakRef(snapshot); - } - - return lastChange.snapshot.deref()!; + else { + cache.set(oldSnapshot, undefined); + } + } + return cache.get(oldSnapshot); + }, + }; + lastChange.snapshot = new WeakRef(snapshot); } - return this.snapshot; + return lastChange.snapshot.deref()!; } getDocument() { - if (this.versions.length) { - this.clearUnReferenceVersions(); + this.clearUnReferenceVersions(); - const lastChange = this.versions[this.versions.length - 1]; - if (!lastChange.changeRange) { - this.applyVersionToRootDocument(lastChange.version, false); - } + const lastChange = this.versions[this.versions.length - 1]; + if (!lastChange.changeRange) { + this.applyVersionToRootDocument(lastChange.version, false); } return this.document; From 38d348e711c9da8939f55b8c09883605bbba5c64 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 10:02:11 +0800 Subject: [PATCH 09/16] fix: snapshot incremental edit incorrect --- packages/vue-language-server/src/snapshots.ts | 69 +++++++++++-------- .../combineMultiLineChangeRanges.spec.ts | 4 +- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/packages/vue-language-server/src/snapshots.ts b/packages/vue-language-server/src/snapshots.ts index 1f2bae026..e15c2ac5b 100644 --- a/packages/vue-language-server/src/snapshots.ts +++ b/packages/vue-language-server/src/snapshots.ts @@ -4,13 +4,13 @@ import * as shared from '@volar/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; interface IncrementalScriptSnapshotVersion { - // if set, it mean this change is applyed to document + applyed: boolean, changeRange: ts.TextChangeRange | undefined, version: number, contentChanges: { range: vscode.Range; text: string; - }[], + }[] | undefined, snapshot: WeakRef | undefined, } @@ -22,18 +22,13 @@ class IncrementalScriptSnapshot { constructor(uri: string, languageId: string, version: number, text: string) { this.uri = uri; - this.document = TextDocument.create(uri, languageId, version - 1, ''); + this.document = TextDocument.create(uri, languageId, version, text); this.versions = [ { + applyed: true, changeRange: undefined, version, - contentChanges: [{ - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - }, - text, - }], + contentChanges: undefined, snapshot: undefined, } ]; @@ -48,7 +43,15 @@ class IncrementalScriptSnapshot { update(params: vscode.DidChangeTextDocumentParams) { TextDocument.update(this.document, params.contentChanges, params.textDocument.version); - this.versions.length = 0; + this.versions = [ + { + applyed: true, + changeRange: undefined, + version: params.textDocument.version, + contentChanges: undefined, + snapshot: undefined, + } + ]; } getSnapshot() { @@ -68,7 +71,7 @@ class IncrementalScriptSnapshot { const start = this.versions.findIndex(change => change.snapshot?.deref() === oldSnapshot) + 1; const end = this.versions.indexOf(lastChange) + 1; if (start >= 0 && end >= 0) { - const changeRanges = this.versions.slice(start, end).map(change => change.changeRange!); + const changeRanges = this.versions.slice(start, end).map(change => change.changeRange).filter(shared.notEmpty); const result = combineContinuousChangeRanges.apply(null, changeRanges); cache.set(oldSnapshot, result); } @@ -90,7 +93,7 @@ class IncrementalScriptSnapshot { this.clearUnReferenceVersions(); const lastChange = this.versions[this.versions.length - 1]; - if (!lastChange.changeRange) { + if (!lastChange.applyed) { this.applyVersionToRootDocument(lastChange.version, false); } @@ -120,16 +123,19 @@ class IncrementalScriptSnapshot { if (change.version > version) { break; } - if (!change.changeRange) { - const changeRanges: ts.TextChangeRange[] = change.contentChanges.map(edit => ({ - span: { - start: this.document.offsetAt(edit.range.start), - length: this.document.offsetAt(edit.range.end) - this.document.offsetAt(edit.range.start), - }, - newLength: edit.text.length, - })); - change.changeRange = combineMultiLineChangeRanges.apply(null, changeRanges); - TextDocument.update(this.document, change.contentChanges, change.version); + if (!change.applyed) { + if (change.contentChanges) { + const changeRanges: ts.TextChangeRange[] = change.contentChanges.map(edit => ({ + span: { + start: this.document.offsetAt(edit.range.start), + length: this.document.offsetAt(edit.range.end) - this.document.offsetAt(edit.range.start), + }, + newLength: edit.text.length, + })); + change.changeRange = combineMultiLineChangeRanges.apply(null, changeRanges); + TextDocument.update(this.document, change.contentChanges, change.version); + } + change.applyed = true; } removeEnd = i + 1; } @@ -172,17 +178,19 @@ export function combineMultiLineChangeRanges(...changeRanges: ts.TextChangeRange if (changeRanges.length === 1) { return changeRanges[0]; } - const firstChangeRange = changeRanges.sort((a, b) => a.span.start - b.span.start)[0]; + const starts = changeRanges.map(change => change.span.start); + const ends = changeRanges.map(change => change.span.start + change.span.length); + const start = Math.min(...starts); + const end = Math.max(...ends); + const lengthDiff = changeRanges.map(change => change.newLength - change.span.length).reduce((a, b) => a + b, 0); const lastChangeRange = changeRanges.sort((a, b) => b.span.start - a.span.start)[0]; - const fullStart = firstChangeRange.span.start; - const fullEnd = lastChangeRange.span.start + lastChangeRange.span.length; - const newLength = fullEnd - fullStart + (lastChangeRange.newLength - lastChangeRange.span.length); + const newEnd = lastChangeRange.span.start + lastChangeRange.span.length + lengthDiff; const lastChange: ts.TextChangeRange = { span: { - start: firstChangeRange.span.start, - length: lastChangeRange.span.start + lastChangeRange.span.length - firstChangeRange.span.start, + start, + length: end - start, }, - newLength, + newLength: newEnd - start, }; return lastChange; } @@ -210,6 +218,7 @@ export function createSnapshots(connection: vscode.Connection) { if (incrementalSnapshot) { if (params.contentChanges.every(vscode.TextDocumentContentChangeEvent.isIncremental)) { incrementalSnapshot.versions.push({ + applyed: false, changeRange: undefined, contentChanges: params.contentChanges, version: params.textDocument.version, diff --git a/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts b/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts index 9f52d8205..ec0bdb3b8 100644 --- a/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts +++ b/packages/vue-language-server/tests/combineMultiLineChangeRanges.spec.ts @@ -7,14 +7,14 @@ describe(`Test combineMultiLineChangeRanges()`, () => { expect(combineMultiLineChangeRanges( { span: { start: 0, length: 0, }, newLength: 1 }, { span: { start: 5, length: 0, }, newLength: 1 }, - )).toEqual({ span: { start: 0, length: 5, }, newLength: 6 }); + )).toEqual({ span: { start: 0, length: 5, }, newLength: 7 }); }); it(`b`, () => { expect(combineMultiLineChangeRanges( { span: { start: 0, length: 0, }, newLength: 1 }, { span: { start: 5, length: 1, }, newLength: 1 }, - )).toEqual({ span: { start: 0, length: 6, }, newLength: 6 }); + )).toEqual({ span: { start: 0, length: 6, }, newLength: 7 }); }); it(`c`, () => { From 397eb6a6696a6f7f6eafcb77d90cf5216b9e3c97 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 10:58:37 +0800 Subject: [PATCH 10/16] feat: apply incremental edits to cache diagnostics range --- packages/vue-language-core/src/sourceFile.ts | 16 +- packages/vue-language-server/src/projects.ts | 12 +- .../vue-language-service-types/src/index.ts | 2 +- .../src/languageFeatures/validation.ts | 175 ++++-- .../src/languageService.ts | 1 + .../src/plugins/typescript.ts | 7 +- packages/vue-language-service/src/types.ts | 26 +- .../tests/updateRange.spec.ts | 558 ++++++++++++++++++ 8 files changed, 728 insertions(+), 69 deletions(-) create mode 100644 packages/vue-language-service/tests/updateRange.spec.ts diff --git a/packages/vue-language-core/src/sourceFile.ts b/packages/vue-language-core/src/sourceFile.ts index ca479fcfd..ee472ca5a 100644 --- a/packages/vue-language-core/src/sourceFile.ts +++ b/packages/vue-language-core/src/sourceFile.ts @@ -1,5 +1,5 @@ import { SFCBlock, SFCParseResult, SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from '@vue/compiler-sfc'; -import { computed, ComputedRef, reactive, ref } from '@vue/reactivity'; +import { computed, ComputedRef, reactive, shallowRef as ref } from '@vue/reactivity'; import { EmbeddedFileMappingData, TeleportMappingData, VueCompilerOptions, _VueCompilerOptions } from './types'; import { EmbeddedFileSourceMap, Teleport } from './utils/sourceMaps'; @@ -88,10 +88,9 @@ export function createSourceFile( plugins: ReturnType[], ) { - let lastScriptSnapshot: ts.IScriptSnapshot | undefined; - // refs - const fileContent = ref(''); + const snapshot = ref(scriptSnapshot); + const fileContent = computed(() => snapshot.value.getText(0, snapshot.value.getLength())); const sfc = reactive({ template: null, script: null, @@ -326,6 +325,9 @@ export function createSourceFile( return { fileName, + get snapshot() { + return snapshot.value; + }, get text() { return fileContent.value; }, @@ -400,15 +402,13 @@ export function createSourceFile( } function update(newScriptSnapshot: ts.IScriptSnapshot) { - const change = lastScriptSnapshot ? newScriptSnapshot.getChangeRange(lastScriptSnapshot) : undefined; - lastScriptSnapshot = newScriptSnapshot; + const change = newScriptSnapshot.getChangeRange(snapshot.value); + snapshot.value = newScriptSnapshot; if (change) { // TODO } - fileContent.value = newScriptSnapshot.getText(0, newScriptSnapshot.getLength()); - // TODO: wait for https://github.com/vuejs/core/pull/5912 if (parsedSfc.value) { updateTemplate(parsedSfc.value.descriptor.template); diff --git a/packages/vue-language-server/src/projects.ts b/packages/vue-language-server/src/projects.ts index 2064aa5f7..f09ae7a3a 100644 --- a/packages/vue-language-server/src/projects.ts +++ b/packages/vue-language-server/src/projects.ts @@ -201,7 +201,7 @@ export function createProjects( if (await isCancel()) return; - await sendDocumentDiagnostics(changeDoc.uri, isCancel); + await sendDocumentDiagnostics(changeDoc.uri, changeDoc.version, isCancel); } for (const doc of otherDocs) { @@ -211,21 +211,23 @@ export function createProjects( if (await isCancel()) return; - await sendDocumentDiagnostics(doc.uri, isCancel); + await sendDocumentDiagnostics(doc.uri, doc.version, isCancel); } - async function sendDocumentDiagnostics(uri: string, isCancel?: () => Promise) { + async function sendDocumentDiagnostics(uri: string, version: number, isCancel?: () => Promise) { const project = (await getProject(uri))?.project; if (!project) return; const languageService = project.getLanguageService(); const errors = await languageService.doValidation(uri, async result => { - connection.sendDiagnostics({ uri: uri, diagnostics: result }); + if (!await isCancel?.()) { + connection.sendDiagnostics({ uri: uri, diagnostics: result, version }); + } }, isCancel); if (!await isCancel?.()) { - connection.sendDiagnostics({ uri: uri, diagnostics: errors }); + connection.sendDiagnostics({ uri: uri, diagnostics: errors, version }); } } } diff --git a/packages/vue-language-service-types/src/index.ts b/packages/vue-language-service-types/src/index.ts index e10d8eda6..a736e06e0 100644 --- a/packages/vue-language-service-types/src/index.ts +++ b/packages/vue-language-service-types/src/index.ts @@ -35,7 +35,7 @@ export interface ExecuteCommandContext { export type EmbeddedLanguageServicePlugin = { - doValidation?(document: TextDocument, options: { + doValidation?(document: TextDocument, __options?: { semantic?: boolean; syntactic?: boolean; suggestion?: boolean; diff --git a/packages/vue-language-service/src/languageFeatures/validation.ts b/packages/vue-language-service/src/languageFeatures/validation.ts index a99ce9dc5..748cf3ecb 100644 --- a/packages/vue-language-service/src/languageFeatures/validation.ts +++ b/packages/vue-language-service/src/languageFeatures/validation.ts @@ -1,21 +1,95 @@ +import { TextDocument } from 'vscode-languageserver-textdocument'; import * as vscode from 'vscode-languageserver-protocol'; import { isTsDocument } from '../plugins/typescript'; import type { LanguageServiceRuntimeContext } from '../types'; import * as dedupe from '../utils/dedupe'; import { languageFeatureWorker } from '../utils/featureWorkers'; import { EmbeddedDocumentSourceMap } from '../vueDocuments'; +import * as shared from '@volar/shared'; + +export function updateRange( + range: vscode.Range, + change: { + range: vscode.Range, + newEnd: vscode.Position; + }, +) { + updatePosition(range.start, change, false); + updatePosition(range.end, change, true); + if (range.end.line === range.start.line && range.end.character <= range.start.character) { + range.end.character++; + } + return range; +} + +function updatePosition( + position: vscode.Position, + change: { + range: vscode.Range, + newEnd: vscode.Position; + }, + isEnd: boolean, +) { + if (change.range.end.line > position.line) { + if (change.newEnd.line > position.line) { + // No change + } + else if (change.newEnd.line === position.line) { + position.character = Math.min(position.character, change.newEnd.character); + } + else if (change.newEnd.line < position.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character; + } + } + else if (change.range.end.line === position.line) { + const characterDiff = change.newEnd.character - change.range.end.character; + if (position.character >= change.range.end.character) { + if (change.newEnd.line !== change.range.end.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character + position.character - change.range.end.character; + } + else { + if (isEnd ? change.range.end.character < position.character : change.range.end.character <= position.character) { + position.character += characterDiff; + } + else { + const offset = change.range.end.character - position.character; + if (-characterDiff > offset) { + position.character += characterDiff + offset; + } + } + } + } + else { + if (change.newEnd.line !== change.range.end.line) { + if (change.newEnd.line < change.range.end.line) { + position.line = change.newEnd.line; + position.character = change.newEnd.character; + } + } + else { + const offset = change.range.end.character - position.character; + if (-characterDiff > offset) { + position.character += characterDiff + offset; + } + } + } + } + else if (change.range.end.line < position.line) { + position.line += change.newEnd.line - change.range.end.line; + } +} export function register(context: LanguageServiceRuntimeContext) { + interface Cache { + snapshot: ts.IScriptSnapshot | undefined; + errors: vscode.Diagnostic[]; + } const responseCache = new Map< string, - { - nonTs: vscode.Diagnostic[], - tsSemantic: vscode.Diagnostic[], - tsDeclaration: vscode.Diagnostic[], - tsSyntactic: vscode.Diagnostic[], - tsSuggestion: vscode.Diagnostic[], - } + { [key in 'nonTs' | 'tsSemantic' | 'tsDeclaration' | 'tsSyntactic' | 'tsSuggestion']: Cache } >(); const nonTsCache = new Map< number, @@ -36,46 +110,60 @@ export function register(context: LanguageServiceRuntimeContext) { return async (uri: string, response?: (result: vscode.Diagnostic[]) => void, isCancel?: () => Promise) => { const cache = responseCache.get(uri) ?? responseCache.set(uri, { - nonTs: [], - tsSemantic: [], - tsDeclaration: [], - tsSuggestion: [], - tsSyntactic: [], + nonTs: { snapshot: undefined, errors: [] }, + tsSemantic: { snapshot: undefined, errors: [] }, + tsDeclaration: { snapshot: undefined, errors: [] }, + tsSuggestion: { snapshot: undefined, errors: [] }, + tsSyntactic: { snapshot: undefined, errors: [] }, }).get(uri)!; + const newSnapshot = context.host.getScriptSnapshot(shared.uriToFsPath(uri)); + const newDocument = newSnapshot ? TextDocument.create('file://a.txt', 'txt', 0, newSnapshot.getText(0, newSnapshot.getLength())) : undefined; + + for (const _cache of Object.values(cache)) { + + const oldSnapshot = _cache.snapshot; + const change = oldSnapshot ? newSnapshot?.getChangeRange(oldSnapshot) : undefined; + + _cache.snapshot = newSnapshot; + + if (newDocument && oldSnapshot && newSnapshot && change) { + const oldDocument = TextDocument.create('file://a.txt', 'txt', 0, oldSnapshot.getText(0, oldSnapshot.getLength())); + const changeRange = { + range: { + start: oldDocument.positionAt(change.span.start), + end: oldDocument.positionAt(change.span.start + change.span.length), + }, + newEnd: newDocument.positionAt(change.span.start + change.newLength), + }; + for (const error of _cache.errors) { + updateRange(error.range, changeRange); + } + } + } - let errorsDirty = false; // avoid cache error range jitter + let shouldSend = false; - await worker(false, { - declaration: true, - semantic: true, - suggestion: true, - syntactic: true, - }, nonTsCache, errors => cache.nonTs = errors ?? []); + await worker(false, undefined, nonTsCache, cache.nonTs); + doResponse(); + await worker(true, { syntactic: true }, scriptTsCache_syntactic, cache.tsSyntactic); doResponse(); - await worker(true, { syntactic: true }, scriptTsCache_syntactic, errors => cache.tsSyntactic = errors ?? []); - await worker(true, { suggestion: true }, scriptTsCache_suggestion, errors => cache.tsSuggestion = errors ?? []); + await worker(true, { suggestion: true }, scriptTsCache_suggestion, cache.tsSuggestion); doResponse(); - await worker(true, { semantic: true }, scriptTsCache_semantic, errors => cache.tsSemantic = errors ?? []); + await worker(true, { semantic: true }, scriptTsCache_semantic, cache.tsSemantic); doResponse(); - await worker(true, { declaration: true }, scriptTsCache_declaration, errors => cache.tsDeclaration = errors ?? []); + await worker(true, { declaration: true }, scriptTsCache_declaration, cache.tsDeclaration); return getErrors(); - function doResponse() { - if (errorsDirty) { + async function doResponse() { + if (shouldSend) { response?.(getErrors()); - errorsDirty = false; + shouldSend = false; } } function getErrors() { - return [ - ...cache.nonTs, - ...cache.tsSyntactic, - ...cache.tsSuggestion, - ...cache.tsSemantic, - ...cache.tsDeclaration, - ]; + return Object.values(cache).flatMap(({ errors }) => errors); } async function worker( @@ -85,9 +173,9 @@ export function register(context: LanguageServiceRuntimeContext) { semantic?: boolean, suggestion?: boolean, syntactic?: boolean, - }, + } | undefined, cacheMap: typeof nonTsCache, - response: (result: vscode.Diagnostic[] | undefined) => void, + cache: Cache, ) { const result = await languageFeatureWorker( context, @@ -100,11 +188,11 @@ export function register(context: LanguageServiceRuntimeContext) { }, async (plugin, document, arg, sourceMap) => { - // avoid duplicate errors from vue plugin & typescript plugin - if (isTsDocument(document) !== isTs) + if (await isCancel?.()) return; - if (await isCancel?.()) + // avoid duplicate errors from vue plugin & typescript plugin + if (isTsDocument(document) !== isTs) return; const pluginId = context.getPluginId(plugin); @@ -118,7 +206,7 @@ export function register(context: LanguageServiceRuntimeContext) { } } else { - if (options.declaration || options.semantic) { + if (!options || options.declaration || options.semantic) { if (cache && cache.documentVersion === document.version && cache.tsProjectVersion === tsProjectVersion) { return cache.errors; } @@ -132,7 +220,7 @@ export function register(context: LanguageServiceRuntimeContext) { const errors = await plugin.doValidation?.(document, options); - errorsDirty = true; + shouldSend = true; pluginCache.set(document.uri, { documentVersion: document.version, @@ -145,8 +233,11 @@ export function register(context: LanguageServiceRuntimeContext) { (errors, sourceMap) => transformErrorRange(sourceMap, errors), arr => dedupe.withDiagnostics(arr.flat()), ); - if (!await isCancel?.()) - response(result); + + if (result) { + cache.errors = result; + cache.snapshot = newSnapshot; + } } }; diff --git a/packages/vue-language-service/src/languageService.ts b/packages/vue-language-service/src/languageService.ts index ab69b954e..ac18334cd 100644 --- a/packages/vue-language-service/src/languageService.ts +++ b/packages/vue-language-service/src/languageService.ts @@ -98,6 +98,7 @@ export function createLanguageService( const documentVersions = new Map(); const context: LanguageServiceRuntimeContext = { + host: vueLsHost, vueDocuments, getTsLs: () => tsLs, getTextDocument, diff --git a/packages/vue-language-service/src/plugins/typescript.ts b/packages/vue-language-service/src/plugins/typescript.ts index bff503e14..c7d8de686 100644 --- a/packages/vue-language-service/src/plugins/typescript.ts +++ b/packages/vue-language-service/src/plugins/typescript.ts @@ -161,7 +161,12 @@ export default function (options: { }, }, - doValidation(document, options_2) { + doValidation(document, options_2 = { + semantic: true, + syntactic: true, + suggestion: true, + declaration: true, + }) { if (isTsDocument(document)) { return options.getTsLs().doValidation(document.uri, options_2); } diff --git a/packages/vue-language-service/src/types.ts b/packages/vue-language-service/src/types.ts index b21236e31..72e9d1942 100644 --- a/packages/vue-language-service/src/types.ts +++ b/packages/vue-language-service/src/types.ts @@ -1,21 +1,23 @@ import type * as ts2 from '@volar/typescript-language-service'; +import { LanguageServiceHost } from '@volar/vue-language-core'; import { EmbeddedLanguageServicePlugin } from '@volar/vue-language-service-types'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import { VueDocument, VueDocuments } from './vueDocuments'; -export type DocumentServiceRuntimeContext = { - typescript: typeof import('typescript/lib/tsserverlibrary'), - getVueDocument(document: TextDocument): VueDocument | undefined, - getPlugins(): EmbeddedLanguageServicePlugin[], - getFormatPlugins(): EmbeddedLanguageServicePlugin[], - updateTsLs(document: TextDocument): void, +export interface DocumentServiceRuntimeContext { + typescript: typeof import('typescript/lib/tsserverlibrary'); + getVueDocument(document: TextDocument): VueDocument | undefined; + getPlugins(): EmbeddedLanguageServicePlugin[]; + getFormatPlugins(): EmbeddedLanguageServicePlugin[]; + updateTsLs(document: TextDocument): void; }; -export type LanguageServiceRuntimeContext = { - vueDocuments: VueDocuments, - getTextDocument(uri: string): TextDocument | undefined, - getPlugins(): EmbeddedLanguageServicePlugin[], - getPluginId(plugin: EmbeddedLanguageServicePlugin): number, - getPluginById(id: number): EmbeddedLanguageServicePlugin | undefined, +export interface LanguageServiceRuntimeContext { + host: LanguageServiceHost; + vueDocuments: VueDocuments; + getTextDocument(uri: string): TextDocument | undefined; + getPlugins(): EmbeddedLanguageServicePlugin[]; + getPluginId(plugin: EmbeddedLanguageServicePlugin): number; + getPluginById(id: number): EmbeddedLanguageServicePlugin | undefined; getTsLs(): ts2.LanguageService; }; diff --git a/packages/vue-language-service/tests/updateRange.spec.ts b/packages/vue-language-service/tests/updateRange.spec.ts new file mode 100644 index 000000000..9513f28e8 --- /dev/null +++ b/packages/vue-language-service/tests/updateRange.spec.ts @@ -0,0 +1,558 @@ +import { describe, expect, it } from 'vitest'; +import { updateRange } from '../out/languageFeatures/validation'; + +describe(`Test updateRange()`, () => { + + // No change + + it(` +123 +^^^ +----- +123x +^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 3 }, + end: { line: 0, character: 3 }, + }, + newEnd: { line: 0, character: 4 }, + }, + )).toEqual({ + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }); + }); + + it(` +x +123 +^^^ +----- +xx +123 +^^^ + `, () => { + expect(updateRange( + { + start: { line: 1, character: 0 }, + end: { line: 1, character: 3 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 1 }, + }, + newEnd: { line: 0, character: 2 }, + }, + )).toEqual({ + start: { line: 1, character: 0 }, + end: { line: 1, character: 3 }, + }); + }); + + it(` +123 +^^^ +----- +1xxxx +^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 3 }, + }, + newEnd: { line: 0, character: 5 }, + }, + )).toEqual({ + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }); + }); + + // Single line change + + it(` +123 +^^^ +----- +x123 + ^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + newEnd: { line: 0, character: 1 }, + }, + )).toEqual( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 4 }, + } + ); + }); + + it(` +x123 + ^^^ +----- +123 +^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 4 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + }, + newEnd: { line: 0, character: 0 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + } + ); + }); + + it(` +123 +^^^ +----- +1x23 +^^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 1 }, + }, + newEnd: { line: 0, character: 2 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 4 }, + } + ); + }); + + it(` +123 +^^^ +----- +xxx23 +^^^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + }, + newEnd: { line: 0, character: 3 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 5 }, + } + ); + }); + + it(` +123xxx +^^^ +----- +12xx +^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 2 }, + end: { line: 0, character: 4 }, + }, + newEnd: { line: 0, character: 2 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 2 }, + } + ); + }); + + it(` +x12x + ^^ +----- +xx + ^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 3 }, + }, + newEnd: { line: 0, character: 1 }, + }, + )).toEqual( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 2 }, + } + ); + }); + + it(` +xx12x + ^^ +----- +xx + ^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 2 }, + end: { line: 0, character: 4 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 4 }, + }, + newEnd: { line: 0, character: 1 }, + }, + )).toEqual( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 2 }, + } + ); + }); + + it(` +1 +| +----- +1x + | + `, () => { + expect(updateRange( + { + start: { line: 0, character: 1 }, + end: { line: 0, character: 1 }, + }, + { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 1 }, + }, + newEnd: { line: 0, character: 2 }, + }, + )).toEqual( + { + start: { line: 0, character: 2 }, + end: { line: 0, character: 2 }, + } + ); + }); + + // Multiple lines + + it(` +123 +^^^ +----- +x +123 +^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + newEnd: { line: 1, character: 0 }, + }, + )).toEqual( + { + start: { line: 1, character: 0 }, + end: { line: 1, character: 3 }, + } + ); + }); + + it(` +123 +^^^ +----- +x +x123 + ^^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + newEnd: { line: 1, character: 1 }, + }, + )).toEqual( + { + start: { line: 1, character: 1 }, + end: { line: 1, character: 4 }, + } + ); + }); + + it(` +x +123 +^^^ +----- +123 +^^^ + `, () => { + expect(updateRange( + { + start: { line: 1, character: 0 }, + end: { line: 1, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 1, character: 0 }, + }, + newEnd: { line: 0, character: 0 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + } + ); + }); + + it(` +123 +^^^ +----- +12 +^^ +3 +^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 2 }, + end: { line: 0, character: 2 }, + }, + newEnd: { line: 1, character: 0 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 1, character: 1 }, + } + ); + }); + + it(` +123 +^^^ +----- +12 +^^ +x3 +^^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 2 }, + end: { line: 0, character: 2 }, + }, + newEnd: { line: 1, character: 1 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 1, character: 2 }, + } + ); + }); + + it(` +123 +^^^ +xxxxx +----- +xxxxx +^ + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 1, character: 0 }, + }, + newEnd: { line: 0, character: 0 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + } + ); + }); + + it(` +xxx +xx123 + ^^^ +----- +xx + ^ + `, () => { + expect(updateRange( + { + start: { line: 1, character: 2 }, + end: { line: 1, character: 5 }, + }, + { + range: { + start: { line: 0, character: 2 }, + end: { line: 1, character: 5 }, + }, + newEnd: { line: 0, character: 2 }, + }, + )).toEqual( + { + start: { line: 0, character: 2 }, + end: { line: 0, character: 3 }, + } + ); + }); + + it(` +xxx +123 +^^^ +----- +xxx123 + ^^^ + `, () => { + expect(updateRange( + { + start: { line: 1, character: 0 }, + end: { line: 1, character: 3 }, + }, + { + range: { + start: { line: 0, character: 3 }, + end: { line: 1, character: 0 }, + }, + newEnd: { line: 0, character: 3 }, + }, + )).toEqual( + { + start: { line: 0, character: 3 }, + end: { line: 0, character: 6 }, + } + ); + }); + + it(` +123 +^^^ +xxx +----- +xxx +| + `, () => { + expect(updateRange( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + { + range: { + start: { line: 0, character: 0 }, + end: { line: 1, character: 0 }, + }, + newEnd: { line: 0, character: 0 }, + }, + )).toEqual( + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + } + ); + }); +}); From bbaec65c63dd6b44f3e2764b9cb9d261f741b1f1 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 12:10:27 +0800 Subject: [PATCH 11/16] feat: block outdate diagnostics in language client --- .../src/nodeClientMain.ts | 21 +++++++++++++++++++ packages/vue-language-server/src/projects.ts | 18 ++++++++++------ .../src/languageFeatures/validation.ts | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/extensions/vscode-vue-language-features/src/nodeClientMain.ts b/extensions/vscode-vue-language-features/src/nodeClientMain.ts index d2907e379..5d2b6f94d 100644 --- a/extensions/vscode-vue-language-features/src/nodeClientMain.ts +++ b/extensions/vscode-vue-language-features/src/nodeClientMain.ts @@ -31,6 +31,27 @@ export function activate(context: vscode.ExtensionContext) { }, }; const clientOptions: lsp.LanguageClientOptions = { + middleware: { + handleDiagnostics: (uri, diagnostics, next) => { + const document = vscode.workspace.textDocuments.find(d => d.uri.toString() === uri.toString()); + if (document) { + let outdated = false; + for (const diagnostic of diagnostics) { + const data = (diagnostic as any).data; + if (typeof data === 'object' && 'version' in data) { + if (document.version !== data.version) { + outdated = true; + break; + } + } + } + if (outdated) { + return; + } + } + next(uri, diagnostics); + }, + }, documentSelector, initializationOptions: initOptions, progressOnInitialization: true, diff --git a/packages/vue-language-server/src/projects.ts b/packages/vue-language-server/src/projects.ts index f09ae7a3a..dd0d36851 100644 --- a/packages/vue-language-server/src/projects.ts +++ b/packages/vue-language-server/src/projects.ts @@ -220,14 +220,20 @@ export function createProjects( if (!project) return; const languageService = project.getLanguageService(); - const errors = await languageService.doValidation(uri, async result => { - if (!await isCancel?.()) { - connection.sendDiagnostics({ uri: uri, diagnostics: result, version }); - } + const errors = await languageService.doValidation(uri, result => { + connection.sendDiagnostics({ uri: uri, diagnostics: result.map(addVersion), version }); }, isCancel); - if (!await isCancel?.()) { - connection.sendDiagnostics({ uri: uri, diagnostics: errors, version }); + connection.sendDiagnostics({ uri: uri, diagnostics: errors.map(addVersion), version }); + + function addVersion(error: vscode.Diagnostic) { + if (error.data === undefined) { + error.data = { version }; + } + else if (typeof error.data === 'object') { + error.data.version = version; + } + return error; } } } diff --git a/packages/vue-language-service/src/languageFeatures/validation.ts b/packages/vue-language-service/src/languageFeatures/validation.ts index 748cf3ecb..4d645f646 100644 --- a/packages/vue-language-service/src/languageFeatures/validation.ts +++ b/packages/vue-language-service/src/languageFeatures/validation.ts @@ -155,7 +155,7 @@ export function register(context: LanguageServiceRuntimeContext) { return getErrors(); - async function doResponse() { + function doResponse() { if (shouldSend) { response?.(getErrors()); shouldSend = false; From f42d193c676cc1cc952e7ec249a5909847d1317b Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 13:04:15 +0800 Subject: [PATCH 12/16] perf: avoid reactive proxy template ast --- packages/vue-language-core/src/sourceFile.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/vue-language-core/src/sourceFile.ts b/packages/vue-language-core/src/sourceFile.ts index ee472ca5a..2c899e535 100644 --- a/packages/vue-language-core/src/sourceFile.ts +++ b/packages/vue-language-core/src/sourceFile.ts @@ -97,15 +97,15 @@ export function createSourceFile( scriptSetup: null, styles: [], customBlocks: [], - get templateAst() { + templateAst: computed(() => { return compiledSFCTemplate.value?.ast; - }, - get scriptAst() { + }) as unknown as Sfc['templateAst'], + scriptAst: computed(() => { return scriptAst.value; - }, - get scriptSetupAst() { + }) as unknown as Sfc['scriptAst'], + scriptSetupAst: computed(() => { return scriptSetupAst.value; - }, + }) as unknown as Sfc['scriptSetupAst'], }) as Sfc /* avoid Sfc unwrap in .d.ts by reactive */; // use @@ -325,9 +325,6 @@ export function createSourceFile( return { fileName, - get snapshot() { - return snapshot.value; - }, get text() { return fileContent.value; }, From 4a6b7bb5b276c651d4c4b34057b12cee009b1bb2 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 13:31:31 +0800 Subject: [PATCH 13/16] perf: combine plugin semantic tokens request --- .../languageFeatures/documentSemanticTokens.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/vue-language-service/src/languageFeatures/documentSemanticTokens.ts b/packages/vue-language-service/src/languageFeatures/documentSemanticTokens.ts index dfff1d0ec..b43d784f4 100644 --- a/packages/vue-language-service/src/languageFeatures/documentSemanticTokens.ts +++ b/packages/vue-language-service/src/languageFeatures/documentSemanticTokens.ts @@ -30,6 +30,11 @@ export function register(context: LanguageServiceRuntimeContext) { if (cancleToken?.isCancellationRequested) return; + let range: { + start: number, + end: number, + } | undefined; + for (const mapping of sourceMap.mappings) { if (cancleToken?.isCancellationRequested) @@ -40,9 +45,19 @@ export function register(context: LanguageServiceRuntimeContext) { && mapping.sourceRange.end > offsetRange.start && mapping.sourceRange.start < offsetRange.end ) { - yield mapping.mappedRange; + if (!range) { + range = { ...mapping.mappedRange }; + } + else { + range.start = Math.min(range.start, mapping.mappedRange.start); + range.end = Math.max(range.end, mapping.mappedRange.end); + } } } + + if (range) { + yield range; + } }, (plugin, document, offsetRange) => plugin.findDocumentSemanticTokens?.( document, From b31a8787b25cdd65d3a137cd35b852e802987072 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 16 Aug 2022 20:13:51 +0800 Subject: [PATCH 14/16] feat: add `VueLanguagePlugin.order` --- packages/alpine-language-core/src/plugins/file-html.ts | 3 +++ packages/vue-language-core/src/lsContext.ts | 6 +++++- packages/vue-language-core/src/sourceFile.ts | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/alpine-language-core/src/plugins/file-html.ts b/packages/alpine-language-core/src/plugins/file-html.ts index 52447de8a..f2a124a8d 100644 --- a/packages/alpine-language-core/src/plugins/file-html.ts +++ b/packages/alpine-language-core/src/plugins/file-html.ts @@ -6,6 +6,9 @@ const plugin: VueLanguagePlugin = (ctx) => { const vueHtmlFilePlugin = useVueHtmlFilePlugin(ctx); return { + + order: -1, + parseSFC(fileName, content) { if (fileName.endsWith('.html')) { diff --git a/packages/vue-language-core/src/lsContext.ts b/packages/vue-language-core/src/lsContext.ts index 11b606bf7..ce45b1ee0 100644 --- a/packages/vue-language-core/src/lsContext.ts +++ b/packages/vue-language-core/src/lsContext.ts @@ -58,7 +58,11 @@ export function getPlugins( compilerOptions, vueCompilerOptions: vueCompilerOptions, }; - const plugins = _plugins.map(plugin => plugin(pluginCtx)); + const plugins = _plugins.map(plugin => plugin(pluginCtx)).sort((a, b) => { + const aOrder = a.order ?? 0; + const bOrder = b.order ?? 0; + return aOrder - bOrder; + }); return plugins; } diff --git a/packages/vue-language-core/src/sourceFile.ts b/packages/vue-language-core/src/sourceFile.ts index 2c899e535..be55061d2 100644 --- a/packages/vue-language-core/src/sourceFile.ts +++ b/packages/vue-language-core/src/sourceFile.ts @@ -15,6 +15,7 @@ export type VueLanguagePlugin = (ctx: { compilerOptions: ts.CompilerOptions, vueCompilerOptions: _VueCompilerOptions, }) => { + order?: number; parseSFC?(fileName: string, content: string): SFCParseResult | undefined; compileSFCTemplate?(lang: string, template: string, options?: CompilerDom.CompilerOptions): CompilerDom.CodegenResult | undefined; getEmbeddedFileNames?(fileName: string, sfc: Sfc): string[]; From 276492b53bb721294ff97c9a6ba27f548eaca68f Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Wed, 17 Aug 2022 05:38:44 +0800 Subject: [PATCH 15/16] chore: remove unneeded getMapped --- packages/source-map/src/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/source-map/src/index.ts b/packages/source-map/src/index.ts index 96cf16cc9..822cfcd63 100644 --- a/packages/source-map/src/index.ts +++ b/packages/source-map/src/index.ts @@ -90,22 +90,18 @@ export class SourceMapBase { const mapped = this.getRange(startOffset, endOffset, sourceToTarget, mapping.mode, mapping.sourceRange, mapping.mappedRange, mapping.data); if (mapped) { - yield getMapped(mapped); + yield mapped; } else if (mapping.additional) { for (const other of mapping.additional) { const mapped = this.getRange(startOffset, endOffset, sourceToTarget, other.mode, other.sourceRange, other.mappedRange, mapping.data); if (mapped) { - yield getMapped(mapped); + yield mapped; break; // only return first match additional range } } } } - - function getMapped(mapped: [{ start: number, end: number; }, Data]) { - return mapped; - } } private getRange(start: number, end: number, sourceToTarget: boolean, mode: Mode, sourceRange: Range, targetRange: Range, data: Data): [{ start: number, end: number; }, Data] | undefined { From 8b05426e2bb448017ff6964ace14cd8d51944759 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Wed, 17 Aug 2022 07:25:54 +0800 Subject: [PATCH 16/16] chore: don't update if snapshot no change --- packages/vue-language-core/src/sourceFile.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vue-language-core/src/sourceFile.ts b/packages/vue-language-core/src/sourceFile.ts index be55061d2..d0e777f12 100644 --- a/packages/vue-language-core/src/sourceFile.ts +++ b/packages/vue-language-core/src/sourceFile.ts @@ -322,7 +322,7 @@ export function createSourceFile( } }); - update(scriptSnapshot); + update(scriptSnapshot, true); return { fileName, @@ -398,7 +398,11 @@ export function createSourceFile( } return range; } - function update(newScriptSnapshot: ts.IScriptSnapshot) { + function update(newScriptSnapshot: ts.IScriptSnapshot, init = false) { + + if (newScriptSnapshot === snapshot.value && !init) { + return; + } const change = newScriptSnapshot.getChangeRange(snapshot.value); snapshot.value = newScriptSnapshot;