Skip to content

Commit

Permalink
feat(language-core): typed directive arg and modifiers (#4813)
Browse files Browse the repository at this point in the history
Co-authored-by: Johnson Chu <[email protected]>
  • Loading branch information
KazariEX and johnsoncodehk authored Oct 23, 2024
1 parent acddb02 commit 1e04fc9
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 154 deletions.
8 changes: 5 additions & 3 deletions packages/language-core/lib/codegen/globalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates
function __VLS_getSlotParams<T>(slot: T): Parameters<__VLS_PickNotAny<NonNullable<T>, (...args: any[]) => any>>;
// @ts-ignore
function __VLS_getSlotParam<T>(slot: T): Parameters<__VLS_PickNotAny<NonNullable<T>, (...args: any[]) => any>>[0];
function __VLS_directiveAsFunction<T extends import('${lib}').Directive>(dir: T): T extends (...args: any) => any
? T | __VLS_unknownDirective
: NonNullable<(T & Record<string, __VLS_unknownDirective>)['created' | 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated' | 'beforeUnmount' | 'unmounted']>;
function __VLS_asFunctionalDirective<T>(dir: T): T extends import('${lib}').ObjectDirective
? NonNullable<T['created' | 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated' | 'beforeUnmount' | 'unmounted']>
: T extends (...args: any) => any
? T
: __VLS_unknownDirective;
function __VLS_withScope<T, K>(ctx: T, scope: K): ctx is T & K;
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
function __VLS_nonNullable<T>(t: T): T extends null | undefined ? never : T;
Expand Down
223 changes: 149 additions & 74 deletions packages/language-core/lib/codegen/template/elementDirectives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { generateCamelized } from './camelized';
import type { TemplateCodegenContext } from './context';
import type { TemplateCodegenOptions } from './index';
import { generateInterpolation } from './interpolation';
import { generateObjectProperty } from './objectProperty';
import { generateStringLiteralKey } from './stringLiteralKey';

export function* generateElementDirectives(
options: TemplateCodegenOptions,
Expand All @@ -15,85 +17,158 @@ export function* generateElementDirectives(
): Generator<Code> {
for (const prop of node.props) {
if (
prop.type === CompilerDOM.NodeTypes.DIRECTIVE
&& prop.name !== 'slot'
&& prop.name !== 'on'
&& prop.name !== 'model'
&& prop.name !== 'bind'
&& prop.name !== 'scope'
&& prop.name !== 'data'
prop.type !== CompilerDOM.NodeTypes.DIRECTIVE
|| prop.name === 'slot'
|| prop.name === 'on'
|| prop.name === 'model'
|| prop.name === 'bind'
|| prop.name === 'scope'
|| prop.name === 'data'
) {
ctx.accessExternalVariable(camelize('v-' + prop.name), prop.loc.start.offset);
continue;
}
ctx.accessExternalVariable(camelize('v-' + prop.name), prop.loc.start.offset);

yield* wrapWith(
prop.loc.start.offset,
prop.loc.end.offset,
ctx.codeFeatures.verification,
`__VLS_asFunctionalDirective(`,
...generateIdentifier(ctx, prop),
`)(null!, { ...__VLS_directiveBindingRestFields, `,
...generateArg(options, ctx, prop),
...generateModifiers(options, ctx, prop),
...generateValue(options, ctx, prop),
`}, null!, null!)`
);
yield endOfLine;
}
}

if (prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic) {
yield* generateInterpolation(
options,
ctx,
prop.arg.content,
prop.arg.loc,
prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content),
ctx.codeFeatures.all,
'(',
')'
);
yield endOfLine;
function* generateIdentifier(
ctx: TemplateCodegenContext,
prop: CompilerDOM.DirectiveNode
): Generator<Code> {
const rawName = 'v-' + prop.name;
yield* wrapWith(
prop.loc.start.offset,
prop.loc.start.offset + rawName.length,
ctx.codeFeatures.verification,
`__VLS_directives.`,
...generateCamelized(
rawName,
prop.loc.start.offset,
{
...ctx.codeFeatures.all,
verification: false,
completion: {
// fix https://github.com/vuejs/language-tools/issues/1905
isAdditional: true,
},
navigation: {
resolveRenameNewName: camelize,
resolveRenameEditText: getPropRenameApply(prop.name),
},
}
)
);
}

yield* wrapWith(
prop.loc.start.offset,
prop.loc.end.offset,
ctx.codeFeatures.verification,
`__VLS_directiveAsFunction(__VLS_directives.`,
...generateCamelized(
'v-' + prop.name,
prop.loc.start.offset,
{
...ctx.codeFeatures.all,
verification: false,
completion: {
// fix https://github.com/vuejs/language-tools/issues/1905
isAdditional: true,
},
navigation: {
resolveRenameNewName: camelize,
resolveRenameEditText: getPropRenameApply(prop.name),
},
}
),
`)(null!, { ...__VLS_directiveBindingRestFields, `,
...(
prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION
? [
...wrapWith(
prop.exp.loc.start.offset,
prop.exp.loc.end.offset,
ctx.codeFeatures.verification,
'value'
),
': ',
...wrapWith(
prop.exp.loc.start.offset,
prop.exp.loc.end.offset,
ctx.codeFeatures.verification,
...generateInterpolation(
options,
ctx,
prop.exp.content,
prop.exp.loc,
prop.exp.loc.start.offset,
ctx.codeFeatures.all,
'(',
')'
)
)
]
: [`undefined`]
),
`}, null!, null!)`
);
yield endOfLine;
}
function* generateArg(
options: TemplateCodegenOptions,
ctx: TemplateCodegenContext,
prop: CompilerDOM.DirectiveNode
): Generator<Code> {
const { arg } = prop;
if (arg?.type !== CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) {
return;
}

const startOffset = arg.loc.start.offset + arg.loc.source.indexOf(arg.content);

yield* wrapWith(
startOffset,
startOffset + arg.content.length,
ctx.codeFeatures.verification,
'arg'
);
yield ': ';
if (arg.isStatic) {
yield* generateStringLiteralKey(
arg.content,
startOffset,
ctx.codeFeatures.withoutHighlight
);
}
else {
yield* generateInterpolation(
options,
ctx,
arg.content,
arg.loc,
startOffset,
ctx.codeFeatures.all,
'(',
')'
);
}
yield ', ';
}

function* generateModifiers(
options: TemplateCodegenOptions,
ctx: TemplateCodegenContext,
prop: CompilerDOM.DirectiveNode
): Generator<Code> {
if (options.vueCompilerOptions.target < 3.5) {
return;
}

yield 'modifiers: { ';
for (const mod of prop.modifiers) {
yield* generateObjectProperty(
options,
ctx,
mod.content,
mod.loc.start.offset,
ctx.codeFeatures.withoutHighlight
);
yield ': true, ';
}
yield '}, ';
}

function* generateValue(
options: TemplateCodegenOptions,
ctx: TemplateCodegenContext,
prop: CompilerDOM.DirectiveNode
): Generator<Code> {
if (prop.exp?.type !== CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) {
return;
}

yield* wrapWith(
prop.exp.loc.start.offset,
prop.exp.loc.end.offset,
ctx.codeFeatures.verification,
'value'
);
yield ': ';
yield* wrapWith(
prop.exp.loc.start.offset,
prop.exp.loc.end.offset,
ctx.codeFeatures.verification,
...generateInterpolation(
options,
ctx,
prop.exp.content,
prop.exp.loc,
prop.exp.loc.start.offset,
ctx.codeFeatures.all,
'(',
')'
)
);
}

function getPropRenameApply(oldName: string) {
Expand Down
6 changes: 3 additions & 3 deletions packages/language-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
},
"dependencies": {
"@volar/language-core": "~2.4.1",
"@vue/compiler-dom": "^3.5.2",
"@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.2",
"@vue/shared": "^3.5.0",
"alien-signals": "^0.2.0",
"minimatch": "^9.0.3",
"muggle-string": "^0.4.1",
Expand All @@ -27,7 +27,7 @@
"@types/node": "latest",
"@types/path-browserify": "^1.0.1",
"@volar/typescript": "~2.4.1",
"@vue/compiler-sfc": "^3.5.2"
"@vue/compiler-sfc": "^3.5.0"
},
"peerDependencies": {
"typescript": "*"
Expand Down
22 changes: 22 additions & 0 deletions packages/language-server/tests/completions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@ describe('Completions', async () => {
await requestCompletionItem('fixture.vue', 'vue', `<template><div v-p|></div></template>`, 'v-pre');
});

// FIXME:
it.skip('Directive Modifiers', async () => {
expect(
(await requestCompletionList('fixture.vue', 'vue', `
<template>
<div v-foo.|></div>
</template>
<script setup lang="ts">
import type { FunctionDirective } from 'vue';
let vFoo!: FunctionDirective<any, any, 'attr' | 'prop'>;
</script>
`)).items.map(item => item.label)
).toMatchInlineSnapshot(`
[
"attr",
"prop"
]
`);
});

it('$event argument', async () => {
await requestCompletionItem('fixture.vue', 'vue', `<template><div @click="console.log($eve|)"></div></template>`, 'event');
});
Expand Down
4 changes: 2 additions & 2 deletions packages/language-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
"@volar/language-core": "~2.4.1",
"@volar/language-service": "~2.4.1",
"@volar/typescript": "~2.4.1",
"@vue/compiler-dom": "^3.4.0",
"@vue/compiler-dom": "^3.5.0",
"@vue/language-core": "2.1.6",
"@vue/shared": "^3.4.0",
"@vue/shared": "^3.5.0",
"@vue/typescript-plugin": "2.1.6",
"alien-signals": "^0.2.0",
"path-browserify": "^1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": {
"@volar/typescript": "~2.4.1",
"@vue/language-core": "2.1.6",
"@vue/shared": "^3.4.0"
"@vue/shared": "^3.5.0"
},
"devDependencies": {
"@types/node": "latest"
Expand Down
Loading

0 comments on commit 1e04fc9

Please sign in to comment.