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

Dont include used attributes in suggestions #2565

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
@@ -0,0 +1 @@
server/src/modes/template/test/completion.test.ts
yoyo930021 marked this conversation as resolved.
Show resolved Hide resolved
45 changes: 44 additions & 1 deletion server/src/modes/template/services/htmlCompletion.ts
Expand Up @@ -16,6 +16,7 @@ import { NULL_COMPLETION } from '../../nullMode';
import { getModifierProvider, Modifier } from '../modifierProvider';
import { toMarkupContent } from '../../../utils/strings';
import { Priority } from '../tagProviders/common';
import { kebabCase } from 'lodash';

export function doComplete(
document: TextDocument,
Expand Down Expand Up @@ -147,8 +148,15 @@ export function doComplete(
return result;
}

function getUsedAttributes(offset: number) {
const node = htmlDocument.findNodeBefore(offset);
return node.attributeNames.map(normalizeAttributeNameToKebabCase);
}

function collectAttributeNameSuggestions(nameStart: number, nameEnd: number = offset): CompletionList {
const execArray = /^[:@]/.exec(scanner.getTokenText());
const usedAttributes = getUsedAttributes(nameStart);
const currentAttribute = scanner.getTokenText();
const execArray = /^[:@]/.exec(currentAttribute);
const filterPrefix = execArray ? execArray[0] : '';
const start = filterPrefix ? nameStart + 1 : nameStart;
const range = getReplaceRange(start, nameEnd);
Expand All @@ -158,6 +166,18 @@ export function doComplete(
tagProviders.forEach(provider => {
const priority = provider.priority;
provider.collectAttributes(currentTag, (attribute, type, documentation) => {
if (
// include current typing attribute for completing `="$1"`
!(attribute === currentAttribute && text[nameEnd] !== '=') &&
// can listen to same event by adding modifiers
type !== 'event' &&
// `class` and `:class`, `style` and `:style` can coexist
attribute !== 'class' &&
attribute !== 'style' &&
usedAttributes.includes(normalizeAttributeNameToKebabCase(attribute))
) {
return;
}
if ((type === 'event' && filterPrefix !== '@') || (type !== 'event' && filterPrefix === '@')) {
return;
}
Expand Down Expand Up @@ -392,3 +412,26 @@ function getWordEnd(s: string, offset: number, limit: number): number {
}
return offset;
}

export function normalizeAttributeNameToKebabCase(attr: string): string {
let result = attr;

if (result.startsWith('v-model:')) {
result = attr.slice('v-model:'.length);
}

if (result.startsWith('v-bind:')) {
result = attr.slice('v-bind:'.length);
} else if (result.startsWith(':')) {
result = attr.slice(':'.length);
}

// Remove modifiers
if (result.includes('.')) {
result = result.slice(0, result.indexOf('.'));
}

result = kebabCase(result);

return result;
}
30 changes: 5 additions & 25 deletions server/src/modes/template/services/vuePropValidation.ts
Expand Up @@ -4,6 +4,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument';
import { HTMLDocument, Node } from '../parser/htmlParser';
import { kebabCase } from 'lodash';
import { getSameTagInSet } from '../tagProviders/common';
import { normalizeAttributeNameToKebabCase } from './htmlCompletion';

export function doPropValidation(document: TextDocument, htmlDocument: HTMLDocument, info: VueFileInfo): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
Expand Down Expand Up @@ -50,7 +51,7 @@ function generateDiagnostic(n: Node, definedProps: PropInfo[], document: TextDoc
const seenProps = n.attributeNames.map(attr => {
return {
name: attr,
normalized: normalizeHtmlAttributeNameToKebabCase(
normalized: normalizeHtmlAttributeNameToKebabCaseAndReplaceVModel(
attr,
definedProps.find(prop => prop.isBoundToModel)?.name ?? 'value'
)
Expand Down Expand Up @@ -86,31 +87,10 @@ function generateDiagnostic(n: Node, definedProps: PropInfo[], document: TextDoc
};
}

function normalizeHtmlAttributeNameToKebabCase(attr: string, modelProp: string) {
let result = attr;

function normalizeHtmlAttributeNameToKebabCaseAndReplaceVModel(attr: string, modelProp: string) {
// v-model.trim
if (!result.startsWith('v-model:') && result.startsWith('v-model')) {
if (!attr.startsWith('v-model:') && attr.startsWith('v-model')) {
return kebabCase(modelProp);
}

// Allow `v-model:prop` in vue 3
if (result.startsWith('v-model:')) {
result = attr.slice('v-model:'.length);
}

if (result.startsWith('v-bind:')) {
result = attr.slice('v-bind:'.length);
} else if (result.startsWith(':')) {
result = attr.slice(':'.length);
}

// Remove modifiers
if (result.includes('.')) {
result = result.slice(0, result.indexOf('.'));
}

result = kebabCase(result);

return result;
return normalizeAttributeNameToKebabCase(attr);
}
21 changes: 15 additions & 6 deletions server/src/modes/template/test/completion.test.ts
Expand Up @@ -77,24 +77,21 @@ suite('HTML Completion', () => {
.become('<input tabindex="$1"');

html`<input t|ype="text"`
.has('type')
.become('<input type="text"')
.hasNo('type')
.has('tabindex')
.become('<input tabindex="text"');

html`<input type="text" |`
.has('style')
.become('<input type="text" style="$1"')
.has('type')
.become('<input type="text" type="$1"')
.hasNo('type')
.has('size')
.become('<input type="text" size="$1"');

html`<input type="text" s|`
.has('style')
.become('<input type="text" style="$1"')
.has('type')
.become('<input type="text" type="$1"')
.hasNo('type')
.has('size')
.become('<input type="text" size="$1"');

Expand All @@ -114,7 +111,19 @@ suite('HTML Completion', () => {

html`<input :di| type="text"`.has('dir').become('<input :dir="$1" type="text"');

html`<input :type="type" |`.hasNo('type');

html`<input :type.prop="type" |`.hasNo('type');

// `class` and `:class`, `style` and `:style` can coexist
html`<input :class="$style.input" |`.has('class');
html`<input style="style" |`.has('style');
html`<input :cl|ass="$style.input"`.has('class').become('<input :class="$style.input"');

html`<input @|`.has('mousemove').become('<input @mousemove="$1"');

// can listen to same event by adding modifiers
html`<input @mousemove="mousemove" @|`.has('mousemove').become('<input @mousemove="mousemove" @mousemove="$1"');
});

test('Complete Value', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/interpolation/features/completion/property.test.ts
Expand Up @@ -93,7 +93,7 @@ describe('Should autocomplete interpolation for <template> in property class com
];

it(`completes child component's props`, async () => {
await testCompletion(parentTemplateDocUri, position(2, 27), propsList);
await testCompletion(parentTemplateDocUri, position(2, 26), propsList);
});

it(`completes child component's props when camel case component name`, async () => {
Expand Down
@@ -1,6 +1,6 @@
<template>
<div>
<basic-property-class :foo=""></basic-property-class>
<basic-property-class ></basic-property-class>
<basic-property-class v-if="" @click="" :foo=""></basic-property-class>
<BasicPropertyClass />
<
Expand Down