From c930cdb7bb4c6284a239aeee13023685048e2784 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 29 Jun 2024 11:31:51 +0800 Subject: [PATCH] refactor(language-service): add project context option to language service --- packages/kit/lib/createChecker.ts | 50 ++++----- packages/kit/lib/createFormatter.ts | 3 +- packages/kit/lib/utils.ts | 4 +- .../lib/project/simpleProject.ts | 3 +- .../lib/project/typescriptProjectLs.ts | 30 +++--- .../lib/register/registerEditorFeatures.ts | 19 ++-- .../language-service/lib/languageService.ts | 6 +- packages/language-service/lib/types.ts | 5 +- packages/monaco/worker.ts | 100 ++++++++++-------- packages/typescript/index.ts | 11 +- .../typescript/lib/protocol/createProject.ts | 21 ++-- 11 files changed, 142 insertions(+), 110 deletions(-) diff --git a/packages/kit/lib/createChecker.ts b/packages/kit/lib/createChecker.ts index e8a69914..98435a6c 100644 --- a/packages/kit/lib/createChecker.ts +++ b/packages/kit/lib/createChecker.ts @@ -3,7 +3,7 @@ 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 { asPosix, defaultCompilerOptions, asUri, asFileName } from './utils'; import { URI } from 'vscode-uri'; import { TypeScriptProjectHost, createLanguageServiceHost, resolveFileLanguageId } from '@volar/typescript'; @@ -82,7 +82,7 @@ function createTypeScriptCheckerWorker( uri => { // fs files const cache = fsFileSnapshots.get(uri); - const fileName = uriToFileName(uri); + const fileName = asFileName(uri); const modifiedTime = ts.sys.getModifiedTime?.(fileName)?.valueOf(); if (!cache || cache[0] !== modifiedTime) { if (ts.sys.fileExists(fileName)) { @@ -104,23 +104,25 @@ function createTypeScriptCheckerWorker( } ); const projectHost = getProjectHost(env); - language.typescript = { - configFileName, - sys: ts.sys, - asFileName: uriToFileName, - asScriptId: fileNameToUri, - ...createLanguageServiceHost( - ts, - ts.sys, - language, - fileNameToUri, - projectHost - ), - }; const languageService = createLanguageService( language, languageServicePlugins, - env + env, + { + typescript: { + configFileName, + sys: ts.sys, + asFileName, + asUri, + ...createLanguageServiceHost( + ts, + ts.sys, + language, + asUri, + projectHost + ), + }, + } ); return { @@ -154,19 +156,19 @@ function createTypeScriptCheckerWorker( function fileEvent(fileName: string, type: FileChangeType) { fileName = asPosix(fileName); for (const cb of didChangeWatchedFilesCallbacks) { - cb({ changes: [{ uri: fileNameToUri(fileName).toString(), type }] }); + cb({ changes: [{ uri: asUri(fileName).toString(), type }] }); } } function check(fileName: string) { fileName = asPosix(fileName); - const uri = fileNameToUri(fileName); + const uri = asUri(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 = fileNameToUri(fileName); + const uri = asUri(fileName); const sourceScript = languageService.context.language.scripts.get(uri); if (sourceScript) { const document = languageService.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot); @@ -188,7 +190,7 @@ function createTypeScriptCheckerWorker( if (editFile) { const editDocument = languageService.context.documents.get(parsedUri, editFile.languageId, editFile.snapshot); const newString = TextDocument.applyEdits(editDocument, edits); - await writeFile(uriToFileName(parsedUri), newString); + await writeFile(asFileName(parsedUri), newString); } } } @@ -199,7 +201,7 @@ function createTypeScriptCheckerWorker( if (editFile) { const editDocument = languageService.context.documents.get(changeUri, editFile.languageId, editFile.snapshot); const newString = TextDocument.applyEdits(editDocument, change.edits); - await writeFile(uriToFileName(changeUri), newString); + await writeFile(asFileName(changeUri), newString); } } // TODO: CreateFile | RenameFile | DeleteFile @@ -219,7 +221,7 @@ function createTypeScriptCheckerWorker( function formatErrors(fileName: string, diagnostics: Diagnostic[], rootPath: string) { fileName = asPosix(fileName); - const uri = fileNameToUri(fileName); + const uri = asUri(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 => ({ @@ -250,7 +252,7 @@ function createTypeScriptProjectHost( const host: TypeScriptProjectHost = { getCurrentDirectory: () => env.workspaceFolders.length - ? uriToFileName(env.workspaceFolders[0]) + ? asFileName(env.workspaceFolders[0]) : process.cwd(), getCompilationSettings: () => { return parsedCommandLine.options; @@ -280,7 +282,7 @@ function createTypeScriptProjectHost( env.onDidChangeWatchedFiles?.(({ changes }) => { for (const change of changes) { const changeUri = URI.parse(change.uri); - const fileName = uriToFileName(changeUri); + const fileName = asFileName(changeUri); if (change.type === 2 satisfies typeof FileChangeType.Changed) { if (scriptSnapshotsCache.has(fileName)) { projectVersion++; diff --git a/packages/kit/lib/createFormatter.ts b/packages/kit/lib/createFormatter.ts index 93071224..d49e5a4e 100644 --- a/packages/kit/lib/createFormatter.ts +++ b/packages/kit/lib/createFormatter.ts @@ -16,7 +16,8 @@ export function createFormatter( const languageService = createLanguageService( language, services, - env + env, + {} ); return { diff --git a/packages/kit/lib/utils.ts b/packages/kit/lib/utils.ts index 997403fe..7535375b 100644 --- a/packages/kit/lib/utils.ts +++ b/packages/kit/lib/utils.ts @@ -15,6 +15,6 @@ export function asPosix(path: string) { return path.replace(/\\/g, '/') as path.PosixPath; } -export const uriToFileName = (uri: URI) => uri.fsPath.replace(/\\/g, '/'); +export const asFileName = (uri: URI) => uri.fsPath.replace(/\\/g, '/'); -export const fileNameToUri = (fileName: string) => URI.file(fileName); +export const asUri = (fileName: string) => URI.file(fileName); diff --git a/packages/language-server/lib/project/simpleProject.ts b/packages/language-server/lib/project/simpleProject.ts index dcd6d73a..ab9e2c68 100644 --- a/packages/language-server/lib/project/simpleProject.ts +++ b/packages/language-server/lib/project/simpleProject.ts @@ -30,7 +30,8 @@ export function createSimpleProject(languagePlugins: LanguagePlugin[]): Lan languageService = createLanguageService( language, server.languageServicePlugins, - createLanguageServiceEnvironment(server, [...server.workspaceFolders.keys()]) + createLanguageServiceEnvironment(server, [...server.workspaceFolders.keys()]), + {} ); }, getLanguageService() { diff --git a/packages/language-server/lib/project/typescriptProjectLs.ts b/packages/language-server/lib/project/typescriptProjectLs.ts index 82bd24a5..94581a7f 100644 --- a/packages/language-server/lib/project/typescriptProjectLs.ts +++ b/packages/language-server/lib/project/typescriptProjectLs.ts @@ -134,24 +134,26 @@ export async function createTypeScriptLS( } } ); - language.typescript = { - configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, - sys, - asScriptId: asUri, - asFileName: asFileName, - ...createLanguageServiceHost( - ts, - sys, - language, - asUri, - projectHost - ), - }; setup(language); const languageService = createLanguageService( language, server.languageServicePlugins, - serviceEnv + serviceEnv, + { + typescript: { + configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, + sys, + asUri, + asFileName, + ...createLanguageServiceHost( + ts, + sys, + language, + asUri, + projectHost + ), + } + } ); return { diff --git a/packages/language-server/lib/register/registerEditorFeatures.ts b/packages/language-server/lib/register/registerEditorFeatures.ts index ee3ef542..5edb9eef 100644 --- a/packages/language-server/lib/register/registerEditorFeatures.ts +++ b/packages/language-server/lib/register/registerEditorFeatures.ts @@ -53,9 +53,10 @@ export function registerEditorFeatures(server: LanguageServer) { server.connection.onRequest(GetMatchTsConfigRequest.type, async params => { const uri = URI.parse(params.uri); const languageService = (await server.project.getLanguageService(uri)); - if (languageService.context.language.typescript?.configFileName) { - const { configFileName, asScriptId } = languageService.context.language.typescript; - return { uri: asScriptId(configFileName).toString() }; + const tsProject = languageService.context.project.typescript; + if (tsProject?.configFileName) { + const { configFileName, asUri } = tsProject; + return { uri: asUri(configFileName).toString() }; } }); server.connection.onRequest(GetVirtualFileRequest.type, async document => { @@ -107,10 +108,11 @@ export function registerEditorFeatures(server: LanguageServer) { const fs = _require('fs'); const uri = URI.parse(params.uri); const languageService = (await server.project.getLanguageService(uri)); + const tsProject = languageService.context.project.typescript; - if (languageService.context.language.typescript) { + if (tsProject) { - const { languageServiceHost } = languageService.context.language.typescript; + const { languageServiceHost } = tsProject; for (const fileName of languageServiceHost.getScriptFileNames()) { if (!fs.existsSync(fileName)) { @@ -121,7 +123,7 @@ export function registerEditorFeatures(server: LanguageServer) { } } else { - const uri = languageService.context.language.typescript.asScriptId(fileName); + const uri = tsProject.asUri(fileName); const sourceScript = languageService.context.language.scripts.get(uri); if (sourceScript?.generated) { const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); @@ -150,8 +152,9 @@ export function registerEditorFeatures(server: LanguageServer) { for (const languageService of await server.project.getExistingLanguageServices()) { const tsLanguageService: ts.LanguageService | undefined = languageService.context.inject('typescript/languageService'); const program = tsLanguageService?.getProgram(); - if (program && languageService.context.language.typescript) { - const { languageServiceHost, configFileName } = languageService.context.language.typescript; + const tsProject = languageService.context.project.typescript; + if (program && tsProject) { + const { languageServiceHost, configFileName } = tsProject; const projectName = configFileName ?? (languageServiceHost.getCurrentDirectory() + '(inferred)'); const sourceFiles = program.getSourceFiles() ?? []; for (const sourceFile of sourceFiles) { diff --git a/packages/language-service/lib/languageService.ts b/packages/language-service/lib/languageService.ts index ff82bc13..be8a405b 100644 --- a/packages/language-service/lib/languageService.ts +++ b/packages/language-service/lib/languageService.ts @@ -37,7 +37,7 @@ import * as completionResolve from './features/resolveCompletionItem'; import * as documentLinkResolve from './features/resolveDocumentLink'; import * as inlayHintResolve from './features/resolveInlayHint'; import * as workspaceSymbolResolve from './features/resolveWorkspaceSymbol'; -import type { LanguageServiceContext, LanguageServiceEnvironment, LanguageServicePlugin } from './types'; +import type { LanguageServiceContext, LanguageServiceEnvironment, LanguageServicePlugin, ProjectContext } from './types'; import { NoneCancellationToken } from './utils/cancellation'; import { UriMap, createUriMap } from './utils/uriMap'; @@ -48,12 +48,14 @@ export const embeddedContentScheme = 'volar-embedded-content'; export function createLanguageService( language: Language, plugins: LanguageServicePlugin[], - env: LanguageServiceEnvironment + env: LanguageServiceEnvironment, + project: ProjectContext ) { const documentVersions = createUriMap(); const snapshot2Doc = new WeakMap>(); const context: LanguageServiceContext = { language, + project, getLanguageService: () => langaugeService, documents: { get(uri, languageId, snapshot) { diff --git a/packages/language-service/lib/types.ts b/packages/language-service/lib/types.ts index db85f685..bd55a286 100644 --- a/packages/language-service/lib/types.ts +++ b/packages/language-service/lib/types.ts @@ -44,11 +44,14 @@ export interface LanguageServiceCommand { is(value: vscode.Command): boolean; } +export interface ProjectContext { } + export interface LanguageServiceContext { language: Language; + project: ProjectContext; getLanguageService(): LanguageService; env: LanguageServiceEnvironment; - inject( + inject( key: K, ...args: Provide[K] extends (...args: any) => any ? Parameters : never ): ReturnType any ? Provide[K] : never> | undefined; diff --git a/packages/monaco/worker.ts b/packages/monaco/worker.ts index b8bda9c3..9fc8b091 100644 --- a/packages/monaco/worker.ts +++ b/packages/monaco/worker.ts @@ -2,6 +2,7 @@ import { Language, LanguagePlugin, LanguageServicePlugin, + ProjectContext, createLanguageService as _createLanguageService, createLanguage, createUriMap, @@ -57,7 +58,7 @@ export function createSimpleWorkerService({ } ); - return createWorkerService(language, servicePlugins, env, extraApis); + return createWorkerService(language, servicePlugins, env, {}, extraApis); } export function createTypeScriptWorkerService({ @@ -133,49 +134,56 @@ export function createTypeScriptWorkerService({ } } ); - language.typescript = { - configFileName: undefined, - sys, - asFileName: uriConverter.asFileName, - asScriptId: uriConverter.asUri, - ...createLanguageServiceHost( - ts, - sys, - language, - uriConverter.asUri, - { - getCurrentDirectory() { - return sys.getCurrentDirectory(); - }, - getScriptFileNames() { - return workerContext.getMirrorModels().map(model => uriConverter.asFileName(model.uri as URI)); - }, - getProjectVersion() { - const models = workerContext.getMirrorModels(); - if (modelVersions.size === workerContext.getMirrorModels().length) { - if (models.every(model => modelVersions.get(model) === model.version)) { + + return createWorkerService( + language, + servicePlugins, + env, + { + typescript: { + configFileName: undefined, + sys, + asFileName: uriConverter.asFileName, + asUri: uriConverter.asUri, + ...createLanguageServiceHost( + ts, + sys, + language, + uriConverter.asUri, + { + getCurrentDirectory() { + return sys.getCurrentDirectory(); + }, + getScriptFileNames() { + return workerContext.getMirrorModels().map(model => uriConverter.asFileName(model.uri as URI)); + }, + getProjectVersion() { + const models = workerContext.getMirrorModels(); + if (modelVersions.size === workerContext.getMirrorModels().length) { + if (models.every(model => modelVersions.get(model) === model.version)) { + return projectVersion.toString(); + } + } + modelVersions.clear(); + for (const model of workerContext.getMirrorModels()) { + modelVersions.set(model, model.version); + } + projectVersion++; return projectVersion.toString(); - } - } - modelVersions.clear(); - for (const model of workerContext.getMirrorModels()) { - modelVersions.set(model, model.version); + }, + getScriptSnapshot(fileName) { + const uri = uriConverter.asUri(fileName); + return getModelSnapshot(uri); + }, + getCompilationSettings() { + return compilerOptions; + }, } - projectVersion++; - return projectVersion.toString(); - }, - getScriptSnapshot(fileName) { - const uri = uriConverter.asUri(fileName); - return getModelSnapshot(uri); - }, - getCompilationSettings() { - return compilerOptions; - }, - } - ), - }; - - return createWorkerService(language, servicePlugins, env, extraApis); + ), + }, + }, + extraApis + ); function getModelSnapshot(uri: URI) { const model = workerContext.getMirrorModels().find(model => model.uri.toString() === uri.toString()); @@ -199,10 +207,16 @@ function createWorkerService( language: Language, servicePlugins: LanguageServicePlugin[], env: LanguageServiceEnvironment, + projectContext: ProjectContext, extraApis: T = {} as any ): LanguageService & T { - const languageService = _createLanguageService(language, servicePlugins, env); + const languageService = _createLanguageService( + language, + servicePlugins, + env, + projectContext + ); class WorkerService { }; diff --git a/packages/typescript/index.ts b/packages/typescript/index.ts index c2087158..6675bc1f 100644 --- a/packages/typescript/index.ts +++ b/packages/typescript/index.ts @@ -8,9 +8,10 @@ export * from './lib/protocol/createSys'; import type { VirtualCode } from '@volar/language-core'; import type * as ts from 'typescript'; +import { URI } from 'vscode-uri'; -declare module '@volar/language-core' { - export interface Language { +declare module '@volar/language-service' { + export interface ProjectContext { typescript?: { configFileName: string | undefined; sys: ts.System & { @@ -19,11 +20,13 @@ declare module '@volar/language-core' { }; languageServiceHost: ts.LanguageServiceHost; getExtraServiceScript(fileName: string): TypeScriptExtraServiceScript | undefined; - asScriptId(fileName: string): T; - asFileName(scriptId: T): string; + asUri(fileName: string): URI; + asFileName(uri: URI): string; }; } +} +declare module '@volar/language-core' { export interface LanguagePlugin { typescript?: TypeScriptGenericOptions & TypeScriptNonTSPluginOptions; } diff --git a/packages/typescript/lib/protocol/createProject.ts b/packages/typescript/lib/protocol/createProject.ts index 5ad746ae..c0854ed7 100644 --- a/packages/typescript/lib/protocol/createProject.ts +++ b/packages/typescript/lib/protocol/createProject.ts @@ -1,6 +1,7 @@ import { FileMap, Language, forEachEmbeddedCode } from '@volar/language-core'; import * as path from 'path-browserify'; import type * as ts from 'typescript'; +import { URI } from 'vscode-uri'; import type { TypeScriptExtraServiceScript } from '../..'; import { createResolveModuleName } from '../resolveModuleName'; import type { createSys } from './createSys'; @@ -16,11 +17,11 @@ export interface TypeScriptProjectHost extends Pick< | 'getScriptSnapshot' > { } -export function createLanguageServiceHost( +export function createLanguageServiceHost( ts: typeof import('typescript'), sys: ReturnType | ts.System, - language: Language, - asScriptId: (fileName: string) => T, + language: Language, + asUri: (fileName: string) => URI, projectHost: TypeScriptProjectHost ) { const scriptVersions = new FileMap<{ lastVersion: number; map: WeakMap; }>(sys.useCaseSensitiveFileNames); @@ -103,7 +104,7 @@ export function createLanguageServiceHost( return extraScriptRegistry.get(fileName)!.scriptKind; } - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(asUri(fileName)); if (sourceScript?.generated) { const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); if (serviceScript) { @@ -139,7 +140,7 @@ export function createLanguageServiceHost( } } - if (language.plugins.some(language => language.typescript?.extraFileExtensions.length)) { + if (language.plugins.some(plugin => plugin.typescript?.extraFileExtensions.length)) { // TODO: can this share between monorepo packages? const moduleCache = ts.createModuleResolutionCache( @@ -147,7 +148,7 @@ export function createLanguageServiceHost( languageServiceHost.useCaseSensitiveFileNames?.() ? s => s : s => s.toLowerCase(), languageServiceHost.getCompilationSettings() ); - const resolveModuleName = createResolveModuleName(ts, languageServiceHost, language.plugins, fileName => language.scripts.get(asScriptId(fileName))); + const resolveModuleName = createResolveModuleName(ts, languageServiceHost, language.plugins, fileName => language.scripts.get(asUri(fileName))); let lastSysVersion = 'version' in sys ? sys.version : undefined; @@ -209,7 +210,7 @@ export function createLanguageServiceHost( const tsFileNamesSet = new Set(); for (const fileName of projectHost.getScriptFileNames()) { - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(asUri(fileName)); if (sourceScript?.generated) { const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); if (serviceScript) { @@ -255,7 +256,7 @@ export function createLanguageServiceHost( return extraScriptRegistry.get(fileName)!.code.snapshot; } - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(asUri(fileName)); if (sourceScript?.generated) { const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); @@ -286,7 +287,7 @@ export function createLanguageServiceHost( return version.map.get(snapshot)!.toString(); } - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(asUri(fileName)); if (sourceScript?.generated) { const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); @@ -301,7 +302,7 @@ export function createLanguageServiceHost( const isOpenedFile = !!projectHost.getScriptSnapshot(fileName); if (isOpenedFile) { - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(asUri(fileName)); if (sourceScript && !sourceScript.generated) { if (!version.map.has(sourceScript.snapshot)) { version.map.set(sourceScript.snapshot, version.lastVersion++);