diff --git a/packages/eslint/index.ts b/packages/eslint/index.ts index a08d56ad..dcdbb632 100644 --- a/packages/eslint/index.ts +++ b/packages/eslint/index.ts @@ -31,7 +31,7 @@ export function createProcessor( const documents = new FileMap<{ sourceDocument: TextDocument; embeddedDocuments: TextDocument[]; - codes: VirtualCode[]; + codes: VirtualCode[]; }>(caseSensitive); return { supportsAutofix, diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index 91ba8f36..58e4661a 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -7,15 +7,15 @@ export * from './lib/utils'; 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 type { CodeInformation, CodegenContext, Language, LanguagePlugin, SourceScript, VirtualCode } from './lib/types'; export function createLanguage( plugins: LanguagePlugin[], scriptRegistry: Map>, sync: (id: T) => void ): Language { - const virtualCodeToSourceScriptMap = new WeakMap>(); - const virtualCodeToSourceMap = new WeakMap]>(); + const virtualCodeToSourceScriptMap = new WeakMap, SourceScript>(); + const virtualCodeToSourceMap = new WeakMap>>(); const virtualCodeToLinkedCodeMap = new WeakMap(); return { @@ -48,14 +48,15 @@ export function createLanguage( this.delete(id); return this.set(id, snapshot, languageId); } - else if (sourceScript.snapshot !== snapshot) { + else if (sourceScript.isAssociationDirty || sourceScript.snapshot !== snapshot) { // snapshot updated sourceScript.snapshot = snapshot; + const codegenCtx = prepareCreateVirtualCode(sourceScript); if (sourceScript.generated) { const { updateVirtualCode, createVirtualCode } = sourceScript.generated.languagePlugin; const newVirtualCode = updateVirtualCode - ? updateVirtualCode(id, sourceScript.generated.root, snapshot) - : createVirtualCode?.(id, languageId, snapshot); + ? updateVirtualCode(id, sourceScript.generated.root, snapshot, codegenCtx) + : createVirtualCode?.(id, languageId, snapshot, codegenCtx); if (newVirtualCode) { sourceScript.generated.root = newVirtualCode; sourceScript.generated.embeddedCodes.clear(); @@ -70,6 +71,7 @@ export function createLanguage( return; } } + triggerTargetsDirty(sourceScript); } else { // not changed @@ -78,10 +80,17 @@ export function createLanguage( } else { // created - const sourceScript: SourceScript = { id, languageId, snapshot }; + const sourceScript: SourceScript = { + id: id, + languageId, + snapshot, + associatedIds: new Set(), + targetIds: new Set(), + }; scriptRegistry.set(id, sourceScript); + for (const languagePlugin of _plugins) { - const virtualCode = languagePlugin.createVirtualCode?.(id, languageId, snapshot); + const virtualCode = languagePlugin.createVirtualCode?.(id, languageId, snapshot, prepareCreateVirtualCode(sourceScript)); if (virtualCode) { sourceScript.generated = { root: virtualCode, @@ -95,36 +104,58 @@ export function createLanguage( break; } } + return sourceScript; } }, delete(id) { - const value = scriptRegistry.get(id); - if (value) { - if (value.generated) { - value.generated.languagePlugin.disposeVirtualCode?.(id, value.generated.root); - } + const sourceScript = scriptRegistry.get(id); + if (sourceScript) { + sourceScript.generated?.languagePlugin.disposeVirtualCode?.(id, sourceScript.generated.root); scriptRegistry.delete(id); + triggerTargetsDirty(sourceScript); } }, }, maps: { get(virtualCode) { - const sourceScript = virtualCodeToSourceScriptMap.get(virtualCode)!; + for (const map of this.forEach(virtualCode)) { + return map[2]; + } + throw `no map found for ${virtualCode.id}`; + }, + *forEach(virtualCode) { let mapCache = virtualCodeToSourceMap.get(virtualCode.snapshot); - if (mapCache?.[0] !== sourceScript.snapshot) { - if (virtualCode.mappings.some(mapping => mapping.source)) { - throw 'not implemented'; - } + if (!mapCache) { virtualCodeToSourceMap.set( virtualCode.snapshot, - mapCache = [ - sourceScript.snapshot, - new SourceMap(virtualCode.mappings), - ] + mapCache = new WeakMap() ); } - return mapCache[1]; + + const sourceScript = virtualCodeToSourceScriptMap.get(virtualCode)!; + if (!mapCache.has(sourceScript.snapshot)) { + mapCache.set( + sourceScript.snapshot, + new SourceMap(virtualCode.mappings) + ); + } + yield [sourceScript.id, sourceScript.snapshot, mapCache.get(sourceScript.snapshot)!]; + + if (virtualCode.associatedScriptMappings) { + for (const [relatedScriptId, relatedMappings] of virtualCode.associatedScriptMappings) { + const relatedSourceScript = scriptRegistry.get(relatedScriptId); + if (relatedSourceScript) { + if (!mapCache.has(relatedSourceScript.snapshot)) { + mapCache.set( + relatedSourceScript.snapshot, + new SourceMap(relatedMappings) + ); + } + yield [relatedSourceScript.id, relatedSourceScript.snapshot, mapCache.get(relatedSourceScript.snapshot)!]; + } + } + } }, }, linkedCodeMaps: { @@ -146,9 +177,37 @@ export function createLanguage( }, }, }; + + function triggerTargetsDirty(sourceScript: SourceScript) { + sourceScript.targetIds.forEach(id => { + const sourceScript = scriptRegistry.get(id); + if (sourceScript) { + sourceScript.isAssociationDirty = true; + } + }); + } + + function prepareCreateVirtualCode(sourceScript: SourceScript): CodegenContext { + for (const id of sourceScript.associatedIds) { + scriptRegistry.get(id)?.targetIds.delete(sourceScript.id); + } + sourceScript.associatedIds.clear(); + sourceScript.isAssociationDirty = false; + return { + getAssociatedScript(id) { + sync(id); + const relatedSourceScript = scriptRegistry.get(id); + if (relatedSourceScript) { + relatedSourceScript.targetIds.add(sourceScript.id); + sourceScript.associatedIds.add(relatedSourceScript.id); + } + return relatedSourceScript; + }, + }; + } } -export function* forEachEmbeddedCode(virtualCode: VirtualCode): Generator { +export function* forEachEmbeddedCode(virtualCode: VirtualCode): Generator> { yield virtualCode; if (virtualCode.embeddedCodes) { for (const embeddedCode of virtualCode.embeddedCodes) { diff --git a/packages/language-core/lib/linkedCodeMap.ts b/packages/language-core/lib/linkedCodeMap.ts index b0e85d6b..eac2bf49 100644 --- a/packages/language-core/lib/linkedCodeMap.ts +++ b/packages/language-core/lib/linkedCodeMap.ts @@ -1,6 +1,6 @@ import { SourceMap } from '@volar/source-map'; -export class LinkedCodeMap extends SourceMap { +export class LinkedCodeMap extends SourceMap { *getLinkedOffsets(start: number) { for (const mapped of this.getGeneratedOffsets(start)) { yield mapped[0]; diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index c849755d..4fb2157d 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -2,19 +2,20 @@ import type { Mapping, SourceMap } from '@volar/source-map'; import type * as ts from 'typescript'; import type { LinkedCodeMap } from './linkedCodeMap'; -export interface Language { +export interface Language { plugins: LanguagePlugin[]; scripts: { get(id: T): SourceScript | undefined; set(id: T, snapshot: ts.IScriptSnapshot, languageId?: string, plugins?: LanguagePlugin[]): SourceScript | undefined; delete(id: T): void; - fromVirtualCode(virtualCode: VirtualCode): SourceScript; + fromVirtualCode(virtualCode: VirtualCode): SourceScript; }; maps: { - get(virtualCode: VirtualCode): SourceMap; + get(virtualCode: VirtualCode): SourceMap; + forEach(virtualCode: VirtualCode): Generator<[id: T, snapshot: ts.IScriptSnapshot, map: SourceMap]>; }; linkedCodeMaps: { - get(virtualCode: VirtualCode): LinkedCodeMap | undefined; + get(virtualCode: VirtualCode): LinkedCodeMap | undefined; }; typescript?: { configFileName: string | undefined; @@ -23,31 +24,35 @@ export interface Language { sync?(): Promise; }; languageServiceHost: ts.LanguageServiceHost; - getExtraServiceScript(fileName: string): TypeScriptExtraServiceScript | undefined; + getExtraServiceScript(fileName: string): TypeScriptExtraServiceScript | undefined; asScriptId(fileName: string): T; asFileName(scriptId: T): string; }; } -export interface SourceScript { +export interface SourceScript { id: T; languageId: string; snapshot: ts.IScriptSnapshot; + targetIds: Set; + associatedIds: Set; + isAssociationDirty?: boolean; generated?: { - root: VirtualCode; + root: VirtualCode; languagePlugin: LanguagePlugin; - embeddedCodes: Map; + embeddedCodes: Map>; }; } export type CodeMapping = Mapping; -export interface VirtualCode { +export interface VirtualCode { id: string; languageId: string; snapshot: ts.IScriptSnapshot; mappings: CodeMapping[]; - embeddedCodes?: VirtualCode[]; + associatedScriptMappings?: Map; + embeddedCodes?: VirtualCode[]; linkedCodeMappings?: Mapping[]; } @@ -77,19 +82,19 @@ export interface CodeInformation { format?: boolean; } -export interface TypeScriptServiceScript { - code: VirtualCode; +export interface TypeScriptServiceScript { + code: VirtualCode; extension: '.ts' | '.js' | '.mts' | '.mjs' | '.cjs' | '.cts' | '.d.ts' | string; scriptKind: ts.ScriptKind; /** See #188 */ preventLeadingOffset?: boolean; } -export interface TypeScriptExtraServiceScript extends TypeScriptServiceScript { +export interface TypeScriptExtraServiceScript extends TypeScriptServiceScript { fileName: string; } -export interface LanguagePlugin { +export interface LanguagePlugin = VirtualCode> { /** * For files that are not opened in the IDE, the language ID will not be synchronized to the language server, so a hook is needed to parse the language ID of files that are known extension but not opened in the IDE. */ @@ -97,31 +102,35 @@ export interface LanguagePlugin { /** * Generate a virtual code. */ - createVirtualCode?(scriptId: T, languageId: string, snapshot: ts.IScriptSnapshot): K | undefined; + createVirtualCode?(scriptId: T, languageId: string, snapshot: ts.IScriptSnapshot, ctx: CodegenContext): K | undefined; /** * Incremental update a virtual code. If not provide, call createVirtualCode again. */ - updateVirtualCode?(scriptId: T, virtualCode: K, newSnapshot: ts.IScriptSnapshot): K | undefined; + updateVirtualCode?(scriptId: T, virtualCode: K, newSnapshot: ts.IScriptSnapshot, ctx: CodegenContext): K | undefined; /** * Cleanup a virtual code. */ disposeVirtualCode?(scriptId: T, virtualCode: K): void; - typescript?: TypeScriptGenericOptions & TypeScriptNonTSPluginOptions; + typescript?: TypeScriptGenericOptions & TypeScriptNonTSPluginOptions; +} + +export interface CodegenContext { + getAssociatedScript(scriptId: T): SourceScript | undefined; } /** * The following options available to all situations. */ -interface TypeScriptGenericOptions { +interface TypeScriptGenericOptions { extraFileExtensions: ts.FileExtensionInfo[]; resolveHiddenExtensions?: boolean; - getServiceScript(rootVirtualCode: T): TypeScriptServiceScript | undefined; + getServiceScript(root: K): TypeScriptServiceScript | undefined; } /** * The following options will not be available in TS plugin. */ -interface TypeScriptNonTSPluginOptions { - getExtraServiceScripts?(fileName: string, rootVirtualCode: T): TypeScriptExtraServiceScript[]; +interface TypeScriptNonTSPluginOptions { + getExtraServiceScripts?(fileName: string, rootVirtualCode: K): TypeScriptExtraServiceScript[]; resolveLanguageServiceHost?(host: ts.LanguageServiceHost): ts.LanguageServiceHost; } diff --git a/packages/language-server/lib/register/registerEditorFeatures.ts b/packages/language-server/lib/register/registerEditorFeatures.ts index 76a3521c..8e155e55 100644 --- a/packages/language-server/lib/register/registerEditorFeatures.ts +++ b/packages/language-server/lib/register/registerEditorFeatures.ts @@ -92,8 +92,9 @@ export function registerEditorFeatures(server: LanguageServer) { const virtualCode = sourceScript?.generated?.embeddedCodes.get(params.virtualCodeId); if (virtualCode) { const mappings: Record = {}; - const map = languageService.context.documents.getSourceMap(virtualCode); - mappings[map.sourceDocument.uri] = map.map.mappings; + for (const map of languageService.context.documents.getMaps(virtualCode)) { + mappings[map.sourceDocument.uri] = map.map.mappings; + } return { content: virtualCode.snapshot.getText(0, virtualCode.snapshot.getLength()), mappings, diff --git a/packages/language-server/protocol.ts b/packages/language-server/protocol.ts index 3143f464..8c650cf5 100644 --- a/packages/language-server/protocol.ts +++ b/packages/language-server/protocol.ts @@ -155,7 +155,6 @@ export namespace GetVirtualCodeRequest { }; export type ResponseType = { content: string; - // TODO: Simplify this, no map required mappings: Record; }; export type ErrorType = never; diff --git a/packages/language-service/lib/features/provideCallHierarchyItems.ts b/packages/language-service/lib/features/provideCallHierarchyItems.ts index c3f5087c..eda7b6d6 100644 --- a/packages/language-service/lib/features/provideCallHierarchyItems.ts +++ b/packages/language-service/lib/features/provideCallHierarchyItems.ts @@ -186,19 +186,22 @@ export function register(context: LanguageServiceContext) { return [tsItem, tsRanges]; } - const map = context.documents.getSourceMap(virtualCode); - - let range = map.getSourceRange(tsItem.range); - if (!range) { - // TODO: