diff --git a/packages/kit/lib/createChecker.ts b/packages/kit/lib/createChecker.ts index f2c2cbf8..2e09344a 100644 --- a/packages/kit/lib/createChecker.ts +++ b/packages/kit/lib/createChecker.ts @@ -1,19 +1,20 @@ -import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, ServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, resolveCommonLanguageId, TypeScriptProjectHost } from '@volar/language-service'; +import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, resolveCommonLanguageId, TypeScriptProjectHost } from '@volar/language-service'; import * as path from 'typesafe-path/posix'; import * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { createServiceEnvironment } from './createServiceEnvironment'; import { asPosix, defaultCompilerOptions, fileNameToUri, uriToFileName } from './utils'; -import { createLanguage } from '@volar/typescript'; +import { createTypeScriptLanguage } from '@volar/typescript'; export function createTypeScriptChecker( languages: LanguagePlugin[], - services: ServicePlugin[], + services: LanguageServicePlugin[], tsconfig: string, ) { const tsconfigPath = asPosix(tsconfig); - return createTypeScriptCheckerWorker(languages, services, tsconfigPath, env => { + return createTypeScriptCheckerWorker(languages, services, env => { return createTypeScriptLanguageHost( + tsconfigPath, env, () => { const parsed = ts.parseJsonSourceFileConfigFileContent( @@ -34,12 +35,13 @@ export function createTypeScriptChecker( export function createTypeScriptInferredChecker( languages: LanguagePlugin[], - services: ServicePlugin[], + services: LanguageServicePlugin[], getScriptFileNames: () => string[], compilerOptions = defaultCompilerOptions ) { - return createTypeScriptCheckerWorker(languages, services, undefined, env => { + return createTypeScriptCheckerWorker(languages, services, env => { return createTypeScriptLanguageHost( + undefined, env, () => ({ options: compilerOptions, @@ -51,8 +53,7 @@ export function createTypeScriptInferredChecker( function createTypeScriptCheckerWorker( languages: LanguagePlugin[], - services: ServicePlugin[], - configFileName: string | undefined, + services: LanguageServicePlugin[], getProjectHost: (env: ServiceEnvironment) => TypeScriptProjectHost ) { @@ -70,17 +71,10 @@ function createTypeScriptCheckerWorker( }; }; - const languageHost = getProjectHost(env); - const language = createLanguage( + const language = createTypeScriptLanguage( ts, - ts.sys, languages, - configFileName, - languageHost, - { - fileNameToFileId: env.typescript!.fileNameToUri, - fileIdToFileName: env.typescript!.uriToFileName, - }, + getProjectHost(env), ); const service = createLanguageService( language, @@ -93,7 +87,7 @@ function createTypeScriptCheckerWorker( check, fixErrors, printErrors, - languageHost, + language, // settings get settings() { @@ -131,9 +125,9 @@ function createTypeScriptCheckerWorker( async function fixErrors(fileName: string, diagnostics: Diagnostic[], only: string[] | undefined, writeFile: (fileName: string, newText: string) => Promise) { fileName = asPosix(fileName); const uri = fileNameToUri(fileName); - const sourceFile = service.context.language.files.get(uri); - if (sourceFile) { - const document = service.context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const sourceScript = service.context.language.scripts.get(uri); + if (sourceScript) { + const document = service.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); const range = { start: document.positionAt(0), end: document.positionAt(document.getText().length) }; const codeActions = await service.doCodeActions(uri, range, { diagnostics, only, triggerKind: 1 satisfies typeof CodeActionTriggerKind.Invoked }); if (codeActions) { @@ -147,7 +141,7 @@ function createTypeScriptCheckerWorker( for (const uri in rootEdit.changes ?? {}) { const edits = rootEdit.changes![uri]; if (edits.length) { - const editFile = service.context.language.files.get(uri); + const editFile = service.context.language.scripts.get(uri); if (editFile) { const editDocument = service.context.documents.get(uri, editFile.languageId, editFile.snapshot); const newString = TextDocument.applyEdits(editDocument, edits); @@ -157,7 +151,7 @@ function createTypeScriptCheckerWorker( } for (const change of rootEdit.documentChanges ?? []) { if ('textDocument' in change) { - const editFile = service.context.language.files.get(change.textDocument.uri); + const editFile = service.context.language.scripts.get(change.textDocument.uri); if (editFile) { const editDocument = service.context.documents.get(change.textDocument.uri, editFile.languageId, editFile.snapshot); const newString = TextDocument.applyEdits(editDocument, change.edits); @@ -182,8 +176,8 @@ function createTypeScriptCheckerWorker( function formatErrors(fileName: string, diagnostics: Diagnostic[], rootPath: string) { fileName = asPosix(fileName); const uri = fileNameToUri(fileName); - const sourceFile = service.context.language.files.get(uri)!; - const document = service.context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const sourceScript = service.context.language.scripts.get(uri)!; + const document = service.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); const errors: ts.Diagnostic[] = diagnostics.map(diagnostic => ({ category: diagnostic.severity === 1 satisfies typeof DiagnosticSeverity.Error ? ts.DiagnosticCategory.Error : ts.DiagnosticCategory.Warning, code: diagnostic.code as number, @@ -202,6 +196,7 @@ function createTypeScriptCheckerWorker( } function createTypeScriptLanguageHost( + configFileName: string | undefined, env: ServiceEnvironment, createParsedCommandLine: () => Pick ) { @@ -212,6 +207,8 @@ function createTypeScriptLanguageHost( let shouldCheckRootFiles = false; const host: TypeScriptProjectHost = { + ...ts.sys, + configFileName, getCurrentDirectory: () => { return uriToFileName(env.workspaceFolder); }, @@ -239,6 +236,8 @@ function createTypeScriptLanguageHost( return scriptSnapshotsCache.get(fileName); }, getLanguageId: resolveCommonLanguageId, + fileNameToScriptId: env.typescript!.fileNameToUri, + scriptIdToFileName: env.typescript!.uriToFileName, }; env.onDidChangeWatchedFiles?.(({ changes }) => { diff --git a/packages/kit/lib/createFormatter.ts b/packages/kit/lib/createFormatter.ts index becde8fa..24e44daa 100644 --- a/packages/kit/lib/createFormatter.ts +++ b/packages/kit/lib/createFormatter.ts @@ -1,20 +1,20 @@ -import { FormattingOptions, LanguagePlugin, ServicePlugin, createFileRegistry, createLanguageService } from '@volar/language-service'; +import { FormattingOptions, LanguagePlugin, LanguageServicePlugin, createLanguage, createLanguageService } from '@volar/language-service'; import * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { createServiceEnvironment } from './createServiceEnvironment'; export function createFormatter( languages: LanguagePlugin[], - services: ServicePlugin[] + services: LanguageServicePlugin[] ) { let fakeUri = 'file:///dummy.txt'; let settings = {}; const env = createServiceEnvironment(() => settings); - const files = createFileRegistry(languages, false, () => { }); + const language = createLanguage(languages, false, () => { }); const service = createLanguageService( - { files }, + language, services, env, ); @@ -33,7 +33,7 @@ export function createFormatter( async function format(content: string, languageId: string, options: FormattingOptions): Promise { const snapshot = ts.ScriptSnapshot.fromString(content); - files.set(fakeUri, languageId, snapshot); + language.scripts.set(fakeUri, languageId, snapshot); const document = service.context.documents.get(fakeUri, languageId, snapshot); const edits = await service.format(fakeUri, options, undefined, undefined); diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index c04442b1..afe3504a 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -1,9 +1,166 @@ +export * from '@volar/source-map'; export * from './lib/editorFeatures'; -export * from './lib/fileRegistry'; export * from './lib/linkedCodeMap'; export * from './lib/types'; export * from './lib/utils'; -export * from '@volar/source-map'; + +import { SourceMap } from '@volar/source-map'; +import type * as ts from 'typescript'; +import { LinkedCodeMap } from './lib/linkedCodeMap'; +import type { CodeInformation, Language, LanguagePlugin, SourceScript, VirtualCode } from './lib/types'; +import { FileMap } from './lib/utils'; + +export function createLanguage(plugins: LanguagePlugin[], caseSensitive: boolean, sync: (id: string) => void): Language { + + const sourceScripts = new FileMap(caseSensitive); + const virtualCodeToSourceFileMap = new WeakMap(); + const virtualCodeToMaps = new WeakMap]>>(); + const virtualCodeToLinkedCodeMap = new WeakMap(); + + return { + plugins, + scripts: { + get(id) { + sync(id); + return sourceScripts.get(id); + }, + set(id, languageId, snapshot, _plugins = plugins) { + if (sourceScripts.has(id)) { + const sourceScript = sourceScripts.get(id)!; + if (sourceScript.languageId !== languageId) { + // languageId changed + this.delete(id); + return this.set(id, languageId, snapshot); + } + else if (sourceScript.snapshot !== snapshot) { + // snapshot updated + sourceScript.snapshot = snapshot; + if (sourceScript.generated) { + sourceScript.generated.root = sourceScript.generated.languagePlugin.updateVirtualCode(id, sourceScript.generated.root, snapshot); + sourceScript.generated.embeddedCodes.clear(); + for (const code of forEachEmbeddedCode(sourceScript.generated.root)) { + virtualCodeToSourceFileMap.set(code, sourceScript); + sourceScript.generated.embeddedCodes.set(code.id, code); + } + } + return sourceScript; + } + else { + // not changed + return sourceScript; + } + } + else { + // created + const sourceScript: SourceScript = { id, languageId, snapshot }; + sourceScripts.set(id, sourceScript); + for (const languagePlugin of _plugins) { + const virtualCode = languagePlugin.createVirtualCode(id, languageId, snapshot); + if (virtualCode) { + sourceScript.generated = { + root: virtualCode, + languagePlugin, + embeddedCodes: new Map(), + }; + for (const code of forEachEmbeddedCode(virtualCode)) { + virtualCodeToSourceFileMap.set(code, sourceScript); + sourceScript.generated.embeddedCodes.set(code.id, code); + } + break; + } + } + return sourceScript; + } + }, + delete(id) { + const value = sourceScripts.get(id); + if (value) { + if (value.generated) { + value.generated.languagePlugin.disposeVirtualCode?.(id, value.generated.root); + } + sourceScripts.delete(id); + } + }, + }, + maps: { + get(virtualCode, scriptId) { + if (!scriptId) { + const sourceScript = virtualCodeToSourceFileMap.get(virtualCode); + if (!sourceScript) { + return; + } + scriptId = sourceScript.id; + } + for (const [id, [_snapshot, map]] of this.forEach(virtualCode)) { + if (id === scriptId) { + return map; + } + } + }, + forEach(virtualCode) { + let map = virtualCodeToMaps.get(virtualCode.snapshot); + if (!map) { + map = new Map(); + virtualCodeToMaps.set(virtualCode.snapshot, map); + } + updateVirtualCodeMapOfMap(virtualCode, map, id => { + if (id) { + const sourceScript = sourceScripts.get(id)!; + return [id, sourceScript.snapshot]; + } + else { + const sourceScript = virtualCodeToSourceFileMap.get(virtualCode)!; + return [sourceScript.id, sourceScript.snapshot]; + } + }); + return map; + }, + }, + linkedCodeMaps: { + get(virtualCode) { + if (!virtualCodeToLinkedCodeMap.has(virtualCode.snapshot)) { + virtualCodeToLinkedCodeMap.set( + virtualCode.snapshot, + virtualCode.linkedCodeMappings + ? new LinkedCodeMap(virtualCode.linkedCodeMappings) + : undefined + ); + } + return virtualCodeToLinkedCodeMap.get(virtualCode.snapshot); + }, + }, + }; +} + +export function updateVirtualCodeMapOfMap( + virtualCode: VirtualCode, + mapOfMap: Map]>, + getSourceSnapshot: (id: string | undefined) => [string, ts.IScriptSnapshot] | undefined, +) { + const sources = new Set(); + for (const mapping of virtualCode.mappings) { + if (sources.has(mapping.source)) { + continue; + } + sources.add(mapping.source); + const source = getSourceSnapshot(mapping.source); + if (!source) { + continue; + } + if (!mapOfMap.has(source[0]) || mapOfMap.get(source[0])![0] !== source[1]) { + mapOfMap.set(source[0], [source[1], new SourceMap(virtualCode.mappings.filter(mapping2 => mapping2.source === mapping.source))]); + } + } +} + +export function* forEachEmbeddedCode(virtualCode: VirtualCode): Generator { + yield virtualCode; + if (virtualCode.embeddedCodes) { + for (const embeddedCode of virtualCode.embeddedCodes) { + yield* forEachEmbeddedCode(embeddedCode); + } + } +} export function resolveCommonLanguageId(fileNameOrUri: string) { const ext = fileNameOrUri.split('.').pop()!; diff --git a/packages/language-core/lib/fileRegistry.ts b/packages/language-core/lib/fileRegistry.ts deleted file mode 100644 index 6a2adb3c..00000000 --- a/packages/language-core/lib/fileRegistry.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { SourceMap } from '@volar/source-map'; -import type * as ts from 'typescript'; -import { LinkedCodeMap } from './linkedCodeMap'; -import type { CodeInformation, LanguagePlugin, SourceFile, VirtualCode } from './types'; -import { FileMap } from './utils'; - -export type FileRegistry = ReturnType; - -export function createFileRegistry(languagePlugins: LanguagePlugin[], caseSensitive: boolean, sync: (id: string) => void) { - - const sourceFiles = new FileMap(caseSensitive); - const virtualCodeToSourceFileMap = new WeakMap(); - const virtualCodeToMaps = new WeakMap]>>(); - const virtualCodeToLinkedCodeMap = new WeakMap(); - - return { - languagePlugins, - set(id: string, languageId: string, snapshot: ts.IScriptSnapshot, plugins = languagePlugins): SourceFile { - - const value = sourceFiles.get(id); - if (value) { - if (value.languageId !== languageId) { - // languageId changed - this.delete(id); - return this.set(id, languageId, snapshot); - } - else if (value.snapshot !== snapshot) { - // snapshot updated - value.snapshot = snapshot; - if (value.generated) { - value.generated.code = value.generated.languagePlugin.updateVirtualCode(id, value.generated.code, snapshot, this); - for (const code of forEachEmbeddedCode(value.generated.code)) { - virtualCodeToSourceFileMap.set(code, value); - } - } - return value; - } - else { - // not changed - return value; - } - } - - // created - const sourceFile: SourceFile = { id, languageId, snapshot }; - sourceFiles.set(id, sourceFile); - - for (const languagePlugin of plugins) { - const virtualCode = languagePlugin.createVirtualCode(id, languageId, snapshot, this); - if (virtualCode) { - sourceFile.generated = { - code: virtualCode, - languagePlugin, - }; - for (const code of forEachEmbeddedCode(virtualCode)) { - virtualCodeToSourceFileMap.set(code, sourceFile); - } - break; - } - } - - return sourceFile; - }, - delete(id: string) { - const value = sourceFiles.get(id); - if (value) { - if (value.generated) { - value.generated.languagePlugin.disposeVirtualCode?.(id, value.generated.code, this); - } - sourceFiles.delete(id); - } - }, - get(id: string) { - sync(id); - return sourceFiles.get(id); - }, - getByVirtualCode(virtualCode: VirtualCode) { - return virtualCodeToSourceFileMap.get(virtualCode)!; - }, - getLinkedCodeMap(virtualCode: VirtualCode) { - if (!virtualCodeToLinkedCodeMap.has(virtualCode.snapshot)) { - virtualCodeToLinkedCodeMap.set( - virtualCode.snapshot, - virtualCode.linkedCodeMappings - ? new LinkedCodeMap(virtualCode.linkedCodeMappings) - : undefined - ); - } - return virtualCodeToLinkedCodeMap.get(virtualCode.snapshot); - }, - getMaps(virtualCode: VirtualCode) { - - if (!virtualCodeToMaps.has(virtualCode.snapshot)) { - virtualCodeToMaps.set(virtualCode.snapshot, new Map()); - } - - updateVirtualCodeMaps(virtualCode, sourceFileId => { - if (sourceFileId) { - const sourceFile = sourceFiles.get(sourceFileId)!; - return [sourceFileId, sourceFile.snapshot]; - } - else { - const sourceFile = virtualCodeToSourceFileMap.get(virtualCode)!; - return [sourceFile.id, sourceFile.snapshot]; - } - }, virtualCodeToMaps.get(virtualCode.snapshot)); - - return virtualCodeToMaps.get(virtualCode.snapshot)!; - }, - getVirtualCode(sourceFileId: string, virtualCodeId: string) { - const sourceFile = this.get(sourceFileId); - if (sourceFile?.generated) { - for (const code of forEachEmbeddedCode(sourceFile.generated.code)) { - if (code.id === virtualCodeId) { - return [code, sourceFile] as const; - } - } - } - return [undefined, undefined] as const; - }, - }; -} - -export function updateVirtualCodeMaps( - virtualCode: VirtualCode, - getSourceSnapshot: (sourceUri: string | undefined) => [string, ts.IScriptSnapshot] | undefined, - map: Map]> = new Map(), -) { - - const sources = new Set(); - - for (const mapping of virtualCode.mappings) { - - if (sources.has(mapping.source)) { - continue; - } - sources.add(mapping.source); - - const source = getSourceSnapshot(mapping.source); - if (!source) { - continue; - } - - if (!map.has(source[0]) || map.get(source[0])![0] !== source[1]) { - map.set(source[0], [source[1], new SourceMap(virtualCode.mappings.filter(mapping2 => mapping2.source === mapping.source))]); - } - } - - return map; -} - -export function* forEachEmbeddedCode(code: VirtualCode): Generator { - yield code; - if (code.embeddedCodes) { - for (const embeddedCode of code.embeddedCodes) { - yield* forEachEmbeddedCode(embeddedCode); - } - } -} diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 9b1716bf..91afd5b0 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -1,8 +1,29 @@ -import type { Mapping, Stack } from '@volar/source-map'; +import type { Mapping, SourceMap, Stack } from '@volar/source-map'; import type * as ts from 'typescript'; -import type { FileRegistry } from './fileRegistry'; +import type { LinkedCodeMap } from './linkedCodeMap'; -export interface SourceFile { +export interface Language { + plugins: LanguagePlugin[]; + scripts: { + get(id: string): SourceScript | undefined; + set(id: string, languageId: string, snapshot: ts.IScriptSnapshot, plugins?: LanguagePlugin[]): SourceScript; + delete(id: string): void; + }; + maps: { + get(virtualCode: VirtualCode, scriptId?: string): SourceMap | undefined; + forEach(virtualCode: VirtualCode): Map]>; + }; + linkedCodeMaps: { + get(virtualCode: VirtualCode): LinkedCodeMap | undefined; + }; + typescript?: { + projectHost: TypeScriptProjectHost; + languageServiceHost: ts.LanguageServiceHost; + getExtraServiceScript(fileName: string): ExtraServiceScript | undefined; + }; +} + +export interface SourceScript { /** * uri or fileName */ @@ -10,8 +31,9 @@ export interface SourceFile { languageId: string; snapshot: ts.IScriptSnapshot; generated?: { - code: VirtualCode; + root: VirtualCode; languagePlugin: LanguagePlugin; + embeddedCodes: Map; }; } @@ -64,9 +86,9 @@ export interface ExtraServiceScript extends ServiceScript { } export interface LanguagePlugin { - createVirtualCode(fileId: string, languageId: string, snapshot: ts.IScriptSnapshot, files?: FileRegistry): T | undefined; - updateVirtualCode(fileId: string, virtualCode: T, newSnapshot: ts.IScriptSnapshot, files?: FileRegistry): T; - disposeVirtualCode?(fileId: string, virtualCode: T, files?: FileRegistry): void; + createVirtualCode(scriptId: string, languageId: string, snapshot: ts.IScriptSnapshot): T | undefined; + updateVirtualCode(scriptId: string, virtualCode: T, newSnapshot: ts.IScriptSnapshot): T; + disposeVirtualCode?(scriptId: string, virtualCode: T): void; typescript?: { /** * LSP + TS Plugin @@ -75,11 +97,11 @@ export interface LanguagePlugin { /** * LSP + TS Plugin */ - getScript(rootVirtualCode: T): ServiceScript | undefined; + getServiceScript(rootVirtualCode: T): ServiceScript | undefined; /** * LSP only */ - getExtraScripts?(fileName: string, rootVirtualCode: T): ExtraServiceScript[]; + getExtraServiceScripts?(fileName: string, rootVirtualCode: T): ExtraServiceScript[]; /** * LSP only */ @@ -87,18 +109,7 @@ export interface LanguagePlugin { }; } -export interface LanguageContext { - files: FileRegistry; - typescript?: { - configFileName: string | undefined; - sys: ts.System & { sync?(): Promise; }; - projectHost: TypeScriptProjectHost; - languageServiceHost: ts.LanguageServiceHost; - getExtraScript(fileName: string): ExtraServiceScript | undefined; - }; -} - -export interface TypeScriptProjectHost extends Pick< +export interface TypeScriptProjectHost extends ts.System, Pick< ts.LanguageServiceHost, 'getLocalizedDiagnosticMessages' | 'getCompilationSettings' @@ -108,5 +119,10 @@ export interface TypeScriptProjectHost extends Pick< | 'getProjectVersion' | 'getScriptSnapshot' > { - getLanguageId(fileId: string): string; + configFileName: string | undefined; + getLanguageId(scriptId: string): string; + getSystemVersion?(): number; + syncSystem?(): Promise; + scriptIdToFileName(scriptId: string): string; + fileNameToScriptId(fileName: string): string; } diff --git a/packages/language-server/lib/project/simpleProject.ts b/packages/language-server/lib/project/simpleProject.ts index b9014017..0edbee7d 100644 --- a/packages/language-server/lib/project/simpleProject.ts +++ b/packages/language-server/lib/project/simpleProject.ts @@ -1,11 +1,11 @@ -import { LanguageService, ServiceEnvironment, ServicePlugin, createFileRegistry, createLanguageService } from '@volar/language-service'; +import { LanguageService, ServiceEnvironment, LanguageServicePlugin, createLanguage, createLanguageService } from '@volar/language-service'; import type { ServerContext, ServerOptions } from '../server'; import type { ServerProject } from '../types'; export async function createSimpleServerProject( context: ServerContext, serviceEnv: ServiceEnvironment, - servicePlugins: ServicePlugin[], + servicePlugins: LanguageServicePlugin[], getLanguagePlugins: ServerOptions['getLanguagePlugins'], ): Promise { @@ -23,17 +23,17 @@ export async function createSimpleServerProject( function getLanguageService() { if (!languageService) { - const files = createFileRegistry(languagePlugins, false, uri => { + const language = createLanguage(languagePlugins, false, uri => { const script = context.documents.get(uri); if (script) { - files.set(uri, script.languageId, script.getSnapshot()); + language.scripts.set(uri, script.languageId, script.getSnapshot()); } else { - files.delete(uri); + language.scripts.delete(uri); } }); languageService = createLanguageService( - { files }, + language, servicePlugins, serviceEnv, ); diff --git a/packages/language-server/lib/project/typescriptProject.ts b/packages/language-server/lib/project/typescriptProject.ts index c488ed6d..c79eeb79 100644 --- a/packages/language-server/lib/project/typescriptProject.ts +++ b/packages/language-server/lib/project/typescriptProject.ts @@ -1,5 +1,5 @@ -import { LanguagePlugin, LanguageService, ServiceEnvironment, ServicePlugin, TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; -import { createLanguage, createSys } from '@volar/typescript'; +import { LanguagePlugin, LanguageService, ServiceEnvironment, LanguageServicePlugin, TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; +import { createTypeScriptLanguage, createSys } from '@volar/typescript'; import * as path from 'path-browserify'; import type * as ts from 'typescript'; import * as vscode from 'vscode-languageserver'; @@ -20,7 +20,7 @@ export async function createTypeScriptServerProject( tsconfig: string | ts.CompilerOptions, context: ServerContext, serviceEnv: ServiceEnvironment, - servicePlugins: ServicePlugin[], + servicePlugins: LanguageServicePlugin[], getLanguagePlugins: ServerOptions['getLanguagePlugins'], ): Promise { @@ -30,20 +30,42 @@ export async function createTypeScriptServerProject( const sys = createSys(ts, serviceEnv, uriToFileName(serviceEnv.workspaceFolder)); const host: TypeScriptProjectHost = { - getCurrentDirectory: () => uriToFileName(serviceEnv.workspaceFolder), - getProjectVersion: () => projectVersion.toString(), - getScriptFileNames: () => rootFiles, - getScriptSnapshot: fileName => { + ...sys, + configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, + getSystemVersion() { + return sys.version; + }, + syncSystem() { + return sys.sync(); + }, + getCurrentDirectory() { + return uriToFileName(serviceEnv.workspaceFolder); + }, + getProjectVersion() { + return projectVersion.toString(); + }, + getScriptFileNames() { + return rootFiles; + }, + getScriptSnapshot(fileName) { askedFiles.pathSet(fileName, true); const doc = context.documents.get(fileNameToUri(fileName)); if (doc) { return doc.getSnapshot(); } }, - getCompilationSettings: () => parsedCommandLine.options, + getCompilationSettings() { + return parsedCommandLine.options; + }, getLocalizedDiagnosticMessages: tsLocalized ? () => tsLocalized : undefined, - getProjectReferences: () => parsedCommandLine.projectReferences, - getLanguageId: uri => context.documents.get(uri)?.languageId ?? resolveCommonLanguageId(uri), + getProjectReferences() { + return parsedCommandLine.projectReferences; + }, + getLanguageId(uri) { + return context.documents.get(uri)?.languageId ?? resolveCommonLanguageId(uri); + }, + fileNameToScriptId: serviceEnv.typescript!.fileNameToUri, + scriptIdToFileName: serviceEnv.typescript!.uriToFileName, }; const languagePlugins = await getLanguagePlugins(serviceEnv, { typescript: { @@ -88,16 +110,10 @@ export async function createTypeScriptServerProject( } function getLanguageService() { if (!languageService) { - const language = createLanguage( + const language = createTypeScriptLanguage( ts, - sys, languagePlugins, - typeof tsconfig === 'string' ? tsconfig : undefined, host, - { - fileNameToFileId: serviceEnv.typescript!.fileNameToUri, - fileIdToFileName: serviceEnv.typescript!.uriToFileName, - }, ); languageService = createLanguageService( language, diff --git a/packages/language-server/lib/register/registerEditorFeatures.ts b/packages/language-server/lib/register/registerEditorFeatures.ts index fc7aa63b..969259a6 100644 --- a/packages/language-server/lib/register/registerEditorFeatures.ts +++ b/packages/language-server/lib/register/registerEditorFeatures.ts @@ -56,39 +56,40 @@ export function registerEditorFeatures( }); connection.onRequest(GetMatchTsConfigRequest.type, async params => { const languageService = (await projects.getProject(params.uri)).getLanguageService(); - const configFileName = languageService.context.language.typescript?.configFileName; + const configFileName = languageService.context.language.typescript?.projectHost.configFileName; if (configFileName) { return { uri: fileNameToUri(configFileName) }; } }); connection.onRequest(GetVirtualFileRequest.type, async document => { const languageService = (await projects.getProject(document.uri)).getLanguageService(); - const sourceFile = languageService.context.language.files.get(document.uri); - if (sourceFile?.generated) { - return prune(sourceFile.generated.code); + const sourceScript = languageService.context.language.scripts.get(document.uri); + if (sourceScript?.generated) { + return prune(sourceScript.generated.root); } - function prune(code: VirtualCode): GetVirtualFileRequest.VirtualCodeInfo { - const uri = languageService.context.documents.getVirtualCodeUri(sourceFile!.id, code.id); + function prune(virtualCode: VirtualCode): GetVirtualFileRequest.VirtualCodeInfo { + const uri = languageService.context.encodeEmbeddedDocumentUri(sourceScript!.id, virtualCode.id); let version = scriptVersions.get(uri) ?? 0; - if (!scriptVersionSnapshots.has(code.snapshot)) { + if (!scriptVersionSnapshots.has(virtualCode.snapshot)) { version++; scriptVersions.set(uri, version); - scriptVersionSnapshots.add(code.snapshot); + scriptVersionSnapshots.add(virtualCode.snapshot); } return { - fileUri: sourceFile!.id, - virtualCodeId: code.id, - languageId: code.languageId, - embeddedCodes: code.embeddedCodes?.map(prune) || [], + fileUri: sourceScript!.id, + virtualCodeId: virtualCode.id, + languageId: virtualCode.languageId, + embeddedCodes: virtualCode.embeddedCodes?.map(prune) || [], version, - disabled: languageService.context.disabledVirtualFileUris.has(uri), + disabled: languageService.context.disabledEmbeddedDocumentUris.has(uri), }; } }); connection.onRequest(GetVirtualCodeRequest.type, async params => { const languageService = (await projects.getProject(params.fileUri)).getLanguageService(); - const [virtualCode] = languageService.context.language.files.getVirtualCode(params.fileUri, params.virtualCodeId); + const sourceScript = languageService.context.language.scripts.get(params.fileUri); + const virtualCode = sourceScript?.generated?.embeddedCodes.get(params.virtualCodeId); if (virtualCode) { const mappings: Record = {}; for (const map of languageService.context.documents.getMaps(virtualCode)) { @@ -126,16 +127,16 @@ export function registerEditorFeatures( else { const uri = languageService.context.env.typescript!.fileNameToUri(fileName); if (uri.startsWith(rootUri)) { - const sourceFile = languageService.context.language.files.get(uri); - if (sourceFile?.generated) { - const mainScript = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (mainScript) { - const { snapshot } = mainScript.code; - fs.writeFile(fileName + mainScript.extension, snapshot.getText(0, snapshot.getLength()), () => { }); + const sourceScript = languageService.context.language.scripts.get(uri); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + const { snapshot } = serviceScript.code; + fs.writeFile(fileName + serviceScript.extension, snapshot.getText(0, snapshot.getLength()), () => { }); } - if (sourceFile.generated.languagePlugin.typescript?.getExtraScripts) { - for (const extraScript of sourceFile.generated.languagePlugin.typescript.getExtraScripts(uri, sourceFile.generated.code)) { - const { snapshot } = extraScript.code; + if (sourceScript.generated.languagePlugin.typescript?.getExtraServiceScripts) { + for (const extraServiceScript of sourceScript.generated.languagePlugin.typescript.getExtraServiceScripts(uri, sourceScript.generated.root)) { + const { snapshot } = extraServiceScript.code; fs.writeFile(fileName, snapshot.getText(0, snapshot.getLength()), () => { }); } } @@ -157,7 +158,8 @@ export function registerEditorFeatures( const tsLanguageService: ts.LanguageService | undefined = languageService.context.inject('typescript/languageService'); const program = tsLanguageService?.getProgram(); if (program && languageService.context.language.typescript) { - const { configFileName, languageServiceHost } = languageService.context.language.typescript; + const { languageServiceHost } = languageService.context.language.typescript; + const { configFileName } = languageService.context.language.typescript.projectHost; const projectName = configFileName ?? (languageServiceHost.getCurrentDirectory() + '(inferred)'); const sourceFiles = program.getSourceFiles() ?? []; for (const sourceFile of sourceFiles) { @@ -212,12 +214,12 @@ export function registerEditorFeatures( const project = await projects.getProject(params.fileUri); const context = project.getLanguageServiceDontCreate()?.context; if (context) { - const virtualFileUri = project.getLanguageService().context.documents.getVirtualCodeUri(params.fileUri, params.virtualCodeId); + const virtualFileUri = project.getLanguageService().context.encodeEmbeddedDocumentUri(params.fileUri, params.virtualCodeId); if (params.disabled) { - context.disabledVirtualFileUris.add(virtualFileUri); + context.disabledEmbeddedDocumentUris.add(virtualFileUri); } else { - context.disabledVirtualFileUris.delete(virtualFileUri); + context.disabledEmbeddedDocumentUris.delete(virtualFileUri); } } }); diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index c11f3583..dacf4161 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -1,4 +1,4 @@ -import { FileSystem, LanguagePlugin, ServiceEnvironment, ServicePlugin, standardSemanticTokensLegend } from '@volar/language-service'; +import { FileSystem, LanguagePlugin, ServiceEnvironment, LanguageServicePlugin, standardSemanticTokensLegend } from '@volar/language-service'; import { SnapshotDocument } from '@volar/snapshot-document'; import * as l10n from '@vscode/l10n'; import { configure as configureHttpRequests } from 'request-light'; @@ -24,7 +24,7 @@ export interface ServerContext { export interface ServerOptions { watchFileExtensions?: string[]; - getServicePlugins(): ServicePlugin[] | Promise; + getServicePlugins(): LanguageServicePlugin[] | Promise; getLanguagePlugins(serviceEnv: ServiceEnvironment, projectContext: ProjectContext): LanguagePlugin[] | Promise; } diff --git a/packages/language-server/lib/serverCapabilities.ts b/packages/language-server/lib/serverCapabilities.ts index 44ee6b7f..cb11e82d 100644 --- a/packages/language-server/lib/serverCapabilities.ts +++ b/packages/language-server/lib/serverCapabilities.ts @@ -1,9 +1,9 @@ -import type { ServicePlugin } from '@volar/language-service'; +import type { LanguageServicePlugin } from '@volar/language-service'; import * as vscode from 'vscode-languageserver'; export function getServerCapabilities( watchExts: string[], - servicePlugins: ServicePlugin[], + servicePlugins: LanguageServicePlugin[], semanticTokensLegend: vscode.SemanticTokensLegend, ) { const capabilities: vscode.ServerCapabilities = { diff --git a/packages/language-server/lib/types.ts b/packages/language-server/lib/types.ts index 1e2b2939..e6771d81 100644 --- a/packages/language-server/lib/types.ts +++ b/packages/language-server/lib/types.ts @@ -1,4 +1,4 @@ -import type { FileSystem, LanguageService, ServicePlugin, TypeScriptProjectHost } from '@volar/language-service'; +import type { FileSystem, LanguageService, LanguageServicePlugin, TypeScriptProjectHost } from '@volar/language-service'; import type * as vscode from 'vscode-languageserver'; import type { ServerContext, ServerOptions } from './server'; import type { createSys } from '@volar/typescript'; @@ -49,7 +49,7 @@ export interface ServerProjectProvider { export interface ServerProjectProviderFactory { ( context: ServerContext, - servicePlugins: ServicePlugin[], + servicePlugins: LanguageServicePlugin[], getLanguagePlugins: ServerOptions['getLanguagePlugins'], ): ServerProjectProvider; } diff --git a/packages/language-service/lib/documents.ts b/packages/language-service/lib/documents.ts index 30cfd204..b390bb7a 100644 --- a/packages/language-service/lib/documents.ts +++ b/packages/language-service/lib/documents.ts @@ -1,9 +1,6 @@ -import { CodeInformation, CodeRangeKey, FileRegistry, LinkedCodeMap, Mapping, SourceMap, VirtualCode, translateOffset } from '@volar/language-core'; -import type * as ts from 'typescript'; +import { CodeRangeKey, LinkedCodeMap, Mapping, SourceMap, translateOffset } from '@volar/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -import { TextDocument } from 'vscode-languageserver-textdocument'; - -export type DocumentProvider = ReturnType; +import type { TextDocument } from 'vscode-languageserver-textdocument'; export class SourceMapWithDocuments { @@ -144,69 +141,3 @@ export class LinkedCodeMapWithDocument extends SourceMapWithDocuments { } } } - -export function createDocumentProvider(files: FileRegistry) { - - let version = 0; - - const map2DocMap = new WeakMap, SourceMapWithDocuments>(); - const mirrorMap2DocMirrorMap = new WeakMap(); - const snapshot2Doc = new WeakMap>(); - - return { - get, - *getMaps(virtualCode: VirtualCode) { - for (const [sourceFileUri, [sourceSnapshot, map]] of files.getMaps(virtualCode)) { - if (!map2DocMap.has(map)) { - map2DocMap.set(map, new SourceMapWithDocuments( - get(sourceFileUri, files.get(sourceFileUri)!.languageId, sourceSnapshot), - get(this.getVirtualCodeUri(sourceFileUri, virtualCode.id), virtualCode.languageId, virtualCode.snapshot), - map, - )); - } - yield map2DocMap.get(map)!; - } - }, - getLinkedCodeMap(virtualCode: VirtualCode) { - const map = files.getLinkedCodeMap(virtualCode); - if (map) { - if (!mirrorMap2DocMirrorMap.has(map)) { - mirrorMap2DocMirrorMap.set(map, new LinkedCodeMapWithDocument( - get(this.getVirtualCodeUri(files.getByVirtualCode(virtualCode).id, virtualCode.id), virtualCode.languageId, virtualCode.snapshot), - map, - )); - } - return mirrorMap2DocMirrorMap.get(map)!; - } - }, - // TODO: rename to getVirtualCodeByDocumentUri - getVirtualCodeByUri(uri: string) { - if (uri.includes('?virtualCodeId=')) { - const sourceFileUri = uri.split('?virtualCodeId=')[0]; - const virtualCodeId = uri.split('?virtualCodeId=')[1]; - return files.getVirtualCode(sourceFileUri, virtualCodeId); - } - return [undefined, undefined] as const; - }, - // TODO: rename to getDocumentUriByVirtualCode - getVirtualCodeUri(sourceFileUri: string, virtualCodeId: string) { - return sourceFileUri + `?virtualCodeId=${virtualCodeId}`; - }, - }; - - function get(uri: string, languageId: string, snapshot: ts.IScriptSnapshot) { - if (!snapshot2Doc.has(snapshot)) { - snapshot2Doc.set(snapshot, new Map()); - } - const map = snapshot2Doc.get(snapshot)!; - if (!map.has(uri)) { - map.set(uri, TextDocument.create( - uri, - languageId, - version++, - snapshot.getText(0, snapshot.getLength()), - )); - } - return map.get(uri)!; - } -} diff --git a/packages/language-service/lib/features/provideCallHierarchyItems.ts b/packages/language-service/lib/features/provideCallHierarchyItems.ts index 5a46f9ed..7dc71dfa 100644 --- a/packages/language-service/lib/features/provideCallHierarchyItems.ts +++ b/packages/language-service/lib/features/provideCallHierarchyItems.ts @@ -10,7 +10,7 @@ export interface PluginCallHierarchyData { uri: string; original: Pick; serviceIndex: number; - virtualDocumentUri: string | undefined; + embeddedDocumentUri: string | undefined; } export function register(context: ServiceContext) { @@ -36,7 +36,7 @@ export function register(context: ServiceContext) { data: item.data, }, serviceIndex: context.services.indexOf(service), - virtualDocumentUri: map?.embeddedDocument.uri, + embeddedDocumentUri: map?.embeddedDocument.uri, } satisfies PluginCallHierarchyData; }); return items; @@ -68,11 +68,11 @@ export function register(context: ServiceContext) { Object.assign(item, data.original); - if (data.virtualDocumentUri) { + if (data.embeddedDocumentUri) { - const [virtualCode] = context.documents.getVirtualCodeByUri(data.virtualDocumentUri); + const isEmbeddedContent = !!context.decodeEmbeddedDocumentUri(data.embeddedDocumentUri); - if (virtualCode) { + if (isEmbeddedContent) { const _calls = await service[1].provideCallHierarchyIncomingCalls(item, token); @@ -129,11 +129,11 @@ export function register(context: ServiceContext) { Object.assign(item, data.original); - if (data.virtualDocumentUri) { + if (data.embeddedDocumentUri) { - const [virtualCode] = context.documents.getVirtualCodeByUri(data.virtualDocumentUri); + const isEmbeddedContent = !!context.decodeEmbeddedDocumentUri(data.embeddedDocumentUri); - if (virtualCode) { + if (isEmbeddedContent) { const _calls = await service[1].provideCallHierarchyOutgoingCalls(item, token); @@ -178,8 +178,9 @@ export function register(context: ServiceContext) { function transformCallHierarchyItem(tsItem: vscode.CallHierarchyItem, tsRanges: vscode.Range[]): [vscode.CallHierarchyItem, vscode.Range[]] | undefined { - const [virtualCode] = context.documents.getVirtualCodeByUri(tsItem.uri); - + const decoded = context.decodeEmbeddedDocumentUri(tsItem.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (!virtualCode) { return [tsItem, tsRanges]; } diff --git a/packages/language-service/lib/features/provideCodeActions.ts b/packages/language-service/lib/features/provideCodeActions.ts index 08bfda4f..33a31ed5 100644 --- a/packages/language-service/lib/features/provideCodeActions.ts +++ b/packages/language-service/lib/features/provideCodeActions.ts @@ -19,8 +19,8 @@ export function register(context: ServiceContext) { return async (uri: string, range: vscode.Range, codeActionContext: vscode.CodeActionContext, token = NoneCancellationToken) => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return; } diff --git a/packages/language-service/lib/features/provideCompletionItems.ts b/packages/language-service/lib/features/provideCompletionItems.ts index de0f361f..80090f09 100644 --- a/packages/language-service/lib/features/provideCompletionItems.ts +++ b/packages/language-service/lib/features/provideCompletionItems.ts @@ -1,9 +1,9 @@ import { isCompletionEnabled, type CodeInformation } from '@volar/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -import type { ServiceContext, ServicePluginInstance } from '../types'; +import type { ServiceContext, LanguageServicePluginInstance } from '../types'; import { NoneCancellationToken } from '../utils/cancellation'; import { transformCompletionList } from '../utils/transform'; -import { eachEmbeddedDocument } from '../utils/featureWorkers'; +import { forEachEmbeddedDocument } from '../utils/featureWorkers'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { SourceMapWithDocuments } from '../documents'; @@ -11,7 +11,7 @@ export interface ServiceCompletionData { uri: string; original: Pick; serviceIndex: number; - virtualDocumentUri: string | undefined; + embeddedDocumentUri: string | undefined; } export function register(context: ServiceContext) { @@ -19,8 +19,8 @@ export function register(context: ServiceContext) { let lastResult: { uri: string; results: { - virtualDocumentUri: string | undefined; - service: ServicePluginInstance; + embeddedDocumentUri: string | undefined; + service: LanguageServicePluginInstance; list: vscode.CompletionList | undefined | null; }[]; } | undefined; @@ -32,8 +32,8 @@ export function register(context: ServiceContext) { token = NoneCancellationToken, ) => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return { isIncomplete: false, items: [], @@ -53,9 +53,12 @@ export function register(context: ServiceContext) { const serviceIndex = context.services.findIndex(service => service[1] === cacheData.service); - if (cacheData.virtualDocumentUri) { + if (cacheData.embeddedDocumentUri) { + + const decoded = context.decodeEmbeddedDocumentUri(cacheData.embeddedDocumentUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - const [virtualCode] = context.documents.getVirtualCodeByUri(cacheData.virtualDocumentUri); if (!virtualCode) { continue; } @@ -83,7 +86,7 @@ export function register(context: ServiceContext) { data: item.data, }, serviceIndex, - virtualDocumentUri: map.embeddedDocument.uri, + embeddedDocumentUri: map.embeddedDocument.uri, } satisfies ServiceCompletionData; } @@ -102,7 +105,7 @@ export function register(context: ServiceContext) { continue; } - const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); cacheData.list = await cacheData.service.provideCompletionItems(document, position, completionContext, token); if (!cacheData.list) { @@ -118,7 +121,7 @@ export function register(context: ServiceContext) { data: item.data, }, serviceIndex, - virtualDocumentUri: undefined, + embeddedDocumentUri: undefined, } satisfies ServiceCompletionData; } } @@ -200,7 +203,7 @@ export function register(context: ServiceContext) { data: item.data, }, serviceIndex, - virtualDocumentUri: map ? document.uri : undefined, + embeddedDocumentUri: map ? document.uri : undefined, } satisfies ServiceCompletionData; } @@ -214,7 +217,7 @@ export function register(context: ServiceContext) { } lastResult?.results.push({ - virtualDocumentUri: map ? document.uri : undefined, + embeddedDocumentUri: map ? document.uri : undefined, service: service[1], list: completionList, }); @@ -223,9 +226,9 @@ export function register(context: ServiceContext) { isFirstMapping = false; }; - if (sourceFile.generated) { + if (sourceScript.generated) { - for (const map of eachEmbeddedDocument(context, sourceFile.generated.code)) { + for (const map of forEachEmbeddedDocument(context, sourceScript.id, sourceScript.generated.root)) { let _data: CodeInformation | undefined; @@ -239,7 +242,7 @@ export function register(context: ServiceContext) { } else { - const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); await worker(document, position); } @@ -247,7 +250,7 @@ export function register(context: ServiceContext) { return combineCompletionList(lastResult.results.map(cacheData => cacheData.list)); - function sortServices(a: ServicePluginInstance, b: ServicePluginInstance) { + function sortServices(a: LanguageServicePluginInstance, b: LanguageServicePluginInstance) { return (b.isAdditionalCompletion ? -1 : 1) - (a.isAdditionalCompletion ? -1 : 1); } diff --git a/packages/language-service/lib/features/provideDefinition.ts b/packages/language-service/lib/features/provideDefinition.ts index f5c50563..a469298d 100644 --- a/packages/language-service/lib/features/provideDefinition.ts +++ b/packages/language-service/lib/features/provideDefinition.ts @@ -30,11 +30,11 @@ export function register( const recursiveChecker = dedupe.createLocationSet(); const result: vscode.LocationLink[] = []; - await withMirrors(document, position, undefined); + await withLinkedCode(document, position, undefined); return result; - async function withMirrors(document: TextDocument, position: vscode.Position, originDefinition: vscode.LocationLink | undefined) { + async function withLinkedCode(document: TextDocument, position: vscode.Position, originDefinition: vscode.LocationLink | undefined) { const api = service[1][apiName]; if (!api) { @@ -55,20 +55,24 @@ export function register( recursiveChecker.add({ uri: definition.targetUri, range: { start: definition.targetRange.start, end: definition.targetRange.start } }); - const [virtualCode] = context.documents.getVirtualCodeByUri(definition.targetUri); - const mirrorMap = virtualCode ? context.documents.getLinkedCodeMap(virtualCode) : undefined; + const decoded = context.decodeEmbeddedDocumentUri(definition.targetUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + const linkedCodeMap = virtualCode && sourceScript + ? context.documents.getLinkedCodeMap(virtualCode, sourceScript.id) + : undefined; - if (mirrorMap) { + if (linkedCodeMap) { - for (const linkedPos of mirrorMap.getLinkedCodePositions(definition.targetSelectionRange.start)) { + for (const linkedPos of linkedCodeMap.getLinkedCodePositions(definition.targetSelectionRange.start)) { - if (recursiveChecker.has({ uri: mirrorMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { + if (recursiveChecker.has({ uri: linkedCodeMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { continue; } foundMirrorPosition = true; - await withMirrors(mirrorMap.document, linkedPos, originDefinition ?? definition); + await withLinkedCode(linkedCodeMap.document, linkedPos, originDefinition ?? definition); } } @@ -101,7 +105,9 @@ export function register( let foundTargetSelectionRange = false; - const [targetVirtualFile] = context.documents.getVirtualCodeByUri(link.targetUri); + const decoded = context.decodeEmbeddedDocumentUri(link.targetUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const targetVirtualFile = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (targetVirtualFile) { diff --git a/packages/language-service/lib/features/provideDiagnostics.ts b/packages/language-service/lib/features/provideDiagnostics.ts index e4d3690c..ef9aaf07 100644 --- a/packages/language-service/lib/features/provideDiagnostics.ts +++ b/packages/language-service/lib/features/provideDiagnostics.ts @@ -152,12 +152,12 @@ export function register(context: ServiceContext) { response?: (result: vscode.Diagnostic[]) => void, ) => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return []; } - const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); const lastResponse = lastResponses.get(uri) ?? lastResponses.set(uri, { semantic: { errors: [] }, syntactic: { errors: [] }, @@ -171,9 +171,9 @@ export function register(context: ServiceContext) { const oldSnapshot = cache.snapshot; const oldDocument = cache.document; - const change = oldSnapshot ? sourceFile.snapshot.getChangeRange(oldSnapshot) : undefined; + const change = oldSnapshot ? sourceScript.snapshot.getChangeRange(oldSnapshot) : undefined; - cache.snapshot = sourceFile.snapshot; + cache.snapshot = sourceScript.snapshot; cache.document = document; if (!updateCacheRangeFailed && oldDocument && change) { @@ -270,7 +270,7 @@ export function register(context: ServiceContext) { ); if (result) { cache.errors = result; - cache.snapshot = sourceFile?.snapshot; + cache.snapshot = sourceScript?.snapshot; } } }; @@ -298,7 +298,9 @@ export function register(context: ServiceContext) { for (const info of _error.relatedInformation) { - const [virtualCode] = context.documents.getVirtualCodeByUri(info.location.uri); + const decoded = context.decodeEmbeddedDocumentUri(info.location.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { for (const map of context.documents.getMaps(virtualCode)) { diff --git a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts index 71374409..dd496095 100644 --- a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts +++ b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts @@ -1,4 +1,4 @@ -import { VirtualCode, forEachEmbeddedCode, isFormattingEnabled, resolveCommonLanguageId, updateVirtualCodeMaps } from '@volar/language-core'; +import { CodeInformation, SourceMap, SourceScript, VirtualCode, forEachEmbeddedCode, isFormattingEnabled, resolveCommonLanguageId, updateVirtualCodeMapOfMap } from '@volar/language-core'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -23,43 +23,41 @@ export function register(context: ServiceContext) { token = NoneCancellationToken ) => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return; } - let document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + let document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); range ??= { start: document.positionAt(0), end: document.positionAt(document.getText().length), }; - if (!sourceFile.generated) { + if (!sourceScript.generated) { return onTypeParams - ? (await tryFormat(document, document, undefined, 0, onTypeParams.position, onTypeParams.ch))?.edits - : (await tryFormat(document, document, undefined, 0, range, undefined))?.edits; + ? (await tryFormat(document, document, sourceScript, undefined, 0, onTypeParams.position, onTypeParams.ch))?.edits + : (await tryFormat(document, document, sourceScript, undefined, 0, range, undefined))?.edits; } const embeddedRanges = new Map(); const startOffset = document.offsetAt(range.start); const endOffset = document.offsetAt(range.end); - for (const code of forEachEmbeddedCode(sourceFile.generated.code)) { - for (const [sourceFileUri, [_snapshot, map]] of context.language.files.getMaps(code)) { - if (sourceFileUri === uri) { - const embeddedRange = findOverlapCodeRange(startOffset, endOffset, map, isFormattingEnabled); - if (embeddedRange) { - if (embeddedRange.start === map.mappings[0].generatedOffsets[0]) { - embeddedRange.start = 0; - } - const lastMapping = map.mappings[map.mappings.length - 1]; - if (embeddedRange.end === lastMapping.generatedOffsets[lastMapping.generatedOffsets.length - 1] + lastMapping.lengths[lastMapping.lengths.length - 1]) { - embeddedRange.end = code.snapshot.getLength(); - } - embeddedRanges.set(code.id, embeddedRange); + for (const code of forEachEmbeddedCode(sourceScript.generated.root)) { + const map = context.language.maps.get(code); + if (map) { + const embeddedRange = findOverlapCodeRange(startOffset, endOffset, map, isFormattingEnabled); + if (embeddedRange) { + if (embeddedRange.start === map.mappings[0].generatedOffsets[0]) { + embeddedRange.start = 0; } - break; + const lastMapping = map.mappings[map.mappings.length - 1]; + if (embeddedRange.end === lastMapping.generatedOffsets[lastMapping.generatedOffsets.length - 1] + lastMapping.lengths[lastMapping.lengths.length - 1]) { + embeddedRange.end = code.snapshot.getLength(); + } + embeddedRanges.set(code.id, embeddedRange); } } } @@ -67,8 +65,8 @@ export function register(context: ServiceContext) { try { const originalDocument = document; - let tempSourceSnapshot = sourceFile.snapshot; - let tempVirtualFile = context.language.files.set(sourceFile.id + '.tmp', sourceFile.languageId, sourceFile.snapshot, [sourceFile.generated.languagePlugin]).generated?.code; + let tempSourceSnapshot = sourceScript.snapshot; + let tempVirtualFile = context.language.scripts.set(sourceScript.id + '.tmp', sourceScript.languageId, sourceScript.snapshot, [sourceScript.generated.languagePlugin]).generated?.root; if (!tempVirtualFile) { return; } @@ -77,7 +75,7 @@ export function register(context: ServiceContext) { while (true) { - const embeddedCodes = getEmbeddedCodesByLevel(context, sourceFile.id, tempVirtualFile, level++); + const embeddedCodes = getEmbeddedCodesByLevel(context, sourceScript.id, tempVirtualFile, level++); if (embeddedCodes.length === 0) { break; } @@ -90,7 +88,7 @@ export function register(context: ServiceContext) { continue; } - const docMap = createDocMap(code, uri, sourceFile.languageId, tempSourceSnapshot); + const docMap = createDocMap(code, uri, sourceScript.languageId, tempSourceSnapshot); if (!docMap) { continue; } @@ -106,6 +104,7 @@ export function register(context: ServiceContext) { embeddedCodeResult = await tryFormat( docMap.sourceDocument, docMap.embeddedDocument, + sourceScript, code, level, embeddedPosition, @@ -117,6 +116,7 @@ export function register(context: ServiceContext) { embeddedCodeResult = await tryFormat( docMap.sourceDocument, docMap.embeddedDocument, + sourceScript, code, level, { @@ -145,7 +145,7 @@ export function register(context: ServiceContext) { const newText = TextDocument.applyEdits(document, edits); document = TextDocument.create(document.uri, document.languageId, document.version + 1, newText); tempSourceSnapshot = stringToSnapshot(newText); - tempVirtualFile = context.language.files.set(sourceFile.id + '.tmp', sourceFile.languageId, tempSourceSnapshot, [sourceFile.generated.languagePlugin]).generated?.code; + tempVirtualFile = context.language.scripts.set(sourceScript.id + '.tmp', sourceScript.languageId, tempSourceSnapshot, [sourceScript.generated.languagePlugin]).generated?.root; if (!tempVirtualFile) { break; } @@ -167,19 +167,20 @@ export function register(context: ServiceContext) { return [textEdit]; } finally { - context.language.files.delete(sourceFile.id + '.tmp'); + context.language.scripts.delete(sourceScript.id + '.tmp'); } async function tryFormat( sourceDocument: TextDocument, document: TextDocument, - code: VirtualCode | undefined, - codeLevel: number, + sourceScript: SourceScript, + virtualCode: VirtualCode | undefined, + embeddedLevel: number, rangeOrPosition: vscode.Range | vscode.Position, ch?: string, ) { - if (context.disabledVirtualFileUris.has(document.uri)) { + if (context.disabledEmbeddedDocumentUris.has(document.uri)) { return; } @@ -190,13 +191,13 @@ export function register(context: ServiceContext) { end: document.positionAt(document.getText().length), }; - if (code) { + if (virtualCode) { codeOptions = { - level: codeLevel - 1, + level: embeddedLevel - 1, initialIndentLevel: 0, }; - if (code.mappings.length) { - const firstMapping = code.mappings[0]; + if (virtualCode.mappings.length) { + const firstMapping = virtualCode.mappings[0]; const startOffset = firstMapping.sourceOffsets[0]; const startPosition = sourceDocument.positionAt(startOffset); codeOptions.initialIndentLevel = computeInitialIndent( @@ -209,7 +210,7 @@ export function register(context: ServiceContext) { if (context.disabledServicePlugins.has(service[1])) { continue; } - codeOptions = await service[1].resolveEmbeddedCodeFormattingOptions?.(code, codeOptions, token) ?? codeOptions; + codeOptions = await service[1].resolveEmbeddedCodeFormattingOptions?.(sourceScript, virtualCode, codeOptions, token) ?? codeOptions; } } @@ -250,27 +251,28 @@ export function register(context: ServiceContext) { } }; - function createDocMap(file: VirtualCode, sourceFileUri: string, sourceLanguageId: string, _sourceSnapshot: ts.IScriptSnapshot) { - const maps = updateVirtualCodeMaps(file, sourceFileUri2 => { + function createDocMap(virtualCode: VirtualCode, documentUri: string, sourceLanguageId: string, _sourceSnapshot: ts.IScriptSnapshot) { + const mapOfMap = new Map]>(); + updateVirtualCodeMapOfMap(virtualCode, mapOfMap, sourceFileUri2 => { if (!sourceFileUri2) { - return [sourceFileUri, _sourceSnapshot]; + return [documentUri, _sourceSnapshot]; } }); - if (maps.has(sourceFileUri) && maps.get(sourceFileUri)![0] === _sourceSnapshot) { - const map = maps.get(sourceFileUri)!; + if (mapOfMap.has(documentUri) && mapOfMap.get(documentUri)![0] === _sourceSnapshot) { + const map = mapOfMap.get(documentUri)!; const version = fakeVersion++; return new SourceMapWithDocuments( TextDocument.create( - sourceFileUri, - sourceLanguageId ?? resolveCommonLanguageId(sourceFileUri), + documentUri, + sourceLanguageId ?? resolveCommonLanguageId(documentUri), version, _sourceSnapshot.getText(0, _sourceSnapshot.getLength()) ), TextDocument.create( - context.documents.getVirtualCodeUri(sourceFileUri, file.id), - file.languageId, + context.encodeEmbeddedDocumentUri(documentUri, virtualCode.id), + virtualCode.languageId, version, - file.snapshot.getText(0, file.snapshot.getLength()) + virtualCode.snapshot.getText(0, virtualCode.snapshot.getLength()) ), map[1], ); diff --git a/packages/language-service/lib/features/provideDocumentHighlights.ts b/packages/language-service/lib/features/provideDocumentHighlights.ts index 8172dbd0..8de51b43 100644 --- a/packages/language-service/lib/features/provideDocumentHighlights.ts +++ b/packages/language-service/lib/features/provideDocumentHighlights.ts @@ -25,11 +25,11 @@ export function register(context: ServiceContext) { const recursiveChecker = dedupe.createLocationSet(); const result: vscode.DocumentHighlight[] = []; - await withMirrors(document, position); + await withLinkedCode(document, position); return result; - async function withMirrors(document: TextDocument, position: vscode.Position) { + async function withLinkedCode(document: TextDocument, position: vscode.Position) { if (!service[1].provideDocumentHighlights) { return; @@ -49,20 +49,24 @@ export function register(context: ServiceContext) { recursiveChecker.add({ uri: document.uri, range: { start: reference.range.start, end: reference.range.start } }); - const [virtualCode] = context.documents.getVirtualCodeByUri(document.uri); - const mirrorMap = virtualCode ? context.documents.getLinkedCodeMap(virtualCode) : undefined; + const decoded = context.decodeEmbeddedDocumentUri(document.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + const linkedCodeMap = virtualCode && sourceScript + ? context.documents.getLinkedCodeMap(virtualCode, sourceScript.id) + : undefined; - if (mirrorMap) { + if (linkedCodeMap) { - for (const linkedPos of mirrorMap.getLinkedCodePositions(reference.range.start)) { + for (const linkedPos of linkedCodeMap.getLinkedCodePositions(reference.range.start)) { - if (recursiveChecker.has({ uri: mirrorMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { + if (recursiveChecker.has({ uri: linkedCodeMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { continue; } foundMirrorPosition = true; - await withMirrors(mirrorMap.document, linkedPos); + await withLinkedCode(linkedCodeMap.document, linkedPos); } } diff --git a/packages/language-service/lib/features/provideDocumentSemanticTokens.ts b/packages/language-service/lib/features/provideDocumentSemanticTokens.ts index d7e71d41..e39b8104 100644 --- a/packages/language-service/lib/features/provideDocumentSemanticTokens.ts +++ b/packages/language-service/lib/features/provideDocumentSemanticTokens.ts @@ -16,12 +16,12 @@ export function register(context: ServiceContext) { _reportProgress?: (tokens: vscode.SemanticTokens) => void, // TODO ): Promise => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return; } - const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); if (!range) { range = { start: { line: 0, character: 0 }, diff --git a/packages/language-service/lib/features/provideFileReferences.ts b/packages/language-service/lib/features/provideFileReferences.ts index ef106ebe..818fd1ef 100644 --- a/packages/language-service/lib/features/provideFileReferences.ts +++ b/packages/language-service/lib/features/provideFileReferences.ts @@ -1,4 +1,4 @@ -import type { NullableResult, ServiceContext } from '../types'; +import type { NullableProviderResult, ServiceContext } from '../types'; import { documentFeatureWorker } from '../utils/featureWorkers'; import * as dedupe from '../utils/dedupe'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -8,7 +8,7 @@ import { isReferencesEnabled } from '@volar/language-core'; export function register(context: ServiceContext) { - return (uri: string, token = NoneCancellationToken): NullableResult => { + return (uri: string, token = NoneCancellationToken): NullableProviderResult => { return documentFeatureWorker( context, @@ -23,7 +23,10 @@ export function register(context: ServiceContext) { data => data .map(reference => { - const [virtualCode] = context.documents.getVirtualCodeByUri(reference.uri); + const decoded = context.decodeEmbeddedDocumentUri(reference.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!virtualCode) { return reference; } diff --git a/packages/language-service/lib/features/provideInlayHints.ts b/packages/language-service/lib/features/provideInlayHints.ts index d09e944c..9ace7d9a 100644 --- a/packages/language-service/lib/features/provideInlayHints.ts +++ b/packages/language-service/lib/features/provideInlayHints.ts @@ -16,8 +16,8 @@ export function register(context: ServiceContext) { return async (uri: string, range: vscode.Range, token = NoneCancellationToken) => { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return; } diff --git a/packages/language-service/lib/features/provideReferences.ts b/packages/language-service/lib/features/provideReferences.ts index ff3f7c0b..bbf0d2bc 100644 --- a/packages/language-service/lib/features/provideReferences.ts +++ b/packages/language-service/lib/features/provideReferences.ts @@ -24,11 +24,11 @@ export function register(context: ServiceContext) { const recursiveChecker = dedupe.createLocationSet(); const result: vscode.Location[] = []; - await withMirrors(document, position); + await withLinkedCode(document, position); return result; - async function withMirrors(document: TextDocument, position: vscode.Position) { + async function withLinkedCode(document: TextDocument, position: vscode.Position) { if (!service[1].provideReferences) { return; @@ -48,20 +48,24 @@ export function register(context: ServiceContext) { recursiveChecker.add({ uri: reference.uri, range: { start: reference.range.start, end: reference.range.start } }); - const [virtualCode] = context.documents.getVirtualCodeByUri(reference.uri); - const mirrorMap = virtualCode ? context.documents.getLinkedCodeMap(virtualCode) : undefined; + const decoded = context.decodeEmbeddedDocumentUri(reference.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + const linkedCodeMap = virtualCode && sourceScript + ? context.documents.getLinkedCodeMap(virtualCode, sourceScript.id) + : undefined; - if (mirrorMap) { + if (linkedCodeMap) { - for (const linkedPos of mirrorMap.getLinkedCodePositions(reference.range.start)) { + for (const linkedPos of linkedCodeMap.getLinkedCodePositions(reference.range.start)) { - if (recursiveChecker.has({ uri: mirrorMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { + if (recursiveChecker.has({ uri: linkedCodeMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { continue; } foundMirrorPosition = true; - await withMirrors(mirrorMap.document, linkedPos); + await withLinkedCode(linkedCodeMap.document, linkedPos); } } @@ -77,7 +81,9 @@ export function register(context: ServiceContext) { for (const reference of data) { - const [virtualCode] = context.documents.getVirtualCodeByUri(reference.uri); + const decoded = context.decodeEmbeddedDocumentUri(reference.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { for (const map of context.documents.getMaps(virtualCode)) { diff --git a/packages/language-service/lib/features/provideRenameEdits.ts b/packages/language-service/lib/features/provideRenameEdits.ts index da004161..62e8d034 100644 --- a/packages/language-service/lib/features/provideRenameEdits.ts +++ b/packages/language-service/lib/features/provideRenameEdits.ts @@ -36,11 +36,11 @@ export function register(context: ServiceContext) { const recursiveChecker = dedupe.createLocationSet(); let result: vscode.WorkspaceEdit | undefined; - await withMirrors(document, params.position, params.newName); + await withLinkedCode(document, params.position, params.newName); return result; - async function withMirrors(document: TextDocument, position: vscode.Position, newName: string) { + async function withLinkedCode(document: TextDocument, position: vscode.Position, newName: string) { if (!service[1].provideRenameEdits) { return; @@ -74,20 +74,24 @@ export function register(context: ServiceContext) { recursiveChecker.add({ uri: editUri, range: { start: textEdit.range.start, end: textEdit.range.start } }); - const [virtualCode] = context.documents.getVirtualCodeByUri(editUri); - const mirrorMap = virtualCode ? context.documents.getLinkedCodeMap(virtualCode) : undefined; + const decoded = context.decodeEmbeddedDocumentUri(editUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + const linkedCodeMap = virtualCode && sourceScript + ? context.documents.getLinkedCodeMap(virtualCode, sourceScript.id) + : undefined; - if (mirrorMap) { + if (linkedCodeMap) { - for (const linkedPos of mirrorMap.getLinkedCodePositions(textEdit.range.start)) { + for (const linkedPos of linkedCodeMap.getLinkedCodePositions(textEdit.range.start)) { - if (recursiveChecker.has({ uri: mirrorMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { + if (recursiveChecker.has({ uri: linkedCodeMap.document.uri, range: { start: linkedPos, end: linkedPos } })) { continue; } foundMirrorPosition = true; - await withMirrors(mirrorMap.document, linkedPos, newName); + await withLinkedCode(linkedCodeMap.document, linkedPos, newName); } } diff --git a/packages/language-service/lib/features/provideWorkspaceSymbols.ts b/packages/language-service/lib/features/provideWorkspaceSymbols.ts index dc913d60..ef09c712 100644 --- a/packages/language-service/lib/features/provideWorkspaceSymbols.ts +++ b/packages/language-service/lib/features/provideWorkspaceSymbols.ts @@ -26,7 +26,9 @@ export function register(context: ServiceContext) { } const symbols = embeddedSymbols.map(symbol => transformWorkspaceSymbol(symbol, loc => { - const [virtualCode] = context.documents.getVirtualCodeByUri(loc.uri); + const decoded = context.decodeEmbeddedDocumentUri(loc.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { for (const map of context.documents.getMaps(virtualCode)) { diff --git a/packages/language-service/lib/features/resolveCompletionItem.ts b/packages/language-service/lib/features/resolveCompletionItem.ts index 43ff8271..9ed8a349 100644 --- a/packages/language-service/lib/features/resolveCompletionItem.ts +++ b/packages/language-service/lib/features/resolveCompletionItem.ts @@ -20,9 +20,11 @@ export function register(context: ServiceContext) { item = Object.assign(item, data.original); - if (data.virtualDocumentUri) { + if (data.embeddedDocumentUri) { - const [virtualCode] = context.documents.getVirtualCodeByUri(data.virtualDocumentUri); + const decoded = context.decodeEmbeddedDocumentUri(data.embeddedDocumentUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { diff --git a/packages/language-service/lib/languageService.ts b/packages/language-service/lib/languageService.ts index 992ff3a5..1dc59bdd 100644 --- a/packages/language-service/lib/languageService.ts +++ b/packages/language-service/lib/languageService.ts @@ -1,6 +1,6 @@ -import { isDefinitionEnabled, isImplementationEnabled, isTypeDefinitionEnabled, type LanguageContext } from '@volar/language-core'; +import { isDefinitionEnabled, isImplementationEnabled, isTypeDefinitionEnabled, type Language } from '@volar/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -import { createDocumentProvider } from './documents'; +import { LinkedCodeMapWithDocument, SourceMapWithDocuments } from './documents'; import * as autoInsert from './features/provideAutoInsertionEdit'; import * as callHierarchy from './features/provideCallHierarchyItems'; import * as codeActions from './features/provideCodeActions'; @@ -33,17 +33,158 @@ import * as codeLensResolve from './features/resolveCodeLens'; import * as completionResolve from './features/resolveCompletionItem'; import * as documentLinkResolve from './features/resolveDocumentLink'; import * as inlayHintResolve from './features/resolveInlayHint'; -import type { ServiceContext, ServiceEnvironment, ServicePlugin } from './types'; +import type { ServiceContext, ServiceEnvironment, LanguageServicePlugin } from './types'; + +import type { CodeInformation, LinkedCodeMap, SourceMap, VirtualCode } from '@volar/language-core'; +import type * as ts from 'typescript'; +import { TextDocument } from 'vscode-languageserver-textdocument'; export type LanguageService = ReturnType; export function createLanguageService( - languageContext: LanguageContext, - servicePlugins: ServicePlugin[], + language: Language, + servicePlugins: LanguageServicePlugin[], env: ServiceEnvironment, ) { + const documentVersions = new Map(); + const map2DocMap = new WeakMap, SourceMapWithDocuments>(); + const mirrorMap2DocMirrorMap = new WeakMap(); + const snapshot2Doc = new WeakMap>(); + const embeddedContentScheme = 'volar-embedded-content'; + const context: ServiceContext = { + language, + documents: { + get(uri: string, languageId: string, snapshot: ts.IScriptSnapshot) { + if (!snapshot2Doc.has(snapshot)) { + snapshot2Doc.set(snapshot, new Map()); + } + const map = snapshot2Doc.get(snapshot)!; + if (!map.has(uri)) { + const version = documentVersions.get(uri) ?? 0; + documentVersions.set(uri, version + 1); + map.set(uri, TextDocument.create( + uri, + languageId, + version, + snapshot.getText(0, snapshot.getLength()), + )); + } + return map.get(uri)!; + }, + *getMaps(virtualCode: VirtualCode) { + for (const [uri, [snapshot, map]] of context.language.maps.forEach(virtualCode)) { + if (!map2DocMap.has(map)) { + const embeddedUri = context.encodeEmbeddedDocumentUri(uri, virtualCode.id); + map2DocMap.set(map, new SourceMapWithDocuments( + this.get(uri, context.language.scripts.get(uri)!.languageId, snapshot), + this.get(embeddedUri, virtualCode.languageId, virtualCode.snapshot), + map, + )); + } + yield map2DocMap.get(map)!; + } + }, + getLinkedCodeMap(virtualCode: VirtualCode, sourceScriptId: string) { + const map = context.language.linkedCodeMaps.get(virtualCode); + if (map) { + if (!mirrorMap2DocMirrorMap.has(map)) { + const embeddedUri = context.encodeEmbeddedDocumentUri(sourceScriptId, virtualCode.id); + mirrorMap2DocMirrorMap.set(map, new LinkedCodeMapWithDocument( + this.get(embeddedUri, virtualCode.languageId, virtualCode.snapshot), + map, + )); + } + return mirrorMap2DocMirrorMap.get(map)!; + } + }, + }, + env, + inject: (key, ...args) => { + for (const service of context.services) { + if (context.disabledServicePlugins.has(service[1])) { + continue; + } + const provide = service[1].provide?.[key as any]; + if (provide) { + return provide(...args as any); + } + } + throw `No service provide ${key as any}`; + }, + services: [], + commands: { + rename: { + create(uri, position) { + return { + title: '', + command: 'editor.action.rename', + arguments: [ + uri, + position, + ], + }; + }, + is(command) { + return command.command === 'editor.action.rename'; + }, + }, + showReferences: { + create(uri, position, locations) { + return { + title: locations.length === 1 ? '1 reference' : `${locations.length} references`, + command: 'editor.action.showReferences', + arguments: [ + uri, + position, + locations, + ], + }; + }, + is(command) { + return command.command === 'editor.action.showReferences'; + }, + }, + setSelection: { + create(position: vscode.Position) { + return { + title: '', + command: 'setSelection', + arguments: [{ + selection: { + selectionStartLineNumber: position.line + 1, + positionLineNumber: position.line + 1, + selectionStartColumn: position.character + 1, + positionColumn: position.character + 1, + }, + }], + }; + }, + is(command) { + return command.command === 'setSelection'; + }, + }, + }, + disabledEmbeddedDocumentUris: new Set(), + disabledServicePlugins: new WeakSet(), + decodeEmbeddedDocumentUri(maybeEmbeddedContentUri: string) { + if (maybeEmbeddedContentUri.startsWith(`${embeddedContentScheme}://`)) { + const trimed = maybeEmbeddedContentUri.substring(`${embeddedContentScheme}://`.length); + const embeddedCodeId = trimed.substring(0, trimed.indexOf('/')); + const documentUri = trimed.substring(embeddedCodeId.length + 1); + return [ + decodeURIComponent(documentUri), + decodeURIComponent(embeddedCodeId), + ]; + } + }, + encodeEmbeddedDocumentUri(documentUri: string, embeddedContentId: string) { + return `${embeddedContentScheme}://${encodeURIComponent(embeddedContentId)}/${encodeURIComponent(documentUri)}`; + }, + }; - const context = createServiceContext(); + for (const servicePlugin of servicePlugins) { + context.services.push([servicePlugin, servicePlugin.create(context)]); + } return { @@ -90,86 +231,4 @@ export function createLanguageService( dispose: () => context.services.forEach(service => service[1].dispose?.()), context, }; - - function createServiceContext() { - - const context: ServiceContext = { - language: languageContext, - documents: createDocumentProvider(languageContext.files), - env, - inject: (key, ...args) => { - for (const service of context.services) { - if (context.disabledServicePlugins.has(service[1])) { - continue; - } - const provide = service[1].provide?.[key as any]; - if (provide) { - return provide(...args as any); - } - } - throw `No service provide ${key as any}`; - }, - services: [], - commands: { - rename: { - create(uri, position) { - return { - title: '', - command: 'editor.action.rename', - arguments: [ - uri, - position, - ], - }; - }, - is(command) { - return command.command === 'editor.action.rename'; - }, - }, - showReferences: { - create(uri, position, locations) { - return { - title: locations.length === 1 ? '1 reference' : `${locations.length} references`, - command: 'editor.action.showReferences', - arguments: [ - uri, - position, - locations, - ], - }; - }, - is(command) { - return command.command === 'editor.action.showReferences'; - }, - }, - setSelection: { - create(position: vscode.Position) { - return { - title: '', - command: 'setSelection', - arguments: [{ - selection: { - selectionStartLineNumber: position.line + 1, - positionLineNumber: position.line + 1, - selectionStartColumn: position.character + 1, - positionColumn: position.character + 1, - }, - }], - }; - }, - is(command) { - return command.command === 'setSelection'; - }, - }, - }, - disabledVirtualFileUris: new Set(), - disabledServicePlugins: new WeakSet(), - }; - - for (const servicePlugin of servicePlugins) { - context.services.push([servicePlugin, servicePlugin.create(context)]); - } - - return context; - } } diff --git a/packages/language-service/lib/types.ts b/packages/language-service/lib/types.ts index d9fd467f..43949ba2 100644 --- a/packages/language-service/lib/types.ts +++ b/packages/language-service/lib/types.ts @@ -1,7 +1,8 @@ -import type { LanguageContext, VirtualCode } from '@volar/language-core'; +import type { Language, SourceScript, VirtualCode } from '@volar/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; import type { TextDocument } from 'vscode-languageserver-textdocument'; -import type { DocumentProvider } from './documents'; +import type * as ts from 'typescript'; +import type { LinkedCodeMapWithDocument, SourceMapWithDocuments } from './documents'; export type * from 'vscode-languageserver-protocol'; @@ -21,9 +22,9 @@ export interface ServiceEnvironment { } export interface FileSystem { - stat(uri: string): Result; - readDirectory(uri: string): Result<[string, FileType][]>; - readFile(uri: string, encoding?: string): Result; + stat(uri: string): ProviderResult; + readDirectory(uri: string): ProviderResult<[string, FileType][]>; + readFile(uri: string, encoding?: string): ProviderResult; } export interface FileStat { @@ -46,7 +47,7 @@ export interface ServiceCommand { } export interface ServiceContext { - language: LanguageContext; + language: Language; env: ServiceEnvironment; inject( key: K, @@ -57,14 +58,23 @@ export interface ServiceContext { rename: ServiceCommand<[uri: string, position: vscode.Position]>; setSelection: ServiceCommand<[position: vscode.Position]>; }; - documents: DocumentProvider; - services: [ServicePlugin, ServicePluginInstance][]; - disabledVirtualFileUris: Set; - disabledServicePlugins: WeakSet; + documents: { + get(uri: string, languageId: string, snapshot: ts.IScriptSnapshot): TextDocument; + getMaps(virtualCode: VirtualCode): Generator; + getLinkedCodeMap(virtualCode: VirtualCode, sourceScriptId: string): LinkedCodeMapWithDocument | undefined; + }; + services: [LanguageServicePlugin, LanguageServicePluginInstance][]; + disabledEmbeddedDocumentUris: Set; + disabledServicePlugins: WeakSet; + decodeEmbeddedDocumentUri(maybeEmbeddedUri: string): [ + documentUri: string, + embeddedCodeId: string, + ] | undefined; + encodeEmbeddedDocumentUri(uri: string, embeededCodeId: string): string; } -export type Result = T | Thenable; -export type NullableResult = Result; +export type ProviderResult = T | Thenable; +export type NullableProviderResult = ProviderResult; export type SemanticToken = [ line: number, character: number, @@ -73,13 +83,13 @@ export type SemanticToken = [ tokenModifiers: number, ]; -export interface ServicePlugin

{ +export interface LanguageServicePlugin

{ name?: string; triggerCharacters?: string[]; signatureHelpTriggerCharacters?: string[]; signatureHelpRetriggerCharacters?: string[]; autoFormatTriggerCharacters?: string[]; - create(context: ServiceContext): ServicePluginInstance

; + create(context: ServiceContext): LanguageServicePluginInstance

; } export interface EmbeddedCodeFormattingOptions { @@ -87,49 +97,49 @@ export interface EmbeddedCodeFormattingOptions { initialIndentLevel: number; } -export interface ServicePluginInstance

{ +export interface LanguageServicePluginInstance

{ provide?: P; isAdditionalCompletion?: boolean; // volar specific - provideHover?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideDocumentSymbols?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideDocumentHighlights?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideLinkedEditingRanges?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideDefinition?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideTypeDefinition?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideImplementation?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideCodeLenses?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideCodeActions?(document: TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): NullableResult; - provideDocumentFormattingEdits?(document: TextDocument, range: vscode.Range, options: vscode.FormattingOptions, embeddedCodeContext: EmbeddedCodeFormattingOptions | undefined, token: vscode.CancellationToken): NullableResult; - provideOnTypeFormattingEdits?(document: TextDocument, position: vscode.Position, key: string, options: vscode.FormattingOptions, embeddedCodeContext: EmbeddedCodeFormattingOptions | undefined, token: vscode.CancellationToken): NullableResult; - provideDocumentLinks?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideCompletionItems?(document: TextDocument, position: vscode.Position, context: vscode.CompletionContext, token: vscode.CancellationToken): NullableResult; - provideDocumentColors?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideColorPresentations?(document: TextDocument, color: vscode.Color, range: vscode.Range, token: vscode.CancellationToken): NullableResult; - provideFoldingRanges?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideSignatureHelp?(document: TextDocument, position: vscode.Position, context: vscode.SignatureHelpContext, token: vscode.CancellationToken): NullableResult; - provideRenameRange?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideRenameEdits?(document: TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): NullableResult; - provideReferences?(document: TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): NullableResult; - provideSelectionRanges?(document: TextDocument, positions: vscode.Position[], token: vscode.CancellationToken): NullableResult; - provideInlayHints?(document: TextDocument, range: vscode.Range, token: vscode.CancellationToken): NullableResult; - provideCallHierarchyItems?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableResult; - provideCallHierarchyIncomingCalls?(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Result; - provideCallHierarchyOutgoingCalls?(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Result; - provideDocumentSemanticTokens?(document: TextDocument, range: vscode.Range, legend: vscode.SemanticTokensLegend, token: vscode.CancellationToken): NullableResult; - provideWorkspaceSymbols?(query: string, token: vscode.CancellationToken): NullableResult; - provideDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideSemanticDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableResult; - provideFileReferences?(document: TextDocument, token: vscode.CancellationToken): NullableResult; // volar specific - provideReferencesCodeLensRanges?(document: TextDocument, token: vscode.CancellationToken): NullableResult; // volar specific - provideAutoInsertionEdit?(document: TextDocument, position: vscode.Position, lastChange: { range: vscode.Range; text: string; }, token: vscode.CancellationToken): NullableResult; // volar specific - provideFileRenameEdits?(oldUri: string, newUri: string, token: vscode.CancellationToken): NullableResult; // volar specific - provideDocumentDropEdits?(document: TextDocument, position: vscode.Position, dataTransfer: Map, token: vscode.CancellationToken): NullableResult; // volar specific - resolveCodeLens?(codeLens: vscode.CodeLens, token: vscode.CancellationToken): Result; - resolveCodeAction?(codeAction: vscode.CodeAction, token: vscode.CancellationToken): Result; - resolveCompletionItem?(item: vscode.CompletionItem, token: vscode.CancellationToken): Result; - resolveDocumentLink?(link: vscode.DocumentLink, token: vscode.CancellationToken): Result; - resolveInlayHint?(inlayHint: vscode.InlayHint, token: vscode.CancellationToken): Result; - resolveEmbeddedCodeFormattingOptions?(code: VirtualCode, options: EmbeddedCodeFormattingOptions, token: vscode.CancellationToken): NullableResult; // volar specific + provideHover?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideDocumentSymbols?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideDocumentHighlights?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideLinkedEditingRanges?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideDefinition?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideTypeDefinition?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideImplementation?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideCodeLenses?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideCodeActions?(document: TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): NullableProviderResult; + provideDocumentFormattingEdits?(document: TextDocument, range: vscode.Range, options: vscode.FormattingOptions, embeddedCodeContext: EmbeddedCodeFormattingOptions | undefined, token: vscode.CancellationToken): NullableProviderResult; + provideOnTypeFormattingEdits?(document: TextDocument, position: vscode.Position, key: string, options: vscode.FormattingOptions, embeddedCodeContext: EmbeddedCodeFormattingOptions | undefined, token: vscode.CancellationToken): NullableProviderResult; + provideDocumentLinks?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideCompletionItems?(document: TextDocument, position: vscode.Position, context: vscode.CompletionContext, token: vscode.CancellationToken): NullableProviderResult; + provideDocumentColors?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideColorPresentations?(document: TextDocument, color: vscode.Color, range: vscode.Range, token: vscode.CancellationToken): NullableProviderResult; + provideFoldingRanges?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideSignatureHelp?(document: TextDocument, position: vscode.Position, context: vscode.SignatureHelpContext, token: vscode.CancellationToken): NullableProviderResult; + provideRenameRange?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideRenameEdits?(document: TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): NullableProviderResult; + provideReferences?(document: TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): NullableProviderResult; + provideSelectionRanges?(document: TextDocument, positions: vscode.Position[], token: vscode.CancellationToken): NullableProviderResult; + provideInlayHints?(document: TextDocument, range: vscode.Range, token: vscode.CancellationToken): NullableProviderResult; + provideCallHierarchyItems?(document: TextDocument, position: vscode.Position, token: vscode.CancellationToken): NullableProviderResult; + provideCallHierarchyIncomingCalls?(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): ProviderResult; + provideCallHierarchyOutgoingCalls?(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): ProviderResult; + provideDocumentSemanticTokens?(document: TextDocument, range: vscode.Range, legend: vscode.SemanticTokensLegend, token: vscode.CancellationToken): NullableProviderResult; + provideWorkspaceSymbols?(query: string, token: vscode.CancellationToken): NullableProviderResult; + provideDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideSemanticDiagnostics?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; + provideFileReferences?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; // volar specific + provideReferencesCodeLensRanges?(document: TextDocument, token: vscode.CancellationToken): NullableProviderResult; // volar specific + provideAutoInsertionEdit?(document: TextDocument, position: vscode.Position, lastChange: { range: vscode.Range; text: string; }, token: vscode.CancellationToken): NullableProviderResult; // volar specific + provideFileRenameEdits?(oldUri: string, newUri: string, token: vscode.CancellationToken): NullableProviderResult; // volar specific + provideDocumentDropEdits?(document: TextDocument, position: vscode.Position, dataTransfer: Map, token: vscode.CancellationToken): NullableProviderResult; // volar specific + resolveCodeLens?(codeLens: vscode.CodeLens, token: vscode.CancellationToken): ProviderResult; + resolveCodeAction?(codeAction: vscode.CodeAction, token: vscode.CancellationToken): ProviderResult; + resolveCompletionItem?(item: vscode.CompletionItem, token: vscode.CancellationToken): ProviderResult; + resolveDocumentLink?(link: vscode.DocumentLink, token: vscode.CancellationToken): ProviderResult; + resolveInlayHint?(inlayHint: vscode.InlayHint, token: vscode.CancellationToken): ProviderResult; + resolveEmbeddedCodeFormattingOptions?(sourceScript: SourceScript, embeddedCode: VirtualCode, options: EmbeddedCodeFormattingOptions, token: vscode.CancellationToken): NullableProviderResult; // volar specific transformCompletionItem?(item: vscode.CompletionItem): vscode.CompletionItem | undefined; // volar specific transformCodeAction?(item: vscode.CodeAction): vscode.CodeAction | undefined; // volar specific dispose?(): void; diff --git a/packages/language-service/lib/utils/featureWorkers.ts b/packages/language-service/lib/utils/featureWorkers.ts index 9a704233..a115030d 100644 --- a/packages/language-service/lib/utils/featureWorkers.ts +++ b/packages/language-service/lib/utils/featureWorkers.ts @@ -1,13 +1,13 @@ import type { CodeInformation, VirtualCode } from '@volar/language-core'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { SourceMapWithDocuments } from '../documents'; -import type { ServiceContext, ServicePluginInstance, ServicePlugin } from '../types'; +import type { ServiceContext, LanguageServicePluginInstance, LanguageServicePlugin } from '../types'; export async function documentFeatureWorker( context: ServiceContext, uri: string, valid: (map: SourceMapWithDocuments) => boolean, - worker: (service: [ServicePlugin, ServicePluginInstance], document: TextDocument) => Thenable | T | null | undefined, + worker: (service: [LanguageServicePlugin, LanguageServicePluginInstance], document: TextDocument) => Thenable | T | null | undefined, transformResult: (result: T, map?: SourceMapWithDocuments) => T | undefined, combineResult?: (results: T[]) => T, ) { @@ -31,21 +31,21 @@ export async function languageFeatureWorker( uri: string, getReadDocParams: () => K, eachVirtualDocParams: (map: SourceMapWithDocuments) => Generator, - worker: (service: [ServicePlugin, ServicePluginInstance], document: TextDocument, params: K, map?: SourceMapWithDocuments) => Thenable | T | null | undefined, + worker: (service: [LanguageServicePlugin, LanguageServicePluginInstance], document: TextDocument, params: K, map?: SourceMapWithDocuments) => Thenable | T | null | undefined, transformResult: (result: T, map?: SourceMapWithDocuments) => T | undefined, combineResult?: (results: T[]) => T, ) { - const sourceFile = context.language.files.get(uri); - if (!sourceFile) { + const sourceScript = context.language.scripts.get(uri); + if (!sourceScript) { return; } let results: T[] = []; - if (sourceFile.generated) { + if (sourceScript.generated) { - for (const map of eachEmbeddedDocument(context, sourceFile.generated.code)) { + for (const map of forEachEmbeddedDocument(context, sourceScript.id, sourceScript.generated.root)) { for (const mappedArg of eachVirtualDocParams(map)) { @@ -78,7 +78,7 @@ export async function languageFeatureWorker( } else { - const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const document = context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); const params = getReadDocParams(); for (const [serviceId, service] of Object.entries(context.services)) { @@ -124,23 +124,22 @@ export async function safeCall(cb: () => Thenable | T, errorMsg?: string) } } -export function* eachEmbeddedDocument( +export function* forEachEmbeddedDocument( context: ServiceContext, + sourceScriptId: string, current: VirtualCode, - rootCode = current, ): Generator> { if (current.embeddedCodes) { for (const embeddedCode of current.embeddedCodes) { - yield* eachEmbeddedDocument(context, embeddedCode, rootCode); + yield* forEachEmbeddedDocument(context, sourceScriptId, embeddedCode); } } for (const map of context.documents.getMaps(current)) { - const sourceFile = context.language.files.get(map.sourceDocument.uri); if ( - sourceFile?.generated?.code === rootCode - && !context.disabledVirtualFileUris.has(context.documents.getVirtualCodeUri(context.language.files.getByVirtualCode(current).id, current.id)) + sourceScriptId === map.sourceDocument.uri + && !context.disabledEmbeddedDocumentUris.has(context.encodeEmbeddedDocumentUri(sourceScriptId, current.id)) ) { yield map; } @@ -162,7 +161,7 @@ export function getEmbeddedFilesByLevel(context: ServiceContext, sourceFileUri: for (const file of embeddedFilesByLevel[embeddedFilesByLevel.length - 1]) { if (file.embeddedCodes) { for (const embedded of file.embeddedCodes) { - if (!context.disabledVirtualFileUris.has(context.documents.getVirtualCodeUri(sourceFileUri, embedded.id))) { + if (!context.disabledEmbeddedDocumentUris.has(context.encodeEmbeddedDocumentUri(sourceFileUri, embedded.id))) { nextLevel.push(embedded); } } diff --git a/packages/language-service/lib/utils/transform.ts b/packages/language-service/lib/utils/transform.ts index bf97c410..7e72f2ac 100644 --- a/packages/language-service/lib/utils/transform.ts +++ b/packages/language-service/lib/utils/transform.ts @@ -8,7 +8,9 @@ import { notEmpty } from './common'; export function transformDocumentLinkTarget(target: string, context: ServiceContext) { const targetUri = URI.parse(target); const clearUri = targetUri.with({ fragment: '' }).toString(true); - const [virtualCode] = context.documents.getVirtualCodeByUri(clearUri); + const decoded = context.decodeEmbeddedDocumentUri(clearUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { for (const map of context.documents.getMaps(virtualCode)) { @@ -306,7 +308,7 @@ export function transformWorkspaceSymbol(symbol: vscode.WorkspaceSymbol, getOthe export function transformWorkspaceEdit( edit: vscode.WorkspaceEdit, - { documents }: ServiceContext, + context: ServiceContext, mode: 'fileName' | 'rename' | 'codeAction' | undefined, versions: Record = {}, ) { @@ -319,10 +321,12 @@ export function transformWorkspaceEdit( sourceResult.changeAnnotations ??= {}; const tsAnno = edit.changeAnnotations[tsUri]; - const [virtualCode] = documents.getVirtualCodeByUri(tsUri); + const decoded = context.decodeEmbeddedDocumentUri(tsUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { - for (const map of documents.getMaps(virtualCode)) { + for (const map of context.documents.getMaps(virtualCode)) { // TODO: check capability? const uri = map.sourceDocument.uri; sourceResult.changeAnnotations[uri] = tsAnno; @@ -336,10 +340,12 @@ export function transformWorkspaceEdit( sourceResult.changes ??= {}; - const [virtualCode] = documents.getVirtualCodeByUri(tsUri); + const decoded = context.decodeEmbeddedDocumentUri(tsUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { - for (const map of documents.getMaps(virtualCode)) { + for (const map of context.documents.getMaps(virtualCode)) { const tsEdits = edit.changes[tsUri]; for (const tsEdit of tsEdits) { if (mode === 'rename' || mode === 'fileName' || mode === 'codeAction') { @@ -384,10 +390,12 @@ export function transformWorkspaceEdit( let sourceEdit: typeof tsDocEdit | undefined; if ('textDocument' in tsDocEdit) { - const [virtualCode] = documents.getVirtualCodeByUri(tsDocEdit.textDocument.uri); + const decoded = context.decodeEmbeddedDocumentUri(tsDocEdit.textDocument.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { - for (const map of documents.getMaps(virtualCode)) { + for (const map of context.documents.getMaps(virtualCode)) { sourceEdit = { textDocument: { uri: map.sourceDocument.uri, @@ -436,10 +444,12 @@ export function transformWorkspaceEdit( } else if (tsDocEdit.kind === 'rename') { - const [virtualCode] = documents.getVirtualCodeByUri(tsDocEdit.oldUri); + const decoded = context.decodeEmbeddedDocumentUri(tsDocEdit.oldUri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { - for (const map of documents.getMaps(virtualCode)) { + for (const map of context.documents.getMaps(virtualCode)) { // TODO: check capability? sourceEdit = { kind: 'rename', @@ -456,10 +466,12 @@ export function transformWorkspaceEdit( } else if (tsDocEdit.kind === 'delete') { - const [virtualCode] = documents.getVirtualCodeByUri(tsDocEdit.uri); + const decoded = context.decodeEmbeddedDocumentUri(tsDocEdit.uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (virtualCode) { - for (const map of documents.getMaps(virtualCode)) { + for (const map of context.documents.getMaps(virtualCode)) { // TODO: check capability? sourceEdit = { kind: 'delete', diff --git a/packages/monaco/worker.ts b/packages/monaco/worker.ts index e868f97b..fe0bf7f9 100644 --- a/packages/monaco/worker.ts +++ b/packages/monaco/worker.ts @@ -1,9 +1,9 @@ import { LanguagePlugin, - LanguageContext, - ServicePlugin, + Language, + LanguageServicePlugin, createLanguageService as _createLanguageService, - createFileRegistry, + createLanguage, resolveCommonLanguageId, type LanguageService, type ServiceEnvironment, @@ -11,7 +11,7 @@ import { } from '@volar/language-service'; import type * as monaco from 'monaco-types'; import type * as ts from 'typescript'; -import { createLanguage, createSys } from '@volar/typescript'; +import { createTypeScriptLanguage, createSys } from '@volar/typescript'; export * from '@volar/language-service'; export * from './lib/ata.js'; @@ -26,11 +26,11 @@ export function createSimpleWorkerService({ env: ServiceEnvironment; workerContext: monaco.worker.IWorkerContext; languagePlugins?: LanguagePlugin[]; - servicePlugins?: ServicePlugin[]; + servicePlugins?: LanguageServicePlugin[]; extraApis?: T; }) { const snapshots = new Map(); - const files = createFileRegistry( + const language = createLanguage( languagePlugins, false, uri => { @@ -47,15 +47,15 @@ export function createSimpleWorkerService({ getChangeRange: () => undefined, }; snapshots.set(model, [model.version, snapshot]); - files.set(uri, resolveCommonLanguageId(uri), snapshot); + language.scripts.set(uri, resolveCommonLanguageId(uri), snapshot); } else { - files.delete(uri); + language.scripts.delete(uri); } } ); - return createWorkerService({ files }, servicePlugins, env, extraApis); + return createWorkerService(language, servicePlugins, env, extraApis); } export function createTypeScriptWorkerService({ @@ -72,7 +72,7 @@ export function createTypeScriptWorkerService({ env: ServiceEnvironment; workerContext: monaco.worker.IWorkerContext; languagePlugins?: LanguagePlugin[]; - servicePlugins?: ServicePlugin[]; + servicePlugins?: LanguageServicePlugin[]; extraApis?: T; }) { @@ -82,6 +82,14 @@ export function createTypeScriptWorkerService({ const modelVersions = new Map(); const sys = createSys(ts, env, env.typescript!.uriToFileName(env.workspaceFolder)); const host: TypeScriptProjectHost = { + ...sys, + configFileName: undefined, + syncSystem() { + return sys.sync(); + }, + getSystemVersion() { + return sys.version; + }, getCurrentDirectory() { return env.typescript!.uriToFileName(env.workspaceFolder); }, @@ -123,30 +131,26 @@ export function createTypeScriptWorkerService({ return compilerOptions; }, getLanguageId: id => resolveCommonLanguageId(id), + fileNameToScriptId: env.typescript!.fileNameToUri, + scriptIdToFileName: env.typescript!.uriToFileName, }; - const languageContext = createLanguage( + const language = createTypeScriptLanguage( ts, - sys, languagePlugins, - undefined, host, - { - fileNameToFileId: env.typescript!.fileNameToUri, - fileIdToFileName: env.typescript!.uriToFileName, - }, ); - return createWorkerService(languageContext, servicePlugins, env, extraApis); + return createWorkerService(language, servicePlugins, env, extraApis); } function createWorkerService( - languageContext: LanguageContext, - servicePlugins: ServicePlugin[], + language: Language, + servicePlugins: LanguageServicePlugin[], env: ServiceEnvironment, extraApis: T = {} as any, ): LanguageService & T { - const languageService = _createLanguageService(languageContext, servicePlugins, env); + const languageService = _createLanguageService(language, servicePlugins, env); class WorkerService { }; diff --git a/packages/test-utils/index.ts b/packages/test-utils/index.ts index ffb79656..3447b2f5 100644 --- a/packages/test-utils/index.ts +++ b/packages/test-utils/index.ts @@ -410,13 +410,13 @@ export function startLanguageServer(serverModule: string, cwd?: string | URL) { } } -export function* printSnapshots(sourceFile: _.SourceFile) { - if (sourceFile.generated) { +export function* printSnapshots(sourceScript: _.SourceScript) { + if (sourceScript.generated) { let lastId = 0; - for (const file of forEachEmbeddedCode(sourceFile.generated.code)) { + for (const file of forEachEmbeddedCode(sourceScript.generated.root)) { const id = lastId++; yield `#${id}`; - for (const line of printSnapshot(sourceFile, file)) { + for (const line of printSnapshot(sourceScript, file)) { yield ' ' + line; } } @@ -424,13 +424,13 @@ export function* printSnapshots(sourceFile: _.SourceFile) { } export function* printSnapshot( - sourceFile: { - snapshot: _.SourceFile['snapshot']; + sourceScript: { + snapshot: _.SourceScript['snapshot']; }, file: _.VirtualCode, ) { - const sourceCode = sourceFile.snapshot.getText(0, sourceFile.snapshot.getLength()); + const sourceCode = sourceScript.snapshot.getText(0, sourceScript.snapshot.getLength()); const sourceFileDocument = TextDocument.create('', '', 0, sourceCode); const virtualCode = file.snapshot.getText(0, file.snapshot.getLength()); const virtualCodeLines = virtualCode.split('\n'); diff --git a/packages/typescript/index.ts b/packages/typescript/index.ts index 9311cd69..925bd514 100644 --- a/packages/typescript/index.ts +++ b/packages/typescript/index.ts @@ -1,4 +1,3 @@ -export * from './lib/documentRegistry'; export * from './lib/node/decorateLanguageService'; export * from './lib/node/decorateLanguageServiceHost'; export * from './lib/node/decorateProgram'; diff --git a/packages/typescript/lib/documentRegistry.ts b/packages/typescript/lib/documentRegistry.ts deleted file mode 100644 index 079f7461..00000000 --- a/packages/typescript/lib/documentRegistry.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type * as ts from 'typescript'; - -const documentRegistries: [boolean, string, ts.DocumentRegistry][] = []; - -export function getDocumentRegistry(ts: typeof import('typescript'), useCaseSensitiveFileNames: boolean, currentDirectory: string) { - let documentRegistry = documentRegistries.find(item => item[0] === useCaseSensitiveFileNames && item[1] === currentDirectory)?.[2]; - if (!documentRegistry) { - documentRegistry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory); - documentRegistries.push([useCaseSensitiveFileNames, currentDirectory, documentRegistry]); - } - return documentRegistry; -} diff --git a/packages/typescript/lib/node/decorateLanguageService.ts b/packages/typescript/lib/node/decorateLanguageService.ts index 1dd3eb93..9858d86d 100644 --- a/packages/typescript/lib/node/decorateLanguageService.ts +++ b/packages/typescript/lib/node/decorateLanguageService.ts @@ -1,6 +1,6 @@ import { CodeInformation, - FileRegistry, + Language, isCallHierarchyEnabled, isCodeActionsEnabled, isCompletionEnabled, @@ -16,10 +16,10 @@ import { } from '@volar/language-core'; import type * as ts from 'typescript'; import { dedupeDocumentSpans, dedupeReferencedSymbols } from './dedupe'; -import { getVirtualFileAndMap, notEmpty } from './utils'; +import { getServiceScript, notEmpty } from './utils'; import { transformCallHierarchyItem, transformDiagnostic, transformDocumentSpan, transformFileTextChanges, transformSpan } from './transform'; -export function decorateLanguageService(files: FileRegistry, languageService: ts.LanguageService) { +export function decorateLanguageService(language: Language, languageService: ts.LanguageService) { // ignored methods @@ -29,8 +29,8 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } = languageService; languageService.getNavigationTree = fileName => { - const [virtualCode] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript] = getServiceScript(language, fileName); + if (serviceScript) { const tree = getNavigationTree(fileName); tree.childItems = undefined; return tree; @@ -40,8 +40,8 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } }; languageService.getOutliningSpans = fileName => { - const [virtualCode] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript] = getServiceScript(language, fileName); + if (serviceScript) { return []; } else { @@ -80,16 +80,16 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } = languageService; languageService.prepareCallHierarchy = (fileName, position) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isCallHierarchyEnabled(mapping.data)) { - const item = prepareCallHierarchy(fileName, generateOffset + sourceFile.snapshot.getLength()); + const item = prepareCallHierarchy(fileName, generateOffset + sourceScript.snapshot.getLength()); if (Array.isArray(item)) { - return item.map(item => transformCallHierarchyItem(files, item, isCallHierarchyEnabled)); + return item.map(item => transformCallHierarchyItem(language, item, isCallHierarchyEnabled)); } else if (item) { - return transformCallHierarchyItem(files, item, isCallHierarchyEnabled); + return transformCallHierarchyItem(language, item, isCallHierarchyEnabled); } } } @@ -100,11 +100,11 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }; languageService.provideCallHierarchyIncomingCalls = (fileName, position) => { let calls: ts.CallHierarchyIncomingCall[] = []; - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isCallHierarchyEnabled(mapping.data)) { - calls = provideCallHierarchyIncomingCalls(fileName, generateOffset + sourceFile.snapshot.getLength()); + calls = provideCallHierarchyIncomingCalls(fileName, generateOffset + sourceScript.snapshot.getLength()); } } } @@ -113,9 +113,9 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } return calls .map(call => { - const from = transformCallHierarchyItem(files, call.from, isCallHierarchyEnabled); + const from = transformCallHierarchyItem(language, call.from, isCallHierarchyEnabled); const fromSpans = call.fromSpans - .map(span => transformSpan(files, call.from.file, span, isCallHierarchyEnabled)?.textSpan) + .map(span => transformSpan(language, call.from.file, span, isCallHierarchyEnabled)?.textSpan) .filter(notEmpty); return { from, @@ -125,11 +125,11 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }; languageService.provideCallHierarchyOutgoingCalls = (fileName, position) => { let calls: ts.CallHierarchyOutgoingCall[] = []; - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isCallHierarchyEnabled(mapping.data)) { - calls = provideCallHierarchyOutgoingCalls(fileName, generateOffset + sourceFile.snapshot.getLength()); + calls = provideCallHierarchyOutgoingCalls(fileName, generateOffset + sourceScript.snapshot.getLength()); } } } @@ -138,9 +138,9 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } return calls .map(call => { - const to = transformCallHierarchyItem(files, call.to, isCallHierarchyEnabled); + const to = transformCallHierarchyItem(language, call.to, isCallHierarchyEnabled); const fromSpans = call.fromSpans - .map(span => transformSpan(files, fileName, span, isCallHierarchyEnabled)?.textSpan) + .map(span => transformSpan(language, fileName, span, isCallHierarchyEnabled)?.textSpan) .filter(notEmpty); return { to, @@ -151,18 +151,18 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts languageService.organizeImports = (args, formatOptions, preferences) => { const unresolved = organizeImports(args, formatOptions, preferences); const resolved = unresolved - .map(changes => transformFileTextChanges(files, changes, isCodeActionsEnabled)) + .map(changes => transformFileTextChanges(language, changes, isCodeActionsEnabled)) .filter(notEmpty); return resolved; }; languageService.getQuickInfoAtPosition = (fileName, position) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isHoverEnabled(mapping.data)) { - const result = getQuickInfoAtPosition(fileName, generateOffset + sourceFile.snapshot.getLength()); + const result = getQuickInfoAtPosition(fileName, generateOffset + sourceScript.snapshot.getLength()); if (result) { - const textSpan = transformSpan(files, fileName, result.textSpan, isHoverEnabled)?.textSpan; + const textSpan = transformSpan(language, fileName, result.textSpan, isHoverEnabled)?.textSpan; if (textSpan) { return { ...result, @@ -198,11 +198,11 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ...highlights, highlightSpans: highlights.highlightSpans .map(span => { - const textSpan = transformSpan(files, span.fileName ?? highlights.fileName, span.textSpan, isHighlightEnabled)?.textSpan; + const textSpan = transformSpan(language, span.fileName ?? highlights.fileName, span.textSpan, isHighlightEnabled)?.textSpan; if (textSpan) { return { ...span, - contextSpan: transformSpan(files, span.fileName ?? highlights.fileName, span.contextSpan, isHighlightEnabled)?.textSpan, + contextSpan: transformSpan(language, span.fileName ?? highlights.fileName, span.contextSpan, isHighlightEnabled)?.textSpan, textSpan, }; } @@ -213,15 +213,15 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts return resolved; }; languageService.getApplicableRefactors = (fileName, positionOrRange, preferences, triggerReason, kind, includeInteractiveActions) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos)) { if (isCodeActionsEnabled(mapping.data)) { const por = typeof positionOrRange === 'number' - ? generateOffset + sourceFile.snapshot.getLength() + ? generateOffset + sourceScript.snapshot.getLength() : { - pos: generateOffset + sourceFile.snapshot.getLength(), - end: generateOffset + positionOrRange.end - positionOrRange.pos + sourceFile.snapshot.getLength(), + pos: generateOffset + sourceScript.snapshot.getLength(), + end: generateOffset + positionOrRange.end - positionOrRange.pos + sourceScript.snapshot.getLength(), }; return getApplicableRefactors(fileName, por, preferences, triggerReason, kind, includeInteractiveActions); } @@ -234,15 +234,15 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }; languageService.getEditsForRefactor = (fileName, formatOptions, positionOrRange, refactorName, actionName, preferences) => { let edits: ts.RefactorEditInfo | undefined; - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos)) { if (isCodeActionsEnabled(mapping.data)) { const por = typeof positionOrRange === 'number' - ? generateOffset + sourceFile.snapshot.getLength() + ? generateOffset + sourceScript.snapshot.getLength() : { - pos: generateOffset + sourceFile.snapshot.getLength(), - end: generateOffset + positionOrRange.end - positionOrRange.pos + sourceFile.snapshot.getLength(), + pos: generateOffset + sourceScript.snapshot.getLength(), + end: generateOffset + positionOrRange.end - positionOrRange.pos + sourceScript.snapshot.getLength(), }; edits = getEditsForRefactor(fileName, formatOptions, por, refactorName, actionName, preferences); } @@ -253,19 +253,19 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts } if (edits) { edits.edits = edits.edits - .map(edit => transformFileTextChanges(files, edit, isCodeActionsEnabled)) + .map(edit => transformFileTextChanges(language, edit, isCodeActionsEnabled)) .filter(notEmpty); return edits; } }; languageService.getRenameInfo = (fileName, position, options) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isRenameEnabled(mapping.data)) { - const info = getRenameInfo(fileName, generateOffset + sourceFile.snapshot.getLength(), options); + const info = getRenameInfo(fileName, generateOffset + sourceScript.snapshot.getLength(), options); if (info.canRename) { - const span = transformSpan(files, fileName, info.triggerSpan, isRenameEnabled); + const span = transformSpan(language, fileName, info.triggerSpan, isRenameEnabled); if (span) { info.triggerSpan = span.textSpan; return info; @@ -287,16 +287,16 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }; languageService.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => { let fixes: readonly ts.CodeFixAction[] = []; - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateStart, mapping] of map.getGeneratedOffsets(start)) { if (isCodeActionsEnabled(mapping.data)) { for (const [generateEnd, mapping] of map.getGeneratedOffsets(end)) { if (isCodeActionsEnabled(mapping.data)) { fixes = getCodeFixesAtPosition( fileName, - generateStart + sourceFile.snapshot.getLength(), - generateEnd + sourceFile.snapshot.getLength(), + generateStart + sourceScript.snapshot.getLength(), + generateEnd + sourceScript.snapshot.getLength(), errorCodes, formatOptions, preferences, @@ -312,14 +312,14 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts fixes = getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences); } fixes = fixes.map(fix => { - fix.changes = fix.changes.map(edit => transformFileTextChanges(files, edit, isCodeActionsEnabled)).filter(notEmpty); + fix.changes = fix.changes.map(edit => transformFileTextChanges(language, edit, isCodeActionsEnabled)).filter(notEmpty); return fix; }); return fixes; }; languageService.getEncodedSemanticClassifications = (fileName, span, format) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { let start: number | undefined; let end: number | undefined; for (const mapping of map.mappings) { @@ -334,14 +334,14 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts start = 0; end = 0; } - start += sourceFile.snapshot.getLength(); - end += sourceFile.snapshot.getLength(); + start += sourceScript.snapshot.getLength(); + end += sourceScript.snapshot.getLength(); const result = getEncodedSemanticClassifications(fileName, { start, length: end - start }, format); const spans: number[] = []; for (let i = 0; i < result.spans.length; i += 3) { - for (const [sourceStart, mapping] of map.getSourceOffsets(result.spans[i] - sourceFile.snapshot.getLength())) { + for (const [sourceStart, mapping] of map.getSourceOffsets(result.spans[i] - sourceScript.snapshot.getLength())) { if (isSemanticTokensEnabled(mapping.data)) { - for (const [sourceEnd, mapping] of map.getSourceOffsets(result.spans[i] + result.spans[i + 1] - sourceFile.snapshot.getLength())) { + for (const [sourceEnd, mapping] of map.getSourceOffsets(result.spans[i] + result.spans[i + 1] - sourceScript.snapshot.getLength())) { if (isSemanticTokensEnabled(mapping.data)) { spans.push( sourceStart, @@ -364,17 +364,17 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }; languageService.getSyntacticDiagnostics = fileName => { return getSyntacticDiagnostics(fileName) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; languageService.getSemanticDiagnostics = fileName => { return getSemanticDiagnostics(fileName) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; languageService.getSuggestionDiagnostics = fileName => { return getSuggestionDiagnostics(fileName) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; languageService.getDefinitionAndBoundSpan = (fileName, position) => { @@ -390,14 +390,14 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts }, ); const textSpan = unresolved - .map(s => transformSpan(files, fileName, s.textSpan, isDefinitionEnabled)?.textSpan) + .map(s => transformSpan(language, fileName, s.textSpan, isDefinitionEnabled)?.textSpan) .filter(notEmpty)[0]; if (!textSpan) { return; } const definitions = unresolved .map(s => s.definitions - ?.map(s => transformDocumentSpan(files, s, isDefinitionEnabled, s.fileName !== fileName)) + ?.map(s => transformDocumentSpan(language, s, isDefinitionEnabled, s.fileName !== fileName)) .filter(notEmpty) ) .filter(notEmpty) @@ -424,12 +424,12 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts const resolved = unresolved .flat() .map(symbol => { - const definition = transformDocumentSpan(files, symbol.definition, isDefinitionEnabled); + const definition = transformDocumentSpan(language, symbol.definition, isDefinitionEnabled); if (definition) { return { definition, references: symbol.references - .map(r => transformDocumentSpan(files, r, isReferencesEnabled)) + .map(r => transformDocumentSpan(language, r, isReferencesEnabled)) .filter(notEmpty), }; } @@ -451,7 +451,7 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ); const resolved = unresolved .flat() - .map(s => transformDocumentSpan(files, s, isDefinitionEnabled, s.fileName !== fileName)) + .map(s => transformDocumentSpan(language, s, isDefinitionEnabled, s.fileName !== fileName)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; @@ -469,7 +469,7 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ); const resolved = unresolved .flat() - .map(s => transformDocumentSpan(files, s, isTypeDefinitionEnabled)) + .map(s => transformDocumentSpan(language, s, isTypeDefinitionEnabled)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; @@ -487,7 +487,7 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ); const resolved = unresolved .flat() - .map(s => transformDocumentSpan(files, s, isImplementationEnabled)) + .map(s => transformDocumentSpan(language, s, isImplementationEnabled)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; @@ -505,7 +505,7 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ); const resolved = unresolved .flat() - .map(s => transformDocumentSpan(files, s, isRenameEnabled)) + .map(s => transformDocumentSpan(language, s, isRenameEnabled)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; @@ -523,13 +523,13 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts ); const resolved = unresolved .flat() - .map(s => transformDocumentSpan(files, s, isReferencesEnabled)) + .map(s => transformDocumentSpan(language, s, isReferencesEnabled)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; languageService.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { let mainResult: ts.CompletionInfo | undefined; let additionalResults: ts.CompletionInfo[] = []; for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { @@ -538,12 +538,12 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts if (!isAdditional && mainResult) { continue; } - const result = getCompletionsAtPosition(fileName, generateOffset + sourceFile.snapshot.getLength(), options, formattingSettings); + const result = getCompletionsAtPosition(fileName, generateOffset + sourceScript.snapshot.getLength(), options, formattingSettings); if (result) { for (const entry of result.entries) { - entry.replacementSpan = transformSpan(files, fileName, entry.replacementSpan, isCompletionEnabled)?.textSpan; + entry.replacementSpan = transformSpan(language, fileName, entry.replacementSpan, isCompletionEnabled)?.textSpan; } - result.optionalReplacementSpan = transformSpan(files, fileName, result.optionalReplacementSpan, isCompletionEnabled)?.textSpan; + result.optionalReplacementSpan = transformSpan(language, fileName, result.optionalReplacementSpan, isCompletionEnabled)?.textSpan; if (isAdditional) { additionalResults.push(result); } @@ -574,11 +574,11 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts let details: ts.CompletionEntryDetails | undefined; - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (isCompletionEnabled(mapping.data)) { - details = getCompletionEntryDetails(fileName, generateOffset + sourceFile.snapshot.getLength(), entryName, formatOptions, source, preferences, data); + details = getCompletionEntryDetails(fileName, generateOffset + sourceScript.snapshot.getLength(), entryName, formatOptions, source, preferences, data); break; } } @@ -589,15 +589,15 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts if (details?.codeActions) { for (const codeAction of details.codeActions) { - codeAction.changes = codeAction.changes.map(edit => transformFileTextChanges(files, edit, isCompletionEnabled)).filter(notEmpty); + codeAction.changes = codeAction.changes.map(edit => transformFileTextChanges(language, edit, isCompletionEnabled)).filter(notEmpty); } } return details; }; languageService.provideInlayHints = (fileName, span, preferences) => { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { let start: number | undefined; let end: number | undefined; for (const mapping of map.mappings) { @@ -612,12 +612,12 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts start = 0; end = 0; } - start += sourceFile.snapshot.getLength(); - end += sourceFile.snapshot.getLength(); + start += sourceScript.snapshot.getLength(); + end += sourceScript.snapshot.getLength(); const result = provideInlayHints(fileName, { start, length: end - start }, preferences); const hints: ts.InlayHint[] = []; for (const hint of result) { - for (const [sourcePosition, mapping] of map.getSourceOffsets(hint.position - sourceFile.snapshot.getLength())) { + for (const [sourcePosition, mapping] of map.getSourceOffsets(hint.position - sourceScript.snapshot.getLength())) { if (isInlayHintsEnabled(mapping.data)) { hints.push({ ...hint, @@ -636,7 +636,7 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts languageService.getFileReferences = fileName => { const unresolved = getFileReferences(fileName); const resolved = unresolved - .map(s => transformDocumentSpan(files, s, isReferencesEnabled)) + .map(s => transformDocumentSpan(language, s, isReferencesEnabled)) .filter(notEmpty); return dedupeDocumentSpans(resolved); }; @@ -652,11 +652,11 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts let results: T[] = []; const processedFilePositions = new Set(); - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, fileName); - if (virtualCode) { + const [serviceScript, sourceScript, map] = getServiceScript(language, fileName); + if (serviceScript) { for (const [generateOffset, mapping] of map.getGeneratedOffsets(position)) { if (filter(mapping.data)) { - process(fileName, generateOffset + sourceFile.snapshot.getLength()); + process(fileName, generateOffset + sourceScript.snapshot.getLength()); } } } @@ -680,18 +680,18 @@ export function decorateLanguageService(files: FileRegistry, languageService: ts processedFilePositions.add(ref[0] + ':' + ref[1]); - const [virtualFile, sourceFile] = getVirtualFileAndMap(files, ref[0]); - if (!virtualFile) { + const [serviceScript, sourceScript] = getServiceScript(language, ref[0]); + if (!serviceScript) { continue; } - const linkedCodeMap = files.getLinkedCodeMap(virtualFile.code); + const linkedCodeMap = language.linkedCodeMaps.get(serviceScript.code); if (!linkedCodeMap) { continue; } - for (const linkedCodeOffset of linkedCodeMap.getLinkedOffsets(ref[1] - sourceFile.snapshot.getLength())) { - process(ref[0], linkedCodeOffset + sourceFile.snapshot.getLength()); + for (const linkedCodeOffset of linkedCodeMap.getLinkedOffsets(ref[1] - sourceScript.snapshot.getLength())) { + process(ref[0], linkedCodeOffset + sourceScript.snapshot.getLength()); } } } diff --git a/packages/typescript/lib/node/decorateLanguageServiceHost.ts b/packages/typescript/lib/node/decorateLanguageServiceHost.ts index a131ec5b..88e15930 100644 --- a/packages/typescript/lib/node/decorateLanguageServiceHost.ts +++ b/packages/typescript/lib/node/decorateLanguageServiceHost.ts @@ -1,17 +1,16 @@ -import { resolveCommonLanguageId, type FileRegistry } from '@volar/language-core'; +import { resolveCommonLanguageId, type Language } from '@volar/language-core'; import type * as ts from 'typescript'; import { createResolveModuleName } from '../resolveModuleName'; export function decorateLanguageServiceHost( - virtualFiles: FileRegistry, + language: Language, languageServiceHost: ts.LanguageServiceHost, ts: typeof import('typescript'), ) { let extraProjectVersion = 0; - const { languagePlugins } = virtualFiles; - const exts = languagePlugins + const exts = language.plugins .map(plugin => plugin.typescript?.extraFileExtensions.map(ext => '.' + ext.extension) ?? []) .flat(); const scripts = new Map language.typescript?.extraFileExtensions.length)) { + if (language.plugins.some(language => language.typescript?.extraFileExtensions.length)) { - const resolveModuleName = createResolveModuleName(ts, languageServiceHost, languagePlugins, fileName => virtualFiles.get(fileName)); + const resolveModuleName = createResolveModuleName(ts, languageServiceHost, language.plugins, fileName => language.scripts.get(fileName)); if (resolveModuleNameLiterals) { languageServiceHost.resolveModuleNameLiterals = ( @@ -123,25 +122,25 @@ export function decorateLanguageServiceHost( if (snapshot) { extraProjectVersion++; - const sourceFile = virtualFiles.set(fileName, resolveCommonLanguageId(fileName), snapshot); - if (sourceFile.generated) { + const sourceScript = language.scripts.set(fileName, resolveCommonLanguageId(fileName), snapshot); + if (sourceScript.generated) { const text = snapshot.getText(0, snapshot.getLength()); let patchedText = text.split('\n').map(line => ' '.repeat(line.length)).join('\n'); - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - extension = script.extension; - scriptKind = script.scriptKind; - patchedText += script.code.snapshot.getText(0, script.code.snapshot.getLength()); + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + extension = serviceScript.extension; + scriptKind = serviceScript.scriptKind; + patchedText += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength()); } snapshotSnapshot = ts.ScriptSnapshot.fromString(patchedText); - if (sourceFile.generated.languagePlugin.typescript?.getExtraScripts) { + if (sourceScript.generated.languagePlugin.typescript?.getExtraServiceScripts) { console.warn('getExtraScripts() is not available in this use case.'); } } } - else if (virtualFiles.get(fileName)) { + else if (language.scripts.get(fileName)) { extraProjectVersion++; - virtualFiles.delete(fileName); + language.scripts.delete(fileName); } scripts.set(fileName, { diff --git a/packages/typescript/lib/node/decorateProgram.ts b/packages/typescript/lib/node/decorateProgram.ts index a80262d8..fdd5354f 100644 --- a/packages/typescript/lib/node/decorateProgram.ts +++ b/packages/typescript/lib/node/decorateProgram.ts @@ -1,9 +1,9 @@ -import type { FileRegistry } from '@volar/language-core'; +import type { Language } from '@volar/language-core'; import type * as ts from 'typescript'; import { notEmpty } from './utils'; import { transformDiagnostic } from './transform'; -export function decorateProgram(files: FileRegistry, program: ts.Program) { +export function decorateProgram(language: Language, program: ts.Program) { const emit = program.emit; @@ -21,29 +21,29 @@ export function decorateProgram(files: FileRegistry, program: ts.Program) { return { ...result, diagnostics: result.diagnostics - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty), }; }; program.getSyntacticDiagnostics = (sourceFile, cancellationToken) => { return getSyntacticDiagnostics(sourceFile, cancellationToken) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; program.getSemanticDiagnostics = (sourceFile, cancellationToken) => { return getSemanticDiagnostics(sourceFile, cancellationToken) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; program.getGlobalDiagnostics = cancellationToken => { return getGlobalDiagnostics(cancellationToken) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; // @ts-ignore program.getBindAndCheckDiagnostics = (sourceFile, cancellationToken) => { return (getBindAndCheckDiagnostics as typeof getSyntacticDiagnostics)(sourceFile, cancellationToken) - .map(d => transformDiagnostic(files, d)) + .map(d => transformDiagnostic(language, d)) .filter(notEmpty); }; } diff --git a/packages/typescript/lib/node/proxyCreateProgram.ts b/packages/typescript/lib/node/proxyCreateProgram.ts index 874f83c2..e2272d64 100644 --- a/packages/typescript/lib/node/proxyCreateProgram.ts +++ b/packages/typescript/lib/node/proxyCreateProgram.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript'; import { decorateProgram } from './decorateProgram'; -import { LanguagePlugin, createFileRegistry, resolveCommonLanguageId } from '@volar/language-core'; +import { LanguagePlugin, createLanguage, resolveCommonLanguageId } from '@volar/language-core'; export function proxyCreateProgram( ts: typeof import('typescript'), @@ -15,7 +15,7 @@ export function proxyCreateProgram( assert(!!options.host, '!!options.host'); const sourceFileToSnapshotMap = new WeakMap(); - const files = createFileRegistry( + const language = createLanguage( getLanguagePlugins(ts, options), ts.sys.useCaseSensitiveFileNames, fileName => { @@ -40,10 +40,10 @@ export function proxyCreateProgram( } } if (snapshot) { - files.set(fileName, resolveCommonLanguageId(fileName), snapshot); + language.scripts.set(fileName, resolveCommonLanguageId(fileName), snapshot); } else { - files.delete(fileName); + language.scripts.delete(fileName); } } ); @@ -79,16 +79,16 @@ export function proxyCreateProgram( if (originalSourceFile && extensions.some(ext => fileName.endsWith(ext))) { let sourceFile2 = parsedSourceFiles.get(originalSourceFile); if (!sourceFile2) { - const sourceFile = files.get(fileName); - assert(!!sourceFile, '!!sourceFile'); + const sourceScript = language.scripts.get(fileName); + assert(!!sourceScript, '!!sourceScript'); let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n'); let scriptKind = ts.ScriptKind.TS; - if (sourceFile.generated?.languagePlugin.typescript) { - const { getScript, getExtraScripts } = sourceFile.generated.languagePlugin.typescript; - const script = getScript(sourceFile.generated.code); - if (script) { - scriptKind = script.scriptKind; - patchedText += script.code.snapshot.getText(0, script.code.snapshot.getLength()); + if (sourceScript.generated?.languagePlugin.typescript) { + const { getServiceScript: getScript, getExtraServiceScripts: getExtraScripts } = sourceScript.generated.languagePlugin.typescript; + const serviceScript = getScript(sourceScript.generated.root); + if (serviceScript) { + scriptKind = serviceScript.scriptKind; + patchedText += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength()); } if (getExtraScripts) { console.warn('getExtraScripts() is not available in this use case.'); @@ -134,9 +134,10 @@ export function proxyCreateProgram( const program = Reflect.apply(target, thisArg, [options]) as ts.Program; - decorateProgram(files, program); + decorateProgram(language, program); - (program as any).__volar__ = { files }; + // TODO: #128 + (program as any).__volar__ = { files: language }; return program; diff --git a/packages/typescript/lib/node/transform.ts b/packages/typescript/lib/node/transform.ts index 94d42efe..0c7d8f7e 100644 --- a/packages/typescript/lib/node/transform.ts +++ b/packages/typescript/lib/node/transform.ts @@ -1,10 +1,10 @@ -import { FileRegistry, CodeInformation, shouldReportDiagnostics, SourceMap, SourceFile } from '@volar/language-core'; +import { Language, CodeInformation, shouldReportDiagnostics, SourceMap, SourceScript } from '@volar/language-core'; import type * as ts from 'typescript'; -import { getVirtualFileAndMap, notEmpty } from './utils'; +import { getServiceScript, notEmpty } from './utils'; const transformedDiagnostics = new WeakMap(); -export function transformCallHierarchyItem(files: FileRegistry, item: ts.CallHierarchyItem, filter: (data: CodeInformation) => boolean): ts.CallHierarchyItem { +export function transformCallHierarchyItem(files: Language, item: ts.CallHierarchyItem, filter: (data: CodeInformation) => boolean): ts.CallHierarchyItem { const span = transformSpan(files, item.file, item.span, filter); const selectionSpan = transformSpan(files, item.file, item.selectionSpan, filter); return { @@ -14,7 +14,7 @@ export function transformCallHierarchyItem(files: FileRegistry, item: ts.CallHie }; } -export function transformDiagnostic(files: FileRegistry, diagnostic: T): T | undefined { +export function transformDiagnostic(files: Language, diagnostic: T): T | undefined { if (!transformedDiagnostics.has(diagnostic)) { transformedDiagnostics.set(diagnostic, undefined); @@ -31,9 +31,9 @@ export function transformDiagnostic(files: FileRegistry && diagnostic.start !== undefined && diagnostic.length !== undefined ) { - const [virtualCode, sourceFile, map] = getVirtualFileAndMap(files, diagnostic.file.fileName); + const [virtualCode, sourceScript, map] = getServiceScript(files, diagnostic.file.fileName); if (virtualCode) { - const sourceRange = transformRange(sourceFile, map, diagnostic.start, diagnostic.start + diagnostic.length, shouldReportDiagnostics); + const sourceRange = transformRange(sourceScript, map, diagnostic.start, diagnostic.start + diagnostic.length, shouldReportDiagnostics); if (sourceRange) { transformedDiagnostics.set(diagnostic, { ...diagnostic, @@ -53,8 +53,8 @@ export function transformDiagnostic(files: FileRegistry return transformedDiagnostics.get(diagnostic) as T | undefined; } -export function transformFileTextChanges(files: FileRegistry, changes: ts.FileTextChanges, filter: (data: CodeInformation) => boolean): ts.FileTextChanges | undefined { - const [_, source] = getVirtualFileAndMap(files, changes.fileName); +export function transformFileTextChanges(files: Language, changes: ts.FileTextChanges, filter: (data: CodeInformation) => boolean): ts.FileTextChanges | undefined { + const [_, source] = getServiceScript(files, changes.fileName); if (source) { return { ...changes, @@ -74,10 +74,10 @@ export function transformFileTextChanges(files: FileRegistry, changes: ts.FileTe } } -export function transformDocumentSpan(files: FileRegistry, documentSpan: T, filter: (data: CodeInformation) => boolean, shouldFallback?: boolean): T | undefined { +export function transformDocumentSpan(files: Language, documentSpan: T, filter: (data: CodeInformation) => boolean, shouldFallback?: boolean): T | undefined { let textSpan = transformSpan(files, documentSpan.fileName, documentSpan.textSpan, filter); if (!textSpan && shouldFallback) { - const [virtualCode] = getVirtualFileAndMap(files, documentSpan.fileName); + const [virtualCode] = getServiceScript(files, documentSpan.fileName); if (virtualCode) { textSpan = { fileName: documentSpan.fileName, @@ -102,7 +102,7 @@ export function transformDocumentSpan(files: FileRegi }; } -export function transformSpan(files: FileRegistry, fileName: string | undefined, textSpan: ts.TextSpan | undefined, filter: (data: CodeInformation) => boolean): { +export function transformSpan(files: Language, fileName: string | undefined, textSpan: ts.TextSpan | undefined, filter: (data: CodeInformation) => boolean): { fileName: string; textSpan: ts.TextSpan; } | undefined { @@ -112,9 +112,9 @@ export function transformSpan(files: FileRegistry, fileName: string | undefined, if (!textSpan) { return; } - const [virtualFile, sourceFile, map] = getVirtualFileAndMap(files, fileName); + const [virtualFile, sourceScript, map] = getServiceScript(files, fileName); if (virtualFile) { - const sourceRange = transformRange(sourceFile, map, textSpan.start, textSpan.start + textSpan.length, filter); + const sourceRange = transformRange(sourceScript, map, textSpan.start, textSpan.start + textSpan.length, filter); if (sourceRange) { return { fileName, @@ -134,15 +134,15 @@ export function transformSpan(files: FileRegistry, fileName: string | undefined, } function transformRange( - sourceFile: SourceFile, + sourceScript: SourceScript, map: SourceMap, start: number, end: number, filter: (data: CodeInformation) => boolean, ) { - for (const sourceStart of map.getSourceOffsets(start - sourceFile.snapshot.getLength())) { + for (const sourceStart of map.getSourceOffsets(start - sourceScript.snapshot.getLength())) { if (filter(sourceStart[1].data)) { - for (const sourceEnd of map.getSourceOffsets(end - sourceFile.snapshot.getLength())) { + for (const sourceEnd of map.getSourceOffsets(end - sourceScript.snapshot.getLength())) { if (sourceEnd[0] >= sourceStart[0] && filter(sourceEnd[1].data)) { return [sourceStart[0], sourceEnd[0]]; } diff --git a/packages/typescript/lib/node/utils.ts b/packages/typescript/lib/node/utils.ts index 0e9e1c3e..e9e80467 100644 --- a/packages/typescript/lib/node/utils.ts +++ b/packages/typescript/lib/node/utils.ts @@ -1,18 +1,17 @@ -import type { FileRegistry } from '@volar/language-core'; +import type { Language } from '@volar/language-core'; export function notEmpty(value: T | null | undefined): value is T { return value !== null && value !== undefined; } -export function getVirtualFileAndMap(files: FileRegistry, fileName: string) { - const sourceFile = files.get(fileName); - if (sourceFile?.generated) { - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - for (const map of files.getMaps(script.code)) { - if (map[1][0] === sourceFile.snapshot) { - return [script, sourceFile, map[1][1]] as const; - } +export function getServiceScript(language: Language, fileName: string) { + const sourceScript = language.scripts.get(fileName); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + const map = language.maps.get(serviceScript.code, sourceScript.id); + if (map) { + return [serviceScript, sourceScript, map] as const; } } } diff --git a/packages/typescript/lib/protocol/createProject.ts b/packages/typescript/lib/protocol/createProject.ts index d4dc1c01..8f6bc95b 100644 --- a/packages/typescript/lib/protocol/createProject.ts +++ b/packages/typescript/lib/protocol/createProject.ts @@ -1,56 +1,51 @@ -import { createFileRegistry, FileMap, LanguagePlugin, LanguageContext, TypeScriptProjectHost, ExtraServiceScript } from '@volar/language-core'; +import { createLanguage, FileMap, LanguagePlugin, Language, TypeScriptProjectHost, ExtraServiceScript } from '@volar/language-core'; import type * as ts from 'typescript'; import { forEachEmbeddedCode } from '@volar/language-core'; import * as path from 'path-browserify'; -import type { createSys } from './createSys'; import { createResolveModuleName } from '../resolveModuleName'; const scriptVersions = new Map; }>(); const fsFileSnapshots = new Map(); -export function createLanguage( +export function createTypeScriptLanguage( ts: typeof import('typescript'), - sys: ReturnType | ts.System, - languagePlugins: LanguagePlugin[], - configFileName: string | undefined, + languagePlugins: LanguagePlugin[], projectHost: TypeScriptProjectHost, - { fileIdToFileName, fileNameToFileId }: { - fileIdToFileName: (uri: string) => string, - fileNameToFileId: (fileName: string) => string, - }, -): LanguageContext { - - const files = createFileRegistry(languagePlugins, sys.useCaseSensitiveFileNames, fileId => { - - const fileName = fileIdToFileName(fileId); - - // opened files - let snapshot = projectHost.getScriptSnapshot(fileName); - - if (!snapshot) { - // fs files - const cache = fsFileSnapshots.get(fileName); - const modifiedTime = sys.getModifiedTime?.(fileName)?.valueOf(); - if (!cache || cache[0] !== modifiedTime) { - if (sys.fileExists(fileName)) { - const text = sys.readFile(fileName); - const snapshot = text !== undefined ? ts.ScriptSnapshot.fromString(text) : undefined; - fsFileSnapshots.set(fileName, [modifiedTime, snapshot]); - } - else { - fsFileSnapshots.set(fileName, [modifiedTime, undefined]); +): Language { + + const language = createLanguage( + languagePlugins, + projectHost.useCaseSensitiveFileNames, + scriptId => { + const fileName = projectHost.scriptIdToFileName(scriptId); + + // opened files + let snapshot = projectHost.getScriptSnapshot(fileName); + if (!snapshot) { + // fs files + const cache = fsFileSnapshots.get(fileName); + const modifiedTime = projectHost.getModifiedTime?.(fileName)?.valueOf(); + if (!cache || cache[0] !== modifiedTime) { + if (projectHost.fileExists(fileName)) { + const text = projectHost.readFile(fileName); + const snapshot = text !== undefined ? ts.ScriptSnapshot.fromString(text) : undefined; + fsFileSnapshots.set(fileName, [modifiedTime, snapshot]); + } + else { + fsFileSnapshots.set(fileName, [modifiedTime, undefined]); + } } + snapshot = fsFileSnapshots.get(fileName)?.[1]; } - snapshot = fsFileSnapshots.get(fileName)?.[1]; - } - if (snapshot) { - files.set(fileId, projectHost.getLanguageId(fileId), snapshot); - } - else { - files.delete(fileId); - } - }); + if (snapshot) { + language.scripts.set(scriptId, projectHost.getLanguageId(scriptId), snapshot); + } + else { + language.scripts.delete(scriptId); + } + }, + ); let { languageServiceHost, getExtraScript } = createLanguageServiceHost(); @@ -68,9 +63,9 @@ export function createLanguage( languageServiceHost.useCaseSensitiveFileNames ? s => s : s => s.toLowerCase(), languageServiceHost.getCompilationSettings() ); - const resolveModuleName = createResolveModuleName(ts, languageServiceHost, languagePlugins, fileName => files.get(fileNameToFileId(fileName))); + const resolveModuleName = createResolveModuleName(ts, languageServiceHost, languagePlugins, fileName => language.scripts.get(projectHost.fileNameToScriptId(fileName))); - let lastSysVersion = 'version' in sys ? sys.version : undefined; + let lastSysVersion = projectHost.getSystemVersion?.(); languageServiceHost.resolveModuleNameLiterals = ( moduleLiterals, @@ -79,8 +74,8 @@ export function createLanguage( options, sourceFile ) => { - if ('version' in sys && lastSysVersion !== sys.version) { - lastSysVersion = sys.version; + if (projectHost.getSystemVersion && lastSysVersion !== projectHost.getSystemVersion()) { + lastSysVersion = projectHost.getSystemVersion(); moduleCache.clear(); } return moduleLiterals.map(moduleLiteral => { @@ -94,8 +89,8 @@ export function createLanguage( redirectedReference, options, ) => { - if ('version' in sys && lastSysVersion !== sys.version) { - lastSysVersion = sys.version; + if (projectHost.getSystemVersion && lastSysVersion !== projectHost.getSystemVersion()) { + lastSysVersion = projectHost.getSystemVersion(); moduleCache.clear(); } return moduleNames.map(moduleName => { @@ -104,28 +99,25 @@ export function createLanguage( }; } - return { - files, - typescript: { - configFileName, - sys, - projectHost, - languageServiceHost, - getExtraScript, - }, + language.typescript = { + projectHost, + languageServiceHost, + getExtraServiceScript: getExtraScript, }; + return language; + function createLanguageServiceHost() { let lastProjectVersion: number | string | undefined; let tsProjectVersion = 0; - let tsFileRegistry = new FileMap(sys.useCaseSensitiveFileNames); - let extraScriptRegistry = new FileMap(sys.useCaseSensitiveFileNames); + let tsFileRegistry = new FileMap(projectHost.useCaseSensitiveFileNames); + let extraScriptRegistry = new FileMap(projectHost.useCaseSensitiveFileNames); let lastTsVirtualFileSnapshots = new Set(); let lastOtherVirtualFileSnapshots = new Set(); const languageServiceHost: ts.LanguageServiceHost = { - ...sys, + ...projectHost, getCurrentDirectory: projectHost.getCurrentDirectory, getCompilationSettings() { const options = projectHost.getCompilationSettings(); @@ -148,16 +140,16 @@ export function createLanguage( } }, useCaseSensitiveFileNames() { - return sys.useCaseSensitiveFileNames; + return projectHost.useCaseSensitiveFileNames; }, getNewLine() { - return sys.newLine; + return projectHost.newLine; }, getTypeRootsVersion: () => { - return 'version' in sys ? sys.version : -1; // TODO: only update for /node_modules changes? + return projectHost.getSystemVersion?.() ?? -1; // TODO: only update for /node_modules changes? }, getDirectories(dirName) { - return sys.getDirectories(dirName); + return projectHost.getDirectories(dirName); }, readDirectory(dirName, extensions, excludes, includes, depth) { const exts = new Set(extensions); @@ -167,7 +159,7 @@ export function createLanguage( } } extensions = [...exts]; - return sys.readDirectory(dirName, extensions, excludes, includes, depth); + return projectHost.readDirectory(dirName, extensions, excludes, includes, depth); }, readFile(fileName) { const snapshot = getScriptSnapshot(fileName); @@ -180,7 +172,7 @@ export function createLanguage( }, getProjectVersion() { sync(); - return tsProjectVersion + ('version' in sys ? `:${sys.version}` : ''); + return tsProjectVersion + (projectHost.getSystemVersion ? `:${projectHost.getSystemVersion()}` : ''); }, getScriptFileNames() { sync(); @@ -194,11 +186,11 @@ export function createLanguage( return extraScriptRegistry.get(fileName)!.scriptKind; } - const sourceFile = files.get(fileNameToFileId(fileName)); - if (sourceFile?.generated) { - const tsCode = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (tsCode) { - return tsCode.scriptKind; + const sourceScript = language.scripts.get(projectHost.fileNameToScriptId(fileName)); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + return serviceScript.scriptKind; } } switch (path.extname(fileName)) { @@ -250,19 +242,19 @@ export function createLanguage( const tsFileNamesSet = new Set(); for (const fileName of projectHost.getScriptFileNames()) { - const sourceFile = files.get(fileNameToFileId(fileName)); - if (sourceFile?.generated) { - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - newTsVirtualFileSnapshots.add(script.code.snapshot); + const sourceScript = language.scripts.get(projectHost.fileNameToScriptId(fileName)); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + newTsVirtualFileSnapshots.add(serviceScript.code.snapshot); tsFileNamesSet.add(fileName); } - for (const extraScript of sourceFile.generated.languagePlugin.typescript?.getExtraScripts?.(fileName, sourceFile.generated.code) ?? []) { - newTsVirtualFileSnapshots.add(extraScript.code.snapshot); - tsFileNamesSet.add(extraScript.fileName); - extraScriptRegistry.set(extraScript.fileName, extraScript); + for (const extraServiceScript of sourceScript.generated.languagePlugin.typescript?.getExtraServiceScripts?.(fileName, sourceScript.generated.root) ?? []) { + newTsVirtualFileSnapshots.add(extraServiceScript.code.snapshot); + tsFileNamesSet.add(extraServiceScript.fileName); + extraScriptRegistry.set(extraServiceScript.fileName, extraServiceScript); } - for (const code of forEachEmbeddedCode(sourceFile.generated.code)) { + for (const code of forEachEmbeddedCode(sourceScript.generated.root)) { newOtherVirtualFileSnapshots.add(code.snapshot); } } @@ -296,16 +288,16 @@ export function createLanguage( return extraScriptRegistry.get(fileName)!.code.snapshot; } - const sourceFile = files.get(fileNameToFileId(fileName)); + const sourceScript = language.scripts.get(projectHost.fileNameToScriptId(fileName)); - if (sourceFile?.generated) { - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - return script.code.snapshot; + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + return serviceScript.code.snapshot; } } - else if (sourceFile) { - return sourceFile.snapshot; + else if (sourceScript) { + return sourceScript.snapshot; } } @@ -327,32 +319,32 @@ export function createLanguage( return version.map.get(snapshot)!.toString(); } - const sourceFile = files.get(fileNameToFileId(fileName)); + const sourceScript = language.scripts.get(projectHost.fileNameToScriptId(fileName)); - if (sourceFile?.generated) { - const script = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (script) { - if (!version.map.has(script.code.snapshot)) { - version.map.set(script.code.snapshot, version.lastVersion++); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + if (!version.map.has(serviceScript.code.snapshot)) { + version.map.set(serviceScript.code.snapshot, version.lastVersion++); } - return version.map.get(script.code.snapshot)!.toString(); + return version.map.get(serviceScript.code.snapshot)!.toString(); } } const isOpenedFile = !!projectHost.getScriptSnapshot(fileName); if (isOpenedFile) { - const sourceFile = files.get(fileNameToFileId(fileName)); - if (sourceFile && !sourceFile.generated) { - if (!version.map.has(sourceFile.snapshot)) { - version.map.set(sourceFile.snapshot, version.lastVersion++); + const sourceScript = language.scripts.get(projectHost.fileNameToScriptId(fileName)); + if (sourceScript && !sourceScript.generated) { + if (!version.map.has(sourceScript.snapshot)) { + version.map.set(sourceScript.snapshot, version.lastVersion++); } - return version.map.get(sourceFile.snapshot)!.toString(); + return version.map.get(sourceScript.snapshot)!.toString(); } } - if (sys.fileExists(fileName)) { - return sys.getModifiedTime?.(fileName)?.valueOf().toString() ?? '0'; + if (projectHost.fileExists(fileName)) { + return projectHost.getModifiedTime?.(fileName)?.valueOf().toString() ?? '0'; } return ''; diff --git a/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts b/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts index f0dd5b3a..46e024a3 100644 --- a/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts +++ b/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts @@ -1,7 +1,7 @@ import type * as ts from 'typescript'; import { decorateLanguageService } from '../node/decorateLanguageService'; import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost'; -import { createFileRegistry, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core'; +import { createLanguage, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core'; import { arrayItemsEqual } from './createLanguageServicePlugin'; const externalFiles = new WeakMap(); @@ -65,25 +65,25 @@ export function createAsyncLanguageServicePlugin( } loadLanguagePlugins(ts, info).then(languagePlugins => { - const files = createFileRegistry( + const language = createLanguage( languagePlugins, ts.sys.useCaseSensitiveFileNames, fileName => { const snapshot = getScriptSnapshot(fileName); if (snapshot) { - files.set( + language.scripts.set( fileName, resolveCommonLanguageId(fileName), snapshot ); } else { - files.delete(fileName); + language.scripts.delete(fileName); } } ); - decorateLanguageService(files, info.languageService); - decorateLanguageServiceHost(files, info.languageServiceHost, ts); + decorateLanguageService(language, info.languageService); + decorateLanguageServiceHost(language, info.languageServiceHost, ts); info.project.markAsDirty(); initialized = true; diff --git a/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts b/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts index 6ea514c4..a1956bb3 100644 --- a/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts +++ b/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts @@ -1,7 +1,7 @@ import type * as ts from 'typescript'; import { decorateLanguageService } from '../node/decorateLanguageService'; import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost'; -import { createFileRegistry, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core'; +import { createLanguage, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core'; const externalFiles = new WeakMap(); const projectExternalFileExtensions = new WeakMap(); @@ -31,22 +31,22 @@ export function createLanguageServicePlugin( .flat(); projectExternalFileExtensions.set(info.project, extensions); const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost); - const files = createFileRegistry( + const language = createLanguage( languagePlugins, ts.sys.useCaseSensitiveFileNames, fileName => { const snapshot = getScriptSnapshot(fileName); if (snapshot) { - files.set(fileName, resolveCommonLanguageId(fileName), snapshot); + language.scripts.set(fileName, resolveCommonLanguageId(fileName), snapshot); } else { - files.delete(fileName); + language.scripts.delete(fileName); } } ); - decorateLanguageService(files, info.languageService); - decorateLanguageServiceHost(files, info.languageServiceHost, ts); + decorateLanguageService(language, info.languageService); + decorateLanguageServiceHost(language, info.languageServiceHost, ts); } return info.languageService; diff --git a/packages/typescript/lib/resolveModuleName.ts b/packages/typescript/lib/resolveModuleName.ts index b5a049a5..5900c32d 100644 --- a/packages/typescript/lib/resolveModuleName.ts +++ b/packages/typescript/lib/resolveModuleName.ts @@ -1,11 +1,11 @@ -import type { LanguagePlugin, SourceFile } from '@volar/language-core'; +import type { LanguagePlugin, SourceScript } from '@volar/language-core'; import type * as ts from 'typescript'; export function createResolveModuleName( ts: typeof import('typescript'), languageServiceHost: ts.LanguageServiceHost, languagePlugins: LanguagePlugin[], - getFile: (fileName: string) => SourceFile | undefined, + getSourceScript: (fileName: string) => SourceScript | undefined, ) { const toPatchResults = new Map(); const moduleResolutionHost: ts.ModuleResolutionHost = { @@ -52,11 +52,11 @@ export function createResolveModuleName( ); if (result.resolvedModule && toPatchResults.has(result.resolvedModule.resolvedFileName)) { result.resolvedModule.resolvedFileName = toPatchResults.get(result.resolvedModule.resolvedFileName)!; - const sourceFile = getFile(result.resolvedModule.resolvedFileName); - if (sourceFile?.generated) { - const tsCode = sourceFile.generated.languagePlugin.typescript?.getScript(sourceFile.generated.code); - if (tsCode) { - result.resolvedModule.extension = tsCode.extension; + const sourceScript = getSourceScript(result.resolvedModule.resolvedFileName); + if (sourceScript?.generated) { + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (serviceScript) { + result.resolvedModule.extension = serviceScript.extension; } } }