From 062895a2eba72ea659b98e23fcc5bbc443af7e7d Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Tue, 20 Dec 2022 14:59:36 +0800 Subject: [PATCH] refactor: rewrite typescript-vue-plugin to make it much faster --- packages/language-core/src/languageContext.ts | 47 ++-- packages/language-core/src/types.ts | 1 - .../typescript-vue-plugin/src/index.ts | 254 +++--------------- 3 files changed, 63 insertions(+), 239 deletions(-) diff --git a/packages/language-core/src/languageContext.ts b/packages/language-core/src/languageContext.ts index 287e273da..455879667 100644 --- a/packages/language-core/src/languageContext.ts +++ b/packages/language-core/src/languageContext.ts @@ -40,7 +40,7 @@ export function createEmbeddedLanguageServiceHost( // .vue.js -> .vue // .vue.ts -> .vue - // .vue.d.ts (never) + // .vue.d.ts -> [ignored] const vueFileName = fileName.substring(0, fileName.lastIndexOf('.')); if (!documentRegistry.get(vueFileName)) { @@ -129,36 +129,36 @@ export function createEmbeddedLanguageServiceHost( let tsFileUpdated = false; - const remainFileNames = new Set(host.getScriptFileNames()); - const sourceFilesToUpdate: [SourceFile, LanguageModule, ts.IScriptSnapshot][] = []; + const checkRemains = new Set(host.getScriptFileNames()); + const sourceFilesShouldUpdate: [SourceFile, LanguageModule, ts.IScriptSnapshot][] = []; // .vue for (const [sourceFile, languageModule] of documentRegistry.getAll()) { - remainFileNames.delete(sourceFile.fileName); + checkRemains.delete(sourceFile.fileName); + + const snapshot = host.getScriptSnapshot(sourceFile.fileName); + if (!snapshot) { + // delete + documentRegistry.delete(sourceFile.fileName) + tsFileUpdated = true; + continue; + } + const newVersion = host.getScriptVersion(sourceFile.fileName); if (sourceVueFileVersions.get(sourceFile.fileName) !== newVersion) { + // update sourceVueFileVersions.set(sourceFile.fileName, newVersion); - const snapshot = host.getScriptSnapshot(sourceFile.fileName); - if (snapshot) { - // update - sourceFilesToUpdate.push([sourceFile, languageModule, snapshot]); - } - else { - // delete - if (documentRegistry.delete(sourceFile.fileName)) { - tsFileUpdated = true; - } - } + sourceFilesShouldUpdate.push([sourceFile, languageModule, snapshot]); } } // no any vue file version change, it mean project version was update by ts file change at this time - if (!sourceFilesToUpdate.length) { + if (!sourceFilesShouldUpdate.length) { tsFileUpdated = true; } // add - for (const fileName of [...remainFileNames]) { + for (const fileName of [...checkRemains]) { const snapshot = host.getScriptSnapshot(fileName); if (snapshot) { for (const languageModule of languageModules) { @@ -166,7 +166,7 @@ export function createEmbeddedLanguageServiceHost( if (sourceFile) { sourceVueFileVersions.set(sourceFile.fileName, host.getScriptVersion(fileName)); documentRegistry.set(fileName, reactive(sourceFile), languageModule); - remainFileNames.delete(fileName); + checkRemains.delete(fileName); break; } } @@ -177,7 +177,7 @@ export function createEmbeddedLanguageServiceHost( for (const [oldTsFileName, oldTsFileVersion] of [...sourceTsFileVersions]) { const newVersion = host.getScriptVersion(oldTsFileName); if (oldTsFileVersion !== newVersion) { - if (!remainFileNames.has(oldTsFileName) && !host.getScriptSnapshot(oldTsFileName)) { + if (!checkRemains.has(oldTsFileName) && !host.getScriptSnapshot(oldTsFileName)) { // delete sourceTsFileVersions.delete(oldTsFileName); } @@ -189,7 +189,7 @@ export function createEmbeddedLanguageServiceHost( } } - for (const nowFileName of remainFileNames) { + for (const nowFileName of checkRemains) { if (!sourceTsFileVersions.has(nowFileName)) { // add const newVersion = host.getScriptVersion(nowFileName); @@ -198,7 +198,7 @@ export function createEmbeddedLanguageServiceHost( } } - for (const [sourceFile, languageModule, snapshot] of sourceFilesToUpdate) { + for (const [sourceFile, languageModule, snapshot] of sourceFilesShouldUpdate) { forEachEmbeddeds(sourceFile, embedded => { virtualFileVersions.delete(embedded.fileName); @@ -250,10 +250,7 @@ export function createEmbeddedLanguageServiceHost( } for (const fileName of host.getScriptFileNames()) { - if (host.isTsPlugin) { - tsFileNames.add(fileName); // .vue + .ts - } - else if (!documentRegistry.has(fileName)) { + if (!documentRegistry.has(fileName)) { tsFileNames.add(fileName); // .ts } } diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index d9aae3531..514f5e164 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -75,6 +75,5 @@ export interface LanguageModule { export type LanguageServiceHost = ts.LanguageServiceHost & { getTypeScriptModule(): typeof import('typescript/lib/tsserverlibrary'); - isTsPlugin?: boolean, isTsc?: boolean, }; diff --git a/vue-language-tools/typescript-vue-plugin/src/index.ts b/vue-language-tools/typescript-vue-plugin/src/index.ts index 1d513dff1..50120ff6e 100644 --- a/vue-language-tools/typescript-vue-plugin/src/index.ts +++ b/vue-language-tools/typescript-vue-plugin/src/index.ts @@ -1,44 +1,66 @@ import * as vue from '@volar/vue-language-core'; import * as vueTs from '@volar/vue-typescript'; -import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as tsFaster from '@volar/typescript-faster'; const init: ts.server.PluginModuleFactory = (modules) => { const { typescript: ts } = modules; - const vueFilesGetter = new WeakMap string[]>(); const pluginModule: ts.server.PluginModule = { create(info) { - // fix: https://github.com/johnsoncodehk/volar/issues/1146 - if (info.project.projectKind === ts.server.ProjectKind.Inferred) { + const projectName = info.project.getProjectName(); + + if (!info.project.fileExists(projectName)) { + // project name not a tsconfig path, this is a inferred project return info.languageService; } - const proxyHost = createProxyHost(ts, info); - - if (proxyHost.getVueFiles().length === 0) { + const extraFileExtensions: ts.FileExtensionInfo[] = [{ + extension: 'vue', + isMixedContent: true, + scriptKind: ts.ScriptKind.Deferred, + }]; + const parsed = vue.createParsedCommandLine(ts, ts.sys, projectName, extraFileExtensions); + if (!parsed.fileNames.some(fileName => fileName.endsWith('.vue'))) { + // no vue file return info.languageService; } // fix: https://github.com/johnsoncodehk/volar/issues/205 + // @ts-expect-error + info.project.__vue_getScriptKind = info.project.getScriptKind; info.project.getScriptKind = fileName => { - switch (path.extname(fileName)) { + switch (fileName.substring(fileName.lastIndexOf('.'))) { case '.vue': return ts.ScriptKind.Deferred; - case '.js': return ts.ScriptKind.JS; - case '.jsx': return ts.ScriptKind.JSX; - case '.ts': return ts.ScriptKind.TS; - case '.tsx': return ts.ScriptKind.TSX; - case '.json': return ts.ScriptKind.JSON; - default: return ts.ScriptKind.Unknown; } + // @ts-expect-error + return info.project.__vue_getScriptKind(fileName); }; - const ls = vueTs.createLanguageService(proxyHost.host); - - tsFaster.decorate(ts, proxyHost.host, ls); + const vueTsLsHost: vue.LanguageServiceHost = { + getNewLine: () => info.project.getNewLine(), + useCaseSensitiveFileNames: () => info.project.useCaseSensitiveFileNames(), + readFile: path => info.project.readFile(path), + writeFile: (path, content) => info.project.writeFile(path, content), + fileExists: path => info.project.fileExists(path), + directoryExists: path => info.project.directoryExists(path), + getDirectories: path => info.project.getDirectories(path), + readDirectory: (path, extensions, exclude, include, depth) => info.project.readDirectory(path, extensions, exclude, include, depth), + realpath: info.project.realpath ? path => info.project.realpath!(path) : undefined, + getCompilationSettings: () => info.project.getCompilationSettings(), + getVueCompilationSettings: () => parsed.vueOptions, + getCurrentDirectory: () => info.project.getCurrentDirectory(), + getDefaultLibFileName: () => info.project.getDefaultLibFileName(), + getProjectVersion: () => info.project.getProjectVersion(), + getProjectReferences: () => info.project.getProjectReferences(), + getScriptFileNames: () => info.project.getScriptFileNames(), + getScriptVersion: (fileName) => info.project.getScriptVersion(fileName), + getScriptSnapshot: (fileName) => info.project.getScriptSnapshot(fileName), + getTypeScriptModule: () => ts, + }; + const vueTsLs = vueTs.createLanguageService(vueTsLsHost); - vueFilesGetter.set(info.project, proxyHost.getVueFiles); + tsFaster.decorate(ts, vueTsLsHost, vueTsLs); return new Proxy(info.languageService, { get: (target: any, property: keyof ts.LanguageService) => { @@ -59,208 +81,14 @@ const init: ts.server.PluginModuleFactory = (modules) => { || property === 'getReferencesAtPosition' || property === 'findReferences' ) { - return ls[property]; + return vueTsLs[property]; } return target[property]; }, }); }, - getExternalFiles(project) { - const getVueFiles = vueFilesGetter.get(project); - if (!getVueFiles) { - return []; - } - return getVueFiles().filter(fileName => project.fileExists(fileName)); - }, }; return pluginModule; }; export = init; - -function createProxyHost(ts: typeof import('typescript/lib/tsserverlibrary'), info: ts.server.PluginCreateInfo) { - - let projectVersion = 0; - let reloadVueFilesSeq = 0; - let sendDiagSeq = 0; - let disposed = false; - - const extraFileExtensions: ts.FileExtensionInfo[] = [{ - extension: 'vue', - isMixedContent: true, - scriptKind: ts.ScriptKind.Deferred, - }]; - const vueFiles = new Map(); - const host: vue.LanguageServiceHost = { - getNewLine: () => info.project.getNewLine(), - useCaseSensitiveFileNames: () => info.project.useCaseSensitiveFileNames(), - readFile: path => info.project.readFile(path), - writeFile: (path, content) => info.project.writeFile(path, content), - fileExists: path => info.project.fileExists(path), - directoryExists: path => info.project.directoryExists(path), - getDirectories: path => info.project.getDirectories(path), - readDirectory: (path, extensions, exclude, include, depth) => info.project.readDirectory(path, extensions, exclude, include, depth), - realpath: info.project.realpath ? path => info.project.realpath!(path) : undefined, - - getCompilationSettings: () => info.project.getCompilationSettings(), - getVueCompilationSettings: () => parsedCommandLine?.vueOptions ?? {}, - getCurrentDirectory: () => info.project.getCurrentDirectory(), - getDefaultLibFileName: () => info.project.getDefaultLibFileName(), - getProjectVersion: () => info.project.getProjectVersion() + '-' + projectVersion, - getProjectReferences: () => info.project.getProjectReferences(), - - getScriptFileNames, - getScriptVersion, - getScriptSnapshot, - - getTypeScriptModule: () => ts, - isTsPlugin: true, - }; - - update(); - - const directoryWatcher = info.serverHost.watchDirectory(info.project.getCurrentDirectory(), onAnyDriveFileUpdated, true); - const projectName = info.project.getProjectName(); - - let tsconfigWatcher = info.project.fileExists(projectName) - ? info.serverHost.watchFile(projectName, () => { - onConfigUpdated(); - onProjectUpdated(); - parsedCommandLine = vue.createParsedCommandLine(ts, ts.sys, projectName, extraFileExtensions); - }) - : undefined; - let parsedCommandLine = tsconfigWatcher // reuse fileExists result - ? vue.createParsedCommandLine(ts, ts.sys, projectName, extraFileExtensions) - : undefined; - - return { - host, - getVueFiles: () => [...vueFiles.keys()], - dispose, - }; - - async function onAnyDriveFileUpdated(fileName: string) { - if (fileName.endsWith('.vue') && info.project.fileExists(fileName) && !vueFiles.has(fileName)) { - onConfigUpdated(); - } - } - async function onConfigUpdated() { - const seq = ++reloadVueFilesSeq; - await sleep(100); - if (seq === reloadVueFilesSeq && !disposed) { - update(); - } - } - function getScriptFileNames() { - return info.project.getScriptFileNames().concat([...vueFiles.keys()]); - } - function getScriptVersion(fileName: string) { - if (vueFiles.has(fileName)) { - return vueFiles.get(fileName)!.version.toString(); - } - return info.project.getScriptVersion(fileName); - } - function getScriptSnapshot(fileName: string) { - if (vueFiles.has(fileName)) { - const version = getScriptVersion(fileName); - const file = vueFiles.get(fileName)!; - if (file.snapshotsVersion !== version) { - const text = getScriptText(fileName); - if (text === undefined) return; - file.snapshots = ts.ScriptSnapshot.fromString(text); - file.snapshotsVersion = version; - return file.snapshots; - } - return file.snapshots; - } - return info.project.getScriptSnapshot(fileName); - } - function getScriptText(fileName: string) { - if (info.project.fileExists(fileName)) { - return info.project.readFile(fileName); - } - } - function getVueFiles() { - const parseConfigHost: ts.ParseConfigHost = { - useCaseSensitiveFileNames: info.project.useCaseSensitiveFileNames(), - readDirectory: (path, extensions, exclude, include, depth) => info.project.readDirectory(path, extensions, exclude, include, depth), - fileExists: fileName => info.project.fileExists(fileName), - readFile: fileName => info.project.readFile(fileName), - }; - // fix https://github.com/johnsoncodehk/volar/issues/1276 - // Should use raw tsconfig json not rootDir but seems cannot get it from plugin info - const includeRoot = path.resolve(info.project.getCurrentDirectory(), info.project.getCompilerOptions().rootDir || '.'); - const { fileNames } = ts.parseJsonConfigFileContent({}, parseConfigHost, includeRoot, info.project.getCompilerOptions(), undefined /* TODO: info.project.config.configFilePath? */, undefined, extraFileExtensions); - return fileNames.filter(fileName => extraFileExtensions.some(ext => fileName.endsWith('.' + ext.extension))); - } - function update() { - const newVueFiles = new Set(getVueFiles()); - let changed = false; - for (const fileName of vueFiles.keys()) { - if (!newVueFiles.has(fileName)) { - vueFiles.get(fileName)?.fileWatcher.close(); - vueFiles.delete(fileName); - changed = true; - } - } - for (const fileName of newVueFiles) { - if (!vueFiles.has(fileName)) { - const fileWatcher = info.serverHost.watchFile(fileName, (_, eventKind) => { - if (eventKind === ts.FileWatcherEventKind.Changed) { - onFileChanged(fileName); - } - else if (eventKind === ts.FileWatcherEventKind.Deleted) { - vueFiles.get(fileName)?.fileWatcher.close(); - vueFiles.delete(fileName); - onProjectUpdated(); - } - }); - vueFiles.set(fileName, { - fileWatcher, - version: 0, - snapshots: undefined, - snapshotsVersion: undefined, - }); - changed = true; - } - } - if (changed) { - onProjectUpdated(); - } - } - function onFileChanged(fileName: string) { - fileName = path.resolve(fileName); - const file = vueFiles.get(fileName); - if (file) { - file.version++; - } - onProjectUpdated(); - } - async function onProjectUpdated() { - projectVersion++; - const seq = ++sendDiagSeq; - await sleep(100); - if (seq === sendDiagSeq) { - info.project.refreshDiagnostics(); - } - } - function dispose() { - directoryWatcher.close(); - if (tsconfigWatcher) { - tsconfigWatcher.close(); - } - for (const [_, file] of vueFiles) { - file.fileWatcher.close(); - } - disposed = true; - } -} - -function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -}