From 2bb908d11e5f1f43ad747c1166b1dcdb444928dc Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 25 Dec 2022 02:39:35 +0800 Subject: [PATCH] feat: Support for generating virtual file from multiple sources (#2253) --- .../angular-language-core/src/modules/html.ts | 22 +- .../angular-language-core/src/modules/ts.ts | 12 +- examples/angular-language-server/src/index.ts | 7 +- examples/svelte-language-core/src/index.ts | 33 ++- .../src/index.ts | 4 +- .../language-core/src/documentRegistry.ts | 126 +++++------ packages/language-core/src/languageContext.ts | 143 ++++++------ packages/language-core/src/sourceMaps.ts | 2 +- packages/language-core/src/types.ts | 15 +- .../src/common/features/customFeatures.ts | 21 +- packages/language-server/src/protocol.ts | 4 +- .../src/baseDocumentService.ts | 14 +- .../src/baseLanguageService.ts | 4 +- .../documentFeatures/colorPresentations.ts | 2 +- .../src/documentFeatures/documentColors.ts | 2 +- .../src/documentFeatures/documentSymbols.ts | 2 +- .../src/documentFeatures/foldingRanges.ts | 2 +- .../src/documentFeatures/format.ts | 65 +++--- .../src/documentFeatures/selectionRanges.ts | 2 +- packages/language-service/src/documents.ts | 212 +++++++++--------- .../src/languageFeatures/autoInsert.ts | 2 +- .../src/languageFeatures/callHierarchy.ts | 78 ++++--- .../src/languageFeatures/codeActionResolve.ts | 4 +- .../src/languageFeatures/codeActions.ts | 16 +- .../src/languageFeatures/complete.ts | 61 +++-- .../src/languageFeatures/completeResolve.ts | 4 +- .../src/languageFeatures/definition.ts | 20 +- .../languageFeatures/documentHighlights.ts | 2 +- .../src/languageFeatures/documentLinks.ts | 13 +- .../documentSemanticTokens.ts | 2 +- .../src/languageFeatures/fileReferences.ts | 7 +- .../src/languageFeatures/fileRename.ts | 10 +- .../src/languageFeatures/inlayHints.ts | 14 +- .../src/languageFeatures/references.ts | 19 +- .../src/languageFeatures/rename.ts | 168 ++++++++------ .../src/languageFeatures/validation.ts | 34 +-- .../src/languageFeatures/workspaceSymbols.ts | 11 +- packages/language-service/src/types.ts | 7 +- .../src/utils/definePlugin.ts | 25 ++- .../src/utils/featureWorkers.ts | 43 ++-- .../pug-language-service/src/pugDocument.ts | 3 +- .../src/services/documentHighlight.ts | 2 +- .../src/services/documentLinks.ts | 2 +- .../src/services/documentSymbol.ts | 4 +- .../src/services/hover.ts | 2 +- .../src/services/scanner.ts | 6 +- .../src/services/selectionRanges.ts | 2 +- packages/transforms/src/symbolInformations.ts | 4 +- packages/typescript/src/getProgram.ts | 20 +- packages/typescript/src/index.ts | 45 ++-- .../src/features/showVirtualFiles.ts | 164 ++++++++------ .../src/generators/script.ts | 2 +- .../vue-language-core/src/languageModule.ts | 4 +- .../vue-language-core/src/plugins/vue-tsx.ts | 4 +- .../vue-language-core/src/sourceFile.ts | 52 ++--- .../src/languageServerPlugin.ts | 4 +- .../src/documentService.ts | 16 +- .../vue-language-service/src/helpers.ts | 2 +- .../src/ideFeatures/nameCasing.ts | 48 ++-- .../src/languageService.ts | 139 ++++++------ .../src/plugins/vue-autoinsert-parentheses.ts | 13 +- .../src/plugins/vue-codelens-references.ts | 70 +++--- .../src/plugins/vue-convert-htmlpug.ts | 23 +- .../src/plugins/vue-convert-refsugar.ts | 60 +++-- .../src/plugins/vue-convert-scriptsetup.ts | 62 +++-- .../src/plugins/vue-template.ts | 66 +++--- .../src/plugins/vue-twoslash-queries.ts | 27 +-- .../vue-language-service/src/plugins/vue.ts | 31 ++- .../vue-tsc-eslint-hook/src/index.ts | 61 ++--- 69 files changed, 1105 insertions(+), 1067 deletions(-) diff --git a/examples/angular-language-core/src/modules/html.ts b/examples/angular-language-core/src/modules/html.ts index 4bb845021..ddb4cc6b6 100644 --- a/examples/angular-language-core/src/modules/html.ts +++ b/examples/angular-language-core/src/modules/html.ts @@ -1,4 +1,4 @@ -import { DocumentCapabilities, EmbeddedFileKind, LanguageModule, VirtualFile } from '@volar/language-core'; +import { DocumentCapabilities, VirtualFileKind, LanguageModule, VirtualFile } from '@volar/language-core'; import type { TmplAstNode, TmplAstTemplate, ParsedTemplate, ParseSourceSpan } from '@angular/compiler'; import { Codegen } from './ts'; import type * as ts from 'typescript/lib/tsserverlibrary'; @@ -11,9 +11,9 @@ export class HTMLTemplateFile implements VirtualFile { public capabilities: DocumentCapabilities = { diagnostic: true, }; - public kind = EmbeddedFileKind.TextFile; + public kind = VirtualFileKind.TextFile; public mappings: VirtualFile['mappings'] = []; - public embeddeds: VirtualFile['embeddeds'] = []; + public embeddedFiles: VirtualFile['embeddedFiles'] = []; public parsed: ParsedTemplate; constructor( @@ -34,10 +34,10 @@ export class HTMLTemplateFile implements VirtualFile { sourceRange: [0, this.text.length], }, ]; - this.embeddeds = [ + this.embeddedFiles = [ { fileName: fileName + '.__template.ts', - text: generated.codegen.text, + snapshot: this.ts.ScriptSnapshot.fromString(generated.codegen.text), capabilities: { diagnostic: true, foldingRange: false, @@ -46,9 +46,9 @@ export class HTMLTemplateFile implements VirtualFile { codeAction: false, inlayHint: true, }, - kind: EmbeddedFileKind.TypeScriptHostFile, + kind: VirtualFileKind.TypeScriptHostFile, mappings: generated.codegen.mappings, - embeddeds: [], + embeddedFiles: [], }, ]; this.parsed = generated.parsed; @@ -66,20 +66,20 @@ export class HTMLTemplateFile implements VirtualFile { sourceRange: [0, this.text.length], }, ]; - this.embeddeds[0].text = generated.codegen.text; - this.embeddeds[0].mappings = generated.codegen.mappings; + this.embeddedFiles[0].snapshot = this.ts.ScriptSnapshot.fromString(generated.codegen.text); + this.embeddedFiles[0].mappings = generated.codegen.mappings; this.parsed = generated.parsed; } } export function createHtmlLanguageModule(ts: typeof import('typescript/lib/tsserverlibrary')): LanguageModule { return { - createSourceFile(fileName, snapshot) { + createFile(fileName, snapshot) { if (fileName.endsWith('.html')) { return new HTMLTemplateFile(ts, fileName, snapshot); } }, - updateSourceFile(sourceFile, snapshot) { + updateFile(sourceFile, snapshot) { sourceFile.update(snapshot); }, }; diff --git a/examples/angular-language-core/src/modules/ts.ts b/examples/angular-language-core/src/modules/ts.ts index d164ecbc3..35b254559 100644 --- a/examples/angular-language-core/src/modules/ts.ts +++ b/examples/angular-language-core/src/modules/ts.ts @@ -1,4 +1,4 @@ -import { LanguageModule, VirtualFile, EmbeddedFileKind, PositionCapabilities } from '@volar/language-core'; +import { LanguageModule, VirtualFile, VirtualFileKind, PositionCapabilities } from '@volar/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as path from 'path'; import type { Mapping } from '@volar/source-map'; @@ -8,7 +8,7 @@ export function createTsLanguageModule( ) { const languageModule: LanguageModule = { - createSourceFile(fileName, snapshot) { + createFile(fileName, snapshot) { if (fileName.endsWith('.ts')) { const text = snapshot.getText(0, snapshot.getLength()); const ast = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest); @@ -26,13 +26,13 @@ export function createTsLanguageModule( codeAction: true, inlayHint: true, }, - kind: EmbeddedFileKind.TypeScriptHostFile, + kind: VirtualFileKind.TypeScriptHostFile, mappings: virtualFile.mappings, - embeddeds: [], + embeddedFiles: [], }; } }, - updateSourceFile(sourceFile, snapshot) { + updateFile(sourceFile, snapshot) { const text = snapshot.getText(0, snapshot.getLength()); const change = snapshot.getChangeRange(sourceFile.snapshot); @@ -43,7 +43,7 @@ export function createTsLanguageModule( sourceFile.snapshot = snapshot; const gen = createVirtualFile(sourceFile.ast); - sourceFile.text = gen.text; + sourceFile.snapshot = ts.ScriptSnapshot.fromString(gen.text); sourceFile.mappings = gen.mappings; }, }; diff --git a/examples/angular-language-server/src/index.ts b/examples/angular-language-server/src/index.ts index 7fc9b633a..56c2495ac 100644 --- a/examples/angular-language-server/src/index.ts +++ b/examples/angular-language-server/src/index.ts @@ -1,7 +1,7 @@ import { createTsLanguageModule, createHtmlLanguageModule, HTMLTemplateFile } from '@volar-examples/angular-language-core'; import createTsPlugin from '@volar-plugins/typescript'; import { createLanguageServer, LanguageServerPlugin } from '@volar/language-server/node'; -import type { LanguageServicePlugin, SourceFileDocuments, Diagnostic } from '@volar/language-service'; +import type { LanguageServicePlugin, DocumentsAndSourceMaps, Diagnostic } from '@volar/language-service'; const plugin: LanguageServerPlugin = () => ({ extraFileExtensions: [{ extension: 'html', isMixedContent: true, scriptKind: 7 }], @@ -34,7 +34,7 @@ const plugin: LanguageServerPlugin = () => ({ }, }); -function createNgTemplateLsPlugin(docs: SourceFileDocuments): LanguageServicePlugin { +function createNgTemplateLsPlugin(docs: DocumentsAndSourceMaps): LanguageServicePlugin { return { @@ -42,7 +42,7 @@ function createNgTemplateLsPlugin(docs: SourceFileDocuments): LanguageServicePlu onSyntactic(document) { - const file = docs.get(document.uri)?.file; + const file = docs.getRootFileBySourceFileUri(document.uri); if (file instanceof HTMLTemplateFile) { return (file.parsed.errors ?? []).map(error => ({ @@ -61,4 +61,3 @@ function createNgTemplateLsPlugin(docs: SourceFileDocuments): LanguageServicePlu } createLanguageServer([plugin]); - diff --git a/examples/svelte-language-core/src/index.ts b/examples/svelte-language-core/src/index.ts index 699182685..b5698e9d5 100644 --- a/examples/svelte-language-core/src/index.ts +++ b/examples/svelte-language-core/src/index.ts @@ -1,5 +1,5 @@ import { decode } from '@jridgewell/sourcemap-codec'; -import { VirtualFile, EmbeddedFileKind, LanguageModule } from '@volar/language-core'; +import { VirtualFile, VirtualFileKind, LanguageModule } from '@volar/language-core'; import { svelte2tsx } from 'svelte2tsx'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; @@ -7,14 +7,13 @@ import { URI } from 'vscode-uri'; export * from '@volar/language-core'; export const languageModule: LanguageModule = { - createSourceFile(fileName, snapshot) { + createFile(fileName, snapshot) { if (fileName.endsWith('.svelte')) { - const text = snapshot.getText(0, snapshot.getLength()); return { fileName, - text, - kind: EmbeddedFileKind.TextFile, - embeddeds: getEmbeddeds(fileName, text), + snapshot, + kind: VirtualFileKind.TextFile, + embeddedFiles: getEmbeddeds(fileName, snapshot.getText(0, snapshot.getLength())), capabilities: { diagnostic: true, foldingRange: true, @@ -27,9 +26,9 @@ export const languageModule: LanguageModule = { }; } }, - updateSourceFile(sourceFile, snapshot) { - sourceFile.text = snapshot.getText(0, snapshot.getLength()); - sourceFile.embeddeds = getEmbeddeds(sourceFile.fileName, sourceFile.text); + updateFile(sourceFile, snapshot) { + sourceFile.snapshot = snapshot; + sourceFile.embeddedFiles = getEmbeddeds(sourceFile.fileName, sourceFile.snapshot.getText(0, sourceFile.snapshot.getLength())); }, }; @@ -108,8 +107,18 @@ function getEmbeddeds(fileName: string, text: string) { embeddeds.push({ fileName: fileName + '.ts', - text: tsx.code, - kind: EmbeddedFileKind.TypeScriptHostFile, + snapshot: { + getText(start, end) { + return tsx.code.substring(start, end); + }, + getLength() { + return tsx.code.length; + }, + getChangeRange() { + return undefined; + }, + }, + kind: VirtualFileKind.TypeScriptHostFile, capabilities: { diagnostic: true, foldingRange: false, @@ -119,7 +128,7 @@ function getEmbeddeds(fileName: string, text: string) { documentFormatting: false, }, mappings: mappings, - embeddeds: [], + embeddedFiles: [], }); return embeddeds; diff --git a/examples/vue-and-svelte-language-server/src/index.ts b/examples/vue-and-svelte-language-server/src/index.ts index b2a2c99ce..914d05af3 100644 --- a/examples/vue-and-svelte-language-server/src/index.ts +++ b/examples/vue-and-svelte-language-server/src/index.ts @@ -43,12 +43,12 @@ const plugin: LanguageServerPlugin void) { +export function forEachEmbeddedFile(file: VirtualFile, cb: (embedded: VirtualFile) => void) { cb(file); - for (const child of file.embeddeds) { - forEachEmbeddeds(child, cb); + for (const embeddedFile of file.embeddedFiles) { + forEachEmbeddedFile(embeddedFile, cb); } } -export type DocumentRegistry = ReturnType; +export type VirtualFiles = ReturnType; type Row = [string, ts.IScriptSnapshot, VirtualFile, LanguageModule]; -export function createVirtualFilesHost(languageModules: LanguageModule[]) { +export function createVirtualFiles(languageModules: LanguageModule[]) { const files = reactive>({}); const all = computed(() => Object.values(files)); - const sourceMapsByFileName = computed(() => { + const virtualFileNameToSource = computed(() => { const map = new Map(); for (const row of all.value) { - forEachEmbeddeds(row[2], file => { + forEachEmbeddedFile(row[2], file => { map.set(normalizePath(file.fileName), [file, row]); }); } return map; }); - const teleports = computed(() => { - const map = new Map(); - for (const key in files) { - const [_1, _2, sourceFile] = files[key]!; - forEachEmbeddeds(sourceFile, embedded => { - if (embedded.teleportMappings) { - const _map = getTeleport(embedded); - if (_map) { - map.set(normalizePath(embedded.fileName), _map); - } - } - }); - } - return map; - }); - const _sourceMaps = new WeakMap[], SourceMapBase>>(); - const _teleports = new WeakMap[], Teleport>>(); + const virtualFileToSourceMapsMap = new WeakMap]>>(); + const _teleports = new WeakMap(); return { - update(fileName: string, snapshot: ts.IScriptSnapshot | undefined) { + update(fileName: string, snapshot: ts.IScriptSnapshot) { const key = normalizePath(fileName); - if (snapshot) { - if (files[key]) { - const virtualFile = files[key][2]; - files[key][1] = snapshot; - files[key][3].updateSourceFile(virtualFile, snapshot); - return virtualFile; // updated - } - for (const languageModule of languageModules) { - const virtualFile = languageModule.createSourceFile(fileName, snapshot); - if (virtualFile) { - files[key] = [fileName, snapshot, reactive(virtualFile), languageModule]; - return virtualFile; // created - } + if (files[key]) { + const virtualFile = files[key][2]; + files[key][1] = snapshot; + files[key][3].updateFile(virtualFile, snapshot); + return virtualFile; // updated + } + for (const languageModule of languageModules) { + const virtualFile = languageModule.createFile(fileName, snapshot); + if (virtualFile) { + files[key] = [fileName, snapshot, reactive(virtualFile), languageModule]; + return virtualFile; // created } } - delete files[key]; // deleted + }, + delete(fileName: string) { + const key = normalizePath(fileName); + if (files[key]) { + const virtualFile = files[key][2]; + files[key][3].deleteFile?.(virtualFile); + delete files[key]; // deleted + } }, get(fileName: string) { const key = normalizePath(fileName); @@ -74,12 +64,12 @@ export function createVirtualFilesHost(languageModules: LanguageModule[]) { ] as const; } }, - has: (fileName: string) => !!files[normalizePath(fileName)], + hasSourceFile: (fileName: string) => !!files[normalizePath(fileName)], all: () => all.value, - getTeleport: (fileName: string) => teleports.value.get(normalizePath(fileName)), - getSourceMap, + getTeleport, + getMaps: getSourceMaps, getSourceByVirtualFileName(fileName: string) { - const source = sourceMapsByFileName.value.get(normalizePath(fileName)); + const source = virtualFileNameToSource.value.get(normalizePath(fileName)); if (source) { return [ source[1][0], @@ -90,36 +80,34 @@ export function createVirtualFilesHost(languageModules: LanguageModule[]) { }, }; - function getSourceMap(file: VirtualFile) { - const snapshot = sourceMapsByFileName.value.get(normalizePath(file.fileName))![1][1]; - let map1 = _sourceMaps.get(snapshot); - if (!map1) { - map1 = new WeakMap(); - _sourceMaps.set(snapshot, map1); + function getSourceMaps(virtualFile: VirtualFile) { + const sourceMapsBySourceFileName = virtualFileToSourceMapsMap.get(virtualFile.snapshot) ?? new Map(); + virtualFileToSourceMapsMap.set(virtualFile.snapshot, sourceMapsBySourceFileName); + + const sources = new Set(); + for (const map of virtualFile.mappings) { + sources.add(map.source); } - let map2 = map1.get(file.mappings); - if (!map2) { - map2 = new SourceMapBase(file.mappings); - map1.set(file.mappings, map2); + + for (const source of sources) { + const sourceFileName = source ?? virtualFileNameToSource.value.get(normalizePath(virtualFile.fileName))![1][0]; + if (!sourceMapsBySourceFileName.has(sourceFileName)) { + sourceMapsBySourceFileName.set(sourceFileName, [ + sourceFileName, + new SourceMapBase(virtualFile.mappings.filter(mapping => mapping.source === source)), + ]); + } } - return map2; + + return [...sourceMapsBySourceFileName.values()]; } function getTeleport(file: VirtualFile) { - const snapshot = sourceMapsByFileName.value.get(normalizePath(file.fileName))![1][1]; - let map1 = _teleports.get(snapshot); - if (!map1) { - map1 = new WeakMap(); - _teleports.set(snapshot, map1); - } - if (file.teleportMappings) { - let map2 = map1.get(file.teleportMappings); - if (!map2) { - map2 = new Teleport(file.teleportMappings); - map1.set(file.teleportMappings, map2); - } - return map2; + const snapshot = virtualFileNameToSource.value.get(normalizePath(file.fileName))![1][1]; + if (!_teleports.has(snapshot)) { + _teleports.set(snapshot, file.teleportMappings ? new Teleport(file.teleportMappings) : undefined); } + return _teleports.get(snapshot); } } diff --git a/packages/language-core/src/languageContext.ts b/packages/language-core/src/languageContext.ts index 101984b4a..8aed17965 100644 --- a/packages/language-core/src/languageContext.ts +++ b/packages/language-core/src/languageContext.ts @@ -1,7 +1,7 @@ import { posix as path } from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import { createVirtualFilesHost, forEachEmbeddeds } from './documentRegistry'; -import { LanguageModule, VirtualFile, LanguageServiceHost, EmbeddedFileKind } from './types'; +import { createVirtualFiles, forEachEmbeddedFile } from './documentRegistry'; +import { LanguageModule, LanguageServiceHost, VirtualFileKind } from './types'; export type EmbeddedLanguageContext = ReturnType; @@ -27,12 +27,12 @@ export function createEmbeddedLanguageServiceHost( let lastProjectVersion: string | undefined; let tsProjectVersion = 0; - const documentRegistry = createVirtualFilesHost(languageModules); + const virtualFiles = createVirtualFiles(languageModules); const ts = host.getTypeScriptModule(); const scriptSnapshots = new Map(); const sourceTsFileVersions = new Map(); - const sourceVueFileVersions = new Map(); - const virtualFileVersions = new Map(); + const sourceFileVersions = new Map(); + const virtualFileVersions = new Map(); const _tsHost: Partial = { fileExists: host.fileExists ? fileName => { @@ -42,15 +42,15 @@ export function createEmbeddedLanguageServiceHost( // .vue.d.ts -> [ignored] const vueFileName = fileName.substring(0, fileName.lastIndexOf('.')); - if (!documentRegistry.has(vueFileName)) { + if (!virtualFiles.hasSourceFile(vueFileName)) { // create virtual files const scriptSnapshot = host.getScriptSnapshot(vueFileName); if (scriptSnapshot) { - documentRegistry.update(vueFileName, scriptSnapshot); + virtualFiles.update(vueFileName, scriptSnapshot); } } - if (documentRegistry.getSourceByVirtualFileName(fileName)) { + if (virtualFiles.getSourceByVirtualFileName(fileName)) { return true; } @@ -65,7 +65,7 @@ export function createEmbeddedLanguageServiceHost( getScriptSnapshot, readDirectory: (_path, extensions, exclude, include, depth) => { const result = host.readDirectory?.(_path, extensions, exclude, include, depth) ?? []; - for (const [fileName] of documentRegistry.all()) { + for (const [fileName] of virtualFiles.all()) { const vuePath2 = path.join(_path, path.basename(fileName)); if (path.relative(_path.toLowerCase(), fileName.toLowerCase()).startsWith('..')) { continue; @@ -81,7 +81,7 @@ export function createEmbeddedLanguageServiceHost( }, getScriptKind(fileName) { - if (documentRegistry.has(fileName)) + if (virtualFiles.hasSourceFile(fileName)) return ts.ScriptKind.Deferred; switch (path.extname(fileName)) { @@ -102,10 +102,10 @@ export function createEmbeddedLanguageServiceHost( return target[property] || host[property]; }, }), - mapper: new Proxy(documentRegistry, { + mapper: new Proxy(virtualFiles, { get: (target, property) => { update(); - return target[property as keyof typeof documentRegistry]; + return target[property as keyof typeof virtualFiles]; }, }), }; @@ -120,43 +120,44 @@ export function createEmbeddedLanguageServiceHost( if (!shouldUpdate) return; - let tsFileUpdated = false; + let shouldUpdateTsProject = false; + let virtualFilesUpdatedNum = 0; - const checkRemains = new Set(host.getScriptFileNames()); - const sourceFilesShouldUpdate: [string, VirtualFile, ts.IScriptSnapshot][] = []; + const remainRootFiles = new Set(host.getScriptFileNames()); // .vue - for (const [fileName, _, virtualFile] of documentRegistry.all()) { - checkRemains.delete(fileName); + for (const [fileName] of virtualFiles.all()) { + remainRootFiles.delete(fileName); const snapshot = host.getScriptSnapshot(fileName); if (!snapshot) { // delete - documentRegistry.update(fileName, undefined); - tsFileUpdated = true; + virtualFiles.delete(fileName); + shouldUpdateTsProject = true; continue; } const newVersion = host.getScriptVersion(fileName); - if (sourceVueFileVersions.get(fileName) !== newVersion) { + if (sourceFileVersions.get(fileName) !== newVersion) { // update - sourceVueFileVersions.set(fileName, newVersion); - sourceFilesShouldUpdate.push([fileName, virtualFile, snapshot]); + sourceFileVersions.set(fileName, newVersion); + virtualFiles.update(fileName, snapshot); + virtualFilesUpdatedNum++; } } // no any vue file version change, it mean project version was update by ts file change at this time - if (!sourceFilesShouldUpdate.length) { - tsFileUpdated = true; + if (!virtualFilesUpdatedNum) { + shouldUpdateTsProject = true; } // add - for (const fileName of [...checkRemains]) { + for (const fileName of [...remainRootFiles]) { const snapshot = host.getScriptSnapshot(fileName); if (snapshot) { - const virtualFile = documentRegistry.update(fileName, snapshot); + const virtualFile = virtualFiles.update(fileName, snapshot); if (virtualFile) { - checkRemains.delete(fileName); + remainRootFiles.delete(fileName); } } } @@ -165,7 +166,7 @@ export function createEmbeddedLanguageServiceHost( for (const [oldTsFileName, oldTsFileVersion] of [...sourceTsFileVersions]) { const newVersion = host.getScriptVersion(oldTsFileName); if (oldTsFileVersion !== newVersion) { - if (!checkRemains.has(oldTsFileName) && !host.getScriptSnapshot(oldTsFileName)) { + if (!remainRootFiles.has(oldTsFileName) && !host.getScriptSnapshot(oldTsFileName)) { // delete sourceTsFileVersions.delete(oldTsFileName); } @@ -173,56 +174,32 @@ export function createEmbeddedLanguageServiceHost( // update sourceTsFileVersions.set(oldTsFileName, newVersion); } - tsFileUpdated = true; + shouldUpdateTsProject = true; } } - for (const nowFileName of checkRemains) { + for (const nowFileName of remainRootFiles) { if (!sourceTsFileVersions.has(nowFileName)) { // add const newVersion = host.getScriptVersion(nowFileName); sourceTsFileVersions.set(nowFileName, newVersion); - tsFileUpdated = true; + shouldUpdateTsProject = true; } } - for (const [fileName, virtualFile, snapshot] of sourceFilesShouldUpdate) { - - forEachEmbeddeds(virtualFile, embedded => { - virtualFileVersions.delete(embedded.fileName); - }); - - const oldScripts: Record = {}; - const newScripts: Record = {}; - - if (!tsFileUpdated) { - forEachEmbeddeds(virtualFile, embedded => { - if (embedded.kind === EmbeddedFileKind.TypeScriptHostFile) { - oldScripts[embedded.fileName] = embedded.text; - } - }); - } - - documentRegistry.update(fileName, snapshot); - - if (!tsFileUpdated) { - forEachEmbeddeds(virtualFile, embedded => { - if (embedded.kind === EmbeddedFileKind.TypeScriptHostFile) { - newScripts[embedded.fileName] = embedded.text; + for (const [_1, _2, virtualFile] of virtualFiles.all()) { + if (!shouldUpdateTsProject) { + forEachEmbeddedFile(virtualFile, embedded => { + if (embedded.kind === VirtualFileKind.TypeScriptHostFile) { + if (virtualFileVersions.has(embedded.fileName) && virtualFileVersions.get(embedded.fileName)?.virtualFileSnapshot !== embedded.snapshot) { + shouldUpdateTsProject = true; + } } }); } - - if ( - !tsFileUpdated - && Object.keys(oldScripts).length !== Object.keys(newScripts).length - || Object.keys(oldScripts).some(fileName => oldScripts[fileName] !== newScripts[fileName]) - ) { - tsFileUpdated = true; - } } - if (tsFileUpdated) { + if (shouldUpdateTsProject) { tsProjectVersion++; } } @@ -230,16 +207,16 @@ export function createEmbeddedLanguageServiceHost( const tsFileNames = new Set(); - for (const [_1, _2, sourceFile] of documentRegistry.all()) { - forEachEmbeddeds(sourceFile, embedded => { - if (embedded.kind === EmbeddedFileKind.TypeScriptHostFile) { + for (const [_1, _2, sourceFile] of virtualFiles.all()) { + forEachEmbeddedFile(sourceFile, embedded => { + if (embedded.kind === VirtualFileKind.TypeScriptHostFile) { tsFileNames.add(embedded.fileName); // virtual .ts } }); } for (const fileName of host.getScriptFileNames()) { - if (!documentRegistry.has(fileName)) { + if (!virtualFiles.hasSourceFile(fileName)) { tsFileNames.add(fileName); // .ts } } @@ -247,20 +224,26 @@ export function createEmbeddedLanguageServiceHost( return [...tsFileNames]; } function getScriptVersion(fileName: string) { - let source = documentRegistry.getSourceByVirtualFileName(fileName); + let source = virtualFiles.getSourceByVirtualFileName(fileName); if (source) { - if (virtualFileVersions.has(source[2].fileName)) { - return virtualFileVersions.get(source[2].fileName)!; - } - else { - let version = ts.sys?.createHash?.(source[2].text) ?? source[2].text; - if (host.isTsc) { - // fix https://github.com/johnsoncodehk/volar/issues/1082 - version = host.getScriptVersion(source[0]) + ':' + version; - } + let version = virtualFileVersions.get(source[2].fileName); + if (!version) { + version = { + value: 0, + virtualFileSnapshot: source[2].snapshot, + sourceFileSnapshot: source[1], + }; virtualFileVersions.set(source[2].fileName, version); - return version; } + else if ( + version.virtualFileSnapshot !== source[2].snapshot + || (host.isTsc && version.sourceFileSnapshot !== source[1]) // fix https://github.com/johnsoncodehk/volar/issues/1082 + ) { + version.value++; + version.virtualFileSnapshot = source[2].snapshot; + version.sourceFileSnapshot = source[1]; + } + return version.value.toString(); } return host.getScriptVersion(fileName); } @@ -270,9 +253,9 @@ export function createEmbeddedLanguageServiceHost( if (cache && cache[0] === version) { return cache[1]; } - const source = documentRegistry.getSourceByVirtualFileName(fileName); + const source = virtualFiles.getSourceByVirtualFileName(fileName); if (source) { - const snapshot = ts.ScriptSnapshot.fromString(source[2].text); + const snapshot = source[2].snapshot; scriptSnapshots.set(fileName.toLowerCase(), [version, snapshot]); return snapshot; } diff --git a/packages/language-core/src/sourceMaps.ts b/packages/language-core/src/sourceMaps.ts index 7331379a4..b9260e4ad 100644 --- a/packages/language-core/src/sourceMaps.ts +++ b/packages/language-core/src/sourceMaps.ts @@ -9,7 +9,7 @@ export class Teleport extends SourceMaps.SourceMapBase { } } for (const mapped of this.toSourceOffsets(start)) { - if (!filter || filter(mapped[1].data.toGenedCapabilities)) { + if (!filter || filter(mapped[1].data.toGeneratedCapabilities)) { yield mapped[0]; } } diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index a67a32a88..586a59804 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -40,7 +40,7 @@ export interface TeleportCapabilities { export interface TeleportMappingData { toSourceCapabilities: TeleportCapabilities, - toGenedCapabilities: TeleportCapabilities, + toGeneratedCapabilities: TeleportCapabilities, } export interface TextRange { @@ -48,24 +48,25 @@ export interface TextRange { end: number, } -export enum EmbeddedFileKind { +export enum VirtualFileKind { TextFile = 0, TypeScriptHostFile = 1, } export interface VirtualFile { fileName: string, - text: string, - kind: EmbeddedFileKind, + snapshot: ts.IScriptSnapshot, + kind: VirtualFileKind, capabilities: DocumentCapabilities, mappings: Mapping[], teleportMappings?: Mapping[], - embeddeds: VirtualFile[], + embeddedFiles: VirtualFile[], } export interface LanguageModule { - createSourceFile(fileName: string, snapshot: ts.IScriptSnapshot): T | undefined; - updateSourceFile(virtualFile: T, snapshot: ts.IScriptSnapshot): void; + createFile(fileName: string, snapshot: ts.IScriptSnapshot): T | undefined; + updateFile(virtualFile: T, snapshot: ts.IScriptSnapshot): void; + deleteFile?(virtualFile: T): void; proxyLanguageServiceHost?(host: LanguageServiceHost): Partial; } diff --git a/packages/language-server/src/common/features/customFeatures.ts b/packages/language-server/src/common/features/customFeatures.ts index c8cf4eb53..a60d47c20 100644 --- a/packages/language-server/src/common/features/customFeatures.ts +++ b/packages/language-server/src/common/features/customFeatures.ts @@ -2,7 +2,7 @@ import * as shared from '@volar/shared'; import * as vscode from 'vscode-languageserver'; import type { Workspaces } from '../workspaces'; import { GetMatchTsConfigRequest, ReloadProjectNotification, WriteVirtualFilesNotification, GetVirtualFileNamesRequest, GetVirtualFileRequest, ReportStats } from '../../protocol'; -import { forEachEmbeddeds } from '@volar/language-core'; +import { forEachEmbeddedFile } from '@volar/language-core'; export function register( connection: vscode.Connection, @@ -76,8 +76,8 @@ export function register( if (project) { const sourceFile = project.project?.getLanguageService().context.core.mapper.get(shared.getPathOfUri(document.uri))?.[1]; if (sourceFile) { - forEachEmbeddeds(sourceFile, e => { - if (e.text && e.kind === 1) { + forEachEmbeddedFile(sourceFile, e => { + if (e.snapshot.getLength() && e.kind === 1) { fileNames.push(e.fileName); } }); @@ -88,11 +88,18 @@ export function register( connection.onRequest(GetVirtualFileRequest.type, async params => { const project = await projects.getProject(params.sourceFileUri); if (project) { - const virtualFile = project.project?.getLanguageService().context.core.mapper.getSourceByVirtualFileName(params.virtualFileName)?.[2]; - if (virtualFile) { + const sourceAndVirtual = project.project?.getLanguageService().context.core.mapper.getSourceByVirtualFileName(params.virtualFileName); + if (sourceAndVirtual) { + const virtualFile = sourceAndVirtual[2]; + const mappings: Record = {}; + for (const mapping of virtualFile.mappings) { + const sourceUri = shared.getUriByPath(mapping.source ?? sourceAndVirtual[0]); + mappings[sourceUri] ??= []; + mappings[sourceUri].push(mapping); + } return { - content: virtualFile.text, - mappings: virtualFile.mappings as any, + content: virtualFile.snapshot.getText(0, virtualFile.snapshot.getLength()), + mappings, }; } } diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index d02947992..2d59233e8 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -80,11 +80,11 @@ export namespace GetVirtualFileRequest { }; export type ResponseType = { content: string; - mappings: { + mappings: Record; }; export type ErrorType = never; export const type = new vscode.RequestType('vue/virtualFile'); diff --git a/packages/language-service/src/baseDocumentService.ts b/packages/language-service/src/baseDocumentService.ts index dbf1d2d3c..0cdfdfc11 100644 --- a/packages/language-service/src/baseDocumentService.ts +++ b/packages/language-service/src/baseDocumentService.ts @@ -1,4 +1,4 @@ -import { createVirtualFilesHost, LanguageModule } from '@volar/language-core'; +import { createVirtualFiles, LanguageModule } from '@volar/language-core'; import * as shared from '@volar/shared'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as autoInsert from './documentFeatures/autoInsert'; @@ -9,7 +9,7 @@ import * as foldingRanges from './documentFeatures/foldingRanges'; import * as format from './documentFeatures/format'; import * as linkedEditingRanges from './documentFeatures/linkedEditingRanges'; import * as selectionRanges from './documentFeatures/selectionRanges'; -import { parseSourceFileDocuments } from './documents'; +import { createDocumentsAndSourceMaps } from './documents'; import { DocumentServiceRuntimeContext, LanguageServicePlugin, LanguageServicePluginContext } from './types'; import { singleFileTypeScriptServiceHost, updateSingleFileTypeScriptServiceHost } from './utils/singleFileTypeScriptService'; @@ -25,7 +25,7 @@ export function createDocumentServiceContext(options: { env: LanguageServicePluginContext['env']; }) { - let plugins: LanguageServicePlugin[]; + let plugins: LanguageServicePlugin[] | undefined; const ts = options.ts; const pluginContext: LanguageServicePluginContext = { @@ -38,8 +38,8 @@ export function createDocumentServiceContext(options: { }; const languageModules = options.getLanguageModules(); const lastUpdateVersions = new Map(); - const virtualFiles = createVirtualFilesHost(languageModules); - const textDocumentMapper = parseSourceFileDocuments(virtualFiles); + const virtualFiles = createVirtualFiles(languageModules); + const textDocumentMapper = createDocumentsAndSourceMaps(virtualFiles); const context: DocumentServiceRuntimeContext = { typescript: ts, get plugins() { @@ -52,14 +52,14 @@ export function createDocumentServiceContext(options: { return plugins; }, pluginContext, - getVirtualDocuments(document) { + documents: textDocumentMapper, + update(document) { let lastVersion = lastUpdateVersions.get(document.uri); if (lastVersion === undefined || lastVersion !== document.version) { const fileName = shared.getPathOfUri(document.uri); virtualFiles.update(fileName, ts.ScriptSnapshot.fromString(document.getText())); lastUpdateVersions.set(document.uri, document.version); } - return textDocumentMapper.get(document.uri); }, updateVirtualFile(fileName, snapshot) { virtualFiles.update(fileName, snapshot); diff --git a/packages/language-service/src/baseLanguageService.ts b/packages/language-service/src/baseLanguageService.ts index 590d2d83c..1371236b2 100644 --- a/packages/language-service/src/baseLanguageService.ts +++ b/packages/language-service/src/baseLanguageService.ts @@ -2,7 +2,7 @@ import { createEmbeddedLanguageServiceHost, LanguageServiceHost } from '@volar/l import * as shared from '@volar/shared'; import * as tsFaster from '@volar/typescript-faster'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { parseSourceFileDocuments } from './documents'; +import { createDocumentsAndSourceMaps } from './documents'; import * as autoInsert from './languageFeatures/autoInsert'; import * as callHierarchy from './languageFeatures/callHierarchy'; import * as codeActionResolve from './languageFeatures/codeActionResolve'; @@ -56,7 +56,7 @@ export function createLanguageServiceContext(options: { languageService: tsLs, }, }; - const textDocumentMapper = parseSourceFileDocuments(options.context.mapper); + const textDocumentMapper = createDocumentsAndSourceMaps(options.context.mapper); const documents = new WeakMap(); const documentVersions = new Map(); const context: LanguageServiceRuntimeContext = { diff --git a/packages/language-service/src/documentFeatures/colorPresentations.ts b/packages/language-service/src/documentFeatures/colorPresentations.ts index 17cd1d80f..00821ca6d 100644 --- a/packages/language-service/src/documentFeatures/colorPresentations.ts +++ b/packages/language-service/src/documentFeatures/colorPresentations.ts @@ -12,7 +12,7 @@ export function register(context: DocumentServiceRuntimeContext) { context, document, range, - map => !!map.file.capabilities.documentSymbol, // TODO: add color capabilitie setting + (file) => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting (range, map) => map.toGeneratedRanges(range), (plugin, document, range) => plugin.getColorPresentations?.(document, color, range), (data, map) => data.map(cp => { diff --git a/packages/language-service/src/documentFeatures/documentColors.ts b/packages/language-service/src/documentFeatures/documentColors.ts index b3848260d..6a9b78aa6 100644 --- a/packages/language-service/src/documentFeatures/documentColors.ts +++ b/packages/language-service/src/documentFeatures/documentColors.ts @@ -11,7 +11,7 @@ export function register(context: DocumentServiceRuntimeContext) { return documentFeatureWorker( context, document, - map => !!map.file.capabilities.documentSymbol, // TODO: add color capabilitie setting + (file) => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting (plugin, document) => plugin.findDocumentColors?.(document), (data, map) => data.map(color => { const range = map.toSourceRange(color.range); diff --git a/packages/language-service/src/documentFeatures/documentSymbols.ts b/packages/language-service/src/documentFeatures/documentSymbols.ts index 5dd530197..4ae1c96f6 100644 --- a/packages/language-service/src/documentFeatures/documentSymbols.ts +++ b/packages/language-service/src/documentFeatures/documentSymbols.ts @@ -11,7 +11,7 @@ export function register(context: DocumentServiceRuntimeContext) { return documentFeatureWorker( context, document, - map => !!map.file.capabilities.documentSymbol, // TODO: add color capabilitie setting + file => !!file.capabilities.documentSymbol, // TODO: add color capabilitie setting (plugin, document) => plugin.findDocumentSymbols?.(document), (data, map) => transformSymbolInformations( data, diff --git a/packages/language-service/src/documentFeatures/foldingRanges.ts b/packages/language-service/src/documentFeatures/foldingRanges.ts index 0a1d95d14..e8ff341c6 100644 --- a/packages/language-service/src/documentFeatures/foldingRanges.ts +++ b/packages/language-service/src/documentFeatures/foldingRanges.ts @@ -11,7 +11,7 @@ export function register(context: DocumentServiceRuntimeContext) { return documentFeatureWorker( context, document, - map => !!map.file.capabilities.foldingRange, + file => !!file.capabilities.foldingRange, (plugin, document) => plugin.getFoldingRanges?.(document), (data, sourceMap) => transformFoldingRanges(data, range => sourceMap?.toSourceRange(range)), arr => arr.flat(), diff --git a/packages/language-service/src/documentFeatures/format.ts b/packages/language-service/src/documentFeatures/format.ts index 7b9337c1d..039f5b6f0 100644 --- a/packages/language-service/src/documentFeatures/format.ts +++ b/packages/language-service/src/documentFeatures/format.ts @@ -1,7 +1,7 @@ import type { VirtualFile } from '@volar/language-core'; import * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { EmbeddedDocumentSourceMap, SourceFileDocument } from '../documents'; +import { SourceMap } from '../documents'; import type { DocumentServiceRuntimeContext } from '../types'; export function register(context: DocumentServiceRuntimeContext) { @@ -22,13 +22,13 @@ export function register(context: DocumentServiceRuntimeContext) { range = vscode.Range.create(document.positionAt(0), document.positionAt(document.getText().length)); } - const vueDocument = context.getVirtualDocuments(document); + const virtualFile = context.documents.getVirtualFileByUri(document.uri); const originalDocument = document; const rootEdits = onTypeParams ? await tryFormat(document, onTypeParams.position, undefined, onTypeParams.ch) : await tryFormat(document, range, undefined); - if (!vueDocument) + if (!virtualFile) return rootEdits; if (rootEdits?.length) { @@ -43,9 +43,8 @@ export function register(context: DocumentServiceRuntimeContext) { tryUpdateVueDocument(); - const embeddeds = getEmbeddedsByLevel(vueDocument, level++); - - if (embeddeds.length === 0) + const embeddedFiles = getEmbeddedFilesByLevel(virtualFile, level++); + if (embeddedFiles.length === 0) break; let edits: vscode.TextEdit[] = []; @@ -53,16 +52,17 @@ export function register(context: DocumentServiceRuntimeContext) { sourceMapEmbeddedDocumentUri: string, } | undefined; - for (const embedded of embeddeds) { + for (const embedded of embeddedFiles) { if (!embedded.capabilities.documentFormatting) continue; - const map = vueDocument.maps.get(embedded); + const maps = [...context.documents.getMapsByVirtualFileName(embedded.fileName)]; + const map = maps.find(map => map[1].sourceFileDocument.uri === document.uri)?.[1]; if (!map) continue; - const initialIndentBracket = typeof embedded.capabilities.documentFormatting === 'object' && initialIndentLanguageId[map.mappedDocument.languageId] + const initialIndentBracket = typeof embedded.capabilities.documentFormatting === 'object' && initialIndentLanguageId[map.virtualFileDocument.languageId] ? embedded.capabilities.documentFormatting.initialIndentBracket : undefined; @@ -74,7 +74,7 @@ export function register(context: DocumentServiceRuntimeContext) { if (embeddedPosition) { _edits = await tryFormat( - map.mappedDocument, + map.virtualFileDocument, embeddedPosition, initialIndentBracket, onTypeParams.ch, @@ -87,15 +87,15 @@ export function register(context: DocumentServiceRuntimeContext) { let genRange = map.toGeneratedRange(range); if (!genRange) { - const firstMapping = map.mappings.sort((a, b) => a.sourceRange[0] - b.sourceRange[0])[0]; - const lastMapping = map.mappings.sort((a, b) => b.sourceRange[0] - a.sourceRange[0])[0]; + const firstMapping = map.map.mappings.sort((a, b) => a.sourceRange[0] - b.sourceRange[0])[0]; + const lastMapping = map.map.mappings.sort((a, b) => b.sourceRange[0] - a.sourceRange[0])[0]; if ( firstMapping && document.offsetAt(range.start) < firstMapping.sourceRange[0] && lastMapping && document.offsetAt(range.end) > lastMapping.sourceRange[1] ) { genRange = { - start: map.mappedDocument.positionAt(firstMapping.generatedRange[0]), - end: map.mappedDocument.positionAt(lastMapping.generatedRange[1]), + start: map.virtualFileDocument.positionAt(firstMapping.generatedRange[0]), + end: map.virtualFileDocument.positionAt(lastMapping.generatedRange[1]), }; } } @@ -103,11 +103,11 @@ export function register(context: DocumentServiceRuntimeContext) { if (genRange) { toPatchIndent = { - sourceMapEmbeddedDocumentUri: map.mappedDocument.uri, + sourceMapEmbeddedDocumentUri: map.virtualFileDocument.uri, }; _edits = await tryFormat( - map.mappedDocument, + map.virtualFileDocument, genRange, initialIndentBracket, ); @@ -136,11 +136,12 @@ export function register(context: DocumentServiceRuntimeContext) { tryUpdateVueDocument(); - const sourceMap = [...vueDocument.maps.values()].find(map => map.mappedDocument.uri === toPatchIndent?.sourceMapEmbeddedDocumentUri); + const maps = [...context.documents.getMapsByVirtualFileName(virtualFile.fileName)]; + const map = maps.find(map => map[1].sourceFileDocument.uri === toPatchIndent?.sourceMapEmbeddedDocumentUri)?.[1]; - if (sourceMap) { + if (map) { - const indentEdits = patchInterpolationIndent(vueDocument, sourceMap); + const indentEdits = patchInterpolationIndent(context.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName), map); if (indentEdits.length > 0) { applyEdits(indentEdits); @@ -161,29 +162,28 @@ export function register(context: DocumentServiceRuntimeContext) { return [textEdit]; function tryUpdateVueDocument() { - if (vueDocument && vueDocument.file.text !== document.getText()) { - context.updateVirtualFile(vueDocument.fileName, ts.ScriptSnapshot.fromString(document.getText())); + if (virtualFile && virtualFile.snapshot.getText(0, virtualFile.snapshot.getLength()) !== document.getText()) { + context.updateVirtualFile(virtualFile.fileName, ts.ScriptSnapshot.fromString(document.getText())); } } - function getEmbeddedsByLevel(vueDocument: SourceFileDocument, level: number) { + function getEmbeddedFilesByLevel(rootFile: VirtualFile, level: number) { - const embeddeds = vueDocument.file.embeddeds; - const embeddedsLevels: VirtualFile[][] = [embeddeds]; + const embeddedFilesByLevel: VirtualFile[][] = [rootFile.embeddedFiles]; while (true) { - if (embeddedsLevels.length > level) - return embeddedsLevels[level]; + if (embeddedFilesByLevel.length > level) + return embeddedFilesByLevel[level]; - let nextEmbeddeds: VirtualFile[] = []; + let nextLevel: VirtualFile[] = []; - for (const embeddeds of embeddedsLevels[embeddedsLevels.length - 1]) { + for (const file of embeddedFilesByLevel[embeddedFilesByLevel.length - 1]) { - nextEmbeddeds = nextEmbeddeds.concat(embeddeds.embeddeds); + nextLevel = nextLevel.concat(file.embeddedFiles); } - embeddedsLevels.push(nextEmbeddeds); + embeddedFilesByLevel.push(nextLevel); } } @@ -281,12 +281,11 @@ export function register(context: DocumentServiceRuntimeContext) { }; } -function patchInterpolationIndent(vueDocument: SourceFileDocument, map: EmbeddedDocumentSourceMap) { +function patchInterpolationIndent(document: TextDocument, map: SourceMap) { const indentTextEdits: vscode.TextEdit[] = []; - const document = vueDocument.document; - for (const mapped of map.mappings) { + for (const mapped of map.map.mappings) { const textRange = { start: document.positionAt(mapped.sourceRange[0]), diff --git a/packages/language-service/src/documentFeatures/selectionRanges.ts b/packages/language-service/src/documentFeatures/selectionRanges.ts index 3c6dbdfc6..3c9f606be 100644 --- a/packages/language-service/src/documentFeatures/selectionRanges.ts +++ b/packages/language-service/src/documentFeatures/selectionRanges.ts @@ -13,7 +13,7 @@ export function register(context: DocumentServiceRuntimeContext) { context, document, positions, - map => !!map.file.capabilities.documentFormatting, + file => !!file.capabilities.documentFormatting, (positions, map) => { const result = positions .map(position => map.toGeneratedPosition(position)) diff --git a/packages/language-service/src/documents.ts b/packages/language-service/src/documents.ts index 9d6c17a25..ae60081c0 100644 --- a/packages/language-service/src/documents.ts +++ b/packages/language-service/src/documents.ts @@ -1,22 +1,19 @@ -import { DocumentRegistry, VirtualFile, forEachEmbeddeds, PositionCapabilities, TeleportMappingData } from '@volar/language-core'; +import { VirtualFiles, VirtualFile, PositionCapabilities, TeleportMappingData, Teleport, forEachEmbeddedFile } from '@volar/language-core'; import * as shared from '@volar/shared'; import { Mapping, SourceMapBase } from '@volar/source-map'; -import ts = require('typescript'); import * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import type * as ts from 'typescript/lib/tsserverlibrary'; -export type SourceFileDocuments = ReturnType; -export type SourceFileDocument = NonNullable['get']>>; +export type DocumentsAndSourceMaps = ReturnType; -export class SourceMap extends SourceMapBase { +export class SourceMap { constructor( - public sourceDocument: TextDocument, - public mappedDocument: TextDocument, - public mappings: Mapping[], - ) { - super(mappings); - } + public sourceFileDocument: TextDocument, + public virtualFileDocument: TextDocument, + public map: SourceMapBase, + ) { } // Range APIs @@ -94,11 +91,11 @@ export class SourceMap extends SourceMapBase { } public toSourcePositionsBase(position: vscode.Position, filter: (data: Data) => boolean = () => true, baseOffset: 'left' | 'right' = 'left') { - return this.toPositions(position, filter, this.mappedDocument, this.sourceDocument, 'generatedRange', 'sourceRange', baseOffset); + return this.toPositions(position, filter, this.virtualFileDocument, this.sourceFileDocument, 'generatedRange', 'sourceRange', baseOffset); } public toGeneratedPositionsBase(position: vscode.Position, filter: (data: Data) => boolean = () => true, baseOffset: 'left' | 'right' = 'left') { - return this.toPositions(position, filter, this.sourceDocument, this.mappedDocument, 'sourceRange', 'generatedRange', baseOffset); + return this.toPositions(position, filter, this.sourceFileDocument, this.virtualFileDocument, 'sourceRange', 'generatedRange', baseOffset); } protected * toPositions( @@ -110,7 +107,7 @@ export class SourceMap extends SourceMapBase { to: 'sourceRange' | 'generatedRange', baseOffset: 'left' | 'right', ) { - for (const mapped of this.matcing(fromDoc.offsetAt(position), from, to, baseOffset === 'right')) { + for (const mapped of this.map.matcing(fromDoc.offsetAt(position), from, to, baseOffset === 'right')) { if (!filter(mapped[1].data)) { continue; } @@ -124,44 +121,31 @@ export class SourceMap extends SourceMapBase { } protected matchSourcePosition(position: vscode.Position, mapping: Mapping, baseOffset: 'left' | 'right') { - let offset = this.matchOffset(this.mappedDocument.offsetAt(position), mapping['generatedRange'], mapping['sourceRange'], baseOffset === 'right'); + let offset = this.map.matchOffset(this.virtualFileDocument.offsetAt(position), mapping['generatedRange'], mapping['sourceRange'], baseOffset === 'right'); if (offset !== undefined) { - return this.sourceDocument.positionAt(offset); + return this.sourceFileDocument.positionAt(offset); } } protected matchGeneratedPosition(position: vscode.Position, mapping: Mapping, baseOffset: 'left' | 'right') { - let offset = this.matchOffset(this.sourceDocument.offsetAt(position), mapping['sourceRange'], mapping['generatedRange'], baseOffset === 'right'); + let offset = this.map.matchOffset(this.sourceFileDocument.offsetAt(position), mapping['sourceRange'], mapping['generatedRange'], baseOffset === 'right'); if (offset !== undefined) { - return this.mappedDocument.positionAt(offset); + return this.virtualFileDocument.positionAt(offset); } } } -export class EmbeddedDocumentSourceMap extends SourceMap { - - constructor( - public rootFile: VirtualFile, - public file: VirtualFile, - public sourceDocument: TextDocument, - public mappedDocument: TextDocument, - mappings: Mapping[], - ) { - super(sourceDocument, mappedDocument, mappings); - } -} - export class TeleportSourceMap extends SourceMap { constructor( public file: VirtualFile, public document: TextDocument, - mappings: Mapping[], + map: SourceMapBase, ) { - super(document, document, mappings); + super(document, document, map); } *findTeleports(start: vscode.Position) { for (const mapped of this.toGeneratedPositionsBase(start)) { - yield [mapped[0], mapped[1].data.toGenedCapabilities] as const; + yield [mapped[0], mapped[1].data.toGeneratedCapabilities] as const; } for (const mapped of this.toSourcePositionsBase(start)) { yield [mapped[0], mapped[1].data.toSourceCapabilities] as const; @@ -169,104 +153,112 @@ export class TeleportSourceMap extends SourceMap { } } -export function parseSourceFileDocuments(mapper: DocumentRegistry) { +export function createDocumentsAndSourceMaps(mapper: VirtualFiles) { let version = 0; - const snapshotsToMaps = new WeakMap, - teleports: Map, - }>(); + const _maps = new WeakMap, [VirtualFile, SourceMap]>(); + const _teleports = new WeakMap(); + const _documents = new WeakMap(); return { - get: (uri: string) => { - - const fileName = shared.getPathOfUri(uri); - const virtualFile = mapper.get(fileName); - - if (virtualFile) { - return getMaps(fileName, virtualFile[0], virtualFile[1]); + getRootFileBySourceFileUri(sourceFileUri: string) { + const fileName = shared.getPathOfUri(sourceFileUri); + const rootFile = mapper.get(fileName); + if (rootFile) { + return rootFile[1]; } }, - getTeleport(virtualFileUri: string) { + getVirtualFileByUri(virtualFileUri: string) { + return mapper.getSourceByVirtualFileName(shared.getPathOfUri(virtualFileUri))?.[2]; + }, + getTeleportByUri(virtualFileUri: string) { const fileName = shared.getPathOfUri(virtualFileUri); - const source = mapper.getSourceByVirtualFileName(fileName); - if (source) { - const maps = getMaps(source[0], source[1], source[2]); - for (const [_, teleport] of maps.teleports) { - if (teleport.file.fileName.toLowerCase() === fileName.toLowerCase()) { - return teleport; + const virtualFile = mapper.getSourceByVirtualFileName(fileName)?.[2]; + if (virtualFile) { + const teleport = mapper.getTeleport(virtualFile); + if (teleport) { + if (!_teleports.has(teleport)) { + _teleports.set(teleport, new TeleportSourceMap( + virtualFile, + getDocumentByFileName(virtualFile.snapshot, fileName), + teleport, + )); } + return _teleports.get(teleport); } } }, - getMap(virtualFileUri: string) { - const fileName = shared.getPathOfUri(virtualFileUri); - const source = mapper.getSourceByVirtualFileName(fileName); + getMapsBySourceFileUri(uri: string) { + return this.getMapsBySourceFileName(shared.getPathOfUri(uri)); + }, + getMapsBySourceFileName(fileName: string) { + const source = mapper.get(fileName); if (source) { - const maps = getMaps(source[0], source[1], source[2]); - for (const [_, map] of maps.maps) { - if (map.file.fileName.toLowerCase() === fileName.toLowerCase()) { - return map; + const result: [VirtualFile, SourceMap][] = []; + forEachEmbeddedFile(source[1], (embedded) => { + for (const [sourceFileName, map] of mapper.getMaps(embedded)) { + if (sourceFileName === fileName) { + if (!_maps.has(map)) { + _maps.set(map, [ + embedded, + new SourceMap( + getDocumentByFileName(source[0], sourceFileName), + getDocumentByFileName(embedded.snapshot, fileName), + map, + ) + ]); + } + if (_maps.has(map)) { + result.push(_maps.get(map)!); + } + } + } + }); + return { + snapshot: source[0], + maps: result, + }; + } + }, + getMapsByVirtualFileUri(virtualFileUri: string) { + return this.getMapsByVirtualFileName(shared.getPathOfUri(virtualFileUri)); + }, + *getMapsByVirtualFileName(virtualFileName: string): IterableIterator<[VirtualFile, SourceMap]> { + const virtualFile = mapper.getSourceByVirtualFileName(virtualFileName)?.[2]; + if (virtualFile) { + for (const [sourceFileName, map] of mapper.getMaps(virtualFile)) { + if (!_maps.has(map)) { + const sourceSnapshot = mapper.get(sourceFileName)?.[0]; + if (sourceSnapshot) { + _maps.set(map, [virtualFile, new SourceMap( + getDocumentByFileName(sourceSnapshot, sourceFileName), + getDocumentByFileName(virtualFile.snapshot, virtualFileName), + map, + )]); + } + } + if (_maps.has(map)) { + yield _maps.get(map)!; } } } }, + getDocumentByUri(snapshot: ts.IScriptSnapshot, uri: string) { + return this.getDocumentByFileName(snapshot, shared.getPathOfUri(uri)); + }, + getDocumentByFileName, }; - function getMaps(fileName: string, snapshot: ts.IScriptSnapshot, rootFile: VirtualFile) { - - let result = snapshotsToMaps.get(snapshot); - - if (!result) { - - const document = TextDocument.create( + function getDocumentByFileName(snapshot: ts.IScriptSnapshot, fileName: string) { + if (!_documents.has(snapshot)) { + _documents.set(snapshot, TextDocument.create( shared.getUriByPath(fileName), - 'vue', + shared.syntaxToLanguageId(fileName.substring(fileName.lastIndexOf('.') + 1)), version++, snapshot.getText(0, snapshot.getLength()), - ); - const maps = new Map(); - const teleports = new Map(); - - forEachEmbeddeds(rootFile, file => { - const virtualFileDocument = TextDocument.create( - shared.getUriByPath(file.fileName), - shared.syntaxToLanguageId(file.fileName.substring(file.fileName.lastIndexOf('.') + 1)), - version++, - file.text, - ); - maps.set(file, new EmbeddedDocumentSourceMap( - rootFile, - file, - document, - virtualFileDocument, - file.mappings, - )); - if (file.teleportMappings) { - teleports.set(file, new TeleportSourceMap( - file, - virtualFileDocument, - file.teleportMappings, - )); - } - }); - - result = { - fileName, - snapshot, - document, - file: rootFile, - maps, - teleports, - }; - snapshotsToMaps.set(snapshot, result); + )); } - - return result; + return _documents.get(snapshot)!; } } diff --git a/packages/language-service/src/languageFeatures/autoInsert.ts b/packages/language-service/src/languageFeatures/autoInsert.ts index c7ff0ca3a..6653e9f18 100644 --- a/packages/language-service/src/languageFeatures/autoInsert.ts +++ b/packages/language-service/src/languageFeatures/autoInsert.ts @@ -14,7 +14,7 @@ export function register(context: LanguageServiceRuntimeContext) { function* (arg, map) { for (const position of map.toGeneratedPositions(arg.position, data => !!data.completion)) { - const rangeOffset = map.toGeneratedOffset(arg.autoInsertContext.lastChange.rangeOffset)?.[0]; + const rangeOffset = map.map.toGeneratedOffset(arg.autoInsertContext.lastChange.rangeOffset)?.[0]; const range = map.toGeneratedRange(arg.autoInsertContext.lastChange.range); if (rangeOffset !== undefined && range) { diff --git a/packages/language-service/src/languageFeatures/callHierarchy.ts b/packages/language-service/src/languageFeatures/callHierarchy.ts index 60b0d6479..ec4222c66 100644 --- a/packages/language-service/src/languageFeatures/callHierarchy.ts +++ b/packages/language-service/src/languageFeatures/callHierarchy.ts @@ -37,7 +37,7 @@ export function register(context: LanguageServiceRuntimeContext) { originalItem: item, pluginId: context.plugins.indexOf(plugin), map: map ? { - embeddedDocumentUri: map.mappedDocument.uri, + embeddedDocumentUri: map.virtualFileDocument.uri, } : undefined, } satisfies PluginCallHierarchyData, }; @@ -69,9 +69,7 @@ export function register(context: LanguageServiceRuntimeContext) { if (data.map) { - const sourceMap = context.documents.getMap(data.map.embeddedDocumentUri); - - if (sourceMap) { + if (context.documents.getVirtualFileByUri(data.map.embeddedDocumentUri)) { const _calls = await plugin.callHierarchy.onIncomingCalls(originalItem); @@ -130,9 +128,7 @@ export function register(context: LanguageServiceRuntimeContext) { if (data.map) { - const sourceMap = context.documents.getMap(data.map.embeddedDocumentUri); - - if (sourceMap) { + if (context.documents.getVirtualFileByUri(data.map.embeddedDocumentUri)) { const _calls = await plugin.callHierarchy.onOutgoingCalls(originalItem); @@ -175,41 +171,43 @@ export function register(context: LanguageServiceRuntimeContext) { function transformCallHierarchyItem(tsItem: vscode.CallHierarchyItem, tsRanges: vscode.Range[]): [vscode.CallHierarchyItem, vscode.Range[]] | undefined { - const map = context.documents.getMap(tsItem.uri); - if (!map) - return [tsItem, tsRanges]; // not virtual file + if (!context.documents.getVirtualFileByUri(tsItem.uri)) + return [tsItem, tsRanges]; + + for (const [_, map] of context.documents.getMapsByVirtualFileUri(tsItem.uri)) { + + let range = map.toSourceRange(tsItem.range); + if (!range) { + // TODO: