From 449e7c609e2ca986ddfa4eac0edeb3e4bad1f0ab Mon Sep 17 00:00:00 2001 From: Pine Wu Date: Sat, 8 Aug 2020 00:31:09 +0800 Subject: [PATCH] Fix #2133 --- .../src/modes/template/tagProviders/common.ts | 4 +- .../modes/template/tagProviders/routerTags.ts | 43 ++++++--- .../modes/template/tagProviders/vueTags.ts | 95 ++++++++++++------- test/completionHelper.ts | 31 +++++- .../features/completion/basic.test.ts | 30 +++++- .../componentData/fixture/completion/Link.vue | 6 ++ .../fixture/completion/VueRouter.vue | 1 + test/componentData/fixture/package.json | 2 +- test/componentData/fixture/yarn.lock | 8 +- 9 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 test/componentData/fixture/completion/Link.vue diff --git a/server/src/modes/template/tagProviders/common.ts b/server/src/modes/template/tagProviders/common.ts index 22d006afe2..a3dea779c9 100644 --- a/server/src/modes/template/tagProviders/common.ts +++ b/server/src/modes/template/tagProviders/common.ts @@ -44,7 +44,7 @@ export interface ITagSet { } export class HTMLTagSpecification { - constructor(public label: string | MarkupContent, public attributes: Attribute[] = []) {} + constructor(public documentation: string | MarkupContent, public attributes: Attribute[] = []) {} } export interface IValueSets { @@ -57,7 +57,7 @@ export function getSameTagInSet(tagSet: Record, tag: string): T | export function collectTagsDefault(collector: TagCollector, tagSet: ITagSet): void { for (const tag in tagSet) { - collector(tag, tagSet[tag].label); + collector(tag, tagSet[tag].documentation); } } diff --git a/server/src/modes/template/tagProviders/routerTags.ts b/server/src/modes/template/tagProviders/routerTags.ts index 948f777600..4e6c5160c8 100644 --- a/server/src/modes/template/tagProviders/routerTags.ts +++ b/server/src/modes/template/tagProviders/routerTags.ts @@ -12,35 +12,52 @@ import { const routerTags = { 'router-link': new HTMLTagSpecification( - 'Link to navigate user. The target location is specified with the to prop.\n\n', + 'Link to navigate user. The target location is specified with the to prop.\n\n[API Reference](https://router.vuejs.org/api/#router-link)', [ genAttribute( 'to', undefined, - 'The target route of the link. It can be either a string or a location descriptor object.' + 'The target route of the link. It can be either a string or a location descriptor object.\n\n[API Reference](https://router.vuejs.org/api/#to)' ), genAttribute( 'replace', undefined, - 'Setting replace prop will call router.replace() instead of router.push() when clicked, so the navigation will not leave a history record.' + 'Setting replace prop will call `router.replace()` instead of `router.push()` when clicked, so the navigation will not leave a history record.\n\n[API Reference](https://router.vuejs.org/api/#replace)' ), genAttribute( 'append', 'v', - 'Setting append prop always appends the relative path to the current path. For example, assuming we are navigating from /a to a relative link b, without append we will end up at /b, but with append we will end up at /a/b.' + 'Setting append prop always appends the relative path to the current path. For example, assuming we are navigating from /a to a relative link b, without append we will end up at /b, but with append we will end up at /a/b.\n\n[API Reference](https://router.vuejs.org/api/#append)' ), genAttribute( 'tag', undefined, - 'Specify which tag to render to, and it will still listen to click events for navigation.' + 'Specify which tag to render to, and it will still listen to click events for navigation.\n\n[API Reference](https://router.vuejs.org/api/#tag)' + ), + genAttribute( + 'active-class', + undefined, + 'Configure the active CSS class applied when the link is active.\n\n[API Reference](https://router.vuejs.org/api/#active-class)' + ), + genAttribute( + 'exact', + 'v', + 'Force the link into "exact match mode".\n\n[API Reference](https://router.vuejs.org/api/#exact)' + ), + genAttribute( + 'event', + undefined, + 'Specify the event(s) that can trigger the link navigation.\n\n[API Reference](https://router.vuejs.org/api/#event)' ), - genAttribute('active-class', undefined, 'Configure the active CSS class applied when the link is active.'), - genAttribute('exact', 'v', 'Force the link into "exact match mode".'), - genAttribute('event', undefined, 'Specify the event(s) that can trigger the link navigation.'), genAttribute( 'exact-active-class', undefined, - 'Configure the active CSS class applied when the link is active with exact match.' + 'Configure the active CSS class applied when the link is active with exact match.\n\n[API Reference](https://router.vuejs.org/api/#exact-active-class)' + ), + genAttribute( + 'aria-current-value', + 'ariaCurrentType', + 'Configure the value of `aria-current` when the link is active with exact match. It must be one of the [allowed values for `aria-current`](https://www.w3.org/TR/wai-aria-1.2/#aria-current) in the ARIA spec. In most cases, the default of `page` should be the best fit.\n\n[API Reference](https://router.vuejs.org/api/#aria-current-value)' ) ] ), @@ -50,12 +67,16 @@ const routerTags = { genAttribute( 'name', undefined, - "When a has a name, it will render the component with the corresponding name in the matched route record's components option" + "When a `` has a name, it will render the component with the corresponding name in the matched route record's components option.\n\n[API Reference](https://router.vuejs.org/api/#to)" ) ] ) }; +const valueSets = { + ariaCurrentType: ['page', 'step', 'location', 'date', 'time'] +}; + export function getRouterTagProvider(): IHTMLTagProvider { return { getId: () => 'vue-router', @@ -65,7 +86,7 @@ export function getRouterTagProvider(): IHTMLTagProvider { collectAttributesDefault(tag, collector, routerTags, []); }, collectValues: (tag: string, attribute: string, collector: (value: string) => void) => { - collectValuesDefault(tag, attribute, collector, routerTags, [], {}); + collectValuesDefault(tag, attribute, collector, routerTags, [], valueSets); } }; } diff --git a/server/src/modes/template/tagProviders/vueTags.ts b/server/src/modes/template/tagProviders/vueTags.ts index 266aa7ab17..3b904ed564 100644 --- a/server/src/modes/template/tagProviders/vueTags.ts +++ b/server/src/modes/template/tagProviders/vueTags.ts @@ -7,53 +7,65 @@ import { collectValuesDefault, genAttribute, AttributeCollector, - Priority + Priority, + Attribute } from './common'; -const u = undefined; +function getAttribute(label: string, type: string | undefined, documentation: string) { + const linkedDocumentation = documentation + '\n\n' + `[API Reference](https://vuejs.org/v2/api/#${label})`; + return genAttribute(label, type, linkedDocumentation); +} const vueDirectives = [ - genAttribute('v-text', u, 'Updates the element’s `textContent`.'), - genAttribute('v-html', u, 'Updates the element’s `innerHTML`. XSS prone.'), - genAttribute( + getAttribute('v-text', undefined, 'Updates the element’s `textContent`.'), + getAttribute('v-html', undefined, 'Updates the element’s `innerHTML`. XSS prone.'), + getAttribute( 'v-show', - u, + undefined, 'Toggle’s the element’s `display` CSS property based on the truthy-ness of the expression value.' ), - genAttribute('v-if', u, 'Conditionally renders the element based on the truthy-ness of the expression value.'), - genAttribute('v-else', 'v', 'Denotes the “else block” for `v-if` or a `v-if`/`v-else-if` chain.'), - genAttribute('v-else-if', u, 'Denotes the “else if block” for `v-if`. Can be chained.'), - genAttribute('v-for', u, 'Renders the element or template block multiple times based on the source data.'), - genAttribute('v-on', u, 'Attaches an event listener to the element.'), - genAttribute('v-bind', u, 'Dynamically binds one or more attributes, or a component prop to an expression.'), - genAttribute('v-model', u, 'Creates a two-way binding on a form input element or a component.'), - genAttribute('v-pre', 'v', 'Skips compilation for this element and all its children.'), - genAttribute('v-cloak', 'v', 'Indicates Vue instance for this element has NOT finished compilation.'), - genAttribute('v-once', 'v', 'Render the element and component once only.'), - genAttribute('key', u, 'Hint at VNodes identity for VDom diffing, e.g. list rendering'), - genAttribute('ref', u, 'Register a reference to an element or a child component.'), - genAttribute( + getAttribute( + 'v-if', + undefined, + 'Conditionally renders the element based on the truthy-ness of the expression value.' + ), + getAttribute('v-else', 'v', 'Denotes the “else block” for `v-if` or a `v-if`/`v-else-if` chain.'), + getAttribute('v-else-if', undefined, 'Denotes the “else if block” for `v-if`. Can be chained.'), + getAttribute('v-for', undefined, 'Renders the element or template block multiple times based on the source data.'), + getAttribute('v-on', undefined, 'Attaches an event listener to the element.'), + getAttribute('v-bind', undefined, 'Dynamically binds one or more attributes, or a component prop to an expression.'), + getAttribute('v-model', undefined, 'Creates a two-way binding on a form input element or a component.'), + getAttribute('v-pre', 'v', 'Skips compilation for this element and all its children.'), + getAttribute('v-cloak', 'v', 'Indicates Vue instance for this element has NOT finished compilation.'), + getAttribute('v-once', 'v', 'Render the element and component once only.'), + getAttribute('key', undefined, 'Hint at VNodes identity for VDom diffing, e.g. list rendering'), + getAttribute('ref', undefined, 'Register a reference to an element or a child component.'), + getAttribute( 'slot', - u, + undefined, 'Used on content inserted into child components to indicate which named slot the content belongs to.' ), - genAttribute('slot-scope', u, 'the name of a temporary variable that holds the props object passed from the child') + getAttribute( + 'slot-scope', + undefined, + 'the name of a temporary variable that holds the props object passed from the child' + ) ]; const transitionProps = [ - genAttribute('name', u, 'Used to automatically generate transition CSS class names. Default: "v"'), - genAttribute('appear', 'b', 'Whether to apply transition on initial render. Default: false'), - genAttribute( + getAttribute('name', undefined, 'Used to automatically generate transition CSS class names. Default: "v"'), + getAttribute('appear', 'b', 'Whether to apply transition on initial render. Default: false'), + getAttribute( 'css', 'b', 'Whether to apply CSS transition classes. Defaults: true. If set to false, will only trigger JavaScript hooks registered via component events.' ), - genAttribute( + getAttribute( 'type', 'transType', 'The event, "transition" or "animation", to determine end timing. Default: the type that has a longer duration.' ), - genAttribute( + getAttribute( 'mode', 'transMode', 'Controls the timing sequence of leaving/entering transitions. Available modes are "out-in" and "in-out"; Defaults to simultaneous.' @@ -72,35 +84,48 @@ const transitionProps = [ ].map(t => genAttribute(t)) ); +function genTag(tag: string, doc: string, attributes: Attribute[]) { + return new HTMLTagSpecification(doc + '\n\n' + `[API Reference](https://vuejs.org/v2/api/#${tag})`, attributes); +} + const vueTags = { - component: new HTMLTagSpecification( + component: genTag( + 'component', 'A meta component for rendering dynamic components. The actual component to render is determined by the `is` prop.', [ - genAttribute('is', u, 'the actual component to render'), + genAttribute('is', undefined, 'the actual component to render'), genAttribute('inline-template', 'v', 'treat inner content as its template rather than distributed content') ] ), - transition: new HTMLTagSpecification( + transition: genTag( + 'transition', ' serves as transition effects for single element/component. It applies the transition behavior to the wrapped content inside.', transitionProps ), - 'transition-group': new HTMLTagSpecification( + 'transition-group': genTag( + 'transition-group', 'transition group serves as transition effects for multiple elements/components. It renders a by default and can render user specified element via `tag` attribute.', transitionProps.concat(genAttribute('tag'), genAttribute('move-class')) ), - 'keep-alive': new HTMLTagSpecification( + 'keep-alive': genTag( + 'keep-alive', 'When wrapped around a dynamic component, caches the inactive component instances without destroying them.', ['include', 'exclude'].map(t => genAttribute(t)) ), - slot: new HTMLTagSpecification( + slot: genTag( + 'slot', ' serve as content distribution outlets in component templates. itself will be replaced.', - [genAttribute('name', u, 'Used for named slot')] + [genAttribute('name', undefined, 'Used for named slot')] ), template: new HTMLTagSpecification( 'The template element is used to declare fragments of HTML that can be cloned and inserted in the document by script.', [ - genAttribute('scope', u, '(deprecated) a temporary variable that holds the props object passed from the child'), - genAttribute('slot', u, 'the name of scoped slot') + genAttribute( + 'scope', + undefined, + '(deprecated) a temporary variable that holds the props object passed from the child' + ), + genAttribute('slot', undefined, 'the name of scoped slot') ] ) }; diff --git a/test/completionHelper.ts b/test/completionHelper.ts index d764e0d245..508cf8beb5 100644 --- a/test/completionHelper.ts +++ b/test/completionHelper.ts @@ -4,7 +4,14 @@ import { CompletionItem, MarkdownString } from 'vscode'; import { showFile } from './editorHelper'; export interface ExpectedCompletionItem extends CompletionItem { + /** + * Documentation has to start with this string + */ documentationStart?: string; + /** + * Documentation has to include this string + */ + documentationFragment?: string; } export async function testCompletion( @@ -67,9 +74,29 @@ export async function testCompletion( if (ei.documentationStart) { if (typeof match.documentation === 'string') { - assert.ok(match.documentation.startsWith(ei.documentationStart)); + assert.ok( + match.documentation.startsWith(ei.documentationStart), + `${match.documentation}\ndoes not start with\n${ei.documentationStart}` + ); } else { - assert.ok((match.documentation as vscode.MarkdownString).value.startsWith(ei.documentationStart)); + assert.ok( + (match.documentation as vscode.MarkdownString).value.startsWith(ei.documentationStart), + `${(match.documentation as vscode.MarkdownString).value}\ndoes not start with\n${ei.documentationStart}` + ); + } + } + + if (ei.documentationFragment) { + if (typeof match.documentation === 'string') { + assert.ok( + match.documentation.includes(ei.documentationFragment), + `${match.documentation}\ndoes not include\n${ei.documentationFragment}` + ); + } else { + assert.ok( + (match.documentation as vscode.MarkdownString).value.includes(ei.documentationFragment), + `${(match.documentation as vscode.MarkdownString).value}\ndoes not include\n${ei.documentationFragment}` + ); } } } diff --git a/test/componentData/features/completion/basic.test.ts b/test/componentData/features/completion/basic.test.ts index 71586d08a6..e0a3f4b077 100644 --- a/test/componentData/features/completion/basic.test.ts +++ b/test/componentData/features/completion/basic.test.ts @@ -7,12 +7,40 @@ describe('Should complete frameworks', () => { const vueRouterUri = getDocUri('completion/VueRouter.vue'); it('completes vue-router tags', async () => { - await testCompletion(vueRouterUri, position(3, 5), ['router-link', 'router-view']); + await testCompletion(vueRouterUri, position(4, 5), ['router-link', 'router-view']); }); it('completes vue-router attributes', async () => { await testCompletion(vueRouterUri, position(2, 17), ['replace']); }); + + it('completes vue-router attribute values', async () => { + await testCompletion(vueRouterUri, position(3, 37), ['page', 'step']); + }); + }); + + describe('Should complete vue/vue-router with documentation', () => { + const linkUri = getDocUri('completion/Link.vue'); + + it('completes attributes with URI to API docs', async () => { + await testCompletion(linkUri, position(3, 5), [ + { + label: 'component', + documentationFragment: '[API Reference](https://vuejs.org/v2/api/#component)' + } + ]); + + await testCompletion(linkUri, position(2, 17), [ + { + label: 'replace', + documentationFragment: '[API Reference](https://router.vuejs.org/api/#replace)' + }, + { + label: 'v-if', + documentationFragment: '[API Reference](https://vuejs.org/v2/api/#v-if)' + } + ]); + }); }); describe('Should complete element-ui components (devDependency, loaded from bundled JSON)', () => { diff --git a/test/componentData/fixture/completion/Link.vue b/test/componentData/fixture/completion/Link.vue new file mode 100644 index 0000000000..6ba8756932 --- /dev/null +++ b/test/componentData/fixture/completion/Link.vue @@ -0,0 +1,6 @@ + diff --git a/test/componentData/fixture/completion/VueRouter.vue b/test/componentData/fixture/completion/VueRouter.vue index 6ba8756932..0b6e87ce69 100644 --- a/test/componentData/fixture/completion/VueRouter.vue +++ b/test/componentData/fixture/completion/VueRouter.vue @@ -1,6 +1,7 @@ diff --git a/test/componentData/fixture/package.json b/test/componentData/fixture/package.json index 3fcf580fa2..445e342630 100644 --- a/test/componentData/fixture/package.json +++ b/test/componentData/fixture/package.json @@ -2,7 +2,7 @@ "dependencies": { "quasar": "^1.12.13", "vue": "^2.6.11", - "vue-router": "^3.3.4" + "vue-router": "3.3.4" }, "devDependencies": { "element-ui": "^2.13.2" diff --git a/test/componentData/fixture/yarn.lock b/test/componentData/fixture/yarn.lock index 90132d0c35..434dbecb93 100644 --- a/test/componentData/fixture/yarn.lock +++ b/test/componentData/fixture/yarn.lock @@ -69,10 +69,10 @@ throttle-debounce@^1.0.1: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz#51853da37be68a155cb6e827b3514a3c422e89cd" integrity sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg== -vue-router@^3.3.4: - version "3.4.2" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.2.tgz#541221d7ac467786c1c9381bcf36019d883b9cf8" - integrity sha512-n3Ok70hW0EpcJF4lcWIwSHAQbFTnIOLl/fhO8+oTs4jHNtBNsovcVvPZeTOyKEd8C3xF1Crft2ASuOiVT5K1mw== +vue-router@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.3.4.tgz#4e38abc34a11c41b6c3d8244449a2e363ba6250b" + integrity sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg== vue@^2.6.11: version "2.6.11"