diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 2c1a907e8b..4f13a1977d 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -152,6 +152,20 @@ export function baseCreate( const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, id => id, + fileName => { + if (ts.sys.useCaseSensitiveFileNames) { + return host.getScriptFileNames().includes(fileName) ?? false; + } + else { + const lowerFileName = fileName.toLowerCase(); + for (const rootFile of host.getScriptFileNames()) { + if (rootFile.toLowerCase() === lowerFileName) { + return true; + } + } + return false; + } + }, host.getCompilationSettings(), vueCompilerOptions, ); diff --git a/packages/language-core/lib/generators/script.ts b/packages/language-core/lib/generators/script.ts index ecca49d3ce..896764629a 100644 --- a/packages/language-core/lib/generators/script.ts +++ b/packages/language-core/lib/generators/script.ts @@ -437,7 +437,7 @@ type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; let propName = 'modelValue'; if (defineProp.name) { propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - propName = propName.replace(/['"]+/g, '') + propName = propName.replace(/['"]+/g, ''); } yield _(`'update:${propName}': [${propName}:`); if (defineProp.type) { @@ -545,7 +545,7 @@ type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; } yield _(`;\n`); - yield* generateModelEmits() + yield* generateModelEmits(); yield _(`let __VLS_fnPropsDefineComponent!: InstanceType['$props'];\n`); yield _(`let __VLS_fnPropsSlots!: `); @@ -755,7 +755,7 @@ type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; yield _(`};\n`); } - yield* generateModelEmits() + yield* generateModelEmits(); yield* generateTemplate(functional); if (mode === 'return' || mode === 'export') { @@ -856,9 +856,9 @@ type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; } yield _(`},\n`); } - yield _(`emits: ({} as __VLS_NormalizeEmits),\n`); diff --git a/packages/language-core/lib/languageModule.ts b/packages/language-core/lib/languageModule.ts index 26fa4cf10b..58f4735cd2 100644 --- a/packages/language-core/lib/languageModule.ts +++ b/packages/language-core/lib/languageModule.ts @@ -1,24 +1,25 @@ import { forEachEmbeddedCode, type LanguagePlugin } from '@volar/language-core'; import type * as ts from 'typescript'; -import { createPluginContext, getDefaultVueLanguagePlugins } from './plugins'; +import { getDefaultVueLanguagePlugins } from './plugins'; import type { VueCompilerOptions, VueLanguagePlugin } from './types'; -import { resolveVueCompilerOptions } from './utils/ts'; import { VueGeneratedCode } from './virtualFile/vueFile'; +import * as CompilerDOM from '@vue/compiler-dom'; +import * as CompilerVue2 from './utils/vue2TemplateCompiler'; -const fileRegistries: { +const normalFileRegistries: { key: string; plugins: VueLanguagePlugin[]; files: Map; }[] = []; +const holderFileRegistries: typeof normalFileRegistries = []; -function getVueFileRegistry(key: string, plugins: VueLanguagePlugin[]) { - +function getVueFileRegistry(isGlobalTypesHolder: boolean, key: string, plugins: VueLanguagePlugin[]) { + const fileRegistries = isGlobalTypesHolder ? holderFileRegistries : normalFileRegistries; let fileRegistry = fileRegistries.find(r => r.key === key && r.plugins.length === plugins.length && r.plugins.every(plugin => plugins.includes(plugin)) )?.files; - if (!fileRegistry) { fileRegistry = new Map(); fileRegistries.push({ @@ -27,7 +28,6 @@ function getVueFileRegistry(key: string, plugins: VueLanguagePlugin[]) { files: fileRegistry, }); } - return fileRegistry; } @@ -35,10 +35,8 @@ function getFileRegistryKey( compilerOptions: ts.CompilerOptions, vueCompilerOptions: VueCompilerOptions, plugins: ReturnType[], - globalTypesHolder: string | undefined, ) { const values = [ - globalTypesHolder, ...Object.keys(vueCompilerOptions) .sort() .filter(key => key !== 'plugins') @@ -53,21 +51,27 @@ function getFileRegistryKey( export function createVueLanguagePlugin( ts: typeof import('typescript'), getFileName: (fileId: string) => string, - compilerOptions: ts.CompilerOptions = {}, - _vueCompilerOptions: Partial = {}, + isValidGlobalTypesHolder: (fileName: string) => boolean, + compilerOptions: ts.CompilerOptions, + vueCompilerOptions: VueCompilerOptions, codegenStack: boolean = false, - globalTypesHolder?: string ): LanguagePlugin { - - const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); const allowLanguageIds = new Set(['vue']); - const pluginContext = createPluginContext( - ts, + const pluginContext: Parameters[0] = { + modules: { + '@vue/compiler-dom': vueCompilerOptions.target < 3 + ? { + ...CompilerDOM, + compile: CompilerVue2.compile, + } + : CompilerDOM, + typescript: ts, + }, compilerOptions, vueCompilerOptions, codegenStack, - globalTypesHolder, - ); + globalTypesHolder: undefined, + }; const plugins = getDefaultVueLanguagePlugins(pluginContext); if (vueCompilerOptions.extensions.includes('.md')) { @@ -77,63 +81,59 @@ export function createVueLanguagePlugin( allowLanguageIds.add('html'); } - let generatedCodeRegistry: Map | undefined; - return { createVirtualCode(fileId, languageId, snapshot) { if (allowLanguageIds.has(languageId)) { - const fileName = getFileName(fileId); - - if (!generatedCodeRegistry) { - - pluginContext.globalTypesHolder ??= fileName; - - generatedCodeRegistry = getVueFileRegistry( - getFileRegistryKey(compilerOptions, vueCompilerOptions, plugins, pluginContext.globalTypesHolder), - vueCompilerOptions.plugins, - ); + if (!pluginContext.globalTypesHolder && isValidGlobalTypesHolder(fileName)) { + pluginContext.globalTypesHolder = fileName; } - - if (generatedCodeRegistry.has(fileId)) { - const reusedResult = generatedCodeRegistry.get(fileId)!; - reusedResult.update(snapshot); - return reusedResult; + const fileRegistry = getFileRegistry(pluginContext.globalTypesHolder === fileName); + const code = fileRegistry.get(fileId); + if (code) { + code.update(snapshot); + return code; + } + else { + const code = new VueGeneratedCode( + fileName, + languageId, + snapshot, + vueCompilerOptions, + plugins, + ts, + codegenStack, + ); + fileRegistry.set(fileId, code); + return code; } - const vueFile = new VueGeneratedCode(fileName, languageId, snapshot, vueCompilerOptions, plugins, ts, codegenStack); - generatedCodeRegistry.set(fileId, vueFile); - return vueFile; } }, - updateVirtualCode(_fileId, vueFile, snapshot) { - vueFile.update(snapshot); - return vueFile; + updateVirtualCode(_fileId, code, snapshot) { + code.update(snapshot); + return code; }, - disposeVirtualCode(fileId, vueFile, files) { - generatedCodeRegistry?.delete(fileId); - if (vueFile.fileName === pluginContext.globalTypesHolder) { - if (generatedCodeRegistry?.size) { - for (const [fileName, file] of generatedCodeRegistry!) { - pluginContext.globalTypesHolder = fileName; - - generatedCodeRegistry = getVueFileRegistry( - getFileRegistryKey(compilerOptions, vueCompilerOptions, plugins, pluginContext.globalTypesHolder), - vueCompilerOptions.plugins, - ); - + disposeVirtualCode(fileId, code, files) { + const isGlobalTypesHolder = code.fileName === pluginContext.globalTypesHolder; + const fileRegistry = getFileRegistry(isGlobalTypesHolder); + fileRegistry.delete(fileId); + if (isGlobalTypesHolder) { + pluginContext.globalTypesHolder = undefined; + const fileRegistry2 = getFileRegistry(false); + for (const [fileId, code] of fileRegistry2) { + if (isValidGlobalTypesHolder(code.fileName)) { + pluginContext.globalTypesHolder = code.fileName; + fileRegistry2.delete(fileId); + // force dirty + files?.delete(fileId); files?.set( fileId, - file.languageId, - // force dirty - { ...file.snapshot }, + code.languageId, + code.snapshot, ); break; } } - else { - generatedCodeRegistry = undefined; - pluginContext.globalTypesHolder = undefined; - } } }, typescript: { @@ -159,4 +159,12 @@ export function createVueLanguagePlugin( }, }, }; + + function getFileRegistry(isGlobalTypesHolder: boolean) { + return getVueFileRegistry( + isGlobalTypesHolder, + getFileRegistryKey(compilerOptions, vueCompilerOptions, plugins), + vueCompilerOptions.plugins, + ); + } } diff --git a/packages/language-core/lib/plugins.ts b/packages/language-core/lib/plugins.ts index 272af3bb9a..b749297219 100644 --- a/packages/language-core/lib/plugins.ts +++ b/packages/language-core/lib/plugins.ts @@ -1,5 +1,3 @@ -import * as CompilerDOM from '@vue/compiler-dom'; -import type * as ts from 'typescript'; import useHtmlFilePlugin from './plugins/file-html'; import useMdFilePlugin from './plugins/file-md'; import useVueFilePlugin from './plugins/file-vue'; @@ -9,33 +7,7 @@ import useVueSfcStyles from './plugins/vue-sfc-styles'; import useVueSfcTemplate from './plugins/vue-sfc-template'; import useHtmlTemplatePlugin from './plugins/vue-template-html'; import useVueTsx from './plugins/vue-tsx'; -import { pluginVersion, type VueCompilerOptions, type VueLanguagePlugin } from './types'; -import * as CompilerVue2 from './utils/vue2TemplateCompiler'; - -export function createPluginContext( - ts: typeof import('typescript'), - compilerOptions: ts.CompilerOptions, - vueCompilerOptions: VueCompilerOptions, - codegenStack: boolean, - globalTypesHolder: string | undefined, -) { - const pluginCtx: Parameters[0] = { - modules: { - '@vue/compiler-dom': vueCompilerOptions.target < 3 - ? { - ...CompilerDOM, - compile: CompilerVue2.compile, - } - : CompilerDOM, - typescript: ts, - }, - compilerOptions, - vueCompilerOptions, - codegenStack, - globalTypesHolder, - }; - return pluginCtx; -} +import { pluginVersion, type VueLanguagePlugin } from './types'; export function getDefaultVueLanguagePlugins(pluginContext: Parameters[0]) { diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 4c8ff8a98f..a338da0c21 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -42,7 +42,27 @@ connection.onInitialize(async params => { async getLanguagePlugins(serviceEnv, projectContext) { const [commandLine, vueOptions] = await parseCommandLine(); const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); - const vueLanguagePlugin = createVueLanguagePlugin(tsdk.typescript, serviceEnv.typescript!.uriToFileName, commandLine?.options ?? {}, resolvedVueOptions, options.codegenStack); + const vueLanguagePlugin = createVueLanguagePlugin( + tsdk.typescript, + serviceEnv.typescript!.uriToFileName, + fileName => { + if (projectContext.typescript?.sys.useCaseSensitiveFileNames ?? false) { + return projectContext.typescript?.host.getScriptFileNames().includes(fileName) ?? false; + } + else { + const lowerFileName = fileName.toLowerCase(); + for (const rootFile of projectContext.typescript?.host.getScriptFileNames() ?? []) { + if (rootFile.toLowerCase() === lowerFileName) { + return true; + } + } + return false; + } + }, + commandLine?.options ?? {}, + resolvedVueOptions, + options.codegenStack, + ); envToVueOptions.set(serviceEnv, resolvedVueOptions); diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index f855c9f544..07fc9ab6fe 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -27,7 +27,26 @@ function createTester(rootUri: string) { getLanguageId: resolveCommonLanguageId, }; const resolvedVueOptions = resolveVueCompilerOptions(parsedCommandLine.vueOptions); - const vueLanguagePlugin = createVueLanguagePlugin(ts, serviceEnv.typescript!.uriToFileName, parsedCommandLine.options, resolvedVueOptions); + const vueLanguagePlugin = createVueLanguagePlugin( + ts, + serviceEnv.typescript!.uriToFileName, + fileName => { + if (ts.sys.useCaseSensitiveFileNames) { + return projectHost.getScriptFileNames().includes(fileName); + } + else { + const lowerFileName = fileName.toLowerCase(); + for (const rootFile of projectHost.getScriptFileNames()) { + if (rootFile.toLowerCase() === lowerFileName) { + return true; + } + } + return false; + } + }, + parsedCommandLine.options, + resolvedVueOptions, + ); const vueServicePlugins = createVueServicePlugins(ts, () => resolvedVueOptions); const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', diff --git a/packages/language-service/tests/utils/format.ts b/packages/language-service/tests/utils/format.ts index dd9a1b2007..d1b5242766 100644 --- a/packages/language-service/tests/utils/format.ts +++ b/packages/language-service/tests/utils/format.ts @@ -4,7 +4,13 @@ import { describe, expect, it } from 'vitest'; import { createVueLanguagePlugin, createVueServicePlugins, resolveVueCompilerOptions } from '../..'; const resolvedVueOptions = resolveVueCompilerOptions({}); -const vueLanguagePlugin = createVueLanguagePlugin(ts, fileId => formatter.env.typescript!.uriToFileName(fileId), {}, resolvedVueOptions); +const vueLanguagePlugin = createVueLanguagePlugin( + ts, + fileId => formatter.env.typescript!.uriToFileName(fileId), + () => false, + {}, + resolvedVueOptions, +); const vueServicePLugins = createVueServicePlugins(ts, () => resolvedVueOptions); const formatter = kit.createFormatter([vueLanguagePlugin], vueServicePLugins); diff --git a/packages/tsc/index.ts b/packages/tsc/index.ts index 4b0f79edd6..3b498fc9e6 100644 --- a/packages/tsc/index.ts +++ b/packages/tsc/index.ts @@ -2,11 +2,12 @@ import { runTsc } from '@volar/typescript/lib/quickstart/runTsc'; import * as vue from '@vue/language-core'; import type * as ts from 'typescript'; +const windowsPathReg = /\\/g; + export function run() { let runExtensions = ['.vue']; - const windowsPathReg = /\\/g; const extensionsChangedException = new Error('extensions changed'); const main = () => runTsc( require.resolve('typescript/lib/tsc'), @@ -16,7 +17,9 @@ export function run() { const vueOptions = typeof configFilePath === 'string' ? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions : {}; - const extensions = vueOptions.extensions ?? ['.vue']; + const resolvedVueOptions = vue.resolveVueCompilerOptions(vueOptions); + const { extensions } = resolvedVueOptions; + const fakeGlobalTypesHolder = createFakeGlobalTypesHolder(options); if ( runExtensions.length === extensions.length && runExtensions.every(ext => extensions.includes(ext)) @@ -24,10 +27,10 @@ export function run() { const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, id => id, + fileName => fileName === fakeGlobalTypesHolder, options.options, - vueOptions, + resolvedVueOptions, false, - createFakeGlobalTypesHolder(options)?.replace(windowsPathReg, '/'), ); return [vueLanguagePlugin]; } @@ -77,6 +80,6 @@ export function createFakeGlobalTypesHolder(options: ts.CreateProgramOptions) { return writeFile(fileName, ...args); }; - return fakeFileName; + return fakeFileName.replace(windowsPathReg, '/'); } } diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index 4caec8dbf1..3cf31c02e7 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -33,10 +33,10 @@ describe('vue-tsc-dts', () => { const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, id => id, + fileName => fileName === fakeGlobalTypesHolder, options.options, - vueOptions, + vue.resolveVueCompilerOptions(vueOptions), false, - fakeGlobalTypesHolder?.replace(windowsPathReg, '/'), ); return [vueLanguagePlugin]; }); @@ -44,7 +44,7 @@ describe('vue-tsc-dts', () => { for (const intputFile of options.rootNames) { - if (intputFile === fakeGlobalTypesHolder) + if (intputFile.endsWith('__VLS_globalTypes.vue')) continue; const expectedOutputFile = intputFile.endsWith('.ts') diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 96325be49d..702b2839e8 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -9,7 +9,7 @@ import { _getComponentNames } from './lib/requests/componentInfos'; import { capitalize } from '@vue/shared'; const windowsPathReg = /\\/g; -const externalFiles = new WeakMap(); +const externalFiles = new WeakMap>(); const projectExternalFileExtensions = new WeakMap(); const decoratedLanguageServices = new WeakSet(); const decoratedLanguageServiceHosts = new WeakSet(); @@ -32,6 +32,20 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { const languagePlugin = vue.createVueLanguagePlugin( ts, id => id, + fileName => { + if (info.languageServiceHost.useCaseSensitiveFileNames?.() ?? false) { + return externalFiles.get(info.project)?.has(fileName) ?? false; + } + else { + const lowerFileName = fileName.toLowerCase(); + for (const externalFile of externalFiles.get(info.project) ?? []) { + if (externalFile.toLowerCase() === lowerFileName) { + return true; + } + } + return false; + } + }, info.languageServiceHost.getCompilationSettings(), vueOptions, ); @@ -204,26 +218,30 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { || !externalFiles.has(project) ) { const oldFiles = externalFiles.get(project); - const newFiles = searchExternalFiles(ts, project, projectExternalFileExtensions.get(project)!); + const newFiles = new Set(searchExternalFiles(ts, project, projectExternalFileExtensions.get(project)!)); externalFiles.set(project, newFiles); - if (oldFiles && !arrayItemsEqual(oldFiles, newFiles)) { + if (oldFiles && !twoSetsEqual(oldFiles, newFiles)) { + for (const oldFile of oldFiles) { + if (!newFiles.has(oldFile)) { + projects.get(project)?.files.delete(oldFile); + } + } project.refreshDiagnostics(); } } - return externalFiles.get(project)!; + return [...externalFiles.get(project)!]; }, }; return pluginModule; }; } -function arrayItemsEqual(a: string[], b: string[]) { - if (a.length !== b.length) { +function twoSetsEqual(a: Set, b: Set) { + if (a.size !== b.size) { return false; } - const set = new Set(a); - for (const file of b) { - if (!set.has(file)) { + for (const file of a) { + if (!b.has(file)) { return false; } }