From 643efb5c388b13533ca4628832b90abb681612f5 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 23 Jan 2023 15:38:40 +0000 Subject: [PATCH 1/2] Use completion list `itemDefaults` --- .../tailwindcss-language-server/src/server.ts | 31 ++- .../src/completionProvider.ts | 251 +++++++++++------- .../src/util/naturalExpand.ts | 11 + .../src/util/state.ts | 1 + packages/vscode-tailwindcss/package.json | 2 +- 5 files changed, 190 insertions(+), 106 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 99bb9894..5dbf6a61 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -378,6 +378,16 @@ async function createProjectService( const disposables: Array> = [] let documentSelector = projectConfig.documentSelector + let itemDefaults = + params.capabilities.textDocument?.completion?.completionList?.itemDefaults ?? [] + + // VS Code _does_ support `itemDefaults.data` since at least 1.67.0 (this extension's min version) + // but it doesn't advertise it in its capabilities. So we manually add it here. + // See also: https://github.com/microsoft/vscode-languageserver-node/issues/1181 + if (params.clientInfo?.name === 'Visual Studio Code' && !itemDefaults.includes('data')) { + itemDefaults.push('data') + } + let state: State = { enabled: false, editor: { @@ -390,6 +400,7 @@ async function createProjectService( capabilities: { configuration: true, diagnosticRelatedInformation: true, + itemDefaults, }, documents: documentService.documents, getConfiguration, @@ -1116,11 +1127,25 @@ async function createProjectService( if (await isExcluded(state, document)) return null let result = await doComplete(state, document, params.position, params.context) if (!result) return result + + let supportsDefaults = state.editor.capabilities.itemDefaults.length > 0 + let supportsDefaultData = state.editor.capabilities.itemDefaults.includes('data') + return { isIncomplete: result.isIncomplete, + ...(supportsDefaults + ? { + itemDefaults: { + ...(result.itemDefaults ?? {}), + ...(supportsDefaultData + ? { data: { _projectKey: projectKey, ...(result.itemDefaults?.data ?? {}) } } + : {}), + }, + } + : {}), items: result.items.map((item) => ({ ...item, - data: { projectKey, originalData: item.data }, + ...(item.data ? { data: { _projectKey: projectKey, ...item.data } } : {}), })), } }, null) @@ -1128,7 +1153,7 @@ async function createProjectService( onCompletionResolve(item: CompletionItem): Promise { return withFallback(() => { if (!state.enabled) return null - return resolveCompletionItem(state, { ...item, data: item.data?.originalData }) + return resolveCompletionItem(state, item) }, null) }, async onCodeAction(params: CodeActionParams): Promise { @@ -2162,7 +2187,7 @@ class TW { } async onCompletionResolve(item: CompletionItem): Promise { - return this.projects.get(item.data.projectKey)?.onCompletionResolve(item) ?? null + return this.projects.get(item.data?._projectKey)?.onCompletionResolve(item) ?? null } onCodeAction(params: CodeActionParams): Promise { diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 63a5edf3..7bdbd962 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -21,7 +21,7 @@ import isObject from './util/isObject' import * as emmetHelper from 'vscode-emmet-helper-bundled' import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation' import { isJsDoc, isJsxContext } from './util/js' -import { naturalExpand } from './util/naturalExpand' +import { createNaturalExpand, naturalExpand } from './util/naturalExpand' import * as semver from './util/semver' import { docsUrl } from './util/docsUrl' import { ensureArray } from './util/array' @@ -102,7 +102,7 @@ export function completionsFromClassList( items: modifiers.map((modifier, index) => { let className = `${beforeSlash}/${modifier}` let kind: CompletionItemKind = 21 - let documentation: string = null + let documentation: string | undefined const color = getColor(state, className) if (color !== null) { @@ -114,10 +114,9 @@ export function completionsFromClassList( return { label: className, - documentation, + ...(documentation ? { documentation } : {}), kind, sortText: naturalExpand(index), - data: [className], textEdit: { newText: className, range: replacementRange, @@ -141,13 +140,11 @@ export function completionsFromClassList( let variantOrder = 0 function variantItem( - item: Omit & { - textEdit?: { newText: string; range?: Range } - } + item: Omit ): CompletionItem { return { kind: 9, - data: 'variant', + data: { _type: 'variant' }, command: item.insertTextFormat === 2 // Snippet ? undefined @@ -157,11 +154,6 @@ export function completionsFromClassList( }, sortText: '-' + naturalExpand(variantOrder++), ...item, - textEdit: { - newText: item.label, - range: replacementRange, - ...item.textEdit, - }, } } @@ -174,9 +166,7 @@ export function completionsFromClassList( variantItem({ label: `${variant.name}${variant.hasDash ? '-' : ''}[]${sep}`, insertTextFormat: 2, - textEdit: { - newText: `${variant.name}${variant.hasDash ? '-' : ''}[\${1}]${sep}\${0}`, - }, + textEditText: `${variant.name}${variant.hasDash ? '-' : ''}[\${1}]${sep}\${0}`, // command: { // title: '', // command: 'tailwindCSS.onInsertArbitraryVariantSnippet', @@ -199,9 +189,7 @@ export function completionsFromClassList( variantItem({ label: `${variant.name}${sep}`, detail: variant.selectors().join(', '), - textEdit: { - newText: resultingVariants[resultingVariants.length - 1] + sep, - }, + textEditText: resultingVariants[resultingVariants.length - 1] + sep, additionalTextEdits: shouldSortVariants && resultingVariants.length > 1 ? [ @@ -248,31 +236,38 @@ export function completionsFromClassList( } if (state.classList) { - return { - isIncomplete: false, - items: items.concat( - state.classList.map(([className, { color }], index) => { - let kind: CompletionItemKind = color ? 16 : 21 - let documentation = null - - if (color && typeof color !== 'string') { - documentation = culori.formatRgb(color) - } + let naturalExpand = createNaturalExpand(state.classList.length) - return { - label: className, - kind, - documentation, - sortText: naturalExpand(index), - data: [...existingVariants, important ? `!${className}` : className], - textEdit: { - newText: className, - range: replacementRange, - }, - } as CompletionItem - }) - ), - } + return withDefaults( + { + isIncomplete: false, + items: items.concat( + state.classList.map(([className, { color }], index) => { + let kind: CompletionItemKind = color ? 16 : 21 + let documentation: string | undefined + + if (color && typeof color !== 'string') { + documentation = culori.formatRgb(color) + } + + return { + label: className, + kind, + ...(documentation ? { documentation } : {}), + sortText: naturalExpand(index), + } as CompletionItem + }) + ), + }, + { + data: { + ...(important ? { important } : {}), + variants: existingVariants, + }, + range: replacementRange, + }, + state.editor.capabilities.itemDefaults + ) } return { @@ -289,7 +284,7 @@ export function completionsFromClassList( }) .map((className, index) => { let kind: CompletionItemKind = 21 - let documentation: string = null + let documentation: string | undefined const color = getColor(state, className) if (color !== null) { @@ -302,9 +297,12 @@ export function completionsFromClassList( return { label: className, kind, - documentation, + ...(documentation ? { documentation } : {}), sortText: naturalExpand(index), - data: [...existingVariants, important ? `!${className}` : className], + data: { + variants: existingVariants, + ...(important ? { important } : {}), + }, textEdit: { newText: className, range: replacementRange, @@ -350,13 +348,12 @@ export function completionsFromClassList( return { label: className + sep, kind: 9, - documentation: null, command: { title: '', command: 'editor.action.triggerSuggest', }, sortText: '-' + naturalExpand(index), - data: [...subsetKey, className], + data: { className, variants: subsetKey }, textEdit: { newText: className + sep, range: replacementRange, @@ -370,7 +367,7 @@ export function completionsFromClassList( ) .map((className, index) => { let kind: CompletionItemKind = 21 - let documentation: string = null + let documentation: string | undefined const color = getColor(state, className) if (color !== null) { @@ -383,9 +380,9 @@ export function completionsFromClassList( return { label: className, kind, - documentation, + ...(documentation ? { documentation } : {}), sortText: naturalExpand(index), - data: [...subsetKey, className], + data: { variants: subsetKey }, textEdit: { newText: className, range: replacementRange, @@ -569,7 +566,9 @@ function provideAtApplyCompletions( semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses') ) } - let validated = validateApply(state, item.data) + let variants = item.data?.variants ?? [] + let className = item.data?.className ?? item.label + let validated = validateApply(state, [...variants, className]) return validated !== null && validated.isApplyable === true } ) @@ -707,10 +706,9 @@ function provideCssHelperCompletions( kind: color ? 16 : isObject(obj[item]) ? 9 : 10, // VS Code bug causes some values to not display in some cases detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail, - documentation: - color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 - ? culori.formatRgb(color) - : null, + ...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 + ? { documentation: culori.formatRgb(color) } + : {}), textEdit: { newText: `${item}${insertClosingBrace ? ']' : ''}`, range: editRange, @@ -729,7 +727,7 @@ function provideCssHelperCompletions( }, ] : [], - data: 'helper', + data: { _type: 'helper' }, } }), } @@ -821,7 +819,7 @@ function provideTailwindDirectiveCompletions( ].map((item) => ({ ...item, kind: 21, - data: '@tailwind', + data: { _type: '@tailwind' }, textEdit: { newText: item.label, range: { @@ -886,7 +884,7 @@ function provideVariantsDirectiveCompletions( label: variant, detail: state.variants[variant], kind: 21, - data: 'variant', + data: { _type: 'variant' }, sortText: naturalExpand(index), textEdit: { newText: variant, @@ -925,7 +923,7 @@ function provideLayerDirectiveCompletions( items: ['base', 'components', 'utilities'].map((layer, index) => ({ label: layer, kind: 21, - data: 'layer', + data: { _type: 'layer' }, sortText: naturalExpand(index), textEdit: { newText: layer, @@ -941,6 +939,44 @@ function provideLayerDirectiveCompletions( } } +function withDefaults( + completionList: CompletionList, + defaults: Partial<{ data: any; range: Range }>, + supportedDefaults: string[] +): CompletionList { + let defaultData = supportedDefaults.includes('data') + let defaultRange = supportedDefaults.includes('editRange') + + return { + ...completionList, + ...(defaultData || defaultRange + ? { + itemDefaults: { + ...(defaultData && defaults.data ? { data: defaults.data } : {}), + ...(defaultRange && defaults.range ? { editRange: defaults.range } : {}), + }, + } + : {}), + items: + defaultData && defaultRange + ? completionList.items + : completionList.items.map(({ textEditText, ...item }) => ({ + ...item, + ...(defaultData || !defaults.data ? {} : { data: defaults.data }), + ...(defaultRange || !defaults.range + ? textEditText + ? { textEditText } + : {} + : { + textEdit: { + newText: textEditText ?? item.label, + range: defaults.range, + }, + }), + })), + } +} + function provideScreenDirectiveCompletions( state: State, document: TextDocument, @@ -963,25 +999,27 @@ function provideScreenDirectiveCompletions( if (!isObject(screens)) return null - return { - isIncomplete: false, - items: Object.keys(screens).map((screen, index) => ({ - label: screen, - kind: 21, - data: 'screen', - sortText: naturalExpand(index), - textEdit: { - newText: screen, - range: { - start: { - line: position.line, - character: position.character - match.groups.partial.length, - }, - end: position, + return withDefaults( + { + isIncomplete: false, + items: Object.keys(screens).map((screen, index) => ({ + label: screen, + kind: 21, + sortText: naturalExpand(index), + })), + }, + { + data: { _type: 'screen' }, + range: { + start: { + line: position.line, + character: position.character - match.groups.partial.length, }, + end: position, }, - })), - } + }, + state.editor.capabilities.itemDefaults + ) } function provideCssDirectiveCompletions( @@ -1089,24 +1127,26 @@ function provideCssDirectiveCompletions( : []), ] - return { - isIncomplete: false, - items: items.map((item) => ({ - ...item, - kind: 14, - data: 'directive', - textEdit: { - newText: item.label, - range: { - start: { - line: position.line, - character: position.character - match.groups.partial.length - 1, - }, - end: position, + return withDefaults( + { + isIncomplete: false, + items: items.map((item) => ({ + ...item, + kind: 14, + })), + }, + { + data: { _type: 'directive' }, + range: { + start: { + line: position.line, + character: position.character - match.groups.partial.length - 1, }, + end: position, }, - })), - } + }, + state.editor.capabilities.itemDefaults + ) } async function provideConfigDirectiveCompletions( @@ -1136,6 +1176,7 @@ async function provideConfigDirectiveCompletions( items: (await state.editor.readDirectory(document, valueBeforeLastSlash || '.')) .filter(([name, type]) => type.isDirectory || /\.c?js$/.test(name)) .map(([name, type]) => ({ + data: { _type: 'filesystem' }, label: type.isDirectory ? name + '/' : name, kind: type.isDirectory ? 19 : 17, textEdit: { @@ -1255,25 +1296,31 @@ export async function resolveCompletionItem( state: State, item: CompletionItem ): Promise { - if (['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)) { + if ( + ['helper', 'directive', 'variant', 'layer', '@tailwind', 'filesystem'].includes( + item.data?._type + ) + ) { return item } - if (item.data === 'screen') { + if (item.data?._type === 'screen') { let screens = dlv(state.config, ['theme', 'screens'], dlv(state.config, ['screens'], {})) if (!isObject(screens)) screens = {} item.detail = stringifyScreen(screens[item.label] as Screen) return item } - if (!Array.isArray(item.data)) { - return item + let className = item.data?.className ?? item.label + if (item.data?.important) { + className = `!${className}` } + let variants = item.data?.variants ?? [] if (state.jit) { if (item.kind === 9) return item if (item.detail && item.documentation) return item - let { root, rules } = jit.generateRules(state, [item.data.join(state.separator)]) + let { root, rules } = jit.generateRules(state, [[...variants, className].join(state.separator)]) if (rules.length === 0) return item if (!item.detail) { if (rules.length === 1) { @@ -1291,14 +1338,14 @@ export async function resolveCompletionItem( return item } - const className = dlv(state.classNames.classNames, [...item.data, '__info']) + const rules = dlv(state.classNames.classNames, [...variants, className, '__info']) if (item.kind === 9) { - item.detail = state.classNames.context[item.data[item.data.length - 1]].join(', ') + item.detail = state.classNames.context[className].join(', ') } else { - item.detail = await getCssDetail(state, className) + item.detail = await getCssDetail(state, rules) if (!item.documentation) { const settings = await state.editor.getConfiguration() - const css = stringifyCss(item.data.join(':'), className, settings) + const css = stringifyCss([...variants, className].join(':'), rules, settings) if (css) { item.documentation = { kind: 'markdown' as typeof MarkupKind.Markdown, diff --git a/packages/tailwindcss-language-service/src/util/naturalExpand.ts b/packages/tailwindcss-language-service/src/util/naturalExpand.ts index bce1690b..141aff76 100644 --- a/packages/tailwindcss-language-service/src/util/naturalExpand.ts +++ b/packages/tailwindcss-language-service/src/util/naturalExpand.ts @@ -6,3 +6,14 @@ export function naturalExpand(value: number | string): string { let str = typeof value === 'string' ? value : value.toString() return str.replace(/\d+/g, pad) } + +export function createNaturalExpand(total: number) { + let length = total.toString().length + function pad(n: string): string { + return ('0'.repeat(length) + n).substr(-length) + } + return function naturalExpand(value: number | string): string { + let str = typeof value === 'string' ? value : value.toString() + return str.replace(/\d+/g, pad) + } +} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 3787bdcf..902521ef 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -25,6 +25,7 @@ export type EditorState = { capabilities: { configuration: boolean diagnosticRelatedInformation: boolean + itemDefaults: string[] } getConfiguration: (uri?: string) => Promise getDocumentSymbols: (uri: string) => Promise diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 434d802a..0cd66304 100755 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -24,7 +24,7 @@ "vscode" ], "engines": { - "vscode": "^1.65.0" + "vscode": "^1.67.0" }, "categories": [ "Linters", From d9e68c4517b8d9e82a2b42ddf3e42fd23f9fb938 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 27 Jan 2023 10:28:09 +0000 Subject: [PATCH 2/2] more defaults --- .../tailwindcss-language-server/src/server.ts | 27 +- .../src/completionProvider.ts | 699 ++++++++++-------- .../src/util/naturalExpand.ts | 21 +- .../src/util/state.ts | 1 + 4 files changed, 381 insertions(+), 367 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 5dbf6a61..190503a8 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -390,6 +390,9 @@ async function createProjectService( let state: State = { enabled: false, + completionItemData: { + _projectKey: projectKey, + }, editor: { connection, folder, @@ -1125,29 +1128,7 @@ async function createProjectService( let settings = await state.editor.getConfiguration(document.uri) if (!settings.tailwindCSS.suggestions) return null if (await isExcluded(state, document)) return null - let result = await doComplete(state, document, params.position, params.context) - if (!result) return result - - let supportsDefaults = state.editor.capabilities.itemDefaults.length > 0 - let supportsDefaultData = state.editor.capabilities.itemDefaults.includes('data') - - return { - isIncomplete: result.isIncomplete, - ...(supportsDefaults - ? { - itemDefaults: { - ...(result.itemDefaults ?? {}), - ...(supportsDefaultData - ? { data: { _projectKey: projectKey, ...(result.itemDefaults?.data ?? {}) } } - : {}), - }, - } - : {}), - items: result.items.map((item) => ({ - ...item, - ...(item.data ? { data: { _projectKey: projectKey, ...item.data } } : {}), - })), - } + return doComplete(state, document, params.position, params.context) }, null) }, onCompletionResolve(item: CompletionItem): Promise { diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 7bdbd962..9f7a246c 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -21,7 +21,7 @@ import isObject from './util/isObject' import * as emmetHelper from 'vscode-emmet-helper-bundled' import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation' import { isJsDoc, isJsxContext } from './util/js' -import { createNaturalExpand, naturalExpand } from './util/naturalExpand' +import { naturalExpand } from './util/naturalExpand' import * as semver from './util/semver' import { docsUrl } from './util/docsUrl' import { ensureArray } from './util/array' @@ -97,33 +97,36 @@ export function completionsFromClassList( } if (modifiers) { - return { - isIncomplete: false, - items: modifiers.map((modifier, index) => { - let className = `${beforeSlash}/${modifier}` - let kind: CompletionItemKind = 21 - let documentation: string | undefined - - const color = getColor(state, className) - if (color !== null) { - kind = 16 - if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) { - documentation = culori.formatRgb(color) + return withDefaults( + { + isIncomplete: false, + items: modifiers.map((modifier, index) => { + let className = `${beforeSlash}/${modifier}` + let kind: CompletionItemKind = 21 + let documentation: string | undefined + + const color = getColor(state, className) + if (color !== null) { + kind = 16 + if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) { + documentation = culori.formatRgb(color) + } } - } - return { - label: className, - ...(documentation ? { documentation } : {}), - kind, - sortText: naturalExpand(index), - textEdit: { - newText: className, - range: replacementRange, - }, - } - }), - } + return { + label: className, + ...(documentation ? { documentation } : {}), + kind, + sortText: naturalExpand(index), + } + }), + }, + { + range: replacementRange, + data: state.completionItemData, + }, + state.editor.capabilities.itemDefaults + ) } } @@ -144,7 +147,10 @@ export function completionsFromClassList( ): CompletionItem { return { kind: 9, - data: { _type: 'variant' }, + data: { + ...(state.completionItemData ?? {}), + _type: 'variant', + }, command: item.insertTextFormat === 2 // Snippet ? undefined @@ -236,8 +242,6 @@ export function completionsFromClassList( } if (state.classList) { - let naturalExpand = createNaturalExpand(state.classList.length) - return withDefaults( { isIncomplete: false, @@ -254,13 +258,14 @@ export function completionsFromClassList( label: className, kind, ...(documentation ? { documentation } : {}), - sortText: naturalExpand(index), + sortText: naturalExpand(index, state.classList.length), } as CompletionItem }) ), }, { data: { + ...(state.completionItemData ?? {}), ...(important ? { important } : {}), variants: existingVariants, }, @@ -270,19 +275,106 @@ export function completionsFromClassList( ) } - return { + return withDefaults( + { + isIncomplete: false, + items: items + .concat( + Object.keys(state.classNames.classNames) + .filter((className) => { + let item = state.classNames.classNames[className] + if (existingVariants.length === 0) { + return item.__info + } + return item.__info && isUtil(item) + }) + .map((className, index, classNames) => { + let kind: CompletionItemKind = 21 + let documentation: string | undefined + + const color = getColor(state, className) + if (color !== null) { + kind = 16 + if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) { + documentation = culori.formatRgb(color) + } + } + + return { + label: className, + kind, + ...(documentation ? { documentation } : {}), + sortText: naturalExpand(index, classNames.length), + } as CompletionItem + }) + ) + .filter((item) => { + if (item === null) { + return false + } + if (filter && !filter(item)) { + return false + } + return true + }), + }, + { + range: replacementRange, + data: { + ...(state.completionItemData ?? {}), + variants: existingVariants, + ...(important ? { important } : {}), + }, + }, + state.editor.capabilities.itemDefaults + ) + } + + for (let i = parts.length - 1; i > 0; i--) { + let keys = parts.slice(0, i).filter(Boolean) + subset = dlv(state.classNames.classNames, keys) + if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') { + isSubset = true + subsetKey = keys + replacementRange = { + ...replacementRange, + start: { + ...replacementRange.start, + character: replacementRange.start.character + keys.join(sep).length + sep.length, + }, + } + break + } + } + + return withDefaults( + { isIncomplete: false, - items: items + items: Object.keys(isSubset ? subset : state.classNames.classNames) + .filter((k) => k !== '__info') + .filter((className) => isContextItem(state, [...subsetKey, className])) + .map((className, index, classNames): CompletionItem => { + return { + label: className + sep, + kind: 9, + command: { + title: '', + command: 'editor.action.triggerSuggest', + }, + sortText: '-' + naturalExpand(index, classNames.length), + data: { + ...(state.completionItemData ?? {}), + className, + variants: subsetKey, + }, + } + }) .concat( - Object.keys(state.classNames.classNames) - .filter((className) => { - let item = state.classNames.classNames[className] - if (existingVariants.length === 0) { - return item.__info - } - return item.__info && isUtil(item) - }) - .map((className, index) => { + Object.keys(isSubset ? subset : state.classNames.classNames) + .filter((className) => + dlv(state.classNames.classNames, [...subsetKey, className, '__info']) + ) + .map((className, index, classNames) => { let kind: CompletionItemKind = 21 let documentation: string | undefined @@ -298,16 +390,8 @@ export function completionsFromClassList( label: className, kind, ...(documentation ? { documentation } : {}), - sortText: naturalExpand(index), - data: { - variants: existingVariants, - ...(important ? { important } : {}), - }, - textEdit: { - newText: className, - range: replacementRange, - }, - } as CompletionItem + sortText: naturalExpand(index, classNames.length), + } }) ) .filter((item) => { @@ -319,87 +403,16 @@ export function completionsFromClassList( } return true }), - } - } - - for (let i = parts.length - 1; i > 0; i--) { - let keys = parts.slice(0, i).filter(Boolean) - subset = dlv(state.classNames.classNames, keys) - if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') { - isSubset = true - subsetKey = keys - replacementRange = { - ...replacementRange, - start: { - ...replacementRange.start, - character: replacementRange.start.character + keys.join(sep).length + sep.length, - }, - } - break - } - } - - return { - isIncomplete: false, - items: Object.keys(isSubset ? subset : state.classNames.classNames) - .filter((k) => k !== '__info') - .filter((className) => isContextItem(state, [...subsetKey, className])) - .map((className, index): CompletionItem => { - return { - label: className + sep, - kind: 9, - command: { - title: '', - command: 'editor.action.triggerSuggest', - }, - sortText: '-' + naturalExpand(index), - data: { className, variants: subsetKey }, - textEdit: { - newText: className + sep, - range: replacementRange, - }, - } - }) - .concat( - Object.keys(isSubset ? subset : state.classNames.classNames) - .filter((className) => - dlv(state.classNames.classNames, [...subsetKey, className, '__info']) - ) - .map((className, index) => { - let kind: CompletionItemKind = 21 - let documentation: string | undefined - - const color = getColor(state, className) - if (color !== null) { - kind = 16 - if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) { - documentation = culori.formatRgb(color) - } - } - - return { - label: className, - kind, - ...(documentation ? { documentation } : {}), - sortText: naturalExpand(index), - data: { variants: subsetKey }, - textEdit: { - newText: className, - range: replacementRange, - }, - } - }) - ) - .filter((item) => { - if (item === null) { - return false - } - if (filter && !filter(item)) { - return false - } - return true - }), - } + }, + { + range: replacementRange, + data: { + ...(state.completionItemData ?? {}), + variants: subsetKey, + }, + }, + state.editor.capabilities.itemDefaults + ) } async function provideClassAttributeCompletions( @@ -672,65 +685,72 @@ function provideCssHelperCompletions( end: position, } - return { - isIncomplete: false, - items: Object.keys(obj) - .sort((a, z) => { - let aIsNumber = isNumber(a) - let zIsNumber = isNumber(z) - if (aIsNumber && !zIsNumber) { - return -1 - } - if (!aIsNumber && zIsNumber) { - return 1 - } - if (aIsNumber && zIsNumber) { - return parseFloat(a) - parseFloat(z) - } - return 0 - }) - .map((item, index) => { - let color = getColorFromValue(obj[item]) - const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.') - const insertClosingBrace: boolean = - text.charAt(text.length - 1) !== ']' && - (replaceDot || (separator && separator.endsWith('['))) - const detail = stringifyConfigValue(obj[item]) - - return { - label: item, - sortText: naturalExpand(index), - commitCharacters: [!item.includes('.') && '.', !item.includes('[') && '['].filter( - Boolean - ), - kind: color ? 16 : isObject(obj[item]) ? 9 : 10, - // VS Code bug causes some values to not display in some cases - detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail, - ...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 - ? { documentation: culori.formatRgb(color) } - : {}), - textEdit: { - newText: `${item}${insertClosingBrace ? ']' : ''}`, - range: editRange, - }, - additionalTextEdits: replaceDot - ? [ - { - newText: '[', - range: { - start: { - ...editRange.start, - character: editRange.start.character - 1, + return withDefaults( + { + isIncomplete: false, + items: Object.keys(obj) + .sort((a, z) => { + let aIsNumber = isNumber(a) + let zIsNumber = isNumber(z) + if (aIsNumber && !zIsNumber) { + return -1 + } + if (!aIsNumber && zIsNumber) { + return 1 + } + if (aIsNumber && zIsNumber) { + return parseFloat(a) - parseFloat(z) + } + return 0 + }) + .map((item, index, items) => { + let color = getColorFromValue(obj[item]) + const replaceDot: boolean = + item.indexOf('.') !== -1 && separator && separator.endsWith('.') + const insertClosingBrace: boolean = + text.charAt(text.length - 1) !== ']' && + (replaceDot || (separator && separator.endsWith('['))) + const detail = stringifyConfigValue(obj[item]) + + return { + label: item, + sortText: naturalExpand(index, items.length), + commitCharacters: [!item.includes('.') && '.', !item.includes('[') && '['].filter( + Boolean + ), + kind: color ? 16 : isObject(obj[item]) ? 9 : 10, + // VS Code bug causes some values to not display in some cases + detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail, + ...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 + ? { documentation: culori.formatRgb(color) } + : {}), + ...(insertClosingBrace ? { textEditText: `${item}]` } : {}), + additionalTextEdits: replaceDot + ? [ + { + newText: '[', + range: { + start: { + ...editRange.start, + character: editRange.start.character - 1, + }, + end: editRange.start, }, - end: editRange.start, }, - }, - ] - : [], - data: { _type: 'helper' }, - } - }), - } + ] + : [], + } + }), + }, + { + range: editRange, + data: { + ...(state.completionItemData ?? {}), + _type: 'helper', + }, + }, + state.editor.capabilities.itemDefaults + ) } function provideTailwindDirectiveCompletions( @@ -751,87 +771,94 @@ function provideTailwindDirectiveCompletions( if (match === null) return null - return { - isIncomplete: false, - items: [ - semver.gte(state.version, '1.0.0-beta.1') - ? { - label: 'base', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s base styles and any base styles registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, - } - : { - label: 'preflight', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s base styles, which is a combination of Normalize.css and some additional base styles.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, + let items = [ + semver.gte(state.version, '1.0.0-beta.1') + ? { + label: 'base', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s base styles and any base styles registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, + }, + } + : { + label: 'preflight', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s base styles, which is a combination of Normalize.css and some additional base styles.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, - { - label: 'components', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s component classes and any component classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, }, + { + label: 'components', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s component classes and any component classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, - { - label: 'utilities', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s utility classes and any utility classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, + }, + { + label: 'utilities', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s utility classes and any utility classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, - state.jit && semver.gte(state.version, '2.1.99') - ? { - label: 'variants', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use this directive to control where Tailwind injects the utility variants.\n\nThis directive is considered an advanced escape hatch and it is recommended to omit it whenever possible. If omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'just-in-time-mode#variants-are-inserted-at-tailwind-variants' - )})`, - }, - } - : { - label: 'screens', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use this directive to control where Tailwind injects the responsive variations of each utility.\n\nIf omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, + }, + state.jit && semver.gte(state.version, '2.1.99') + ? { + label: 'variants', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use this directive to control where Tailwind injects the utility variants.\n\nThis directive is considered an advanced escape hatch and it is recommended to omit it whenever possible. If omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'just-in-time-mode#variants-are-inserted-at-tailwind-variants' + )})`, }, - ].map((item) => ({ - ...item, - kind: 21, - data: { _type: '@tailwind' }, - textEdit: { - newText: item.label, - range: { - start: { - line: position.line, - character: position.character - match.groups.partial.length, + } + : { + label: 'screens', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use this directive to control where Tailwind injects the responsive variations of each utility.\n\nIf omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, - end: position, }, + ] + + return withDefaults( + { + isIncomplete: false, + items: items.map((item) => ({ + ...item, + kind: 21, + })), + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: '@tailwind', + }, + range: { + start: { + line: position.line, + character: position.character - match.groups.partial.length, + }, + end: position, }, - })), - } + }, + state.editor.capabilities.itemDefaults + ) } function provideVariantsDirectiveCompletions( @@ -875,29 +902,33 @@ function provideVariantsDirectiveCompletions( possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v)) } - return { - isIncomplete: false, - items: possibleVariants - .filter((v) => existingVariants.indexOf(v) === -1) - .map((variant, index) => ({ - // TODO: detail - label: variant, - detail: state.variants[variant], - kind: 21, - data: { _type: 'variant' }, - sortText: naturalExpand(index), - textEdit: { - newText: variant, - range: { - start: { - line: position.line, - character: position.character - parts[parts.length - 1].length, - }, - end: position, - }, + return withDefaults( + { + isIncomplete: false, + items: possibleVariants + .filter((v) => existingVariants.indexOf(v) === -1) + .map((variant, index, variants) => ({ + // TODO: detail + label: variant, + kind: 21, + sortText: naturalExpand(index, variants.length), + })), + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: 'variant', + }, + range: { + start: { + line: position.line, + character: position.character - parts[parts.length - 1].length, }, - })), - } + end: position, + }, + }, + state.editor.capabilities.itemDefaults + ) } function provideLayerDirectiveCompletions( @@ -918,25 +949,30 @@ function provideLayerDirectiveCompletions( if (match === null) return null - return { - isIncomplete: false, - items: ['base', 'components', 'utilities'].map((layer, index) => ({ - label: layer, - kind: 21, - data: { _type: 'layer' }, - sortText: naturalExpand(index), - textEdit: { - newText: layer, - range: { - start: { - line: position.line, - character: position.character - match.groups.partial.length, - }, - end: position, + return withDefaults( + { + isIncomplete: false, + items: ['base', 'components', 'utilities'].map((layer, index, layers) => ({ + label: layer, + kind: 21, + sortText: naturalExpand(index, layers.length), + })), + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: 'layer', + }, + range: { + start: { + line: position.line, + character: position.character - match.groups.partial.length, }, + end: position, }, - })), - } + }, + state.editor.capabilities.itemDefaults + ) } function withDefaults( @@ -962,7 +998,7 @@ function withDefaults( ? completionList.items : completionList.items.map(({ textEditText, ...item }) => ({ ...item, - ...(defaultData || !defaults.data ? {} : { data: defaults.data }), + ...(defaultData || !defaults.data || item.data ? {} : { data: defaults.data }), ...(defaultRange || !defaults.range ? textEditText ? { textEditText } @@ -1009,7 +1045,10 @@ function provideScreenDirectiveCompletions( })), }, { - data: { _type: 'screen' }, + data: { + ...(state.completionItemData ?? {}), + _type: 'screen', + }, range: { start: { line: position.line, @@ -1136,7 +1175,10 @@ function provideCssDirectiveCompletions( })), }, { - data: { _type: 'directive' }, + data: { + ...(state.completionItemData ?? {}), + _type: 'directive', + }, range: { start: { line: position.line, @@ -1171,29 +1213,34 @@ async function provideConfigDirectiveCompletions( let valueBeforeLastSlash = partial.substring(0, partial.lastIndexOf('/')) let valueAfterLastSlash = partial.substring(partial.lastIndexOf('/') + 1) - return { - isIncomplete: false, - items: (await state.editor.readDirectory(document, valueBeforeLastSlash || '.')) - .filter(([name, type]) => type.isDirectory || /\.c?js$/.test(name)) - .map(([name, type]) => ({ - data: { _type: 'filesystem' }, - label: type.isDirectory ? name + '/' : name, - kind: type.isDirectory ? 19 : 17, - textEdit: { - newText: type.isDirectory ? name + '/' : name, - range: { - start: { - line: position.line, - character: position.character - valueAfterLastSlash.length, - }, - end: position, - }, + return withDefaults( + { + isIncomplete: false, + items: (await state.editor.readDirectory(document, valueBeforeLastSlash || '.')) + .filter(([name, type]) => type.isDirectory || /\.c?js$/.test(name)) + .map(([name, type]) => ({ + label: type.isDirectory ? name + '/' : name, + kind: type.isDirectory ? 19 : 17, + command: type.isDirectory + ? { command: 'editor.action.triggerSuggest', title: '' } + : undefined, + })), + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: 'filesystem', + }, + range: { + start: { + line: position.line, + character: position.character - valueAfterLastSlash.length, }, - command: type.isDirectory - ? { command: 'editor.action.triggerSuggest', title: '' } - : undefined, - })), - } + end: position, + }, + }, + state.editor.capabilities.itemDefaults + ) } async function provideEmmetCompletions( diff --git a/packages/tailwindcss-language-service/src/util/naturalExpand.ts b/packages/tailwindcss-language-service/src/util/naturalExpand.ts index 141aff76..bedbd804 100644 --- a/packages/tailwindcss-language-service/src/util/naturalExpand.ts +++ b/packages/tailwindcss-language-service/src/util/naturalExpand.ts @@ -1,19 +1,4 @@ -function pad(n: string): string { - return ('00000000' + n).substr(-8) -} - -export function naturalExpand(value: number | string): string { - let str = typeof value === 'string' ? value : value.toString() - return str.replace(/\d+/g, pad) -} - -export function createNaturalExpand(total: number) { - let length = total.toString().length - function pad(n: string): string { - return ('0'.repeat(length) + n).substr(-length) - } - return function naturalExpand(value: number | string): string { - let str = typeof value === 'string' ? value : value.toString() - return str.replace(/\d+/g, pad) - } +export function naturalExpand(value: number, total?: number): string { + let length = typeof total === 'number' ? total.toString().length : 8 + return ('0'.repeat(length) + value).slice(-length) } diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 902521ef..cba7c64f 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -119,6 +119,7 @@ export interface State { jitContext?: any classList?: Array<[string, { color: culori.Color | KeywordColor | null; modifiers?: string[] }]> pluginVersions?: string + completionItemData?: Record // postcssPlugins?: { before: any[]; after: any[] } }