diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 838069c96f..1f475d8346 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -60,6 +60,11 @@ "type": "boolean", "default": true, "description": "Enable/disable color preview decorations" + }, + "unocss.selectionStyle": { + "type": "boolean", + "default": true, + "description": "Enable/disable selection style decorations" } } } diff --git a/packages/vscode/src/index.ts b/packages/vscode/src/index.ts index 7996b7e530..ff78279ba8 100644 --- a/packages/vscode/src/index.ts +++ b/packages/vscode/src/index.ts @@ -6,6 +6,7 @@ import { log } from './log' import { registerAnnotations } from './annotation' import { registerAutoComplete } from './autocomplete' import { ContextLoader } from './contextLoader' +import { registerSelectionStyle } from './selectionStyle' export async function activate(ext: ExtensionContext) { log.appendLine(`⚪️ UnoCSS for VS Code v${version}\n`) @@ -50,6 +51,7 @@ export async function activate(ext: ExtensionContext) { registerAutoComplete(cwd, contextLoader, ext) registerAnnotations(cwd, contextLoader, status, ext) + registerSelectionStyle(cwd, contextLoader) } export function deactivate() {} diff --git a/packages/vscode/src/selectionStyle.ts b/packages/vscode/src/selectionStyle.ts new file mode 100644 index 0000000000..87d9c318e9 --- /dev/null +++ b/packages/vscode/src/selectionStyle.ts @@ -0,0 +1,113 @@ +import { MarkdownString, Position, Range, window, workspace } from 'vscode' +import parserCSS from 'prettier/parser-postcss' +import prettier from 'prettier/standalone' +import type { TextEditorSelectionChangeEvent } from 'vscode' +import { regexScopePlaceholder } from '@unocss/core' +import { log } from './log' +import { throttle } from './utils' +import type { ContextLoader } from './contextLoader' +import { getMatchedPositionsFromCode } from './integration' + +export async function registerSelectionStyle(cwd: string, contextLoader: ContextLoader) { + const hasSelectionStyle = (): boolean => workspace.getConfiguration().get('unocss.selectionStyle') ?? true + + const integrationDecoration = window.createTextEditorDecorationType({}) + + async function selectionStyle(editor: TextEditorSelectionChangeEvent) { + try { + if (!hasSelectionStyle()) + return reset() + + const doc = editor.textEditor.document + if (!doc) + return reset() + + const id = doc.uri.fsPath + const selection = editor.textEditor.selection + const range = new Range( + new Position(selection.start.line, selection.start.character), + new Position(selection.end.line, selection.end.character), + ) + let code = editor.textEditor.document.getText(range).trim() + if (!code.startsWith('<')) + code = `
')) + code = `${code} >` + const ctx = await contextLoader.resolveContext(code, id) || (await contextLoader.resolveClosestContext(code, id)) + const result = await getMatchedPositionsFromCode(ctx.uno, code) + if (!result.length) + return reset() + + const uniqMap = new Map() + for (const [start, end, className] of result) + uniqMap.set(`${start}-${end}`, className) + + const classNamePlaceholder = '___' + const sheetMap = new Map() + const mediaSheetMap = new Map>() + await Promise.all(Array.from(uniqMap.values()) + .map(async (name) => { + const tokens = await ctx.uno.parseToken(name, classNamePlaceholder) || [] + tokens.forEach(([, className, cssText, media]) => { + if (className && cssText) { + const key = className + .replace(`.${classNamePlaceholder}`, '&') + .replace(regexScopePlaceholder, ' ') + .trim() + + let activeSheetMap = sheetMap + if (media) { + if (!mediaSheetMap.has(media)) + mediaSheetMap.set(media, new Map()) + + activeSheetMap = mediaSheetMap.get(media)! + } + + activeSheetMap.set(key, (activeSheetMap.get(key) || '') + cssText) + } + }) + }), + ) + + const css = Array.from(sheetMap.keys()) + .sort() + .map(key => `${key}{${sheetMap.get(key)}}`) + .concat( + Array.from(mediaSheetMap.keys()) + .sort() + .map((media) => { + const sheetMap = mediaSheetMap.get(media)! + return `${media}{${ + Array.from(sheetMap!.keys()) + .sort() + .map(key => `${key}{${sheetMap!.get(key)}}`) + .join('\n') + }}` + }), + ) + .join('\n') + + const prettified = prettier.format(css, { + parser: 'css', + plugins: [parserCSS], + }) + + editor.textEditor.setDecorations(integrationDecoration, [{ + range, + get hoverMessage() { + return new MarkdownString(`UnoCSS utilities in the selection will be equivalent to:\n\`\`\`css\n${prettified.trim()}\n\`\`\``) + }, + }]) + + function reset() { + editor.textEditor.setDecorations(integrationDecoration, []) + } + } + catch (e) { + log.appendLine('⚠️ Error on selectionStyle') + log.appendLine(String(e)) + } + } + + window.onDidChangeTextEditorSelection(throttle(selectionStyle, 200)) +} diff --git a/packages/vscode/src/utils.ts b/packages/vscode/src/utils.ts index 1ea1443ca1..dd23e47db1 100644 --- a/packages/vscode/src/utils.ts +++ b/packages/vscode/src/utils.ts @@ -9,15 +9,15 @@ import { colorToString } from '@unocss/preset-mini/utils' export function throttle any)>(func: T, timeFrame: number): T { let lastTime = 0 let timer: any - return function () { + return function (...args) { const now = Date.now() clearTimeout(timer) if (now - lastTime >= timeFrame) { lastTime = now - return func() + return func(...args) } else { - timer = setTimeout(func, timeFrame) + timer = setTimeout(func, timeFrame, ...args) } } as T }