Skip to content

Commit

Permalink
feat(vscode): support autocomplete with fuzzy search. (#2769)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris <1633711653@qq.com>
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
3 people committed Jun 23, 2023
1 parent 8aa8d3c commit f611dca
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/autocomplete/package.json
Expand Up @@ -37,6 +37,7 @@
"stub": "unbuild --stub"
},
"dependencies": {
"fzf": "^0.5.2",
"lru-cache": "^10.0.0"
},
"devDependencies": {
Expand Down
16 changes: 12 additions & 4 deletions packages/autocomplete/src/create.ts
@@ -1,17 +1,22 @@
import type { AutoCompleteExtractorResult, AutoCompleteFunction, AutoCompleteTemplate, SuggestResult, UnoGenerator, Variant } from '@unocss/core'
import { escapeRegExp, toArray, uniq } from '@unocss/core'
import { LRUCache } from 'lru-cache'
import { Fzf } from 'fzf'
import { parseAutocomplete } from './parse'
import type { ParsedAutocompleteTemplate, UnocssAutocomplete } from './types'
import type { AutocompleteOptions, ParsedAutocompleteTemplate, UnocssAutocomplete } from './types'
import { searchAttrKey, searchUsageBoundary } from './utils'

export function createAutocomplete(uno: UnoGenerator): UnocssAutocomplete {
export function createAutocomplete(uno: UnoGenerator, options: AutocompleteOptions = {}): UnocssAutocomplete {
const templateCache = new Map<string, ParsedAutocompleteTemplate>()
const cache = new LRUCache<string, string[]>({ max: 5000 })

let staticUtils: string[] = []
let staticFzf = new Fzf<string[]>([])

const templates: (AutoCompleteTemplate | AutoCompleteFunction)[] = []

const matchType = options.matchType ?? 'prefix'

reset()

return {
Expand Down Expand Up @@ -133,6 +138,8 @@ export function createAutocomplete(uno: UnoGenerator): UnocssAutocomplete {
}

async function suggestStatic(input: string) {
if (matchType === 'fuzzy')
return staticFzf.find(input).map(i => i.item)
return staticUtils.filter(i => i.startsWith(input))
}

Expand All @@ -146,7 +153,7 @@ export function createAutocomplete(uno: UnoGenerator): UnocssAutocomplete {
return templates.map(fn =>
typeof fn === 'function'
? fn(input)
: getParsed(fn)(input),
: getParsed(fn)(input, matchType),
) || []
}

Expand All @@ -157,7 +164,7 @@ export function createAutocomplete(uno: UnoGenerator): UnocssAutocomplete {
.map(fn =>
typeof fn === 'function'
? fn(input)
: getParsed(fn)(input),
: getParsed(fn)(input, matchType),
)
}

Expand All @@ -168,6 +175,7 @@ export function createAutocomplete(uno: UnoGenerator): UnocssAutocomplete {
...Object.keys(uno.config.rulesStaticMap),
...uno.config.shortcuts.filter(i => typeof i[0] === 'string').map(i => i[0] as string),
]
staticFzf = new Fzf(staticUtils)
templates.length = 0
templates.push(
...uno.config.autocomplete.templates || [],
Expand Down
44 changes: 42 additions & 2 deletions packages/autocomplete/src/parse.ts
@@ -1,4 +1,7 @@
import type { AutocompleteTemplatePart, ParsedAutocompleteTemplate } from './types'
import { uniq } from '@unocss/core'
import { Fzf } from 'fzf'
import type { AutoCompleteMatchType, AutocompleteTemplatePart, ParsedAutocompleteTemplate } from './types'
import { cartesian } from './utils'

export const shorthands: Record<string, string> = {
num: `(${[0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 24, 36].join('|')})`,
Expand Down Expand Up @@ -39,6 +42,8 @@ export function parseAutocomplete(template: string, theme: any = {}): ParsedAuto

handleGroups(template)

const fzf = new Fzf(getAllCombination(parts))

return {
parts,
suggest,
Expand Down Expand Up @@ -85,7 +90,9 @@ export function parseAutocomplete(template: string, theme: any = {}): ParsedAuto
)
}

function suggest(input: string) {
function suggest(input: string, matchType: AutoCompleteMatchType = 'prefix') {
if (input.length && matchType === 'fuzzy')
return fzf.find(input).map(i => i.item)
let rest = input
let matched = ''
let combinations: string[] = []
Expand Down Expand Up @@ -179,3 +186,36 @@ export function parseAutocomplete(template: string, theme: any = {}): ParsedAuto
.filter(i => i.length >= input.length)
}
}

function getValuesFromPartTemplate(part: AutocompleteTemplatePart): string[] {
if (part.type === 'static')
return [part.value]
if (part.type === 'theme') {
return part.objects.flatMap((i) => {
const keys = Object.keys(i).filter(i => i && i[0] !== '_')
for (const key in i) {
const value = i[key]
if (value === null || value === undefined)
continue
if (typeof value === 'object' && !Array.isArray(value)) {
const subKeys = getValuesFromPartTemplate({
type: 'theme',
objects: [value as Record<string, unknown>],
}).map(i => `${key}-${i}`)

keys.push(...subKeys)
}
}
return keys
})
}
if (part.type === 'group')
return [...part.values]
return []
}

function getAllCombination(parts: AutocompleteTemplatePart[]) {
const values = parts.map(i => getValuesFromPartTemplate(i))
const list = uniq(cartesian(values).flatMap(i => i.join('').replace('-DEFAULT', '')))
return list
}
10 changes: 8 additions & 2 deletions packages/autocomplete/src/types.ts
@@ -1,6 +1,12 @@
import type { AutoCompleteFunction, SuggestResult } from '@unocss/core'
import type { LRUCache } from 'lru-cache'

export type AutoCompleteMatchType = 'prefix' | 'fuzzy'

export interface AutocompleteOptions {
matchType?: AutoCompleteMatchType
}

export type AutocompleteTemplatePart = AutocompleteTemplateStatic | AutocompleteTemplateGroup | AutocompleteTemplateTheme

export interface AutocompleteTemplateStatic {
Expand All @@ -20,11 +26,11 @@ export interface AutocompleteTemplateTheme {

export interface ParsedAutocompleteTemplate {
parts: AutocompleteTemplatePart[]
suggest(input: string): string[] | undefined
suggest(input: string, matchType?: AutoCompleteMatchType): string[] | undefined
}

export interface UnocssAutocomplete {
suggest: (input: string) => Promise<string[]>
suggest: (input: string, allowsEmptyInput?: boolean) => Promise<string[]>
suggestInFile: (content: string, cursor: number) => Promise<SuggestResult>
templates: (string | AutoCompleteFunction)[]
cache: LRUCache<string, string[]>
Expand Down
17 changes: 17 additions & 0 deletions packages/autocomplete/src/utils.ts
Expand Up @@ -18,3 +18,20 @@ export function searchAttrKey(content: string, cursor: number) {
if (text.match(/(<\w+\s*)[^>]*$/) !== null)
return text.match(/\S+(?=\s*=\s*["']?[^"']*$)/)?.[0]
}

export function cartesian<T>(arr: T[][]): T[][] {
if (arr.length < 2)
return arr
return arr.reduce(
(a, b) => {
const ret: T[][] = []
a.forEach((a) => {
b.forEach((b) => {
ret.push(a.concat([b]))
})
})
return ret
},
[[]] as T[][],
)
}
14 changes: 14 additions & 0 deletions packages/vscode/package.json
Expand Up @@ -72,6 +72,20 @@
"type": "boolean",
"default": true,
"description": "Enable/disable selection style decorations"
},
"unocss.autocomplete.matchType": {
"type": "string",
"default": "prefix",
"enum": [
"prefix",
"fuzzy"
],
"description": "The matching type for autocomplete"
},
"unocss.autocomplete.maxItems": {
"type": "number",
"default": 1000,
"description": "The maximum number of items to show in autocomplete"
}
}
}
Expand Down
23 changes: 20 additions & 3 deletions packages/vscode/src/autocomplete.ts
@@ -1,4 +1,4 @@
import type { UnocssAutocomplete } from '@unocss/autocomplete'
import type { AutoCompleteMatchType, UnocssAutocomplete } from '@unocss/autocomplete'
import { createAutocomplete } from '@unocss/autocomplete'
import type { CompletionItemProvider, Disposable, ExtensionContext } from 'vscode'
import { CompletionItem, CompletionItemKind, CompletionList, MarkdownString, Range, languages, window, workspace } from 'vscode'
Expand Down Expand Up @@ -59,12 +59,18 @@ export async function registerAutoComplete(
autoCompletes.delete(ctx)
})

let matchType = workspace.getConfiguration().get<AutoCompleteMatchType>('unocss.autocomplete.matchType', 'prefix')

let maxItems = workspace.getConfiguration().get<number>('unocss.autocomplete.maxItems', 1000)

function getAutocomplete(ctx: UnocssPluginContext) {
const cached = autoCompletes.get(ctx)
if (cached)
return cached

const autocomplete = createAutocomplete(ctx.uno)
const autocomplete = createAutocomplete(ctx.uno, {
matchType,
})

autoCompletes.set(ctx, autocomplete)
return autocomplete
Expand Down Expand Up @@ -117,7 +123,10 @@ export async function registerAutoComplete(
return

const completionItems: UnoCompletionItem[] = []
for (const [value, label] of result.suggestions) {

const suggestions = result.suggestions.slice(0, maxItems)

for (const [value, label] of suggestions) {
const css = await getCSS(ctx!.uno, value)
const colorString = getColorString(css)
const itemKind = colorString ? CompletionItemKind.Color : CompletionItemKind.EnumMember
Expand Down Expand Up @@ -175,6 +184,14 @@ export async function registerAutoComplete(
registerProvider(),
)
}
if (event.affectsConfiguration('unocss.autocomplete.matchType')) {
autoCompletes.clear()
matchType = workspace.getConfiguration().get<AutoCompleteMatchType>('unocss.autocomplete.matchType', 'prefix')
}
if (event.affectsConfiguration('unocss.autocomplete.maxItems')) {
autoCompletes.clear()
maxItems = workspace.getConfiguration().get<number>('unocss.autocomplete.maxItems', 1000)
}
}))

ext.subscriptions.push(
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions test/autocomplete-fuzzy.test.ts
@@ -0,0 +1,46 @@
import { createAutocomplete } from '@unocss/autocomplete'
import { createGenerator } from '@unocss/core'
import { describe, expect, it } from 'vitest'
import presetUno from '@unocss/preset-uno'

describe('autocomplete-fuzzy', () => {
const uno = createGenerator({
presets: [
presetUno(),
],
shortcuts: [
{
'foo': 'text-red',
'foo-bar': 'text-red',
},
[/^bg-mode-(.+)$/, ([, mode]) => `bg-blend-${mode}`, { autocomplete: ['bg-mode-(color|normal)'] }],
],
})

const ac = createAutocomplete(uno, {
matchType: 'fuzzy',
})

it('static', async () => {
expect(await ac.suggest('itct'))
.includes('items-center')

expect(await ac.suggest('jc'))
.includes('justify-center')
})

it('variant', async () => {
expect(await ac.suggest('tsm'))
.includes('text-sm')

expect(await ac.suggest('tbl5'))
.includes('text-blue-500')
})

it('shortcuts', async () => {
expect(await ac.suggest('fb'))
.includes('foo-bar')
expect(await ac.suggest('bmc'))
.includes('bg-mode-color')
})
})

0 comments on commit f611dca

Please sign in to comment.