Use `itemDefaults` to reduce size of completion lists (#706)

* Use completion list `itemDefaults`

* more defaults
master
Brad Cornes 2023-01-27 10:30:27 +00:00 committed by GitHub
parent 7235aeab48
commit 637f838725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 520 additions and 422 deletions

View File

@ -378,8 +378,21 @@ async function createProjectService(
const disposables: Array<Disposable | Promise<Disposable>> = []
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,
completionItemData: {
_projectKey: projectKey,
},
editor: {
connection,
folder,
@ -390,6 +403,7 @@ async function createProjectService(
capabilities: {
configuration: true,
diagnosticRelatedInformation: true,
itemDefaults,
},
documents: documentService.documents,
getConfiguration,
@ -1114,21 +1128,13 @@ 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
return {
isIncomplete: result.isIncomplete,
items: result.items.map((item) => ({
...item,
data: { projectKey, originalData: item.data },
})),
}
return doComplete(state, document, params.position, params.context)
}, null)
},
onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
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<CodeAction[]> {
@ -2162,7 +2168,7 @@ class TW {
}
async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
return this.projects.get(item.data.projectKey)?.onCompletionResolve(item) ?? null
return this.projects.get(item.data?._projectKey)?.onCompletionResolve(item) ?? null
}
onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {

View File

@ -97,12 +97,13 @@ export function completionsFromClassList(
}
if (modifiers) {
return {
return withDefaults(
{
isIncomplete: false,
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,17 +115,18 @@ export function completionsFromClassList(
return {
label: className,
documentation,
...(documentation ? { documentation } : {}),
kind,
sortText: naturalExpand(index),
data: [className],
textEdit: {
newText: className,
range: replacementRange,
},
}
}),
}
},
{
range: replacementRange,
data: state.completionItemData,
},
state.editor.capabilities.itemDefaults
)
}
}
@ -141,13 +143,14 @@ export function completionsFromClassList(
let variantOrder = 0
function variantItem(
item: Omit<CompletionItem, 'kind' | 'data' | 'sortText' | 'textEdit'> & {
textEdit?: { newText: string; range?: Range }
}
item: Omit<CompletionItem, 'kind' | 'data' | 'command' | 'sortText' | 'textEdit'>
): CompletionItem {
return {
kind: 9,
data: 'variant',
data: {
...(state.completionItemData ?? {}),
_type: 'variant',
},
command:
item.insertTextFormat === 2 // Snippet
? undefined
@ -157,11 +160,6 @@ export function completionsFromClassList(
},
sortText: '-' + naturalExpand(variantOrder++),
...item,
textEdit: {
newText: item.label,
range: replacementRange,
...item.textEdit,
},
}
}
@ -174,9 +172,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 +195,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,12 +242,13 @@ export function completionsFromClassList(
}
if (state.classList) {
return {
return withDefaults(
{
isIncomplete: false,
items: items.concat(
state.classList.map(([className, { color }], index) => {
let kind: CompletionItemKind = color ? 16 : 21
let documentation = null
let documentation: string | undefined
if (color && typeof color !== 'string') {
documentation = culori.formatRgb(color)
@ -262,20 +257,26 @@ export function completionsFromClassList(
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...existingVariants, important ? `!${className}` : className],
textEdit: {
newText: className,
range: replacementRange,
},
...(documentation ? { documentation } : {}),
sortText: naturalExpand(index, state.classList.length),
} as CompletionItem
})
),
}
},
{
data: {
...(state.completionItemData ?? {}),
...(important ? { important } : {}),
variants: existingVariants,
},
range: replacementRange,
},
state.editor.capabilities.itemDefaults
)
}
return {
return withDefaults(
{
isIncomplete: false,
items: items
.concat(
@ -287,9 +288,9 @@ export function completionsFromClassList(
}
return item.__info && isUtil(item)
})
.map((className, index) => {
.map((className, index, classNames) => {
let kind: CompletionItemKind = 21
let documentation: string = null
let documentation: string | undefined
const color = getColor(state, className)
if (color !== null) {
@ -302,13 +303,8 @@ export function completionsFromClassList(
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...existingVariants, important ? `!${className}` : className],
textEdit: {
newText: className,
range: replacementRange,
},
...(documentation ? { documentation } : {}),
sortText: naturalExpand(index, classNames.length),
} as CompletionItem
})
)
@ -321,7 +317,17 @@ export function completionsFromClassList(
}
return true
}),
}
},
{
range: replacementRange,
data: {
...(state.completionItemData ?? {}),
variants: existingVariants,
...(important ? { important } : {}),
},
},
state.editor.capabilities.itemDefaults
)
}
for (let i = parts.length - 1; i > 0; i--) {
@ -341,25 +347,25 @@ export function completionsFromClassList(
}
}
return {
return withDefaults(
{
isIncomplete: false,
items: Object.keys(isSubset ? subset : state.classNames.classNames)
.filter((k) => k !== '__info')
.filter((className) => isContextItem(state, [...subsetKey, className]))
.map((className, index): CompletionItem => {
.map((className, index, classNames): CompletionItem => {
return {
label: className + sep,
kind: 9,
documentation: null,
command: {
title: '',
command: 'editor.action.triggerSuggest',
},
sortText: '-' + naturalExpand(index),
data: [...subsetKey, className],
textEdit: {
newText: className + sep,
range: replacementRange,
sortText: '-' + naturalExpand(index, classNames.length),
data: {
...(state.completionItemData ?? {}),
className,
variants: subsetKey,
},
}
})
@ -368,9 +374,9 @@ export function completionsFromClassList(
.filter((className) =>
dlv(state.classNames.classNames, [...subsetKey, className, '__info'])
)
.map((className, index) => {
.map((className, index, classNames) => {
let kind: CompletionItemKind = 21
let documentation: string = null
let documentation: string | undefined
const color = getColor(state, className)
if (color !== null) {
@ -383,13 +389,8 @@ export function completionsFromClassList(
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...subsetKey, className],
textEdit: {
newText: className,
range: replacementRange,
},
...(documentation ? { documentation } : {}),
sortText: naturalExpand(index, classNames.length),
}
})
)
@ -402,7 +403,16 @@ export function completionsFromClassList(
}
return true
}),
}
},
{
range: replacementRange,
data: {
...(state.completionItemData ?? {}),
variants: subsetKey,
},
},
state.editor.capabilities.itemDefaults
)
}
async function provideClassAttributeCompletions(
@ -569,7 +579,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
}
)
@ -673,7 +685,8 @@ function provideCssHelperCompletions(
end: position,
}
return {
return withDefaults(
{
isIncomplete: false,
items: Object.keys(obj)
.sort((a, z) => {
@ -690,9 +703,10 @@ function provideCssHelperCompletions(
}
return 0
})
.map((item, index) => {
.map((item, index, items) => {
let color = getColorFromValue(obj[item])
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
const replaceDot: boolean =
item.indexOf('.') !== -1 && separator && separator.endsWith('.')
const insertClosingBrace: boolean =
text.charAt(text.length - 1) !== ']' &&
(replaceDot || (separator && separator.endsWith('[')))
@ -700,21 +714,17 @@ function provideCssHelperCompletions(
return {
label: item,
sortText: naturalExpand(index),
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,
documentation:
color && typeof color !== 'string' && (color.alpha ?? 1) !== 0
? culori.formatRgb(color)
: null,
textEdit: {
newText: `${item}${insertClosingBrace ? ']' : ''}`,
range: editRange,
},
...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0
? { documentation: culori.formatRgb(color) }
: {}),
...(insertClosingBrace ? { textEditText: `${item}]` } : {}),
additionalTextEdits: replaceDot
? [
{
@ -729,10 +739,18 @@ function provideCssHelperCompletions(
},
]
: [],
data: 'helper',
}
}),
}
},
{
range: editRange,
data: {
...(state.completionItemData ?? {}),
_type: 'helper',
},
},
state.editor.capabilities.itemDefaults
)
}
function provideTailwindDirectiveCompletions(
@ -753,9 +771,7 @@ function provideTailwindDirectiveCompletions(
if (match === null) return null
return {
isIncomplete: false,
items: [
let items = [
semver.gte(state.version, '1.0.0-beta.1')
? {
label: 'base',
@ -818,12 +834,21 @@ function provideTailwindDirectiveCompletions(
)})`,
},
},
].map((item) => ({
]
return withDefaults(
{
isIncomplete: false,
items: items.map((item) => ({
...item,
kind: 21,
data: '@tailwind',
textEdit: {
newText: item.label,
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: '@tailwind',
},
range: {
start: {
line: position.line,
@ -832,8 +857,8 @@ function provideTailwindDirectiveCompletions(
end: position,
},
},
})),
}
state.editor.capabilities.itemDefaults
)
}
function provideVariantsDirectiveCompletions(
@ -877,19 +902,23 @@ function provideVariantsDirectiveCompletions(
possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v))
}
return {
return withDefaults(
{
isIncomplete: false,
items: possibleVariants
.filter((v) => existingVariants.indexOf(v) === -1)
.map((variant, index) => ({
.map((variant, index, variants) => ({
// TODO: detail
label: variant,
detail: state.variants[variant],
kind: 21,
data: 'variant',
sortText: naturalExpand(index),
textEdit: {
newText: variant,
sortText: naturalExpand(index, variants.length),
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: 'variant',
},
range: {
start: {
line: position.line,
@ -898,8 +927,8 @@ function provideVariantsDirectiveCompletions(
end: position,
},
},
})),
}
state.editor.capabilities.itemDefaults
)
}
function provideLayerDirectiveCompletions(
@ -920,15 +949,20 @@ function provideLayerDirectiveCompletions(
if (match === null) return null
return {
return withDefaults(
{
isIncomplete: false,
items: ['base', 'components', 'utilities'].map((layer, index) => ({
items: ['base', 'components', 'utilities'].map((layer, index, layers) => ({
label: layer,
kind: 21,
data: 'layer',
sortText: naturalExpand(index),
textEdit: {
newText: layer,
sortText: naturalExpand(index, layers.length),
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: 'layer',
},
range: {
start: {
line: position.line,
@ -937,6 +971,44 @@ function provideLayerDirectiveCompletions(
end: position,
},
},
state.editor.capabilities.itemDefaults
)
}
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 || item.data ? {} : { data: defaults.data }),
...(defaultRange || !defaults.range
? textEditText
? { textEditText }
: {}
: {
textEdit: {
newText: textEditText ?? item.label,
range: defaults.range,
},
}),
})),
}
}
@ -963,15 +1035,20 @@ function provideScreenDirectiveCompletions(
if (!isObject(screens)) return null
return {
return withDefaults(
{
isIncomplete: false,
items: Object.keys(screens).map((screen, index) => ({
label: screen,
kind: 21,
data: 'screen',
sortText: naturalExpand(index),
textEdit: {
newText: screen,
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: 'screen',
},
range: {
start: {
line: position.line,
@ -980,8 +1057,8 @@ function provideScreenDirectiveCompletions(
end: position,
},
},
})),
}
state.editor.capabilities.itemDefaults
)
}
function provideCssDirectiveCompletions(
@ -1089,14 +1166,19 @@ function provideCssDirectiveCompletions(
: []),
]
return {
return withDefaults(
{
isIncomplete: false,
items: items.map((item) => ({
...item,
kind: 14,
data: 'directive',
textEdit: {
newText: item.label,
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: 'directive',
},
range: {
start: {
line: position.line,
@ -1105,8 +1187,8 @@ function provideCssDirectiveCompletions(
end: position,
},
},
})),
}
state.editor.capabilities.itemDefaults
)
}
async function provideConfigDirectiveCompletions(
@ -1131,15 +1213,24 @@ async function provideConfigDirectiveCompletions(
let valueBeforeLastSlash = partial.substring(0, partial.lastIndexOf('/'))
let valueAfterLastSlash = partial.substring(partial.lastIndexOf('/') + 1)
return {
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,
textEdit: {
newText: type.isDirectory ? name + '/' : name,
command: type.isDirectory
? { command: 'editor.action.triggerSuggest', title: '' }
: undefined,
})),
},
{
data: {
...(state.completionItemData ?? {}),
_type: 'filesystem',
},
range: {
start: {
line: position.line,
@ -1148,11 +1239,8 @@ async function provideConfigDirectiveCompletions(
end: position,
},
},
command: type.isDirectory
? { command: 'editor.action.triggerSuggest', title: '' }
: undefined,
})),
}
state.editor.capabilities.itemDefaults
)
}
async function provideEmmetCompletions(
@ -1255,25 +1343,31 @@ export async function resolveCompletionItem(
state: State,
item: CompletionItem
): Promise<CompletionItem> {
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 +1385,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,

View File

@ -1,8 +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 naturalExpand(value: number, total?: number): string {
let length = typeof total === 'number' ? total.toString().length : 8
return ('0'.repeat(length) + value).slice(-length)
}

View File

@ -25,6 +25,7 @@ export type EditorState = {
capabilities: {
configuration: boolean
diagnosticRelatedInformation: boolean
itemDefaults: string[]
}
getConfiguration: (uri?: string) => Promise<Settings>
getDocumentSymbols: (uri: string) => Promise<SymbolInformation[]>
@ -118,6 +119,7 @@ export interface State {
jitContext?: any
classList?: Array<[string, { color: culori.Color | KeywordColor | null; modifiers?: string[] }]>
pluginVersions?: string
completionItemData?: Record<string, any>
// postcssPlugins?: { before: any[]; after: any[] }
}

View File

@ -24,7 +24,7 @@
"vscode"
],
"engines": {
"vscode": "^1.65.0"
"vscode": "^1.67.0"
},
"categories": [
"Linters",