Skip to content
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
153 changes: 79 additions & 74 deletions packages/language-service/lib/nameCasing.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,89 @@
import type { LanguageServiceContext, VirtualCode } from '@volar/language-service';
import type { NodeTypes } from '@vue/compiler-dom';
import type * as CompilerDOM from '@vue/compiler-dom';
import { forEachElementNode, hyphenateTag, VueVirtualCode } from '@vue/language-core';
import type { URI } from 'vscode-uri';

export enum TagNameCasing {
type CollectResult = Map<string, [tagType: CompilerDOM.ElementTypes, attrs: string[]]>;

const collectCache = new WeakMap<VirtualCode, CollectResult>();

export const enum TagNameCasing {
Kebab,
Pascal,
}

export enum AttrNameCasing {
export const enum AttrNameCasing {
Kebab,
Camel,
}

export async function checkCasing(context: LanguageServiceContext, uri: URI) {
const detected = detect(context, uri);
const [attr, tag] = await Promise.all([
context.env.getConfiguration<'preferKebabCase' | 'preferCamelCase' | 'alwaysKebabCase' | 'alwaysCamelCase'>?.(
'vue.suggest.propNameCasing',
uri.toString(),
),
context.env.getConfiguration<'preferKebabCase' | 'preferPascalCase' | 'alwaysKebabCase' | 'alwaysPascalCase'>?.(
'vue.suggest.componentNameCasing',
uri.toString(),
),
]);
const tagNameCasing = detected.tag.length === 1 && (tag === 'preferPascalCase' || tag === 'preferKebabCase')
? detected.tag[0]
: (tag === 'preferKebabCase' || tag === 'alwaysKebabCase')
? TagNameCasing.Kebab
: TagNameCasing.Pascal;
const attrNameCasing = detected.attr.length === 1 && (attr === 'preferCamelCase' || attr === 'preferKebabCase')
? detected.attr[0]
: (attr === 'preferCamelCase' || attr === 'alwaysCamelCase')
? AttrNameCasing.Camel
: AttrNameCasing.Kebab;
return {
tag: tagNameCasing,
attr: attrNameCasing,
};
}

type Tags = Map<string, string[]>;
export async function getTagNameCasing(context: LanguageServiceContext, uri: URI) {
const config = await context.env.getConfiguration<
'preferKebabCase' | 'preferPascalCase' | 'alwaysKebabCase' | 'alwaysPascalCase'
>?.('vue.suggest.componentNameCasing', uri.toString());

const cache = new WeakMap<VirtualCode, Tags | undefined>();
if (config === 'alwaysKebabCase') {
return TagNameCasing.Kebab;
}
if (config === 'alwaysPascalCase') {
return TagNameCasing.Pascal;
}

function detect(
context: LanguageServiceContext,
uri: URI,
): {
tag: TagNameCasing[];
attr: AttrNameCasing[];
} {
const root = context.language.scripts.get(uri)?.generated?.root;
if (!(root instanceof VueVirtualCode)) {
return { tag: [], attr: [] };

if (root instanceof VueVirtualCode) {
const detectedCasings = detectTagCasing(root);
if (detectedCasings.length === 1) {
return detectedCasings[0];
}
}
if (config === 'preferKebabCase') {
return TagNameCasing.Kebab;
}
return {
tag: detectTagCasing(root),
attr: detectAttrCasing(root),
};

return TagNameCasing.Pascal;
}

function detectAttrCasing(code: VirtualCode) {
let tags: Tags | undefined;
if (cache.has(code)) {
tags = cache.get(code);
export async function getAttrNameCasing(context: LanguageServiceContext, uri: URI) {
const config = await context.env.getConfiguration<
'preferKebabCase' | 'preferCamelCase' | 'alwaysKebabCase' | 'alwaysCamelCase'
>?.('vue.suggest.propNameCasing', uri.toString());

if (config === 'alwaysKebabCase') {
return AttrNameCasing.Kebab;
}
if (config === 'alwaysCamelCase') {
return AttrNameCasing.Camel;
}

const root = context.language.scripts.get(uri)?.generated?.root;

if (root instanceof VueVirtualCode) {
const detectedCasings = detectAttrCasing(root);
if (detectedCasings.length === 1) {
return detectedCasings[0];
}
}
else {
cache.set(code, tags = collectTags(code));
if (config === 'preferKebabCase') {
return AttrNameCasing.Kebab;
}

return AttrNameCasing.Camel;
}

function detectAttrCasing(code: VueVirtualCode) {
const tags = collectTagsWithCache(code);
const result = new Set<AttrNameCasing>();

for (const [, attrs] of tags ?? []) {
for (const [, [_, attrs]] of tags) {
for (const attr of attrs) {
// attrName
if (attr !== hyphenateTag(attr)) {
result.add(AttrNameCasing.Camel);
break;
}
}
for (const attr of attrs) {
// attr-name
if (attr.includes('-')) {
result.add(AttrNameCasing.Kebab);
break;
Expand All @@ -92,41 +94,43 @@ function detectAttrCasing(code: VirtualCode) {
}

function detectTagCasing(code: VueVirtualCode): TagNameCasing[] {
let tags: Tags | undefined;
if (cache.has(code)) {
tags = cache.get(code);
}
else {
cache.set(code, tags = collectTags(code));
}
const tags = collectTagsWithCache(code);
const result = new Set<TagNameCasing>();

for (const [tag] of tags ?? []) {
for (const [tag, [tagType]] of tags) {
if (
tagType === 0 satisfies CompilerDOM.ElementTypes.ELEMENT
|| tagType === 3 satisfies CompilerDOM.ElementTypes.TEMPLATE
) {
continue;
}
if (tag !== hyphenateTag(tag)) {
// TagName
result.add(TagNameCasing.Pascal);
}
else {
// tag-name
result.add(TagNameCasing.Kebab);
}
}
return [...result];
}

function collectTags(root: VirtualCode) {
if (!(root instanceof VueVirtualCode)) {
return undefined;
function collectTagsWithCache(code: VueVirtualCode) {
let cache = collectCache.get(code);
if (!cache) {
const ast = code.sfc.template?.ast;
cache = ast ? collectTags(ast) : new Map();
collectCache.set(code, cache);
}
const ast = root.sfc.template?.ast;
if (!ast) {
return undefined;
}
const tags: Tags = new Map();
return cache;
}

function collectTags(ast: CompilerDOM.RootNode) {
const tags: CollectResult = new Map();

for (const node of forEachElementNode(ast)) {
let tag = tags.get(node.tag);
if (!tag) {
tags.set(node.tag, tag = []);
tags.set(node.tag, tag = [node.tagType, []]);
}
for (const prop of node.props) {
let name: string | undefined;
Expand All @@ -143,9 +147,10 @@ function collectTags(root: VirtualCode) {
name = prop.name;
}
if (name !== undefined) {
tag.push(name);
tag[1].push(name);
}
}
}

return tags;
}
6 changes: 3 additions & 3 deletions packages/language-service/lib/plugins/vue-document-drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { camelize, capitalize, hyphenate } from '@vue/shared';
import { posix as path } from 'path-browserify';
import { getUserPreferences } from 'volar-service-typescript/lib/configs/getUserPreferences';
import { URI } from 'vscode-uri';
import { checkCasing, TagNameCasing } from '../nameCasing';
import { getTagNameCasing, TagNameCasing } from '../nameCasing';
import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file';
import { resolveEmbeddedCode } from '../utils';

Expand Down Expand Up @@ -44,7 +44,7 @@ export function create(
return;
}

const casing = await checkCasing(context, info.script.id);
const tagNameCasing = await getTagNameCasing(context, info.script.id);
const baseName = path.basename(importUri);
const newName = capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.'))));

Expand Down Expand Up @@ -114,7 +114,7 @@ export function create(
}

return {
insertText: `<${casing.tag === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`,
insertText: `<${tagNameCasing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`,
insertTextFormat: 2 satisfies typeof InsertTextFormat.Snippet,
additionalEdit,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
} from '@volar/language-service';
import { hyphenateAttr, hyphenateTag } from '@vue/language-core';
import * as html from 'vscode-html-languageservice';
import { AttrNameCasing, checkCasing } from '../nameCasing';
import { AttrNameCasing, getAttrNameCasing } from '../nameCasing';
import { resolveEmbeddedCode } from '../utils';

export function create(
Expand Down Expand Up @@ -39,7 +39,7 @@ export function create(
}

const result: InlayHint[] = [];
const casing = await checkCasing(context, info.script.id);
const attrNameCasing = await getAttrNameCasing(context, info.script.id);
const components = await getComponentNames(info.root.fileName) ?? [];
const componentProps = new Map<string, string[]>();

Expand Down Expand Up @@ -141,7 +141,7 @@ export function create(
end: document.positionAt(current.labelOffset),
},
newText: ` :${
casing.attr === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp
attrNameCasing === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp
}=`,
}],
});
Expand Down
37 changes: 20 additions & 17 deletions packages/language-service/lib/plugins/vue-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { create as createPugService } from 'volar-service-pug';
import * as html from 'vscode-html-languageservice';
import { URI, Utils } from 'vscode-uri';
import { loadModelModifiersData, loadTemplateData } from '../data';
import { AttrNameCasing, checkCasing, TagNameCasing } from '../nameCasing';
import { AttrNameCasing, getAttrNameCasing, getTagNameCasing, TagNameCasing } from '../nameCasing';
import { createReferenceResolver, resolveEmbeddedCode } from '../utils';

const specialTags = new Set([
Expand All @@ -38,6 +38,14 @@ const specialProps = new Set([
'style',
]);

const builtInComponents = new Set([
'Transition',
'TransitionGroup',
'KeepAlive',
'Suspense',
'Teleport',
]);

let builtInData: html.HTMLDataV1 | undefined;
let modelData: html.HTMLDataV1 | undefined;

Expand Down Expand Up @@ -405,13 +413,14 @@ export function create(
async function provideHtmlData(sourceDocumentUri: URI, root: VueVirtualCode) {
await (initializing ??= initialize());

const casing = await checkCasing(context, sourceDocumentUri);
const tagNameCasing = await getTagNameCasing(context, sourceDocumentUri);
const attrNameCasing = await getAttrNameCasing(context, sourceDocumentUri);

for (const tag of builtInData!.tags ?? []) {
if (specialTags.has(tag.name)) {
continue;
}
if (casing.tag === TagNameCasing.Kebab) {
if (tagNameCasing === TagNameCasing.Kebab) {
tag.name = hyphenateTag(tag.name);
}
else {
Expand Down Expand Up @@ -472,13 +481,7 @@ export function create(
components = [];
tasks.push((async () => {
components = (await getComponentNames(root.fileName) ?? [])
.filter(name =>
name !== 'Transition'
&& name !== 'TransitionGroup'
&& name !== 'KeepAlive'
&& name !== 'Suspense'
&& name !== 'Teleport'
);
.filter(name => !builtInComponents.has(name));
version++;
})());
}
Expand All @@ -487,7 +490,7 @@ export function create(
const tags: html.ITagData[] = [];

for (const tag of components) {
if (casing.tag === TagNameCasing.Kebab) {
if (tagNameCasing === TagNameCasing.Kebab) {
names.add(hyphenateTag(tag));
}
else {
Expand All @@ -497,7 +500,7 @@ export function create(

for (const binding of scriptSetupRanges?.bindings ?? []) {
const name = root.sfc.scriptSetup!.content.slice(binding.range.start, binding.range.end);
if (casing.tag === TagNameCasing.Kebab) {
if (tagNameCasing === TagNameCasing.Kebab) {
names.add(hyphenateTag(name));
}
else {
Expand Down Expand Up @@ -559,11 +562,11 @@ export function create(
]
) {
const isGlobal = prop.isAttribute || !propNameSet.has(prop.name);
const propName = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
const propName = attrNameCasing === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
const isEvent = hyphenateAttr(propName).startsWith('on-');

if (isEvent) {
const eventName = casing.attr === AttrNameCasing.Camel
const eventName = attrNameCasing === AttrNameCasing.Camel
? propName['on'.length]!.toLowerCase() + propName.slice('onX'.length)
: propName.slice('on-'.length);

Expand All @@ -584,7 +587,7 @@ export function create(
}
else {
const propInfo = propInfos.find(prop => {
const name = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
const name = attrNameCasing === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name);
return name === propName;
});

Expand All @@ -610,7 +613,7 @@ export function create(
}

for (const event of events) {
const eventName = casing.attr === AttrNameCasing.Camel ? event : hyphenateAttr(event);
const eventName = attrNameCasing === AttrNameCasing.Camel ? event : hyphenateAttr(event);

for (
const name of [
Expand Down Expand Up @@ -652,7 +655,7 @@ export function create(
}

for (const model of models) {
const name = casing.attr === AttrNameCasing.Camel ? model : hyphenateAttr(model);
const name = attrNameCasing === AttrNameCasing.Camel ? model : hyphenateAttr(model);

attributes.push({ name: 'v-model:' + name });
propMap.set('v-model:' + name, {
Expand Down
Loading