From b3a0cb2cc1b920598a4fc0a1795f5152355259db Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 17 Nov 2023 05:54:22 +0800 Subject: [PATCH 01/19] updates --- packages/component-meta/src/base.ts | 89 ++++++++------- packages/language-core/src/languageModule.ts | 6 +- .../src/virtualFile/computedFiles.ts | 5 +- .../language-core/src/virtualFile/vueFile.ts | 1 + .../src/languageServerPlugin.ts | 78 +++++++------ packages/language-server/src/nodeServer.ts | 4 +- packages/language-server/src/webServer.ts | 4 +- .../src/ideFeatures/nameCasing.ts | 4 +- .../language-service/src/languageService.ts | 43 ++++---- .../src/plugins/vue-codelens-references.ts | 2 +- packages/language-service/tests/complete.ts | 2 +- .../language-service/tests/findDefinition.ts | 4 +- packages/language-service/tests/reference.ts | 2 +- packages/language-service/tests/rename.ts | 2 +- .../tests/utils/createTester.ts | 104 +++--------------- .../language-service/tests/utils/format.ts | 10 +- .../language-service/tests/utils/mockEnv.ts | 87 +++++++++++++++ packages/tsc-eslint-hook/src/index.ts | 8 +- packages/tsc/src/index.ts | 33 +++--- packages/typescript-plugin/src/index.ts | 7 +- 20 files changed, 254 insertions(+), 241 deletions(-) create mode 100644 packages/language-service/tests/utils/mockEnv.ts diff --git a/packages/component-meta/src/base.ts b/packages/component-meta/src/base.ts index c2ab232db9..82acc2c724 100644 --- a/packages/component-meta/src/base.ts +++ b/packages/component-meta/src/base.ts @@ -33,6 +33,7 @@ export function createCheckerByJsonBase( checkerOptions, rootPath, path.join(rootPath, 'jsconfig.json.global.vue'), + undefined, ); } @@ -48,6 +49,7 @@ export function createCheckerBase( checkerOptions, path.dirname(tsconfig), tsconfig + '.global.vue', + tsconfig, ); } @@ -57,6 +59,7 @@ function createCheckerWorker( checkerOptions: MetaCheckerOptions, rootPath: string, globalComponentName: string, + configFileName: string | undefined, ) { /** @@ -68,9 +71,9 @@ function createCheckerWorker( let projectVersion = 0; const scriptSnapshots = new Map(); - const _host: vue.TypeScriptLanguageHost = { - workspacePath: rootPath, - rootPath: rootPath, + const _host: vue.TypeScriptProjectHost = { + configFileName, + getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => parsedCommandLine.options, getScriptFileNames: () => fileNames, @@ -112,55 +115,49 @@ function createCheckerWorker( export function baseCreate( ts: typeof import('typescript/lib/tsserverlibrary'), - _host: vue.TypeScriptLanguageHost, + host: vue.TypeScriptProjectHost, vueCompilerOptions: vue.VueCompilerOptions, checkerOptions: MetaCheckerOptions, globalComponentName: string, ) { const globalComponentSnapshot = ts.ScriptSnapshot.fromString(''); const metaSnapshots: Record = {}; - const host = new Proxy>({ - getScriptFileNames: () => { - const names = _host.getScriptFileNames(); - return [ - ...names, - ...names.map(getMetaFileName), - globalComponentName, - getMetaFileName(globalComponentName), - ]; - }, - getScriptSnapshot: fileName => { - if (isMetaFileName(fileName)) { - if (!metaSnapshots[fileName]) { - metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); - } - return metaSnapshots[fileName]; - } - else if (fileName === globalComponentName) { - return globalComponentSnapshot; - } - else { - return _host.getScriptSnapshot(fileName); - } - }, - }, { - get(target, prop) { - if (prop in target) { - return target[prop as keyof typeof target]; + const getScriptFileNames = host.getScriptFileNames; + const getScriptSnapshot = host.getScriptSnapshot; + host.getScriptFileNames = () => { + const names = getScriptFileNames(); + return [ + ...names, + ...names.map(getMetaFileName), + globalComponentName, + getMetaFileName(globalComponentName), + ]; + }; + host.getScriptSnapshot = (fileName) => { + if (isMetaFileName(fileName)) { + if (!metaSnapshots[fileName]) { + metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); } - return _host[prop as keyof typeof _host]; - }, - }) as vue.TypeScriptLanguageHost; + return metaSnapshots[fileName]; + } + else if (fileName === globalComponentName) { + return globalComponentSnapshot; + } + else { + return getScriptSnapshot(fileName); + } + }; + const vueLanguages = vue.createLanguages( ts, host.getCompilationSettings(), vueCompilerOptions, ); - const core = vue.createLanguageContext(host, vueLanguages); - const tsLsHost = createLanguageServiceHost(core, ts, ts.sys); + const project = vue.createTypeScriptProject(host, vueLanguages, vue.resolveCommonLanguageId); + const tsLsHost = createLanguageServiceHost(project.typescript!.projectHost, project.fileProvider, ts, ts.sys); const tsLs = ts.createLanguageService(tsLsHost); - decorateLanguageService(core.virtualFiles, tsLs, false); + decorateLanguageService(project.fileProvider, tsLs, false); if (checkerOptions.forceUseTs) { const getScriptKind = tsLsHost.getScriptKind; @@ -281,7 +278,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} .map((prop) => { const { resolveNestedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); return resolveNestedProperties(prop); }) @@ -300,7 +297,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const printer = ts.createPrinter(checkerOptions.printer); const snapshot = host.getScriptSnapshot(componentPath)!; - const vueSourceFile = core.virtualFiles.getSource(componentPath)?.root; + const vueSourceFile = project.fileProvider.getSource(componentPath)?.root; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; @@ -345,7 +342,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const { resolveEventSignature, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); return resolveEventSignature(call); }).filter(event => event.name); @@ -365,7 +362,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveSlotProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); return resolveSlotProperties(prop); }); @@ -388,7 +385,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveExposedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); return resolveExposedProperties(prop); }); @@ -447,7 +444,7 @@ function createSchemaResolvers( symbolNode: ts.Expression, { rawType, schema: options, noDeclarations }: MetaCheckerOptions, ts: typeof import('typescript/lib/tsserverlibrary'), - core: vue.LanguageContext, + core: vue.Project, ) { const visited = new Set(); @@ -638,9 +635,9 @@ function createSchemaResolvers( } function getDeclaration(declaration: ts.Declaration): Declaration | undefined { const fileName = declaration.getSourceFile().fileName; - const [virtualFile] = core.virtualFiles.getVirtualFile(fileName); + const [virtualFile] = core.fileProvider.getVirtualFile(fileName); if (virtualFile) { - const maps = core.virtualFiles.getMaps(virtualFile); + const maps = core.fileProvider.getMaps(virtualFile); for (const [source, [_, map]] of maps) { const start = map.toSourceOffset(declaration.getStart()); const end = map.toSourceOffset(declaration.getEnd()); diff --git a/packages/language-core/src/languageModule.ts b/packages/language-core/src/languageModule.ts index dcf477b78d..975a88992b 100644 --- a/packages/language-core/src/languageModule.ts +++ b/packages/language-core/src/languageModule.ts @@ -78,7 +78,7 @@ export function createVueLanguage( reusedVueFile.update(snapshot); return reusedVueFile; } - const vueFile = new VueFile(fileName, snapshot, vueCompilerOptions, plugins, ts, codegenStack); + const vueFile = new VueFile(fileName, languageId, snapshot, vueCompilerOptions, plugins, ts, codegenStack); fileRegistry.set(fileName, vueFile); return vueFile; } @@ -86,9 +86,9 @@ export function createVueLanguage( updateVirtualFile(sourceFile, snapshot) { sourceFile.update(snapshot); }, - resolveHost(host) { + resolveTypeScriptProjectHost(host) { const sharedTypesSnapshot = ts.ScriptSnapshot.fromString(sharedTypes.getTypesCode(vueCompilerOptions)); - const sharedTypesFileName = path.join(host.rootPath, sharedTypes.baseName); + const sharedTypesFileName = path.join(host.getCurrentDirectory(), sharedTypes.baseName); return { ...host, resolveModuleName(moduleName, impliedNodeFormat) { diff --git a/packages/language-core/src/virtualFile/computedFiles.ts b/packages/language-core/src/virtualFile/computedFiles.ts index 8a6d4fbc8e..c6827ea754 100644 --- a/packages/language-core/src/virtualFile/computedFiles.ts +++ b/packages/language-core/src/virtualFile/computedFiles.ts @@ -1,4 +1,4 @@ -import { VirtualFile } from '@volar/language-core'; +import { VirtualFile, resolveCommonLanguageId } from '@volar/language-core'; import { buildMappings, buildStacks, toString } from '@volar/source-map'; import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; @@ -51,6 +51,7 @@ export function computedFiles( for (const { file, snapshot, mappings, codegenStacks } of remain) { embeddedFiles.push({ ...file, + languageId: resolveCommonLanguageId(file.fileName), snapshot, mappings, codegenStacks, @@ -67,6 +68,7 @@ export function computedFiles( if (!file.parentFileName) { embeddedFiles.push({ ...file, + languageId: resolveCommonLanguageId(file.fileName), snapshot, mappings, codegenStacks, @@ -79,6 +81,7 @@ export function computedFiles( if (parent) { parent.embeddedFiles.push({ ...file, + languageId: resolveCommonLanguageId(file.fileName), snapshot, mappings, codegenStacks, diff --git a/packages/language-core/src/virtualFile/vueFile.ts b/packages/language-core/src/virtualFile/vueFile.ts index c0c0ab48fb..b1f027a65b 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -49,6 +49,7 @@ export class VueFile implements VirtualFile { constructor( public fileName: string, + public languageId: string, public initSnapshot: ts.IScriptSnapshot, public vueCompilerOptions: VueCompilerOptions, public plugins: ReturnType[], diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 7ae55990d5..27968b3d41 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -1,5 +1,4 @@ -import * as embedded from '@volar/language-core'; -import { LanguageServerPlugin, Connection } from '@volar/language-server'; +import { TypeScriptServerPlugin, Connection, ServerProject } from '@volar/language-server'; import * as vue from '@vue/language-service'; import * as vue2 from '@vue/language-core'; import * as nameCasing from '@vue/language-service'; @@ -12,19 +11,20 @@ import { createSys } from '@volar/typescript'; export function createServerPlugin(connection: Connection) { - const plugin: LanguageServerPlugin = (initOptions: VueServerInitializationOptions, modules): ReturnType => { + const plugin: TypeScriptServerPlugin = ({ initializationOptions, modules }): ReturnType => { if (!modules.typescript) { console.warn('No typescript found, vue-language-server will not work.'); return {}; } + const options: VueServerInitializationOptions = initializationOptions; const ts = modules.typescript; const vueFileExtensions: string[] = ['vue']; - const hostToVueOptions = new WeakMap(); + const envToVueOptions = new WeakMap(); - if (initOptions.additionalExtensions) { - for (const additionalExtension of initOptions.additionalExtensions) { + if (options.additionalExtensions) { + for (const additionalExtension of options.additionalExtensions) { vueFileExtensions.push(additionalExtension); } } @@ -32,40 +32,35 @@ export function createServerPlugin(connection: Connection) { return { extraFileExtensions: vueFileExtensions.map(ext => ({ extension: ext, isMixedContent: true, scriptKind: ts.ScriptKind.Deferred })), watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], - async resolveConfig(config, ctx) { + async resolveConfig(config, env, projectHost) { const vueOptions = await getVueCompilerOptions(); - if (ctx) { - hostToVueOptions.set(ctx.host, vue.resolveVueCompilerOptions(vueOptions)); + if (env) { + envToVueOptions.set(env, vue.resolveVueCompilerOptions(vueOptions)); } - return vue.resolveConfig( - ts, - config, - ctx?.host.getCompilationSettings() ?? {}, - vueOptions, - initOptions.codegenStack, - ); + config.languages = vue.resolveLanguages(ts, config.languages ?? {}, projectHost?.getCompilationSettings() ?? {}, vueOptions, options.codegenStack); + config.services = vue.resolveServices(config.services ?? {}, vueOptions); - async function getVueCompilerOptions() { + return config; - const ts = modules.typescript; + async function getVueCompilerOptions() { let vueOptions: Partial = {}; - if (ts && ctx) { - const sys = createSys(ts, ctx.env); + if (env && projectHost) { + const sys = createSys(ts, env, env.uriToFileName(env.workspaceFolder.uri.toString())); let sysVersion: number | undefined; let newSysVersion = await sys.sync(); while (sysVersion !== newSysVersion) { sysVersion = newSysVersion; - if (typeof ctx?.project.tsConfig === 'string' && ts) { - vueOptions = vue2.createParsedCommandLine(ts, sys, ctx.project.tsConfig).vueOptions; + if (projectHost.configFileName) { + vueOptions = vue2.createParsedCommandLine(ts, sys, projectHost.configFileName).vueOptions; } - else if (typeof ctx?.project.tsConfig === 'object' && ts) { - vueOptions = vue2.createParsedCommandLineByJson(ts, sys, ctx.host.rootPath, ctx.project.tsConfig).vueOptions; + else { + vueOptions = vue2.createParsedCommandLineByJson(ts, sys, projectHost.getCurrentDirectory(), projectHost.getCompilationSettings()).vueOptions; } newSysVersion = await sys.sync(); } @@ -80,7 +75,7 @@ export function createServerPlugin(connection: Connection) { return vueOptions; } }, - onInitialized(getService, env) { + onInitialized(projects) { connection.onRequest(ParseSFCRequest.type, params => { return vue2.parse(params); @@ -89,14 +84,14 @@ export function createServerPlugin(connection: Connection) { connection.onRequest(DetectNameCasingRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return nameCasing.detect(ts, languageService.context, params.textDocument.uri, hostToVueOptions.get(languageService.context.rawHost)!); + return nameCasing.detect(ts, languageService.context, params.textDocument.uri, envToVueOptions.get(languageService.context.env)!); } }); connection.onRequest(GetConvertTagCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return nameCasing.convertTagName(ts, languageService.context, params.textDocument.uri, params.casing, hostToVueOptions.get(languageService.context.rawHost)!); + return nameCasing.convertTagName(ts, languageService.context, params.textDocument.uri, params.casing, envToVueOptions.get(languageService.context.env)!); } }); @@ -110,36 +105,37 @@ export function createServerPlugin(connection: Connection) { connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - const vueOptions = hostToVueOptions.get(languageService.context.host); + const vueOptions = envToVueOptions.get(languageService.context.env); if (vueOptions) { - return nameCasing.convertAttrName(ts, languageService.context, params.textDocument.uri, params.casing, hostToVueOptions.get(languageService.context.rawHost)!); + return nameCasing.convertAttrName(ts, languageService.context, params.textDocument.uri, params.casing, envToVueOptions.get(languageService.context.env)!); } } }); - const checkers = new WeakMap(); + const checkers = new WeakMap(); connection.onRequest(GetComponentMeta.type, async params => { - const languageService = await getService(params.uri); - if (!languageService) - return; - - const host = languageService.context.rawHost; + const project = await projects.getProject(params.uri); + const langaugeService = project.getLanguageService(); - let checker = checkers.get(host); + let checker = checkers.get(project); if (!checker) { checker = componentMeta.baseCreate( ts, - host, - hostToVueOptions.get(host)!, + langaugeService.context.project.typescript!.projectHost, + envToVueOptions.get(langaugeService.context.env)!, {}, - host.rootPath + '/tsconfig.json.global.vue', + langaugeService.context.project.typescript!.projectHost.getCurrentDirectory() + '/tsconfig.json.global.vue', ); - checkers.set(host, checker); + checkers.set(project, checker); } - return checker.getComponentMeta(env.uriToFileName(params.uri)); + return checker.getComponentMeta(langaugeService.context.env.uriToFileName(params.uri)); }); + + async function getService(uri: string) { + return (await projects.getProject(uri)).getLanguageService(); + } }, }; }; diff --git a/packages/language-server/src/nodeServer.ts b/packages/language-server/src/nodeServer.ts index f1b9409fdd..25654597a9 100644 --- a/packages/language-server/src/nodeServer.ts +++ b/packages/language-server/src/nodeServer.ts @@ -1,7 +1,7 @@ -import { createConnection, startLanguageServer } from '@volar/language-server/node'; +import { createConnection, startTypeScriptServer } from '@volar/language-server/node'; import { createServerPlugin } from './languageServerPlugin'; const connection = createConnection(); const plugin = createServerPlugin(connection); -startLanguageServer(connection, plugin); +startTypeScriptServer(connection, plugin); diff --git a/packages/language-server/src/webServer.ts b/packages/language-server/src/webServer.ts index d64a772dc3..35f1b53e35 100644 --- a/packages/language-server/src/webServer.ts +++ b/packages/language-server/src/webServer.ts @@ -1,7 +1,7 @@ -import { createConnection, startLanguageServer } from '@volar/language-server/browser'; +import { createConnection, startTypeScriptServer } from '@volar/language-server/browser'; import { createServerPlugin } from './languageServerPlugin'; const connection = createConnection(); const plugin = createServerPlugin(connection); -startLanguageServer(connection, plugin); +startTypeScriptServer(connection, plugin); diff --git a/packages/language-service/src/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index d64264b907..516ffff682 100644 --- a/packages/language-service/src/ideFeatures/nameCasing.ts +++ b/packages/language-service/src/ideFeatures/nameCasing.ts @@ -23,7 +23,7 @@ export async function convertTagName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName, rootFile.languageId); const edits: vscode.TextEdit[] = []; const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); @@ -66,7 +66,7 @@ export async function convertAttrName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName, rootFile.languageId); const edits: vscode.TextEdit[] = []; const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 5b5a92a1da..9af52c2f83 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -1,5 +1,5 @@ -import { Config, Service, ServiceContext } from '@volar/language-service'; -import { VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; +import { Service, ServiceContext } from '@volar/language-service'; +import { Language, VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; import { capitalize } from '@vue/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Data } from 'volar-service-typescript/out/features/completions/basic'; @@ -35,34 +35,39 @@ export interface Settings { json?: Parameters[0]; } -export function resolveConfig( +export function resolveLanguages( ts: typeof import('typescript/lib/tsserverlibrary'), - config: Config, + languages: Record = {}, compilerOptions: ts.CompilerOptions = {}, - vueCompilerOptions: Partial = {}, + _vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -) { - - const resolvedVueCompilerOptions = resolveVueCompilerOptions(vueCompilerOptions); - const vueLanguageModules = createLanguages(ts, compilerOptions, resolvedVueCompilerOptions, codegenStack); +): Record { - config.languages = Object.assign({}, vueLanguageModules, config.languages); - config.services = resolvePlugins(config.services, resolvedVueCompilerOptions); + const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); + const vueLanguageModules = createLanguages(ts, compilerOptions, vueCompilerOptions, codegenStack); - return config; + return { + ...languages, + ...vueLanguageModules.reduce((obj, module, i) => { + obj['vue_' + i] = module; + return obj; + }, {} as Record), + }; } -function resolvePlugins( - services: Config['services'], - vueCompilerOptions: VueCompilerOptions, +export function resolveServices( + services: Record = {}, + _vueCompilerOptions: Partial = {}, ) { - const originalTsPlugin: Service = services?.typescript ?? TsService.create(); + const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); + + const _tsPlugin: Service = services?.typescript ?? TsService.create(); services ??= {}; services.typescript = (ctx: ServiceContext | undefined, modules): ReturnType => { - const base = typeof originalTsPlugin === 'function' ? originalTsPlugin(ctx, modules) : originalTsPlugin; + const base = typeof _tsPlugin === 'function' ? _tsPlugin(ctx, modules) : _tsPlugin; if (!ctx || !modules?.typescript) return base; @@ -164,14 +169,14 @@ function resolvePlugins( if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { const fileName = ctx.env.uriToFileName(itemData.uri); const langaugeService = ctx.inject('typescript/languageService'); - const [virtualFile] = ctx.virtualFiles.getVirtualFile(fileName); + const [virtualFile] = ctx.project.fileProvider.getVirtualFile(fileName); const ast = langaugeService.getProgram()?.getSourceFile(fileName); const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined; if (virtualFile && ast && exportDefault) { const componentName = newName ?? item.textEdit.newText; const optionEdit = ExtractComponentService.createAddComponentToOptionEdit(ts, ast, componentName); if (optionEdit) { - const textDoc = ctx.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName); + const textDoc = ctx.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName, virtualFile.languageId); item.additionalTextEdits.push({ range: { start: textDoc.positionAt(optionEdit.range.start), diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index 4badb080b4..e8a247335d 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -38,7 +38,7 @@ export const create = function (): Service { await worker(document.uri, async (vueFile) => { - const document = context.documents.getDocumentByFileName(vueFile.snapshot, vueFile.fileName); + const document = context.documents.getDocumentByFileName(vueFile.snapshot, vueFile.fileName, vueFile.languageId); const offset = document.offsetAt(range.start); const blocks = [ vueFile.sfc.script, diff --git a/packages/language-service/tests/complete.ts b/packages/language-service/tests/complete.ts index fecc37f744..a4cf530ecc 100644 --- a/packages/language-service/tests/complete.ts +++ b/packages/language-service/tests/complete.ts @@ -20,7 +20,7 @@ for (const dirName of testDirs) { for (const file in inputFiles) { const filePath = path.join(dir, 'input', file); - const uri = tester.fileNameToUri(filePath); + const uri = tester.serviceEnv.fileNameToUri(filePath); const fileText = inputFiles[file]; const document = TextDocument.create('', '', 0, fileText); const actions = findCompleteActions(fileText); diff --git a/packages/language-service/tests/findDefinition.ts b/packages/language-service/tests/findDefinition.ts index b537b415f7..5fa6e7a150 100644 --- a/packages/language-service/tests/findDefinition.ts +++ b/packages/language-service/tests/findDefinition.ts @@ -17,7 +17,7 @@ for (const dirName of testDirs) { for (const file in inputFiles) { const filePath = path.join(dir, file); - const uri = tester.fileNameToUri(filePath); + const uri = tester.serviceEnv.fileNameToUri(filePath); const fileText = inputFiles[file]; const document = TextDocument.create('', '', 0, fileText); const actions = findActions(fileText); @@ -41,7 +41,7 @@ for (const dirName of testDirs) { expect(locations).toBeDefined(); const location = locations?.find(loc => - loc.targetUri === tester.fileNameToUri(targetFile) + loc.targetUri === tester.serviceEnv.fileNameToUri(targetFile) && targetDocument.offsetAt(loc.targetSelectionRange.start) === action.targeRange.start && targetDocument.offsetAt(loc.targetSelectionRange.end) === action.targeRange.end ); diff --git a/packages/language-service/tests/reference.ts b/packages/language-service/tests/reference.ts index 625f1f26be..9ebf7c56c4 100644 --- a/packages/language-service/tests/reference.ts +++ b/packages/language-service/tests/reference.ts @@ -17,7 +17,7 @@ for (const dirName of testDirs) { for (const file in inputFiles) { const filePath = path.join(dir, file); - const uri = tester.fileNameToUri(filePath); + const uri = tester.serviceEnv.fileNameToUri(filePath); const fileText = inputFiles[file]; const document = TextDocument.create('', '', 0, fileText); const actions = findActions(fileText); diff --git a/packages/language-service/tests/rename.ts b/packages/language-service/tests/rename.ts index fe9a0dd19b..79d6ec0ff8 100644 --- a/packages/language-service/tests/rename.ts +++ b/packages/language-service/tests/rename.ts @@ -18,7 +18,7 @@ for (const dirName of testDirs) { for (const file in inputFiles) { const filePath = path.join(dir, 'input', file); - const uri = tester.fileNameToUri(filePath); + const uri = tester.serviceEnv.fileNameToUri(filePath); const fileText = inputFiles[file]; const document = TextDocument.create('', '', 0, fileText); const actions = findRenameActions(fileText); diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 03001d0556..e69974ce37 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -1,12 +1,10 @@ -import { FileType, TypeScriptLanguageHost, createLanguageService } from '@volar/language-service'; -import * as fs from 'fs'; +import { TypeScriptProjectHost, createLanguageService, createTypeScriptProject, resolveCommonLanguageId } from '@volar/language-service'; import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { URI } from 'vscode-uri'; -import { resolveConfig } from '../../out'; +import { createParsedCommandLine, resolveLanguages, resolveServices } from '../../out'; +import { createMockServiceEnv } from './mockEnv'; -const uriToFileName = (uri: string) => URI.parse(uri).fsPath.replace(/\\/g, '/'); -const fileNameToUri = (fileName: string) => URI.file(fileName).toString(); const testRoot = path.resolve(__dirname, '../../../../test-workspace/language-service').replace(/\\/g, '/'); export const rootUri = URI.file(testRoot); @@ -16,105 +14,31 @@ function createTester(root: string) { const ts = require('typescript') as typeof import('typescript/lib/tsserverlibrary'); const realTsConfig = path.join(root, 'tsconfig.json').replace(/\\/g, '/'); - const config = ts.readJsonConfigFile(realTsConfig, ts.sys.readFile); - const parsedCommandLine = ts.parseJsonSourceFileConfigFileContent(config, ts.sys, path.dirname(realTsConfig), {}, realTsConfig, undefined, [{ extension: 'vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }]); + const parsedCommandLine = createParsedCommandLine(ts, ts.sys, realTsConfig); parsedCommandLine.fileNames = parsedCommandLine.fileNames.map(fileName => fileName.replace(/\\/g, '/')); const scriptSnapshots = new Map(); - const host: TypeScriptLanguageHost = { - workspacePath: root, - rootPath: root, + const projectHost: TypeScriptProjectHost = { + configFileName: realTsConfig, + getCurrentDirectory: () => root, getProjectVersion: () => '0', getScriptFileNames: () => parsedCommandLine.fileNames, getCompilationSettings: () => parsedCommandLine.options, getScriptSnapshot, }; + const languages = resolveLanguages(ts, {}, parsedCommandLine.options, parsedCommandLine.vueOptions); + const services = resolveServices({}, parsedCommandLine.vueOptions); + const project = createTypeScriptProject(projectHost, Object.values(languages), resolveCommonLanguageId); const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', 'javascript.preferences.quoteStyle': 'single', }; let currentVSCodeSettings: any; - const languageService = createLanguageService( - { typescript: ts as any }, - { - workspaceUri: rootUri, - rootUri, - uriToFileName, - fileNameToUri, - async getConfiguration(section: string) { - const settings = currentVSCodeSettings ?? defaultVSCodeSettings; - if (settings[section]) { - return settings[section]; - } - let result: Record | undefined; - for (const key in settings) { - if (key.startsWith(section + '.')) { - const newKey = key.slice(section.length + 1); - result ??= {}; - result[newKey] = settings[key]; - } - } - return result; - }, - fs: { - stat(uri) { - if (uri.startsWith('file://')) { - try { - const stats = fs.statSync(uriToFileName(uri), { throwIfNoEntry: false }); - if (stats) { - return { - type: stats.isFile() ? FileType.File - : stats.isDirectory() ? FileType.Directory - : stats.isSymbolicLink() ? FileType.SymbolicLink - : FileType.Unknown, - ctime: stats.ctimeMs, - mtime: stats.mtimeMs, - size: stats.size, - }; - } - } - catch { - return undefined; - } - } - }, - readFile(uri, encoding) { - if (uri.startsWith('file://')) { - try { - return fs.readFileSync(uriToFileName(uri), { encoding: encoding as 'utf-8' ?? 'utf-8' }); - } - catch { - return undefined; - } - } - }, - readDirectory(uri) { - if (uri.startsWith('file://')) { - try { - const dirName = uriToFileName(uri); - const files = fs.readdirSync(dirName, { withFileTypes: true }); - return files.map<[string, FileType]>(file => { - return [file.name, file.isFile() ? FileType.File - : file.isDirectory() ? FileType.Directory - : file.isSymbolicLink() ? FileType.SymbolicLink - : FileType.Unknown]; - }); - } - catch { - return []; - } - } - return []; - }, - } - }, - resolveConfig(ts, {}), - host, - ); + const serviceEnv = createMockServiceEnv(rootUri, () => currentVSCodeSettings ?? defaultVSCodeSettings); + const languageService = createLanguageService({ typescript: ts as any }, Object.values(services), serviceEnv, project); return { - uriToFileName, - fileNameToUri, - host, + serviceEnv, + projectHost, languageService, setVSCodeSettings, }; diff --git a/packages/language-service/tests/utils/format.ts b/packages/language-service/tests/utils/format.ts index 3757ac4f46..e5aa604c61 100644 --- a/packages/language-service/tests/utils/format.ts +++ b/packages/language-service/tests/utils/format.ts @@ -1,9 +1,10 @@ -import { describe, expect, it } from 'vitest'; import * as kit from '@volar/kit'; -import { resolveConfig } from '../../out'; import * as ts from 'typescript'; +import { describe, expect, it } from 'vitest'; +import { resolveLanguages, resolveServices } from '../../out'; -const formatter = kit.createFormatter(resolveConfig(ts as any, {})); +const languages = resolveLanguages(ts as any); +const services = resolveServices(); export function defineFormatTest(options: { title: string; @@ -16,8 +17,7 @@ export function defineFormatTest(options: { it(`format`, async () => { - formatter.settings = options.settings ?? {}; - + const formatter = kit.createFormatter(Object.values(languages), Object.values(services), options.settings); const formatted = await formatter.formatCode( options.input, options.languageId, diff --git a/packages/language-service/tests/utils/mockEnv.ts b/packages/language-service/tests/utils/mockEnv.ts new file mode 100644 index 0000000000..863c78a56e --- /dev/null +++ b/packages/language-service/tests/utils/mockEnv.ts @@ -0,0 +1,87 @@ +import { FileType, ServiceEnvironment } from '@volar/language-service'; +import { URI } from 'vscode-uri'; +import * as fs from 'fs'; + +const uriToFileName = (uri: string) => URI.parse(uri).fsPath.replace(/\\/g, '/'); +const fileNameToUri = (fileName: string) => URI.file(fileName).toString(); + + +export function createMockServiceEnv( + rootUri: URI, + getSettings = () => ({} as any) +): ServiceEnvironment { + return { + workspaceFolder: { + uri: rootUri, + name: 'test-root', + }, + uriToFileName, + fileNameToUri, + async getConfiguration(section: string) { + const settings = getSettings(); + if (settings[section]) { + return settings[section]; + } + let result: Record | undefined; + for (const key in settings) { + if (key.startsWith(section + '.')) { + const newKey = key.slice(section.length + 1); + result ??= {}; + result[newKey] = settings[key]; + } + } + return result; + }, + fs: { + stat(uri) { + if (uri.startsWith('file://')) { + try { + const stats = fs.statSync(uriToFileName(uri), { throwIfNoEntry: false }); + if (stats) { + return { + type: stats.isFile() ? FileType.File + : stats.isDirectory() ? FileType.Directory + : stats.isSymbolicLink() ? FileType.SymbolicLink + : FileType.Unknown, + ctime: stats.ctimeMs, + mtime: stats.mtimeMs, + size: stats.size, + }; + } + } + catch { + return undefined; + } + } + }, + readFile(uri, encoding) { + if (uri.startsWith('file://')) { + try { + return fs.readFileSync(uriToFileName(uri), { encoding: encoding as 'utf-8' ?? 'utf-8' }); + } + catch { + return undefined; + } + } + }, + readDirectory(uri) { + if (uri.startsWith('file://')) { + try { + const dirName = uriToFileName(uri); + const files = fs.readdirSync(dirName, { withFileTypes: true }); + return files.map<[string, FileType]>(file => { + return [file.name, file.isFile() ? FileType.File + : file.isDirectory() ? FileType.Directory + : file.isSymbolicLink() ? FileType.SymbolicLink + : FileType.Unknown]; + }); + } + catch { + return []; + } + } + return []; + }, + } + }; +} diff --git a/packages/tsc-eslint-hook/src/index.ts b/packages/tsc-eslint-hook/src/index.ts index d074951ffa..f884f0cd59 100644 --- a/packages/tsc-eslint-hook/src/index.ts +++ b/packages/tsc-eslint-hook/src/index.ts @@ -13,13 +13,13 @@ export = async function ( baseConfig: resolveConfig(tsProgram), useEslintrc: false, }); - const fileNames = program.__vue.languageHost.getScriptFileNames(); - const mapper = program.__vue.langaugeContext.virtualFiles; + const fileNames = program.__vue.project.typescript!.projectHost.getScriptFileNames(); + const fileProvider = program.__vue.project.fileProvider; const formatter = await eslint.loadFormatter(); for (const fileName of fileNames) { - const vueFile = mapper.getSource(fileName)?.root; + const vueFile = fileProvider.getSource(fileName)?.root; if (vueFile) { @@ -66,7 +66,7 @@ export = async function ( character: (message.endColumn ?? message.column) - 1, }); - for (const [_, [sourceSnapshot, map]] of mapper.getMaps(embeddedFile)) { + for (const [_, [sourceSnapshot, map]] of fileProvider.getMaps(embeddedFile)) { if (sourceSnapshot !== vueFile.snapshot) continue; diff --git a/packages/tsc/src/index.ts b/packages/tsc/src/index.ts index 5f6be90e49..7d154d1059 100644 --- a/packages/tsc/src/index.ts +++ b/packages/tsc/src/index.ts @@ -10,9 +10,8 @@ export type _Program = ts.Program & { __vue: ProgramContext; }; interface ProgramContext { projectVersion: number; options: ts.CreateProgramOptions; - languageHost: vue.TypeScriptLanguageHost; vueCompilerOptions: Partial; - langaugeContext: vue.LanguageContext; + project: vue.Project; languageService: ts.LanguageService; } @@ -45,17 +44,14 @@ export function createProgram(options: ts.CreateProgramOptions) { const ctx: ProgramContext = { projectVersion: 0, options, - get languageHost() { - return languageHost; - }, get vueCompilerOptions() { return vueCompilerOptions; }, get languageService() { return vueTsLs; }, - get langaugeContext() { - return languageContext; + get project() { + return project; }, }; const vueCompilerOptions = getVueCompilerOptions(); @@ -64,9 +60,11 @@ export function createProgram(options: ts.CreateProgramOptions) { modifiedTime: number, scriptSnapshot: ts.IScriptSnapshot, }>(); - const languageHost: vue.TypeScriptLanguageHost = { - workspacePath: ctx.options.host!.getCurrentDirectory().replace(windowsPathReg, '/'), - rootPath: ctx.options.host!.getCurrentDirectory().replace(windowsPathReg, '/'), + const projectHost: vue.TypeScriptProjectHost = { + configFileName: undefined, + getCurrentDirectory() { + return ctx.options.host!.getCurrentDirectory().replace(windowsPathReg, '/'); + }, getCompilationSettings: () => ctx.options.options, getScriptFileNames: () => { return ctx.options.rootNames as string[]; @@ -78,20 +76,21 @@ export function createProgram(options: ts.CreateProgramOptions) { getProjectReferences: () => ctx.options.projectReferences, getCancellationToken: ctx.options.host!.getCancellationToken ? () => ctx.options.host!.getCancellationToken!() : undefined, }; - const languageContext = vue.createLanguageContext( - languageHost, + const project = vue.createTypeScriptProject( + projectHost, vue.createLanguages( ts, - languageHost.getCompilationSettings(), + projectHost.getCompilationSettings(), vueCompilerOptions, ), + vue.resolveCommonLanguageId ); - const languageServiceHost = volarTs.createLanguageServiceHost(languageContext, ts, ts.sys); - const vueTsLs = ts.createLanguageService(languageServiceHost, volarTs.getDocumentRegistry(ts, ts.sys.useCaseSensitiveFileNames, languageHost.workspacePath)); + const languageServiceHost = volarTs.createLanguageServiceHost(project.typescript!.projectHost, project.fileProvider, ts, ts.sys); + const vueTsLs = ts.createLanguageService(languageServiceHost, volarTs.getDocumentRegistry(ts, ts.sys.useCaseSensitiveFileNames, projectHost.getCurrentDirectory())); - volarTs.decorateLanguageService(languageContext.virtualFiles, vueTsLs, false); + volarTs.decorateLanguageService(project.fileProvider, vueTsLs, false); - program = volarTs.getProgram(ts as any, languageContext, vueTsLs, ts.sys) as (ts.Program & { __vue: ProgramContext; }); + program = volarTs.getProgram(ts as any, project.fileProvider, vueTsLs, ts.sys) as (ts.Program & { __vue: ProgramContext; }); program.__vue = ctx; function getVueCompilerOptions(): Partial { diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 8339b08a05..cc0aeb3f57 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -10,16 +10,17 @@ const init: ts.server.PluginModuleFactory = (modules) => { const pluginModule: ts.server.PluginModule = { create(info) { - const virtualFiles = vue.createVirtualFiles( + const fileProvider = vue.createFileProvider( vue.createLanguages( ts, info.languageServiceHost.getCompilationSettings(), getVueCompilerOptions(), ), + () => { } ); - decorateLanguageService(virtualFiles, info.languageService, true); - decorateLanguageServiceHost(virtualFiles, info.languageServiceHost, ts, ['.vue']); + decorateLanguageService(fileProvider, info.languageService, true); + decorateLanguageServiceHost(fileProvider, info.languageServiceHost, ts, ['.vue']); const getCompletionsAtPosition = info.languageService.getCompletionsAtPosition.bind(info.languageService); From d10b0e5cd59093873993d4be27b8a9f686b3ec31 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 17 Nov 2023 12:33:57 +0800 Subject: [PATCH 02/19] updates [skip ci] --- packages/language-service/tests/utils/format.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/language-service/tests/utils/format.ts b/packages/language-service/tests/utils/format.ts index e5aa604c61..eda3201801 100644 --- a/packages/language-service/tests/utils/format.ts +++ b/packages/language-service/tests/utils/format.ts @@ -5,6 +5,7 @@ import { resolveLanguages, resolveServices } from '../../out'; const languages = resolveLanguages(ts as any); const services = resolveServices(); +const formatter = kit.createFormatter(Object.values(languages), Object.values(services)); export function defineFormatTest(options: { title: string; @@ -17,8 +18,9 @@ export function defineFormatTest(options: { it(`format`, async () => { - const formatter = kit.createFormatter(Object.values(languages), Object.values(services), options.settings); - const formatted = await formatter.formatCode( + formatter.settings = options.settings ?? {}; + + const formatted = await formatter.format( options.input, options.languageId, { insertSpaces: false, tabSize: 4 }, From 1c3a7b5a7a48ca60e7ccf54a4dac0fa33287ea4b Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 17 Nov 2023 21:40:54 +0800 Subject: [PATCH 03/19] sync https://github.com/volarjs/volar.js/pull/86 changes [skip ci] --- packages/component-meta/src/base.ts | 10 ++- packages/language-core/src/languageModule.ts | 15 ++-- .../src/virtualFile/computedFiles.ts | 23 ++++-- .../language-core/src/virtualFile/vueFile.ts | 18 ++--- packages/language-service/src/helpers.ts | 32 ++++----- .../src/ideFeatures/dragImport.ts | 2 +- .../src/ideFeatures/nameCasing.ts | 18 ++--- .../language-service/src/languageService.ts | 39 +++++----- .../src/plugins/vue-autoinsert-parentheses.ts | 6 +- .../src/plugins/vue-codelens-references.ts | 16 ++--- .../src/plugins/vue-extract-file.ts | 15 ++-- .../src/plugins/vue-template.ts | 72 ++++++++++++------- .../plugins/vue-toggle-v-bind-codeaction.ts | 7 +- .../src/plugins/vue-twoslash-queries.ts | 10 +-- .../vue-visualize-hidden-callback-param.ts | 9 ++- packages/language-service/src/plugins/vue.ts | 11 ++- .../tests/utils/createTester.ts | 7 +- packages/tsc-eslint-hook/src/index.ts | 6 +- packages/tsc/src/index.ts | 23 +++++- 19 files changed, 204 insertions(+), 135 deletions(-) diff --git a/packages/component-meta/src/base.ts b/packages/component-meta/src/base.ts index 82acc2c724..039b3694e1 100644 --- a/packages/component-meta/src/base.ts +++ b/packages/component-meta/src/base.ts @@ -153,8 +153,12 @@ export function baseCreate( host.getCompilationSettings(), vueCompilerOptions, ); - const project = vue.createTypeScriptProject(host, vueLanguages, vue.resolveCommonLanguageId); - const tsLsHost = createLanguageServiceHost(project.typescript!.projectHost, project.fileProvider, ts, ts.sys); + const fileNameResolutionHost = { + fileNameToId: (fileName: string) => fileName, + idToFileName: (id: string) => id, + }; + const project = vue.createTypeScriptProject(host, vueLanguages, fileNameResolutionHost.fileNameToId, vue.resolveCommonLanguageId); + const tsLsHost = createLanguageServiceHost(project.typescript!.projectHost, project.fileProvider, fileNameResolutionHost, ts, ts.sys); const tsLs = ts.createLanguageService(tsLsHost); decorateLanguageService(project.fileProvider, tsLs, false); @@ -297,7 +301,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const printer = ts.createPrinter(checkerOptions.printer); const snapshot = host.getScriptSnapshot(componentPath)!; - const vueSourceFile = project.fileProvider.getSource(componentPath)?.root; + const vueSourceFile = project.fileProvider.getSourceFile(componentPath)?.root; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; diff --git a/packages/language-core/src/languageModule.ts b/packages/language-core/src/languageModule.ts index 975a88992b..5460f1d1ff 100644 --- a/packages/language-core/src/languageModule.ts +++ b/packages/language-core/src/languageModule.ts @@ -68,18 +68,15 @@ export function createVueLanguage( } return { - createVirtualFile(fileName, snapshot, languageId) { - if ( - (languageId && allowLanguageIds.has(languageId)) - || (!languageId && vueCompilerOptions.extensions.some(ext => fileName.endsWith(ext))) - ) { - if (fileRegistry.has(fileName)) { - const reusedVueFile = fileRegistry.get(fileName)!; + createVirtualFile(id, languageId, snapshot) { + if (allowLanguageIds.has(languageId)) { + if (fileRegistry.has(id)) { + const reusedVueFile = fileRegistry.get(id)!; reusedVueFile.update(snapshot); return reusedVueFile; } - const vueFile = new VueFile(fileName, languageId, snapshot, vueCompilerOptions, plugins, ts, codegenStack); - fileRegistry.set(fileName, vueFile); + const vueFile = new VueFile(id, languageId, snapshot, vueCompilerOptions, plugins, ts, codegenStack); + fileRegistry.set(id, vueFile); return vueFile; } }, diff --git a/packages/language-core/src/virtualFile/computedFiles.ts b/packages/language-core/src/virtualFile/computedFiles.ts index c6827ea754..d1f50250bb 100644 --- a/packages/language-core/src/virtualFile/computedFiles.ts +++ b/packages/language-core/src/virtualFile/computedFiles.ts @@ -1,10 +1,10 @@ import { VirtualFile, resolveCommonLanguageId } from '@volar/language-core'; import { buildMappings, buildStacks, toString } from '@volar/source-map'; +import { computed } from 'computeds'; import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Sfc, SfcBlock, VueLanguagePlugin } from '../types'; import { VueEmbeddedFile } from './embeddedFile'; -import { computed } from 'computeds'; export function computedFiles( plugins: ReturnType[], @@ -50,8 +50,11 @@ export function computedFiles( for (const { file, snapshot, mappings, codegenStacks } of remain) { embeddedFiles.push({ - ...file, + id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), + kind: file.kind, + capabilities: file.capabilities, + mirrorBehaviorMappings: file.mirrorBehaviorMappings, snapshot, mappings, codegenStacks, @@ -67,8 +70,11 @@ export function computedFiles( const { file, snapshot, mappings, codegenStacks } = remain[i]; if (!file.parentFileName) { embeddedFiles.push({ - ...file, + id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), + kind: file.kind, + capabilities: file.capabilities, + mirrorBehaviorMappings: file.mirrorBehaviorMappings, snapshot, mappings, codegenStacks, @@ -80,8 +86,11 @@ export function computedFiles( const parent = findParentStructure(file.parentFileName, embeddedFiles); if (parent) { parent.embeddedFiles.push({ - ...file, + id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), + kind: file.kind, + capabilities: file.capabilities, + mirrorBehaviorMappings: file.mirrorBehaviorMappings, snapshot, mappings, codegenStacks, @@ -92,12 +101,12 @@ export function computedFiles( } } } - function findParentStructure(fileName: string, current: VirtualFile[]): VirtualFile | undefined { + function findParentStructure(id: string, current: VirtualFile[]): VirtualFile | undefined { for (const child of current) { - if (child.fileName === fileName) { + if (child.id === id) { return child; } - let parent = findParentStructure(fileName, child.embeddedFiles); + let parent = findParentStructure(id, child.embeddedFiles); if (parent) { return parent; } diff --git a/packages/language-core/src/virtualFile/vueFile.ts b/packages/language-core/src/virtualFile/vueFile.ts index b1f027a65b..35e65e59e4 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -18,10 +18,10 @@ export class VueFile implements VirtualFile { // computeds - getVueSfc = computedVueSfc(this.plugins, this.fileName, () => this._snapshot()); - sfc = computedSfc(this.ts, this.plugins, this.fileName, () => this._snapshot(), this.getVueSfc); + getVueSfc = computedVueSfc(this.plugins, this.id, () => this._snapshot()); + sfc = computedSfc(this.ts, this.plugins, this.id, () => this._snapshot(), this.getVueSfc); getMappings = computedMappings(() => this._snapshot(), this.sfc); - getEmbeddedFiles = computedFiles(this.plugins, this.fileName, this.sfc, this.codegenStack); + getEmbeddedFiles = computedFiles(this.plugins, this.id, this.sfc, this.codegenStack); // others @@ -31,14 +31,14 @@ export class VueFile implements VirtualFile { get embeddedFiles() { return this.getEmbeddedFiles(); } - get mainScriptName() { - let res: string = ''; + get mainTsFile() { + let result: VirtualFile | undefined; forEachEmbeddedFile(this, file => { - if (file.kind === FileKind.TypeScriptHostFile && file.fileName.replace(this.fileName, '').match(jsxReg)) { - res = file.fileName; + if (file.kind === FileKind.TypeScriptHostFile && file.id.substring(this.id.length).match(jsxReg)) { + result = file; } }); - return res; + return result; } get snapshot() { return this._snapshot(); @@ -48,7 +48,7 @@ export class VueFile implements VirtualFile { } constructor( - public fileName: string, + public id: string, public languageId: string, public initSnapshot: ts.IScriptSnapshot, public vueCompilerOptions: VueCompilerOptions, diff --git a/packages/language-service/src/helpers.ts b/packages/language-service/src/helpers.ts index 34cfe02890..8d3dc7c852 100644 --- a/packages/language-service/src/helpers.ts +++ b/packages/language-service/src/helpers.ts @@ -1,15 +1,16 @@ -import * as vue from '@vue/language-core'; -import type { CompilerDOM } from '@vue/language-core'; import * as embedded from '@volar/language-core'; -import { computed } from 'computeds'; +import type { CompilerDOM } from '@vue/language-core'; +import * as vue from '@vue/language-core'; import { sharedTypes } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; - +import { computed } from 'computeds'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import type { ServiceEnvironment } from './types'; export function getPropsByTag( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, tag: string, vueCompilerOptions: vue.VueCompilerOptions, @@ -17,7 +18,7 @@ export function getPropsByTag( ) { const checker = tsLs.getProgram()!.getTypeChecker(); - const components = getVariableType(ts, tsLs, sourceFile, '__VLS_components'); + const components = getVariableType(ts, tsLs, env, sourceFile, '__VLS_components'); if (!components) return []; @@ -83,13 +84,14 @@ export function getPropsByTag( export function getEventsOfTag( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, tag: string, vueCompilerOptions: vue.VueCompilerOptions, ) { const checker = tsLs.getProgram()!.getTypeChecker(); - const components = getVariableType(ts, tsLs, sourceFile, '__VLS_components'); + const components = getVariableType(ts, tsLs, env, sourceFile, '__VLS_components'); if (!components) return []; @@ -149,9 +151,10 @@ export function getEventsOfTag( export function getTemplateCtx( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, ) { - return getVariableType(ts, tsLs, sourceFile, '__VLS_ctx') + return getVariableType(ts, tsLs, env, sourceFile, '__VLS_ctx') ?.type ?.getProperties() .map(c => c.name); @@ -160,10 +163,11 @@ export function getTemplateCtx( export function getComponentNames( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, vueCompilerOptions: vue.VueCompilerOptions, ) { - return getVariableType(ts, tsLs, sourceFile, '__VLS_components') + return getVariableType(ts, tsLs, env, sourceFile, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) @@ -207,6 +211,7 @@ export function getElementAttrs( function getVariableType( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, name: string, ) { @@ -215,16 +220,11 @@ function getVariableType( return; } - let file: embedded.VirtualFile | undefined; - let tsSourceFile: ts.SourceFile | undefined; + const file = sourceFile.mainTsFile; - embedded.forEachEmbeddedFile(sourceFile, embedded => { - if (embedded.fileName === sourceFile.mainScriptName) { - file = embedded; - } - }); + let tsSourceFile: ts.SourceFile | undefined; - if (file && (tsSourceFile = tsLs.getProgram()?.getSourceFile(file.fileName))) { + if (file && (tsSourceFile = tsLs.getProgram()?.getSourceFile(env.uriToFileName(file.id)))) { const node = searchVariableDeclarationNode(ts, tsSourceFile, name); const checker = tsLs.getProgram()?.getTypeChecker(); diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts index 1c4a9f1050..41ddc6203e 100644 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ b/packages/language-service/src/ideFeatures/dragImport.ts @@ -23,7 +23,7 @@ export function getDragImportEdits( const newName = capitalize(camelize(baseName)); const document = ctx!.getTextDocument(uri)!; - const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri) as [VueFile, any]; + const [vueFile] = ctx!.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; const { sfc } = vueFile; const script = sfc.scriptSetup ?? sfc.script; diff --git a/packages/language-service/src/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index 516ffff682..b49f91ca0a 100644 --- a/packages/language-service/src/ideFeatures/nameCasing.ts +++ b/packages/language-service/src/ideFeatures/nameCasing.ts @@ -13,7 +13,7 @@ export async function convertTagName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; if (!(rootFile instanceof VueFile)) return; @@ -23,9 +23,9 @@ export async function convertTagName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName, rootFile.languageId); + const document = context.documents.getDocumentByUri(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); for (const [tagName, { offsets }] of tags) { @@ -56,7 +56,7 @@ export async function convertAttrName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; if (!(rootFile instanceof VueFile)) return; @@ -66,15 +66,15 @@ export async function convertAttrName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName, rootFile.languageId); + const document = context.documents.getDocumentByUri(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); for (const [tagName, { attrs }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); if (componentName) { - const props = getPropsByTag(ts, languageService, rootFile, componentName, vueCompilerOptions); + const props = getPropsByTag(ts, languageService, context.env, rootFile, componentName, vueCompilerOptions); for (const [attrName, { offsets }] of attrs) { const propName = props.find(prop => prop === attrName || hyphenateAttr(prop) === attrName); if (propName) { @@ -128,7 +128,7 @@ export function detect( attr: AttrNameCasing[], } { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; if (!(rootFile instanceof VueFile)) { return { tag: [], @@ -169,7 +169,7 @@ export function detect( } function getTagNameCase(file: VirtualFile): TagNameCasing[] { - const components = getComponentNames(ts, languageService, file, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, file, vueCompilerOptions); const tagNames = getTemplateTagsAndAttrs(file); const result: TagNameCasing[] = []; diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 9af52c2f83..7f70b65e3a 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -89,19 +89,26 @@ export function resolveServices( // handle component auto-import patch let casing: Awaited> | undefined; - for (const [_, map] of ctx.documents.getMapsByVirtualFileUri(document.uri)) { - const virtualFile = ctx.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (virtualFile instanceof VueFile) { - const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.autoImportOnly); - if (isAutoImport) { - const source = ctx.documents.getVirtualFileByUri(document.uri)[1]; - for (const item of result.items) { - item.data.__isComponentAutoImport = true; - } + const [virtualFile, sourceFile] = ctx.project.fileProvider.getVirtualFile(document.uri); + + if (virtualFile && sourceFile) { + + for (const map of ctx.documents.getMapsByVirtualFile(virtualFile)) { + + const sourceVirtualFile = ctx.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + + if (sourceVirtualFile instanceof VueFile) { + + const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.autoImportOnly); + if (isAutoImport) { + + for (const item of result.items) { + item.data.__isComponentAutoImport = true; + } + + // fix #2458 + casing ??= await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); - // fix #2458 - if (source) { - casing ??= await getNameCasing(ts, ctx, ctx.env.fileNameToUri(source.fileName), vueCompilerOptions); if (casing.tag === TagNameCasing.Kebab) { for (const item of result.items) { item.filterText = hyphenateTag(item.filterText ?? item.label); @@ -151,9 +158,9 @@ export function resolveServices( 'import ' + newName + ' from ', ); item.textEdit.newText = newName; - const source = ctx.documents.getVirtualFileByUri(itemData.uri)[1]; - if (source) { - const casing = await getNameCasing(ts, ctx, ctx.env.fileNameToUri(source.fileName), vueCompilerOptions); + const [_, sourceFile] = ctx.project.fileProvider.getVirtualFile(itemData.uri); + if (sourceFile) { + const casing = await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); if (casing.tag === TagNameCasing.Kebab) { item.textEdit.newText = hyphenateTag(item.textEdit.newText); } @@ -176,7 +183,7 @@ export function resolveServices( const componentName = newName ?? item.textEdit.newText; const optionEdit = ExtractComponentService.createAddComponentToOptionEdit(ts, ast, componentName); if (optionEdit) { - const textDoc = ctx.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName, virtualFile.languageId); + const textDoc = ctx.documents.getDocumentByUri(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); item.additionalTextEdits.push({ range: { start: textDoc.positionAt(optionEdit.range.start), diff --git a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts index fba00b5f71..f6e134a86f 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -24,8 +24,8 @@ const plugin: Service = (context, modules) => { if (!isCharacterTyping(document, options_2)) return; - const [virtualFile] = context.documents.getVirtualFileByUri(document.uri); - if (!virtualFile?.fileName.endsWith('.template_format.ts')) + const [virtualFile] = context.project.fileProvider.getVirtualFile(document.uri); + if (!virtualFile?.id.endsWith('.template_format.ts')) return; const offset = document.offsetAt(position); @@ -33,7 +33,7 @@ const plugin: Service = (context, modules) => { for (const mappedRange of virtualFile.mappings) { if (mappedRange.generatedRange[1] === offset) { const text = document.getText().substring(mappedRange.generatedRange[0], mappedRange.generatedRange[1]); - const ast = ts.createSourceFile(virtualFile.fileName, text, ts.ScriptTarget.Latest); + const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); if (ast.statements.length === 1) { const statement = ast.statements[0]; if ( diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index e8a247335d..bded680896 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,5 +1,5 @@ import { Service } from '@volar/language-service'; -import { VueFile } from '@vue/language-core'; +import { SourceFile, VueFile } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; export const create = function (): Service { @@ -13,11 +13,11 @@ export const create = function (): Service { provideReferencesCodeLensRanges(document) { - return worker(document.uri, async () => { + return worker(document.uri, async (_, sourceFile) => { const result: vscode.Range[] = []; - for (const [_, map] of context.documents.getMapsBySourceFileUri(document.uri)?.maps ?? []) { + for (const [_, map] of context.documents.getMapsBySourceFile(sourceFile) ?? []) { for (const mapping of map.map.mappings) { if (!mapping.data.referencesCodeLens) @@ -38,7 +38,7 @@ export const create = function (): Service { await worker(document.uri, async (vueFile) => { - const document = context.documents.getDocumentByFileName(vueFile.snapshot, vueFile.fileName, vueFile.languageId); + const document = context.documents.getDocumentByUri(vueFile.id, vueFile.languageId, vueFile.snapshot); const offset = document.offsetAt(range.start); const blocks = [ vueFile.sfc.script, @@ -58,13 +58,13 @@ export const create = function (): Service { }, }; - function worker(uri: string, callback: (vueSourceFile: VueFile) => T) { + function worker(uri: string, callback: (vueFile: VueFile, sourceFile: SourceFile) => T) { - const [virtualFile] = context!.documents.getVirtualFileByUri(uri); - if (!(virtualFile instanceof VueFile)) + const [virtualFile, sourceFile] = context!.project.fileProvider.getVirtualFile(uri); + if (!(virtualFile instanceof VueFile) || !sourceFile) return; - return callback(virtualFile); + return callback(virtualFile, sourceFile); } }; }; diff --git a/packages/language-service/src/plugins/vue-extract-file.ts b/packages/language-service/src/plugins/vue-extract-file.ts index 615640b970..63a544d133 100644 --- a/packages/language-service/src/plugins/vue-extract-file.ts +++ b/packages/language-service/src/plugins/vue-extract-file.ts @@ -32,7 +32,7 @@ export const create = function (): Service { return; } - const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri); + const [vueFile] = ctx!.project.fileProvider.getVirtualFile(document.uri); if (!vueFile || !(vueFile instanceof VueFile)) return; @@ -64,7 +64,7 @@ export const create = function (): Service { const { uri, range, newName } = codeAction.data as ActionData; const document = ctx!.getTextDocument(uri)!; const [startOffset, endOffset]: [number, number] = range; - const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri) as [VueFile, any]; + const [vueFile] = ctx!.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; const { sfc } = vueFile; const script = sfc.scriptSetup ?? sfc.script; @@ -77,8 +77,10 @@ export const create = function (): Service { const languageService = ctx!.inject('typescript/languageService'); const languageServiceHost = ctx!.inject('typescript/languageServiceHost'); - const sourceFile = languageService.getProgram()!.getSourceFile(vueFile.mainScriptName)!; - const sourceFileKind = languageServiceHost.getScriptKind?.(vueFile.mainScriptName); + const tsScriptUri = vueFile.mainTsFile!.id; + const tsScriptName = ctx!.env.uriToFileName(tsScriptUri); + const sourceFile = languageService.getProgram()!.getSourceFile(tsScriptName)!; + const sourceFileKind = languageServiceHost.getScriptKind?.(tsScriptName); const toExtract = collectExtractProps(); const initialIndentSetting = await ctx!.env.getConfiguration!('volar.format.initialIndent') as Record; const newUri = document.uri.substring(0, document.uri.lastIndexOf('/') + 1) + `${newName}.vue`; @@ -177,7 +179,8 @@ export const create = function (): Service { model: boolean; }>(); const checker = languageService.getProgram()!.getTypeChecker(); - const maps = [...ctx!.documents.getMapsByVirtualFileName(vueFile.mainScriptName)]; + const [virtualFile] = ctx!.project.fileProvider.getVirtualFile(tsScriptUri); + const maps = virtualFile ? [...ctx!.documents.getMapsByVirtualFile(virtualFile)] : []; sourceFile.forEachChild(function visit(node) { if ( @@ -187,7 +190,7 @@ export const create = function (): Service { && ts.isIdentifier(node.name) ) { const { name } = node; - for (const [_, map] of maps) { + for (const map of maps) { const source = map.map.toSourceOffset(name.getEnd()); if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1].data.semanticTokens) { if (!result.has(name.text)) { diff --git a/packages/language-service/src/plugins/vue-template.ts b/packages/language-service/src/plugins/vue-template.ts index 1d465e73af..bc41d886af 100644 --- a/packages/language-service/src/plugins/vue-template.ts +++ b/packages/language-service/src/plugins/vue-template.ts @@ -75,10 +75,14 @@ export const create = (options: { if (!options.isSupportedDocument(document)) return; - for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) { - const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (virtualFile && virtualFile instanceof VueFile) { - await provideHtmlData(map, virtualFile); + const [virtualFile] = _context.project.fileProvider.getVirtualFile(document.uri); + + if (virtualFile) { + for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + if (sourceVirtualFile instanceof VueFile) { + await provideHtmlData(map, sourceVirtualFile); + } } } @@ -86,10 +90,12 @@ export const create = (options: { if (!htmlComplete) return; - for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) { - const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (virtualFile && virtualFile instanceof VueFile) { - afterHtmlCompletion(htmlComplete, map, virtualFile); + if (virtualFile) { + for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + if (sourceVirtualFile instanceof VueFile) { + afterHtmlCompletion(htmlComplete, map, sourceVirtualFile); + } } } @@ -107,15 +113,20 @@ export const create = (options: { const languageService = _context.inject('typescript/languageService'); const result: vscode.InlayHint[] = []; + const [virtualFile] = _context.project.fileProvider.getVirtualFile(document.uri); + if (!virtualFile) + return; + + for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { - for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) { - const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; const scanner = options.getScanner(htmlOrPugService, document); - if (virtualFile && virtualFile instanceof VueFile && scanner) { + + if (sourceVirtualFile instanceof VueFile && scanner) { // visualize missing required props const casing = await getNameCasing(ts, _context, map.sourceFileDocument.uri, options.vueCompilerOptions); - const components = getComponentNames(ts, languageService, virtualFile, options.vueCompilerOptions); + const components = getComponentNames(ts, languageService, _context!.env, sourceVirtualFile, options.vueCompilerOptions); const componentProps: Record = {}; let token: html.TokenType; let current: { @@ -132,7 +143,7 @@ export const create = (options: { : components.find(component => component === tagName || hyphenateTag(component) === tagName); const checkTag = tagName.indexOf('.') >= 0 ? tagName : component; if (checkTag) { - componentProps[checkTag] ??= getPropsByTag(ts, languageService, virtualFile, checkTag, options.vueCompilerOptions, true); + componentProps[checkTag] ??= getPropsByTag(ts, languageService, _context!.env, sourceVirtualFile, checkTag, options.vueCompilerOptions, true); current = { unburnedRequiredProps: [...componentProps[checkTag]], labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), @@ -213,7 +224,7 @@ export const create = (options: { if (!options.isSupportedDocument(document)) return; - if (_context.documents.isVirtualFileUri(document.uri)) + if (_context.project.fileProvider.getVirtualFile(document.uri)[0]) options.updateCustomData(htmlOrPugService, []); return htmlOrPugService.provideHover?.(document, position, token); @@ -225,15 +236,19 @@ export const create = (options: { return; const originalResult = await htmlOrPugService.provideDiagnostics?.(document, token); + const [virtualFile] = _context.project.fileProvider.getVirtualFile(document.uri); + + if (!virtualFile) + return; - for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) { + for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { - const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (!virtualFile || !(virtualFile instanceof VueFile)) + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + if (!(sourceVirtualFile instanceof VueFile)) continue; const templateErrors: vscode.Diagnostic[] = []; - const { template } = virtualFile.sfc; + const { template } = sourceVirtualFile.sfc; if (template) { @@ -284,14 +299,17 @@ export const create = (options: { return; const languageService = _context.inject('typescript/languageService'); + const [virtualFile] = _context.project.fileProvider.getVirtualFile(document.uri); + if (!virtualFile) + return; - for (const [_, map] of _context.documents.getMapsByVirtualFileUri(document.uri)) { + for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { - const virtualFile = _context.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (!virtualFile || !(virtualFile instanceof VueFile)) + const sourceFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + if (!(sourceFile instanceof VueFile)) continue; - const templateScriptData = getComponentNames(ts, languageService, virtualFile, options.vueCompilerOptions); + const templateScriptData = getComponentNames(ts, languageService, _context!.env, sourceFile, options.vueCompilerOptions); const components = new Set([ ...templateScriptData, ...templateScriptData.map(hyphenateTag), @@ -367,7 +385,7 @@ export const create = (options: { isApplicable: () => true, provideTags: () => { - const components = getComponentNames(ts, languageService, vueSourceFile, options.vueCompilerOptions) + const components = getComponentNames(ts, languageService, _context!.env, vueSourceFile, options.vueCompilerOptions) .filter(name => name !== 'Transition' && name !== 'TransitionGroup' @@ -410,8 +428,8 @@ export const create = (options: { provideAttributes: (tag) => { const attrs = getElementAttrs(ts, languageService, languageServiceHost, tag); - const props = new Set(getPropsByTag(ts, languageService, vueSourceFile, tag, options.vueCompilerOptions)); - const events = getEventsOfTag(ts, languageService, vueSourceFile, tag, options.vueCompilerOptions); + const props = new Set(getPropsByTag(ts, languageService, _context!.env, vueSourceFile, tag, options.vueCompilerOptions)); + const events = getEventsOfTag(ts, languageService, _context!.env, vueSourceFile, tag, options.vueCompilerOptions); const attributes: html.IAttributeData[] = []; const _tsCodegen = tsCodegen.get(vueSourceFile.sfc); @@ -419,7 +437,7 @@ export const create = (options: { let ctxVars = [ ..._tsCodegen.scriptRanges()?.bindings.map(binding => vueSourceFile.sfc.script!.content.substring(binding.start, binding.end)) ?? [], ..._tsCodegen.scriptSetupRanges()?.bindings.map(binding => vueSourceFile.sfc.scriptSetup!.content.substring(binding.start, binding.end)) ?? [], - ...getTemplateCtx(ts, languageService, vueSourceFile) ?? [], + ...getTemplateCtx(ts, languageService, _context!.env, vueSourceFile) ?? [], ]; ctxVars = [...new Set(ctxVars)]; const dirs = ctxVars.map(hyphenateAttr).filter(v => v.startsWith('v-')); @@ -535,7 +553,7 @@ export const create = (options: { const languageService = _context!.inject('typescript/languageService'); const replacement = getReplacement(completionList, map.sourceFileDocument); - const componentNames = new Set(getComponentNames(ts, languageService, vueSourceFile, options.vueCompilerOptions).map(hyphenateTag)); + const componentNames = new Set(getComponentNames(ts, languageService, _context!.env, vueSourceFile, options.vueCompilerOptions).map(hyphenateTag)); if (replacement) { diff --git a/packages/language-service/src/plugins/vue-toggle-v-bind-codeaction.ts b/packages/language-service/src/plugins/vue-toggle-v-bind-codeaction.ts index f59ae11d8d..16fb334f34 100644 --- a/packages/language-service/src/plugins/vue-toggle-v-bind-codeaction.ts +++ b/packages/language-service/src/plugins/vue-toggle-v-bind-codeaction.ts @@ -14,15 +14,16 @@ export const create = function (): Service { return { provideCodeActions(document, range, _context) { + const startOffset = document.offsetAt(range.start); const endOffset = document.offsetAt(range.end); + const [virtualFile] = ctx!.project.fileProvider.getVirtualFile(document.uri); - const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri); - if (!vueFile || !(vueFile instanceof VueFile)) { + if (!(virtualFile instanceof VueFile)) { return; } - const { template } = vueFile.sfc; + const { template } = virtualFile.sfc; if (!template?.ast) return; diff --git a/packages/language-service/src/plugins/vue-twoslash-queries.ts b/packages/language-service/src/plugins/vue-twoslash-queries.ts index 51c511ff09..bc1057fe09 100644 --- a/packages/language-service/src/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/src/plugins/vue-twoslash-queries.ts @@ -29,13 +29,13 @@ const plugin: Service = (context: ServiceContext { - if (embedded.kind === FileKind.TypeScriptHostFile) { - for (const [_, map] of context.documents.getMapsByVirtualFileName(embedded.fileName)) { + forEachEmbeddedFile(vueFile, (virtualFile) => { + if (virtualFile.kind === FileKind.TypeScriptHostFile) { + for (const map of context.documents.getMapsByVirtualFile(virtualFile)) { for (const [pointerPosition, hoverOffset] of hoverOffsets) { for (const [tsOffset, mapping] of map.map.toGeneratedOffsets(hoverOffset)) { if (mapping.data.hover) { - const quickInfo = languageService.getQuickInfoAtPosition(embedded.fileName, tsOffset); + const quickInfo = languageService.getQuickInfoAtPosition(context.env.uriToFileName(virtualFile.id), tsOffset); if (quickInfo) { inlayHints.push({ position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, @@ -59,7 +59,7 @@ const plugin: Service = (context: ServiceContext(uri: string, callback: (vueSourceFile: vue.VueFile) => T) { - const [virtualFile] = context!.documents.getVirtualFileByUri(uri); + const [virtualFile] = context!.project.fileProvider.getVirtualFile(uri); if (!(virtualFile instanceof vue.VueFile)) return; diff --git a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts index 1aca2da3e4..8edb3f1104 100644 --- a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts +++ b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts @@ -12,11 +12,14 @@ const plugin: Service = (context) => { const settings: Record = {}; const result: vscode.InlayHint[] = []; - const [file] = context.documents.getVirtualFileByUri(document.uri); - if (file) { + const [vitualFile] = context.project.fileProvider.getVirtualFile(document.uri); + + if (vitualFile) { + const start = document.offsetAt(range.start); const end = document.offsetAt(range.end); - for (const mapping of file.mappings) { + + for (const mapping of vitualFile.mappings) { const hint: { setting: string; diff --git a/packages/language-service/src/plugins/vue.ts b/packages/language-service/src/plugins/vue.ts index a9b14dd2c9..4338436fb8 100644 --- a/packages/language-service/src/plugins/vue.ts +++ b/packages/language-service/src/plugins/vue.ts @@ -38,11 +38,16 @@ export const create = (): Service => (context: ServiceContext { + if (!vueSourceFile.mainTsFile) { + return; + } + const result: vscode.Diagnostic[] = []; const sfc = vueSourceFile.sfc; const program = context.inject('typescript/languageService').getProgram(); + const tsFileName = context.env.uriToFileName(vueSourceFile.mainTsFile.id); - if (program && !program.getSourceFile(vueSourceFile.mainScriptName)) { + if (program && !program.getSourceFile(tsFileName)) { for (const script of [sfc.script, sfc.scriptSetup]) { if (!script || script.content === '') @@ -53,7 +58,7 @@ export const create = (): Service => (context: ServiceContext / "allowJs": true / jsconfig.json.`, + message: `Virtual script ${JSON.stringify(tsFileName)} not found, may missing '); const metaSnapshots: Record = {}; - const getScriptFileNames = host.getScriptFileNames; - const getScriptSnapshot = host.getScriptSnapshot; - host.getScriptFileNames = () => { + const getScriptFileNames = projectHost.getScriptFileNames; + const getScriptSnapshot = projectHost.getScriptSnapshot; + projectHost.getScriptFileNames = () => { const names = getScriptFileNames(); return [ ...names, @@ -133,7 +136,7 @@ export function baseCreate( getMetaFileName(globalComponentName), ]; }; - host.getScriptSnapshot = (fileName) => { + projectHost.getScriptSnapshot = (fileName) => { if (isMetaFileName(fileName)) { if (!metaSnapshots[fileName]) { metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); @@ -150,22 +153,24 @@ export function baseCreate( const vueLanguages = vue.createLanguages( ts, - host.getCompilationSettings(), + projectHost.getCompilationSettings(), vueCompilerOptions, ); - const fileNameResolutionHost = { - fileNameToId: (fileName: string) => fileName, - idToFileName: (id: string) => id, - }; - const project = vue.createTypeScriptProject(vueLanguages, host, fileNameResolutionHost.fileNameToId, vue.resolveCommonLanguageId); - const tsLsHost = createLanguageServiceHost(project.typescript!.projectHost, project.fileProvider, fileNameResolutionHost, ts, ts.sys); - const tsLs = ts.createLanguageService(tsLsHost); + const project = createProject( + ts, + ts.sys, + vueLanguages, + configFileName, + projectHost, + ); + const { languageServiceHost } = project.typescript!; + const tsLs = ts.createLanguageService(languageServiceHost); decorateLanguageService(project.fileProvider, tsLs, false); if (checkerOptions.forceUseTs) { - const getScriptKind = tsLsHost.getScriptKind; - tsLsHost.getScriptKind = (fileName) => { + const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost); + languageServiceHost.getScriptKind = (fileName) => { if (fileName.endsWith('.vue.js')) { return ts.ScriptKind.TS; } @@ -299,9 +304,9 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} // fill defaults const printer = ts.createPrinter(checkerOptions.printer); - const snapshot = host.getScriptSnapshot(componentPath)!; + const snapshot = projectHost.getScriptSnapshot(componentPath)!; - const vueSourceFile = project.fileProvider.getSourceFile(componentPath)?.root; + const vueSourceFile = project.fileProvider.getSourceFile(componentPath)?.virtualFile?.[0]; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; @@ -643,8 +648,8 @@ function createSchemaResolvers( if (virtualFile) { const maps = core.fileProvider.getMaps(virtualFile); for (const [source, [_, map]] of maps) { - const start = map.toSourceOffset(declaration.getStart()); - const end = map.toSourceOffset(declaration.getEnd()); + const start = map.getSourceOffset(declaration.getStart()); + const end = map.getSourceOffset(declaration.getEnd()); if (start && end) { return { file: source, diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index 9a0f859ea1..115cd5891b 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -1,4 +1,4 @@ -import { FileRangeCapabilities, MirrorBehaviorCapabilities } from '@volar/language-core'; +import { LinkedCodeTrigger } from '@volar/language-core'; import * as SourceMaps from '@volar/source-map'; import { Segment, getLength } from '@volar/source-map'; import * as muggle from 'muggle-string'; @@ -7,7 +7,7 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import type * as templateGen from '../generators/template'; import type { ScriptRanges } from '../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../parsers/scriptSetupRanges'; -import type { TextRange, VueCompilerOptions } from '../types'; +import type { TextRange, VueCodeInformation, VueCompilerOptions } from '../types'; import { Sfc } from '../types'; import { getSlotsPropertyName, hyphenateTag } from '../utils/shared'; import { walkInterpolationFragment } from '../utils/transform'; @@ -27,8 +27,8 @@ export function generate( codegenStack: boolean, ) { - const [codes, codeStacks] = codegenStack ? muggle.track([] as Segment[]) : [[], []]; - const mirrorBehaviorMappings: SourceMaps.Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; + const [codes, codeStacks] = codegenStack ? muggle.track([] as Segment[]) : [[], []]; + const mirrorBehaviorMappings: SourceMaps.Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 if (!script && !scriptSetup) { @@ -155,10 +155,10 @@ export function generate( 'script', [script.srcOffset - 1, script.srcOffset + script.src.length + 1], { - ...FileRangeCapabilities.full, - rename: src === script.src ? true : { - normalize: undefined, - apply(newName) { + renameEdits: src === script.src ? true : { + shouldRename: false, + shouldEdit: true, + resolveEditText(newName) { if ( newName.endsWith('.jsx') || newName.endsWith('.js') @@ -245,7 +245,7 @@ export function generate( scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + '\n', 'scriptSetup', 0, - FileRangeCapabilities.full, + {}, ]); } function generateExportDefaultEndMapping() { @@ -257,7 +257,7 @@ export function generate( '', 'scriptSetup', scriptSetup.content.length, - { diagnostic: true }, + { diagnostics: true }, ]); codes.push(`\n`); } @@ -279,7 +279,7 @@ export function generate( scriptSetup.generic, scriptSetup.name, scriptSetup.genericOffset, - FileRangeCapabilities.full, + {}, ]); if (!scriptSetup.generic.endsWith(',')) { codes.push(`,`); @@ -410,14 +410,12 @@ export function generate( const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); const propMirror = definePropMirrors[propName]; if (propMirror) { - mirrorBehaviorMappings.push({ - sourceRange: [defineProp.name.start + scriptSetupGeneratedOffset, defineProp.name.end + scriptSetupGeneratedOffset], - generatedRange: propMirror, - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], - }); + mirrorBehaviorMappings.push([ + undefined, + [defineProp.name.start + scriptSetupGeneratedOffset, defineProp.name.end + scriptSetupGeneratedOffset], + propMirror, + [{}, {}], + ]); } } } @@ -761,14 +759,12 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: const scriptEnd = getLength(codes); codes.push(',\n'); - mirrorBehaviorMappings.push({ - sourceRange: [scriptStart, scriptEnd], - generatedRange: [templateStart, templateEnd], - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], - }); + mirrorBehaviorMappings.push([ + undefined, + [scriptStart, scriptEnd], + [templateStart, templateEnd], + [{}, {}], + ]); } } codes.push(`};\n`); // return { @@ -793,7 +789,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: componentsOption.start, { references: true, - rename: true, + renameEdits: true, }, ]); } @@ -907,7 +903,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: classRange.start, { references: true, - referencesCodeLens, + __referencesCodeLens: referencesCodeLens, }, ]); codes.push(`'`); @@ -917,9 +913,11 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: [classRange.start, classRange.end], { references: true, - rename: { - normalize: normalizeCssRename, - apply: applyCssRename, + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: normalizeCssRename, + resolveEditText: applyCssRename, }, }, ]); @@ -954,8 +952,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: style.name, cssBind.offset + fragOffset, onlyForErrorMapping - ? { diagnostic: true } - : FileRangeCapabilities.full, + ? { diagnostics: true } + : {}, ]); } }, @@ -999,7 +997,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, start, - FileRangeCapabilities.full, // diagnostic also working for setup() returns unused in template checking + {}, // diagnostic also working for setup() returns unused in template checking ]); muggle.resetOffsetStack(); } @@ -1011,8 +1009,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: start, { references: true, - definition: true, - rename: true, + definitions: true, + renameEdits: true, }, ]); muggle.resetOffsetStack(); diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 6a069f79a3..8df3ee1589 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1,18 +1,39 @@ -import { FileRangeCapabilities } from '@volar/language-core'; import { Segment } from '@volar/source-map'; import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import { minimatch } from 'minimatch'; import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import { Sfc, VueCompilerOptions } from '../types'; +import { Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; import { hyphenateAttr, hyphenateTag } from '../utils/shared'; import { collectVars, walkInterpolationFragment } from '../utils/transform'; const capabilitiesPresets = { - all: FileRangeCapabilities.full, + disabledAll: { + diagnostics: false, + renameEdits: false, + formattingEdits: false, + completionItems: false, + definitions: false, + references: false, + foldingRanges: false, + inlayHints: false, + codeActions: false, + symbols: false, + selectionRanges: false, + linkedEditingRanges: false, + colors: false, + autoInserts: false, + codeLenses: false, + highlights: false, + links: false, + semanticTokens: false, + hover: false, + signatureHelps: false, + } satisfies VueCodeInformation, + all: {} satisfies VueCodeInformation, allWithHiddenParam: { - ...FileRangeCapabilities.full, __hint: { + __hint: { setting: 'vue.inlayHints.inlineHandlerLeading', label: '$event =>', tooltip: [ @@ -21,20 +42,20 @@ const capabilitiesPresets = { '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', ].join('\n\n'), paddingRight: true, - } /* TODO */ - } as FileRangeCapabilities, - noDiagnostic: { ...FileRangeCapabilities.full, diagnostic: false } satisfies FileRangeCapabilities, - diagnosticOnly: { diagnostic: true } satisfies FileRangeCapabilities, - tagHover: { hover: true } satisfies FileRangeCapabilities, - event: { hover: true, diagnostic: true } satisfies FileRangeCapabilities, - tagReference: { references: true, definition: true, rename: { normalize: undefined, apply: noEditApply } } satisfies FileRangeCapabilities, - attr: { hover: true, diagnostic: true, references: true, definition: true, rename: true } satisfies FileRangeCapabilities, - attrReference: { references: true, definition: true, rename: true } satisfies FileRangeCapabilities, - slotProp: { references: true, definition: true, rename: true, diagnostic: true } satisfies FileRangeCapabilities, - scopedClassName: { references: true, definition: true, rename: true, completion: true } satisfies FileRangeCapabilities, - slotName: { hover: true, diagnostic: true, references: true, definition: true, completion: true } satisfies FileRangeCapabilities, - slotNameExport: { hover: true, diagnostic: true, references: true, definition: true, /* referencesCodeLens: true */ } satisfies FileRangeCapabilities, - refAttr: { references: true, definition: true, rename: true } satisfies FileRangeCapabilities, + } + } as VueCodeInformation, + noDiagnostics: { diagnostics: false } satisfies VueCodeInformation, + diagnosticOnly: { diagnostics: true } satisfies VueCodeInformation, + tagHover: { hover: true } satisfies VueCodeInformation, + event: { hover: true, diagnostics: true } satisfies VueCodeInformation, + tagReference: { references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } } satisfies VueCodeInformation, + attr: { hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, + attrReference: { references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, + slotProp: { references: true, definitions: true, renameEdits: true, diagnostics: true } satisfies VueCodeInformation, + scopedClassName: { references: true, definitions: true, renameEdits: true, completionItems: true } satisfies VueCodeInformation, + slotName: { hover: true, diagnostics: true, references: true, definitions: true, completionItems: true } satisfies VueCodeInformation, + slotNameExport: { hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ } satisfies VueCodeInformation, + refAttr: { references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, }; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], @@ -63,7 +84,7 @@ const transformContext: CompilerDOM.TransformContext = { expressionPlugins: ['typescript'], }; -type Code = Segment; +type Code = Segment; export function generate( ts: typeof import('typescript/lib/tsserverlibrary'), @@ -153,7 +174,7 @@ export function generate( slot.loc, { ...capabilitiesPresets.slotNameExport, - referencesCodeLens: true, + __referencesCodeLens: true, }, ], slot.nodeLoc), ); @@ -173,8 +194,8 @@ export function generate( offset, { ...capabilitiesPresets.scopedClassName, - displayWithLink: stylesScopedClasses.has(className), - }, + __displayWithLink: stylesScopedClasses.has(className), + } satisfies VueCodeInformation, ])); codes.push(`];\n`); } @@ -235,9 +256,11 @@ export function generate( tagRange, { ...capabilitiesPresets.tagReference, - rename: { - normalize: tagName === name ? capabilitiesPresets.tagReference.rename.normalize : camelizeComponentName, - apply: getTagRenameApply(tagName), + renameEdits: { + shouldEdit: true, + shouldRename: true, + resolveNewName: tagName !== name ? camelizeComponentName : undefined, + resolveEditText: getTagRenameApply(tagName), }, ...nativeTags.has(tagName) ? { ...capabilitiesPresets.tagHover, @@ -269,9 +292,9 @@ export function generate( 'template', tagRange, { - completion: { - additional: true, - autoImportOnly: true, + completionItems: { + isAdditional: true, + onlyImport: true, }, }, ]); @@ -331,10 +354,10 @@ export function generate( continue; } const cap = code[3]; - if (cap.diagnostic) { + if (cap.diagnostics) { code[3] = { ...cap, - diagnostic: false, + diagnostics: false, }; } } @@ -352,10 +375,10 @@ export function generate( continue; } const cap = code[3]; - if (cap.diagnostic) { + if (cap.diagnostics) { code[3] = { ...cap, - diagnostic: { + diagnostics: { shouldReport: suppressError, }, }; @@ -367,7 +390,7 @@ export function generate( 'template', [expectedErrorNode.loc.start.offset, expectedErrorNode.loc.end.offset], { - diagnostic: { + diagnostics: { shouldReport: () => errors === 0, }, }, @@ -907,7 +930,7 @@ export function generate( 'default', 'template', [slotDir.loc.start.offset, slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0)], - { ...capabilitiesPresets.slotName, completion: false }, + { ...capabilitiesPresets.slotName, completionItems: false }, ]) ), ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, capabilitiesPresets.diagnosticOnly], @@ -943,7 +966,7 @@ export function generate( '', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), - { completion: true }, + { completionItems: true }, ], `'/* empty slot name completion */]\n`, ); @@ -998,13 +1021,15 @@ export function generate( [prop.arg.loc.start.offset, prop.arg.loc.end.offset], { ...capabilitiesPresets.attrReference, - rename: { + renameEdits: { + shouldRename: true, + shouldEdit: true, // @click-outside -> onClickOutside - normalize(newName) { + resolveNewName(newName) { return camelize('on-' + newName); }, // onClickOutside -> @click-outside - apply(newName) { + resolveEditText(newName) { const hName = hyphenateAttr(newName); if (hyphenateAttr(newName).startsWith('on-')) { return camelize(hName.slice('on-'.length)); @@ -1170,22 +1195,22 @@ export function generate( const codes: Code[] = []; - let caps_all: FileRangeCapabilities = capabilitiesPresets.all; - let caps_diagnosticOnly: FileRangeCapabilities = capabilitiesPresets.diagnosticOnly; - let caps_attr: FileRangeCapabilities = capabilitiesPresets.attr; + let caps_all: VueCodeInformation = capabilitiesPresets.all; + let caps_diagnosticOnly: VueCodeInformation = capabilitiesPresets.diagnosticOnly; + let caps_attr: VueCodeInformation = capabilitiesPresets.attr; if (mode === 'extraReferences') { caps_all = { references: caps_all.references, - rename: caps_all.rename, + renameEdits: caps_all.renameEdits, }; caps_diagnosticOnly = { references: caps_diagnosticOnly.references, - rename: caps_diagnosticOnly.rename, + renameEdits: caps_diagnosticOnly.renameEdits, }; caps_attr = { references: caps_attr.references, - rename: caps_attr.rename, + renameEdits: caps_attr.renameEdits, }; } @@ -1272,9 +1297,11 @@ export function generate( [prop.arg.loc.start.offset, prop.arg.loc.start.offset + attrNameText.length], // patch style attr, { ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: camelized ? hyphenateAttr : undefined, }, }, ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), @@ -1288,9 +1315,11 @@ export function generate( [prop.arg.loc.start.offset, prop.arg.loc.end.offset], { ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: camelized ? hyphenateAttr : undefined, }, }, ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), @@ -1367,9 +1396,11 @@ export function generate( [prop.loc.start.offset, prop.loc.start.offset + prop.name.length], { ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: camelized ? hyphenateAttr : undefined, }, }, ], (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {})) @@ -1472,7 +1503,11 @@ export function generate( content, 'template', prop.arg.loc.start.offset + start, - capabilitiesPresets.all, + { + formattingEdits: false, + foldingRanges: false, + symbols: false, + }, ]); cssCodes.push(` }\n`); } @@ -1526,14 +1561,16 @@ export function generate( 'template', [prop.loc.start.offset, prop.loc.start.offset + 'v-'.length + prop.name.length], { - ...capabilitiesPresets.noDiagnostic, - completion: { + ...capabilitiesPresets.noDiagnostics, + completionItems: { // fix https://github.com/vuejs/language-tools/issues/1905 - additional: true, + isAdditional: true, }, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.name), }, }, ], @@ -1694,9 +1731,11 @@ export function generate( [prop.arg.loc.start.offset, prop.arg.loc.end.offset], { ...capabilitiesPresets.slotProp, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.arg.content), + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.arg.content), }, }, ], prop.arg.loc), @@ -1723,9 +1762,11 @@ export function generate( prop.loc.start.offset, { ...capabilitiesPresets.attr, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.name), }, }, ], prop.loc), @@ -1806,7 +1847,15 @@ export function generate( codes.push('['); for (const _vars of tempVars) { for (const v of _vars) { - codes.push([v.text, 'template', v.offset, { completion: { additional: true } }]); + codes.push([ + v.text, + 'template', + v.offset, + { + ...capabilitiesPresets, + completionItems: { isAdditional: true }, + }, + ]); codes.push(','); } } @@ -1819,7 +1868,16 @@ export function generate( function createFormatCode(mapCode: string, sourceOffset: number, formatWrapper: [string, string]): Code[] { return [ formatWrapper[0], - [mapCode, 'template', sourceOffset, { completion: true /* fix vue-autoinsert-parentheses not working */ }], + [ + mapCode, + 'template', + sourceOffset, + { + ...capabilitiesPresets, + formattingEdits: true, + linkedEditingRanges: true, // support vue-autoinsert-parentheses + }, + ], formatWrapper[1], '\n', ]; @@ -1851,7 +1909,7 @@ export function generate( _code: string, astHolder: any, start: number | undefined, - data: FileRangeCapabilities | (() => FileRangeCapabilities) | undefined, + data: VueCodeInformation | (() => VueCodeInformation) | undefined, prefix: string, suffix: string, ): Code[] { @@ -2007,15 +2065,11 @@ function camelizeComponentName(newName: string) { } function getTagRenameApply(oldName: string) { - return oldName === hyphenateTag(oldName) ? hyphenateTag : noEditApply; + return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; } function getPropRenameApply(oldName: string) { - return oldName === hyphenateAttr(oldName) ? hyphenateAttr : noEditApply; -} - -function noEditApply(n: string) { - return n; + return oldName === hyphenateAttr(oldName) ? hyphenateAttr : undefined; } function getModelValuePropName(node: CompilerDOM.ElementNode, vueVersion: number, vueCompilerOptions: VueCompilerOptions) { diff --git a/packages/language-core/src/languageModule.ts b/packages/language-core/src/languageModule.ts index 283a098eaa..5e34a58962 100644 --- a/packages/language-core/src/languageModule.ts +++ b/packages/language-core/src/languageModule.ts @@ -84,17 +84,22 @@ export function createVueLanguage( sourceFile.update(snapshot); }, typescript: { - resolveProjectHost(host) { + resolveSourceFileName(tsFileName) { + const baseName = path.basename(tsFileName); + if (baseName.indexOf('.vue.')) { // .vue.ts .vue.d.ts .vue.js .vue.jsx .vue.tsx + return tsFileName.substring(0, tsFileName.lastIndexOf('.vue.') + '.vue'.length); + } + }, + resolveModuleName(moduleName, impliedNodeFormat) { + if (impliedNodeFormat === 99 satisfies ts.ModuleKind.ESNext && vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) { + return `${moduleName}.js`; + } + }, + resolveLanguageServiceHost(host) { const sharedTypesSnapshot = ts.ScriptSnapshot.fromString(sharedTypes.getTypesCode(vueCompilerOptions)); const sharedTypesFileName = path.join(host.getCurrentDirectory(), sharedTypes.baseName); return { ...host, - resolveModuleName(moduleName, impliedNodeFormat) { - if (impliedNodeFormat === ts.ModuleKind.ESNext && vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) { - return `${moduleName}.js`; - } - return host.resolveModuleName?.(moduleName, impliedNodeFormat) ?? moduleName; - }, getScriptFileNames() { return [ sharedTypesFileName, diff --git a/packages/language-core/src/plugins/file-md.ts b/packages/language-core/src/plugins/file-md.ts index b4d37b339b..e9883a29f8 100644 --- a/packages/language-core/src/plugins/file-md.ts +++ b/packages/language-core/src/plugins/file-md.ts @@ -74,8 +74,8 @@ const plugin: VueLanguagePlugin = () => { return sfc; function transformRange(block: SFCBlock) { - block.loc.start.offset = file2VueSourceMap.toSourceOffset(block.loc.start.offset)?.[0] ?? -1; - block.loc.end.offset = file2VueSourceMap.toSourceOffset(block.loc.end.offset)?.[0] ?? -1; + block.loc.start.offset = file2VueSourceMap.getSourceOffset(block.loc.start.offset)?.[0] ?? -1; + block.loc.end.offset = file2VueSourceMap.getSourceOffset(block.loc.end.offset)?.[0] ?? -1; } }; } diff --git a/packages/language-core/src/plugins/vue-sfc-customblocks.ts b/packages/language-core/src/plugins/vue-sfc-customblocks.ts index b2bdf2e79e..835e877416 100644 --- a/packages/language-core/src/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/src/plugins/vue-sfc-customblocks.ts @@ -1,4 +1,3 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; import { VueLanguagePlugin } from '../types'; const customBlockReg = /^(.*)\.customBlock_([^_]+)_(\d+)\.([^.]+)$/; @@ -24,12 +23,11 @@ const plugin: VueLanguagePlugin = () => { const index = parseInt(match[3]); const customBlock = sfc.customBlocks[index]; - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ customBlock.content, customBlock.name, 0, - FileRangeCapabilities.full, + {}, ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-scripts.ts b/packages/language-core/src/plugins/vue-sfc-scripts.ts index e50695bf7c..af566e5a5a 100644 --- a/packages/language-core/src/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/src/plugins/vue-sfc-scripts.ts @@ -1,4 +1,3 @@ -import { FileCapabilities, FileKind } from '@volar/language-core'; import { VueLanguagePlugin } from '../types'; const scriptFormatReg = /^(.*)\.script_format\.([^.]+)$/; @@ -26,19 +25,19 @@ const plugin: VueLanguagePlugin = () => { const scriptSetupMatch = embeddedFile.fileName.match(scriptSetupFormatReg); const script = scriptMatch ? sfc.script : scriptSetupMatch ? sfc.scriptSetup : undefined; if (script) { - embeddedFile.kind = FileKind.TextFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - diagnostic: false, - codeAction: false, - inlayHint: false, - }; embeddedFile.content.push([ script.content, script.name, 0, {}, ]); + embeddedFile.content.forEach(code => { + if (typeof code !== 'string') { + code[3].diagnostics = false; + code[3].codeActions = false; + code[3].inlayHints = false; + } + }); } }, }; diff --git a/packages/language-core/src/plugins/vue-sfc-styles.ts b/packages/language-core/src/plugins/vue-sfc-styles.ts index b750349a1d..9aacbd28aa 100644 --- a/packages/language-core/src/plugins/vue-sfc-styles.ts +++ b/packages/language-core/src/plugins/vue-sfc-styles.ts @@ -1,4 +1,3 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; import { VueLanguagePlugin } from '../types'; const styleReg = /^(.*)\.style_(\d+)\.([^.]+)$/; @@ -24,12 +23,11 @@ const plugin: VueLanguagePlugin = () => { const index = parseInt(match[2]); const style = sfc.styles[index]; - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ style.content, style.name, 0, - FileRangeCapabilities.full, + {}, ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-template.ts b/packages/language-core/src/plugins/vue-sfc-template.ts index b21e8e8cd4..439d4d565e 100644 --- a/packages/language-core/src/plugins/vue-sfc-template.ts +++ b/packages/language-core/src/plugins/vue-sfc-template.ts @@ -1,4 +1,3 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; import { VueLanguagePlugin } from '../types'; const templateReg = /^(.*)\.template\.([^.]+)$/; @@ -19,12 +18,11 @@ const plugin: VueLanguagePlugin = () => { resolveEmbeddedFile(_fileName, sfc, embeddedFile) { const match = embeddedFile.fileName.match(templateReg); if (match && sfc.template) { - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ sfc.template.content, sfc.template.name, 0, - FileRangeCapabilities.full, + {}, ]); } }, diff --git a/packages/language-core/src/plugins/vue-tsx.ts b/packages/language-core/src/plugins/vue-tsx.ts index b3cb5e826b..092658444a 100644 --- a/packages/language-core/src/plugins/vue-tsx.ts +++ b/packages/language-core/src/plugins/vue-tsx.ts @@ -1,11 +1,11 @@ +import type { CodeInformation } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; +import * as muggle from 'muggle-string'; import { generate as generateScript } from '../generators/script'; import { generate as generateTemplate } from '../generators/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges'; import { Sfc, VueLanguagePlugin } from '../types'; -import { FileCapabilities, FileKind } from '@volar/language-core'; -import * as muggle from 'muggle-string'; const templateFormatReg = /^\.template_format\.ts$/; const templateStyleCssReg = /^\.template_style\.css$/; @@ -43,35 +43,34 @@ const plugin: VueLanguagePlugin = (ctx) => { resolveEmbeddedFile(fileName, sfc, embeddedFile) { const _tsx = useTsx(fileName, sfc); + const lang = _tsx.lang(); const suffix = embeddedFile.fileName.replace(fileName, ''); - if (suffix === '.' + _tsx.lang()) { - embeddedFile.kind = FileKind.TypeScriptHostFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - foldingRange: false, - documentFormatting: false, - documentSymbol: false, + if (suffix === '.' + lang) { + embeddedFile.typescript = { + scriptKind: lang === 'js' ? ctx.modules.typescript.ScriptKind.JS + : lang === 'jsx' ? ctx.modules.typescript.ScriptKind.JSX + : lang === 'tsx' ? ctx.modules.typescript.ScriptKind.TSX + : ctx.modules.typescript.ScriptKind.TS }; const tsx = _tsx.generatedScript(); if (tsx) { const [content, contentStacks] = ctx.codegenStack ? muggle.track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + content.forEach(code => { + if (typeof code !== 'string') { + code[3].foldingRanges = false; + code[3].formattingEdits = false; + code[3].symbols = false; + } + }); embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; - embeddedFile.mirrorBehaviorMappings = [...tsx.mirrorBehaviorMappings]; + embeddedFile.linkedCodeMappings = [...tsx.mirrorBehaviorMappings]; } } else if (suffix.match(templateFormatReg)) { embeddedFile.parentFileName = fileName + '.template.' + sfc.template?.lang; - embeddedFile.kind = FileKind.TextFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - diagnostic: false, - foldingRange: false, - codeAction: false, - inlayHint: false, - }; const template = _tsx.generatedTemplate(); if (template) { @@ -105,12 +104,9 @@ const plugin: VueLanguagePlugin = (ctx) => { const [content, contentStacks] = ctx.codegenStack ? muggle.track([...template.cssCodes], [...template.cssCodeStacks]) : [[...template.cssCodes], [...template.cssCodeStacks]]; - embeddedFile.content = content; + embeddedFile.content = content as muggle.Segment[]; embeddedFile.contentStacks = contentStacks; } - - // for color pickers support - embeddedFile.capabilities.documentSymbol = true; } }, }; diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index 4096cadafd..e88c6e048b 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -2,6 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCParseResult } from '@vue/compiler-sfc'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { VueEmbeddedFile } from './virtualFile/embeddedFile'; +import type { CodeInformation } from '@volar/language-core'; export type { SFCParseResult } from '@vue/compiler-sfc'; @@ -10,6 +11,18 @@ export type RawVueCompilerOptions = Partial { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, FileRangeCapabilities.full]]; + const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, {}]]; for (const block of [ sfc.script, sfc.scriptSetup, @@ -30,15 +29,16 @@ export function computedMappings( ); } } - return str.map>((m) => { + return str.map>((m) => { const text = m[0]; const start = m[2] as number; const end = start + text.length; - return { - sourceRange: [start, end], - generatedRange: [start, end], - data: m[3] as FileRangeCapabilities, - }; + return [ + undefined, + [start, end], + [start, end], + m[3] as VueCodeInformation, + ]; }); }); } diff --git a/packages/language-core/src/virtualFile/embeddedFile.ts b/packages/language-core/src/virtualFile/embeddedFile.ts index 001ec2e871..0520b35562 100644 --- a/packages/language-core/src/virtualFile/embeddedFile.ts +++ b/packages/language-core/src/virtualFile/embeddedFile.ts @@ -1,16 +1,15 @@ -import { FileCapabilities, FileKind, FileRangeCapabilities, MirrorBehaviorCapabilities } from '@volar/language-core'; +import { CodeInformation, LinkedCodeTrigger, VirtualFile } from '@volar/language-core'; import { Mapping, Segment, StackNode } from '@volar/source-map'; export class VueEmbeddedFile { public parentFileName?: string; - public kind = FileKind.TextFile; - public capabilities: FileCapabilities = {}; - public mirrorBehaviorMappings: Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; + public typescript: VirtualFile['typescript']; + public linkedCodeMappings: Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; constructor( public fileName: string, - public content: Segment[], + public content: Segment[], public contentStacks: StackNode[], ) { } } diff --git a/packages/language-core/src/virtualFile/vueFile.ts b/packages/language-core/src/virtualFile/vueFile.ts index 35e65e59e4..79546b53ef 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileKind, VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; +import { VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; import { Stack } from '@volar/source-map'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { VueCompilerOptions, VueLanguagePlugin } from '../types'; @@ -25,20 +25,16 @@ export class VueFile implements VirtualFile { // others - capabilities = FileCapabilities.full; - kind = FileKind.TextFile; codegenStacks: Stack[] = []; get embeddedFiles() { return this.getEmbeddedFiles(); } get mainTsFile() { - let result: VirtualFile | undefined; - forEachEmbeddedFile(this, file => { - if (file.kind === FileKind.TypeScriptHostFile && file.id.substring(this.id.length).match(jsxReg)) { - result = file; + for (const file of forEachEmbeddedFile(this)) { + if (file.typescript && file.id.substring(this.id.length).match(jsxReg)) { + return file; } - }); - return result; + } } get snapshot() { return this._snapshot(); diff --git a/packages/language-plugin-pug/src/index.ts b/packages/language-plugin-pug/src/index.ts index d6e078a922..a96af4647c 100644 --- a/packages/language-plugin-pug/src/index.ts +++ b/packages/language-plugin-pug/src/index.ts @@ -38,7 +38,7 @@ const plugin: VueLanguagePlugin = ({ modules }) => { get(target, prop) { if (prop === 'offset') { const htmlOffset = target.offset; - for (const mapped of map.toSourceOffsets(htmlOffset)) { + for (const mapped of map.getSourceOffsets(htmlOffset)) { return mapped[0]; } return -1; diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 27968b3d41..50e1118444 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -32,7 +32,7 @@ export function createServerPlugin(connection: Connection) { return { extraFileExtensions: vueFileExtensions.map(ext => ({ extension: ext, isMixedContent: true, scriptKind: ts.ScriptKind.Deferred })), watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], - async resolveConfig(config, env, projectHost) { + async resolveConfig(config, env, info) { const vueOptions = await getVueCompilerOptions(); @@ -40,7 +40,7 @@ export function createServerPlugin(connection: Connection) { envToVueOptions.set(env, vue.resolveVueCompilerOptions(vueOptions)); } - config.languages = vue.resolveLanguages(ts, config.languages ?? {}, projectHost?.getCompilationSettings() ?? {}, vueOptions, options.codegenStack); + config.languages = vue.resolveLanguages(ts, config.languages ?? {}, info?.parsedCommandLine.options ?? {}, vueOptions, options.codegenStack); config.services = vue.resolveServices(config.services ?? {}, vueOptions); return config; @@ -49,18 +49,18 @@ export function createServerPlugin(connection: Connection) { let vueOptions: Partial = {}; - if (env && projectHost) { + if (env && info) { const sys = createSys(ts, env, env.uriToFileName(env.workspaceFolder.uri.toString())); let sysVersion: number | undefined; let newSysVersion = await sys.sync(); while (sysVersion !== newSysVersion) { sysVersion = newSysVersion; - if (projectHost.configFileName) { - vueOptions = vue2.createParsedCommandLine(ts, sys, projectHost.configFileName).vueOptions; + if (info.configFileName) { + vueOptions = vue2.createParsedCommandLine(ts, sys, info.configFileName).vueOptions; } else { - vueOptions = vue2.createParsedCommandLineByJson(ts, sys, projectHost.getCurrentDirectory(), projectHost.getCompilationSettings()).vueOptions; + vueOptions = vue2.createParsedCommandLineByJson(ts, sys, env.uriToFileName(env.workspaceFolder.uri.toString()), info.parsedCommandLine.options).vueOptions; } newSysVersion = await sys.sync(); } @@ -120,17 +120,18 @@ export function createServerPlugin(connection: Connection) { const langaugeService = project.getLanguageService(); let checker = checkers.get(project); - if (!checker) { - checker = componentMeta.baseCreate( - ts, - langaugeService.context.project.typescript!.projectHost, - envToVueOptions.get(langaugeService.context.env)!, - {}, - langaugeService.context.project.typescript!.projectHost.getCurrentDirectory() + '/tsconfig.json.global.vue', - ); - checkers.set(project, checker); - } - return checker.getComponentMeta(langaugeService.context.env.uriToFileName(params.uri)); + // if (!checker) { + // checker = componentMeta.baseCreate( + // ts, + // langaugeService.context.project.typescript?.configFileName, + // langaugeService.context.project.typescript!.projectHost, + // envToVueOptions.get(langaugeService.context.env)!, + // {}, + // langaugeService.context.project.typescript!.projectHost.getCurrentDirectory() + '/tsconfig.json.global.vue', + // ); + // checkers.set(project, checker); + // } + return checker?.getComponentMeta(langaugeService.context.env.uriToFileName(params.uri)); }); async function getService(uri: string) { diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts index 41ddc6203e..193178e5ae 100644 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ b/packages/language-service/src/ideFeatures/dragImport.ts @@ -18,12 +18,16 @@ export function getDragImportEdits( additionalEdits: vscode.TextEdit[]; } | undefined { + const sourceFile = ctx.project.fileProvider.getSourceFile(uri); + if (!sourceFile) + return; + let baseName = importUri.substring(importUri.lastIndexOf('/') + 1); baseName = baseName.substring(0, baseName.lastIndexOf('.')); const newName = capitalize(camelize(baseName)); - const document = ctx!.getTextDocument(uri)!; - const [vueFile] = ctx!.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; + const document = ctx.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); + const [vueFile] = ctx.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; const { sfc } = vueFile; const script = sfc.scriptSetup ?? sfc.script; diff --git a/packages/language-service/src/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index b49f91ca0a..af5273b425 100644 --- a/packages/language-service/src/ideFeatures/nameCasing.ts +++ b/packages/language-service/src/ideFeatures/nameCasing.ts @@ -13,7 +13,7 @@ export async function convertTagName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -23,7 +23,7 @@ export async function convertTagName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByUri(rootFile.id, rootFile.languageId, rootFile.snapshot); + const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); @@ -56,7 +56,7 @@ export async function convertAttrName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -66,7 +66,7 @@ export async function convertAttrName( const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByUri(rootFile.id, rootFile.languageId, rootFile.snapshot); + const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); @@ -128,7 +128,7 @@ export function detect( attr: AttrNameCasing[], } { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.root; + const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) { return { tag: [], diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 7f70b65e3a..c8a6f21565 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -93,13 +93,13 @@ export function resolveServices( if (virtualFile && sourceFile) { - for (const map of ctx.documents.getMapsByVirtualFile(virtualFile)) { + for (const map of ctx.documents.getMaps(virtualFile)) { - const sourceVirtualFile = ctx.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + const sourceVirtualFile = ctx.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; if (sourceVirtualFile instanceof VueFile) { - const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.autoImportOnly); + const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completionItems === 'object' && !!data.completionItems.onlyImport); if (isAutoImport) { for (const item of result.items) { @@ -183,7 +183,7 @@ export function resolveServices( const componentName = newName ?? item.textEdit.newText; const optionEdit = ExtractComponentService.createAddComponentToOptionEdit(ts, ast, componentName); if (optionEdit) { - const textDoc = ctx.documents.getDocumentByUri(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); + const textDoc = ctx.documents.get(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); item.additionalTextEdits.push({ range: { start: textDoc.positionAt(optionEdit.range.start), diff --git a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts index f6e134a86f..ad87f690e5 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -1,4 +1,4 @@ -import { Service } from '@volar/language-service'; +import { MappingKey, Service } from '@volar/language-service'; import { isCharacterTyping } from './vue-autoinsert-dotvalue'; const plugin: Service = (context, modules) => { @@ -31,8 +31,8 @@ const plugin: Service = (context, modules) => { const offset = document.offsetAt(position); for (const mappedRange of virtualFile.mappings) { - if (mappedRange.generatedRange[1] === offset) { - const text = document.getText().substring(mappedRange.generatedRange[0], mappedRange.generatedRange[1]); + if (mappedRange[MappingKey.GENERATED_CODE_RANGE][1] === offset) { + const text = document.getText().substring(mappedRange[MappingKey.GENERATED_CODE_RANGE][0], mappedRange[MappingKey.GENERATED_CODE_RANGE][1]); const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); if (ast.statements.length === 1) { const statement = ast.statements[0]; @@ -63,8 +63,8 @@ const plugin: Service = (context, modules) => { .replaceAll('}', '\\}'); return { range: { - start: document.positionAt(mappedRange.generatedRange[0]), - end: document.positionAt(mappedRange.generatedRange[1]), + start: document.positionAt(mappedRange[MappingKey.GENERATED_CODE_RANGE][0]), + end: document.positionAt(mappedRange[MappingKey.GENERATED_CODE_RANGE][1]), }, newText: '(' + escapedText + '$0' + ')', }; diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index bded680896..6d15e91e62 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,5 +1,5 @@ import { Service } from '@volar/language-service'; -import { SourceFile, VueFile } from '@vue/language-core'; +import { MappingKey, SourceFile, VueCodeInformation, VueFile } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; export const create = function (): Service { @@ -13,19 +13,19 @@ export const create = function (): Service { provideReferencesCodeLensRanges(document) { - return worker(document.uri, async (_, sourceFile) => { + return worker(document.uri, async (vueFile) => { const result: vscode.Range[] = []; - for (const [_, map] of context.documents.getMapsBySourceFile(sourceFile) ?? []) { - for (const mapping of map.map.mappings) { + for (const map of context.documents.getMaps(vueFile) ?? []) { + for (const mapping of map.map.codeMappings) { - if (!mapping.data.referencesCodeLens) + if (!(mapping[MappingKey.DATA] as VueCodeInformation).__referencesCodeLens) continue; result.push({ - start: document.positionAt(mapping.sourceRange[0]), - end: document.positionAt(mapping.sourceRange[1]), + start: document.positionAt(mapping[MappingKey.SOURCE_CODE_RANGE][0]), + end: document.positionAt(mapping[MappingKey.SOURCE_CODE_RANGE][1]), }); } } @@ -38,7 +38,7 @@ export const create = function (): Service { await worker(document.uri, async (vueFile) => { - const document = context.documents.getDocumentByUri(vueFile.id, vueFile.languageId, vueFile.snapshot); + const document = context.documents.get(vueFile.id, vueFile.languageId, vueFile.snapshot); const offset = document.offsetAt(range.start); const blocks = [ vueFile.sfc.script, diff --git a/packages/language-service/src/plugins/vue-extract-file.ts b/packages/language-service/src/plugins/vue-extract-file.ts index 63a544d133..dd5029cc22 100644 --- a/packages/language-service/src/plugins/vue-extract-file.ts +++ b/packages/language-service/src/plugins/vue-extract-file.ts @@ -1,6 +1,6 @@ import { CreateFile, Service, ServiceContext, TextDocumentEdit, TextEdit } from '@volar/language-service'; import { ExpressionNode, type TemplateChildNode } from '@vue/compiler-dom'; -import { Sfc, VueFile, scriptRanges } from '@vue/language-core'; +import { MappingKey, Sfc, VueFile, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Provide } from 'volar-service-typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -62,9 +62,9 @@ export const create = function (): Service { async resolveCodeAction(codeAction) { const { uri, range, newName } = codeAction.data as ActionData; - const document = ctx!.getTextDocument(uri)!; const [startOffset, endOffset]: [number, number] = range; - const [vueFile] = ctx!.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; + const [vueFile] = ctx!.project.fileProvider.getVirtualFile(uri) as [VueFile, any]; + const document = ctx!.documents.get(uri, vueFile.languageId, vueFile.snapshot)!; const { sfc } = vueFile; const script = sfc.scriptSetup ?? sfc.script; @@ -180,7 +180,7 @@ export const create = function (): Service { }>(); const checker = languageService.getProgram()!.getTypeChecker(); const [virtualFile] = ctx!.project.fileProvider.getVirtualFile(tsScriptUri); - const maps = virtualFile ? [...ctx!.documents.getMapsByVirtualFile(virtualFile)] : []; + const maps = virtualFile ? [...ctx!.documents.getMaps(virtualFile)] : []; sourceFile.forEachChild(function visit(node) { if ( @@ -191,8 +191,8 @@ export const create = function (): Service { ) { const { name } = node; for (const map of maps) { - const source = map.map.toSourceOffset(name.getEnd()); - if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1].data.semanticTokens) { + const source = map.map.getSourceOffset(name.getEnd()); + if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1][MappingKey.DATA].semanticTokens) { if (!result.has(name.text)) { const type = checker.getTypeAtLocation(node); const typeString = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation); diff --git a/packages/language-service/src/plugins/vue-template.ts b/packages/language-service/src/plugins/vue-template.ts index bc41d886af..9040ab06f2 100644 --- a/packages/language-service/src/plugins/vue-template.ts +++ b/packages/language-service/src/plugins/vue-template.ts @@ -1,4 +1,4 @@ -import { FileRangeCapabilities, Service, ServiceContext, SourceMapWithDocuments } from '@volar/language-service'; +import { CodeInformation, Service, ServiceContext, SourceMapWithDocuments } from '@volar/language-service'; import { VueFile, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import * as html from 'vscode-html-languageservice'; @@ -78,8 +78,8 @@ export const create = (options: { const [virtualFile] = _context.project.fileProvider.getVirtualFile(document.uri); if (virtualFile) { - for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { - const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + for (const map of _context.documents.getMaps(virtualFile)) { + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; if (sourceVirtualFile instanceof VueFile) { await provideHtmlData(map, sourceVirtualFile); } @@ -91,8 +91,8 @@ export const create = (options: { return; if (virtualFile) { - for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { - const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + for (const map of _context.documents.getMaps(virtualFile)) { + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; if (sourceVirtualFile instanceof VueFile) { afterHtmlCompletion(htmlComplete, map, sourceVirtualFile); } @@ -117,9 +117,9 @@ export const create = (options: { if (!virtualFile) return; - for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { + for (const map of _context.documents.getMaps(virtualFile)) { - const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; const scanner = options.getScanner(htmlOrPugService, document); if (sourceVirtualFile instanceof VueFile && scanner) { @@ -241,9 +241,9 @@ export const create = (options: { if (!virtualFile) return; - for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { + for (const map of _context.documents.getMaps(virtualFile)) { - const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + const sourceVirtualFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; if (!(sourceVirtualFile instanceof VueFile)) continue; @@ -303,9 +303,9 @@ export const create = (options: { if (!virtualFile) return; - for (const map of _context.documents.getMapsByVirtualFile(virtualFile)) { + for (const map of _context.documents.getMaps(virtualFile)) { - const sourceFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.root; + const sourceFile = _context.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; if (!(sourceFile instanceof VueFile)) continue; @@ -355,7 +355,7 @@ export const create = (options: { }, }; - async function provideHtmlData(map: SourceMapWithDocuments, vueSourceFile: VueFile) { + async function provideHtmlData(map: SourceMapWithDocuments, vueSourceFile: VueFile) { const languageService = _context!.inject('typescript/languageService'); const languageServiceHost = _context!.inject('typescript/languageServiceHost'); @@ -549,7 +549,7 @@ export const create = (options: { ]); } - function afterHtmlCompletion(completionList: vscode.CompletionList, map: SourceMapWithDocuments, vueSourceFile: VueFile) { + function afterHtmlCompletion(completionList: vscode.CompletionList, map: SourceMapWithDocuments, vueSourceFile: VueFile) { const languageService = _context!.inject('typescript/languageService'); const replacement = getReplacement(completionList, map.sourceFileDocument); diff --git a/packages/language-service/src/plugins/vue-twoslash-queries.ts b/packages/language-service/src/plugins/vue-twoslash-queries.ts index bc1057fe09..dfb1384624 100644 --- a/packages/language-service/src/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/src/plugins/vue-twoslash-queries.ts @@ -1,4 +1,4 @@ -import { FileKind, forEachEmbeddedFile, Service, ServiceContext } from '@volar/language-service'; +import { forEachEmbeddedFile, Service, ServiceContext } from '@volar/language-service'; import * as vue from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -29,12 +29,12 @@ const plugin: Service = (context: ServiceContext { - if (virtualFile.kind === FileKind.TypeScriptHostFile) { - for (const map of context.documents.getMapsByVirtualFile(virtualFile)) { + for (const virtualFile of forEachEmbeddedFile(vueFile)) { + if (virtualFile.typescript) { + for (const map of context.documents.getMaps(virtualFile)) { for (const [pointerPosition, hoverOffset] of hoverOffsets) { - for (const [tsOffset, mapping] of map.map.toGeneratedOffsets(hoverOffset)) { - if (mapping.data.hover) { + for (const [tsOffset, mapping] of map.map.getGeneratedOffsets(hoverOffset)) { + if (mapping[vue.MappingKey.DATA].hover) { const quickInfo = languageService.getQuickInfoAtPosition(context.env.uriToFileName(virtualFile.id), tsOffset); if (quickInfo) { inlayHints.push({ @@ -50,7 +50,7 @@ const plugin: Service = (context: ServiceContext { @@ -21,17 +22,11 @@ const plugin: Service = (context) => { for (const mapping of vitualFile.mappings) { - const hint: { - setting: string; - label: string; - tooltip: string; - paddingRight?: boolean; - paddingLeft?: boolean; - } | undefined = (mapping.data as any).__hint; + const hint = (mapping[MappingKey.DATA] as VueCodeInformation).__hint; if ( - mapping.generatedRange[0] >= start - && mapping.generatedRange[1] <= end + mapping[MappingKey.GENERATED_CODE_RANGE][0] >= start + && mapping[MappingKey.GENERATED_CODE_RANGE][1] <= end && hint ) { @@ -44,7 +39,7 @@ const plugin: Service = (context) => { label: hint.label, paddingRight: hint.paddingRight, paddingLeft: hint.paddingLeft, - position: document.positionAt(mapping.generatedRange[0]), + position: document.positionAt(mapping[MappingKey.GENERATED_CODE_RANGE][0]), kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, tooltip: { kind: 'markdown', diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 0f7c0342e1..3585655e89 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -1,4 +1,5 @@ -import { TypeScriptProjectHost, createLanguageService, createTypeScriptProject, resolveCommonLanguageId } from '@volar/language-service'; +import { createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; +import { createProject, ProjectHost } from '@volar/typescript'; import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { URI } from 'vscode-uri'; @@ -17,13 +18,16 @@ function createTester(root: string) { const parsedCommandLine = createParsedCommandLine(ts, ts.sys, realTsConfig); parsedCommandLine.fileNames = parsedCommandLine.fileNames.map(fileName => fileName.replace(/\\/g, '/')); const scriptSnapshots = new Map(); - const projectHost: TypeScriptProjectHost = { - configFileName: realTsConfig, + const serviceEnv = createMockServiceEnv(rootUri, () => currentVSCodeSettings ?? defaultVSCodeSettings); + const projectHost: ProjectHost = { getCurrentDirectory: () => root, getProjectVersion: () => '0', getScriptFileNames: () => parsedCommandLine.fileNames, getCompilationSettings: () => parsedCommandLine.options, getScriptSnapshot, + getFileName: serviceEnv.uriToFileName, + getFileId: serviceEnv.fileNameToUri, + getLanguageId: resolveCommonLanguageId, }; const languages = resolveLanguages(ts, {}, parsedCommandLine.options, parsedCommandLine.vueOptions); const services = resolveServices({}, parsedCommandLine.vueOptions); @@ -32,12 +36,12 @@ function createTester(root: string) { 'javascript.preferences.quoteStyle': 'single', }; let currentVSCodeSettings: any; - const serviceEnv = createMockServiceEnv(rootUri, () => currentVSCodeSettings ?? defaultVSCodeSettings); - const project = createTypeScriptProject( + const project = createProject( + ts, + ts.sys, Object.values(languages), + realTsConfig, projectHost, - serviceEnv.fileNameToUri, - resolveCommonLanguageId ); const languageService = createLanguageService({ typescript: ts as any }, Object.values(services), serviceEnv, project); diff --git a/packages/tsc-eslint-hook/src/index.ts b/packages/tsc-eslint-hook/src/index.ts index f84742efe8..454b42dcd2 100644 --- a/packages/tsc-eslint-hook/src/index.ts +++ b/packages/tsc-eslint-hook/src/index.ts @@ -13,13 +13,13 @@ export = async function ( baseConfig: resolveConfig(tsProgram), useEslintrc: false, }); - const fileNames = program.__vue.project.typescript!.projectHost.getScriptFileNames(); + const fileNames = program.__vue.project.typescript!.languageServiceHost.getScriptFileNames(); const fileProvider = program.__vue.project.fileProvider; const formatter = await eslint.loadFormatter(); for (const fileName of fileNames) { - const vueFile = fileProvider.getSourceFile(fileName)?.root; + const vueFile = fileProvider.getSourceFile(fileName)?.virtualFile?.[0]; if (vueFile) { @@ -27,7 +27,7 @@ export = async function ( const all: typeof vueFile.embeddedFiles = []; vueFile.embeddedFiles.forEach(async function visit(embeddedFile) { - if (embeddedFile.capabilities.diagnostic) { + if (embeddedFile.mappings.some(mapping => mapping[3].diagnostics ?? true)) { all.push(embeddedFile); } embeddedFile.embeddedFiles.forEach(visit); @@ -71,15 +71,19 @@ export = async function ( if (sourceSnapshot !== vueFile.snapshot) continue; - for (const start of map.toSourceOffsets(msgStart)) { + for (const start of map.getSourceOffsets(msgStart)) { - const reportStart = typeof start[1].data.diagnostic === 'object' ? typeof start[1].data.diagnostic.shouldReport() : !!start[1].data.diagnostic; + const reportStart = typeof start[1][3].diagnostics === 'object' + ? typeof start[1][3].diagnostics.shouldReport() + : (start[1][3].diagnostics ?? true); if (!reportStart) continue; - for (const end of map.toSourceOffsets(msgEnd, true)) { + for (const end of map.getSourceOffsets(msgEnd, true)) { - const reportEnd = typeof end[1].data.diagnostic === 'object' ? typeof end[1].data.diagnostic.shouldReport() : !!end[1].data.diagnostic; + const reportEnd = typeof end[1][3].diagnostics === 'object' + ? typeof end[1][3].diagnostics.shouldReport() + : (end[1][3].diagnostics ?? true); if (!reportEnd) continue; diff --git a/packages/tsc/src/index.ts b/packages/tsc/src/index.ts index 2664923368..11e8a2d204 100644 --- a/packages/tsc/src/index.ts +++ b/packages/tsc/src/index.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import * as vue from '@vue/language-core'; -import * as volarTs from '@volar/typescript'; +import { createProject, decorateLanguageService, getDocumentRegistry, getProgram, ProjectHost } from '@volar/typescript'; import { state } from './shared'; export type Hook = (program: _Program) => void; @@ -19,17 +19,10 @@ const windowsPathReg = /\\/g; export function createProgram(options: ts.CreateProgramOptions) { - if (!options.options.noEmit && !options.options.emitDeclarationOnly) - throw toThrow('js emit is not supported'); - - if (!options.options.noEmit && options.options.noEmitOnError) - throw toThrow('noEmitOnError is not supported'); - - if (options.options.extendedDiagnostics || options.options.generateTrace) - throw toThrow('--extendedDiagnostics / --generateTrace is not supported, please run `Write Virtual Files` in VSCode to write virtual files and use `--extendedDiagnostics` / `--generateTrace` via tsc instead of vue-tsc to debug.'); - - if (!options.host) - throw toThrow('!options.host'); + assert(options.options.noEmit || options.options.emitDeclarationOnly, 'js emit is not supported'); + assert(options.options.noEmit || !options.options.noEmitOnError, 'noEmitOnError is not supported'); + assert(!options.options.extendedDiagnostics && !options.options.generateTrace, '--extendedDiagnostics / --generateTrace is not supported, please run `Write Virtual Files` in VSCode to write virtual files and use `--extendedDiagnostics` / `--generateTrace` via tsc instead of vue-tsc to debug.'); + assert(options.host, '!options.host'); const ts = require('typescript') as typeof import('typescript/lib/tsserverlibrary'); @@ -60,8 +53,7 @@ export function createProgram(options: ts.CreateProgramOptions) { modifiedTime: number, scriptSnapshot: ts.IScriptSnapshot, }>(); - const projectHost: vue.TypeScriptProjectHost = { - configFileName: undefined, + const projectHost: ProjectHost = { getCurrentDirectory() { return ctx.options.host!.getCurrentDirectory().replace(windowsPathReg, '/'); }, @@ -75,36 +67,36 @@ export function createProgram(options: ts.CreateProgramOptions) { }, getProjectReferences: () => ctx.options.projectReferences, getCancellationToken: ctx.options.host!.getCancellationToken ? () => ctx.options.host!.getCancellationToken!() : undefined, + getFileId: fileName => fileName, + getFileName: id => id, + getLanguageId: vue.resolveCommonLanguageId, }; - const fileNameResolutionHost = { - fileNameToId: (fileName: string) => fileName, - idToFileName: (id: string) => id, - }; - const project = vue.createTypeScriptProject( + const project = createProject( + ts, + ts.sys, vue.createLanguages( ts, projectHost.getCompilationSettings(), vueCompilerOptions, ), + undefined, projectHost, - fileNameResolutionHost.fileNameToId, - vue.resolveCommonLanguageId, ); - const languageServiceHost = volarTs.createLanguageServiceHost( - project.typescript!.projectHost, - project.fileProvider, - fileNameResolutionHost, - ts, - ts.sys + const vueTsLs = ts.createLanguageService( + project.typescript!.languageServiceHost, + getDocumentRegistry( + ts, + ts.sys.useCaseSensitiveFileNames, + projectHost.getCurrentDirectory() + ) ); - const vueTsLs = ts.createLanguageService(languageServiceHost, volarTs.getDocumentRegistry(ts, ts.sys.useCaseSensitiveFileNames, projectHost.getCurrentDirectory())); - volarTs.decorateLanguageService(project.fileProvider, vueTsLs, false); + decorateLanguageService(project.fileProvider, vueTsLs, false); - program = volarTs.getProgram( + program = getProgram( ts as any, project.fileProvider, - fileNameResolutionHost, + projectHost, vueTsLs, ts.sys ) as (ts.Program & { __vue: ProgramContext; }); @@ -176,7 +168,9 @@ export function createProgram(options: ts.CreateProgramOptions) { return program; } -function toThrow(msg: string) { - console.error(msg); - return msg; +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + console.error(message); + throw new Error(message); + } } diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index cc0aeb3f57..f4a84e17b2 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -16,6 +16,7 @@ const init: ts.server.PluginModuleFactory = (modules) => { info.languageServiceHost.getCompilationSettings(), getVueCompilerOptions(), ), + ts.sys.useCaseSensitiveFileNames, () => { } ); From e2943931485863bb92ce4c2fbc01e8d2531eb8b4 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 24 Nov 2023 16:59:23 +0800 Subject: [PATCH 06/19] second fix [skip ci] --- .../language-core/src/generators/template.ts | 384 ++++++++++-------- 1 file changed, 218 insertions(+), 166 deletions(-) diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 8df3ee1589..2ce8372e18 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -8,31 +8,68 @@ import { Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; import { hyphenateAttr, hyphenateTag } from '../utils/shared'; import { collectVars, walkInterpolationFragment } from '../utils/transform'; -const capabilitiesPresets = { - disabledAll: { - diagnostics: false, - renameEdits: false, - formattingEdits: false, - completionItems: false, - definitions: false, - references: false, - foldingRanges: false, - inlayHints: false, - codeActions: false, - symbols: false, - selectionRanges: false, - linkedEditingRanges: false, - colors: false, - autoInserts: false, - codeLenses: false, - highlights: false, - links: false, - semanticTokens: false, - hover: false, - signatureHelps: false, - } satisfies VueCodeInformation, - all: {} satisfies VueCodeInformation, - allWithHiddenParam: { +const withAllDisabled = (override: VueCodeInformation): VueCodeInformation => ({ + diagnostics: false, + renameEdits: false, + formattingEdits: false, + completionItems: false, + definitions: false, + references: false, + foldingRanges: false, + inlayHints: false, + codeActions: false, + symbols: false, + selectionRanges: false, + linkedEditingRanges: false, + colors: false, + autoInserts: false, + codeLenses: false, + highlights: false, + links: false, + semanticTokens: false, + hover: false, + signatureHelps: false, + ...override, +}); +const withAllEnabled = (override: VueCodeInformation): VueCodeInformation => ({ + diagnostics: true, + renameEdits: true, + formattingEdits: true, + completionItems: true, + definitions: true, + references: true, + foldingRanges: true, + inlayHints: true, + codeActions: true, + symbols: true, + selectionRanges: true, + linkedEditingRanges: true, + colors: true, + autoInserts: true, + codeLenses: true, + highlights: true, + links: true, + semanticTokens: true, + hover: true, + signatureHelps: true, + ...override, +}); +const combineEnabled = (...infos: VueCodeInformation[]): VueCodeInformation => { + const result: VueCodeInformation = { ...infos[0] }; + for (const info of infos) { + for (const key in info) { + const value = info[key as keyof VueCodeInformation]; + if (value) { + result[key as keyof VueCodeInformation] = value as any; + } + } + } + return result; +}; +const presetInfos = { + disabledAll: withAllDisabled({}), + all: withAllEnabled({}), + allWithHiddenParam: withAllEnabled({ __hint: { setting: 'vue.inlayHints.inlineHandlerLeading', label: '$event =>', @@ -43,19 +80,19 @@ const capabilitiesPresets = { ].join('\n\n'), paddingRight: true, } - } as VueCodeInformation, - noDiagnostics: { diagnostics: false } satisfies VueCodeInformation, - diagnosticOnly: { diagnostics: true } satisfies VueCodeInformation, - tagHover: { hover: true } satisfies VueCodeInformation, - event: { hover: true, diagnostics: true } satisfies VueCodeInformation, - tagReference: { references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } } satisfies VueCodeInformation, - attr: { hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, - attrReference: { references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, - slotProp: { references: true, definitions: true, renameEdits: true, diagnostics: true } satisfies VueCodeInformation, - scopedClassName: { references: true, definitions: true, renameEdits: true, completionItems: true } satisfies VueCodeInformation, - slotName: { hover: true, diagnostics: true, references: true, definitions: true, completionItems: true } satisfies VueCodeInformation, - slotNameExport: { hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ } satisfies VueCodeInformation, - refAttr: { references: true, definitions: true, renameEdits: true } satisfies VueCodeInformation, + }), + noDiagnostics: withAllEnabled({ diagnostics: false }), + diagnosticOnly: withAllDisabled({ diagnostics: true }), + tagHover: withAllDisabled({ hover: true }), + event: withAllDisabled({ hover: true, diagnostics: true }), + tagReference: withAllDisabled({ references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } }), + attr: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true }), + attrReference: withAllDisabled({ references: true, definitions: true, renameEdits: true }), + slotProp: withAllDisabled({ references: true, definitions: true, renameEdits: true, diagnostics: true }), + scopedClassName: withAllDisabled({ references: true, definitions: true, renameEdits: true, completionItems: true }), + slotName: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, completionItems: true }), + slotNameExport: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ }), + refAttr: withAllDisabled({ references: true, definitions: true, renameEdits: true }), }; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], @@ -172,10 +209,10 @@ export function generate( name, 'template', slot.loc, - { - ...capabilitiesPresets.slotNameExport, - __referencesCodeLens: true, - }, + combineEnabled( + presetInfos.slotNameExport, + { __referencesCodeLens: true }, + ), ], slot.nodeLoc), ); codes.push(`?(_: typeof ${slot.varName}): any,\n`); @@ -192,10 +229,10 @@ export function generate( className, 'template', offset, - { - ...capabilitiesPresets.scopedClassName, - __displayWithLink: stylesScopedClasses.has(className), - } satisfies VueCodeInformation, + combineEnabled( + presetInfos.scopedClassName, + { __displayWithLink: stylesScopedClasses.has(className) }, + ), ])); codes.push(`];\n`); } @@ -254,19 +291,21 @@ export function generate( name, 'template', tagRange, - { - ...capabilitiesPresets.tagReference, - renameEdits: { - shouldEdit: true, - shouldRename: true, - resolveNewName: tagName !== name ? camelizeComponentName : undefined, - resolveEditText: getTagRenameApply(tagName), + combineEnabled( + presetInfos.tagReference, + { + renameEdits: { + shouldEdit: true, + shouldRename: true, + resolveNewName: tagName !== name ? camelizeComponentName : undefined, + resolveEditText: getTagRenameApply(tagName), + } }, - ...nativeTags.has(tagName) ? { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - } : {}, - }, + ...(nativeTags.has(tagName) ? [ + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ] : []), + ), ]), ';', ); @@ -480,7 +519,7 @@ export function generate( content, node.content.loc, start, - capabilitiesPresets.all, + presetInfos.all, '(', ');\n', ), @@ -535,7 +574,7 @@ export function generate( branch.condition.content, branch.condition.loc, branch.condition.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -590,7 +629,7 @@ export function generate( for (const varName of forBlockVars) localVars.set(varName, (localVars.get(varName) ?? 0) + 1); - codes.push([leftExpressionText, 'template', leftExpressionRange.start, capabilitiesPresets.all]); + codes.push([leftExpressionText, 'template', leftExpressionRange.start, presetInfos.all]); formatCodes.push(...createFormatCode(leftExpressionText, leftExpressionRange.start, formatBrackets.normal)); } codes.push(`] of __VLS_getVForSourceType`); @@ -601,7 +640,7 @@ export function generate( source.content, source.loc, source.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -686,7 +725,7 @@ export function generate( tag, 'template', tagOffsets[0], - capabilitiesPresets.diagnosticOnly, + presetInfos.diagnosticOnly, ]), '];\n', ); @@ -694,14 +733,14 @@ export function generate( else if (isNamespacedTag) { codes.push( `const ${var_originalComponent} = `, - ...createInterpolationCode(tag, node.loc, startTagOffset, capabilitiesPresets.all, '', ''), + ...createInterpolationCode(tag, node.loc, startTagOffset, presetInfos.all, '', ''), ';\n', ); } else if (dynamicTagExp) { codes.push( `const ${var_originalComponent} = `, - ...createInterpolationCode(dynamicTagExp.loc.source, dynamicTagExp.loc, dynamicTagExp.loc.start.offset, capabilitiesPresets.all, '(', ')'), + ...createInterpolationCode(dynamicTagExp.loc.source, dynamicTagExp.loc, dynamicTagExp.loc.start.offset, presetInfos.all, '(', ')'), ';\n', ); } @@ -738,10 +777,10 @@ export function generate( key, 'template', [offset, offset + tag.length], - { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - }, + combineEnabled( + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ), ], ';\n', ); @@ -752,15 +791,15 @@ export function generate( codes.push( `const ${var_componentInstance} = ${var_functionalComponent}(`, // diagnostic start - tagOffsets.length ? ['', 'template', tagOffsets[0], capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly] + tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] : '', '{ ', ...createPropsCode(node, props, 'normal', propsFailedExps), '}', // diagnostic end - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, capabilitiesPresets.diagnosticOnly] + tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] : '', `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`, ); @@ -778,15 +817,15 @@ export function generate( codes.push( `({} as (props: __VLS_FunctionalComponentProps & Record) => void)(`, // diagnostic start - tagOffsets.length ? ['', 'template', tagOffsets[0], capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly] + tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] : '', '{ ', ...createPropsCode(node, props, 'normal', propsFailedExps), '}', // diagnostic end - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, capabilitiesPresets.diagnosticOnly] + tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] : '', `);\n`, ); @@ -809,7 +848,7 @@ export function generate( failedExp.loc.source, failedExp.loc, failedExp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -843,7 +882,7 @@ export function generate( vScope.exp.loc.source, 'template', vScope.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, ]); codes.push(';\n'); codes.push(`if (${condition}) {\n`); @@ -897,7 +936,7 @@ export function generate( slotDir.exp.content, 'template', slotDir.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, ], `] = __VLS_getSlotParams(`, ); @@ -909,14 +948,14 @@ export function generate( slotDir.exp.content, 'template', slotDir.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, ], ` = __VLS_getSlotParam(`, ); } } codes.push( - ['', 'template', (slotDir.arg ?? slotDir).loc.start.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', (slotDir.arg ?? slotDir).loc.start.offset, presetInfos.diagnosticOnly], `(${componentCtxVar}.slots!)`, ...( (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content) @@ -924,16 +963,19 @@ export function generate( slotDir.arg.loc.source, 'template', slotDir.arg.loc.start.offset, - slotDir.arg.isStatic ? capabilitiesPresets.slotName : capabilitiesPresets.all + slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all ], slotDir.arg.loc) : createPropertyAccessCode([ 'default', 'template', [slotDir.loc.start.offset, slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0)], - { ...capabilitiesPresets.slotName, completionItems: false }, + combineEnabled( + presetInfos.slotName, + { completionItems: false }, + ), ]) ), - ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly], ); if (hasProps) { codes.push(')'); @@ -1019,25 +1061,27 @@ export function generate( camelize('on-' + prop.arg.loc.source), // onClickOutside 'template', [prop.arg.loc.start.offset, prop.arg.loc.end.offset], - { - ...capabilitiesPresets.attrReference, - renameEdits: { - shouldRename: true, - shouldEdit: true, - // @click-outside -> onClickOutside - resolveNewName(newName) { - return camelize('on-' + newName); - }, - // onClickOutside -> @click-outside - resolveEditText(newName) { - const hName = hyphenateAttr(newName); - if (hyphenateAttr(newName).startsWith('on-')) { - return camelize(hName.slice('on-'.length)); - } - return newName; + combineEnabled( + presetInfos.attrReference, + { + renameEdits: { + shouldRename: true, + shouldEdit: true, + // @click-outside -> onClickOutside + resolveNewName(newName) { + return camelize('on-' + newName); + }, + // onClickOutside -> @click-outside + resolveEditText(newName) { + const hName = hyphenateAttr(newName); + if (hyphenateAttr(newName).startsWith('on-')) { + return camelize(hName.slice('on-'.length)); + } + return newName; + }, }, }, - }, + ), ]), `) };\n`, `${eventVar} = { `, @@ -1049,7 +1093,7 @@ export function generate( prop.arg.loc.source.slice(1, -1), prop.arg.loc, prop.arg.loc.start.offset + 1, - capabilitiesPresets.all, + presetInfos.all, '', '', ), @@ -1062,7 +1106,7 @@ export function generate( prop.arg.loc.source, 'template', prop.arg.loc.start.offset, - capabilitiesPresets.event, + presetInfos.event, ], prop.arg.loc) ); } @@ -1082,7 +1126,7 @@ export function generate( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '$event => {(', ')}', ), @@ -1145,9 +1189,9 @@ export function generate( () => { if (isCompoundExpression && isFirstMapping) { isFirstMapping = false; - return capabilitiesPresets.allWithHiddenParam; + return presetInfos.allWithHiddenParam; } - return capabilitiesPresets.all; + return presetInfos.all; }, prefix, suffix, @@ -1195,9 +1239,9 @@ export function generate( const codes: Code[] = []; - let caps_all: VueCodeInformation = capabilitiesPresets.all; - let caps_diagnosticOnly: VueCodeInformation = capabilitiesPresets.diagnosticOnly; - let caps_attr: VueCodeInformation = capabilitiesPresets.attr; + let caps_all: VueCodeInformation = presetInfos.all; + let caps_diagnosticOnly: VueCodeInformation = presetInfos.diagnosticOnly; + let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { caps_all = { @@ -1428,7 +1472,7 @@ export function generate( && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { codes.push( - ['', 'template', prop.exp.loc.start.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly], '...', ...createInterpolationCode( prop.exp.content, @@ -1438,7 +1482,7 @@ export function generate( '(', ')', ), - ['', 'template', prop.exp.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly], ', ', ); if (mode === 'normal') { @@ -1533,7 +1577,7 @@ export function generate( prop.arg.content, prop.arg.loc, prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content), - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -1553,42 +1597,44 @@ export function generate( '', 'template', prop.loc.start.offset, - capabilitiesPresets.diagnosticOnly, + presetInfos.diagnosticOnly, ], `__VLS_directiveFunction(__VLS_ctx.`, [ camelize('v-' + prop.name), 'template', [prop.loc.start.offset, prop.loc.start.offset + 'v-'.length + prop.name.length], - { - ...capabilitiesPresets.noDiagnostics, - completionItems: { - // fix https://github.com/vuejs/language-tools/issues/1905 - isAdditional: true, - }, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.name), + combineEnabled( + presetInfos.noDiagnostics, + { + completionItems: { + // fix https://github.com/vuejs/language-tools/issues/1905 + isAdditional: true, + }, + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.name), + }, }, - }, + ), ], ')', '(', ); if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { codes.push( - ['', 'template', prop.exp.loc.start.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly], ...createInterpolationCode( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), - ['', 'template', prop.exp.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly], ); formatCodes.push( ...createFormatCode( @@ -1603,7 +1649,7 @@ export function generate( } codes.push( ')', - ['', 'template', prop.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.loc.end.offset, presetInfos.diagnosticOnly], ';\n', ); } @@ -1623,7 +1669,7 @@ export function generate( prop.value.content, prop.value.loc, prop.value.loc.start.offset + 1, - capabilitiesPresets.refAttr, + presetInfos.refAttr, '(', ')', ), @@ -1669,7 +1715,7 @@ export function generate( prop.exp.content, 'template', prop.exp.loc.start.offset, - capabilitiesPresets.scopedClassName, + presetInfos.scopedClassName, ]); codes.push(`);\n`); } @@ -1684,15 +1730,15 @@ export function generate( if (hasScriptSetupSlots) { codes.push( '__VLS_normalizeSlot(', - ['', 'template', node.loc.start.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], `${slotsAssignName ?? '__VLS_slots'}[`, - ['', 'template', node.loc.start.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], slotNameExpNode?.content ?? `('${getSlotName()}' as const)`, - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], ']', - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], ')?.(', - ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly], + ['', 'template', startTagOffset, presetInfos.diagnosticOnly], '{\n', ); } @@ -1711,7 +1757,7 @@ export function generate( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, + presetInfos.attrReference, '(', ')', ), @@ -1729,22 +1775,24 @@ export function generate( prop.arg.content, 'template', [prop.arg.loc.start.offset, prop.arg.loc.end.offset], - { - ...capabilitiesPresets.slotProp, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.arg.content), + combineEnabled( + presetInfos.slotProp, + { + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.arg.content), + }, }, - }, + ), ], prop.arg.loc), ': ', ...createInterpolationCode( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, + presetInfos.attrReference, '(', ')', ), @@ -1760,15 +1808,17 @@ export function generate( prop.name, 'template', prop.loc.start.offset, - { - ...capabilitiesPresets.attr, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.name), + combineEnabled( + presetInfos.attr, + { + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: getPropRenameApply(prop.name), + }, }, - }, + ), ], prop.loc), ': (', prop.value !== undefined ? `"${toUnicodeIfNeed(prop.value.content)}"` : 'true', @@ -1778,7 +1828,7 @@ export function generate( } codes.push( '}', - hasScriptSetupSlots ? ['', 'template', startTagOffset + node.tag.length, capabilitiesPresets.diagnosticOnly] : '', + hasScriptSetupSlots ? ['', 'template', startTagOffset + node.tag.length, presetInfos.diagnosticOnly] : '', hasScriptSetupSlots ? `);\n` : `;\n` ); @@ -1851,10 +1901,10 @@ export function generate( v.text, 'template', v.offset, - { - ...capabilitiesPresets, - completionItems: { isAdditional: true }, - }, + combineEnabled( + presetInfos.disabledAll, + { completionItems: { isAdditional: true }, }, + ), ]); codes.push(','); } @@ -1872,11 +1922,13 @@ export function generate( mapCode, 'template', sourceOffset, - { - ...capabilitiesPresets, - formattingEdits: true, - linkedEditingRanges: true, // support vue-autoinsert-parentheses - }, + combineEnabled( + presetInfos.disabledAll, + { + formattingEdits: true, + autoInserts: true, // support vue-autoinsert-parentheses + }, + ), ], formatWrapper[1], '\n', @@ -1939,7 +1991,7 @@ export function generate( 'template', start + fragOffset, isJustForErrorMapping - ? capabilitiesPresets.diagnosticOnly + ? presetInfos.diagnosticOnly : typeof data === 'function' ? data() : data, ]); } From 516eccd1148e5e4c39f5f9ba0dbaceb02d212289 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 27 Nov 2023 04:47:52 +0800 Subject: [PATCH 07/19] updates for https://github.com/volarjs/volar.js/pull/95 --- packages/language-core/package.json | 2 - .../language-core/src/generators/script.ts | 96 +-- .../language-core/src/generators/template.ts | 812 +++++++++--------- packages/language-core/src/index.ts | 1 - packages/language-core/src/plugins/file-md.ts | 2 +- packages/language-core/src/plugins/vue-tsx.ts | 11 +- packages/language-core/src/types.ts | 5 +- .../src/virtualFile/computedFiles.ts | 33 +- .../src/virtualFile/computedMappings.ts | 36 +- .../src/virtualFile/embeddedFile.ts | 6 +- .../language-core/src/virtualFile/vueFile.ts | 3 +- .../src/plugins/vue-autoinsert-parentheses.ts | 12 +- .../src/plugins/vue-codelens-references.ts | 49 +- .../src/plugins/vue-extract-file.ts | 4 +- .../src/plugins/vue-twoslash-queries.ts | 2 +- .../vue-visualize-hidden-callback-param.ts | 10 +- packages/tsc-eslint-hook/src/index.ts | 16 +- .../reference/slot-named/entry.vue | 2 +- 18 files changed, 568 insertions(+), 534 deletions(-) diff --git a/packages/language-core/package.json b/packages/language-core/package.json index fe7e1865e3..f8681f47bb 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -14,12 +14,10 @@ }, "dependencies": { "@volar/language-core": "~1.11.0", - "@volar/source-map": "~1.11.0", "@vue/compiler-dom": "^3.3.0", "@vue/shared": "^3.3.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "muggle-string": "^0.3.1", "path-browserify": "^1.0.1", "vue-template-compiler": "^2.7.14" }, diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index 115cd5891b..54a4cbc2f8 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -1,13 +1,10 @@ -import { LinkedCodeTrigger } from '@volar/language-core'; -import * as SourceMaps from '@volar/source-map'; -import { Segment, getLength } from '@volar/source-map'; -import * as muggle from 'muggle-string'; +import { LinkedCodeTrigger, Mapping, getLength, offsetStack, resetOffsetStack, setTracking, track } from '@volar/language-core'; import * as path from 'path-browserify'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type * as templateGen from '../generators/template'; import type { ScriptRanges } from '../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../parsers/scriptSetupRanges'; -import type { TextRange, VueCodeInformation, VueCompilerOptions } from '../types'; +import type { Code, VueCompilerOptions } from '../types'; import { Sfc } from '../types'; import { getSlotsPropertyName, hyphenateTag } from '../utils/shared'; import { walkInterpolationFragment } from '../utils/transform'; @@ -27,8 +24,8 @@ export function generate( codegenStack: boolean, ) { - const [codes, codeStacks] = codegenStack ? muggle.track([] as Segment[]) : [[], []]; - const mirrorBehaviorMappings: SourceMaps.Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; + const [codes, codeStacks] = codegenStack ? track([] as Code[]) : [[], []]; + const mirrorBehaviorMappings: Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 if (!script && !scriptSetup) { @@ -153,7 +150,7 @@ export function generate( codes.push([ `'${src}'`, 'script', - [script.srcOffset - 1, script.srcOffset + script.src.length + 1], + script.srcOffset - 1, { renameEdits: src === script.src ? true : { shouldRename: false, @@ -267,7 +264,7 @@ export function generate( return; } - const definePropMirrors: Record = {}; + const definePropMirrors = new Map(); let scriptSetupGeneratedOffset: number | undefined; if (scriptSetup.generic) { @@ -337,8 +334,7 @@ export function generate( let propName = 'modelValue'; if (defineProp.name) { propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirrorStart = muggle.getLength(codes); - definePropMirrors[propName] = [propMirrorStart, propMirrorStart + propName.length]; + definePropMirrors.set(propName, getLength(codes)); } codes.push(`${propName}${defineProp.required ? '' : '?'}: `); if (defineProp.type) { @@ -408,19 +404,19 @@ export function generate( continue; } const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirror = definePropMirrors[propName]; - if (propMirror) { - mirrorBehaviorMappings.push([ - undefined, - [defineProp.name.start + scriptSetupGeneratedOffset, defineProp.name.end + scriptSetupGeneratedOffset], - propMirror, - [{}, {}], - ]); + const propMirror = definePropMirrors.get(propName); + if (propMirror !== undefined) { + mirrorBehaviorMappings.push({ + sourceOffsets: [defineProp.name.start + scriptSetupGeneratedOffset], + generatedOffsets: [propMirror], + lengths: [defineProp.name.end - defineProp.name.start], + data: [{}, {}], + }); } } } } - function generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Record) { + function generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Map) { if (!scriptSetupRanges || !scriptSetup) { return; @@ -453,7 +449,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: `.trim() + '\n'); } - const scriptSetupGeneratedOffset = muggle.getLength(codes) - scriptSetupRanges.importSectionEndOffset; + const scriptSetupGeneratedOffset = getLength(codes) - scriptSetupRanges.importSectionEndOffset; let setupCodeModifies: [() => void, number, number][] = []; if (scriptSetupRanges.props.define && !scriptSetupRanges.props.name) { @@ -534,8 +530,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: } else if (defineProp.name) { propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const start = muggle.getLength(codes); - definePropMirrors[propName] = [start, start + propName.length]; + const start = getLength(codes); + definePropMirrors.set(propName, start); codes.push(propName); } else { @@ -749,22 +745,20 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: if (!templateUsageVars.has(varName) && !cssIds.has(varName)) { continue; } - const templateStart = getLength(codes); + const templateOffset = getLength(codes); codes.push(varName); - const templateEnd = getLength(codes); codes.push(`: ${varName} as typeof `); - const scriptStart = getLength(codes); + const scriptOffset = getLength(codes); codes.push(varName); - const scriptEnd = getLength(codes); codes.push(',\n'); - mirrorBehaviorMappings.push([ - undefined, - [scriptStart, scriptEnd], - [templateStart, templateEnd], - [{}, {}], - ]); + mirrorBehaviorMappings.push({ + sourceOffsets: [scriptOffset], + generatedOffsets: [templateOffset], + lengths: [varName.length], + data: [{}, {}], + }); } } codes.push(`};\n`); // return { @@ -828,8 +822,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: for (const className of style.classNames) { generateCssClassProperty( i, - className.text.substring(1), - { start: className.offset, end: className.offset + className.text.length }, + className.text, + className.offset, 'string', false, true, @@ -857,8 +851,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: for (const className of style.classNames) { generateCssClassProperty( i, - className.text.substring(1), - { start: className.offset, end: className.offset + className.text.length }, + className.text, + className.offset, 'boolean', true, !style.module, @@ -874,11 +868,11 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: codes.push(`/* CSS variable injection end */\n`); if (htmlGen) { - muggle.setTracking(false); + setTracking(false); for (const s of htmlGen.codes) { codes.push(s); } - muggle.setTracking(true); + setTracking(true); for (const s of htmlGen.codeStacks) { codeStacks.push(s); } @@ -895,12 +889,12 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: return { cssIds }; - function generateCssClassProperty(styleIndex: number, className: string, classRange: TextRange, propertyType: string, optional: boolean, referencesCodeLens: boolean) { + function generateCssClassProperty(styleIndex: number, classNameWithDot: string, offset: number, propertyType: string, optional: boolean, referencesCodeLens: boolean) { codes.push(`\n & { `); codes.push([ '', 'style_' + styleIndex, - classRange.start, + offset, { references: true, __referencesCodeLens: referencesCodeLens, @@ -908,9 +902,9 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: ]); codes.push(`'`); codes.push([ - className, + '', 'style_' + styleIndex, - [classRange.start, classRange.end], + offset, { references: true, renameEdits: { @@ -921,11 +915,17 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: }, }, ]); + codes.push([ + classNameWithDot.substring(1), + 'style_' + styleIndex, + offset + 1, + { __combineLastMappping: true }, + ]); codes.push(`'`); codes.push([ '', 'style_' + styleIndex, - classRange.end, + offset + classNameWithDot.length, {}, ]); codes.push(`${optional ? '?' : ''}: ${propertyType}`); @@ -992,17 +992,17 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: return usageVars; } function addVirtualCode(vueTag: 'script' | 'scriptSetup', start: number, end?: number) { - muggle.offsetStack(); + offsetStack(); codes.push([ (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, start, {}, // diagnostic also working for setup() returns unused in template checking ]); - muggle.resetOffsetStack(); + resetOffsetStack(); } function addExtraReferenceVirtualCode(vueTag: 'script' | 'scriptSetup', start: number, end: number) { - muggle.offsetStack(); + offsetStack(); codes.push([ (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, @@ -1013,7 +1013,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: renameEdits: true, }, ]); - muggle.resetOffsetStack(); + resetOffsetStack(); } } diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 2ce8372e18..251ac46fe6 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1,10 +1,9 @@ -import { Segment } from '@volar/source-map'; +import { toString, track } from '@volar/language-core'; import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import { minimatch } from 'minimatch'; -import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import { Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; +import { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; import { hyphenateAttr, hyphenateTag } from '../utils/shared'; import { collectVars, walkInterpolationFragment } from '../utils/transform'; @@ -121,8 +120,6 @@ const transformContext: CompilerDOM.TransformContext = { expressionPlugins: ['typescript'], }; -type Code = Segment; - export function generate( ts: typeof import('typescript/lib/tsserverlibrary'), compilerOptions: ts.CompilerOptions, @@ -137,10 +134,16 @@ export function generate( ) { const nativeTags = new Set(vueCompilerOptions.nativeTags); - const [codes, codeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const [formatCodes, formatCodeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const [cssCodes, cssCodeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const slots = new Map(); + const [codes, codeStacks] = codegenStack ? track([] as Code[]) : [[], []]; + const [formatCodes, formatCodeStacks] = codegenStack ? track([] as Code[]) : [[], []]; + const [cssCodes, cssCodeStacks] = codegenStack ? track([] as Code[]) : [[], []]; + const slots = new Map(); const slotExps = new Map(); const tagNames = collectTagOffsets(); const localVars = new Map(); @@ -195,55 +198,71 @@ export function generate( hasSlot, }; - function createSlotsTypeCode(): Code[] { - const codes: Code[] = []; + function* createSlotsTypeCode(): Generator { for (const [exp, slot] of slotExps) { hasSlot = true; - codes.push(`Partial, (_: typeof ${slot.varName}) => any>> &\n`); + yield `Partial, (_: typeof ${slot.varName}) => any>> &\n`; } - codes.push(`{\n`); - for (const [name, slot] of slots) { + yield `{\n`; + for (const [_, slot] of slots) { hasSlot = true; - codes.push( - ...createObjectPropertyCode([ - name, - 'template', + if (slot.name && slot.loc !== undefined) { + yield* createObjectPropertyCode( + slot.name, slot.loc, - combineEnabled( - presetInfos.slotNameExport, - { __referencesCodeLens: true }, - ), - ], slot.nodeLoc), - ); - codes.push(`?(_: typeof ${slot.varName}): any,\n`); + combineEnabled(presetInfos.slotNameExport, { __referencesCodeLens: true }), + slot.nodeLoc + ); + } + else { + yield ['', 'template', slot.tagRange[0], combineEnabled(presetInfos.slotNameExport, { __referencesCodeLens: true })]; + yield 'default'; + yield ['', 'template', slot.tagRange[1], { __combineLastMappping: true }]; + } + yield `?(_: typeof ${slot.varName}): any,\n`; } - codes.push(`}`); - return codes; + yield `}`; } function generateStyleScopedClasses() { codes.push(`if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {\n`); for (const { className, offset } of scopedClasses) { - codes.push(`__VLS_styleScopedClasses[`); - codes.push(...createStringLiteralKeyCode([ - className, - 'template', - offset, - combineEnabled( - presetInfos.scopedClassName, - { __displayWithLink: stylesScopedClasses.has(className) }, + codes.push( + `__VLS_styleScopedClasses[`, + ...createStringLiteralKeyCode( + className, + offset, + combineEnabled( + presetInfos.scopedClassName, + { __displayWithLink: stylesScopedClasses.has(className) }, + ), ), - ])); - codes.push(`];\n`); + `];\n`, + ); } codes.push('}\n'); } function toCanonicalComponentName(tagText: string) { - return validTsVarReg.test(tagText) ? tagText : capitalize(camelize(tagText.replace(colonReg, '-'))); + return validTsVarReg.test(tagText) + ? tagText + : capitalize(camelize(tagText.replace(colonReg, '-'))); + } + + function* createCanonicalComponentNameCode(tagText: string, offset: number, info: VueCodeInformation): Generator { + if (validTsVarReg.test(tagText)) { + yield [tagText, 'template', offset, info]; + } + else { + yield* createCamelizeCode( + capitalize(tagText.replace(colonReg, '-')), + offset, + info + ); + } } - function getPossibleOriginalComponentName(tagText: string) { + function getPossibleOriginalComponentNames(tagText: string) { return [...new Set([ // order is important: https://github.com/vuejs/language-tools/issues/2010 capitalize(camelize(tagText)), @@ -280,66 +299,84 @@ export function generate( for (const tagName in tagNames) { const tagOffsets = tagNames[tagName]; - const tagRanges: [number, number][] = tagOffsets.map(offset => [offset, offset + tagName.length]); - const names = nativeTags.has(tagName) ? [tagName] : getPossibleOriginalComponentName(tagName); - for (const name of names) { - for (const tagRange of tagRanges) { + for (const tagOffset of tagOffsets) { + if (nativeTags.has(tagName)) { codes.push( - nativeTags.has(tagName) ? '__VLS_intrinsicElements' : '__VLS_components', - ...createPropertyAccessCode([ - name, - 'template', - tagRange, + '__VLS_intrinsicElements', + ...createPropertyAccessCode( + tagName, + tagOffset, combineEnabled( presetInfos.tagReference, { - renameEdits: { - shouldEdit: true, - shouldRename: true, - resolveNewName: tagName !== name ? camelizeComponentName : undefined, - resolveEditText: getTagRenameApply(tagName), - } + renameEdits: true }, ...(nativeTags.has(tagName) ? [ presetInfos.tagHover, presetInfos.diagnosticOnly, ] : []), ), - ]), + ), ';', ); } + else if (validTsVarReg.test(camelize(tagName))) { + for (const shouldCapitalize of tagName[0] === tagName.toUpperCase() ? [false] : [true, false]) { + const expectName = shouldCapitalize ? capitalize(camelize(tagName)) : camelize(tagName); + codes.push( + '__VLS_components.', + ...createCamelizeCode( + shouldCapitalize ? capitalize(tagName) : tagName, + tagOffset, + combineEnabled( + presetInfos.tagReference, + { + renameEdits: { + shouldEdit: true, + shouldRename: true, + resolveNewName: tagName !== expectName ? camelizeComponentName : undefined, + resolveEditText: getTagRenameApply(tagName), + } + }, + ...(nativeTags.has(tagName) ? [ + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ] : []), + ), + ), + ';', + ); + } + } } codes.push('\n'); - if (nativeTags.has(tagName)) - continue; - - const isNamespacedTag = tagName.indexOf('.') >= 0; - if (isNamespacedTag) - continue; - - codes.push( - '// @ts-ignore\n', // #2304 - '[', - ); - const validName = toCanonicalComponentName(tagName); - for (const tagRange of tagRanges) { - codes.push([ - validName, - 'template', - tagRange, - { - completionItems: { - isAdditional: true, - onlyImport: true, - }, - }, - ]); - codes.push(','); + if ( + !nativeTags.has(tagName) + && validTsVarReg.test(camelize(tagName)) + ) { + codes.push( + '// @ts-ignore\n', // #2304 + '[', + ); + for (const tagOffset of tagOffsets) { + codes.push( + ...createCamelizeCode( + capitalize(tagName), + tagOffset, + withAllDisabled({ + completionItems: { + isAdditional: true, + onlyImport: true, + }, + }), + ), + ',', + ); + } + codes.push(`];\n`); } - codes.push(`];\n`); } } @@ -392,10 +429,10 @@ export function generate( if (typeof code === 'string') { continue; } - const cap = code[3]; - if (cap.diagnostics) { + const data = code[3]; + if (data.diagnostics) { code[3] = { - ...cap, + ...data, diagnostics: false, }; } @@ -413,10 +450,10 @@ export function generate( if (typeof code === 'string') { continue; } - const cap = code[3]; - if (cap.diagnostics) { + const data = code[3]; + if (data.diagnostics) { code[3] = { - ...cap, + ...data, diagnostics: { shouldReport: suppressError, }, @@ -425,15 +462,22 @@ export function generate( } codes.push( [ - '// @ts-expect-error __VLS_TS_EXPECT_ERROR', + '', 'template', - [expectedErrorNode.loc.start.offset, expectedErrorNode.loc.end.offset], + expectedErrorNode.loc.start.offset, { diagnostics: { shouldReport: () => errors === 0, }, }, ], + '// @ts-expect-error __VLS_TS_EXPECT_ERROR', + [ + '', + 'template', + expectedErrorNode.loc.end.offset, + { __combineLastMappping: true }, + ], '\n;\n', ); expectedErrorStart = undefined; @@ -589,7 +633,7 @@ export function generate( ), ); - blockConditions.push(muggle.toString(codes.slice(beforeCodeLength, afterCodeLength))); + blockConditions.push(toString(codes.slice(beforeCodeLength, afterCodeLength))); addedBlockCondition = true; } @@ -721,12 +765,11 @@ export function generate( 'const ', var_originalComponent, ` = __VLS_intrinsicElements[`, - ...createStringLiteralKeyCode([ + ...createStringLiteralKeyCode( tag, - 'template', tagOffsets[0], presetInfos.diagnosticOnly, - ]), + ), '];\n', ); } @@ -746,7 +789,7 @@ export function generate( } else { codes.push(`let ${var_originalComponent}!: `); - for (const componentName of getPossibleOriginalComponentName(tag)) { + for (const componentName of getPossibleOriginalComponentNames(tag)) { codes.push(`'${componentName}' extends keyof typeof __VLS_ctx ? typeof __VLS_ctx${validTsVarReg.test(componentName) ? `.${componentName}` : `['${componentName}']`} : `); } codes.push(`typeof __VLS_resolvedLocalAndGlobalComponents['${toCanonicalComponentName(tag)}'];\n`); @@ -770,18 +813,16 @@ export function generate( if (isNamespacedTag || dynamicTagExp || isIntrinsicElement) { continue; } - const key = toCanonicalComponentName(tag); - codes.push(`({} as { ${key}: typeof ${var_originalComponent} }).`); codes.push( - [ - key, - 'template', - [offset, offset + tag.length], + `({} as { ${toCanonicalComponentName(tag)}: typeof ${var_originalComponent} }).`, + ...createCanonicalComponentNameCode( + tag, + offset, combineEnabled( presetInfos.tagHover, presetInfos.diagnosticOnly, ), - ], + ), ';\n', ); } @@ -959,21 +1000,18 @@ export function generate( `(${componentCtxVar}.slots!)`, ...( (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content) - ? createPropertyAccessCode([ + ? createPropertyAccessCode( slotDir.arg.loc.source, - 'template', slotDir.arg.loc.start.offset, - slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all - ], slotDir.arg.loc) - : createPropertyAccessCode([ + slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all, + slotDir.arg.loc + ) + : [ + '.', + ['', 'template', slotDir.loc.start.offset, combineEnabled(presetInfos.slotName, { completionItems: false })] satisfies Code, 'default', - 'template', - [slotDir.loc.start.offset, slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0)], - combineEnabled( - presetInfos.slotName, - { completionItems: false }, - ), - ]) + ['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), { __combineLastMappping: true }] satisfies Code, + ] ), ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly], ); @@ -1026,16 +1064,10 @@ export function generate( // fix https://github.com/vuejs/language-tools/issues/932 if (!hasSlotElements.has(node) && node.children.length) { codes.push( - `(${componentCtxVar}.slots!)`, - ...createPropertyAccessCode([ - 'default', - 'template', - [ - node.children[0].loc.start.offset, - node.children[node.children.length - 1].loc.end.offset, - ], - { references: true }, - ]), + `(${componentCtxVar}.slots!).`, + ['', 'template', node.children[0].loc.start.offset, withAllDisabled({ references: true })], + 'default', + ['', 'template', node.children[node.children.length - 1].loc.end.offset, { __combineLastMappping: true }], ';\n', ); } @@ -1056,33 +1088,66 @@ export function generate( const eventVar = `__VLS_${elementIndex++}`; codes.push( `let ${eventVar} = { '${prop.arg.loc.source}': `, - `__VLS_pickEvent(${eventsVar}['${prop.arg.loc.source}'], ({} as __VLS_FunctionalComponentProps)`, - ...createPropertyAccessCode([ - camelize('on-' + prop.arg.loc.source), // onClickOutside - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], - combineEnabled( - presetInfos.attrReference, - { - renameEdits: { - shouldRename: true, - shouldEdit: true, - // @click-outside -> onClickOutside - resolveNewName(newName) { - return camelize('on-' + newName); - }, - // onClickOutside -> @click-outside - resolveEditText(newName) { - const hName = hyphenateAttr(newName); - if (hyphenateAttr(newName).startsWith('on-')) { - return camelize(hName.slice('on-'.length)); - } - return newName; - }, + `__VLS_pickEvent(`, + `${eventsVar}['${prop.arg.loc.source}'], `, + `({} as __VLS_FunctionalComponentProps)`, + ); + const startCode: Code = [ + '', + 'template', + prop.arg.loc.start.offset, + combineEnabled( + presetInfos.attrReference, + { + renameEdits: { + shouldRename: true, + shouldEdit: true, + // @click-outside -> onClickOutside + resolveNewName(newName) { + return camelize('on-' + newName); + }, + // onClickOutside -> @click-outside + resolveEditText(newName) { + const hName = hyphenateAttr(newName); + if (hyphenateAttr(newName).startsWith('on-')) { + return camelize(hName.slice('on-'.length)); + } + return newName; }, }, + }, + ), + ]; + if (validTsVarReg.test(camelize(prop.arg.loc.source))) { + codes.push( + `.`, + startCode, + `on`, + ...createCamelizeCode( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + { __combineLastMappping: true }, ), - ]), + ); + } + else { + codes.push( + `[`, + startCode, + `'`, + ['', 'template', prop.arg.loc.start.offset, { __combineLastMappping: true }], + 'on', + ...createCamelizeCode( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + { __combineLastMappping: true }, + ), + `'`, + ['', 'template', prop.arg.loc.end.offset, { __combineLastMappping: true }], + `]`, + ); + } + codes.push( `) };\n`, `${eventVar} = { `, ); @@ -1102,12 +1167,12 @@ export function generate( } else { codes.push( - ...createObjectPropertyCode([ + ...createObjectPropertyCode( prop.arg.loc.source, - 'template', prop.arg.loc.start.offset, presetInfos.event, - ], prop.arg.loc) + prop.arg.loc + ) ); } codes.push(`: `); @@ -1265,10 +1330,7 @@ export function generate( && prop.name === 'on' && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { - codes.push( - ...createObjectPropertyCode(camelize('on-' + prop.arg.loc.source)), - ': {} as any, ', - ); + codes.push(`'${camelize('on-' + prop.arg.loc.source)}': {} as any, `); } } codes.push(`}, `); @@ -1281,7 +1343,7 @@ export function generate( && (!prop.exp || prop.exp.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) ) { - let attrNameText = + let propName = prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ? prop.arg.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY ? prop.arg.content @@ -1289,15 +1351,15 @@ export function generate( : getModelValuePropName(node, vueCompilerOptions.target, vueCompilerOptions); if (prop.modifiers.some(m => m === 'prop' || m === 'attr')) { - attrNameText = attrNameText?.substring(1); + propName = propName?.substring(1); } if ( - attrNameText === undefined - || vueCompilerOptions.dataAttributes.some(pattern => minimatch(attrNameText!, pattern)) - || (attrNameText === 'style' && ++styleAttrNum >= 2) - || (attrNameText === 'class' && ++classAttrNum >= 2) - || (attrNameText === 'name' && node.tag === 'slot') // #2308 + propName === undefined + || vueCompilerOptions.dataAttributes.some(pattern => minimatch(propName!, pattern)) + || (propName === 'style' && ++styleAttrNum >= 2) + || (propName === 'class' && ++classAttrNum >= 2) + || (propName === 'name' && node.tag === 'slot') // #2308 ) { if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { propsFailedExps?.push(prop.exp); @@ -1305,71 +1367,35 @@ export function generate( continue; } - let camelized = false; + const shouldCamelize = (!prop.arg || (prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.isStatic)) // isStatic + && hyphenateAttr(propName) === propName + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(propName!, pattern)); - if ( - (!prop.arg || (prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.isStatic)) // isStatic - && hyphenateAttr(attrNameText) === attrNameText - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(attrNameText!, pattern)) - ) { - attrNameText = camelize(attrNameText); - camelized = true; - } - - // camelize name - codes.push([ - '', - 'template', - prop.loc.start.offset, - caps_diagnosticOnly, - ]); - if (!prop.arg) { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.loc.start.offset, prop.loc.start.offset + prop.loc.source.indexOf('=')], - caps_attr, - ], (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {})), - ); - } - else if (prop.exp?.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY) { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.start.offset + attrNameText.length], // patch style attr, - { - ...caps_attr, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: camelized ? hyphenateAttr : undefined, - }, - }, - ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), - ); - } - else { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], - { - ...caps_attr, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: camelized ? hyphenateAttr : undefined, + codes.push( + ['', 'template', prop.loc.start.offset, caps_diagnosticOnly], + ...createObjectPropertyCode( + propName, + prop.arg + ? prop.arg.loc.start.offset + : prop.loc.start.offset, + prop.arg + ? combineEnabled( + caps_attr, + { + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: shouldCamelize ? hyphenateAttr : undefined, + }, }, - }, - ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), - ); - } - codes.push(': ('); + ) + : caps_attr, + (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {}), + shouldCamelize, + ), + ': (', + ); if (prop.exp && !(prop.exp.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY)) { // style='z-index: 2' will compile to {'z-index':'2'} codes.push( ...createInterpolationCode( @@ -1405,64 +1431,46 @@ export function generate( } else if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { - let attrNameText = prop.name; - if ( - vueCompilerOptions.dataAttributes.some(pattern => minimatch(attrNameText!, pattern)) - || (attrNameText === 'style' && ++styleAttrNum >= 2) - || (attrNameText === 'class' && ++classAttrNum >= 2) - || (attrNameText === 'name' && node.tag === 'slot') // #2308 - ) { - continue; - } + vueCompilerOptions.dataAttributes.some(pattern => minimatch(prop.name, pattern)) + || (prop.name === 'style' && ++styleAttrNum >= 2) + || (prop.name === 'class' && ++classAttrNum >= 2) + || (prop.name === 'name' && node.tag === 'slot') // #2308 + ) continue; - let camelized = false; + const shouldCamelize = hyphenateAttr(prop.name) === prop.name + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(prop.name, pattern)); - if ( - hyphenateAttr(prop.name) === prop.name - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(attrNameText!, pattern)) - ) { - attrNameText = camelize(prop.name); - camelized = true; - } - - // camelize name - codes.push([ - '', - 'template', - prop.loc.start.offset, - caps_diagnosticOnly, - ]); codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.loc.start.offset, prop.loc.start.offset + prop.name.length], - { - ...caps_attr, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: camelized ? hyphenateAttr : undefined, - }, - }, - ], (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {})) + ['', 'template', prop.loc.start.offset, caps_diagnosticOnly], + ...createObjectPropertyCode( + prop.name, + prop.loc.start.offset, + shouldCamelize + ? combineEnabled(caps_attr, { + renameEdits: { + shouldRename: true, + shouldEdit: true, + resolveNewName: camelize, + resolveEditText: hyphenateAttr, + }, + }) + : caps_attr, + (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {}), + shouldCamelize, + ), + ': (', ); - codes.push(': ('); if (prop.value) { - generateAttrValue(prop.value); + codes.push( + ...createAttrValueCode(prop.value, caps_all), + ); } else { codes.push('true'); } codes.push(')'); - codes.push([ - '', - 'template', - prop.loc.end.offset, - caps_diagnosticOnly, - ]); + codes.push(['', 'template', prop.loc.end.offset, caps_diagnosticOnly]); codes.push(', '); } else if ( @@ -1502,29 +1510,6 @@ export function generate( } return codes; - - function generateAttrValue(attrNode: CompilerDOM.TextNode) { - const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; - codes.push(char); - let start = attrNode.loc.start.offset; - let end = attrNode.loc.end.offset; - let content = attrNode.loc.source; - if ( - (content.startsWith('"') && content.endsWith('"')) - || (content.startsWith("'") && content.endsWith("'")) - ) { - start++; - end--; - content = content.slice(1, -1); - } - codes.push([ - toUnicodeIfNeed(content), - 'template', - [start, end], - caps_all, - ]); - codes.push(char); - } } function generateInlineCss(props: CompilerDOM.ElementNode['props']) { @@ -1593,17 +1578,11 @@ export function generate( } codes.push( - [ - '', - 'template', - prop.loc.start.offset, - presetInfos.diagnosticOnly, - ], + ['', 'template', prop.loc.start.offset, presetInfos.diagnosticOnly], `__VLS_directiveFunction(__VLS_ctx.`, - [ - camelize('v-' + prop.name), - 'template', - [prop.loc.start.offset, prop.loc.start.offset + 'v-'.length + prop.name.length], + ...createCamelizeCode( + 'v-' + prop.name, + prop.loc.start.offset, combineEnabled( presetInfos.noDiagnostics, { @@ -1619,7 +1598,7 @@ export function generate( }, }, ), - ], + ), ')', '(', ); @@ -1733,7 +1712,7 @@ export function generate( ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], `${slotsAssignName ?? '__VLS_slots'}[`, ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], - slotNameExpNode?.content ?? `('${getSlotName()}' as const)`, + slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`, ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], ']', ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], @@ -1771,10 +1750,9 @@ export function generate( && prop.arg.content !== 'name' ) { codes.push( - ...createObjectPropertyCode([ + ...createObjectPropertyCode( prop.arg.content, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], + prop.arg.loc.start.offset, combineEnabled( presetInfos.slotProp, { @@ -1786,7 +1764,8 @@ export function generate( }, }, ), - ], prop.arg.loc), + prop.arg.loc + ), ': ', ...createInterpolationCode( prop.exp.content, @@ -1804,9 +1783,8 @@ export function generate( && prop.name !== 'name' // slot name ) { codes.push( - ...createObjectPropertyCode([ + ...createObjectPropertyCode( prop.name, - 'template', prop.loc.start.offset, combineEnabled( presetInfos.attr, @@ -1819,9 +1797,12 @@ export function generate( }, }, ), - ], prop.loc), + prop.loc + ), ': (', - prop.value !== undefined ? `"${toUnicodeIfNeed(prop.value.content)}"` : 'true', + prop.value !== undefined + ? `"${needToUnicode(prop.value.content) ? toUnicode(prop.value.content) : prop.value.content}"` + : 'true', '),\n', ); } @@ -1860,9 +1841,11 @@ export function generate( } else { const slotName = getSlotName(); - slots.set(slotName, { + slots.set(slotName?.[0] ?? 'default', { + name: slotName?.[0], + loc: slotName?.[1], + tagRange: [startTagOffset, startTagOffset + node.tag.length], varName: varSlot, - loc: [startTagOffset, startTagOffset + node.tag.length], nodeLoc: node.loc, }); } @@ -1871,11 +1854,13 @@ export function generate( for (const prop2 of node.props) { if (prop2.name === 'name' && prop2.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop2.value) { if (prop2.value.content) { - return prop2.value.content; + return [ + prop2.value.content, + prop2.loc.start.offset + prop2.loc.source.indexOf(prop2.value.content, prop2.name.length), + ] as const; } } } - return 'default'; } function getSlotNameExpNode() { for (const prop2 of node.props) { @@ -1901,10 +1886,7 @@ export function generate( v.text, 'template', v.offset, - combineEnabled( - presetInfos.disabledAll, - { completionItems: { isAdditional: true }, }, - ), + withAllDisabled({ completionItems: { isAdditional: true }, }), ]); codes.push(','); } @@ -1915,56 +1897,106 @@ export function generate( // functional like - function createFormatCode(mapCode: string, sourceOffset: number, formatWrapper: [string, string]): Code[] { - return [ - formatWrapper[0], - [ - mapCode, - 'template', - sourceOffset, - combineEnabled( - presetInfos.disabledAll, - { - formattingEdits: true, - autoInserts: true, // support vue-autoinsert-parentheses - }, - ), - ], - formatWrapper[1], - '\n', + function* createAttrValueCode(attrNode: CompilerDOM.TextNode, info: VueCodeInformation): Generator { + const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; + yield char; + let start = attrNode.loc.start.offset; + let end = attrNode.loc.end.offset; + let content = attrNode.loc.source; + if ( + (content.startsWith('"') && content.endsWith('"')) + || (content.startsWith("'") && content.endsWith("'")) + ) { + start++; + end--; + content = content.slice(1, -1); + } + if (needToUnicode(content)) { + yield ['', 'template', start, info]; + yield toUnicode(content); + yield ['', 'template', end, { __combineLastMappping: true }]; + } + else { + yield [content, 'template', start, info]; + } + yield char; + } + + function* createCamelizeCode(code: string, offset: number, info: VueCodeInformation): Generator { + const parts = code.split('-'); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part !== '') { + yield [ + i === 0 + ? part + : capitalize(part), + 'template', + offset, + i === 0 + ? info + : { __combineLastMappping: true }, + ]; + } + offset += part.length + 1; + } + } + + function* createFormatCode(code: string, offset: number, formatWrapper: [string, string]): Generator { + yield formatWrapper[0]; + yield [ + code, + 'template', + offset, + combineEnabled( + presetInfos.disabledAll, + { + formattingEdits: true, + autoInserts: true, // support vue-autoinsert-parentheses + }, + ), ]; + yield formatWrapper[1]; + yield '\n'; } - function createObjectPropertyCode(a: Code, astHolder?: any): Code[] { - const aStr = typeof a === 'string' ? a : a[0]; - if (validTsVarReg.test(aStr)) { - return [a]; + function* createObjectPropertyCode(code: string, offset: number, info: VueCodeInformation, astHolder?: any, shouldCamelize = false): Generator { + + if (code.startsWith('[') && code.endsWith(']') && astHolder) { + yield* createInterpolationCode(code, astHolder, offset, info, '', ''); + return; } - else if (aStr.startsWith('[') && aStr.endsWith(']') && astHolder) { - const range = typeof a === 'object' ? a[2] : undefined; - const data = typeof a === 'object' ? a[3] : undefined; - return createInterpolationCode( - aStr, - astHolder, - range && typeof range === 'object' ? range[0] : range, - data, - '', - '', - ); + + if (shouldCamelize) { + if (validTsVarReg.test(camelize(code))) { + yield* createCamelizeCode(code, offset, info); + } + else { + yield ['', 'template', offset, info]; + yield '"'; + yield* createCamelizeCode(code, offset, { __combineLastMappping: true }); + yield '"'; + yield ['', 'template', offset + code.length, { __combineLastMappping: true }]; + } } else { - return createStringLiteralKeyCode(a); + if (validTsVarReg.test(code)) { + yield [code, 'template', offset, info]; + } + else { + yield* createStringLiteralKeyCode(code, offset, info); + } } } - function createInterpolationCode( + function* createInterpolationCode( _code: string, astHolder: any, start: number | undefined, data: VueCodeInformation | (() => VueCodeInformation) | undefined, prefix: string, suffix: string, - ): Code[] { + ): Generator { const code = prefix + _code + suffix; const ast = createTsAst(astHolder, code); const codes: Code[] = []; @@ -2009,7 +2041,9 @@ export function generate( tempVars.push(vars); } } - return codes; + for (const code of codes) { + yield code; // TODO: rewrite to yield* + } } function createTsAst(astHolder: any, text: string) { @@ -2020,45 +2054,36 @@ export function generate( return astHolder.__volar_ast as ts.SourceFile; } - function createPropertyAccessCode(a: Code, astHolder?: any): Code[] { - const aStr = typeof a === 'string' ? a : a[0]; - if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(aStr)) { - return ['.', a]; + function* createPropertyAccessCode(code: string, offset: number, info: VueCodeInformation, astHolder?: any): Generator { + if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(code)) { + yield '.'; + yield [code, 'template', offset, info]; } - else if (aStr.startsWith('[') && aStr.endsWith(']')) { - if (typeof a === 'string' || !astHolder) { - return [a]; - } - else { - return createInterpolationCode( - a[0], - astHolder, - typeof a[2] === 'number' ? a[2] : a[2][0], - a[3], - '', - '', - ); - } + else if (code.startsWith('[') && code.endsWith(']')) { + yield* createInterpolationCode( + code, + astHolder, + offset, + info, + '', + '', + ); } else { - return ['[', ...createStringLiteralKeyCode(a), ']']; + yield '['; + yield* createStringLiteralKeyCode(code, offset, info); + yield ']'; } } - function createStringLiteralKeyCode(a: Code): Code[] { - let codes: Code[] = ['"', a, '"']; - if (typeof a === 'object') { - const start = typeof a[2] === 'number' ? a[2] : a[2][0]; - const end = typeof a[2] === 'number' ? a[2] : a[2][1]; - codes = [ - ['', 'template', start, a[3]], - ...codes, - ['', 'template', end, a[3]], - ]; - } - return codes; + function* createStringLiteralKeyCode(code: string, offset: number, info: VueCodeInformation): Generator { + yield ['', 'template', offset, info]; + yield '"'; + yield [code, 'template', offset, { __combineLastMappping: true }]; + yield '"'; + yield ['', 'template', offset + code.length, { __combineLastMappping: true }]; } -}; +} export function walkElementNodes(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode, cb: (node: CompilerDOM.ElementNode) => void) { if (node.type === CompilerDOM.NodeTypes.ROOT) { @@ -2095,11 +2120,8 @@ export function walkElementNodes(node: CompilerDOM.RootNode | CompilerDOM.Templa } } -function toUnicodeIfNeed(str: string) { - if (str.indexOf('\\') === -1 && str.indexOf('\n') === -1) { - return str; - } - return toUnicode(str); +function needToUnicode(str: string) { + return str.indexOf('\\') >= 0 || str.indexOf('\n') >= 0; } function toUnicode(str: string) { diff --git a/packages/language-core/src/index.ts b/packages/language-core/src/index.ts index 30594dbcbd..11f1e53f8d 100644 --- a/packages/language-core/src/index.ts +++ b/packages/language-core/src/index.ts @@ -13,5 +13,4 @@ export * from './utils/shared'; export { tsCodegen } from './plugins/vue-tsx'; export * from '@volar/language-core'; -export * from '@volar/source-map'; export type * as CompilerDOM from '@vue/compiler-dom'; diff --git a/packages/language-core/src/plugins/file-md.ts b/packages/language-core/src/plugins/file-md.ts index e9883a29f8..d8a22f8c6c 100644 --- a/packages/language-core/src/plugins/file-md.ts +++ b/packages/language-core/src/plugins/file-md.ts @@ -1,4 +1,4 @@ -import { buildMappings, Segment, SourceMap, toString } from '@volar/source-map'; +import { buildMappings, Segment, SourceMap, toString } from '@volar/language-core'; import type { SFCBlock } from '@vue/compiler-sfc'; import { VueLanguagePlugin } from '../types'; import { parse } from '../utils/parseSfc'; diff --git a/packages/language-core/src/plugins/vue-tsx.ts b/packages/language-core/src/plugins/vue-tsx.ts index 092658444a..19dc120900 100644 --- a/packages/language-core/src/plugins/vue-tsx.ts +++ b/packages/language-core/src/plugins/vue-tsx.ts @@ -1,6 +1,5 @@ -import type { CodeInformation } from '@volar/language-core'; +import { CodeInformation, Segment, track } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; -import * as muggle from 'muggle-string'; import { generate as generateScript } from '../generators/script'; import { generate as generateTemplate } from '../generators/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; @@ -55,7 +54,7 @@ const plugin: VueLanguagePlugin = (ctx) => { }; const tsx = _tsx.generatedScript(); if (tsx) { - const [content, contentStacks] = ctx.codegenStack ? muggle.track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + const [content, contentStacks] = ctx.codegenStack ? track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; content.forEach(code => { if (typeof code !== 'string') { code[3].foldingRanges = false; @@ -75,7 +74,7 @@ const plugin: VueLanguagePlugin = (ctx) => { const template = _tsx.generatedTemplate(); if (template) { const [content, contentStacks] = ctx.codegenStack - ? muggle.track([...template.formatCodes], [...template.formatCodeStacks]) + ? track([...template.formatCodes], [...template.formatCodeStacks]) : [[...template.formatCodes], [...template.formatCodeStacks]]; embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; @@ -102,9 +101,9 @@ const plugin: VueLanguagePlugin = (ctx) => { const template = _tsx.generatedTemplate(); if (template) { const [content, contentStacks] = ctx.codegenStack - ? muggle.track([...template.cssCodes], [...template.cssCodeStacks]) + ? track([...template.cssCodes], [...template.cssCodeStacks]) : [[...template.cssCodes], [...template.cssCodeStacks]]; - embeddedFile.content = content as muggle.Segment[]; + embeddedFile.content = content as Segment[]; embeddedFile.contentStacks = contentStacks; } } diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index e88c6e048b..9e5ee90e2d 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -2,7 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCParseResult } from '@vue/compiler-sfc'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { VueEmbeddedFile } from './virtualFile/embeddedFile'; -import type { CodeInformation } from '@volar/language-core'; +import type { CodeInformation, Segment } from '@volar/language-core'; export type { SFCParseResult } from '@vue/compiler-sfc'; @@ -21,8 +21,11 @@ export interface VueCodeInformation extends CodeInformation { paddingRight?: boolean; paddingLeft?: boolean; }; + __combineLastMappping?: boolean; } +export type Code = Segment; + export interface VueCompilerOptions { target: number; lib: string; diff --git a/packages/language-core/src/virtualFile/computedFiles.ts b/packages/language-core/src/virtualFile/computedFiles.ts index 90bbb421e4..e66c092b35 100644 --- a/packages/language-core/src/virtualFile/computedFiles.ts +++ b/packages/language-core/src/virtualFile/computedFiles.ts @@ -1,7 +1,5 @@ -import { VirtualFile, resolveCommonLanguageId } from '@volar/language-core'; -import { MappingKey, buildMappings, buildStacks, toString } from '@volar/source-map'; +import { VirtualFile, buildMappings, buildStacks, resolveCommonLanguageId, toString, track } from '@volar/language-core'; import { computed } from 'computeds'; -import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Sfc, SfcBlock, VueLanguagePlugin } from '../types'; import { VueEmbeddedFile } from './embeddedFile'; @@ -137,7 +135,7 @@ function compiledPluginFiles( for (const embeddedFileName of embeddedFileNames) { if (!embeddedFiles[embeddedFileName]) { embeddedFiles[embeddedFileName] = computed(() => { - const [content, stacks] = codegenStack ? muggle.track([]) : [[], []]; + const [content, stacks] = codegenStack ? track([]) : [[], []]; const file = new VueEmbeddedFile(embeddedFileName, content, stacks); for (const plugin of plugins) { if (!plugin.resolveEmbeddedFile) { @@ -183,28 +181,37 @@ function compiledPluginFiles( return computed(() => { return files().map(_file => { + const { file, snapshot } = _file(); const mappings = buildMappings(file.content); + let lastValidMapping: typeof mappings[number]; + for (const mapping of mappings) { - const source = mapping[MappingKey.SOURCE_FILE]; - if (source !== undefined) { - const block = nameToBlock()[source]; + if (mapping.source !== undefined) { + const block = nameToBlock()[mapping.source]; if (block) { - mapping[MappingKey.SOURCE_CODE_RANGE] = [ - mapping[MappingKey.SOURCE_CODE_RANGE][0] + block.startTagEnd, - mapping[MappingKey.SOURCE_CODE_RANGE][1] + block.startTagEnd, - ]; + mapping.sourceOffsets = mapping.sourceOffsets.map(offset => offset + block.startTagEnd); } else { // ignore } - mapping[MappingKey.SOURCE_FILE] = undefined; + mapping.source = undefined; + } + if (mapping.data.__combineLastMappping) { + lastValidMapping!.sourceOffsets.push(...mapping.sourceOffsets); + lastValidMapping!.generatedOffsets.push(...mapping.generatedOffsets); + lastValidMapping!.lengths.push(...mapping.lengths); + continue; + } + else { + lastValidMapping = mapping; } } + return { file, snapshot, - mappings, + mappings: mappings.filter(mapping => !mapping.data.__combineLastMappping), codegenStacks: buildStacks(file.content, file.contentStacks), }; }); diff --git a/packages/language-core/src/virtualFile/computedMappings.ts b/packages/language-core/src/virtualFile/computedMappings.ts index 1f755cf8f3..8cfed8198d 100644 --- a/packages/language-core/src/virtualFile/computedMappings.ts +++ b/packages/language-core/src/virtualFile/computedMappings.ts @@ -1,5 +1,4 @@ -import { Mapping, Segment } from '@volar/source-map'; -import * as muggle from 'muggle-string'; +import { Mapping, Segment, replaceSourceRange } from '@volar/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { Sfc, VueCodeInformation } from '../types'; import { computed } from 'computeds'; @@ -18,27 +17,20 @@ export function computedMappings( ...sfc.customBlocks, ]) { if (block) { - muggle.replaceSourceRange( - str, undefined, block.startTagEnd, block.endTagStart, - [ - block.content, - undefined, - block.startTagEnd, - {}, - ], - ); + replaceSourceRange(str, undefined, block.startTagEnd, block.endTagStart, '\n\n'); } } - return str.map>((m) => { - const text = m[0]; - const start = m[2] as number; - const end = start + text.length; - return [ - undefined, - [start, end], - [start, end], - m[3] as VueCodeInformation, - ]; - }); + return str + .filter(s => typeof s !== 'string') + .map>((m) => { + const text = m[0]; + const start = m[2] as number; + return { + sourceOffsets: [start], + generatedOffsets: [start], + lengths: [text.length], + data: m[3] as VueCodeInformation, + }; + }); }); } diff --git a/packages/language-core/src/virtualFile/embeddedFile.ts b/packages/language-core/src/virtualFile/embeddedFile.ts index 0520b35562..13058b61f9 100644 --- a/packages/language-core/src/virtualFile/embeddedFile.ts +++ b/packages/language-core/src/virtualFile/embeddedFile.ts @@ -1,5 +1,5 @@ -import { CodeInformation, LinkedCodeTrigger, VirtualFile } from '@volar/language-core'; -import { Mapping, Segment, StackNode } from '@volar/source-map'; +import { LinkedCodeTrigger, Mapping, StackNode, VirtualFile } from '@volar/language-core'; +import { Code } from '../types'; export class VueEmbeddedFile { @@ -9,7 +9,7 @@ export class VueEmbeddedFile { constructor( public fileName: string, - public content: Segment[], + public content: Code[], public contentStacks: StackNode[], ) { } } diff --git a/packages/language-core/src/virtualFile/vueFile.ts b/packages/language-core/src/virtualFile/vueFile.ts index 79546b53ef..b60c944ade 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -1,5 +1,4 @@ -import { VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; -import { Stack } from '@volar/source-map'; +import { Stack, VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { VueCompilerOptions, VueLanguagePlugin } from '../types'; import { computedFiles } from './computedFiles'; diff --git a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts index ad87f690e5..85f504d46e 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -1,4 +1,4 @@ -import { MappingKey, Service } from '@volar/language-service'; +import { Service } from '@volar/language-service'; import { isCharacterTyping } from './vue-autoinsert-dotvalue'; const plugin: Service = (context, modules) => { @@ -31,8 +31,10 @@ const plugin: Service = (context, modules) => { const offset = document.offsetAt(position); for (const mappedRange of virtualFile.mappings) { - if (mappedRange[MappingKey.GENERATED_CODE_RANGE][1] === offset) { - const text = document.getText().substring(mappedRange[MappingKey.GENERATED_CODE_RANGE][0], mappedRange[MappingKey.GENERATED_CODE_RANGE][1]); + const generatedCodeEnd = mappedRange.generatedOffsets[mappedRange.generatedOffsets.length - 1] + + mappedRange.lengths[mappedRange.lengths.length - 1]; + if (generatedCodeEnd === offset) { + const text = document.getText().substring(mappedRange.generatedOffsets[0], generatedCodeEnd); const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); if (ast.statements.length === 1) { const statement = ast.statements[0]; @@ -63,8 +65,8 @@ const plugin: Service = (context, modules) => { .replaceAll('}', '\\}'); return { range: { - start: document.positionAt(mappedRange[MappingKey.GENERATED_CODE_RANGE][0]), - end: document.positionAt(mappedRange[MappingKey.GENERATED_CODE_RANGE][1]), + start: document.positionAt(mappedRange.generatedOffsets[0]), + end: document.positionAt(generatedCodeEnd), }, newText: '(' + escapedText + '$0' + ')', }; diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index 6d15e91e62..431415bfba 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,5 +1,5 @@ import { Service } from '@volar/language-service'; -import { MappingKey, SourceFile, VueCodeInformation, VueFile } from '@vue/language-core'; +import { SourceFile, VirtualFile, VueCodeInformation, VueFile } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; export const create = function (): Service { @@ -13,19 +13,22 @@ export const create = function (): Service { provideReferencesCodeLensRanges(document) { - return worker(document.uri, async (vueFile) => { + return worker(document.uri, async virtualFile => { const result: vscode.Range[] = []; - for (const map of context.documents.getMaps(vueFile) ?? []) { + for (const map of context.documents.getMaps(virtualFile) ?? []) { for (const mapping of map.map.codeMappings) { - if (!(mapping[MappingKey.DATA] as VueCodeInformation).__referencesCodeLens) + if (!(mapping.data as VueCodeInformation).__referencesCodeLens) continue; result.push({ - start: document.positionAt(mapping[MappingKey.SOURCE_CODE_RANGE][0]), - end: document.positionAt(mapping[MappingKey.SOURCE_CODE_RANGE][1]), + start: document.positionAt(mapping.generatedOffsets[0]), + end: document.positionAt( + mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + + mapping.lengths[mapping.lengths.length - 1] + ), }); } } @@ -36,10 +39,9 @@ export const create = function (): Service { async resolveReferencesCodeLensLocations(document, range, references) { - await worker(document.uri, async (vueFile) => { - - const document = context.documents.get(vueFile.id, vueFile.languageId, vueFile.snapshot); - const offset = document.offsetAt(range.start); + const [virtualFile, sourceFile] = context.project.fileProvider.getVirtualFile(document.uri); + if (virtualFile && sourceFile?.virtualFile?.[0] instanceof VueFile) { + const vueFile = sourceFile.virtualFile[0]; const blocks = [ vueFile.sfc.script, vueFile.sfc.scriptSetup, @@ -47,21 +49,32 @@ export const create = function (): Service { ...vueFile.sfc.styles, ...vueFile.sfc.customBlocks, ]; - const sourceBlock = blocks.find(block => block && offset >= block.startTagEnd && offset <= block.endTagStart); - references = references.filter(reference => - reference.uri !== document.uri // different file - || sourceBlock !== blocks.find(block => block && document.offsetAt(reference.range.start) >= block.startTagEnd && document.offsetAt(reference.range.end) <= block.endTagStart) // different block - ); - }); + for (const map of context.documents.getMaps(virtualFile)) { + const sourceOffset = map.map.getSourceOffset(document.offsetAt(range.start)); + if (sourceOffset !== undefined) { + const sourceBlock = blocks.find(block => block && sourceOffset[0] >= block.startTagEnd && sourceOffset[0] <= block.endTagStart); + const sourceDocument = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); + references = references.filter(reference => + reference.uri !== sourceDocument.uri // different file + || sourceBlock !== blocks.find(block => + block + && sourceDocument.offsetAt(reference.range.start) >= block.startTagEnd + && sourceDocument.offsetAt(reference.range.end) <= block.endTagStart + ) // different block + ); + break; + } + } + } return references; }, }; - function worker(uri: string, callback: (vueFile: VueFile, sourceFile: SourceFile) => T) { + function worker(uri: string, callback: (vueFile: VirtualFile, sourceFile: SourceFile) => T) { const [virtualFile, sourceFile] = context!.project.fileProvider.getVirtualFile(uri); - if (!(virtualFile instanceof VueFile) || !sourceFile) + if (!(sourceFile?.virtualFile?.[0] instanceof VueFile) || !sourceFile) return; return callback(virtualFile, sourceFile); diff --git a/packages/language-service/src/plugins/vue-extract-file.ts b/packages/language-service/src/plugins/vue-extract-file.ts index dd5029cc22..457a9c82b1 100644 --- a/packages/language-service/src/plugins/vue-extract-file.ts +++ b/packages/language-service/src/plugins/vue-extract-file.ts @@ -1,6 +1,6 @@ import { CreateFile, Service, ServiceContext, TextDocumentEdit, TextEdit } from '@volar/language-service'; import { ExpressionNode, type TemplateChildNode } from '@vue/compiler-dom'; -import { MappingKey, Sfc, VueFile, scriptRanges } from '@vue/language-core'; +import { Sfc, VueFile, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Provide } from 'volar-service-typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -192,7 +192,7 @@ export const create = function (): Service { const { name } = node; for (const map of maps) { const source = map.map.getSourceOffset(name.getEnd()); - if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1][MappingKey.DATA].semanticTokens) { + if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1].data.semanticTokens) { if (!result.has(name.text)) { const type = checker.getTypeAtLocation(node); const typeString = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation); diff --git a/packages/language-service/src/plugins/vue-twoslash-queries.ts b/packages/language-service/src/plugins/vue-twoslash-queries.ts index dfb1384624..899262b2a0 100644 --- a/packages/language-service/src/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/src/plugins/vue-twoslash-queries.ts @@ -34,7 +34,7 @@ const plugin: Service = (context: ServiceContext { for (const mapping of vitualFile.mappings) { - const hint = (mapping[MappingKey.DATA] as VueCodeInformation).__hint; + const hint = (mapping.data as VueCodeInformation).__hint; if ( - mapping[MappingKey.GENERATED_CODE_RANGE][0] >= start - && mapping[MappingKey.GENERATED_CODE_RANGE][1] <= end + mapping.generatedOffsets[0] >= start + && mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1] <= end && hint ) { @@ -39,7 +39,7 @@ const plugin: Service = (context) => { label: hint.label, paddingRight: hint.paddingRight, paddingLeft: hint.paddingLeft, - position: document.positionAt(mapping[MappingKey.GENERATED_CODE_RANGE][0]), + position: document.positionAt(mapping.generatedOffsets[0]), kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, tooltip: { kind: 'markdown', diff --git a/packages/tsc-eslint-hook/src/index.ts b/packages/tsc-eslint-hook/src/index.ts index 454b42dcd2..8cf4bb400d 100644 --- a/packages/tsc-eslint-hook/src/index.ts +++ b/packages/tsc-eslint-hook/src/index.ts @@ -27,7 +27,7 @@ export = async function ( const all: typeof vueFile.embeddedFiles = []; vueFile.embeddedFiles.forEach(async function visit(embeddedFile) { - if (embeddedFile.mappings.some(mapping => mapping[3].diagnostics ?? true)) { + if (embeddedFile.mappings.some(mapping => mapping.data.diagnostics ?? true)) { all.push(embeddedFile); } embeddedFile.embeddedFiles.forEach(visit); @@ -73,17 +73,17 @@ export = async function ( for (const start of map.getSourceOffsets(msgStart)) { - const reportStart = typeof start[1][3].diagnostics === 'object' - ? typeof start[1][3].diagnostics.shouldReport() - : (start[1][3].diagnostics ?? true); + const reportStart = typeof start[1].data.diagnostics === 'object' + ? typeof start[1].data.diagnostics.shouldReport() + : (start[1].data.diagnostics ?? true); if (!reportStart) continue; - for (const end of map.getSourceOffsets(msgEnd, true)) { + for (const end of map.getSourceOffsets(msgEnd)) { - const reportEnd = typeof end[1][3].diagnostics === 'object' - ? typeof end[1][3].diagnostics.shouldReport() - : (end[1][3].diagnostics ?? true); + const reportEnd = typeof end[1].data.diagnostics === 'object' + ? typeof end[1].data.diagnostics.shouldReport() + : (end[1].data.diagnostics ?? true); if (!reportEnd) continue; diff --git a/test-workspace/language-service/reference/slot-named/entry.vue b/test-workspace/language-service/reference/slot-named/entry.vue index ba43324499..b4f17d8bc7 100644 --- a/test-workspace/language-service/reference/slot-named/entry.vue +++ b/test-workspace/language-service/reference/slot-named/entry.vue @@ -1,4 +1,4 @@ From dabf16ae4f3f0c73c5810c94cf1db69e3409ccb4 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 27 Nov 2023 18:57:39 +0800 Subject: [PATCH 08/19] fix empty slot name completion [skip ci] --- .../language-core/src/generators/template.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 251ac46fe6..481d4e4c6b 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1008,7 +1008,7 @@ export function generate( ) : [ '.', - ['', 'template', slotDir.loc.start.offset, combineEnabled(presetInfos.slotName, { completionItems: false })] satisfies Code, + ['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completionItems: false }] satisfies Code, 'default', ['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), { __combineLastMappping: true }] satisfies Code, ] @@ -1045,8 +1045,13 @@ export function generate( [ '', 'template', - slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), - { completionItems: true }, + slotDir.loc.start.offset + ( + slotDir.loc.source.startsWith('#') + ? '#'.length : slotDir.loc.source.startsWith('v-slot:') + ? 'v-slot:'.length + : 0 + ), + withAllDisabled({ completionItems: true }), ], `'/* empty slot name completion */]\n`, ); @@ -1711,13 +1716,13 @@ export function generate( '__VLS_normalizeSlot(', ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], `${slotsAssignName ?? '__VLS_slots'}[`, - ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], + ['', 'template', node.loc.start.offset, { __combineLastMappping: true }], slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`, - ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], + ['', 'template', node.loc.end.offset, { __combineLastMappping: true }], ']', - ['', 'template', node.loc.end.offset, presetInfos.diagnosticOnly], + ['', 'template', node.loc.end.offset, { __combineLastMappping: true }], ')?.(', - ['', 'template', startTagOffset, presetInfos.diagnosticOnly], + ['', 'template', startTagOffset, { __combineLastMappping: true }], '{\n', ); } From 9cf5dd46d293912693fca8c3f9f21fa578d39044 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 27 Nov 2023 19:04:46 +0800 Subject: [PATCH 09/19] fix prop completion [ckip ci] --- packages/language-core/src/generators/template.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 481d4e4c6b..b4799643c3 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1314,18 +1314,18 @@ export function generate( let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { - caps_all = { + caps_all = withAllDisabled({ references: caps_all.references, renameEdits: caps_all.renameEdits, - }; - caps_diagnosticOnly = { + }); + caps_diagnosticOnly = withAllDisabled({ references: caps_diagnosticOnly.references, renameEdits: caps_diagnosticOnly.renameEdits, - }; - caps_attr = { + }); + caps_attr = withAllDisabled({ references: caps_attr.references, renameEdits: caps_attr.renameEdits, - }; + }); } codes.push(`...{ `); From f275f94f31a842eff2c359231ab0a79aa3ba3655 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 27 Nov 2023 20:07:54 +0800 Subject: [PATCH 10/19] fix script codegen feature settings [skip ci] --- .../language-core/src/generators/script.ts | 44 +++--- .../language-core/src/generators/template.ts | 129 +++++------------- .../language-core/src/generators/utils.ts | 66 +++++++++ 3 files changed, 122 insertions(+), 117 deletions(-) create mode 100644 packages/language-core/src/generators/utils.ts diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index 54a4cbc2f8..c873fe6d9b 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -8,6 +8,7 @@ import type { Code, VueCompilerOptions } from '../types'; import { Sfc } from '../types'; import { getSlotsPropertyName, hyphenateTag } from '../utils/shared'; import { walkInterpolationFragment } from '../utils/transform'; +import { disableAllFeatures, enableAllFeatures } from './utils'; export function generate( ts: typeof import('typescript/lib/tsserverlibrary'), @@ -88,7 +89,7 @@ export function generate( '', 'scriptSetup', scriptSetup.content.length, - {}, + disableAllFeatures({}), ]); } @@ -126,9 +127,7 @@ export function generate( usedHelperTypes.PropsChildren = true; codes.push(`$props: __VLS_PropsChildren;\n`); } - codes.push( - `} };\n`, - ); + codes.push(`} };\n`); } if (usedHelperTypes.PropsChildren) { codes.push(`type __VLS_PropsChildren = { [K in keyof (boolean extends (JSX.ElementChildrenAttribute extends never ? true : false) ? never : JSX.ElementChildrenAttribute)]?: S; };\n`); @@ -151,15 +150,12 @@ export function generate( `'${src}'`, 'script', script.srcOffset - 1, - { + enableAllFeatures({ renameEdits: src === script.src ? true : { shouldRename: false, shouldEdit: true, resolveEditText(newName) { - if ( - newName.endsWith('.jsx') - || newName.endsWith('.js') - ) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { newName = newName.split('.').slice(0, -1).join('.'); } if (script?.src?.endsWith('.d.ts')) { @@ -174,7 +170,7 @@ export function generate( return newName; }, }, - }, + }), ]); codes.push(`;\n`); codes.push(`export { default } from '${src}';\n`); @@ -195,7 +191,7 @@ export function generate( addVirtualCode('script', 0, scriptRanges.exportDefault.expression.start); codes.push(vueCompilerOptions.optionsWrapper[0]); { - codes.push(['', 'script', scriptRanges.exportDefault.expression.start, { + codes.push(['', 'script', scriptRanges.exportDefault.expression.start, disableAllFeatures({ __hint: { setting: 'vue.inlayHints.optionsWrapper', label: vueCompilerOptions.optionsWrapper[0], @@ -204,15 +200,15 @@ export function generate( 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', ].join('\n\n'), } - } as any]); + })]); addVirtualCode('script', scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end); - codes.push(['', 'script', scriptRanges.exportDefault.expression.end, { + codes.push(['', 'script', scriptRanges.exportDefault.expression.end, disableAllFeatures({ __hint: { setting: 'vue.inlayHints.optionsWrapper', label: vueCompilerOptions.optionsWrapper[1], tooltip: '', } - } as any]); + })]); } codes.push(vueCompilerOptions.optionsWrapper[1]); addVirtualCode('script', scriptRanges.exportDefault.expression.end, script.content.length); @@ -242,7 +238,7 @@ export function generate( scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + '\n', 'scriptSetup', 0, - {}, + enableAllFeatures({}), ]); } function generateExportDefaultEndMapping() { @@ -254,7 +250,7 @@ export function generate( '', 'scriptSetup', scriptSetup.content.length, - { diagnostics: true }, + disableAllFeatures({ diagnostics: true }), ]); codes.push(`\n`); } @@ -276,7 +272,7 @@ export function generate( scriptSetup.generic, scriptSetup.name, scriptSetup.genericOffset, - {}, + enableAllFeatures({}), ]); if (!scriptSetup.generic.endsWith(',')) { codes.push(`,`); @@ -781,10 +777,10 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: script.content.substring(componentsOption.start, componentsOption.end), 'script', componentsOption.start, - { + disableAllFeatures({ references: true, renameEdits: true, - }, + }), ]); } else { @@ -952,8 +948,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: style.name, cssBind.offset + fragOffset, onlyForErrorMapping - ? { diagnostics: true } - : {}, + ? disableAllFeatures({ diagnostics: true }) + : enableAllFeatures({}), ]); } }, @@ -997,7 +993,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, start, - {}, // diagnostic also working for setup() returns unused in template checking + enableAllFeatures({}), // diagnostic also working for setup() returns unused in template checking ]); resetOffsetStack(); } @@ -1007,11 +1003,11 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, start, - { + disableAllFeatures({ references: true, definitions: true, renameEdits: true, - }, + }), ]); resetOffsetStack(); } diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index b4799643c3..e683197e41 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -6,69 +6,12 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; import { hyphenateAttr, hyphenateTag } from '../utils/shared'; import { collectVars, walkInterpolationFragment } from '../utils/transform'; +import { mergeFeatureSettings, disableAllFeatures, enableAllFeatures } from './utils'; -const withAllDisabled = (override: VueCodeInformation): VueCodeInformation => ({ - diagnostics: false, - renameEdits: false, - formattingEdits: false, - completionItems: false, - definitions: false, - references: false, - foldingRanges: false, - inlayHints: false, - codeActions: false, - symbols: false, - selectionRanges: false, - linkedEditingRanges: false, - colors: false, - autoInserts: false, - codeLenses: false, - highlights: false, - links: false, - semanticTokens: false, - hover: false, - signatureHelps: false, - ...override, -}); -const withAllEnabled = (override: VueCodeInformation): VueCodeInformation => ({ - diagnostics: true, - renameEdits: true, - formattingEdits: true, - completionItems: true, - definitions: true, - references: true, - foldingRanges: true, - inlayHints: true, - codeActions: true, - symbols: true, - selectionRanges: true, - linkedEditingRanges: true, - colors: true, - autoInserts: true, - codeLenses: true, - highlights: true, - links: true, - semanticTokens: true, - hover: true, - signatureHelps: true, - ...override, -}); -const combineEnabled = (...infos: VueCodeInformation[]): VueCodeInformation => { - const result: VueCodeInformation = { ...infos[0] }; - for (const info of infos) { - for (const key in info) { - const value = info[key as keyof VueCodeInformation]; - if (value) { - result[key as keyof VueCodeInformation] = value as any; - } - } - } - return result; -}; const presetInfos = { - disabledAll: withAllDisabled({}), - all: withAllEnabled({}), - allWithHiddenParam: withAllEnabled({ + disabledAll: disableAllFeatures({}), + all: enableAllFeatures({}), + allWithHiddenParam: enableAllFeatures({ __hint: { setting: 'vue.inlayHints.inlineHandlerLeading', label: '$event =>', @@ -80,18 +23,18 @@ const presetInfos = { paddingRight: true, } }), - noDiagnostics: withAllEnabled({ diagnostics: false }), - diagnosticOnly: withAllDisabled({ diagnostics: true }), - tagHover: withAllDisabled({ hover: true }), - event: withAllDisabled({ hover: true, diagnostics: true }), - tagReference: withAllDisabled({ references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } }), - attr: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true }), - attrReference: withAllDisabled({ references: true, definitions: true, renameEdits: true }), - slotProp: withAllDisabled({ references: true, definitions: true, renameEdits: true, diagnostics: true }), - scopedClassName: withAllDisabled({ references: true, definitions: true, renameEdits: true, completionItems: true }), - slotName: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, completionItems: true }), - slotNameExport: withAllDisabled({ hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ }), - refAttr: withAllDisabled({ references: true, definitions: true, renameEdits: true }), + noDiagnostics: enableAllFeatures({ diagnostics: false }), + diagnosticOnly: disableAllFeatures({ diagnostics: true }), + tagHover: disableAllFeatures({ hover: true }), + event: disableAllFeatures({ hover: true, diagnostics: true }), + tagReference: disableAllFeatures({ references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } }), + attr: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true }), + attrReference: disableAllFeatures({ references: true, definitions: true, renameEdits: true }), + slotProp: disableAllFeatures({ references: true, definitions: true, renameEdits: true, diagnostics: true }), + scopedClassName: disableAllFeatures({ references: true, definitions: true, renameEdits: true, completionItems: true }), + slotName: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, completionItems: true }), + slotNameExport: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ }), + refAttr: disableAllFeatures({ references: true, definitions: true, renameEdits: true }), }; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], @@ -210,12 +153,12 @@ export function generate( yield* createObjectPropertyCode( slot.name, slot.loc, - combineEnabled(presetInfos.slotNameExport, { __referencesCodeLens: true }), + mergeFeatureSettings(presetInfos.slotNameExport, { __referencesCodeLens: true }), slot.nodeLoc ); } else { - yield ['', 'template', slot.tagRange[0], combineEnabled(presetInfos.slotNameExport, { __referencesCodeLens: true })]; + yield ['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, { __referencesCodeLens: true })]; yield 'default'; yield ['', 'template', slot.tagRange[1], { __combineLastMappping: true }]; } @@ -232,7 +175,7 @@ export function generate( ...createStringLiteralKeyCode( className, offset, - combineEnabled( + mergeFeatureSettings( presetInfos.scopedClassName, { __displayWithLink: stylesScopedClasses.has(className) }, ), @@ -307,7 +250,7 @@ export function generate( ...createPropertyAccessCode( tagName, tagOffset, - combineEnabled( + mergeFeatureSettings( presetInfos.tagReference, { renameEdits: true @@ -329,7 +272,7 @@ export function generate( ...createCamelizeCode( shouldCapitalize ? capitalize(tagName) : tagName, tagOffset, - combineEnabled( + mergeFeatureSettings( presetInfos.tagReference, { renameEdits: { @@ -365,7 +308,7 @@ export function generate( ...createCamelizeCode( capitalize(tagName), tagOffset, - withAllDisabled({ + disableAllFeatures({ completionItems: { isAdditional: true, onlyImport: true, @@ -818,7 +761,7 @@ export function generate( ...createCanonicalComponentNameCode( tag, offset, - combineEnabled( + mergeFeatureSettings( presetInfos.tagHover, presetInfos.diagnosticOnly, ), @@ -1051,7 +994,7 @@ export function generate( ? 'v-slot:'.length : 0 ), - withAllDisabled({ completionItems: true }), + disableAllFeatures({ completionItems: true }), ], `'/* empty slot name completion */]\n`, ); @@ -1070,7 +1013,7 @@ export function generate( if (!hasSlotElements.has(node) && node.children.length) { codes.push( `(${componentCtxVar}.slots!).`, - ['', 'template', node.children[0].loc.start.offset, withAllDisabled({ references: true })], + ['', 'template', node.children[0].loc.start.offset, disableAllFeatures({ references: true })], 'default', ['', 'template', node.children[node.children.length - 1].loc.end.offset, { __combineLastMappping: true }], ';\n', @@ -1101,7 +1044,7 @@ export function generate( '', 'template', prop.arg.loc.start.offset, - combineEnabled( + mergeFeatureSettings( presetInfos.attrReference, { renameEdits: { @@ -1314,15 +1257,15 @@ export function generate( let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { - caps_all = withAllDisabled({ + caps_all = disableAllFeatures({ references: caps_all.references, renameEdits: caps_all.renameEdits, }); - caps_diagnosticOnly = withAllDisabled({ + caps_diagnosticOnly = disableAllFeatures({ references: caps_diagnosticOnly.references, renameEdits: caps_diagnosticOnly.renameEdits, }); - caps_attr = withAllDisabled({ + caps_attr = disableAllFeatures({ references: caps_attr.references, renameEdits: caps_attr.renameEdits, }); @@ -1384,7 +1327,7 @@ export function generate( ? prop.arg.loc.start.offset : prop.loc.start.offset, prop.arg - ? combineEnabled( + ? mergeFeatureSettings( caps_attr, { renameEdits: { @@ -1452,7 +1395,7 @@ export function generate( prop.name, prop.loc.start.offset, shouldCamelize - ? combineEnabled(caps_attr, { + ? mergeFeatureSettings(caps_attr, { renameEdits: { shouldRename: true, shouldEdit: true, @@ -1588,7 +1531,7 @@ export function generate( ...createCamelizeCode( 'v-' + prop.name, prop.loc.start.offset, - combineEnabled( + mergeFeatureSettings( presetInfos.noDiagnostics, { completionItems: { @@ -1758,7 +1701,7 @@ export function generate( ...createObjectPropertyCode( prop.arg.content, prop.arg.loc.start.offset, - combineEnabled( + mergeFeatureSettings( presetInfos.slotProp, { renameEdits: { @@ -1791,7 +1734,7 @@ export function generate( ...createObjectPropertyCode( prop.name, prop.loc.start.offset, - combineEnabled( + mergeFeatureSettings( presetInfos.attr, { renameEdits: { @@ -1891,7 +1834,7 @@ export function generate( v.text, 'template', v.offset, - withAllDisabled({ completionItems: { isAdditional: true }, }), + disableAllFeatures({ completionItems: { isAdditional: true }, }), ]); codes.push(','); } @@ -1953,7 +1896,7 @@ export function generate( code, 'template', offset, - combineEnabled( + mergeFeatureSettings( presetInfos.disabledAll, { formattingEdits: true, diff --git a/packages/language-core/src/generators/utils.ts b/packages/language-core/src/generators/utils.ts new file mode 100644 index 0000000000..76320e850a --- /dev/null +++ b/packages/language-core/src/generators/utils.ts @@ -0,0 +1,66 @@ +import { VueCodeInformation } from '../types'; + +export function disableAllFeatures(override: VueCodeInformation): VueCodeInformation { + return { + diagnostics: false, + renameEdits: false, + formattingEdits: false, + completionItems: false, + definitions: false, + references: false, + foldingRanges: false, + inlayHints: false, + codeActions: false, + symbols: false, + selectionRanges: false, + linkedEditingRanges: false, + colors: false, + autoInserts: false, + codeLenses: false, + highlights: false, + links: false, + semanticTokens: false, + hover: false, + signatureHelps: false, + ...override, + }; +} + +export function enableAllFeatures(override: VueCodeInformation): VueCodeInformation { + return { + diagnostics: true, + renameEdits: true, + formattingEdits: true, + completionItems: true, + definitions: true, + references: true, + foldingRanges: true, + inlayHints: true, + codeActions: true, + symbols: true, + selectionRanges: true, + linkedEditingRanges: true, + colors: true, + autoInserts: true, + codeLenses: true, + highlights: true, + links: true, + semanticTokens: true, + hover: true, + signatureHelps: true, + ...override, + }; +} + +export function mergeFeatureSettings(...infos: VueCodeInformation[]): VueCodeInformation { + const result: VueCodeInformation = { ...infos[0] }; + for (const info of infos) { + for (const key in info) { + const value = info[key as keyof VueCodeInformation]; + if (value) { + result[key as keyof VueCodeInformation] = value as any; + } + } + } + return result; +} From 91d18ef11a65857eb1be2cb53f75059b9c21d640 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 27 Nov 2023 20:11:27 +0800 Subject: [PATCH 11/19] Update script.ts [skip ci] --- packages/language-core/src/generators/script.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index c873fe6d9b..a1dd6e5540 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -891,17 +891,17 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: '', 'style_' + styleIndex, offset, - { + disableAllFeatures({ references: true, __referencesCodeLens: referencesCodeLens, - }, + }), ]); codes.push(`'`); codes.push([ '', 'style_' + styleIndex, offset, - { + disableAllFeatures({ references: true, renameEdits: { shouldRename: true, @@ -909,7 +909,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: resolveNewName: normalizeCssRename, resolveEditText: applyCssRename, }, - }, + }), ]); codes.push([ classNameWithDot.substring(1), @@ -922,7 +922,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: '', 'style_' + styleIndex, offset + classNameWithDot.length, - {}, + disableAllFeatures({}), ]); codes.push(`${optional ? '?' : ''}: ${propertyType}`); codes.push(` }`); From 100ca5bfddd0b03b1c3b561f17303734f7ef01f4 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 28 Nov 2023 14:02:14 +0800 Subject: [PATCH 12/19] sync https://github.com/volarjs/volar.js/pull/96 changes [skip ci] --- .../language-core/src/generators/script.ts | 41 ++-- .../language-core/src/generators/template.ts | 178 ++++++++---------- .../language-core/src/generators/utils.ts | 62 ++---- .../src/plugins/vue-sfc-customblocks.ts | 3 +- .../src/plugins/vue-sfc-scripts.ts | 13 +- .../src/plugins/vue-sfc-styles.ts | 3 +- .../src/plugins/vue-sfc-template.ts | 3 +- packages/language-core/src/plugins/vue-tsx.ts | 10 +- .../src/virtualFile/computedFiles.ts | 6 +- .../src/virtualFile/computedMappings.ts | 5 +- .../src/virtualFile/embeddedFile.ts | 4 +- .../language-service/src/languageService.ts | 2 +- .../src/plugins/vue-extract-file.ts | 9 +- .../src/plugins/vue-twoslash-queries.ts | 2 +- packages/tsc-eslint-hook/package.json | 1 + packages/tsc-eslint-hook/src/index.ts | 15 +- 16 files changed, 152 insertions(+), 205 deletions(-) diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index a1dd6e5540..071daf5f99 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -1,4 +1,4 @@ -import { LinkedCodeTrigger, Mapping, getLength, offsetStack, resetOffsetStack, setTracking, track } from '@volar/language-core'; +import { Mapping, getLength, offsetStack, resetOffsetStack, setTracking, track } from '@volar/language-core'; import * as path from 'path-browserify'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type * as templateGen from '../generators/template'; @@ -26,7 +26,7 @@ export function generate( ) { const [codes, codeStacks] = codegenStack ? track([] as Code[]) : [[], []]; - const mirrorBehaviorMappings: Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; + const mirrorBehaviorMappings: Mapping[] = []; //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 if (!script && !scriptSetup) { @@ -151,10 +151,9 @@ export function generate( 'script', script.srcOffset - 1, enableAllFeatures({ - renameEdits: src === script.src ? true : { - shouldRename: false, - shouldEdit: true, - resolveEditText(newName) { + navigation: src === script.src ? true : { + shouldRename: () => false, + resolveRenameEditText(newName) { if (newName.endsWith('.jsx') || newName.endsWith('.js')) { newName = newName.split('.').slice(0, -1).join('.'); } @@ -250,7 +249,7 @@ export function generate( '', 'scriptSetup', scriptSetup.content.length, - disableAllFeatures({ diagnostics: true }), + disableAllFeatures({ verification: true }), ]); codes.push(`\n`); } @@ -406,7 +405,7 @@ export function generate( sourceOffsets: [defineProp.name.start + scriptSetupGeneratedOffset], generatedOffsets: [propMirror], lengths: [defineProp.name.end - defineProp.name.start], - data: [{}, {}], + data: undefined, }); } } @@ -753,7 +752,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: sourceOffsets: [scriptOffset], generatedOffsets: [templateOffset], lengths: [varName.length], - data: [{}, {}], + data: undefined, }); } } @@ -778,8 +777,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: 'script', componentsOption.start, disableAllFeatures({ - references: true, - renameEdits: true, + navigation: true, }), ]); } @@ -892,7 +890,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: 'style_' + styleIndex, offset, disableAllFeatures({ - references: true, + navigation: true, __referencesCodeLens: referencesCodeLens, }), ]); @@ -902,12 +900,9 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: 'style_' + styleIndex, offset, disableAllFeatures({ - references: true, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: normalizeCssRename, - resolveEditText: applyCssRename, + navigation: { + resolveRenameNewName: normalizeCssRename, + resolveRenameEditText: applyCssRename, }, }), ]); @@ -915,7 +910,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: classNameWithDot.substring(1), 'style_' + styleIndex, offset + 1, - { __combineLastMappping: true }, + disableAllFeatures({ __combineLastMappping: true }), ]); codes.push(`'`); codes.push([ @@ -948,7 +943,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: style.name, cssBind.offset + fragOffset, onlyForErrorMapping - ? disableAllFeatures({ diagnostics: true }) + ? disableAllFeatures({ verification: true }) : enableAllFeatures({}), ]); } @@ -1003,11 +998,7 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), vueTag, start, - disableAllFeatures({ - references: true, - definitions: true, - renameEdits: true, - }), + disableAllFeatures({ navigation: true }), ]); resetOffsetStack(); } diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index e683197e41..36152f1ab9 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -23,18 +23,18 @@ const presetInfos = { paddingRight: true, } }), - noDiagnostics: enableAllFeatures({ diagnostics: false }), - diagnosticOnly: disableAllFeatures({ diagnostics: true }), - tagHover: disableAllFeatures({ hover: true }), - event: disableAllFeatures({ hover: true, diagnostics: true }), - tagReference: disableAllFeatures({ references: true, definitions: true, renameEdits: { shouldRename: false, shouldEdit: true } }), - attr: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, renameEdits: true }), - attrReference: disableAllFeatures({ references: true, definitions: true, renameEdits: true }), - slotProp: disableAllFeatures({ references: true, definitions: true, renameEdits: true, diagnostics: true }), - scopedClassName: disableAllFeatures({ references: true, definitions: true, renameEdits: true, completionItems: true }), - slotName: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, completionItems: true }), - slotNameExport: disableAllFeatures({ hover: true, diagnostics: true, references: true, definitions: true, /* __referencesCodeLens: true */ }), - refAttr: disableAllFeatures({ references: true, definitions: true, renameEdits: true }), + noDiagnostics: enableAllFeatures({ verification: false }), + diagnosticOnly: disableAllFeatures({ verification: true }), + tagHover: disableAllFeatures({ semantic: true }), + event: disableAllFeatures({ semantic: true, verification: true }), + tagReference: disableAllFeatures({ navigation: { shouldRename: () => false } }), + attr: disableAllFeatures({ semantic: true, verification: true, navigation: true }), + attrReference: disableAllFeatures({ navigation: true }), + slotProp: disableAllFeatures({ navigation: true, verification: true }), + scopedClassName: disableAllFeatures({ navigation: true, completion: true }), + slotName: disableAllFeatures({ semantic: true, verification: true, navigation: true, completion: true }), + slotNameExport: disableAllFeatures({ semantic: true, verification: true, navigation: true, /* __navigationCodeLens: true */ }), + refAttr: disableAllFeatures({ navigation: true }), }; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], @@ -153,14 +153,14 @@ export function generate( yield* createObjectPropertyCode( slot.name, slot.loc, - mergeFeatureSettings(presetInfos.slotNameExport, { __referencesCodeLens: true }), + mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true })), slot.nodeLoc ); } else { - yield ['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, { __referencesCodeLens: true })]; + yield ['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true }))]; yield 'default'; - yield ['', 'template', slot.tagRange[1], { __combineLastMappping: true }]; + yield ['', 'template', slot.tagRange[1], disableAllFeatures({ __combineLastMappping: true })]; } yield `?(_: typeof ${slot.varName}): any,\n`; } @@ -177,7 +177,7 @@ export function generate( offset, mergeFeatureSettings( presetInfos.scopedClassName, - { __displayWithLink: stylesScopedClasses.has(className) }, + disableAllFeatures({ __displayWithLink: stylesScopedClasses.has(className) }), ), ), `];\n`, @@ -253,7 +253,7 @@ export function generate( mergeFeatureSettings( presetInfos.tagReference, { - renameEdits: true + navigation: true }, ...(nativeTags.has(tagName) ? [ presetInfos.tagHover, @@ -275,11 +275,9 @@ export function generate( mergeFeatureSettings( presetInfos.tagReference, { - renameEdits: { - shouldEdit: true, - shouldRename: true, - resolveNewName: tagName !== expectName ? camelizeComponentName : undefined, - resolveEditText: getTagRenameApply(tagName), + navigation: { + resolveRenameNewName: tagName !== expectName ? camelizeComponentName : undefined, + resolveRenameEditText: getTagRenameApply(tagName), } }, ...(nativeTags.has(tagName) ? [ @@ -309,7 +307,7 @@ export function generate( capitalize(tagName), tagOffset, disableAllFeatures({ - completionItems: { + completion: { isAdditional: true, onlyImport: true, }, @@ -373,10 +371,10 @@ export function generate( continue; } const data = code[3]; - if (data.diagnostics) { + if (data.verification) { code[3] = { ...data, - diagnostics: false, + verification: false, }; } } @@ -394,10 +392,10 @@ export function generate( continue; } const data = code[3]; - if (data.diagnostics) { + if (data.verification) { code[3] = { ...data, - diagnostics: { + verification: { shouldReport: suppressError, }, }; @@ -408,18 +406,18 @@ export function generate( '', 'template', expectedErrorNode.loc.start.offset, - { - diagnostics: { + disableAllFeatures({ + verification: { shouldReport: () => errors === 0, }, - }, + }), ], '// @ts-expect-error __VLS_TS_EXPECT_ERROR', [ '', 'template', expectedErrorNode.loc.end.offset, - { __combineLastMappping: true }, + disableAllFeatures({ __combineLastMappping: true }), ], '\n;\n', ); @@ -951,9 +949,9 @@ export function generate( ) : [ '.', - ['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completionItems: false }] satisfies Code, + ['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completion: false }] satisfies Code, 'default', - ['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), { __combineLastMappping: true }] satisfies Code, + ['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), disableAllFeatures({ __combineLastMappping: true })] satisfies Code, ] ), ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly], @@ -994,7 +992,7 @@ export function generate( ? 'v-slot:'.length : 0 ), - disableAllFeatures({ completionItems: true }), + disableAllFeatures({ completion: true }), ], `'/* empty slot name completion */]\n`, ); @@ -1013,9 +1011,9 @@ export function generate( if (!hasSlotElements.has(node) && node.children.length) { codes.push( `(${componentCtxVar}.slots!).`, - ['', 'template', node.children[0].loc.start.offset, disableAllFeatures({ references: true })], + ['', 'template', node.children[0].loc.start.offset, disableAllFeatures({ navigation: true })], 'default', - ['', 'template', node.children[node.children.length - 1].loc.end.offset, { __combineLastMappping: true }], + ['', 'template', node.children[node.children.length - 1].loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ';\n', ); } @@ -1047,15 +1045,13 @@ export function generate( mergeFeatureSettings( presetInfos.attrReference, { - renameEdits: { - shouldRename: true, - shouldEdit: true, + navigation: { // @click-outside -> onClickOutside - resolveNewName(newName) { + resolveRenameNewName(newName) { return camelize('on-' + newName); }, // onClickOutside -> @click-outside - resolveEditText(newName) { + resolveRenameEditText(newName) { const hName = hyphenateAttr(newName); if (hyphenateAttr(newName).startsWith('on-')) { return camelize(hName.slice('on-'.length)); @@ -1074,7 +1070,7 @@ export function generate( ...createCamelizeCode( capitalize(prop.arg.loc.source), prop.arg.loc.start.offset, - { __combineLastMappping: true }, + disableAllFeatures({ __combineLastMappping: true }), ), ); } @@ -1083,15 +1079,15 @@ export function generate( `[`, startCode, `'`, - ['', 'template', prop.arg.loc.start.offset, { __combineLastMappping: true }], + ['', 'template', prop.arg.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })], 'on', ...createCamelizeCode( capitalize(prop.arg.loc.source), prop.arg.loc.start.offset, - { __combineLastMappping: true }, + disableAllFeatures({ __combineLastMappping: true }), ), `'`, - ['', 'template', prop.arg.loc.end.offset, { __combineLastMappping: true }], + ['', 'template', prop.arg.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], `]`, ); } @@ -1257,18 +1253,9 @@ export function generate( let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { - caps_all = disableAllFeatures({ - references: caps_all.references, - renameEdits: caps_all.renameEdits, - }); - caps_diagnosticOnly = disableAllFeatures({ - references: caps_diagnosticOnly.references, - renameEdits: caps_diagnosticOnly.renameEdits, - }); - caps_attr = disableAllFeatures({ - references: caps_attr.references, - renameEdits: caps_attr.renameEdits, - }); + caps_all = disableAllFeatures({ navigation: caps_all.navigation }); + caps_diagnosticOnly = disableAllFeatures({ navigation: caps_diagnosticOnly.navigation }); + caps_attr = disableAllFeatures({ navigation: caps_attr.navigation }); } codes.push(`...{ `); @@ -1330,11 +1317,9 @@ export function generate( ? mergeFeatureSettings( caps_attr, { - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: shouldCamelize ? hyphenateAttr : undefined, + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, }, }, ) @@ -1396,11 +1381,9 @@ export function generate( prop.loc.start.offset, shouldCamelize ? mergeFeatureSettings(caps_attr, { - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: hyphenateAttr, + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: hyphenateAttr, }, }) : caps_attr, @@ -1480,11 +1463,10 @@ export function generate( content, 'template', prop.arg.loc.start.offset + start, - { - formattingEdits: false, - foldingRanges: false, - symbols: false, - }, + enableAllFeatures({ + format: false, + structure: false, + }), ]); cssCodes.push(` }\n`); } @@ -1534,15 +1516,13 @@ export function generate( mergeFeatureSettings( presetInfos.noDiagnostics, { - completionItems: { + completion: { // fix https://github.com/vuejs/language-tools/issues/1905 isAdditional: true, }, - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.name), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), }, }, ), @@ -1659,13 +1639,13 @@ export function generate( '__VLS_normalizeSlot(', ['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly], `${slotsAssignName ?? '__VLS_slots'}[`, - ['', 'template', node.loc.start.offset, { __combineLastMappping: true }], + ['', 'template', node.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })], slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`, - ['', 'template', node.loc.end.offset, { __combineLastMappping: true }], + ['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ']', - ['', 'template', node.loc.end.offset, { __combineLastMappping: true }], + ['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ')?.(', - ['', 'template', startTagOffset, { __combineLastMappping: true }], + ['', 'template', startTagOffset, disableAllFeatures({ __combineLastMappping: true })], '{\n', ); } @@ -1704,11 +1684,9 @@ export function generate( mergeFeatureSettings( presetInfos.slotProp, { - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.arg.content), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.arg.content), }, }, ), @@ -1737,11 +1715,9 @@ export function generate( mergeFeatureSettings( presetInfos.attr, { - renameEdits: { - shouldRename: true, - shouldEdit: true, - resolveNewName: camelize, - resolveEditText: getPropRenameApply(prop.name), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), }, }, ), @@ -1834,7 +1810,7 @@ export function generate( v.text, 'template', v.offset, - disableAllFeatures({ completionItems: { isAdditional: true }, }), + disableAllFeatures({ completion: { isAdditional: true }, }), ]); codes.push(','); } @@ -1862,7 +1838,7 @@ export function generate( if (needToUnicode(content)) { yield ['', 'template', start, info]; yield toUnicode(content); - yield ['', 'template', end, { __combineLastMappping: true }]; + yield ['', 'template', end, disableAllFeatures({ __combineLastMappping: true })]; } else { yield [content, 'template', start, info]; @@ -1883,7 +1859,7 @@ export function generate( offset, i === 0 ? info - : { __combineLastMappping: true }, + : disableAllFeatures({ __combineLastMappping: true }), ]; } offset += part.length + 1; @@ -1899,8 +1875,8 @@ export function generate( mergeFeatureSettings( presetInfos.disabledAll, { - formattingEdits: true, - autoInserts: true, // support vue-autoinsert-parentheses + format: true, + // autoInserts: true, // TODO: support vue-autoinsert-parentheses }, ), ]; @@ -1922,9 +1898,9 @@ export function generate( else { yield ['', 'template', offset, info]; yield '"'; - yield* createCamelizeCode(code, offset, { __combineLastMappping: true }); + yield* createCamelizeCode(code, offset, disableAllFeatures({ __combineLastMappping: true })); yield '"'; - yield ['', 'template', offset + code.length, { __combineLastMappping: true }]; + yield ['', 'template', offset + code.length, disableAllFeatures({ __combineLastMappping: true })]; } } else { @@ -2027,9 +2003,9 @@ export function generate( function* createStringLiteralKeyCode(code: string, offset: number, info: VueCodeInformation): Generator { yield ['', 'template', offset, info]; yield '"'; - yield [code, 'template', offset, { __combineLastMappping: true }]; + yield [code, 'template', offset, disableAllFeatures({ __combineLastMappping: true })]; yield '"'; - yield ['', 'template', offset + code.length, { __combineLastMappping: true }]; + yield ['', 'template', offset + code.length, disableAllFeatures({ __combineLastMappping: true })]; } } diff --git a/packages/language-core/src/generators/utils.ts b/packages/language-core/src/generators/utils.ts index 76320e850a..041cb87f9e 100644 --- a/packages/language-core/src/generators/utils.ts +++ b/packages/language-core/src/generators/utils.ts @@ -1,60 +1,32 @@ import { VueCodeInformation } from '../types'; -export function disableAllFeatures(override: VueCodeInformation): VueCodeInformation { +export function disableAllFeatures(override: Partial): VueCodeInformation { return { - diagnostics: false, - renameEdits: false, - formattingEdits: false, - completionItems: false, - definitions: false, - references: false, - foldingRanges: false, - inlayHints: false, - codeActions: false, - symbols: false, - selectionRanges: false, - linkedEditingRanges: false, - colors: false, - autoInserts: false, - codeLenses: false, - highlights: false, - links: false, - semanticTokens: false, - hover: false, - signatureHelps: false, + verification: false, + completion: false, + semantic: false, + navigation: false, + structure: false, + format: false, ...override, }; } -export function enableAllFeatures(override: VueCodeInformation): VueCodeInformation { +export function enableAllFeatures(override: Partial): VueCodeInformation { return { - diagnostics: true, - renameEdits: true, - formattingEdits: true, - completionItems: true, - definitions: true, - references: true, - foldingRanges: true, - inlayHints: true, - codeActions: true, - symbols: true, - selectionRanges: true, - linkedEditingRanges: true, - colors: true, - autoInserts: true, - codeLenses: true, - highlights: true, - links: true, - semanticTokens: true, - hover: true, - signatureHelps: true, + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, ...override, }; } -export function mergeFeatureSettings(...infos: VueCodeInformation[]): VueCodeInformation { - const result: VueCodeInformation = { ...infos[0] }; - for (const info of infos) { +export function mergeFeatureSettings(base: VueCodeInformation, ...others: Partial[]): VueCodeInformation { + const result: VueCodeInformation = { ...base }; + for (const info of others) { for (const key in info) { const value = info[key as keyof VueCodeInformation]; if (value) { diff --git a/packages/language-core/src/plugins/vue-sfc-customblocks.ts b/packages/language-core/src/plugins/vue-sfc-customblocks.ts index 835e877416..7ec8d1d5d0 100644 --- a/packages/language-core/src/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/src/plugins/vue-sfc-customblocks.ts @@ -1,3 +1,4 @@ +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const customBlockReg = /^(.*)\.customBlock_([^_]+)_(\d+)\.([^.]+)$/; @@ -27,7 +28,7 @@ const plugin: VueLanguagePlugin = () => { customBlock.content, customBlock.name, 0, - {}, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-scripts.ts b/packages/language-core/src/plugins/vue-sfc-scripts.ts index af566e5a5a..df45f8cb0e 100644 --- a/packages/language-core/src/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/src/plugins/vue-sfc-scripts.ts @@ -1,3 +1,4 @@ +import { disableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const scriptFormatReg = /^(.*)\.script_format\.([^.]+)$/; @@ -29,15 +30,11 @@ const plugin: VueLanguagePlugin = () => { script.content, script.name, 0, - {}, + disableAllFeatures({ + structure: true, + format: true, + }), ]); - embeddedFile.content.forEach(code => { - if (typeof code !== 'string') { - code[3].diagnostics = false; - code[3].codeActions = false; - code[3].inlayHints = false; - } - }); } }, }; diff --git a/packages/language-core/src/plugins/vue-sfc-styles.ts b/packages/language-core/src/plugins/vue-sfc-styles.ts index 9aacbd28aa..b661a8d88f 100644 --- a/packages/language-core/src/plugins/vue-sfc-styles.ts +++ b/packages/language-core/src/plugins/vue-sfc-styles.ts @@ -1,3 +1,4 @@ +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const styleReg = /^(.*)\.style_(\d+)\.([^.]+)$/; @@ -27,7 +28,7 @@ const plugin: VueLanguagePlugin = () => { style.content, style.name, 0, - {}, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-template.ts b/packages/language-core/src/plugins/vue-sfc-template.ts index 439d4d565e..1b366f235a 100644 --- a/packages/language-core/src/plugins/vue-sfc-template.ts +++ b/packages/language-core/src/plugins/vue-sfc-template.ts @@ -1,3 +1,4 @@ +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const templateReg = /^(.*)\.template\.([^.]+)$/; @@ -22,7 +23,7 @@ const plugin: VueLanguagePlugin = () => { sfc.template.content, sfc.template.name, 0, - {}, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-tsx.ts b/packages/language-core/src/plugins/vue-tsx.ts index 19dc120900..92ef46e700 100644 --- a/packages/language-core/src/plugins/vue-tsx.ts +++ b/packages/language-core/src/plugins/vue-tsx.ts @@ -5,6 +5,7 @@ import { generate as generateTemplate } from '../generators/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges'; import { Sfc, VueLanguagePlugin } from '../types'; +import { enableAllFeatures } from '../generators/utils'; const templateFormatReg = /^\.template_format\.ts$/; const templateStyleCssReg = /^\.template_style\.css$/; @@ -57,14 +58,13 @@ const plugin: VueLanguagePlugin = (ctx) => { const [content, contentStacks] = ctx.codegenStack ? track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; content.forEach(code => { if (typeof code !== 'string') { - code[3].foldingRanges = false; - code[3].formattingEdits = false; - code[3].symbols = false; + code[3].structure = false; + code[3].format = false; } }); embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; - embeddedFile.linkedCodeMappings = [...tsx.mirrorBehaviorMappings]; + embeddedFile.linkedNavigationMappings = [...tsx.mirrorBehaviorMappings]; } } else if (suffix.match(templateFormatReg)) { @@ -88,7 +88,7 @@ const plugin: VueLanguagePlugin = (ctx) => { cssVar.text, style.name, cssVar.offset, - {}, + enableAllFeatures({}), ]); embeddedFile.content.push(');\n'); } diff --git a/packages/language-core/src/virtualFile/computedFiles.ts b/packages/language-core/src/virtualFile/computedFiles.ts index e66c092b35..1692e7a87e 100644 --- a/packages/language-core/src/virtualFile/computedFiles.ts +++ b/packages/language-core/src/virtualFile/computedFiles.ts @@ -51,7 +51,7 @@ export function computedFiles( id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), typescript: file.typescript, - linkedCodeMappings: file.linkedCodeMappings, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -70,7 +70,7 @@ export function computedFiles( id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), typescript: file.typescript, - linkedCodeMappings: file.linkedCodeMappings, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -85,7 +85,7 @@ export function computedFiles( id: file.fileName, languageId: resolveCommonLanguageId(file.fileName), typescript: file.typescript, - linkedCodeMappings: file.linkedCodeMappings, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, diff --git a/packages/language-core/src/virtualFile/computedMappings.ts b/packages/language-core/src/virtualFile/computedMappings.ts index 8cfed8198d..a4661aec48 100644 --- a/packages/language-core/src/virtualFile/computedMappings.ts +++ b/packages/language-core/src/virtualFile/computedMappings.ts @@ -1,14 +1,15 @@ import { Mapping, Segment, replaceSourceRange } from '@volar/language-core'; +import { computed } from 'computeds'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import { enableAllFeatures } from '../generators/utils'; import { Sfc, VueCodeInformation } from '../types'; -import { computed } from 'computeds'; export function computedMappings( snapshot: () => ts.IScriptSnapshot, sfc: Sfc ) { return computed(() => { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, {}]]; + const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, enableAllFeatures({})]]; for (const block of [ sfc.script, sfc.scriptSetup, diff --git a/packages/language-core/src/virtualFile/embeddedFile.ts b/packages/language-core/src/virtualFile/embeddedFile.ts index 13058b61f9..f5e9fa7b27 100644 --- a/packages/language-core/src/virtualFile/embeddedFile.ts +++ b/packages/language-core/src/virtualFile/embeddedFile.ts @@ -1,11 +1,11 @@ -import { LinkedCodeTrigger, Mapping, StackNode, VirtualFile } from '@volar/language-core'; +import { Mapping, StackNode, VirtualFile } from '@volar/language-core'; import { Code } from '../types'; export class VueEmbeddedFile { public parentFileName?: string; public typescript: VirtualFile['typescript']; - public linkedCodeMappings: Mapping<[LinkedCodeTrigger, LinkedCodeTrigger]>[] = []; + public linkedNavigationMappings: Mapping[] = []; constructor( public fileName: string, diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index c8a6f21565..42b4877775 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -99,7 +99,7 @@ export function resolveServices( if (sourceVirtualFile instanceof VueFile) { - const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completionItems === 'object' && !!data.completionItems.onlyImport); + const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); if (isAutoImport) { for (const item of result.items) { diff --git a/packages/language-service/src/plugins/vue-extract-file.ts b/packages/language-service/src/plugins/vue-extract-file.ts index 457a9c82b1..e68ad22bd1 100644 --- a/packages/language-service/src/plugins/vue-extract-file.ts +++ b/packages/language-service/src/plugins/vue-extract-file.ts @@ -1,6 +1,6 @@ import { CreateFile, Service, ServiceContext, TextDocumentEdit, TextEdit } from '@volar/language-service'; import { ExpressionNode, type TemplateChildNode } from '@vue/compiler-dom'; -import { Sfc, VueFile, scriptRanges } from '@vue/language-core'; +import { Sfc, VueFile, isSemanticTokensEnabled, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Provide } from 'volar-service-typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -192,7 +192,12 @@ export const create = function (): Service { const { name } = node; for (const map of maps) { const source = map.map.getSourceOffset(name.getEnd()); - if (source && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] && source[1].data.semanticTokens) { + if ( + source + && source[0] >= sfc.template!.startTagEnd + templateCodeRange![0] + && source[0] <= sfc.template!.startTagEnd + templateCodeRange![1] + && isSemanticTokensEnabled(source[1].data) + ) { if (!result.has(name.text)) { const type = checker.getTypeAtLocation(node); const typeString = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation); diff --git a/packages/language-service/src/plugins/vue-twoslash-queries.ts b/packages/language-service/src/plugins/vue-twoslash-queries.ts index 899262b2a0..56ed01185f 100644 --- a/packages/language-service/src/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/src/plugins/vue-twoslash-queries.ts @@ -34,7 +34,7 @@ const plugin: Service = (context: ServiceContext mapping.data.diagnostics ?? true)) { + if (embeddedFile.mappings.some(mapping => isDiagnosticsEnabled(mapping.data))) { all.push(embeddedFile); } embeddedFile.embeddedFiles.forEach(visit); @@ -73,17 +74,17 @@ export = async function ( for (const start of map.getSourceOffsets(msgStart)) { - const reportStart = typeof start[1].data.diagnostics === 'object' - ? typeof start[1].data.diagnostics.shouldReport() - : (start[1].data.diagnostics ?? true); + const reportStart = typeof start[1].data.verification === 'object' + ? start[1].data.verification.shouldReport?.() ?? true + : isDiagnosticsEnabled(start[1].data); if (!reportStart) continue; for (const end of map.getSourceOffsets(msgEnd)) { - const reportEnd = typeof end[1].data.diagnostics === 'object' - ? typeof end[1].data.diagnostics.shouldReport() - : (end[1].data.diagnostics ?? true); + const reportEnd = typeof end[1].data.verification === 'object' + ? end[1].data.verification.shouldReport?.() ?? true + : isDiagnosticsEnabled(end[1].data); if (!reportEnd) continue; From 9c5d245be4d7feb6c7db7553f3f32f56283144e1 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 28 Nov 2023 17:16:21 +0800 Subject: [PATCH 13/19] sync https://github.com/volarjs/volar.js/pull/97 changes [skip ci] --- extensions/vscode/src/common.ts | 12 ++- extensions/vscode/src/features/dragImport.ts | 64 ------------ .../src/languageServerPlugin.ts | 9 +- packages/language-server/src/protocol.ts | 15 --- .../src/ideFeatures/dragImport.ts | 69 ------------- packages/language-service/src/index.ts | 1 - .../language-service/src/languageService.ts | 2 + .../src/plugins/vue-document-drop.ts | 99 +++++++++++++++++++ 8 files changed, 112 insertions(+), 159 deletions(-) delete mode 100644 extensions/vscode/src/features/dragImport.ts delete mode 100644 packages/language-service/src/ideFeatures/dragImport.ts create mode 100644 packages/language-service/src/plugins/vue-document-drop.ts diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 46960764f6..f841ebde95 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -1,5 +1,6 @@ import { activateAutoInsertion, + activateDocumentDropEdit, activateFindFileReferences, activateReloadProjects, activateServerSys, @@ -16,7 +17,6 @@ import { config } from './config'; import * as componentMeta from './features/componentMeta'; import * as doctor from './features/doctor'; import * as nameCasing from './features/nameCasing'; -import * as dragImport from './features/dragImport'; import * as splitEditors from './features/splitEditors'; let semanticClient: lsp.BaseLanguageClient; @@ -94,7 +94,6 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang splitEditors.register(context, syntacticClient); doctor.register(context, semanticClient); componentMeta.register(context, semanticClient); - dragImport.register(context, semanticClient); const supportedLanguages: Record = { vue: true, @@ -104,8 +103,17 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang javascriptreact: true, typescriptreact: true, }; + const selectors: vscode.DocumentFilter[] = [{ language: 'vue' }]; + + if (config.server.petiteVue.supportHtmlFile) { + selectors.push({ language: 'html' }); + } + if (config.server.vitePress.supportMdFile) { + selectors.push({ language: 'markdown' }); + } activateAutoInsertion([syntacticClient, semanticClient], document => supportedLanguages[document.languageId]); + activateDocumentDropEdit(selectors, semanticClient); activateWriteVirtualFiles('volar.action.writeVirtualFiles', semanticClient); activateFindFileReferences('volar.vue.findAllFileReferences', semanticClient); activateTsConfigStatusItem('volar.openTsconfig', semanticClient, diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts deleted file mode 100644 index 46878bd4ed..0000000000 --- a/extensions/vscode/src/features/dragImport.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GetDragAndDragImportEditsRequest, TagNameCasing } from '@vue/language-server'; -import * as vscode from 'vscode'; -import type { BaseLanguageClient, DocumentFilter, InsertTextFormat } from 'vscode-languageclient'; -import { tagNameCasings } from './nameCasing'; -import { config } from '../config'; - -export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { - - const selectors: DocumentFilter[] = [{ language: 'vue' }]; - - if (config.server.petiteVue.supportHtmlFile) { - selectors.push({ language: 'html' }); - } - if (config.server.vitePress.supportMdFile) { - selectors.push({ language: 'markdown' }); - } - - context.subscriptions.push( - vscode.languages.registerDocumentDropEditProvider( - selectors, - { - async provideDocumentDropEdits(document, _position, dataTransfer) { - for (const [mimeType, item] of dataTransfer) { - if (mimeType === 'text/uri-list') { - const uri = item.value as string; - if ( - uri.endsWith('.vue') - || (uri.endsWith('.md') && config.server.vitePress.supportMdFile) - ) { - const response = await client.sendRequest(GetDragAndDragImportEditsRequest.type, { - uri: document.uri.toString(), - importUri: uri, - casing: tagNameCasings.get(document.uri.toString()) ?? TagNameCasing.Pascal, - }); - if (!response) { - return; - } - const additionalEdit = new vscode.WorkspaceEdit(); - for (const edit of response.additionalEdits) { - additionalEdit.replace( - document.uri, - new vscode.Range( - edit.range.start.line, - edit.range.start.character, - edit.range.end.line, - edit.range.end.character, - ), - edit.newText - ); - } - return { - insertText: response.insertTextFormat === 2 satisfies typeof InsertTextFormat.Snippet - ? new vscode.SnippetString(response.insertText) - : response.insertText, - additionalEdit, - }; - } - } - } - }, - } - ), - ); -} diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 50e1118444..c13d1a429d 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -2,7 +2,7 @@ import { TypeScriptServerPlugin, Connection, ServerProject } from '@volar/langua import * as vue from '@vue/language-service'; import * as vue2 from '@vue/language-core'; import * as nameCasing from '@vue/language-service'; -import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta, GetDragAndDragImportEditsRequest } from './protocol'; +import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta } from './protocol'; import { VueServerInitializationOptions } from './types'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as componentMeta from 'vue-component-meta/out/base'; @@ -95,13 +95,6 @@ export function createServerPlugin(connection: Connection) { } }); - connection.onRequest(GetDragAndDragImportEditsRequest.type, async params => { - const languageService = await getService(params.uri); - if (languageService) { - return nameCasing.getDragImportEdits(ts, languageService.context, params.uri, params.importUri, params.casing); - } - }); - connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index e7638c67f8..a36d7c0752 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -31,21 +31,6 @@ export namespace GetConvertTagCasingEditsRequest { export const type = new vscode.RequestType('vue/convertTagNameCasing'); } -export namespace GetDragAndDragImportEditsRequest { - export type ParamsType = { - uri: string, - importUri: string, - casing: TagNameCasing, - }; - export type ResponseType = { - insertText: string; - insertTextFormat: vscode.InsertTextFormat; - additionalEdits: vscode.TextEdit[]; - } | null | undefined; - export type ErrorType = never; - export const type = new vscode.RequestType('vue/dragImportEdits'); -} - export namespace GetConvertAttrCasingEditsRequest { export type ParamsType = { textDocument: vscode.TextDocumentIdentifier, diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts deleted file mode 100644 index 193178e5ae..0000000000 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ServiceContext } from '@volar/language-service'; -import { VueFile } from '@vue/language-core'; -import { camelize, capitalize, hyphenate } from '@vue/shared'; -import * as path from 'path-browserify'; -import type * as vscode from 'vscode-languageserver-protocol'; -import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file'; -import { TagNameCasing } from '../types'; - -export function getDragImportEdits( - ts: typeof import('typescript/lib/tsserverlibrary'), - ctx: ServiceContext, - uri: string, - importUri: string, - casing: TagNameCasing -): { - insertText: string; - insertTextFormat: vscode.InsertTextFormat; - additionalEdits: vscode.TextEdit[]; -} | undefined { - - const sourceFile = ctx.project.fileProvider.getSourceFile(uri); - if (!sourceFile) - return; - - let baseName = importUri.substring(importUri.lastIndexOf('/') + 1); - baseName = baseName.substring(0, baseName.lastIndexOf('.')); - - const newName = capitalize(camelize(baseName)); - const document = ctx.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); - const [vueFile] = ctx.project.fileProvider.getVirtualFile(document.uri) as [VueFile, any]; - const { sfc } = vueFile; - const script = sfc.scriptSetup ?? sfc.script; - - if (!sfc.template || !script) - return; - - const lastImportNode = getLastImportNode(ts, script.ast); - const edits: vscode.TextEdit[] = [ - { - range: lastImportNode ? { - start: document.positionAt(script.startTagEnd + lastImportNode.end), - end: document.positionAt(script.startTagEnd + lastImportNode.end), - } : { - start: document.positionAt(script.startTagEnd), - end: document.positionAt(script.startTagEnd), - }, - newText: `\nimport ${newName} from './${path.relative(path.dirname(uri), importUri) || importUri.substring(importUri.lastIndexOf('/') + 1)}'`, - }, - ]; - - if (sfc.script) { - const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName); - if (edit) { - edits.push({ - range: { - start: document.positionAt(sfc.script.startTagEnd + edit.range.start), - end: document.positionAt(sfc.script.startTagEnd + edit.range.end), - }, - newText: edit.newText, - }); - } - } - - return { - insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, - insertTextFormat: 2 satisfies typeof vscode.InsertTextFormat.Snippet, - additionalEdits: edits, - }; -} diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index 4626d9053b..75191f5bb7 100644 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -1,7 +1,6 @@ export * from '@volar/language-service'; export * from '@vue/language-core'; export * from './ideFeatures/nameCasing'; -export * from './ideFeatures/dragImport'; export * from './languageService'; export { TagNameCasing, AttrNameCasing } from './types'; export { Provide } from './plugins/vue'; diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 42b4877775..4f9fd69e02 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -20,6 +20,7 @@ import * as TsTqService from 'volar-service-typescript-twoslash-queries'; // our services import * as VueService from './plugins/vue'; +import * as DocumentDropService from './plugins/vue-document-drop'; import * as AutoDotValueService from './plugins/vue-autoinsert-dotvalue'; import * as AutoWrapParenthesesService from './plugins/vue-autoinsert-parentheses'; import * as AutoAddSpaceService from './plugins/vue-autoinsert-space'; @@ -271,6 +272,7 @@ export function resolveServices( services.json ??= JsonService.create(); services['typescript/twoslash-queries'] ??= TsTqService.create(); services['vue/referencesCodeLens'] ??= ReferencesCodeLensService.create(); + services['vue/documentDrop'] ??= DocumentDropService.create(); services['vue/autoInsertDotValue'] ??= AutoDotValueService.create(); services['vue/twoslash-queries'] ??= VueTqService.create(); services['vue/autoInsertParentheses'] ??= AutoWrapParenthesesService.create(); diff --git a/packages/language-service/src/plugins/vue-document-drop.ts b/packages/language-service/src/plugins/vue-document-drop.ts new file mode 100644 index 0000000000..41f46c6e80 --- /dev/null +++ b/packages/language-service/src/plugins/vue-document-drop.ts @@ -0,0 +1,99 @@ +import { Service } from '@volar/language-service'; +import { VueFile, forEachEmbeddedFile, isFoldingRangesEnabled } from '@vue/language-core'; +import { camelize, capitalize, hyphenate } from '@vue/shared'; +import * as path from 'path-browserify'; +import type * as vscode from 'vscode-languageserver-protocol'; +import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file'; +import { TagNameCasing } from '../types'; + +const plugin: Service = (context, modules) => { + + if (!modules?.typescript) + return {}; + + const ts = modules.typescript; + + let casing: TagNameCasing = TagNameCasing.Pascal; // TODO + + return { + async provideDocumentDropEdits(document, _position, dataTransfer) { + + if (document.languageId !== 'html') + return; + + const [virtualFile, sourceFile] = context!.project.fileProvider.getVirtualFile(document.uri); + const vueFile = sourceFile?.virtualFile?.[0]; + if (!virtualFile || !(vueFile instanceof VueFile)) + return; + + let importUri: string | undefined; + for (const [mimeType, item] of dataTransfer) { + if (mimeType === 'text/uri-list') { + importUri = item.value as string; + } + } + if (!importUri?.endsWith('.vue')) + return; + + let baseName = importUri.substring(importUri.lastIndexOf('/') + 1); + baseName = baseName.substring(0, baseName.lastIndexOf('.')); + const newName = capitalize(camelize(baseName)); + + let additionalEdit: vscode.WorkspaceEdit | undefined; + + for (const file of forEachEmbeddedFile(vueFile)) { + if ( + ( + file.languageId === 'typescript' + || file.languageId === 'javascript' + || file.languageId === 'typescriptreact' + || file.languageId === 'javascriptreact' + ) + && file.mappings.some(mapping => isFoldingRangesEnabled(mapping.data)) + ) { + additionalEdit ??= {}; + additionalEdit.changes ??= {}; + additionalEdit.changes[file.id] = []; + + const { sfc } = vueFile; + const script = sfc.scriptSetup ?? sfc.script; + if (!sfc.template || !script) + return; + + const lastImportNode = getLastImportNode(ts, script.ast); + additionalEdit.changes[file.id].push({ + range: lastImportNode ? { + start: document.positionAt(lastImportNode.end), + end: document.positionAt(lastImportNode.end), + } : { + start: document.positionAt(0), + end: document.positionAt(0), + }, + newText: `\nimport ${newName} from './${path.relative(path.dirname(document.uri), importUri) || importUri.substring(importUri.lastIndexOf('/') + 1)}'` + + (lastImportNode ? '' : '\n'), + }); + if (sfc.script) { + const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName); + if (edit) { + additionalEdit.changes[file.id].push({ + range: { + start: document.positionAt(edit.range.start), + end: document.positionAt(edit.range.end), + }, + newText: edit.newText, + }); + } + } + } + } + + return { + insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, + insertTextFormat: 2 satisfies typeof vscode.InsertTextFormat.Snippet, + additionalEdit, + }; + }, + }; +}; + +export const create = () => plugin; From 4a10d31f59001c53a6903258910c2a0e912ec254 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 29 Nov 2023 18:05:15 +0800 Subject: [PATCH 14/19] fix merge [skip ci] --- packages/language-core/src/generators/template.ts | 4 ++-- packages/language-service/tests/inlayHint.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index c4af778b51..16aa0b1337 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -2002,8 +2002,8 @@ export function generate( if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(code)) { yield '.'; yield offset !== undefined && info - ? code - : [code, 'template', offset, info]; + ? [code, 'template', offset, info] + : code; } else if (code.startsWith('[') && code.endsWith(']')) { yield* createInterpolationCode( diff --git a/packages/language-service/tests/inlayHint.ts b/packages/language-service/tests/inlayHint.ts index 1993f2596c..dc1f5e602f 100644 --- a/packages/language-service/tests/inlayHint.ts +++ b/packages/language-service/tests/inlayHint.ts @@ -18,7 +18,7 @@ for (const dirName of testDirs) { for (const file in inputFiles) { const filePath = path.join(dir, file); - const uri = tester.fileNameToUri(filePath); + const uri = tester.serviceEnv.fileNameToUri(filePath); const fileText = inputFiles[file]; const document = TextDocument.create('', '', 0, fileText); const actions = findActions(fileText); From b0034021d3a9438ea7af38022422dcdfcdf3e01b Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 6 Dec 2023 03:45:02 +0800 Subject: [PATCH 15/19] updates --- packages/component-meta/src/base.ts | 46 +- packages/component-meta/src/index.ts | 4 +- .../language-core/src/generators/template.ts | 97 +- packages/language-core/src/languageModule.ts | 6 +- .../src/languageServerPlugin.ts | 36 +- .../src/ideFeatures/nameCasing.ts | 14 +- .../language-service/src/languageService.ts | 418 +++---- .../src/plugins/vue-autoinsert-dotvalue.ts | 120 +- .../src/plugins/vue-autoinsert-parentheses.ts | 128 +- .../src/plugins/vue-autoinsert-space.ts | 65 +- .../src/plugins/vue-codelens-references.ts | 128 +- .../src/plugins/vue-directive-comments.ts | 89 +- .../src/plugins/vue-document-drop.ts | 144 ++- .../src/plugins/vue-extract-file.ts | 426 ++++--- .../src/plugins/vue-template.ts | 1101 +++++++++-------- .../plugins/vue-toggle-v-bind-codeaction.ts | 238 ++-- .../src/plugins/vue-twoslash-queries.ts | 100 +- .../vue-visualize-hidden-callback-param.ts | 99 +- packages/language-service/src/plugins/vue.ts | 351 +++--- .../tests/utils/createTester.ts | 16 +- .../language-service/tests/utils/format.ts | 4 +- packages/tsc-eslint-hook/src/index.ts | 4 +- packages/tsc/src/index.ts | 30 +- packages/typescript-plugin/src/index.ts | 26 +- 24 files changed, 1841 insertions(+), 1849 deletions(-) diff --git a/packages/component-meta/src/base.ts b/packages/component-meta/src/base.ts index fd5e3614ff..1f1dd25c33 100644 --- a/packages/component-meta/src/base.ts +++ b/packages/component-meta/src/base.ts @@ -3,7 +3,7 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import * as path from 'path-browserify'; import { code as typeHelpersCode } from 'vue-component-type-helpers'; import { code as vue2TypeHelpersCode } from 'vue-component-type-helpers/vue2'; -import { createProject, decorateLanguageService, ProjectHost } from '@volar/typescript'; +import { createLanguage, decorateLanguageService } from '@volar/typescript'; import type { MetaCheckerOptions, @@ -71,7 +71,7 @@ function createCheckerWorker( let projectVersion = 0; const scriptSnapshots = new Map(); - const _host: ProjectHost = { + const _host: vue.TypeScriptProjectHost = { getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => parsedCommandLine.options, @@ -118,16 +118,16 @@ function createCheckerWorker( export function baseCreate( ts: typeof import('typescript/lib/tsserverlibrary'), configFileName: string | undefined, - projectHost: ProjectHost, + host: vue.TypeScriptProjectHost, vueCompilerOptions: vue.VueCompilerOptions, checkerOptions: MetaCheckerOptions, globalComponentName: string, ) { const globalComponentSnapshot = ts.ScriptSnapshot.fromString(''); const metaSnapshots: Record = {}; - const getScriptFileNames = projectHost.getScriptFileNames; - const getScriptSnapshot = projectHost.getScriptSnapshot; - projectHost.getScriptFileNames = () => { + const getScriptFileNames = host.getScriptFileNames; + const getScriptSnapshot = host.getScriptSnapshot; + host.getScriptFileNames = () => { const names = getScriptFileNames(); return [ ...names, @@ -136,7 +136,7 @@ export function baseCreate( getMetaFileName(globalComponentName), ]; }; - projectHost.getScriptSnapshot = (fileName) => { + host.getScriptSnapshot = (fileName) => { if (isMetaFileName(fileName)) { if (!metaSnapshots[fileName]) { metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); @@ -151,22 +151,22 @@ export function baseCreate( } }; - const vueLanguages = vue.createLanguages( + const vueLanguagePlugins = vue.createLanguages( ts, - projectHost.getCompilationSettings(), + host.getCompilationSettings(), vueCompilerOptions, ); - const project = createProject( + const language = createLanguage( ts, ts.sys, - vueLanguages, + vueLanguagePlugins, configFileName, - projectHost, + host, ); - const { languageServiceHost } = project.typescript!; + const { languageServiceHost } = language.typescript!; const tsLs = ts.createLanguageService(languageServiceHost); - decorateLanguageService(project.fileProvider, tsLs, false); + decorateLanguageService(language.files, tsLs, false); if (checkerOptions.forceUseTs) { const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost); @@ -287,7 +287,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} .map((prop) => { const { resolveNestedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveNestedProperties(prop); }) @@ -304,9 +304,9 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} // fill defaults const printer = ts.createPrinter(checkerOptions.printer); - const snapshot = projectHost.getScriptSnapshot(componentPath)!; + const snapshot = host.getScriptSnapshot(componentPath)!; - const vueSourceFile = project.fileProvider.getSourceFile(componentPath)?.virtualFile?.[0]; + const vueSourceFile = language.files.getSourceFile(componentPath)?.virtualFile?.[0]; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; @@ -351,7 +351,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const { resolveEventSignature, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveEventSignature(call); }).filter(event => event.name); @@ -371,7 +371,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveSlotProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveSlotProperties(prop); }); @@ -394,7 +394,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveExposedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, project); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveExposedProperties(prop); }); @@ -453,7 +453,7 @@ function createSchemaResolvers( symbolNode: ts.Expression, { rawType, schema: options, noDeclarations }: MetaCheckerOptions, ts: typeof import('typescript/lib/tsserverlibrary'), - core: vue.Project, + core: vue.Language, ) { const visited = new Set(); @@ -644,9 +644,9 @@ function createSchemaResolvers( } function getDeclaration(declaration: ts.Declaration): Declaration | undefined { const fileName = declaration.getSourceFile().fileName; - const [virtualFile] = core.fileProvider.getVirtualFile(fileName); + const [virtualFile] = core.files.getVirtualFile(fileName); if (virtualFile) { - const maps = core.fileProvider.getMaps(virtualFile); + const maps = core.files.getMaps(virtualFile); for (const [source, [_, map]] of maps) { const start = map.getSourceOffset(declaration.getStart()); const end = map.getSourceOffset(declaration.getEnd()); diff --git a/packages/component-meta/src/index.ts b/packages/component-meta/src/index.ts index 77922556bd..c3820ebeb5 100644 --- a/packages/component-meta/src/index.ts +++ b/packages/component-meta/src/index.ts @@ -10,7 +10,7 @@ export function createComponentMetaCheckerByJsonConfig( checkerOptions: MetaCheckerOptions = {}, ) { return createCheckerByJsonConfigBase( - ts as any, + ts, rootPath, json, checkerOptions, @@ -22,7 +22,7 @@ export function createComponentMetaChecker( checkerOptions: MetaCheckerOptions = {}, ) { return createCheckerBase( - ts as any, + ts, tsconfig, checkerOptions, ); diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index 16aa0b1337..3b91e05f4a 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -25,15 +25,15 @@ const presetInfos = { }), noDiagnostics: enableAllFeatures({ verification: false }), diagnosticOnly: disableAllFeatures({ verification: true }), - tagHover: disableAllFeatures({ semantic: true }), - event: disableAllFeatures({ semantic: true, verification: true }), + tagHover: disableAllFeatures({ semantic: { shouldHighlight: () => false } }), + event: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true }), tagReference: disableAllFeatures({ navigation: { shouldRename: () => false } }), - attr: disableAllFeatures({ semantic: true, verification: true, navigation: true }), + attr: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true }), attrReference: disableAllFeatures({ navigation: true }), slotProp: disableAllFeatures({ navigation: true, verification: true }), scopedClassName: disableAllFeatures({ navigation: true, completion: true }), - slotName: disableAllFeatures({ semantic: true, verification: true, navigation: true, completion: true }), - slotNameExport: disableAllFeatures({ semantic: true, verification: true, navigation: true, /* __navigationCodeLens: true */ }), + slotName: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, completion: true }), + slotNameExport: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, /* __navigationCodeLens: true */ }), refAttr: disableAllFeatures({ navigation: true }), }; const formatBrackets = { @@ -90,7 +90,11 @@ export function generate( const slotExps = new Map(); const tagNames = collectTagOffsets(); const localVars = new Map(); - const tempVars: ReturnType[] = []; + const tempVars: { + text: string, + isShorthand: boolean, + offset: number, + }[][] = []; const accessedGlobalVariables = new Set(); const scopedClasses: { className: string, offset: number; }[] = []; const blockConditions: string[] = []; @@ -1336,10 +1340,10 @@ export function generate( ? mergeFeatureSettings( caps_attr, { - navigation: { + navigation: caps_attr.navigation ? { resolveRenameNewName: camelize, resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, - }, + } : undefined, }, ) : caps_attr, @@ -1401,10 +1405,10 @@ export function generate( prop.loc.start.offset, shouldCamelize ? mergeFeatureSettings(caps_attr, { - navigation: { + navigation: caps_attr.navigation ? { resolveRenameNewName: camelize, resolveRenameEditText: hyphenateAttr, - }, + } : undefined, }) : caps_attr, (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {}), @@ -1944,39 +1948,52 @@ export function generate( const code = prefix + _code + suffix; const ast = createTsAst(astHolder, code); const codes: Code[] = []; - const vars = walkInterpolationFragment(ts, code, ast, (frag, fragOffset, isJustForErrorMapping) => { - if (fragOffset === undefined) { - codes.push(frag); - } - else { - fragOffset -= prefix.length; - let addSuffix = ''; - const overLength = fragOffset + frag.length - _code.length; - if (overLength > 0) { - addSuffix = frag.substring(frag.length - overLength); - frag = frag.substring(0, frag.length - overLength); - } - if (fragOffset < 0) { - codes.push(frag.substring(0, -fragOffset)); - frag = frag.substring(-fragOffset); - fragOffset = 0; - } - if (start !== undefined && data !== undefined) { - codes.push([ - frag, - 'template', - start + fragOffset, - isJustForErrorMapping - ? presetInfos.diagnosticOnly - : typeof data === 'function' ? data() : data, - ]); + const vars: { + text: string, + isShorthand: boolean, + offset: number, + }[] = []; + walkInterpolationFragment( + ts, + code, + ast, + (section, offset, onlyError) => { + if (offset === undefined) { + codes.push(section); } else { - codes.push(frag); + offset -= prefix.length; + let addSuffix = ''; + const overLength = offset + section.length - _code.length; + if (overLength > 0) { + addSuffix = section.substring(section.length - overLength); + section = section.substring(0, section.length - overLength); + } + if (offset < 0) { + codes.push(section.substring(0, -offset)); + section = section.substring(-offset); + offset = 0; + } + if (start !== undefined && data !== undefined) { + codes.push([ + section, + 'template', + start + offset, + onlyError + ? presetInfos.diagnosticOnly + : typeof data === 'function' ? data() : data, + ]); + } + else { + codes.push(section); + } + codes.push(addSuffix); } - codes.push(addSuffix); - } - }, localVars, accessedGlobalVariables, vueCompilerOptions); + }, + localVars, + accessedGlobalVariables, + vueCompilerOptions, + ); if (start !== undefined) { for (const v of vars) { v.offset = start + v.offset - prefix.length; diff --git a/packages/language-core/src/languageModule.ts b/packages/language-core/src/languageModule.ts index 5e34a58962..79aec83a9b 100644 --- a/packages/language-core/src/languageModule.ts +++ b/packages/language-core/src/languageModule.ts @@ -1,4 +1,4 @@ -import type { Language } from '@volar/language-core'; +import type { LanguagePlugin } from '@volar/language-core'; import * as path from 'path-browserify'; import { getDefaultVueLanguagePlugins } from './plugins'; import { VueFile } from './virtualFile/vueFile'; @@ -38,7 +38,7 @@ export function createVueLanguage( compilerOptions: ts.CompilerOptions = {}, _vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -): Language { +): LanguagePlugin { const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); const plugins = getDefaultVueLanguagePlugins( @@ -126,7 +126,7 @@ export function createLanguages( compilerOptions: ts.CompilerOptions = {}, vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -): Language[] { +): LanguagePlugin[] { return [ createVueLanguage(ts, compilerOptions, vueCompilerOptions, codegenStack), ...vueCompilerOptions.experimentalAdditionalLanguageModules?.map(module => require(module)) ?? [], diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index d1fa8f3f18..6aa8e8dd71 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -1,13 +1,13 @@ -import { TypeScriptServerPlugin, Connection, ServerProject } from '@volar/language-server'; -import * as vue from '@vue/language-service'; +import { Connection, ServerProject, TypeScriptServerPlugin } from '@volar/language-server'; +import { createSys } from '@volar/typescript'; import * as vue2 from '@vue/language-core'; +import { VueCompilerOptions } from '@vue/language-core'; import * as nameCasing from '@vue/language-service'; -import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta } from './protocol'; -import { VueServerInitializationOptions } from './types'; +import * as vue from '@vue/language-service'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as componentMeta from 'vue-component-meta/out/base'; -import { VueCompilerOptions } from '@vue/language-core'; -import { createSys } from '@volar/typescript'; +import { DetectNameCasingRequest, GetComponentMeta, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './protocol'; +import { VueServerInitializationOptions } from './types'; export function createServerPlugin(connection: Connection) { @@ -41,7 +41,7 @@ export function createServerPlugin(connection: Connection) { } config.languages = vue.resolveLanguages(ts, config.languages ?? {}, info?.parsedCommandLine.options ?? {}, vueOptions, options.codegenStack); - config.services = vue.resolveServices(config.services ?? {}, vueOptions); + config.services = vue.resolveServices(ts, config.services ?? {}, vueOptions); return config; @@ -113,17 +113,17 @@ export function createServerPlugin(connection: Connection) { const langaugeService = project.getLanguageService(); let checker = checkers.get(project); - // if (!checker) { - // checker = componentMeta.baseCreate( - // ts, - // langaugeService.context.project.typescript?.configFileName, - // langaugeService.context.project.typescript!.projectHost, - // envToVueOptions.get(langaugeService.context.env)!, - // {}, - // langaugeService.context.project.typescript!.projectHost.getCurrentDirectory() + '/tsconfig.json.global.vue', - // ); - // checkers.set(project, checker); - // } + if (!checker) { + checker = componentMeta.baseCreate( + ts, + langaugeService.context.language.typescript!.configFileName, + langaugeService.context.language.typescript!.projectHost, + envToVueOptions.get(langaugeService.context.env)!, + {}, + langaugeService.context.language.typescript!.languageServiceHost.getCurrentDirectory() + '/tsconfig.json.global.vue', + ); + checkers.set(project, checker); + } return checker?.getComponentMeta(langaugeService.context.env.uriToFileName(params.uri)); }); diff --git a/packages/language-service/src/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index af5273b425..fe75e59757 100644 --- a/packages/language-service/src/ideFeatures/nameCasing.ts +++ b/packages/language-service/src/ideFeatures/nameCasing.ts @@ -7,13 +7,13 @@ import { AttrNameCasing, TagNameCasing } from '../types'; export async function convertTagName( ts: typeof import('typescript/lib/tsserverlibrary'), - context: ServiceContext, + context: ServiceContext, uri: string, casing: TagNameCasing, vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -21,7 +21,7 @@ export async function convertTagName( if (!desc.template) return; - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); const template = desc.template; const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; @@ -56,7 +56,7 @@ export async function convertAttrName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -64,7 +64,7 @@ export async function convertAttrName( if (!desc.template) return; - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); const template = desc.template; const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; @@ -128,7 +128,7 @@ export function detect( attr: AttrNameCasing[], } { - const rootFile = context.project.fileProvider.getSourceFile(uri)?.virtualFile?.[0]; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) { return { tag: [], @@ -136,7 +136,7 @@ export function detect( }; } - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); return { tag: getTagNameCase(rootFile), diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 4f9fd69e02..89cfb291e4 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -1,5 +1,5 @@ -import { Service, ServiceContext } from '@volar/language-service'; -import { Language, VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; +import { ServicePlugin } from '@volar/language-service'; +import { LanguagePlugin, VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; import { capitalize } from '@vue/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Data } from 'volar-service-typescript/out/features/completions/basic'; @@ -9,40 +9,40 @@ import { getNameCasing } from './ideFeatures/nameCasing'; import { TagNameCasing, VueCompilerOptions } from './types'; // volar services -import * as CssService from 'volar-service-css'; -import * as EmmetService from 'volar-service-emmet'; -import * as HtmlService from 'volar-service-html'; -import * as JsonService from 'volar-service-json'; -import * as PugService from 'volar-service-pug'; -import * as PugFormatService from 'volar-service-pug-beautify'; -import * as TsService from 'volar-service-typescript'; -import * as TsTqService from 'volar-service-typescript-twoslash-queries'; +import { create as createCssService } from 'volar-service-css'; +import { create as createEmmetService } from 'volar-service-emmet'; +import { create as createHtmlService } from 'volar-service-html'; +import { create as createJsonService } from 'volar-service-json'; +import { create as createPugService } from 'volar-service-pug'; +import { create as createPugFormatService } from 'volar-service-pug-beautify'; +import { create as createTsService, Provide as TSProvide } from 'volar-service-typescript'; +import { create as createTsTqService } from 'volar-service-typescript-twoslash-queries'; // our services -import * as VueService from './plugins/vue'; -import * as DocumentDropService from './plugins/vue-document-drop'; -import * as AutoDotValueService from './plugins/vue-autoinsert-dotvalue'; -import * as AutoWrapParenthesesService from './plugins/vue-autoinsert-parentheses'; -import * as AutoAddSpaceService from './plugins/vue-autoinsert-space'; -import * as ReferencesCodeLensService from './plugins/vue-codelens-references'; -import * as DirectiveCommentsService from './plugins/vue-directive-comments'; -import * as ExtractComponentService from './plugins/vue-extract-file'; -import * as VueTemplateLanguageService from './plugins/vue-template'; -import * as ToggleVBindService from './plugins/vue-toggle-v-bind-codeaction'; -import * as VueTqService from './plugins/vue-twoslash-queries'; -import * as VisualizeHiddenCallbackParamService from './plugins/vue-visualize-hidden-callback-param'; +import { create as createVueService } from './plugins/vue'; +import { create as createDocumentDropService } from './plugins/vue-document-drop'; +import { create as createAutoDotValueService } from './plugins/vue-autoinsert-dotvalue'; +import { create as createAutoWrapParenthesesService } from './plugins/vue-autoinsert-parentheses'; +import { create as createAutoAddSpaceService } from './plugins/vue-autoinsert-space'; +import { create as createReferencesCodeLensService } from './plugins/vue-codelens-references'; +import { create as createDirectiveCommentsService } from './plugins/vue-directive-comments'; +import { create as createVueExtractFileService, createAddComponentToOptionEdit } from './plugins/vue-extract-file'; +import { create as createVueTemplateLanguageService } from './plugins/vue-template'; +import { create as createToggleVBindService } from './plugins/vue-toggle-v-bind-codeaction'; +import { create as createVueTqService } from './plugins/vue-twoslash-queries'; +import { create as createVisualizeHiddenCallbackParamService } from './plugins/vue-visualize-hidden-callback-param'; export interface Settings { - json?: Parameters[0]; + json?: Parameters[0]; } export function resolveLanguages( ts: typeof import('typescript/lib/tsserverlibrary'), - languages: Record = {}, + languages: Record = {}, compilerOptions: ts.CompilerOptions = {}, _vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -): Record { +): Record { const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); const vueLanguageModules = createLanguages(ts, compilerOptions, vueCompilerOptions, codegenStack); @@ -52,236 +52,238 @@ export function resolveLanguages( ...vueLanguageModules.reduce((obj, module, i) => { obj['vue_' + i] = module; return obj; - }, {} as Record), + }, {} as Record), }; } export function resolveServices( - services: Record = {}, + ts: typeof import('typescript/lib/tsserverlibrary'), + services: Record = {}, _vueCompilerOptions: Partial = {}, ) { const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); - - const _tsPlugin: Service = services?.typescript ?? TsService.create(); + const tsService: ServicePlugin = services?.typescript ?? createTsService(ts); services ??= {}; - services.typescript = (ctx: ServiceContext | undefined, modules): ReturnType => { - - const base = typeof _tsPlugin === 'function' ? _tsPlugin(ctx, modules) : _tsPlugin; - - if (!ctx || !modules?.typescript) - return base; - - const ts = modules.typescript; - - return { - ...base, - async provideCompletionItems(document, position, context, item) { - const result = await base.provideCompletionItems?.(document, position, context, item); - if (result) { - - // filter __VLS_ - result.items = result.items.filter(item => - item.label.indexOf('__VLS_') === -1 - && (!item.labelDetails?.description || item.labelDetails.description.indexOf('__VLS_') === -1) - ); + services.typescript = { + ...tsService, + create(ctx) { + const base = tsService.create(ctx); + return { + ...base, + async provideCompletionItems(document, position, context, item) { + const result = await base.provideCompletionItems?.(document, position, context, item); + if (result) { + + // filter __VLS_ + result.items = result.items.filter(item => + item.label.indexOf('__VLS_') === -1 + && (!item.labelDetails?.description || item.labelDetails.description.indexOf('__VLS_') === -1) + ); - // handle component auto-import patch - let casing: Awaited> | undefined; + // handle component auto-import patch + let casing: Awaited> | undefined; - const [virtualFile, sourceFile] = ctx.project.fileProvider.getVirtualFile(document.uri); + const [virtualFile, sourceFile] = ctx.language.files.getVirtualFile(document.uri); - if (virtualFile && sourceFile) { + if (virtualFile && sourceFile) { - for (const map of ctx.documents.getMaps(virtualFile)) { + for (const map of ctx.documents.getMaps(virtualFile)) { - const sourceVirtualFile = ctx.project.fileProvider.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; + const sourceVirtualFile = ctx.language.files.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; - if (sourceVirtualFile instanceof VueFile) { + if (sourceVirtualFile instanceof VueFile) { - const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); - if (isAutoImport) { + const isAutoImport = !!map.getSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); + if (isAutoImport) { - for (const item of result.items) { - item.data.__isComponentAutoImport = true; - } + for (const item of result.items) { + item.data.__isComponentAutoImport = true; + } - // fix #2458 - casing ??= await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); + // fix #2458 + casing ??= await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); - if (casing.tag === TagNameCasing.Kebab) { - for (const item of result.items) { - item.filterText = hyphenateTag(item.filterText ?? item.label); + if (casing.tag === TagNameCasing.Kebab) { + for (const item of result.items) { + item.filterText = hyphenateTag(item.filterText ?? item.label); + } } } } } } } - } - return result; - }, - async resolveCompletionItem(item, token) { + return result; + }, + async resolveCompletionItem(item, token) { - item = await base.resolveCompletionItem?.(item, token) ?? item; + item = await base.resolveCompletionItem?.(item, token) ?? item; - const itemData = item.data as { uri?: string; } | undefined; + const itemData = item.data as { uri?: string; } | undefined; - let newName: string | undefined; + let newName: string | undefined; - if (itemData?.uri && item.additionalTextEdits) { - patchAdditionalTextEdits(itemData.uri, item.additionalTextEdits); - } + if (itemData?.uri && item.additionalTextEdits) { + patchAdditionalTextEdits(itemData.uri, item.additionalTextEdits); + } - for (const ext of vueCompilerOptions.extensions) { - const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue - if ( - itemData?.uri - && item.textEdit?.newText.endsWith(suffix) - && item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0 - && (await ctx.env.getConfiguration?.('vue.complete.normalizeComponentImportName') ?? true) - ) { - newName = item.textEdit.newText.slice(0, -suffix.length); - newName = newName[0].toUpperCase() + newName.substring(1); - if (newName === 'Index') { - const tsItem = (item.data as Data).originalItem; - if (tsItem.source) { - const dirs = tsItem.source.split('/'); - if (dirs.length >= 3) { - newName = dirs[dirs.length - 2]; - newName = newName[0].toUpperCase() + newName.substring(1); + for (const ext of vueCompilerOptions.extensions) { + const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue + if ( + itemData?.uri + && item.textEdit?.newText.endsWith(suffix) + && item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0 + && (await ctx.env.getConfiguration?.('vue.complete.normalizeComponentImportName') ?? true) + ) { + newName = item.textEdit.newText.slice(0, -suffix.length); + newName = newName[0].toUpperCase() + newName.substring(1); + if (newName === 'Index') { + const tsItem = (item.data as Data).originalItem; + if (tsItem.source) { + const dirs = tsItem.source.split('/'); + if (dirs.length >= 3) { + newName = dirs[dirs.length - 2]; + newName = newName[0].toUpperCase() + newName.substring(1); + } } } - } - item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace( - 'import ' + item.textEdit.newText + ' from ', - 'import ' + newName + ' from ', - ); - item.textEdit.newText = newName; - const [_, sourceFile] = ctx.project.fileProvider.getVirtualFile(itemData.uri); - if (sourceFile) { - const casing = await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); - if (casing.tag === TagNameCasing.Kebab) { - item.textEdit.newText = hyphenateTag(item.textEdit.newText); + item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace( + 'import ' + item.textEdit.newText + ' from ', + 'import ' + newName + ' from ', + ); + item.textEdit.newText = newName; + const [_, sourceFile] = ctx.language.files.getVirtualFile(itemData.uri); + if (sourceFile) { + const casing = await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); + if (casing.tag === TagNameCasing.Kebab) { + item.textEdit.newText = hyphenateTag(item.textEdit.newText); + } } } + else if (item.textEdit?.newText && new RegExp(`import \\w*${suffix}\\$1 from [\\S\\s]*`).test(item.textEdit.newText)) { + // https://github.com/vuejs/language-tools/issues/2286 + item.textEdit.newText = item.textEdit.newText.replace(`${suffix}$1`, '$1'); + } } - else if (item.textEdit?.newText && new RegExp(`import \\w*${suffix}\\$1 from [\\S\\s]*`).test(item.textEdit.newText)) { - // https://github.com/vuejs/language-tools/issues/2286 - item.textEdit.newText = item.textEdit.newText.replace(`${suffix}$1`, '$1'); - } - } - const data: Data = item.data; - if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { - const fileName = ctx.env.uriToFileName(itemData.uri); - const langaugeService = ctx.inject('typescript/languageService'); - const [virtualFile] = ctx.project.fileProvider.getVirtualFile(fileName); - const ast = langaugeService.getProgram()?.getSourceFile(fileName); - const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined; - if (virtualFile && ast && exportDefault) { - const componentName = newName ?? item.textEdit.newText; - const optionEdit = ExtractComponentService.createAddComponentToOptionEdit(ts, ast, componentName); - if (optionEdit) { - const textDoc = ctx.documents.get(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); - item.additionalTextEdits.push({ - range: { - start: textDoc.positionAt(optionEdit.range.start), - end: textDoc.positionAt(optionEdit.range.end), - }, - newText: optionEdit.newText, - }); + const data: Data = item.data; + if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { + const fileName = ctx.env.uriToFileName(itemData.uri); + const langaugeService = ctx.inject('typescript/languageService'); + const [virtualFile] = ctx.language.files.getVirtualFile(fileName); + const ast = langaugeService.getProgram()?.getSourceFile(fileName); + const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined; + if (virtualFile && ast && exportDefault) { + const componentName = newName ?? item.textEdit.newText; + const optionEdit = createAddComponentToOptionEdit(ts, ast, componentName); + if (optionEdit) { + const textDoc = ctx.documents.get(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); + item.additionalTextEdits.push({ + range: { + start: textDoc.positionAt(optionEdit.range.start), + end: textDoc.positionAt(optionEdit.range.end), + }, + newText: optionEdit.newText, + }); + } } } - } - - return item; - }, - async provideCodeActions(document, range, context, token) { - const result = await base.provideCodeActions?.(document, range, context, token); - return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1); - }, - async resolveCodeAction(item, token) { - - const result = await base.resolveCodeAction?.(item, token) ?? item; - if (result?.edit?.changes) { - for (const uri in result.edit.changes) { - const edits = result.edit.changes[uri]; - if (edits) { - patchAdditionalTextEdits(uri, edits); + return item; + }, + async provideCodeActions(document, range, context, token) { + const result = await base.provideCodeActions?.(document, range, context, token); + return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1); + }, + async resolveCodeAction(item, token) { + + const result = await base.resolveCodeAction?.(item, token) ?? item; + + if (result?.edit?.changes) { + for (const uri in result.edit.changes) { + const edits = result.edit.changes[uri]; + if (edits) { + patchAdditionalTextEdits(uri, edits); + } } } - } - if (result?.edit?.documentChanges) { - for (const documentChange of result.edit.documentChanges) { - if ('textDocument' in documentChange) { - patchAdditionalTextEdits(documentChange.textDocument.uri, documentChange.edits); + if (result?.edit?.documentChanges) { + for (const documentChange of result.edit.documentChanges) { + if ('textDocument' in documentChange) { + patchAdditionalTextEdits(documentChange.textDocument.uri, documentChange.edits); + } } } - } - return result; + return result; + }, + async provideSemanticDiagnostics(document, token) { + const result = await base.provideSemanticDiagnostics?.(document, token); + return result?.map(diagnostic => { + if ( + diagnostic.source === 'ts' + && diagnostic.code === 2578 /* Unused '@ts-expect-error' directive. */ + && document.getText(diagnostic.range) === '// @ts-expect-error __VLS_TS_EXPECT_ERROR' + ) { + diagnostic.source = 'vue'; + diagnostic.code = 'ts-2578'; + diagnostic.message = diagnostic.message.replace(/@ts-expect-error/g, '@vue-expect-error'); + } + return diagnostic; + }); + }, + }; + }, + }; + services.html ??= createVueTemplateLanguageService( + ts, + createHtmlService(), + { + getScanner: (htmlService, document): html.Scanner | undefined => { + return htmlService.provide['html/languageService']().createScanner(document.getText()); }, - async provideSemanticDiagnostics(document, token) { - const result = await base.provideSemanticDiagnostics?.(document, token); - return result?.map(diagnostic => { - if ( - diagnostic.source === 'ts' - && diagnostic.code === 2578 /* Unused '@ts-expect-error' directive. */ - && document.getText(diagnostic.range) === '// @ts-expect-error __VLS_TS_EXPECT_ERROR' - ) { - diagnostic.source = 'vue'; - diagnostic.code = 'ts-2578'; - diagnostic.message = diagnostic.message.replace(/@ts-expect-error/g, '@vue-expect-error'); - } - return diagnostic; - }); + updateCustomData(htmlService, extraData) { + htmlService.provide['html/updateCustomData'](extraData); }, - }; - }; - services.html ??= VueTemplateLanguageService.create({ - baseService: HtmlService.create(), - getScanner: (htmlService, document): html.Scanner | undefined => { - return htmlService.provide['html/languageService']().createScanner(document.getText()); - }, - updateCustomData(htmlService, extraData) { - htmlService.provide['html/updateCustomData'](extraData); - }, - isSupportedDocument: (document) => document.languageId === 'html', - vueCompilerOptions, - }); - services.pug ??= VueTemplateLanguageService.create({ - baseService: PugService.create(), - getScanner: (pugService, document): html.Scanner | undefined => { - const pugDocument = pugService.provide['pug/pugDocument'](document); - if (pugDocument) { - return pugService.provide['pug/languageService']().createScanner(pugDocument); - } - }, - updateCustomData(pugService, extraData) { - pugService.provide['pug/updateCustomData'](extraData); - }, - isSupportedDocument: (document) => document.languageId === 'jade', - vueCompilerOptions, - }); - services.vue ??= VueService.create(); - services.css ??= CssService.create(); - services['pug-beautify'] ??= PugFormatService.create(); - services.json ??= JsonService.create(); - services['typescript/twoslash-queries'] ??= TsTqService.create(); - services['vue/referencesCodeLens'] ??= ReferencesCodeLensService.create(); - services['vue/documentDrop'] ??= DocumentDropService.create(); - services['vue/autoInsertDotValue'] ??= AutoDotValueService.create(); - services['vue/twoslash-queries'] ??= VueTqService.create(); - services['vue/autoInsertParentheses'] ??= AutoWrapParenthesesService.create(); - services['vue/autoInsertSpaces'] ??= AutoAddSpaceService.create(); - services['vue/visualizeHiddenCallbackParam'] ??= VisualizeHiddenCallbackParamService.create(); - services['vue/directiveComments'] ??= DirectiveCommentsService.create(); - services['vue/extractComponent'] ??= ExtractComponentService.create(); - services['vue/toggleVBind'] ??= ToggleVBindService.create(); - services.emmet ??= EmmetService.create(); + isSupportedDocument: (document) => document.languageId === 'html', + vueCompilerOptions, + } + ); + services.pug ??= createVueTemplateLanguageService( + ts, + createPugService(), + { + getScanner: (pugService, document): html.Scanner | undefined => { + const pugDocument = pugService.provide['pug/pugDocument'](document); + if (pugDocument) { + return pugService.provide['pug/languageService']().createScanner(pugDocument); + } + }, + updateCustomData(pugService, extraData) { + pugService.provide['pug/updateCustomData'](extraData); + }, + isSupportedDocument: (document) => document.languageId === 'jade', + vueCompilerOptions, + } + ); + services.vue ??= createVueService(); + services.css ??= createCssService(); + services['pug-beautify'] ??= createPugFormatService(); + services.json ??= createJsonService(); + services['typescript/twoslash-queries'] ??= createTsTqService(); + services['vue/referencesCodeLens'] ??= createReferencesCodeLensService(); + services['vue/documentDrop'] ??= createDocumentDropService(ts); + services['vue/autoInsertDotValue'] ??= createAutoDotValueService(ts); + services['vue/twoslash-queries'] ??= createVueTqService(ts); + services['vue/autoInsertParentheses'] ??= createAutoWrapParenthesesService(ts); + services['vue/autoInsertSpaces'] ??= createAutoAddSpaceService(); + services['vue/visualizeHiddenCallbackParam'] ??= createVisualizeHiddenCallbackParamService(); + services['vue/directiveComments'] ??= createDirectiveCommentsService(); + services['vue/extractComponent'] ??= createVueExtractFileService(ts); + services['vue/toggleVBind'] ??= createToggleVBindService(ts); + services.emmet ??= createEmmetService(); return services; } diff --git a/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts index 336863dff1..a682839a31 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts @@ -1,83 +1,79 @@ -import { AutoInsertionContext, Service, ServiceContext } from '@volar/language-service'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { hyphenateAttr } from '@vue/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import { Provide } from 'volar-service-typescript'; import type * as vscode from 'vscode-languageserver-protocol'; import type { TextDocument } from 'vscode-languageserver-textdocument'; -const plugin: Service = (context: ServiceContext | undefined, modules) => { - - if (!modules?.typescript) - return {}; - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, position, lastChange) { - async provideAutoInsertionEdit(document, position, insertContext) { + if (!isTsDocument(document)) + return; - if (!isTsDocument(document)) - return; + if (!isCharacterTyping(document, lastChange)) + return; - if (!isCharacterTyping(document, insertContext)) - return; + const enabled = await context.env.getConfiguration?.('vue.autoInsert.dotValue') ?? true; + if (!enabled) + return; - const enabled = await context!.env.getConfiguration?.('vue.autoInsert.dotValue') ?? true; - if (!enabled) - return; + const program = context.inject('typescript/languageService').getProgram(); + if (!program) + return; - const program = context!.inject('typescript/languageService').getProgram(); - if (!program) - return; + const sourceFile = program.getSourceFile(context.env.uriToFileName(document.uri)); + if (!sourceFile) + return; - const sourceFile = program.getSourceFile(context!.env.uriToFileName(document.uri)); - if (!sourceFile) - return; + if (isBlacklistNode(ts, sourceFile, document.offsetAt(position), false)) + return; - if (isBlacklistNode(ts, sourceFile, document.offsetAt(position), false)) - return; + const node = findPositionIdentifier(sourceFile, sourceFile, document.offsetAt(position)); + if (!node) + return; - const node = findPositionIdentifier(sourceFile, sourceFile, document.offsetAt(position)); - if (!node) - return; + const token = context.inject('typescript/languageServiceHost').getCancellationToken?.(); + if (token) { + context.inject('typescript/languageService').getQuickInfoAtPosition(context.env.uriToFileName(document.uri), node.end); + if (token?.isCancellationRequested()) { + return; // check cancel here because type checker do not use cancel token + } + } - const token = context!.inject('typescript/languageServiceHost').getCancellationToken?.(); - if (token) { - context!.inject('typescript/languageService').getQuickInfoAtPosition(context!.env.uriToFileName(document.uri), node.end); - if (token?.isCancellationRequested()) { - return; // check cancel here because type checker do not use cancel token - } - } + const checker = program.getTypeChecker(); + const type = checker.getTypeAtLocation(node); + const props = type.getProperties(); - const checker = program.getTypeChecker(); - const type = checker.getTypeAtLocation(node); - const props = type.getProperties(); + if (props.some(prop => prop.name === 'value')) { + return '${1:.value}'; + } - if (props.some(prop => prop.name === 'value')) { - return '${1:.value}'; - } + function findPositionIdentifier(sourceFile: ts.SourceFile, node: ts.Node, offset: number) { - function findPositionIdentifier(sourceFile: ts.SourceFile, node: ts.Node, offset: number) { + let result: ts.Node | undefined; - let result: ts.Node | undefined; + node.forEachChild(child => { + if (!result) { + if (child.end === offset && ts.isIdentifier(child)) { + result = child; + } + else if (child.end >= offset && child.getStart(sourceFile) < offset) { + result = findPositionIdentifier(sourceFile, child, offset); + } + } + }); - node.forEachChild(child => { - if (!result) { - if (child.end === offset && ts.isIdentifier(child)) { - result = child; - } - else if (child.end >= offset && child.getStart(sourceFile) < offset) { - result = findPositionIdentifier(sourceFile, child, offset); - } + return result; } - }); - - return result; - } + }, + }; }, }; -}; - -export const create = () => plugin; +} function isTsDocument(document: TextDocument) { return document.languageId === 'javascript' || @@ -88,13 +84,13 @@ function isTsDocument(document: TextDocument) { const charReg = /\w/; -export function isCharacterTyping(document: TextDocument, options: AutoInsertionContext) { +export function isCharacterTyping(document: TextDocument, lastChange: { range: vscode.Range; text: string; }) { - const lastCharacter = options.lastChange.text[options.lastChange.text.length - 1]; - const rangeStart = options.lastChange.range.start; + const lastCharacter = lastChange.text[lastChange.text.length - 1]; + const rangeStart = lastChange.range.start; const position: vscode.Position = { line: rangeStart.line, - character: rangeStart.character + options.lastChange.text.length, + character: rangeStart.character + lastChange.text.length, }; const nextCharacter = document.getText({ start: position, @@ -104,7 +100,7 @@ export function isCharacterTyping(document: TextDocument, options: AutoInsertion if (lastCharacter === undefined) { // delete text return false; } - if (options.lastChange.text.indexOf('\n') >= 0) { // multi-line change + if (lastChange.text.indexOf('\n') >= 0) { // multi-line change return false; } diff --git a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts index 85f504d46e..09363d0cd9 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -1,81 +1,71 @@ -import { Service } from '@volar/language-service'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { isCharacterTyping } from './vue-autoinsert-dotvalue'; -const plugin: Service = (context, modules) => { - - if (!context) { - return {}; - } - - if (!modules?.typescript) { - return {}; - } - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, position, lastChange) { - async provideAutoInsertionEdit(document, position, options_2) { - - const enabled = await context.env.getConfiguration?.('vue.autoInsert.parentheses') ?? false; - if (!enabled) - return; + const enabled = await context.env.getConfiguration?.('vue.autoInsert.parentheses') ?? false; + if (!enabled) + return; - if (!isCharacterTyping(document, options_2)) - return; + if (!isCharacterTyping(document, lastChange)) + return; - const [virtualFile] = context.project.fileProvider.getVirtualFile(document.uri); - if (!virtualFile?.id.endsWith('.template_format.ts')) - return; + const [virtualFile] = context.language.files.getVirtualFile(document.uri); + if (!virtualFile?.id.endsWith('.template_format.ts')) + return; - const offset = document.offsetAt(position); + const offset = document.offsetAt(position); - for (const mappedRange of virtualFile.mappings) { - const generatedCodeEnd = mappedRange.generatedOffsets[mappedRange.generatedOffsets.length - 1] - + mappedRange.lengths[mappedRange.lengths.length - 1]; - if (generatedCodeEnd === offset) { - const text = document.getText().substring(mappedRange.generatedOffsets[0], generatedCodeEnd); - const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); - if (ast.statements.length === 1) { - const statement = ast.statements[0]; - if ( - ts.isExpressionStatement(statement) - && ( - ( - ts.isAsExpression(statement.expression) - && ts.isTypeReferenceNode(statement.expression.type) - && ts.isIdentifier(statement.expression.type.typeName) - && statement.expression.type.typeName.text - ) - || ( - ts.isBinaryExpression(statement.expression) - && statement.expression.right.getText(ast) - && statement.expression.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword - ) - || ( - ts.isTypeOfExpression(statement.expression) - && statement.expression.expression.getText(ast) - ) - ) - ) { - // https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar - const escapedText = text - .replaceAll('\\', '\\\\') - .replaceAll('$', '\\$') - .replaceAll('}', '\\}'); - return { - range: { - start: document.positionAt(mappedRange.generatedOffsets[0]), - end: document.positionAt(generatedCodeEnd), - }, - newText: '(' + escapedText + '$0' + ')', - }; + for (const mappedRange of virtualFile.mappings) { + const generatedCodeEnd = mappedRange.generatedOffsets[mappedRange.generatedOffsets.length - 1] + + mappedRange.lengths[mappedRange.lengths.length - 1]; + if (generatedCodeEnd === offset) { + const text = document.getText().substring(mappedRange.generatedOffsets[0], generatedCodeEnd); + const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); + if (ast.statements.length === 1) { + const statement = ast.statements[0]; + if ( + ts.isExpressionStatement(statement) + && ( + ( + ts.isAsExpression(statement.expression) + && ts.isTypeReferenceNode(statement.expression.type) + && ts.isIdentifier(statement.expression.type.typeName) + && statement.expression.type.typeName.text + ) + || ( + ts.isBinaryExpression(statement.expression) + && statement.expression.right.getText(ast) + && statement.expression.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword + ) + || ( + ts.isTypeOfExpression(statement.expression) + && statement.expression.expression.getText(ast) + ) + ) + ) { + // https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar + const escapedText = text + .replaceAll('\\', '\\\\') + .replaceAll('$', '\\$') + .replaceAll('}', '\\}'); + return { + range: { + start: document.positionAt(mappedRange.generatedOffsets[0]), + end: document.positionAt(generatedCodeEnd), + }, + newText: '(' + escapedText + '$0' + ')', + }; + } + } } } - } - } + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue-autoinsert-space.ts b/packages/language-service/src/plugins/vue-autoinsert-space.ts index 5b71baa6bc..cfe0a097bd 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-space.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-space.ts @@ -1,38 +1,35 @@ -import { Service } from '@volar/language-service'; - -const plugin: Service = (context): ReturnType => { - - if (!context) - return {}; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +export function create(): ServicePlugin { return { - - async provideAutoInsertionEdit(document, _, { lastChange }) { - - if (document.languageId === 'html' || document.languageId === 'jade') { - - const enabled = await context.env.getConfiguration?.('vue.autoInsert.bracketSpacing') ?? true; - if (!enabled) - return; - - if ( - lastChange.text === '{}' - && document.getText({ - start: { line: lastChange.range.start.line, character: lastChange.range.start.character - 1 }, - end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 3 } - }) === '{{}}' - ) { - return { - newText: ` $0 `, - range: { - start: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 }, - end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 } - }, - }; - } - } + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, _, lastChange) { + + if (document.languageId === 'html' || document.languageId === 'jade') { + + const enabled = await context.env.getConfiguration?.('vue.autoInsert.bracketSpacing') ?? true; + if (!enabled) + return; + + if ( + lastChange.text === '{}' + && document.getText({ + start: { line: lastChange.range.start.line, character: lastChange.range.start.character - 1 }, + end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 3 } + }) === '{{}}' + ) { + return { + newText: ` $0 `, + range: { + start: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 }, + end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 } + }, + }; + } + } + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index 431415bfba..38f8f4370d 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,83 +1,79 @@ -import { Service } from '@volar/language-service'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { SourceFile, VirtualFile, VueCodeInformation, VueFile } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -export const create = function (): Service { +export function create(): ServicePlugin { + return { + create(context): ServicePluginInstance { + return { + provideReferencesCodeLensRanges(document) { - return (context): ReturnType => { + return worker(document.uri, async virtualFile => { - if (!context) - return {}; + const result: vscode.Range[] = []; - return { + for (const map of context.documents.getMaps(virtualFile) ?? []) { + for (const mapping of map.map.mappings) { - provideReferencesCodeLensRanges(document) { + if (!(mapping.data as VueCodeInformation).__referencesCodeLens) + continue; - return worker(document.uri, async virtualFile => { - - const result: vscode.Range[] = []; - - for (const map of context.documents.getMaps(virtualFile) ?? []) { - for (const mapping of map.map.codeMappings) { - - if (!(mapping.data as VueCodeInformation).__referencesCodeLens) - continue; - - result.push({ - start: document.positionAt(mapping.generatedOffsets[0]), - end: document.positionAt( - mapping.generatedOffsets[mapping.generatedOffsets.length - 1] - + mapping.lengths[mapping.lengths.length - 1] - ), - }); + result.push({ + start: document.positionAt(mapping.generatedOffsets[0]), + end: document.positionAt( + mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + + mapping.lengths[mapping.lengths.length - 1] + ), + }); + } } - } - - return result; - }); - }, - - async resolveReferencesCodeLensLocations(document, range, references) { - const [virtualFile, sourceFile] = context.project.fileProvider.getVirtualFile(document.uri); - if (virtualFile && sourceFile?.virtualFile?.[0] instanceof VueFile) { - const vueFile = sourceFile.virtualFile[0]; - const blocks = [ - vueFile.sfc.script, - vueFile.sfc.scriptSetup, - vueFile.sfc.template, - ...vueFile.sfc.styles, - ...vueFile.sfc.customBlocks, - ]; - for (const map of context.documents.getMaps(virtualFile)) { - const sourceOffset = map.map.getSourceOffset(document.offsetAt(range.start)); - if (sourceOffset !== undefined) { - const sourceBlock = blocks.find(block => block && sourceOffset[0] >= block.startTagEnd && sourceOffset[0] <= block.endTagStart); - const sourceDocument = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); - references = references.filter(reference => - reference.uri !== sourceDocument.uri // different file - || sourceBlock !== blocks.find(block => - block - && sourceDocument.offsetAt(reference.range.start) >= block.startTagEnd - && sourceDocument.offsetAt(reference.range.end) <= block.endTagStart - ) // different block - ); - break; + return result; + }); + }, + + async resolveReferencesCodeLensLocations(document, range, references) { + + const [virtualFile, sourceFile] = context.language.files.getVirtualFile(document.uri); + if (virtualFile && sourceFile?.virtualFile?.[0] instanceof VueFile) { + const vueFile = sourceFile.virtualFile[0]; + const blocks = [ + vueFile.sfc.script, + vueFile.sfc.scriptSetup, + vueFile.sfc.template, + ...vueFile.sfc.styles, + ...vueFile.sfc.customBlocks, + ]; + for (const map of context.documents.getMaps(virtualFile)) { + const sourceOffset = map.map.getSourceOffset(document.offsetAt(range.start)); + if (sourceOffset !== undefined) { + const sourceBlock = blocks.find(block => block && sourceOffset[0] >= block.startTagEnd && sourceOffset[0] <= block.endTagStart); + const sourceDocument = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); + references = references.filter(reference => + reference.uri !== sourceDocument.uri // different file + || sourceBlock !== blocks.find(block => + block + && sourceDocument.offsetAt(reference.range.start) >= block.startTagEnd + && sourceDocument.offsetAt(reference.range.end) <= block.endTagStart + ) // different block + ); + break; + } } } - } - return references; - }, - }; + return references; + }, + }; - function worker(uri: string, callback: (vueFile: VirtualFile, sourceFile: SourceFile) => T) { + function worker(uri: string, callback: (vueFile: VirtualFile, sourceFile: SourceFile) => T) { - const [virtualFile, sourceFile] = context!.project.fileProvider.getVirtualFile(uri); - if (!(sourceFile?.virtualFile?.[0] instanceof VueFile) || !sourceFile) - return; + const [virtualFile, sourceFile] = context.language.files.getVirtualFile(uri); + if (!(sourceFile?.virtualFile?.[0] instanceof VueFile) || !sourceFile) + return; - return callback(virtualFile, sourceFile); - } + return callback(virtualFile, sourceFile); + } + }, }; -}; +} diff --git a/packages/language-service/src/plugins/vue-directive-comments.ts b/packages/language-service/src/plugins/vue-directive-comments.ts index ceda0d2028..1e0ecd01ac 100644 --- a/packages/language-service/src/plugins/vue-directive-comments.ts +++ b/packages/language-service/src/plugins/vue-directive-comments.ts @@ -1,4 +1,4 @@ -import { CompletionItem, Service } from '@volar/language-service'; +import { CompletionItem, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; const cmds = [ 'vue-ignore', @@ -8,58 +8,57 @@ const cmds = [ const directiveCommentReg = //g; -const plugin: Service = (context: ServiceContext | undefined, modules) => { - - if (!context || !modules?.typescript) - return {}; - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + provideInlayHints(document, range) { + return worker(document.uri, (vueFile) => { - provideInlayHints(document, range) { - return worker(document.uri, (vueFile) => { + const hoverOffsets: [vscode.Position, number][] = []; + const inlayHints: vscode.InlayHint[] = []; + const languageService = context.inject('typescript/languageService'); - const hoverOffsets: [vscode.Position, number][] = []; - const inlayHints: vscode.InlayHint[] = []; - const languageService = context.inject('typescript/languageService'); - - for (const pointer of document.getText(range).matchAll(twoslashReg)) { - const offset = pointer.index! + pointer[0].indexOf('^?') + document.offsetAt(range.start); - const position = document.positionAt(offset); - hoverOffsets.push([position, document.offsetAt({ - line: position.line - 1, - character: position.character, - })]); - } + for (const pointer of document.getText(range).matchAll(twoslashReg)) { + const offset = pointer.index! + pointer[0].indexOf('^?') + document.offsetAt(range.start); + const position = document.positionAt(offset); + hoverOffsets.push([position, document.offsetAt({ + line: position.line - 1, + character: position.character, + })]); + } - for (const virtualFile of forEachEmbeddedFile(vueFile)) { - if (virtualFile.typescript) { - for (const map of context.documents.getMaps(virtualFile)) { - for (const [pointerPosition, hoverOffset] of hoverOffsets) { - for (const [tsOffset, mapping] of map.map.getGeneratedOffsets(hoverOffset)) { - if (vue.isHoverEnabled(mapping.data)) { - const quickInfo = languageService.getQuickInfoAtPosition(context.env.uriToFileName(virtualFile.id), tsOffset); - if (quickInfo) { - inlayHints.push({ - position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, - label: ts.displayPartsToString(quickInfo.displayParts), - paddingLeft: true, - paddingRight: false, - }); + for (const virtualFile of forEachEmbeddedFile(vueFile)) { + if (virtualFile.typescript) { + for (const map of context.documents.getMaps(virtualFile)) { + for (const [pointerPosition, hoverOffset] of hoverOffsets) { + for (const [tsOffset, mapping] of map.map.getGeneratedOffsets(hoverOffset)) { + if (vue.isHoverEnabled(mapping.data)) { + const quickInfo = languageService.getQuickInfoAtPosition(context.env.uriToFileName(virtualFile.id), tsOffset); + if (quickInfo) { + inlayHints.push({ + position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, + label: ts.displayPartsToString(quickInfo.displayParts), + paddingLeft: true, + paddingRight: false, + }); + } + break; + } } - break; } } } } - } - } - return inlayHints; - }); - }, - }; - - function worker(uri: string, callback: (vueSourceFile: vue.VueFile) => T) { + return inlayHints; + }); + }, + }; - const [virtualFile] = context!.project.fileProvider.getVirtualFile(uri); - if (!(virtualFile instanceof vue.VueFile)) - return; + function worker(uri: string, callback: (vueSourceFile: vue.VueFile) => T) { - return callback(virtualFile); - } -}; + const [virtualFile] = context.language.files.getVirtualFile(uri); + if (!(virtualFile instanceof vue.VueFile)) + return; -export const create = () => plugin; + return callback(virtualFile); + } + }, + }; +} diff --git a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts index 56484fcc96..e58468c86a 100644 --- a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts +++ b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts @@ -1,57 +1,54 @@ -import { Service } from '@volar/language-service'; +import { ServicePluginInstance } from '@volar/language-service'; import type * as vscode from 'vscode-languageserver-protocol'; -import type { VueCodeInformation } from '../types'; - -const plugin: Service = (context) => { - - if (!context) - return {}; +import type { ServicePlugin, VueCodeInformation } from '../types'; +export function create(): ServicePlugin { return { - - async provideInlayHints(document, range) { - - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const [vitualFile] = context.project.fileProvider.getVirtualFile(document.uri); - - if (vitualFile) { - - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - - for (const mapping of vitualFile.mappings) { - - const hint = (mapping.data as VueCodeInformation).__hint; - - if ( - mapping.generatedOffsets[0] >= start - && mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1] <= end - && hint - ) { - - settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; - - if (!settings[hint.setting]) - continue; - - result.push({ - label: hint.label, - paddingRight: hint.paddingRight, - paddingLeft: hint.paddingLeft, - position: document.positionAt(mapping.generatedOffsets[0]), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - tooltip: { - kind: 'markdown', - value: hint.tooltip, - }, - }); + create(context): ServicePluginInstance { + return { + async provideInlayHints(document, range) { + + const settings: Record = {}; + const result: vscode.InlayHint[] = []; + const [vitualFile] = context.language.files.getVirtualFile(document.uri); + + if (vitualFile) { + + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + + for (const mapping of vitualFile.mappings) { + + const hint = (mapping.data as VueCodeInformation).__hint; + + if ( + mapping.generatedOffsets[0] >= start + && mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1] <= end + && hint + ) { + + settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; + + if (!settings[hint.setting]) + continue; + + result.push({ + label: hint.label, + paddingRight: hint.paddingRight, + paddingLeft: hint.paddingLeft, + position: document.positionAt(mapping.generatedOffsets[0]), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + tooltip: { + kind: 'markdown', + value: hint.tooltip, + }, + }); + } + } } - } - } - return result; + return result; + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue.ts b/packages/language-service/src/plugins/vue.ts index 4338436fb8..79f4c79d2e 100644 --- a/packages/language-service/src/plugins/vue.ts +++ b/packages/language-service/src/plugins/vue.ts @@ -1,9 +1,10 @@ -import type { Service, ServiceContext } from '@volar/language-service'; +import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import * as vue from '@vue/language-core'; +import { create as createHtmlService } from 'volar-service-html'; +import type { Provide as TSProvide } from 'volar-service-typescript'; import * as html from 'vscode-html-languageservice'; import type * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import createHtmlPlugin from 'volar-service-html'; -import * as vue from '@vue/language-core'; import { loadLanguageBlocks } from './data'; let sfcDataProvider: html.IHTMLDataProvider | undefined; @@ -12,191 +13,195 @@ export interface Provide { 'vue/vueFile': (document: TextDocument) => vue.VueFile | undefined; } -export const create = (): Service => (context: ServiceContext | undefined, modules): ReturnType> => { +export function create(): ServicePlugin { + return { + create(context): ServicePluginInstance { - const htmlPlugin = createHtmlPlugin({ languageId: 'vue', useCustomDataProviders: false })(context, modules); + const htmlPlugin = createHtmlService({ languageId: 'vue', useCustomDataProviders: false }).create(context); - if (!context) - return htmlPlugin as any; + if (!context) + return htmlPlugin as any; - sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); + sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); - htmlPlugin.provide['html/languageService']().setDataProviders(false, [sfcDataProvider]); + htmlPlugin.provide['html/languageService']().setDataProviders(false, [sfcDataProvider]); - return { + return { - ...htmlPlugin, + ...htmlPlugin, - provide: { - 'vue/vueFile': document => { - return worker(document, (vueFile) => { - return vueFile; - }); - }, - }, + provide: { + 'vue/vueFile': document => { + return worker(document, (vueFile) => { + return vueFile; + }); + }, + }, + + provideSemanticDiagnostics(document) { + return worker(document, (vueSourceFile) => { + + if (!vueSourceFile.mainTsFile) { + return; + } + + const result: vscode.Diagnostic[] = []; + const sfc = vueSourceFile.sfc; + const program = context.inject('typescript/languageService').getProgram(); + const tsFileName = context.env.uriToFileName(vueSourceFile.mainTsFile.id); + + if (program && !program.getSourceFile(tsFileName)) { + for (const script of [sfc.script, sfc.scriptSetup]) { + + if (!script || script.content === '') + continue; + + const error: vscode.Diagnostic = { + range: { + start: document.positionAt(script.start), + end: document.positionAt(script.startTagEnd), + }, + message: `Virtual script ${JSON.stringify(tsFileName)} not found, may missing