From e3a33182e95bed68ac6fb47936864a2ddbc224c6 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 30 Apr 2024 08:50:31 +0800 Subject: [PATCH] refactor(language-core): resolved language ID by LangaugePlugin (#168) --- packages/kit/lib/createChecker.ts | 8 +-- packages/kit/lib/createFormatter.ts | 2 +- packages/language-core/index.ts | 53 ++++++++++--------- packages/language-core/lib/types.ts | 8 +-- .../lib/project/simpleProject.ts | 6 +-- .../lib/project/typescriptProject.ts | 14 +++-- packages/language-server/lib/server.ts | 6 +-- .../provideDocumentFormattingEdits.ts | 4 +- packages/monaco/worker.ts | 8 +-- packages/typescript/lib/common.ts | 17 ++++++ .../typescript/lib/node/proxyCreateProgram.ts | 9 ++-- .../typescript/lib/protocol/createProject.ts | 8 ++- .../createAsyncLanguageServicePlugin.ts | 8 +-- .../quickstart/createLanguageServicePlugin.ts | 9 ++-- 14 files changed, 91 insertions(+), 69 deletions(-) create mode 100644 packages/typescript/lib/common.ts diff --git a/packages/kit/lib/createChecker.ts b/packages/kit/lib/createChecker.ts index 70190d2f..63a9e867 100644 --- a/packages/kit/lib/createChecker.ts +++ b/packages/kit/lib/createChecker.ts @@ -1,4 +1,4 @@ -import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, resolveCommonLanguageId, TypeScriptProjectHost } from '@volar/language-service'; +import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, TypeScriptProjectHost } from '@volar/language-service'; import * as path from 'typesafe-path/posix'; import * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -10,7 +10,6 @@ export function createTypeScriptChecker( languagePlugins: LanguagePlugin[], languageServicePlugins: LanguageServicePlugin[], tsconfig: string, - getLanguageId = resolveCommonLanguageId, ) { const tsconfigPath = asPosix(tsconfig); return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, env => { @@ -30,7 +29,6 @@ export function createTypeScriptChecker( parsed.fileNames = parsed.fileNames.map(asPosix); return parsed; }, - getLanguageId, ); }); } @@ -39,7 +37,6 @@ export function createTypeScriptInferredChecker( languagePlugins: LanguagePlugin[], languageServicePlugins: LanguageServicePlugin[], getScriptFileNames: () => string[], - getLanguageId = resolveCommonLanguageId, compilerOptions = defaultCompilerOptions ) { return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, env => { @@ -50,7 +47,6 @@ export function createTypeScriptInferredChecker( options: compilerOptions, fileNames: getScriptFileNames().map(asPosix), }), - getLanguageId, ); }); } @@ -203,7 +199,6 @@ function createTypeScriptProjectHost( configFileName: string | undefined, env: ServiceEnvironment, createParsedCommandLine: () => Pick, - getLanguageId: (fileName: string) => string, ) { let scriptSnapshotsCache: Map = new Map(); @@ -240,7 +235,6 @@ function createTypeScriptProjectHost( } return scriptSnapshotsCache.get(fileName); }, - getLanguageId, fileNameToScriptId: env.typescript!.fileNameToUri, scriptIdToFileName: env.typescript!.uriToFileName, }; diff --git a/packages/kit/lib/createFormatter.ts b/packages/kit/lib/createFormatter.ts index 24e44daa..41fefff7 100644 --- a/packages/kit/lib/createFormatter.ts +++ b/packages/kit/lib/createFormatter.ts @@ -33,7 +33,7 @@ export function createFormatter( async function format(content: string, languageId: string, options: FormattingOptions): Promise { const snapshot = ts.ScriptSnapshot.fromString(content); - language.scripts.set(fakeUri, languageId, snapshot); + language.scripts.set(fakeUri, snapshot, languageId); 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 b61c1646..e9bd238b 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -24,26 +24,44 @@ export function createLanguage(plugins: LanguagePlugin[], caseSensitive: boolean sync(id); return sourceScripts.get(id); }, - set(id, languageId, snapshot, _plugins = plugins) { + set(id, snapshot, languageId, _plugins = plugins) { + if (!languageId) { + for (const plugin of plugins) { + languageId = plugin.getLanguageId?.(id); + if (languageId) { + break; + } + } + } + if (!languageId) { + return; + } if (sourceScripts.has(id)) { const sourceScript = sourceScripts.get(id)!; if (sourceScript.languageId !== languageId) { // languageId changed this.delete(id); - return this.set(id, languageId, snapshot); + return this.set(id, snapshot, languageId); } 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); + const newVirtualCode = sourceScript.generated.languagePlugin.updateVirtualCode?.(id, sourceScript.generated.root, snapshot); + if (newVirtualCode) { + sourceScript.generated.root = newVirtualCode; + 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 { + this.delete(id); + return; } } - return sourceScript; } else { // not changed @@ -55,7 +73,7 @@ export function createLanguage(plugins: LanguagePlugin[], caseSensitive: boolean const sourceScript: SourceScript = { id, languageId, snapshot }; sourceScripts.set(id, sourceScript); for (const languagePlugin of _plugins) { - const virtualCode = languagePlugin.createVirtualCode(id, languageId, snapshot); + const virtualCode = languagePlugin.createVirtualCode?.(id, languageId, snapshot); if (virtualCode) { sourceScript.generated = { root: virtualCode, @@ -167,20 +185,3 @@ export function* forEachEmbeddedCode(virtualCode: VirtualCode): Generator { - createVirtualCode(scriptId: string, languageId: string, snapshot: ts.IScriptSnapshot): T | undefined; - updateVirtualCode(scriptId: string, virtualCode: T, newSnapshot: ts.IScriptSnapshot): T; + getLanguageId(scriptId: string): string | undefined; + createVirtualCode?(scriptId: string, languageId: string, snapshot: ts.IScriptSnapshot): T | undefined; + updateVirtualCode?(scriptId: string, virtualCode: T, newSnapshot: ts.IScriptSnapshot): T | undefined; disposeVirtualCode?(scriptId: string, virtualCode: T): void; typescript?: { /** @@ -120,7 +121,6 @@ export interface TypeScriptProjectHost extends ts.System, Pick< | 'getScriptSnapshot' > { configFileName: string | undefined; - getLanguageId(scriptId: string): string; getSystemVersion?(): number; syncSystem?(): Promise; scriptIdToFileName(scriptId: string): string; diff --git a/packages/language-server/lib/project/simpleProject.ts b/packages/language-server/lib/project/simpleProject.ts index 7dd29d58..0805757a 100644 --- a/packages/language-server/lib/project/simpleProject.ts +++ b/packages/language-server/lib/project/simpleProject.ts @@ -19,9 +19,9 @@ export async function createSimpleServerProject( function getLanguageService() { if (!languageService) { const language = createLanguage(languagePlugins, false, uri => { - const script = server.documents.get(uri); - if (script) { - language.scripts.set(uri, script.languageId, script.getSnapshot()); + const document = server.documents.get(uri); + if (document) { + language.scripts.set(uri, document.getSnapshot(), document.uri); } else { language.scripts.delete(uri); diff --git a/packages/language-server/lib/project/typescriptProject.ts b/packages/language-server/lib/project/typescriptProject.ts index b9d39ef0..8a377d72 100644 --- a/packages/language-server/lib/project/typescriptProject.ts +++ b/packages/language-server/lib/project/typescriptProject.ts @@ -1,4 +1,4 @@ -import { LanguagePlugin, LanguageService, ProviderResult, ServiceEnvironment, TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; +import { LanguagePlugin, LanguageService, ProviderResult, ServiceEnvironment, TypeScriptProjectHost, createLanguageService } from '@volar/language-service'; import { createSys, createTypeScriptLanguage } from '@volar/typescript'; import * as path from 'path-browserify'; import type * as ts from 'typescript'; @@ -63,9 +63,6 @@ export async function createTypeScriptServerProject( getProjectReferences() { return parsedCommandLine.projectReferences; }, - getLanguageId(uri) { - return server.documents.get(uri)?.languageId ?? resolveCommonLanguageId(uri); - }, fileNameToScriptId: serviceEnv.typescript!.fileNameToUri, scriptIdToFileName: serviceEnv.typescript!.uriToFileName, }; @@ -112,7 +109,14 @@ export async function createTypeScriptServerProject( if (!languageService) { const language = createTypeScriptLanguage( ts, - languagePlugins, + [ + { + getLanguageId(uri) { + return server.documents.get(uri)?.languageId; + }, + }, + ...languagePlugins, + ], host, ); languageService = createLanguageService( diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index dd398583..0b03ecb0 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -4,12 +4,12 @@ import * as l10n from '@vscode/l10n'; import { configure as configureHttpRequests } from 'request-light'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; +import { registerEditorFeatures } from './register/registerEditorFeatures.js'; +import { registerLanguageFeatures } from './register/registerLanguageFeatures.js'; import { getServerCapabilities } from './serverCapabilities.js'; -import type { VolarInitializeParams, ServerProjectProvider } from './types.js'; +import type { ServerProjectProvider, VolarInitializeParams } from './types.js'; import { fileNameToUri } from './uri.js'; import { createUriMap } from './utils/uriMap.js'; -import { registerEditorFeatures } from './register/registerEditorFeatures.js'; -import { registerLanguageFeatures } from './register/registerLanguageFeatures.js'; export * from '@volar/snapshot-document'; diff --git a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts index 39acfcad..00eacc9f 100644 --- a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts +++ b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts @@ -66,7 +66,7 @@ export function register(context: ServiceContext) { const originalDocument = document; let tempSourceSnapshot = sourceScript.snapshot; - let tempVirtualFile = context.language.scripts.set(sourceScript.id + '.tmp', sourceScript.languageId, sourceScript.snapshot, [sourceScript.generated.languagePlugin]).generated?.root; + let tempVirtualFile = context.language.scripts.set(sourceScript.id + '.tmp', sourceScript.snapshot, sourceScript.languageId, [sourceScript.generated.languagePlugin])?.generated?.root; if (!tempVirtualFile) { return; } @@ -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.scripts.set(sourceScript.id + '.tmp', sourceScript.languageId, tempSourceSnapshot, [sourceScript.generated.languagePlugin]).generated?.root; + tempVirtualFile = context.language.scripts.set(sourceScript.id + '.tmp', tempSourceSnapshot, sourceScript.languageId, [sourceScript.generated.languagePlugin])?.generated?.root; if (!tempVirtualFile) { break; } diff --git a/packages/monaco/worker.ts b/packages/monaco/worker.ts index b2ff183a..56dff953 100644 --- a/packages/monaco/worker.ts +++ b/packages/monaco/worker.ts @@ -4,7 +4,6 @@ import { LanguageServicePlugin, createLanguageService as _createLanguageService, createLanguage, - resolveCommonLanguageId, type LanguageService, type ServiceEnvironment, TypeScriptProjectHost, @@ -22,14 +21,12 @@ export function createSimpleWorkerService({ languagePlugins = [], servicePlugins = [], extraApis = {} as T, - getLanguageId = resolveCommonLanguageId, }: { env: ServiceEnvironment; workerContext: monaco.worker.IWorkerContext; languagePlugins?: LanguagePlugin[]; servicePlugins?: LanguageServicePlugin[]; extraApis?: T; - getLanguageId?: (uri: string) => string; }) { const snapshots = new Map(); const language = createLanguage( @@ -49,7 +46,7 @@ export function createSimpleWorkerService({ getChangeRange: () => undefined, }; snapshots.set(model, [model.version, snapshot]); - language.scripts.set(uri, getLanguageId(uri), snapshot); + language.scripts.set(uri, snapshot); } else { language.scripts.delete(uri); @@ -68,7 +65,6 @@ export function createTypeScriptWorkerService({ languagePlugins = [], servicePlugins = [], extraApis = {} as T, - getLanguageId = resolveCommonLanguageId, }: { typescript: typeof import('typescript'), compilerOptions: ts.CompilerOptions, @@ -77,7 +73,6 @@ export function createTypeScriptWorkerService({ languagePlugins?: LanguagePlugin[]; servicePlugins?: LanguageServicePlugin[]; extraApis?: T; - getLanguageId?: (uri: string) => string; }) { let projectVersion = 0; @@ -134,7 +129,6 @@ export function createTypeScriptWorkerService({ getCompilationSettings() { return compilerOptions; }, - getLanguageId: id => getLanguageId(id), fileNameToScriptId: env.typescript!.fileNameToUri, scriptIdToFileName: env.typescript!.uriToFileName, }; diff --git a/packages/typescript/lib/common.ts b/packages/typescript/lib/common.ts new file mode 100644 index 00000000..1f6a7eb2 --- /dev/null +++ b/packages/typescript/lib/common.ts @@ -0,0 +1,17 @@ +import type { LanguagePlugin } from "@volar/language-core"; + +export const fileLanguageIdProviderPlugin: LanguagePlugin = { + getLanguageId(scriptId) { + const ext = scriptId.split('.').pop()!; + switch (ext) { + case 'js': return 'javascript'; + case 'cjs': return 'javascript'; + case 'mjs': return 'javascript'; + case 'ts': return 'typescript'; + case 'cts': return 'typescript'; + case 'mts': return 'typescript'; + case 'jsx': return 'javascriptreact'; + case 'tsx': return 'typescriptreact'; + } + }, +}; diff --git a/packages/typescript/lib/node/proxyCreateProgram.ts b/packages/typescript/lib/node/proxyCreateProgram.ts index cd0a2bf4..f9ec45f3 100644 --- a/packages/typescript/lib/node/proxyCreateProgram.ts +++ b/packages/typescript/lib/node/proxyCreateProgram.ts @@ -2,6 +2,7 @@ import { Language, LanguagePlugin, createLanguage } from '@volar/language-core'; import type * as ts from 'typescript'; import { createResolveModuleName } from '../resolveModuleName'; import { decorateProgram } from './decorateProgram'; +import { fileLanguageIdProviderPlugin } from '../common'; const arrayEqual = (a: readonly any[], b: readonly any[]) => { if (a.length !== b.length) { @@ -32,7 +33,6 @@ export function proxyCreateProgram( ts: typeof import('typescript'), original: typeof ts['createProgram'], getLanguagePlugins: (ts: typeof import('typescript'), options: ts.CreateProgramOptions) => LanguagePlugin[], - getLanguageId: (fileName: string) => string, ) { const sourceFileSnapshots = new Map(); const parsedSourceFiles = new WeakMap(); @@ -59,7 +59,10 @@ export function proxyCreateProgram( lastOptions = options; languagePlugins = getLanguagePlugins(ts, options); language = createLanguage( - languagePlugins, + [ + ...languagePlugins, + fileLanguageIdProviderPlugin, + ], ts.sys.useCaseSensitiveFileNames, fileName => { if (!sourceFileSnapshots.has(fileName)) { @@ -83,7 +86,7 @@ export function proxyCreateProgram( } const snapshot = sourceFileSnapshots.get(fileName)?.[1]; if (snapshot) { - language!.scripts.set(fileName, getLanguageId(fileName), snapshot); + language!.scripts.set(fileName, snapshot); } else { language!.scripts.delete(fileName); diff --git a/packages/typescript/lib/protocol/createProject.ts b/packages/typescript/lib/protocol/createProject.ts index ff9e3b4c..24c1831a 100644 --- a/packages/typescript/lib/protocol/createProject.ts +++ b/packages/typescript/lib/protocol/createProject.ts @@ -3,6 +3,7 @@ import type * as ts from 'typescript'; import { forEachEmbeddedCode } from '@volar/language-core'; import * as path from 'path-browserify'; import { createResolveModuleName } from '../resolveModuleName'; +import { fileLanguageIdProviderPlugin } from '../common'; const scriptVersions = new Map; }>(); const fsFileSnapshots = new Map(); @@ -14,7 +15,10 @@ export function createTypeScriptLanguage( ): Language { const language = createLanguage( - languagePlugins, + [ + ...languagePlugins, + fileLanguageIdProviderPlugin, + ], projectHost.useCaseSensitiveFileNames, scriptId => { const fileName = projectHost.scriptIdToFileName(scriptId); @@ -39,7 +43,7 @@ export function createTypeScriptLanguage( } if (snapshot) { - language.scripts.set(scriptId, projectHost.getLanguageId(scriptId), snapshot); + language.scripts.set(scriptId, snapshot); } else { language.scripts.delete(scriptId); diff --git a/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts b/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts index 3fca8851..3df08390 100644 --- a/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts +++ b/packages/typescript/lib/quickstart/createAsyncLanguageServicePlugin.ts @@ -3,6 +3,7 @@ import { decorateLanguageService } from '../node/decorateLanguageService'; import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost'; import { createLanguage, LanguagePlugin } from '@volar/language-core'; import { arrayItemsEqual } from './createLanguageServicePlugin'; +import { fileLanguageIdProviderPlugin } from '../common'; const externalFiles = new WeakMap(); const decoratedLanguageServices = new WeakSet(); @@ -15,7 +16,6 @@ export function createAsyncLanguageServicePlugin( ts: typeof import('typescript'), info: ts.server.PluginCreateInfo ) => Promise, - getLanguageId: (fileName: string) => string, ): ts.server.PluginModuleFactory { return modules => { const { typescript: ts } = modules; @@ -67,14 +67,16 @@ export function createAsyncLanguageServicePlugin( loadLanguagePlugins(ts, info).then(languagePlugins => { const language = createLanguage( - languagePlugins, + [ + ...languagePlugins, + fileLanguageIdProviderPlugin, + ], ts.sys.useCaseSensitiveFileNames, fileName => { const snapshot = getScriptSnapshot(fileName); if (snapshot) { language.scripts.set( fileName, - getLanguageId(fileName), snapshot ); } else { diff --git a/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts b/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts index 379306c3..597c65e6 100644 --- a/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts +++ b/packages/typescript/lib/quickstart/createLanguageServicePlugin.ts @@ -2,6 +2,7 @@ import type * as ts from 'typescript'; import { decorateLanguageService } from '../node/decorateLanguageService'; import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost'; import { createLanguage, LanguagePlugin } from '@volar/language-core'; +import { fileLanguageIdProviderPlugin } from '../common'; const externalFiles = new WeakMap(); const projectExternalFileExtensions = new WeakMap(); @@ -13,7 +14,6 @@ export function createLanguageServicePlugin( ts: typeof import('typescript'), info: ts.server.PluginCreateInfo ) => LanguagePlugin[], - getLanguageId: (fileName: string) => string, ): ts.server.PluginModuleFactory { return modules => { const { typescript: ts } = modules; @@ -33,12 +33,15 @@ export function createLanguageServicePlugin( projectExternalFileExtensions.set(info.project, extensions); const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost); const language = createLanguage( - languagePlugins, + [ + ...languagePlugins, + fileLanguageIdProviderPlugin, + ], ts.sys.useCaseSensitiveFileNames, fileName => { const snapshot = getScriptSnapshot(fileName); if (snapshot) { - language.scripts.set(fileName, getLanguageId(fileName), snapshot); + language.scripts.set(fileName, snapshot); } else { language.scripts.delete(fileName);