From e561e42e38e678aa1481be676b61b958ab782aa4 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 6 Dec 2023 21:54:37 +0800 Subject: [PATCH] feat: upgrade to Volar 2.0 alpha (#3736) --- extensions/vscode/package.json | 2 +- extensions/vscode/src/common.ts | 10 + extensions/vscode/src/features/dragImport.ts | 64 - package.json | 2 +- packages/component-meta/package.json | 2 +- packages/component-meta/src/base.ts | 114 +- packages/component-meta/src/index.ts | 4 +- packages/language-core/package.json | 4 +- .../language-core/src/generators/script.ts | 167 +- .../language-core/src/generators/template.ts | 1101 ++++++------ .../language-core/src/generators/utils.ts | 38 + packages/language-core/src/index.ts | 1 - packages/language-core/src/languageModule.ts | 76 +- packages/language-core/src/plugins/file-md.ts | 6 +- .../src/plugins/vue-sfc-customblocks.ts | 5 +- .../src/plugins/vue-sfc-scripts.ts | 14 +- .../src/plugins/vue-sfc-styles.ts | 5 +- .../src/plugins/vue-sfc-template.ts | 5 +- packages/language-core/src/plugins/vue-tsx.ts | 47 +- packages/language-core/src/types.ts | 16 + .../src/virtualFile/computedFiles.ts | 49 +- .../src/virtualFile/computedMappings.ts | 43 +- .../src/virtualFile/embeddedFile.ts | 11 +- .../language-core/src/virtualFile/vueFile.ts | 26 +- packages/language-plugin-pug/package.json | 4 +- packages/language-plugin-pug/src/index.ts | 2 +- packages/language-server/package.json | 6 +- .../src/languageServerPlugin.ts | 96 +- packages/language-server/src/nodeServer.ts | 4 +- packages/language-server/src/protocol.ts | 15 - packages/language-server/src/webServer.ts | 4 +- packages/language-service/package.json | 24 +- packages/language-service/src/helpers.ts | 32 +- .../src/ideFeatures/dragImport.ts | 65 - .../src/ideFeatures/nameCasing.ts | 26 +- packages/language-service/src/index.ts | 1 - .../language-service/src/languageService.ts | 442 ++--- .../src/plugins/vue-autoinsert-dotvalue.ts | 120 +- .../src/plugins/vue-autoinsert-parentheses.ts | 126 +- .../src/plugins/vue-autoinsert-space.ts | 65 +- .../src/plugins/vue-codelens-references.ts | 119 +- .../src/plugins/vue-directive-comments.ts | 89 +- .../src/plugins/vue-document-drop.ts | 95 ++ .../src/plugins/vue-extract-file.ts | 420 ++--- .../src/plugins/vue-template.ts | 1087 ++++++------ .../plugins/vue-toggle-v-bind-codeaction.ts | 237 ++- .../src/plugins/vue-twoslash-queries.ts | 100 +- .../vue-visualize-hidden-callback-param.ts | 101 +- packages/language-service/src/plugins/vue.ts | 350 ++-- packages/language-service/tests/complete.ts | 2 +- .../language-service/tests/findDefinition.ts | 4 +- packages/language-service/tests/inlayHint.ts | 2 +- packages/language-service/tests/reference.ts | 2 +- packages/language-service/tests/rename.ts | 2 +- .../tests/utils/createTester.ts | 118 +- .../language-service/tests/utils/format.ts | 10 +- .../language-service/tests/utils/mockEnv.ts | 87 + packages/tsc-eslint-hook/package.json | 1 + packages/tsc-eslint-hook/src/index.ts | 27 +- packages/tsc/package.json | 2 +- packages/tsc/src/index.ts | 76 +- packages/typescript-plugin/package.json | 2 +- packages/typescript-plugin/src/index.ts | 26 +- pnpm-lock.yaml | 1485 +++++++++++------ .../reference/slot-named/entry.vue | 2 +- 65 files changed, 3914 insertions(+), 3376 deletions(-) delete mode 100644 extensions/vscode/src/features/dragImport.ts create mode 100644 packages/language-core/src/generators/utils.ts delete mode 100644 packages/language-service/src/ideFeatures/dragImport.ts create mode 100644 packages/language-service/src/plugins/vue-document-drop.ts create mode 100644 packages/language-service/tests/utils/mockEnv.ts diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 5dcdc9eac1..726acea83c 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -737,7 +737,7 @@ "devDependencies": { "@types/semver": "^7.5.3", "@types/vscode": "^1.82.0", - "@volar/vscode": "~1.11.1", + "@volar/vscode": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "@vue/language-server": "1.8.26", "esbuild": "latest", diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 0adef5beb4..f841ebde95 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -1,5 +1,6 @@ import { activateAutoInsertion, + activateDocumentDropEdit, activateFindFileReferences, activateReloadProjects, activateServerSys, @@ -102,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/package.json b/package.json index 92a6a85a90..815abf6fc0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@lerna-lite/cli": "latest", "@lerna-lite/publish": "latest", - "@volar/language-service": "~1.11.1", + "@volar/language-service": "2.0.0-alpha.0", "typescript": "latest", "vite": "latest", "vitest": "latest" diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index 4deb45fb13..85742c89cf 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -13,7 +13,7 @@ "directory": "packages/component-meta" }, "dependencies": { - "@volar/typescript": "~1.11.1", + "@volar/typescript": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "path-browserify": "^1.0.1", "vue-component-type-helpers": "1.8.26" diff --git a/packages/component-meta/src/base.ts b/packages/component-meta/src/base.ts index c5b684399c..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 { createLanguageServiceHost, decorateLanguageService } from '@volar/typescript'; +import { createLanguage, decorateLanguageService } from '@volar/typescript'; import type { MetaCheckerOptions, @@ -33,6 +33,7 @@ export function createCheckerByJsonConfigBase( checkerOptions, rootDir, path.join(rootDir, '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,8 @@ function createCheckerWorker( let projectVersion = 0; const scriptSnapshots = new Map(); - const _host: vue.TypeScriptLanguageHost = { - workspacePath: rootPath, - rootPath: rootPath, + const _host: vue.TypeScriptProjectHost = { + getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => parsedCommandLine.options, getScriptFileNames: () => fileNames, @@ -84,10 +86,13 @@ function createCheckerWorker( } return scriptSnapshots.get(fileName); }, + getFileId: fileName => fileName, + getFileName: id => id, + getLanguageId: vue.resolveCommonLanguageId, }; return { - ...baseCreate(ts, _host, vue.resolveVueCompilerOptions(parsedCommandLine.vueOptions), checkerOptions, globalComponentName), + ...baseCreate(ts, configFileName, _host, vue.resolveVueCompilerOptions(parsedCommandLine.vueOptions), checkerOptions, globalComponentName), updateFile(fileName: string, text: string) { fileName = fileName.replace(windowsPathReg, '/'); scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text)); @@ -112,59 +117,60 @@ function createCheckerWorker( export function baseCreate( ts: typeof import('typescript/lib/tsserverlibrary'), - _host: vue.TypeScriptLanguageHost, + configFileName: string | undefined, + 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; - const vueLanguages = vue.createLanguages( + return metaSnapshots[fileName]; + } + else if (fileName === globalComponentName) { + return globalComponentSnapshot; + } + else { + return getScriptSnapshot(fileName); + } + }; + + const vueLanguagePlugins = vue.createLanguages( ts, host.getCompilationSettings(), vueCompilerOptions, ); - const core = vue.createLanguageContext(host, vueLanguages); - const tsLsHost = createLanguageServiceHost(core, ts, ts.sys); - const tsLs = ts.createLanguageService(tsLsHost); + const language = createLanguage( + ts, + ts.sys, + vueLanguagePlugins, + configFileName, + host, + ); + const { languageServiceHost } = language.typescript!; + const tsLs = ts.createLanguageService(languageServiceHost); - decorateLanguageService(core.virtualFiles, tsLs, false); + decorateLanguageService(language.files, 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; } @@ -281,7 +287,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} .map((prop) => { const { resolveNestedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveNestedProperties(prop); }) @@ -300,7 +306,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 = language.files.getSourceFile(componentPath)?.virtualFile?.[0]; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; @@ -345,7 +351,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const { resolveEventSignature, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveEventSignature(call); }).filter(event => event.name); @@ -365,7 +371,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveSlotProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveSlotProperties(prop); }); @@ -388,7 +394,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveExposedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveExposedProperties(prop); }); @@ -447,7 +453,7 @@ function createSchemaResolvers( symbolNode: ts.Expression, { rawType, schema: options, noDeclarations }: MetaCheckerOptions, ts: typeof import('typescript/lib/tsserverlibrary'), - core: vue.LanguageContext, + core: vue.Language, ) { const visited = new Set(); @@ -638,12 +644,12 @@ function createSchemaResolvers( } function getDeclaration(declaration: ts.Declaration): Declaration | undefined { const fileName = declaration.getSourceFile().fileName; - const [virtualFile] = core.virtualFiles.getVirtualFile(fileName); + const [virtualFile] = core.files.getVirtualFile(fileName); if (virtualFile) { - const maps = core.virtualFiles.getMaps(virtualFile); + const maps = core.files.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/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/package.json b/packages/language-core/package.json index 70ac7e1cb9..220a4ba5ac 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -13,13 +13,11 @@ "directory": "packages/language-core" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/source-map": "~1.11.1", + "@volar/language-core": "2.0.0-alpha.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 72776cec2d..117ac4c878 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -1,16 +1,14 @@ -import { FileRangeCapabilities, MirrorBehaviorCapabilities } 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 { 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, VueCompilerOptions } from '../types'; +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'), @@ -27,8 +25,8 @@ export function generate( codegenStack: boolean, ) { - const [codes, codeStacks] = codegenStack ? muggle.track([] as Segment[]) : [[], []]; - const mirrorBehaviorMappings: SourceMaps.Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; + const [codes, codeStacks] = codegenStack ? track([] as Code[]) : [[], []]; + const mirrorBehaviorMappings: Mapping[] = []; //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 if (!script && !scriptSetup) { @@ -91,7 +89,7 @@ export function generate( '', 'scriptSetup', scriptSetup.content.length, - {}, + disableAllFeatures({}), ]); } @@ -129,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`); @@ -153,16 +149,12 @@ export function generate( codes.push([ `'${src}'`, 'script', - [script.srcOffset - 1, script.srcOffset + script.src.length + 1], - { - ...FileRangeCapabilities.full, - rename: src === script.src ? true : { - normalize: undefined, - apply(newName) { - if ( - newName.endsWith('.jsx') - || newName.endsWith('.js') - ) { + script.srcOffset - 1, + enableAllFeatures({ + navigation: src === script.src ? true : { + shouldRename: () => false, + resolveRenameEditText(newName) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { newName = newName.split('.').slice(0, -1).join('.'); } if (script?.src?.endsWith('.d.ts')) { @@ -177,7 +169,7 @@ export function generate( return newName; }, }, - }, + }), ]); codes.push(`;\n`); codes.push(`export { default } from '${src}';\n`); @@ -198,7 +190,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], @@ -207,15 +199,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); @@ -245,7 +237,7 @@ export function generate( scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + '\n', 'scriptSetup', 0, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); } function generateScriptSetupAndTemplate() { @@ -254,7 +246,7 @@ export function generate( return; } - const definePropMirrors: Record = {}; + const definePropMirrors = new Map(); let scriptSetupGeneratedOffset: number | undefined; if (scriptSetup.generic) { @@ -266,7 +258,7 @@ export function generate( scriptSetup.generic, scriptSetup.name, scriptSetup.genericOffset, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); if (!scriptSetup.generic.endsWith(',')) { codes.push(`,`); @@ -324,8 +316,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) { @@ -393,21 +384,19 @@ export function generate( continue; } const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirror = definePropMirrors[propName]; - if (propMirror) { + const propMirror = definePropMirrors.get(propName); + if (propMirror !== undefined) { mirrorBehaviorMappings.push({ - sourceRange: [defineProp.name.start + scriptSetupGeneratedOffset, defineProp.name.end + scriptSetupGeneratedOffset], - generatedRange: propMirror, - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], + sourceOffsets: [defineProp.name.start + scriptSetupGeneratedOffset], + generatedOffsets: [propMirror], + lengths: [defineProp.name.end - defineProp.name.start], + data: undefined, }); } } } } - function generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Record) { + function generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Map) { if (!scriptSetupRanges || !scriptSetup) { return; @@ -440,7 +429,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) { @@ -521,8 +510,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 { @@ -728,23 +717,19 @@ 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({ - sourceRange: [scriptStart, scriptEnd], - generatedRange: [templateStart, templateEnd], - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], + sourceOffsets: [scriptOffset], + generatedOffsets: [templateOffset], + lengths: [varName.length], + data: undefined, }); } } @@ -769,10 +754,9 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: script.content.substring(componentsOption.start, componentsOption.end), 'script', componentsOption.start, - { - references: true, - rename: true, - }, + disableAllFeatures({ + navigation: true, + }), ]); } else { @@ -810,8 +794,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, @@ -839,8 +823,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, @@ -856,11 +840,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); } @@ -877,36 +861,41 @@ 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, - { - references: true, - referencesCodeLens, - }, + offset, + disableAllFeatures({ + navigation: true, + __referencesCodeLens: referencesCodeLens, + }), ]); codes.push(`'`); codes.push([ - className, + '', 'style_' + styleIndex, - [classRange.start, classRange.end], - { - references: true, - rename: { - normalize: normalizeCssRename, - apply: applyCssRename, + offset, + disableAllFeatures({ + navigation: { + resolveRenameNewName: normalizeCssRename, + resolveRenameEditText: applyCssRename, }, - }, + }), + ]); + codes.push([ + classNameWithDot.substring(1), + 'style_' + styleIndex, + offset + 1, + disableAllFeatures({ __combineLastMappping: true }), ]); codes.push(`'`); codes.push([ '', 'style_' + styleIndex, - classRange.end, - {}, + offset + classNameWithDot.length, + disableAllFeatures({}), ]); codes.push(`${optional ? '?' : ''}: ${propertyType}`); codes.push(` }`); @@ -932,8 +921,8 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: style.name, cssBind.offset + fragOffset, onlyForErrorMapping - ? { diagnostic: true } - : FileRangeCapabilities.full, + ? disableAllFeatures({ verification: true }) + : enableAllFeatures({}), ]); } }, @@ -972,28 +961,24 @@ 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, - FileRangeCapabilities.full, // diagnostic also working for setup() returns unused in template checking + enableAllFeatures({}), // 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, start, - { - references: true, - definition: true, - rename: true, - }, + disableAllFeatures({ navigation: true }), ]); - muggle.resetOffsetStack(); + resetOffsetStack(); } } diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index f95543da24..8b4e6af233 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1,18 +1,18 @@ -import { FileRangeCapabilities } from '@volar/language-core'; -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, VueCompilerOptions } from '../types'; +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 capabilitiesPresets = { - all: FileRangeCapabilities.full, - allWithHiddenParam: { - ...FileRangeCapabilities.full, __hint: { +const presetInfos = { + disabledAll: disableAllFeatures({}), + all: enableAllFeatures({}), + allWithHiddenParam: enableAllFeatures({ + __hint: { setting: 'vue.inlayHints.inlineHandlerLeading', label: '$event =>', tooltip: [ @@ -21,20 +21,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, + } + }), + noDiagnostics: enableAllFeatures({ verification: false }), + diagnosticOnly: disableAllFeatures({ verification: true }), + tagHover: disableAllFeatures({ semantic: { shouldHighlight: () => false } }), + event: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true }), + tagReference: disableAllFeatures({ navigation: { shouldRename: () => false } }), + 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: { 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 = { normal: ['`${', '}`;'] as [string, string], @@ -63,8 +63,6 @@ const transformContext: CompilerDOM.TransformContext = { expressionPlugins: ['typescript'], }; -type Code = Segment; - export function generate( ts: typeof import('typescript/lib/tsserverlibrary'), compilerOptions: ts.CompilerOptions, @@ -79,14 +77,24 @@ 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(); - const tempVars: ReturnType[] = []; + const tempVars: { + text: string, + isShorthand: boolean, + offset: number, + }[][] = []; const accessedGlobalVariables = new Set(); const scopedClasses: { className: string, offset: number; }[] = []; const blockConditions: string[] = []; @@ -137,55 +145,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, - { - ...capabilitiesPresets.slotNameExport, - referencesCodeLens: true, - }, - ], slot.nodeLoc), - ); - codes.push(`?(_: typeof ${slot.varName}): any,\n`); + mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true })), + slot.nodeLoc + ); + } + else { + yield ['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true }))]; + yield 'default'; + yield ['', 'template', slot.tagRange[1], disableAllFeatures({ __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, - { - ...capabilitiesPresets.scopedClassName, - displayWithLink: stylesScopedClasses.has(className), - }, - ])); - codes.push(`];\n`); + codes.push( + `__VLS_styleScopedClasses[`, + ...createStringLiteralKeyCode( + className, + offset, + mergeFeatureSettings( + presetInfos.scopedClassName, + disableAllFeatures({ __displayWithLink: stylesScopedClasses.has(className) }), + ), + ), + `];\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)), @@ -222,62 +246,82 @@ 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, - { - ...capabilitiesPresets.tagReference, - rename: { - normalize: tagName === name ? capabilitiesPresets.tagReference.rename.normalize : camelizeComponentName, - apply: getTagRenameApply(tagName), + '__VLS_intrinsicElements', + ...createPropertyAccessCode( + tagName, + tagOffset, + mergeFeatureSettings( + presetInfos.tagReference, + { + navigation: true }, - ...nativeTags.has(tagName) ? { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - } : {}, - }, - ]), + ...(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, + mergeFeatureSettings( + presetInfos.tagReference, + { + navigation: { + resolveRenameNewName: tagName !== expectName ? camelizeComponentName : undefined, + resolveRenameEditText: 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, - { - completion: { - additional: true, - autoImportOnly: 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, + disableAllFeatures({ + completion: { + isAdditional: true, + onlyImport: true, + }, + }), + ), + ',', + ); + } + codes.push(`];\n`); } - codes.push(`];\n`); } } @@ -330,11 +374,11 @@ export function generate( if (typeof code === 'string') { continue; } - const cap = code[3]; - if (cap.diagnostic) { + const data = code[3]; + if (data.verification) { code[3] = { - ...cap, - diagnostic: false, + ...data, + verification: false, }; } } @@ -351,11 +395,11 @@ export function generate( if (typeof code === 'string') { continue; } - const cap = code[3]; - if (cap.diagnostic) { + const data = code[3]; + if (data.verification) { code[3] = { - ...cap, - diagnostic: { + ...data, + verification: { shouldReport: suppressError, }, }; @@ -363,14 +407,21 @@ export function generate( } codes.push( [ - '// @ts-expect-error __VLS_TS_EXPECT_ERROR', + '', 'template', - [expectedErrorNode.loc.start.offset, expectedErrorNode.loc.end.offset], - { - diagnostic: { + expectedErrorNode.loc.start.offset, + disableAllFeatures({ + verification: { shouldReport: () => errors === 0, }, - }, + }), + ], + '// @ts-expect-error __VLS_TS_EXPECT_ERROR', + [ + '', + 'template', + expectedErrorNode.loc.end.offset, + disableAllFeatures({ __combineLastMappping: true }), ], '\n;\n', ); @@ -457,7 +508,7 @@ export function generate( content, node.content.loc, start, - capabilitiesPresets.all, + presetInfos.all, '(', ');\n', ), @@ -512,7 +563,7 @@ export function generate( branch.condition.content, branch.condition.loc, branch.condition.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -527,7 +578,7 @@ export function generate( ), ); - blockConditions.push(muggle.toString(codes.slice(beforeCodeLength, afterCodeLength))); + blockConditions.push(toString(codes.slice(beforeCodeLength, afterCodeLength))); addedBlockCondition = true; } @@ -567,7 +618,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`); @@ -578,7 +629,7 @@ export function generate( source.content, source.loc, source.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -659,26 +710,25 @@ export function generate( 'const ', var_originalComponent, ` = __VLS_intrinsicElements[`, - ...createStringLiteralKeyCode([ + ...createStringLiteralKeyCode( tag, - 'template', tagOffsets[0], - capabilitiesPresets.diagnosticOnly, - ]), + presetInfos.diagnosticOnly, + ), '];\n', ); } 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', ); } @@ -686,7 +736,7 @@ export function generate( codes.push( `const ${var_originalComponent} = ({} as `, ); - for (const componentName of getPossibleOriginalComponentName(tag)) { + for (const componentName of getPossibleOriginalComponentNames(tag)) { codes.push( `'${componentName}' extends keyof typeof __VLS_ctx ? `, `{ '${toCanonicalComponentName(tag)}': typeof __VLS_ctx`, @@ -697,12 +747,11 @@ export function generate( codes.push( `typeof __VLS_resolvedLocalAndGlobalComponents)`, ...(tagOffsets.length - ? createPropertyAccessCode([ + ? createPropertyAccessCode( toCanonicalComponentName(tag), - 'template', - [tagOffsets[0], tagOffsets[0] + tag.length], - capabilitiesPresets.diagnosticOnly, - ]) + tagOffsets[0], + presetInfos.diagnosticOnly, + ) : createPropertyAccessCode(toCanonicalComponentName(tag)) ), ';\n', @@ -727,18 +776,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], - { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - }, - ], + `({} as { ${toCanonicalComponentName(tag)}: typeof ${var_originalComponent} }).`, + ...createCanonicalComponentNameCode( + tag, + offset, + mergeFeatureSettings( + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ), + ), ';\n', ); } @@ -748,15 +795,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`, ); @@ -774,15 +821,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`, ); @@ -805,7 +852,7 @@ export function generate( failedExp.loc.source, failedExp.loc, failedExp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '(', ')', ), @@ -839,7 +886,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`); @@ -893,7 +940,7 @@ export function generate( slotDir.exp.content, 'template', slotDir.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, ], `] = __VLS_getSlotParams(`, ); @@ -905,31 +952,31 @@ 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) - ? createPropertyAccessCode([ + ? createPropertyAccessCode( slotDir.arg.loc.source, - 'template', slotDir.arg.loc.start.offset, - slotDir.arg.isStatic ? capabilitiesPresets.slotName : capabilitiesPresets.all - ], slotDir.arg.loc) - : createPropertyAccessCode([ + slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all, + slotDir.arg.loc + ) + : [ + '.', + ['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completion: 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)], - { ...capabilitiesPresets.slotName, completion: false }, - ]) + ['', '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, capabilitiesPresets.diagnosticOnly], + ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly], ); if (hasProps) { codes.push(')'); @@ -961,8 +1008,13 @@ 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 }, + slotDir.loc.start.offset + ( + slotDir.loc.source.startsWith('#') + ? '#'.length : slotDir.loc.source.startsWith('v-slot:') + ? 'v-slot:'.length + : 0 + ), + disableAllFeatures({ completion: true }), ], `'/* empty slot name completion */]\n`, ); @@ -980,16 +1032,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, disableAllFeatures({ navigation: true })], + 'default', + ['', 'template', node.children[node.children.length - 1].loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ';\n', ); } @@ -1010,20 +1056,24 @@ 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], + `__VLS_pickEvent(`, + `${eventsVar}['${prop.arg.loc.source}'], `, + `({} as __VLS_FunctionalComponentProps)`, + ); + const startCode: Code = [ + '', + 'template', + prop.arg.loc.start.offset, + mergeFeatureSettings( + presetInfos.attrReference, { - ...capabilitiesPresets.attrReference, - rename: { + navigation: { // @click-outside -> onClickOutside - normalize(newName) { + resolveRenameNewName(newName) { return camelize('on-' + newName); }, // onClickOutside -> @click-outside - apply(newName) { + resolveRenameEditText(newName) { const hName = hyphenateAttr(newName); if (hyphenateAttr(newName).startsWith('on-')) { return camelize(hName.slice('on-'.length)); @@ -1032,7 +1082,38 @@ export function generate( }, }, }, - ]), + ), + ]; + if (validTsVarReg.test(camelize(prop.arg.loc.source))) { + codes.push( + `.`, + startCode, + `on`, + ...createCamelizeCode( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + disableAllFeatures({ __combineLastMappping: true }), + ), + ); + } + else { + codes.push( + `[`, + startCode, + `'`, + ['', 'template', prop.arg.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })], + 'on', + ...createCamelizeCode( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + disableAllFeatures({ __combineLastMappping: true }), + ), + `'`, + ['', 'template', prop.arg.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], + `]`, + ); + } + codes.push( `) };\n`, `${eventVar} = { `, ); @@ -1043,7 +1124,7 @@ export function generate( prop.arg.loc.source.slice(1, -1), prop.arg.loc, prop.arg.loc.start.offset + 1, - capabilitiesPresets.all, + presetInfos.all, '', '', ), @@ -1052,12 +1133,12 @@ export function generate( } else { codes.push( - ...createObjectPropertyCode([ + ...createObjectPropertyCode( prop.arg.loc.source, - 'template', prop.arg.loc.start.offset, - capabilitiesPresets.event, - ], prop.arg.loc) + presetInfos.event, + prop.arg.loc + ) ); } codes.push(`: `); @@ -1076,7 +1157,7 @@ export function generate( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, '$event => {(', ')}', ), @@ -1139,9 +1220,9 @@ export function generate( () => { if (isCompoundExpression && isFirstMapping) { isFirstMapping = false; - return capabilitiesPresets.allWithHiddenParam; + return presetInfos.allWithHiddenParam; } - return capabilitiesPresets.all; + return presetInfos.all; }, prefix, suffix, @@ -1189,23 +1270,14 @@ 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 = presetInfos.all; + let caps_diagnosticOnly: VueCodeInformation = presetInfos.diagnosticOnly; + let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { - caps_all = { - references: caps_all.references, - rename: caps_all.rename, - }; - caps_diagnosticOnly = { - references: caps_diagnosticOnly.references, - rename: caps_diagnosticOnly.rename, - }; - caps_attr = { - references: caps_attr.references, - rename: caps_attr.rename, - }; + caps_all = disableAllFeatures({ navigation: caps_all.navigation }); + caps_diagnosticOnly = disableAllFeatures({ navigation: caps_diagnosticOnly.navigation }); + caps_attr = disableAllFeatures({ navigation: caps_attr.navigation }); } codes.push(`...{ `); @@ -1215,10 +1287,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(`}, `); @@ -1233,7 +1302,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 @@ -1241,15 +1310,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); @@ -1257,68 +1326,34 @@ export function generate( continue; } - let camelized = false; - - if ( - canCamelize + const shouldCamelize = canCamelize && (!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; - } + && hyphenateAttr(propName) === propName + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(propName!, pattern)); - // 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, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, - }, - }, - ], (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, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, + codes.push( + ['', 'template', prop.loc.start.offset, caps_diagnosticOnly], + ...createObjectPropertyCode( + propName, + prop.arg + ? prop.arg.loc.start.offset + : prop.loc.start.offset, + prop.arg + ? mergeFeatureSettings( + caps_attr, + { + navigation: caps_attr.navigation ? { + resolveRenameNewName: camelize, + resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, + } : 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( @@ -1354,63 +1389,45 @@ 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; - - if ( - canCamelize + const shouldCamelize = canCamelize && hyphenateAttr(prop.name) === prop.name - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(attrNameText!, pattern)) - ) { - attrNameText = camelize(prop.name); - camelized = true; - } + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(prop.name, pattern)); - // 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, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, - }, - }, - ], (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 + ? mergeFeatureSettings(caps_attr, { + navigation: caps_attr.navigation ? { + resolveRenameNewName: camelize, + resolveRenameEditText: hyphenateAttr, + } : undefined, + }) + : 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 ( @@ -1420,7 +1437,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, @@ -1430,7 +1447,7 @@ export function generate( '(', ')', ), - ['', 'template', prop.exp.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly], ', ', ); if (mode === 'normal') { @@ -1450,29 +1467,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']) { @@ -1495,7 +1489,10 @@ export function generate( content, 'template', prop.arg.loc.start.offset + start, - capabilitiesPresets.all, + enableAllFeatures({ + format: false, + structure: false, + }), ]); cssCodes.push(` }\n`); } @@ -1521,7 +1518,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, '(', ')', ), @@ -1537,44 +1534,40 @@ export function generate( } codes.push( - [ - '', - 'template', - prop.loc.start.offset, - capabilitiesPresets.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], - { - ...capabilitiesPresets.noDiagnostic, - completion: { - // fix https://github.com/vuejs/language-tools/issues/1905 - additional: true, - }, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + ...createCamelizeCode( + 'v-' + prop.name, + prop.loc.start.offset, + mergeFeatureSettings( + presetInfos.noDiagnostics, + { + completion: { + // fix https://github.com/vuejs/language-tools/issues/1905 + isAdditional: true, + }, + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: 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( @@ -1589,7 +1582,7 @@ export function generate( } codes.push( ')', - ['', 'template', prop.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', prop.loc.end.offset, presetInfos.diagnosticOnly], ';\n', ); } @@ -1609,7 +1602,7 @@ export function generate( prop.value.content, prop.value.loc, prop.value.loc.start.offset + 1, - capabilitiesPresets.refAttr, + presetInfos.refAttr, '(', ')', ), @@ -1655,7 +1648,7 @@ export function generate( prop.exp.content, 'template', prop.exp.loc.start.offset, - capabilitiesPresets.scopedClassName, + presetInfos.scopedClassName, ]); codes.push(`);\n`); } @@ -1670,15 +1663,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], - slotNameExpNode?.content ?? `('${getSlotName()}' as const)`, - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })], + slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`, + ['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ']', - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], + ['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })], ')?.(', - ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly], + ['', 'template', startTagOffset, disableAllFeatures({ __combineLastMappping: true })], '{\n', ); } @@ -1697,7 +1690,7 @@ export function generate( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, + presetInfos.attrReference, '(', ')', ), @@ -1711,24 +1704,26 @@ 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], - { - ...capabilitiesPresets.slotProp, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.arg.content), + prop.arg.loc.start.offset, + mergeFeatureSettings( + presetInfos.slotProp, + { + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.arg.content), + }, }, - }, - ], prop.arg.loc), + ), + prop.arg.loc + ), ': ', ...createInterpolationCode( prop.exp.content, prop.exp.loc, prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, + presetInfos.attrReference, '(', ')', ), @@ -1740,27 +1735,31 @@ export function generate( && prop.name !== 'name' // slot name ) { codes.push( - ...createObjectPropertyCode([ + ...createObjectPropertyCode( prop.name, - 'template', prop.loc.start.offset, - { - ...capabilitiesPresets.attr, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + mergeFeatureSettings( + presetInfos.attr, + { + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), + }, }, - }, - ], 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', ); } } codes.push( '}', - hasScriptSetupSlots ? ['', 'template', startTagOffset + node.tag.length, capabilitiesPresets.diagnosticOnly] : '', + hasScriptSetupSlots ? ['', 'template', startTagOffset + node.tag.length, presetInfos.diagnosticOnly] : '', hasScriptSetupSlots ? `);\n` : `;\n` ); @@ -1792,9 +1791,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, }); } @@ -1803,11 +1804,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) { @@ -1829,7 +1832,12 @@ 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, + disableAllFeatures({ completion: { isAdditional: true }, }), + ]); codes.push(','); } } @@ -1839,81 +1847,150 @@ export function generate( // functional like - function createFormatCode(mapCode: string, sourceOffset: number, formatWrapper: [string, string]): Code[] { - return [ - formatWrapper[0], - [mapCode, 'template', sourceOffset, { completion: true /* fix vue-autoinsert-parentheses not working */ }], - 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, disableAllFeatures({ __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 + : disableAllFeatures({ __combineLastMappping: true }), + ]; + } + offset += part.length + 1; + } + } + + function* createFormatCode(code: string, offset: number, formatWrapper: [string, string]): Generator { + yield formatWrapper[0]; + yield [ + code, + 'template', + offset, + mergeFeatureSettings( + presetInfos.disabledAll, + { + format: true, + // autoInserts: true, // TODO: 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, disableAllFeatures({ __combineLastMappping: true })); + yield '"'; + yield ['', 'template', offset + code.length, disableAllFeatures({ __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: FileRangeCapabilities | (() => FileRangeCapabilities) | undefined, + data: VueCodeInformation | (() => VueCodeInformation) | undefined, prefix: string, suffix: string, - ): Code[] { + ): Generator { 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 - ? capabilitiesPresets.diagnosticOnly - : typeof data === 'function' ? data() : data, - ]); + const vars = 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; @@ -1922,7 +1999,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) { @@ -1933,45 +2012,42 @@ 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 offset !== undefined && info + ? [code, 'template', offset, info] + : code; } - 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]], - ]; + function* createStringLiteralKeyCode(code: string, offset?: number, info?: VueCodeInformation): Generator { + if (offset === undefined || !info) { + yield `"${code}"`; + return; } - return codes; + yield ['', 'template', offset, info]; + yield '"'; + yield [code, 'template', offset, disableAllFeatures({ __combineLastMappping: true })]; + yield '"'; + yield ['', 'template', offset + code.length, disableAllFeatures({ __combineLastMappping: true })]; } -}; +} export function walkElementNodes(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode, cb: (node: CompilerDOM.ElementNode) => void) { if (node.type === CompilerDOM.NodeTypes.ROOT) { @@ -2008,11 +2084,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) { @@ -2030,15 +2103,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/generators/utils.ts b/packages/language-core/src/generators/utils.ts new file mode 100644 index 0000000000..041cb87f9e --- /dev/null +++ b/packages/language-core/src/generators/utils.ts @@ -0,0 +1,38 @@ +import { VueCodeInformation } from '../types'; + +export function disableAllFeatures(override: Partial): VueCodeInformation { + return { + verification: false, + completion: false, + semantic: false, + navigation: false, + structure: false, + format: false, + ...override, + }; +} + +export function enableAllFeatures(override: Partial): VueCodeInformation { + return { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, + ...override, + }; +} + +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) { + result[key as keyof VueCodeInformation] = value as any; + } + } + } + return result; +} 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/languageModule.ts b/packages/language-core/src/languageModule.ts index dcf477b78d..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( @@ -68,48 +68,52 @@ 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, 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; } }, updateVirtualFile(sourceFile, snapshot) { sourceFile.update(snapshot); }, - resolveHost(host) { - const sharedTypesSnapshot = ts.ScriptSnapshot.fromString(sharedTypes.getTypesCode(vueCompilerOptions)); - const sharedTypesFileName = path.join(host.rootPath, 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, - ...host.getScriptFileNames(), - ]; - }, - getScriptSnapshot(fileName) { - if (fileName === sharedTypesFileName) { - return sharedTypesSnapshot; - } - return host.getScriptSnapshot(fileName); - }, - }; + typescript: { + 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, + getScriptFileNames() { + return [ + sharedTypesFileName, + ...host.getScriptFileNames(), + ]; + }, + getScriptSnapshot(fileName) { + if (fileName === sharedTypesFileName) { + return sharedTypesSnapshot; + } + return host.getScriptSnapshot(fileName); + }, + }; + }, }, }; } @@ -122,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-core/src/plugins/file-md.ts b/packages/language-core/src/plugins/file-md.ts index b4d37b339b..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'; @@ -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..7ec8d1d5d0 100644 --- a/packages/language-core/src/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/src/plugins/vue-sfc-customblocks.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const customBlockReg = /^(.*)\.customBlock_([^_]+)_(\d+)\.([^.]+)$/; @@ -24,12 +24,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, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-scripts.ts b/packages/language-core/src/plugins/vue-sfc-scripts.ts index e50695bf7c..df45f8cb0e 100644 --- a/packages/language-core/src/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/src/plugins/vue-sfc-scripts.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileKind } from '@volar/language-core'; +import { disableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const scriptFormatReg = /^(.*)\.script_format\.([^.]+)$/; @@ -26,18 +26,14 @@ 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, - {}, + disableAllFeatures({ + structure: true, + format: true, + }), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-styles.ts b/packages/language-core/src/plugins/vue-sfc-styles.ts index b750349a1d..b661a8d88f 100644 --- a/packages/language-core/src/plugins/vue-sfc-styles.ts +++ b/packages/language-core/src/plugins/vue-sfc-styles.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const styleReg = /^(.*)\.style_(\d+)\.([^.]+)$/; @@ -24,12 +24,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, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-template.ts b/packages/language-core/src/plugins/vue-sfc-template.ts index b21e8e8cd4..1b366f235a 100644 --- a/packages/language-core/src/plugins/vue-sfc-template.ts +++ b/packages/language-core/src/plugins/vue-sfc-template.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const templateReg = /^(.*)\.template\.([^.]+)$/; @@ -19,12 +19,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, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-tsx.ts b/packages/language-core/src/plugins/vue-tsx.ts index b3cb5e826b..92ef46e700 100644 --- a/packages/language-core/src/plugins/vue-tsx.ts +++ b/packages/language-core/src/plugins/vue-tsx.ts @@ -1,11 +1,11 @@ +import { CodeInformation, Segment, track } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; 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'; +import { enableAllFeatures } from '../generators/utils'; const templateFormatReg = /^\.template_format\.ts$/; const templateStyleCssReg = /^\.template_style\.css$/; @@ -43,40 +43,38 @@ 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]]; + const [content, contentStacks] = ctx.codegenStack ? track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + content.forEach(code => { + if (typeof code !== 'string') { + code[3].structure = false; + code[3].format = false; + } + }); embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; - embeddedFile.mirrorBehaviorMappings = [...tsx.mirrorBehaviorMappings]; + embeddedFile.linkedNavigationMappings = [...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) { 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; @@ -90,7 +88,7 @@ const plugin: VueLanguagePlugin = (ctx) => { cssVar.text, style.name, cssVar.offset, - {}, + enableAllFeatures({}), ]); embeddedFile.content.push(');\n'); } @@ -103,14 +101,11 @@ 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; + embeddedFile.content = content as 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..9e5ee90e2d 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, Segment } from '@volar/language-core'; export type { SFCParseResult } from '@vue/compiler-sfc'; @@ -10,6 +11,21 @@ export type RawVueCompilerOptions = Partial; + 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 8a6d4fbc8e..1692e7a87e 100644 --- a/packages/language-core/src/virtualFile/computedFiles.ts +++ b/packages/language-core/src/virtualFile/computedFiles.ts @@ -1,10 +1,8 @@ -import { VirtualFile } from '@volar/language-core'; -import { buildMappings, buildStacks, toString } from '@volar/source-map'; -import * as muggle from 'muggle-string'; +import { VirtualFile, buildMappings, buildStacks, resolveCommonLanguageId, toString, track } from '@volar/language-core'; +import { computed } from 'computeds'; 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,7 +48,10 @@ export function computedFiles( for (const { file, snapshot, mappings, codegenStacks } of remain) { embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -66,7 +67,10 @@ export function computedFiles( const { file, snapshot, mappings, codegenStacks } = remain[i]; if (!file.parentFileName) { embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -78,7 +82,10 @@ export function computedFiles( const parent = findParentStructure(file.parentFileName, embeddedFiles); if (parent) { parent.embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -89,12 +96,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; } @@ -128,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) { @@ -174,27 +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) { if (mapping.source !== undefined) { const block = nameToBlock()[mapping.source]; if (block) { - mapping.sourceRange = [ - mapping.sourceRange[0] + block.startTagEnd, - mapping.sourceRange[1] + block.startTagEnd, - ]; + mapping.sourceOffsets = mapping.sourceOffsets.map(offset => offset + block.startTagEnd); } else { // ignore } 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 490c063841..a4661aec48 100644 --- a/packages/language-core/src/virtualFile/computedMappings.ts +++ b/packages/language-core/src/virtualFile/computedMappings.ts @@ -1,16 +1,15 @@ -import { FileRangeCapabilities } from '@volar/language-core'; -import { Mapping, Segment } from '@volar/source-map'; -import * as muggle from 'muggle-string'; -import type * as ts from 'typescript/lib/tsserverlibrary'; -import { Sfc } from '../types'; +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'; export function computedMappings( snapshot: () => ts.IScriptSnapshot, sfc: Sfc ) { return computed(() => { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, FileRangeCapabilities.full]]; + const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, enableAllFeatures({})]]; for (const block of [ sfc.script, sfc.scriptSetup, @@ -19,26 +18,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 { - sourceRange: [start, end], - generatedRange: [start, end], - data: m[3] as FileRangeCapabilities, - }; - }); + 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 001ec2e871..f5e9fa7b27 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 { Mapping, Segment, StackNode } from '@volar/source-map'; +import { Mapping, StackNode, VirtualFile } from '@volar/language-core'; +import { Code } from '../types'; export class VueEmbeddedFile { public parentFileName?: string; - public kind = FileKind.TextFile; - public capabilities: FileCapabilities = {}; - public mirrorBehaviorMappings: Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; + public typescript: VirtualFile['typescript']; + public linkedNavigationMappings: Mapping[] = []; 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 c0c0ab48fb..b60c944ade 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -1,5 +1,4 @@ -import { FileCapabilities, FileKind, 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'; @@ -18,27 +17,23 @@ 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 - capabilities = FileCapabilities.full; - kind = FileKind.TextFile; codegenStacks: Stack[] = []; get embeddedFiles() { return this.getEmbeddedFiles(); } - get mainScriptName() { - let res: string = ''; - forEachEmbeddedFile(this, file => { - if (file.kind === FileKind.TypeScriptHostFile && file.fileName.replace(this.fileName, '').match(jsxReg)) { - res = file.fileName; + get mainTsFile() { + for (const file of forEachEmbeddedFile(this)) { + if (file.typescript && file.id.substring(this.id.length).match(jsxReg)) { + return file; } - }); - return res; + } } get snapshot() { return this._snapshot(); @@ -48,7 +43,8 @@ export class VueFile implements VirtualFile { } constructor( - public fileName: string, + public id: string, + public languageId: string, public initSnapshot: ts.IScriptSnapshot, public vueCompilerOptions: VueCompilerOptions, public plugins: ReturnType[], diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index 7de2260710..5bd6c6c4a9 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -17,7 +17,7 @@ "@vue/language-core": "1.8.26" }, "dependencies": { - "@volar/source-map": "~1.11.1", - "volar-service-pug": "0.0.17" + "@volar/source-map": "2.0.0-alpha.0", + "volar-service-pug": "0.0.18" } } 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/package.json b/packages/language-server/package.json index 7e1b8aa360..27ba49da1d 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -16,9 +16,9 @@ "directory": "packages/language-server" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/language-server": "~1.11.1", - "@volar/typescript": "~1.11.1", + "@volar/language-core": "2.0.0-alpha.0", + "@volar/language-server": "2.0.0-alpha.0", + "@volar/typescript": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "@vue/language-service": "1.8.26", "vscode-languageserver-protocol": "^3.17.5", diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 3cba8789e4..6aa8e8dd71 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -1,30 +1,30 @@ -import * as embedded from '@volar/language-core'; -import { LanguageServerPlugin, Connection } 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, GetDragAndDragImportEditsRequest } 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) { - 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, info) { 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 ?? {}, info?.parsedCommandLine.options ?? {}, vueOptions, options.codegenStack); + config.services = vue.resolveServices(ts, 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 && 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 (typeof ctx?.project.tsConfig === 'string' && ts) { - vueOptions = vue2.createParsedCommandLine(ts, sys, ctx.project.tsConfig).vueOptions; + if (info.configFileName) { + vueOptions = vue2.createParsedCommandLine(ts, sys, info.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, env.uriToFileName(env.workspaceFolder.uri.toString()), info.parsedCommandLine.options).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,57 +84,52 @@ 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)!); - } - }); - - 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); + return nameCasing.convertTagName(ts, languageService.context, params.textDocument.uri, params.casing, envToVueOptions.get(languageService.context.env)!); } }); 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.language.typescript!.configFileName, + langaugeService.context.language.typescript!.projectHost, + envToVueOptions.get(langaugeService.context.env)!, {}, - host.rootPath + '/tsconfig.json.global.vue', + langaugeService.context.language.typescript!.languageServiceHost.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/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-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/package.json b/packages/language-service/package.json index 2622ae73cd..8378e08e14 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -17,29 +17,29 @@ "update-html-data": "node ./scripts/update-html-data.js" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/language-service": "~1.11.1", - "@volar/typescript": "~1.11.1", + "@volar/language-core": "2.0.0-alpha.0", + "@volar/language-service": "2.0.0-alpha.0", + "@volar/typescript": "2.0.0-alpha.0", "@vue/compiler-dom": "^3.3.0", "@vue/language-core": "1.8.26", "@vue/shared": "^3.3.0", "computeds": "^0.0.1", "path-browserify": "^1.0.1", - "volar-service-css": "0.0.17", - "volar-service-emmet": "0.0.17", - "volar-service-html": "0.0.17", - "volar-service-json": "0.0.17", - "volar-service-pug": "0.0.17", - "volar-service-pug-beautify": "0.0.17", - "volar-service-typescript": "0.0.17", - "volar-service-typescript-twoslash-queries": "0.0.17", + "volar-service-css": "0.0.18", + "volar-service-emmet": "0.0.18", + "volar-service-html": "0.0.18", + "volar-service-json": "0.0.18", + "volar-service-pug": "0.0.18", + "volar-service-pug-beautify": "0.0.18", + "volar-service-typescript": "0.0.18", + "volar-service-typescript-twoslash-queries": "0.0.18", "vscode-html-languageservice": "^5.1.0", "vscode-languageserver-textdocument": "^1.0.11" }, "devDependencies": { "@types/node": "latest", "@types/path-browserify": "latest", - "@volar/kit": "~1.11.1", + "@volar/kit": "2.0.0-alpha.0", "vscode-languageserver-protocol": "^3.17.5", "vscode-uri": "^3.0.8" } 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 deleted file mode 100644 index 1c4a9f1050..0000000000 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ /dev/null @@ -1,65 +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 { - - 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!.documents.getVirtualFileByUri(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/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index d64264b907..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.documents.getSourceByUri(uri)?.root; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -21,11 +21,11 @@ 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.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.get(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.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -64,17 +64,17 @@ 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.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.get(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.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), @@ -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/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 5b5a92a1da..89cfb291e4 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 { 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,265 +9,281 @@ 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 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 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( + ts: typeof import('typescript/lib/tsserverlibrary'), + services: Record = {}, + _vueCompilerOptions: Partial = {}, ) { - const originalTsPlugin: Service = services?.typescript ?? TsService.create(); + const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); + const tsService: ServicePlugin = services?.typescript ?? createTsService(ts); services ??= {}; - services.typescript = (ctx: ServiceContext | undefined, modules): ReturnType => { - - const base = typeof originalTsPlugin === 'function' ? originalTsPlugin(ctx, modules) : originalTsPlugin; - - 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) - ); - - // 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; - } + 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; + + const [virtualFile, sourceFile] = ctx.language.files.getVirtualFile(document.uri); + + if (virtualFile && sourceFile) { + + for (const map of ctx.documents.getMaps(virtualFile)) { + + const sourceVirtualFile = ctx.language.files.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; + + if (sourceVirtualFile instanceof VueFile) { + + const isAutoImport = !!map.getSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); + if (isAutoImport) { - // 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); + item.data.__isComponentAutoImport = true; + } + + // 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); + } } } } } } } - } - 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 source = ctx.documents.getVirtualFileByUri(itemData.uri)[1]; - if (source) { - const casing = await getNameCasing(ts, ctx, ctx.env.fileNameToUri(source.fileName), 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.virtualFiles.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); - 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) { + 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; + 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?.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/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 fba00b5f71..09363d0cd9 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -1,79 +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.documents.getVirtualFileByUri(document.uri); - if (!virtualFile?.fileName.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) { - 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); - 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.generatedRange[0]), - end: document.positionAt(mappedRange.generatedRange[1]), - }, - 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 4badb080b4..38f8f4370d 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,70 +1,79 @@ -import { Service } from '@volar/language-service'; -import { VueFile } from '@vue/language-core'; +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 () => { - - const result: vscode.Range[] = []; - - for (const [_, map] of context.documents.getMapsBySourceFileUri(document.uri)?.maps ?? []) { - for (const mapping of map.map.mappings) { - - if (!mapping.data.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.sourceRange[0]), - end: document.positionAt(mapping.sourceRange[1]), - }); + 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 result; - }); - }, - - async resolveReferencesCodeLensLocations(document, range, references) { - - await worker(document.uri, async (vueFile) => { - - const document = context.documents.getDocumentByFileName(vueFile.snapshot, vueFile.fileName); - const offset = document.offsetAt(range.start); - const blocks = [ - vueFile.sfc.script, - vueFile.sfc.scriptSetup, - vueFile.sfc.template, - ...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 - ); - }); - - return references; - }, - }; + return references; + }, + }; - function worker(uri: string, callback: (vueSourceFile: VueFile) => T) { + function worker(uri: string, callback: (vueFile: VirtualFile, sourceFile: SourceFile) => T) { - const [virtualFile] = context!.documents.getVirtualFileByUri(uri); - if (!(virtualFile instanceof VueFile)) - return; + const [virtualFile, sourceFile] = context.language.files.getVirtualFile(uri); + if (!(sourceFile?.virtualFile?.[0] instanceof VueFile) || !sourceFile) + return; - return callback(virtualFile); - } + 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, + })]); + } - forEachEmbeddedFile(vueFile, (embedded) => { - if (embedded.kind === FileKind.TypeScriptHostFile) { - for (const [_, map] of context.documents.getMapsByVirtualFileName(embedded.fileName)) { - 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); - 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!.documents.getVirtualFileByUri(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 1aca2da3e4..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,59 +1,54 @@ -import { Service } from '@volar/language-service'; +import { ServicePluginInstance } from '@volar/language-service'; import type * as vscode from 'vscode-languageserver-protocol'; +import type { ServicePlugin, VueCodeInformation } from '../types'; -const plugin: Service = (context) => { - - if (!context) - return {}; - +export function create(): ServicePlugin { return { - - async provideInlayHints(document, range) { - - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const [file] = context.documents.getVirtualFileByUri(document.uri); - if (file) { - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - for (const mapping of file.mappings) { - - const hint: { - setting: string; - label: string; - tooltip: string; - paddingRight?: boolean; - paddingLeft?: boolean; - } | undefined = (mapping.data as any).__hint; - - if ( - mapping.generatedRange[0] >= start - && mapping.generatedRange[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.generatedRange[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 a9b14dd2c9..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,186 +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