Skip to content

Commit

Permalink
feat: support SFC style CSS variable injection
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Aug 20, 2021
1 parent 4af8f29 commit c5b375d
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 29 deletions.
13 changes: 11 additions & 2 deletions packages/vscode-vue-languageservice/src/generators/script.ts
Expand Up @@ -20,6 +20,7 @@ export function generate(
scriptRanges: ScriptRanges | undefined,
scriptSetupRanges: ScriptSetupRanges | undefined,
getHtmlGen: () => ReturnType<typeof templateGen['generate']> | undefined,
getSfcStyles: () => ReturnType<(typeof import('../use/useSfcStyles'))['useSfcStyles']>['textDocuments']['value'],
) {

const codeGen = createCodeGen<SourceMaps.TsMappingData>();
Expand Down Expand Up @@ -254,6 +255,7 @@ export function generate(
codeGen.addText(`setup() {\n`);
if (lsType === 'script') {
codeGen.addText(`return () => {\n`);
withCssBinds();
writeTemplate();
codeGen.addText(`};\n`);
}
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions 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;
}
11 changes: 10 additions & 1 deletion packages/vscode-vue-languageservice/src/sourceFile.ts
Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -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 ? {
Expand Down Expand Up @@ -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();
Expand All @@ -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,
};

Expand Down
12 changes: 7 additions & 5 deletions packages/vscode-vue-languageservice/src/use/useSfcScriptGen.ts
Expand Up @@ -17,6 +17,7 @@ export function useSfcScriptGen(
scriptAst: Ref<ts.SourceFile | undefined>,
scriptSetupAst: Ref<ts.SourceFile | undefined>,
sfcTemplateCompileResult: ReturnType<(typeof import('./useSfcTemplateCompileResult'))['useSfcTemplateCompileResult']>,
sfcStyles: ReturnType<(typeof import('./useSfcStyles'))['useSfcStyles']>['textDocuments'],
) {

let version = 0;
Expand All @@ -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,
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions packages/vscode-vue-languageservice/src/use/useSfcStyles.ts
Expand Up @@ -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,
Expand All @@ -18,6 +20,7 @@ export function useSfcStyles(
const documents: {
textDocument: TextDocument,
stylesheet: css.Stylesheet | undefined,
binds: TextRange[],
links: {
textDocument: TextDocument,
stylesheet: css.Stylesheet,
Expand All @@ -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,
Expand Down
83 changes: 63 additions & 20 deletions packages/vscode-vue-languageservice/src/use/useSfcTemplateScript.ts
Expand Up @@ -13,6 +13,7 @@ import { SearchTexts } from '../utils/string';
export function useSfcTemplateScript(
getUnreactiveDoc: () => TextDocument,
template: Ref<IDescriptor['template']>,
styles: Ref<IDescriptor['styles']>,
templateScriptData: ITemplateScriptData,
styleDocuments: Ref<{
textDocument: TextDocument;
Expand All @@ -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;
Expand Down Expand Up @@ -64,8 +66,6 @@ export function useSfcTemplateScript(
);
});
const data = computed(() => {
if (!templateCodeGens.value)
return;

const codeGen = createCodeGen<SourceMaps.TsMappingData>();

Expand All @@ -83,18 +83,20 @@ export function useSfcTemplateScript(
codeGen.addText('declare var __VLS_componentProps: __VLS_MapPropsType<typeof __VLS_components>;\n');
codeGen.addText('declare var __VLS_componentEmits: __VLS_MapEmitType<typeof __VLS_components>;\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 */
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion packages/vscode-vue-languageservice/src/utils/sourceMaps.ts
Expand Up @@ -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: {
Expand Down
7 changes: 7 additions & 0 deletions packages/vscode-vue-languageservice/testCases/cssVars.vue
@@ -0,0 +1,7 @@
<script lang="ts" setup>
const foo = 1;
</script>

<style>
.bar { color: v-bind(foo); }
</style>
28 changes: 28 additions & 0 deletions 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: `
<script lang="ts" setup>
const baz = 1;
</script>
<style>
.bar { color: v-bind(baz); }
</style>
`.trim(),
});
@@ -1,5 +1,6 @@
import './cssModule';
import './cssScoped';
import './cssVars';
import './scriptSetup';
import './scriptSetup_component';
import './scriptSetup_prop';
Expand Down

0 comments on commit c5b375d

Please sign in to comment.