From d691e20e0761a5ce4e7ba8fcf99105e35acf8a3f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 18 Aug 2024 23:19:26 +0800 Subject: [PATCH] feat(kit): add support for checking project references files --- packages/kit/lib/createChecker.ts | 213 ++++++++++-------- packages/language-core/index.ts | 8 +- packages/language-core/lib/types.ts | 2 +- .../lib/project/typescriptProjectLs.ts | 9 +- packages/monaco/worker.ts | 42 ++-- .../typescript/lib/node/proxyCreateProgram.ts | 4 +- .../typescript/lib/protocol/createProject.ts | 14 +- 7 files changed, 155 insertions(+), 137 deletions(-) diff --git a/packages/kit/lib/createChecker.ts b/packages/kit/lib/createChecker.ts index 659e17d6..42b6a129 100644 --- a/packages/kit/lib/createChecker.ts +++ b/packages/kit/lib/createChecker.ts @@ -1,36 +1,30 @@ -import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, LanguageServiceEnvironment, createLanguageService, mergeWorkspaceEdits, createLanguage, createUriMap } from '@volar/language-service'; +import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, Language, LanguagePlugin, LanguageServiceEnvironment, LanguageServicePlugin, NotificationHandler, createLanguage, createLanguageService, createUriMap, mergeWorkspaceEdits } from '@volar/language-service'; +import { TypeScriptProjectHost, createLanguageServiceHost, resolveFileLanguageId } from '@volar/typescript'; 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, asUri, asFileName } from './utils'; import { URI } from 'vscode-uri'; -import { TypeScriptProjectHost, createLanguageServiceHost, resolveFileLanguageId } from '@volar/typescript'; +import { createServiceEnvironment } from './createServiceEnvironment'; +import { asFileName, asPosix, asUri, defaultCompilerOptions } from './utils'; export function createTypeScriptChecker( languagePlugins: LanguagePlugin[], languageServicePlugins: LanguageServicePlugin[], - tsconfig: string + tsconfig: string, + includeProjectReference = false ) { const tsconfigPath = asPosix(tsconfig); - return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, tsconfigPath, env => { - return createTypeScriptProjectHost( - env, - () => { - const parsed = ts.parseJsonSourceFileConfigFileContent( - ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile), - ts.sys, - path.dirname(tsconfigPath), - undefined, - tsconfigPath, - undefined, - languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat() - ); - parsed.fileNames = parsed.fileNames.map(asPosix); - return parsed; - } + return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, tsconfigPath, () => { + return ts.parseJsonSourceFileConfigFileContent( + ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile), + ts.sys, + path.dirname(tsconfigPath), + undefined, + tsconfigPath, + undefined, + languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat() ); - }); + }, includeProjectReference); } export function createTypeScriptInferredChecker( @@ -39,15 +33,13 @@ export function createTypeScriptInferredChecker( getScriptFileNames: () => string[], compilerOptions = defaultCompilerOptions ) { - return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, undefined, env => { - return createTypeScriptProjectHost( - env, - () => ({ - options: compilerOptions, - fileNames: getScriptFileNames().map(asPosix), - }) - ); - }); + return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, undefined, () => { + return { + options: compilerOptions, + fileNames: getScriptFileNames(), + errors: [], + }; + }, false); } const fsFileSnapshots = createUriMap<[number | undefined, ts.IScriptSnapshot | undefined]>(); @@ -56,14 +48,13 @@ function createTypeScriptCheckerWorker( languagePlugins: LanguagePlugin[], languageServicePlugins: LanguageServicePlugin[], configFileName: string | undefined, - getProjectHost: (env: LanguageServiceEnvironment) => TypeScriptProjectHost + getCommandLine: () => ts.ParsedCommandLine, + includeProjectReference: boolean ) { - let settings = {}; - const env = createServiceEnvironment(() => settings); const didChangeWatchedFilesCallbacks = new Set>(); - + const env = createServiceEnvironment(() => settings); env.onDidChangeWatchedFiles = cb => { didChangeWatchedFilesCallbacks.add(cb); return { @@ -72,15 +63,16 @@ function createTypeScriptCheckerWorker( }, }; }; - const language = createLanguage( [ ...languagePlugins, { getLanguageId: uri => resolveFileLanguageId(uri.path) }, ], createUriMap(ts.sys.useCaseSensitiveFileNames), - uri => { - // fs files + (uri, includeFsFiles) => { + if (!includeFsFiles) { + return; + } const cache = fsFileSnapshots.get(uri); const fileName = asFileName(uri); const modifiedTime = ts.sys.getModifiedTime?.(fileName)?.valueOf(); @@ -103,36 +95,40 @@ function createTypeScriptCheckerWorker( } } ); - const projectHost = getProjectHost(env); - const languageService = createLanguageService( - language, - languageServicePlugins, - env, - { - typescript: { - configFileName, - sys: ts.sys, - uriConverter: { - asFileName, - asUri, - }, - ...createLanguageServiceHost( - ts, - ts.sys, - language, - asUri, - projectHost - ), - }, + const [projectHost, languageService] = createTypeScriptCheckerLanguageService(env, language, languageServicePlugins, configFileName, getCommandLine); + const projectReferenceLanguageServices = new Map>(); + + if (includeProjectReference) { + const tsconfigs = new Set(); + const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService'); + const projectReferences = tsLs.getProgram()?.getResolvedProjectReferences(); + if (configFileName) { + tsconfigs.add(asPosix(configFileName)); } - ); + projectReferences?.forEach(visit); + + function visit(ref: ts.ResolvedProjectReference | undefined) { + if (ref && !tsconfigs.has(ref.sourceFile.fileName)) { + tsconfigs.add(ref.sourceFile.fileName); + const projectReferenceLanguageService = createTypeScriptCheckerLanguageService(env, language, languageServicePlugins, ref.sourceFile.fileName, () => ref.commandLine); + projectReferenceLanguageServices.set(ref.sourceFile.fileName, projectReferenceLanguageService); + ref.references?.forEach(visit); + } + } + } return { // apis check, fixErrors, printErrors, - projectHost, + getRootFileNames: () => { + const fileNames = projectHost.getScriptFileNames(); + for (const [projectHost] of projectReferenceLanguageServices.values()) { + fileNames.push(...projectHost.getScriptFileNames()); + } + return [...new Set(fileNames)]; + }, language, // settings @@ -165,12 +161,14 @@ function createTypeScriptCheckerWorker( function check(fileName: string) { fileName = asPosix(fileName); const uri = asUri(fileName); + const languageService = getLanguageServiceForFile(fileName); return languageService.getDiagnostics(uri); } async function fixErrors(fileName: string, diagnostics: Diagnostic[], only: string[] | undefined, writeFile: (fileName: string, newText: string) => Promise) { fileName = asPosix(fileName); const uri = asUri(fileName); + const languageService = getLanguageServiceForFile(fileName); const sourceScript = languageService.context.language.scripts.get(uri); if (sourceScript) { const document = languageService.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); @@ -224,6 +222,7 @@ function createTypeScriptCheckerWorker( function formatErrors(fileName: string, diagnostics: Diagnostic[], rootPath: string) { fileName = asPosix(fileName); const uri = asUri(fileName); + const languageService = getLanguageServiceForFile(fileName); const sourceScript = languageService.context.language.scripts.get(uri)!; const document = languageService.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); const errors: ts.Diagnostic[] = diagnostics.map(diagnostic => ({ @@ -241,23 +240,43 @@ function createTypeScriptCheckerWorker( }); return text; } + + function getLanguageServiceForFile(fileName: string) { + if (!includeProjectReference) { + return languageService; + } + fileName = asPosix(fileName); + for (const [_1, languageService] of projectReferenceLanguageServices.values()) { + const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService'); + if (tsLs.getProgram()?.getSourceFile(fileName)) { + return languageService; + } + } + return languageService; + } } -function createTypeScriptProjectHost( +function createTypeScriptCheckerLanguageService( env: LanguageServiceEnvironment, - createParsedCommandLine: () => Pick + language: Language, + languageServicePlugins: LanguageServicePlugin[], + configFileName: string | undefined, + getCommandLine: () => ts.ParsedCommandLine ) { - let scriptSnapshotsCache: Map = new Map(); - let parsedCommandLine = createParsedCommandLine(); + let commandLine = getCommandLine(); let projectVersion = 0; let shouldCheckRootFiles = false; - const host: TypeScriptProjectHost = { + const resolvedFileNameByCommandLine = new WeakMap(); + const projectHost: TypeScriptProjectHost = { getCurrentDirectory: () => env.workspaceFolders.length ? asFileName(env.workspaceFolders[0]) : process.cwd(), getCompilationSettings: () => { - return parsedCommandLine.options; + return commandLine.options; + }, + getProjectReferences: () => { + return commandLine.projectReferences; }, getProjectVersion: () => { checkRootFilesUpdate(); @@ -265,57 +284,73 @@ function createTypeScriptProjectHost( }, getScriptFileNames: () => { checkRootFilesUpdate(); - return parsedCommandLine.fileNames; - }, - getScriptSnapshot: fileName => { - if (!scriptSnapshotsCache.has(fileName)) { - const fileText = ts.sys.readFile(fileName, 'utf8'); - if (fileText !== undefined) { - scriptSnapshotsCache.set(fileName, ts.ScriptSnapshot.fromString(fileText)); - } - else { - scriptSnapshotsCache.set(fileName, undefined); - } + let fileNames = resolvedFileNameByCommandLine.get(commandLine); + if (!fileNames) { + fileNames = commandLine.fileNames.map(asPosix); + resolvedFileNameByCommandLine.set(commandLine, fileNames); } - return scriptSnapshotsCache.get(fileName); + return fileNames; }, }; + const languageService = createLanguageService( + language, + languageServicePlugins, + env, + { + typescript: { + configFileName, + sys: ts.sys, + uriConverter: { + asFileName, + asUri, + }, + ...createLanguageServiceHost( + ts, + ts.sys, + language, + asUri, + projectHost + ), + }, + } + ); env.onDidChangeWatchedFiles?.(({ changes }) => { + const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService'); + const program = tsLs.getProgram(); for (const change of changes) { const changeUri = URI.parse(change.uri); const fileName = asFileName(changeUri); if (change.type === 2 satisfies typeof FileChangeType.Changed) { - if (scriptSnapshotsCache.has(fileName)) { + if (program?.getSourceFile(fileName)) { projectVersion++; - scriptSnapshotsCache.delete(fileName); } } else if (change.type === 3 satisfies typeof FileChangeType.Deleted) { - if (scriptSnapshotsCache.has(fileName)) { + if (program?.getSourceFile(fileName)) { projectVersion++; - scriptSnapshotsCache.delete(fileName); - parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(name => name !== fileName); + shouldCheckRootFiles = true; + break; } } else if (change.type === 1 satisfies typeof FileChangeType.Created) { shouldCheckRootFiles = true; + break; } } }); - return host; + return [projectHost, languageService] as const; function checkRootFilesUpdate() { - if (!shouldCheckRootFiles) { return; } shouldCheckRootFiles = false; - const newParsedCommandLine = createParsedCommandLine(); - if (!arrayItemsEqual(newParsedCommandLine.fileNames, parsedCommandLine.fileNames)) { - parsedCommandLine.fileNames = newParsedCommandLine.fileNames; + const newCommandLine = getCommandLine(); + if (!arrayItemsEqual(newCommandLine.fileNames, commandLine.fileNames)) { + commandLine.fileNames = newCommandLine.fileNames; projectVersion++; } } diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index e0e43588..048a3468 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -22,7 +22,7 @@ export const defaultMapperFactory: MapperFactory = mappings => new SourceMap(map export function createLanguage( plugins: LanguagePlugin[], scriptRegistry: Map>, - sync: (id: T) => void + sync: (id: T, includeFsFiles: boolean) => void ) { const virtualCodeToSourceScriptMap = new WeakMap>(); const virtualCodeToSourceMap = new WeakMap>(); @@ -34,8 +34,8 @@ export function createLanguage( fromVirtualCode(virtualCode) { return virtualCodeToSourceScriptMap.get(virtualCode)!; }, - get(id) { - sync(id); + get(id, includeFsFiles = true) { + sync(id, includeFsFiles); const result = scriptRegistry.get(id); // The sync function provider may not always call the set function due to caching, so it is necessary to explicitly check isAssociationDirty. if (result?.isAssociationDirty) { @@ -220,7 +220,7 @@ export function createLanguage( sourceScript.isAssociationDirty = false; return { getAssociatedScript(id) { - sync(id); + sync(id, true); const relatedSourceScript = scriptRegistry.get(id); if (relatedSourceScript) { relatedSourceScript.targetIds.add(sourceScript.id); diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index f07a92b7..e2ae5afb 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -15,7 +15,7 @@ export interface Language { mapperFactory: MapperFactory; plugins: LanguagePlugin[]; scripts: { - get(id: T): SourceScript | undefined; + get(id: T, includeFsFiles?: boolean): SourceScript | undefined; set(id: T, snapshot: IScriptSnapshot, languageId?: string, plugins?: LanguagePlugin[]): SourceScript | undefined; delete(id: T): void; fromVirtualCode(virtualCode: VirtualCode): SourceScript; diff --git a/packages/language-server/lib/project/typescriptProjectLs.ts b/packages/language-server/lib/project/typescriptProjectLs.ts index 1981ef56..6e83f5a0 100644 --- a/packages/language-server/lib/project/typescriptProjectLs.ts +++ b/packages/language-server/lib/project/typescriptProjectLs.ts @@ -59,10 +59,6 @@ export async function createTypeScriptLS( getScriptFileNames() { return rootFiles; }, - getScriptSnapshot(fileName) { - const uri = uriConverter.asUri(fileName); - return server.documents.get(uri)?.getSnapshot(); - }, getCompilationSettings() { return parsedCommandLine.options; }, @@ -92,7 +88,7 @@ export async function createTypeScriptLS( { getLanguageId: uri => resolveFileLanguageId(uri.path) }, ], createUriMap(sys.useCaseSensitiveFileNames), - uri => { + (uri, includeFsFiles) => { const syncedDocument = server.documents.get(uri); let snapshot: ts.IScriptSnapshot | undefined; @@ -100,8 +96,7 @@ export async function createTypeScriptLS( if (syncedDocument) { snapshot = syncedDocument.getSnapshot(); } - else { - // fs files + else if (includeFsFiles) { const cache = fsFileSnapshots.get(uri); const fileName = uriConverter.asFileName(uri); const modifiedTime = sys.getModifiedTime?.(fileName)?.valueOf(); diff --git a/packages/monaco/worker.ts b/packages/monaco/worker.ts index 922cf245..360ff791 100644 --- a/packages/monaco/worker.ts +++ b/packages/monaco/worker.ts @@ -121,11 +121,24 @@ export function createTypeScriptWorkerLanguageService({ { getLanguageId: uri => resolveFileLanguageId(uri.path) }, ], createUriMap(sys.useCaseSensitiveFileNames), - uri => { - let snapshot = getModelSnapshot(uri); + (uri, includeFsFiles) => { + let snapshot: ts.IScriptSnapshot | undefined; - if (!snapshot) { - // fs files + const model = workerContext.getMirrorModels().find(model => model.uri.toString() === uri.toString()); + if (model) { + const cache = modelSnapshot.get(model); + if (cache && cache[0] === model.version) { + return cache[1]; + } + const text = model.getValue(); + modelSnapshot.set(model, [model.version, { + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: () => undefined, + }]); + snapshot = modelSnapshot.get(model)?.[1]; + } + else if (includeFsFiles) { const cache = fsFileSnapshots.get(uri); const fileName = uriConverter.asFileName(uri); const modifiedTime = sys.getModifiedTime?.(fileName)?.valueOf(); @@ -181,10 +194,6 @@ export function createTypeScriptWorkerLanguageService({ projectVersion++; return projectVersion.toString(); }, - getScriptSnapshot(fileName) { - const uri = uriConverter.asUri(fileName); - return getModelSnapshot(uri); - }, getCompilationSettings() { return compilerOptions; }, @@ -202,23 +211,6 @@ export function createTypeScriptWorkerLanguageService({ project ) ); - - function getModelSnapshot(uri: URI) { - const model = workerContext.getMirrorModels().find(model => model.uri.toString() === uri.toString()); - if (model) { - const cache = modelSnapshot.get(model); - if (cache && cache[0] === model.version) { - return cache[1]; - } - const text = model.getValue(); - modelSnapshot.set(model, [model.version, { - getText: (start, end) => text.substring(start, end), - getLength: () => text.length, - getChangeRange: () => undefined, - }]); - return modelSnapshot.get(model)?.[1]; - } - } } export interface UriComponents { diff --git a/packages/typescript/lib/node/proxyCreateProgram.ts b/packages/typescript/lib/node/proxyCreateProgram.ts index bc445fb5..22c16194 100644 --- a/packages/typescript/lib/node/proxyCreateProgram.ts +++ b/packages/typescript/lib/node/proxyCreateProgram.ts @@ -73,8 +73,8 @@ export function proxyCreateProgram( { getLanguageId: resolveFileLanguageId }, ], new FileMap(ts.sys.useCaseSensitiveFileNames), - fileName => { - if (!sourceFileSnapshots.has(fileName)) { + (fileName, includeFsFiles) => { + if (includeFsFiles && !sourceFileSnapshots.has(fileName)) { const sourceFileText = originalHost.readFile(fileName); if (sourceFileText !== undefined) { sourceFileSnapshots.set(fileName, [undefined, { diff --git a/packages/typescript/lib/protocol/createProject.ts b/packages/typescript/lib/protocol/createProject.ts index 51b6b83a..699d8b56 100644 --- a/packages/typescript/lib/protocol/createProject.ts +++ b/packages/typescript/lib/protocol/createProject.ts @@ -13,7 +13,6 @@ export interface TypeScriptProjectHost extends Pick< | 'getProjectReferences' | 'getScriptFileNames' | 'getProjectVersion' - | 'getScriptSnapshot' > { } export function createLanguageServiceHost( @@ -312,16 +311,13 @@ export function createLanguageServiceHost( } } - const isOpenedFile = !!projectHost.getScriptSnapshot(fileName); + const openedFile = language.scripts.get(asScriptId(fileName), false); - if (isOpenedFile) { - const sourceScript = language.scripts.get(asScriptId(fileName)); - if (sourceScript && !sourceScript.generated) { - if (!version.map.has(sourceScript.snapshot)) { - version.map.set(sourceScript.snapshot, version.lastVersion++); - } - return version.map.get(sourceScript.snapshot)!.toString(); + if (openedFile && !openedFile.generated) { + if (!version.map.has(openedFile.snapshot)) { + version.map.set(openedFile.snapshot, version.lastVersion++); } + return version.map.get(openedFile.snapshot)!.toString(); } if (sys.fileExists(fileName)) {