Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language-core): typed directive arg and modifiers #4813

Merged
merged 13 commits into from
Oct 23, 2024
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