Skip to content
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

feat(vscode): resolve config by file path #1101

Merged
merged 8 commits into from Jun 18, 2022
4 changes: 2 additions & 2 deletions packages/autocomplete/src/create.ts
Expand Up @@ -2,10 +2,10 @@ import type { AutoCompleteExtractorResult, AutoCompleteFunction, AutoCompleteTem
import { escapeRegExp, toArray, uniq } from '@unocss/core'
import LRU from 'lru-cache'
import { parseAutocomplete } from './parse'
import type { ParsedAutocompleteTemplate } from './types'
import type { ParsedAutocompleteTemplate, UnocssAutocomplete } from './types'
import { searchUsageBoundary } from './utils'

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

Expand Down
12 changes: 12 additions & 0 deletions packages/autocomplete/src/types.ts
@@ -1,3 +1,6 @@
import type { AutoCompleteFunction, SuggestResult } from '@unocss/core'
import type LRU from 'lru-cache'

export type AutocompleteTemplatePart = AutocompleteTemplateStatic | AutocompleteTemplateGroup | AutocompleteTemplateTheme

export interface AutocompleteTemplateStatic {
Expand All @@ -19,3 +22,12 @@ export interface ParsedAutocompleteTemplate {
parts: AutocompleteTemplatePart[]
suggest(input: string): string[] | undefined
}

export interface UnocssAutocomplete {
suggest: (input: string) => Promise<string[]>
suggestInFile: (content: string, cursor: number) => Promise<SuggestResult>
templates: (string | AutoCompleteFunction)[]
cache: LRU<string, string[]>
reset: () => void
enumerate: () => Promise<Set<string>>
}
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Expand Up @@ -462,10 +462,12 @@ export interface UnocssPluginContext<Config extends UserConfig = UserConfig> {
/** Map for all module's raw content */
modules: BetterMap<string, string>
filter: (code: string, id: string) => boolean
rollupFilter: (id: unknown) => boolean
QiroNT marked this conversation as resolved.
Show resolved Hide resolved
extract: (code: string, id?: string) => Promise<void>

reloadConfig: () => Promise<LoadConfigResult<Config>>
getConfig: () => Promise<Config>
onReload: (fn: () => void) => void

invalidate: () => void
onInvalidate: (fn: () => void) => void
Expand Down
10 changes: 10 additions & 0 deletions packages/shared-integration/src/context.ts
Expand Up @@ -18,6 +18,7 @@ export function createContext<Config extends UserConfig<any> = UserConfig<any>>(
let rollupFilter = createFilter(defaultInclude, defaultExclude)

const invalidations: Array<() => void> = []
const reloadListeners: Array<() => void> = []

const modules = new BetterMap<string, string>()
const tokens = new Set<string>()
Expand All @@ -38,6 +39,7 @@ export function createContext<Config extends UserConfig<any> = UserConfig<any>>(
tokens.clear()
await Promise.all(modules.map((code, id) => uno.applyExtractors(code, id, tokens)))
invalidate()
dispatchReload()

// check preset duplication
const presets = new Set<string>()
Expand Down Expand Up @@ -65,6 +67,10 @@ export function createContext<Config extends UserConfig<any> = UserConfig<any>>(
invalidations.forEach(cb => cb())
}

function dispatchReload() {
reloadListeners.forEach(cb => cb())
}

async function extract(code: string, id?: string) {
if (id)
modules.set(id, code)
Expand Down Expand Up @@ -96,7 +102,11 @@ export function createContext<Config extends UserConfig<any> = UserConfig<any>>(
invalidations.push(fn)
},
filter,
rollupFilter,
reloadConfig,
onReload(fn: () => void) {
reloadListeners.push(fn)
},
uno,
extract,
getConfig,
Expand Down
30 changes: 19 additions & 11 deletions packages/vscode/src/annonation.ts
@@ -1,19 +1,17 @@
import { relative } from 'path'
import path from 'path'
import type { DecorationOptions, ExtensionContext, StatusBarItem } from 'vscode'
import { DecorationRangeBehavior, MarkdownString, Range, window, workspace } from 'vscode'
import type { UnocssPluginContext } from '@unocss/core'
import { INCLUDE_COMMENT_IDE, getMatchedPositions } from './integration'
import { log } from './log'
import { getPrettiedMarkdown, isCssId, throttle } from './utils'
import type { ContextLoader } from './contextLoader'

export async function registerAnnonations(
cwd: string,
context: UnocssPluginContext,
contextLoader: ContextLoader,
status: StatusBarItem,
ext: ExtensionContext,
) {
const { sources } = await context.ready
const { uno, filter } = context
let underline: boolean = workspace.getConfiguration().get('unocss.underline') ?? true
ext.subscriptions.push(workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('unocss.underline')) {
Expand All @@ -23,10 +21,14 @@ export async function registerAnnonations(
}))

workspace.onDidSaveTextDocument(async (doc) => {
if (sources.includes(doc.uri.fsPath)) {
const id = doc.uri.fsPath
const dir = path.dirname(id)

if (contextLoader.contexts.has(dir)) {
const ctx = contextLoader.contexts.get(dir)!
try {
await context.reloadConfig()
log.appendLine(`Config reloaded by ${relative(cwd, doc.uri.fsPath)}`)
await ctx.reloadConfig()
log.appendLine(`Config reloaded by ${path.relative(cwd, doc.uri.fsPath)}`)
}
catch (e) {
log.appendLine('Error on loading config')
Expand Down Expand Up @@ -54,17 +56,23 @@ export async function registerAnnonations(
const code = doc.getText()
const id = doc.uri.fsPath

if (!code || (!code.includes(INCLUDE_COMMENT_IDE) && !isCssId(id) && !filter(code, id)))
if (!code)
return reset()

const result = await uno.generate(code, { id, preflights: false, minify: true })
let ctx = await contextLoader.resolveContext(id)
if (!ctx && (code.includes(INCLUDE_COMMENT_IDE) || isCssId(id)))
ctx = await contextLoader.resolveCloestContext(id)
else if (!ctx?.filter(code, id))
return null

const result = await ctx.uno.generate(code, { id, preflights: false, minify: true })

const ranges: DecorationOptions[] = (
await Promise.all(
getMatchedPositions(code, Array.from(result.matched))
.map(async (i): Promise<DecorationOptions> => {
try {
const md = await getPrettiedMarkdown(uno, i[2])
const md = await getPrettiedMarkdown(ctx!.uno, i[2])
return {
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
get hoverMessage() {
Expand Down
58 changes: 46 additions & 12 deletions packages/vscode/src/autocomplete.ts
@@ -1,9 +1,11 @@
import type { UnocssPluginContext } from '@unocss/core'
import type { UnocssAutocomplete } from '@unocss/autocomplete'
import { createAutocomplete } from '@unocss/autocomplete'
import type { CompletionItemProvider, ExtensionContext, Position, TextDocument } from 'vscode'
import type { CompletionItemProvider, ExtensionContext } from 'vscode'
import { CompletionItem, CompletionItemKind, CompletionList, MarkdownString, Range, languages } from 'vscode'
import type { UnoGenerator, UnocssPluginContext } from '@unocss/core'
import { getPrettiedMarkdown, isCssId } from './utils'
import { log } from './log'
import type { ContextLoader } from './contextLoader'

const languageIds = [
'erb',
Expand All @@ -24,27 +26,59 @@ const languageIds = [
]
const delimiters = ['-', ':']

class UnoCompletionItem extends CompletionItem {
uno: UnoGenerator

constructor(label: string, kind: CompletionItemKind, uno: UnoGenerator) {
super(label, kind)
this.uno = uno
}
}

export async function registerAutoComplete(
context: UnocssPluginContext,
contextLoader: ContextLoader,
ext: ExtensionContext,
) {
const { uno, filter } = context
const autoCompletes = new Map<UnocssPluginContext, UnocssAutocomplete>()
contextLoader.events.on('contextReload', (ctx) => {
autoCompletes.delete(ctx)
})
contextLoader.events.on('contextUnload', (ctx) => {
autoCompletes.delete(ctx)
})

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

const autoComplete = createAutocomplete(uno)
const autocomplete = createAutocomplete(ctx.uno)

async function getMarkdown(util: string) {
autoCompletes.set(ctx, autocomplete)
return autocomplete
}

async function getMarkdown(uno: UnoGenerator, util: string) {
return new MarkdownString(await getPrettiedMarkdown(uno, util))
}

const provider: CompletionItemProvider = {
async provideCompletionItems(doc: TextDocument, position: Position) {
const provider: CompletionItemProvider<UnoCompletionItem> = {
async provideCompletionItems(doc, position) {
const code = doc.getText()
const id = doc.uri.fsPath

if (!code || (!isCssId(id) && !filter(code, id)))
if (!code)
return null

let ctx = await contextLoader.resolveContext(id)
if (!ctx && isCssId(id))
ctx = await contextLoader.resolveCloestContext(id)
else if (!ctx?.filter(code, id))
return null

try {
const autoComplete = getAutocomplete(ctx)

const result = await autoComplete.suggestInFile(code, doc.offsetAt(position))

log.appendLine(`[autocomplete] ${id} | ${result.suggestions.slice(0, 10).map(v => `[${v[0]}, ${v[1]}]`).join(', ')}`)
Expand All @@ -54,7 +88,7 @@ export async function registerAutoComplete(

return new CompletionList(result.suggestions.map(([value, label]) => {
const resolved = result.resolveReplacement(value)
const item = new CompletionItem(label, CompletionItemKind.EnumMember)
const item = new UnoCompletionItem(label, CompletionItemKind.EnumMember, ctx!.uno)
item.insertText = resolved.replacement
item.range = new Range(doc.positionAt(resolved.start), doc.positionAt(resolved.end))
return item
Expand All @@ -66,10 +100,10 @@ export async function registerAutoComplete(
}
},

async resolveCompletionItem(item: CompletionItem) {
async resolveCompletionItem(item) {
return {
...item,
documentation: await getMarkdown(item.label as string),
documentation: await getMarkdown(item.uno, item.label as string),
}
},
}
Expand Down