Skip to content

feat: allow load customTags from ./package.json #2018

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- `vls` now only supports Node `>=10`, as Prettier 2.0 drops support for Node 8.
- Upgrade to prettier 2.0. #1925 and #1794.
- Add [prettier/plugin-pug](https://github.com/prettier/plugin-pug) as default formatter for `pug`. #527.
- 🙌 Cusom tags IntelliSense for local `tags.json`/`attributes.json`. [Usage Docs](https://vuejs.github.io/vetur/framework.html#workspace-custom-tags). Thanks to contribution from [Carlos Rodrigues](https://github.com/pikax). #1364 and #2018.
- 🙌 Detect tags from @nuxt/components. Thanks to contribution from [pooya parsa](https://github.com/pi0). #1921.
- 🙌 Fix VTI crash by passing correct PID to language server. Thanks to contribution from [Daniil Yastremskiy](@TheBeastOfCaerbannog). #1699 and #1805.
- 🙌 Fix template interpolation hover info of v-for readonly array item. Thanks to contribution from [@yoyo930021](https://github.com/yoyo930021). #1788.
Expand Down
24 changes: 23 additions & 1 deletion docs/framework.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Framework Support

Vue frameworks can define custom components used in `<template>` region. For example, `vue-router` provides `<router-link>` component that could have attributes such as `to` and `replace`. Vetur currently provides autocomplete support for the component names and attributes.
Vue libraries or frameworks can define custom components used in `<template>` region. For example, [`vue-router`](https://router.vuejs.org/) provides [`<router-link>`](https://router.vuejs.org/api/#router-link) component that could have attributes such as `to` and `replace`. Vetur currently provides autocomplete support for the component names and attributes.

Vetur currently provides framework support for the following vue libraries:

Expand All @@ -14,6 +14,8 @@ Vetur currently provides framework support for the following vue libraries:
- [Quasar Framework](https://quasar.dev/)
- [Gridsome](https://gridsome.org/)

🚧 The data format is not specified yet. 🚧

## Usage

Vetur reads the `package.json` **in your project root** to determine if it should offer tags & attributes completions. Here are the exact dependencies and sources of their definitions.
Expand Down Expand Up @@ -48,6 +50,26 @@ If a package listed in `dependencies` has a `vetur` key, then Vetur will try to

By bundling the tags / attributes definitions together with the framework library, you ensure that users will always get the matching tags / attributes with the specific version of your library they are using.

## Workspace Custom Tags

You can define custom tags/attributes for your workspace by specifying a `vetur` key in package.json. For example, to get auto completion for tag `<foo-tag>`, all you need to do is:

- Create a file `tags.json` with:

```json
{ "foo-bar": { "description": "A foo tag" } }
```

- Add this line to `package.json`:

```json
{
"vetur": { "tags": "./tags.json" }
}
```

- Reload VS Code. You'll get `foo-bar` when completing `<|`.

## Adding a Framework

If your Vue UI framework has a lot of users, we might consider bundling its support in Vetur. However, this means Vetur's definition for the framework might become outdated.
Expand Down
36 changes: 31 additions & 5 deletions server/src/modes/template/tagProviders/externalTagProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,53 @@ export const onsenTagProvider = getExternalTagProvider('onsen', onsenTags, onsen
export const bootstrapTagProvider = getExternalTagProvider('bootstrap', bootstrapTags, bootstrapAttributes);
export const gridsomeTagProvider = getExternalTagProvider('gridsome', gridsomeTags, gridsomeAttributes);

export function getRuntimeTagProvider(workspacePath: string, pkg: any): IHTMLTagProvider | null {
if (!pkg.vetur) {
/**
* Get tag providers specified in workspace root's packaage.json
*/
export function getWorkspaceTagProvider(workspacePath: string, rootPkgJson: any): IHTMLTagProvider | null {
if (!rootPkgJson.vetur) {
return null;
}

const tagsPath = ts.findConfigFile(workspacePath, ts.sys.fileExists, rootPkgJson.vetur.tags);
const attrsPath = ts.findConfigFile(workspacePath, ts.sys.fileExists, rootPkgJson.vetur.attributes);

try {
if (tagsPath && attrsPath) {
const tagsJson = JSON.parse(fs.readFileSync(tagsPath, 'utf-8'));
const attrsJson = JSON.parse(fs.readFileSync(attrsPath, 'utf-8'));
return getExternalTagProvider(rootPkgJson.name, tagsJson, attrsJson);
}
return null;
} catch (err) {
return null;
}
}

/**
* Get tag providers specified in packaage.json's `vetur` key
*/
export function getDependencyTagProvider(workspacePath: string, depPkgJson: any): IHTMLTagProvider | null {
if (!depPkgJson.vetur) {
return null;
}

const tagsPath = ts.findConfigFile(
workspacePath,
ts.sys.fileExists,
path.join('node_modules/', pkg.name, pkg.vetur.tags)
path.join('node_modules/', depPkgJson.name, depPkgJson.vetur.tags)
);
const attrsPath = ts.findConfigFile(
workspacePath,
ts.sys.fileExists,
path.join('node_modules/', pkg.name, pkg.vetur.attributes)
path.join('node_modules/', depPkgJson.name, depPkgJson.vetur.attributes)
);

try {
if (tagsPath && attrsPath) {
const tagsJson = JSON.parse(fs.readFileSync(tagsPath, 'utf-8'));
const attrsJson = JSON.parse(fs.readFileSync(attrsPath, 'utf-8'));
return getExternalTagProvider(pkg.name, tagsJson, attrsJson);
return getExternalTagProvider(depPkgJson.name, tagsJson, attrsJson);
}
return null;
} catch (err) {
Expand Down
28 changes: 17 additions & 11 deletions server/src/modes/template/tagProviders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
onsenTagProvider,
bootstrapTagProvider,
gridsomeTagProvider,
getRuntimeTagProvider
getDependencyTagProvider,
getWorkspaceTagProvider
} from './externalTagProviders';
export { getComponentInfoTagProvider as getComponentTags } from './componentInfoTagProvider';
export { IHTMLTagProvider } from './common';
Expand Down Expand Up @@ -55,9 +56,9 @@ export function getTagProviderSettings(workspacePath: string | null | undefined)
return settings;
}

const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
const dependencies = packageJson.dependencies || {};
const devDependencies = packageJson.devDependencies || {};
const rootPkgJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
const dependencies = rootPkgJson.dependencies || {};
const devDependencies = rootPkgJson.devDependencies || {};

if (dependencies['vue-router']) {
settings['router'] = true;
Expand Down Expand Up @@ -108,28 +109,33 @@ export function getTagProviderSettings(workspacePath: string | null | undefined)
settings['gridsome'] = true;
}

const workspaceTagProvider = getWorkspaceTagProvider(workspacePath, rootPkgJson);
if (workspaceTagProvider) {
allTagProviders.push(workspaceTagProvider);
}

for (const dep in dependencies) {
const runtimePkgPath = ts.findConfigFile(
const runtimePkgJsonPath = ts.findConfigFile(
workspacePath,
ts.sys.fileExists,
join('node_modules', dep, 'package.json')
);

if (!runtimePkgPath) {
if (!runtimePkgJsonPath) {
continue;
}

const runtimePkg = JSON.parse(fs.readFileSync(runtimePkgPath, 'utf-8'));
if (!runtimePkg) {
const runtimePkgJson = JSON.parse(fs.readFileSync(runtimePkgJsonPath, 'utf-8'));
if (!runtimePkgJson) {
continue;
}

const tagProvider = getRuntimeTagProvider(workspacePath, runtimePkg);
if (!tagProvider) {
const depTagProvider = getDependencyTagProvider(workspacePath, runtimePkgJson);
if (!depTagProvider) {
continue;
}

allTagProviders.push(tagProvider);
allTagProviders.push(depTagProvider);
settings[dep] = true;
}
} catch (e) {}
Expand Down
64 changes: 39 additions & 25 deletions test/lsp/completion/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,113 @@ import { position, getDocUri } from '../util';
import { testCompletion } from './helper';

describe('Should autocomplete for <template>', () => {
const templateDocUri = getDocUri('client/completion/template/Basic.vue');
const templateFrameworkDocUri = getDocUri('client/completion/template/Framework.vue');
const templateQuasarDocUri = getDocUri('client/completion/template/Quasar.vue');
const templateVuetifyDocUri = getDocUri('client/completion/template/Vuetify.vue');
const basicUri = getDocUri('client/completion/template/Basic.vue');
const elementUri = getDocUri('client/completion/template/Element.vue');
const quasarUri = getDocUri('client/completion/template/Quasar.vue');
const vuetifyUri = getDocUri('client/completion/template/Vuetify.vue');
const workspaceCustomTagsUri = getDocUri('client/completion/template/WorkspaceCustomTags.vue');

before('activate', async () => {
await activateLS();
await showFile(templateDocUri);
await showFile(templateFrameworkDocUri);
await showFile(basicUri);
await showFile(elementUri);
await showFile(quasarUri);
await showFile(vuetifyUri);
await showFile(workspaceCustomTagsUri);
await sleep(FILE_LOAD_SLEEP_TIME);
});

describe('Should complete <template> section', () => {
it('completes directives such as v-if', async () => {
await testCompletion(templateDocUri, position(1, 8), ['v-if', 'v-cloak']);
await testCompletion(basicUri, position(1, 8), ['v-if', 'v-cloak']);
});

it('completes html tags', async () => {
await testCompletion(templateDocUri, position(2, 6), ['img', 'iframe']);
await testCompletion(basicUri, position(2, 6), ['img', 'iframe']);
});

it('completes imported components', async () => {
await testCompletion(templateDocUri, position(2, 6), ['item']);
await testCompletion(basicUri, position(2, 6), ['item']);
});

it('completes event modifiers when attribute startsWith @', async () => {
await testCompletion(templateDocUri, position(3, 17), ['stop', 'prevent', 'capture']);
await testCompletion(basicUri, position(3, 17), ['stop', 'prevent', 'capture']);
});

it('completes event modifiers when attribute startsWith v-on', async () => {
await testCompletion(templateDocUri, position(4, 21), ['stop', 'prevent', 'capture']);
await testCompletion(basicUri, position(4, 21), ['stop', 'prevent', 'capture']);
});

it('completes key modifiers when keyEvent', async () => {
await testCompletion(templateDocUri, position(5, 21), ['enter', 'space', 'right']);
await testCompletion(basicUri, position(5, 21), ['enter', 'space', 'right']);
});

it('completes system modifiers when keyEvent', async () => {
await testCompletion(templateDocUri, position(6, 26), ['ctrl', 'shift', 'exact']);
await testCompletion(basicUri, position(6, 26), ['ctrl', 'shift', 'exact']);
});

it('completes mouse modifiers when MouseEvent', async () => {
await testCompletion(templateDocUri, position(7, 19), ['left', 'right', 'middle']);
await testCompletion(basicUri, position(7, 19), ['left', 'right', 'middle']);
});

it('completes system modifiers when MouseEvent', async () => {
await testCompletion(templateDocUri, position(8, 25), ['ctrl', 'shift', 'exact']);
await testCompletion(basicUri, position(8, 25), ['ctrl', 'shift', 'exact']);
});

it('completes prop modifiers when attribute startsWith :', async () => {
await testCompletion(templateDocUri, position(9, 17), ['sync']);
await testCompletion(basicUri, position(9, 17), ['sync']);
});

it('completes prop modifiers when attribute startsWith v-bind', async () => {
await testCompletion(templateDocUri, position(10, 23), ['sync']);
await testCompletion(basicUri, position(10, 23), ['sync']);
});

it('completes vModel modifiers when attribute startsWith v-model', async () => {
await testCompletion(templateDocUri, position(11, 19), ['lazy', 'number', 'trim']);
await testCompletion(basicUri, position(11, 19), ['lazy', 'number', 'trim']);
});

it('completes modifiers when have attribute value', async () => {
await testCompletion(templateDocUri, position(12, 19), ['stop', 'prevent', 'capture']);
await testCompletion(basicUri, position(12, 19), ['stop', 'prevent', 'capture']);
});
});

describe('Should complete element-ui components', () => {
it('completes <el-button> and <el-card>', async () => {
await testCompletion(templateFrameworkDocUri, position(2, 5), ['el-button', 'el-card']);
await testCompletion(elementUri, position(2, 5), ['el-button', 'el-card']);
});

it('completes attributes for <el-button>', async () => {
await testCompletion(templateFrameworkDocUri, position(1, 14), ['size', 'type', 'plain']);
await testCompletion(elementUri, position(1, 14), ['size', 'type', 'plain']);
});
});

describe('Should complete Quasar components', () => {
it('completes <q-btn>', async () => {
await testCompletion(templateQuasarDocUri, position(2, 5), ['q-btn']);
await testCompletion(quasarUri, position(2, 5), ['q-btn']);
});

it('completes attributes for <q-btn>', async () => {
await testCompletion(templateQuasarDocUri, position(1, 10), ['label', 'icon']);
await testCompletion(quasarUri, position(1, 10), ['label', 'icon']);
});
});

describe('Should complete Vuetify components', () => {
it('completes <v-btn>', async () => {
await testCompletion(templateVuetifyDocUri, position(2, 5), ['v-btn']);
await testCompletion(vuetifyUri, position(2, 5), ['v-btn']);
});

it('completes attributes for <v-btn>', async () => {
await testCompletion(templateVuetifyDocUri, position(1, 10), ['color', 'fab']);
await testCompletion(vuetifyUri, position(1, 10), ['color', 'fab']);
});
});

describe('Should complete tags defined in workspace', () => {
it('completes <foo-tag>', async () => {
await testCompletion(workspaceCustomTagsUri, position(2, 6), ['foo-tag']);
});

it('completes attributes for <foo-bar>', async () => {
await testCompletion(workspaceCustomTagsUri, position(1, 12), ['foo-attr']);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<template>
<foo-tag f></foo-tag>
<foo
</template>
3 changes: 3 additions & 0 deletions test/lsp/fixture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@
"prettier-eslint-cli": "^5.0.0",
"typescript": "^3.8.3",
"vue-template-compiler": "^2.6.11"
},
"vetur": {
"tags": "./tags.json"
}
}
11 changes: 11 additions & 0 deletions test/lsp/fixture/tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"foo-tag": {
"attributes": [
{
"name": "foo-attr",
"description": "A foo attribute"
}
],
"description": "A foo tag"
}
}