Skip to content

Custom event auto complete #2558

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

Merged
merged 13 commits into from
Jan 21, 2021
90 changes: 90 additions & 0 deletions server/src/modes/script/componentInfo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import _ from 'lodash';
import type ts from 'typescript';
import { BasicComponentInfo } from '../../config';
import { RuntimeLibrary } from '../../services/dependencyService';
import {
VueFileInfo,
EmitInfo,
PropInfo,
ComputedInfo,
DataInfo,
Expand Down Expand Up @@ -87,6 +89,7 @@ export function analyzeDefaultExportExpr(
const defaultExportType = checker.getTypeAtLocation(defaultExportNode);

const insertInOptionAPIPos = getInsertInOptionAPIPos(tsModule, defaultExportType, checker);
const emits = getEmits(tsModule, defaultExportType, checker);
const props = getProps(tsModule, defaultExportType, checker);
const data = getData(tsModule, defaultExportType, checker);
const computed = getComputed(tsModule, defaultExportType, checker);
Expand All @@ -95,6 +98,7 @@ export function analyzeDefaultExportExpr(
return {
componentInfo: {
insertInOptionAPIPos,
emits,
props,
data,
computed,
Expand Down Expand Up @@ -137,6 +141,92 @@ function getInsertInOptionAPIPos(
return undefined;
}

function getEmits(
tsModule: RuntimeLibrary['typescript'],
defaultExportType: ts.Type,
checker: ts.TypeChecker
): EmitInfo[] | undefined {
const result: EmitInfo[] = getClassAndObjectInfo(tsModule, defaultExportType, checker, getClassEmits, getObjectEmits);

return result.length === 0 ? undefined : result;

function getEmitValidatorInfo(propertyValue: ts.Node): { hasValidator: boolean; typeString?: string } {
/**
* case `foo: null`
*/
if (propertyValue.kind === tsModule.SyntaxKind.NullKeyword) {
return { hasValidator: false };
}

return { hasValidator: true };
}

function getClassEmits(type: ts.Type) {
return undefined;
}

function getObjectEmits(type: ts.Type) {
const emitsSymbol = checker.getPropertyOfType(type, 'emits');
if (!emitsSymbol || !emitsSymbol.valueDeclaration) {
return undefined;
}

const emitsDeclaration = getLastChild(emitsSymbol.valueDeclaration);
if (!emitsDeclaration) {
return undefined;
}

/**
* Plain array emits like `emits: ['foo', 'bar']`
*/
if (emitsDeclaration.kind === tsModule.SyntaxKind.ArrayLiteralExpression) {
return (emitsDeclaration as ts.ArrayLiteralExpression).elements
.filter(expr => expr.kind === tsModule.SyntaxKind.StringLiteral)
.map(expr => {
return {
name: (expr as ts.StringLiteral).text,
hasValidator: false,
documentation: `\`\`\`js\n${formatJSLikeDocumentation(
emitsDeclaration.parent.getFullText().trim()
)}\n\`\`\`\n`
};
});
}

/**
* Object literal emits like
* ```
* {
* emits: {
* foo: () => true,
* bar: (arg1: string, arg2: number) => arg1.startsWith('s') || arg2 > 0,
* car: null
* }
* }
* ```
*/
if (emitsDeclaration.kind === tsModule.SyntaxKind.ObjectLiteralExpression) {
const emitsType = checker.getTypeOfSymbolAtLocation(emitsSymbol, emitsDeclaration);

return checker.getPropertiesOfType(emitsType).map(s => {
const node = getNodeFromSymbol(s);
const status =
node !== undefined && tsModule.isPropertyAssignment(node)
? getEmitValidatorInfo(node.initializer)
: { hasValidator: false };

return {
name: s.name,
...status,
documentation: buildDocumentation(tsModule, s, checker)
};
});
}

return undefined;
}
}

function getProps(
tsModule: RuntimeLibrary['typescript'],
defaultExportType: ts.Type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ export function getComponentInfoTagProvider(childComponents: ChildComponent[]):
const tagSet: ITagSet = {};

for (const cc of childComponents) {
const props: Attribute[] = [];
if (cc.info && cc.info.componentInfo.props) {
cc.info.componentInfo.props.forEach(p => {
props.push(genAttribute(`:${p.name}`, undefined, { kind: 'markdown', value: p.documentation || '' }));
const attributes: Attribute[] = [];
if (cc.info) {
cc.info.componentInfo.props?.forEach(p => {
attributes.push(genAttribute(`:${p.name}`, undefined, { kind: 'markdown', value: p.documentation || '' }));
});
cc.info.componentInfo.emits?.forEach(e => {
attributes.push(genAttribute(`@${e.name}`, 'event', { kind: 'markdown', value: e.documentation || '' }));
});
}
tagSet[cc.name] = new HTMLTagSpecification(
{
kind: 'markdown',
value: cc.documentation || ''
},
props
attributes
);
}

Expand Down
17 changes: 17 additions & 0 deletions server/src/services/vueInfoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ComponentInfo {
* foo: String
* }
*/
emits?: EmitInfo[];
props?: PropInfo[];
data?: DataInfo[];
computed?: ComputedInfo[];
Expand All @@ -50,6 +51,22 @@ export interface ChildComponent {
info?: VueFileInfo;
}

export interface EmitInfo {
name: string;
/**
* `true` if
* emits: {
* foo: (...) => {...}
* }
*
* `false` if
* - `emits: ['foo']`
*
*/
hasValidator: boolean;
documentation?: string;
}

export interface PropInfo {
name: string;
/**
Expand Down
20 changes: 20 additions & 0 deletions test/vue3/features/completion/interpolation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CompletionItem, CompletionItemKind, MarkdownString } from 'vscode';
import { position } from '../../../util';
import { testCompletion, testNoSuchCompletion } from '../../../completionHelper';
import { getDocUri } from '../../path';

describe('Should autocomplete interpolation for <template>', () => {
const parentTemplateDocUri = getDocUri('completion/interpolation/Parent.vue');

describe('Should complete emits', () => {
it(`completes child component's emits`, async () => {
await testCompletion(parentTemplateDocUri, position(1, 10), [
{
label: '@foo',
kind: CompletionItemKind.Function,
documentation: new MarkdownString('My foo').appendCodeblock(`foo: () => true`, 'js')
}
]);
});
});
});
20 changes: 20 additions & 0 deletions test/vue3/features/completion/interpolationClassComponent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CompletionItem, CompletionItemKind, MarkdownString } from 'vscode';
import { position } from '../../../util';
import { getDocUri } from '../../path';
import { testCompletion, testNoSuchCompletion } from '../../../completionHelper';

describe('Should autocomplete interpolation for <template> in class component', () => {
const parentTemplateDocUri = getDocUri('completion/interpolation/classComponent/Parent.vue');

describe('Should complete emits', () => {
it(`completes child component's emits`, async () => {
await testCompletion(parentTemplateDocUri, position(1, 16), [
{
label: '@foo',
kind: CompletionItemKind.Function,
documentation: new MarkdownString('My foo').appendCodeblock(`foo: () => true`, 'js')
}
]);
});
});
});
17 changes: 17 additions & 0 deletions test/vue3/fixture/completion/interpolation/Basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div></div>
</template>

<script>
/**
* My basic tag
*/
export default {
emits: {
/**
* My foo
*/
foo: () => true
}
};
</script>
13 changes: 13 additions & 0 deletions test/vue3/fixture/completion/interpolation/Parent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<basic @></basic>
</template>

<script>
import Basic from './Basic.vue'

export default {
components: {
Basic
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div></div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

/**
* My basic tag
*/
@Component({
emits: {
/**
* My foo
*/
foo: () => true
}
})
export default class BasicClass extends Vue {
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<basic-class @></basic-class>
</template>

<script>
import Vue from 'vue'
import BasicClass from './Child.vue'
import Component from 'vue-class-component'

@Component({
components: {
BasicClass
}
})
export default class ParentClass extends Vue {
}
</script>