diff --git a/packages/vscode-vue-languageservice/src/generators/script.ts b/packages/vscode-vue-languageservice/src/generators/script.ts index 5a4b860a4..2dff16493 100644 --- a/packages/vscode-vue-languageservice/src/generators/script.ts +++ b/packages/vscode-vue-languageservice/src/generators/script.ts @@ -20,6 +20,7 @@ export function generate( scriptRanges: ScriptRanges | undefined, scriptSetupRanges: ScriptSetupRanges | undefined, getHtmlGen: () => ReturnType | undefined, + getSfcStyles: () => ReturnType<(typeof import('../use/useSfcStyles'))['useSfcStyles']>['textDocuments']['value'], ) { const codeGen = createCodeGen(); @@ -254,6 +255,7 @@ export function generate( codeGen.addText(`setup() {\n`); if (lsType === 'script') { codeGen.addText(`return () => {\n`); + withCssBinds(); writeTemplate(); codeGen.addText(`};\n`); } @@ -433,9 +435,16 @@ export function generate( codeGen.addText(`export const __VLS_name = undefined;\n`); } } + function withCssBinds() { + for (const style of getSfcStyles()) { + const docText = style.textDocument.getText(); + for (const cssBind of style.binds) { + const bindText = docText.substring(cssBind.start, cssBind.end); + codeGen.addText(bindText + ';\n'); + } + } + } function writeTemplate() { - if (!scriptSetup) - return; const htmlGen = getHtmlGen(); if (!htmlGen) diff --git a/packages/vscode-vue-languageservice/src/parsers/cssBinds.ts b/packages/vscode-vue-languageservice/src/parsers/cssBinds.ts new file mode 100644 index 000000000..b155534bf --- /dev/null +++ b/packages/vscode-vue-languageservice/src/parsers/cssBinds.ts @@ -0,0 +1,34 @@ +import type * as css from 'vscode-css-languageservice'; +import type { TextRange } from './types'; + +type StylesheetNode = { + children: StylesheetNode[] | undefined, + end: number, + length: number, + offset: number, + parent: Node | null, + type: number, +}; + +export function parse(docText: string, ss: css.Stylesheet) { + const result: TextRange[] = []; + visChild(ss as StylesheetNode); + function visChild(node: StylesheetNode) { + if (node.type === 22) { + const nodeText = docText.substring(node.offset, node.end); + const reg = /^v-bind\s*\(\s*(\S*)\s*\)$/; + const match = nodeText.match(reg); + if (match) { + const matchText = match[1]; + const offset = node.offset + nodeText.lastIndexOf(matchText); + result.push({ start: offset, end: offset + matchText.length }); + } + } + else if (node.children) { + for (let i = 0; i < node.children.length; i++) { + visChild(node.children[i]); + } + } + } + return result; +} diff --git a/packages/vscode-vue-languageservice/src/sourceFile.ts b/packages/vscode-vue-languageservice/src/sourceFile.ts index 9c28974f8..2a050b040 100644 --- a/packages/vscode-vue-languageservice/src/sourceFile.ts +++ b/packages/vscode-vue-languageservice/src/sourceFile.ts @@ -116,6 +116,7 @@ export function createSourceFile( computed(() => sfcScript.ast.value), computed(() => sfcScriptSetup.ast.value), sfcTemplateCompileResult, + computed(() => sfcStyles.textDocuments.value), ); const sfcScriptForScriptLs = useSfcScriptGen('script', context.modules.typescript, @@ -125,6 +126,7 @@ export function createSourceFile( computed(() => sfcScript.ast.value), computed(() => sfcScriptSetup.ast.value), sfcTemplateCompileResult, + computed(() => sfcStyles.textDocuments.value), ); const sfcEntryForTemplateLs = useSfcEntryForTemplateLs( untrack(() => document.value), @@ -136,11 +138,13 @@ export function createSourceFile( const sfcTemplateScript = useSfcTemplateScript( untrack(() => document.value), computed(() => descriptor.template), + computed(() => descriptor.styles), templateScriptData, sfcStyles.textDocuments, sfcStyles.sourceMaps, sfcTemplateData, sfcTemplateCompileResult, + computed(() => sfcStyles.textDocuments.value), context, ); const sfcRefSugarRanges = computed(() => (sfcScriptSetup.ast.value ? { @@ -244,6 +248,8 @@ export function createSourceFile( function update(newDocument: TextDocument) { const parsedSfc = vueSfc.parse(newDocument.getText(), { sourceMap: false, ignoreEmpty: false }); const newDescriptor = parsedSfc.descriptor; + const scriptLang_1 = sfcScriptForScriptLs.textDocument.value.languageId; + const scriptText_1 = sfcScriptForScriptLs.textDocument.value.getText(); const templateScriptVersion_1 = sfcTemplateScript.textDocument.value?.version; updateSfcErrors(); @@ -257,10 +263,13 @@ export function createSourceFile( version.value = newDocument.version; sfcTemplateScript.update(); // TODO + + const scriptLang_2 = sfcScriptForScriptLs.textDocument.value.languageId; + const scriptText_2 = sfcScriptForScriptLs.textDocument.value.getText(); const templateScriptVersion_2 = sfcTemplateScript.textDocument.value?.version; return { - scriptUpdated: lastUpdated.script || lastUpdated.scriptSetup, + scriptUpdated: scriptLang_1 !== scriptLang_2 || scriptText_1 !== scriptText_2, // TODO templateScriptUpdated: templateScriptVersion_1 !== templateScriptVersion_2, }; diff --git a/packages/vscode-vue-languageservice/src/use/useSfcScriptGen.ts b/packages/vscode-vue-languageservice/src/use/useSfcScriptGen.ts index b4c3bc1b2..cdebc5871 100644 --- a/packages/vscode-vue-languageservice/src/use/useSfcScriptGen.ts +++ b/packages/vscode-vue-languageservice/src/use/useSfcScriptGen.ts @@ -17,6 +17,7 @@ export function useSfcScriptGen( scriptAst: Ref, scriptSetupAst: Ref, sfcTemplateCompileResult: ReturnType<(typeof import('./useSfcTemplateCompileResult'))['useSfcTemplateCompileResult']>, + sfcStyles: ReturnType<(typeof import('./useSfcStyles'))['useSfcStyles']>['textDocuments'], ) { let version = 0; @@ -32,6 +33,11 @@ export function useSfcScriptGen( ? parseScriptSetupRanges(ts, scriptSetupAst.value) : undefined ); + const htmlGen = computed(() => { + if (sfcTemplateCompileResult.value?.ast) { + return templateGen.generate(sfcTemplateCompileResult.value.ast); + } + }); const codeGen = computed(() => genScript( lsType, @@ -41,13 +47,9 @@ export function useSfcScriptGen( scriptRanges.value, scriptSetupRanges.value, () => htmlGen.value, + () => sfcStyles.value, ) ); - const htmlGen = computed(() => { - if (sfcTemplateCompileResult.value?.ast) { - return templateGen.generate(sfcTemplateCompileResult.value.ast); - } - }) const lang = computed(() => { return !script.value && !scriptSetup.value ? 'ts' : scriptSetup.value && scriptSetup.value.lang !== 'js' ? shared.getValidScriptSyntax(scriptSetup.value.lang) diff --git a/packages/vscode-vue-languageservice/src/use/useSfcStyles.ts b/packages/vscode-vue-languageservice/src/use/useSfcStyles.ts index a444e0b3e..6126bc794 100644 --- a/packages/vscode-vue-languageservice/src/use/useSfcStyles.ts +++ b/packages/vscode-vue-languageservice/src/use/useSfcStyles.ts @@ -5,6 +5,8 @@ import * as SourceMaps from '../utils/sourceMaps'; import type * as css from 'vscode-css-languageservice'; import * as shared from '@volar/shared'; import * as upath from 'upath'; +import { TextRange } from '../parsers/types'; +import { parse as parseCssBinds } from '../parsers/cssBinds'; export function useSfcStyles( context: LanguageServiceContext, @@ -18,6 +20,7 @@ export function useSfcStyles( const documents: { textDocument: TextDocument, stylesheet: css.Stylesheet | undefined, + binds: TextRange[], links: { textDocument: TextDocument, stylesheet: css.Stylesheet, @@ -44,6 +47,7 @@ export function useSfcStyles( documents.push({ textDocument: document, stylesheet, + binds: stylesheet ? parseCssBinds(content, stylesheet) : [], links: linkStyles, module: style.module, scoped: style.scoped, diff --git a/packages/vscode-vue-languageservice/src/use/useSfcTemplateScript.ts b/packages/vscode-vue-languageservice/src/use/useSfcTemplateScript.ts index 90ab72563..a40e577f1 100644 --- a/packages/vscode-vue-languageservice/src/use/useSfcTemplateScript.ts +++ b/packages/vscode-vue-languageservice/src/use/useSfcTemplateScript.ts @@ -13,6 +13,7 @@ import { SearchTexts } from '../utils/string'; export function useSfcTemplateScript( getUnreactiveDoc: () => TextDocument, template: Ref, + styles: Ref, templateScriptData: ITemplateScriptData, styleDocuments: Ref<{ textDocument: TextDocument; @@ -31,6 +32,7 @@ export function useSfcTemplateScript( htmlToTemplate: (start: number, end: number) => number | undefined, } | undefined>, sfcTemplateCompileResult: ReturnType<(typeof import('./useSfcTemplateCompileResult'))['useSfcTemplateCompileResult']>, + sfcStyles: ReturnType<(typeof import('./useSfcStyles'))['useSfcStyles']>['textDocuments'], context: LanguageServiceContext, ) { let version = 0; @@ -64,8 +66,6 @@ export function useSfcTemplateScript( ); }); const data = computed(() => { - if (!templateCodeGens.value) - return; const codeGen = createCodeGen(); @@ -83,18 +83,20 @@ export function useSfcTemplateScript( codeGen.addText('declare var __VLS_componentProps: __VLS_MapPropsType;\n'); codeGen.addText('declare var __VLS_componentEmits: __VLS_MapEmitType;\n'); - /* Completion */ - codeGen.addText(`({} as __VLS_GlobalAttrs).${SearchTexts.GlobalAttrs};\n`); + if (templateCodeGens.value) { + /* Completion */ + codeGen.addText(`({} as __VLS_GlobalAttrs).${SearchTexts.GlobalAttrs};\n`); - codeGen.addText('/* Completion: Emits */\n'); - for (const name of templateCodeGens.value.usedComponents) { - codeGen.addText(`// @ts-ignore\n`); - codeGen.addText(`__VLS_componentEmits['${name}']('');\n`); // TODO - } - codeGen.addText('/* Completion: Props */\n'); - for (const name of templateCodeGens.value.usedComponents) { - codeGen.addText(`// @ts-ignore\n`); - codeGen.addText(`__VLS_componentPropsBase['${name}'][''];\n`); // TODO + codeGen.addText('/* Completion: Emits */\n'); + for (const name of templateCodeGens.value.usedComponents) { + codeGen.addText(`// @ts-ignore\n`); + codeGen.addText(`__VLS_componentEmits['${name}']('');\n`); // TODO + } + codeGen.addText('/* Completion: Props */\n'); + for (const name of templateCodeGens.value.usedComponents) { + codeGen.addText(`// @ts-ignore\n`); + codeGen.addText(`__VLS_componentPropsBase['${name}'][''];\n`); // TODO + } } /* CSS Module */ @@ -116,7 +118,12 @@ export function useSfcTemplateScript( codeGen.addText(`/* Props */\n`); const ctxMappings = writeProps(); - margeCodeGen(codeGen as CodeGen, templateCodeGens.value.codeGen as CodeGen); + codeGen.addText(`/* CSS variable injection */\n`); + writeCssVars(); + + if (templateCodeGens.value) { + margeCodeGen(codeGen as CodeGen, templateCodeGens.value.codeGen as CodeGen); + } return { ...codeGen, @@ -237,9 +244,37 @@ export function useSfcTemplateScript( } return mappings; } + function writeCssVars() { + for (let i = 0; i < sfcStyles.value.length; i++) { + const style = sfcStyles.value[i]; + const docText = style.textDocument.getText(); + for (const cssBind of style.binds) { + const bindText = docText.substring(cssBind.start, cssBind.end); + codeGen.addCode( + bindText, + cssBind, + SourceMaps.Mode.Offset, + { + vueTag: 'style', + vueTagIndex: i, + capabilities: { + basic: true, + references: true, + definitions: true, + diagnostic: true, + rename: true, + completion: true, + semanticTokens: true, + }, + }, + ); + codeGen.addText(';\n'); + } + } + } }); const sourceMap = computed(() => { - if (data.value && textDoc.value && template.value) { + if (textDoc.value) { const vueDoc = getUnreactiveDoc(); const sourceMap = new SourceMaps.TsSourceMap( vueDoc, @@ -352,18 +387,26 @@ export function useSfcTemplateScript( }; function parseMappingSourceRange(data: any, range: SourceMaps.Range) { + if (data.vueTag === 'style' && data.vueTagIndex !== undefined) { + return { + start: styles.value[data.vueTagIndex].loc.start + range.start, + end: styles.value[data.vueTagIndex].loc.start + range.end, + }; + } const templateOffset = template.value?.loc.start ?? 0; return { - start: range.start + templateOffset, - end: range.end + templateOffset, + start: templateOffset + range.start, + end: templateOffset + range.end, }; } function update() { - if (data.value?.getText() !== textDoc.value?.getText()) { - if (data.value && templateCodeGens.value) { + if (data.value.getText() !== textDoc.value?.getText()) { + if (data.value) { const _version = version++; textDoc.value = TextDocument.create(vueUri + '.__VLS_template.ts', shared.syntaxToLanguageId('ts'), _version, data.value.getText()); - formatTextDoc.value = TextDocument.create(vueUri + '.__VLS_template.format.ts', shared.syntaxToLanguageId('ts'), _version, templateCodeGens.value.formatCodeGen.getText()); + formatTextDoc.value = templateCodeGens.value + ? TextDocument.create(vueUri + '.__VLS_template.format.ts', shared.syntaxToLanguageId('ts'), _version, templateCodeGens.value.formatCodeGen.getText()) + : undefined; const sourceMap = new SourceMaps.TeleportSourceMap(textDoc.value, true); for (const maped of data.value.ctxMappings) { diff --git a/packages/vscode-vue-languageservice/src/utils/sourceMaps.ts b/packages/vscode-vue-languageservice/src/utils/sourceMaps.ts index 03ef6c8cf..cdd5b12dd 100644 --- a/packages/vscode-vue-languageservice/src/utils/sourceMaps.ts +++ b/packages/vscode-vue-languageservice/src/utils/sourceMaps.ts @@ -7,7 +7,8 @@ import type { JSONDocument } from 'vscode-json-languageservice'; import * as SourceMaps from '@volar/source-map'; export interface TsMappingData { - vueTag: 'sfc' | 'template' | 'script' | 'scriptSetup' | 'style' | 'scriptSrc', + vueTag: 'sfc' | 'template' | 'script' | 'scriptSetup' | 'scriptSrc' | 'style', + vueTagIndex?: number, beforeRename?: (newName: string) => string, doRename?: (oldName: string, newName: string) => string, capabilities: { diff --git a/packages/vscode-vue-languageservice/testCases/cssVars.vue b/packages/vscode-vue-languageservice/testCases/cssVars.vue new file mode 100644 index 000000000..cfb26878e --- /dev/null +++ b/packages/vscode-vue-languageservice/testCases/cssVars.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/vscode-vue-languageservice/tests/rename/cssVars.ts b/packages/vscode-vue-languageservice/tests/rename/cssVars.ts new file mode 100644 index 000000000..d0e7f8456 --- /dev/null +++ b/packages/vscode-vue-languageservice/tests/rename/cssVars.ts @@ -0,0 +1,28 @@ +import * as path from 'upath'; +import { Position } from 'vscode-languageserver/node'; +import { defineRename } from '../utils/defineRename'; + +defineRename({ + fileName: path.resolve(__dirname, '../../testCases/cssVars.vue'), + actions: [ + { + position: Position.create(1, 6), + newName: 'baz', + length: 4, + }, + { + position: Position.create(5, 21), + newName: 'baz', + length: 4, + }, + ], + result: ` + + + +`.trim(), +}); diff --git a/packages/vscode-vue-languageservice/tests/rename/index.spec.ts b/packages/vscode-vue-languageservice/tests/rename/index.spec.ts index 429e7a258..ee99cb69e 100644 --- a/packages/vscode-vue-languageservice/tests/rename/index.spec.ts +++ b/packages/vscode-vue-languageservice/tests/rename/index.spec.ts @@ -1,5 +1,6 @@ import './cssModule'; import './cssScoped'; +import './cssVars'; import './scriptSetup'; import './scriptSetup_component'; import './scriptSetup_prop';