diff --git a/packages/vscode/package.json b/packages/vscode/package.json index c946898c9e..aa445f17e9 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,6 +1,6 @@ { "publisher": "antfu", - "name": "@unocss/vscode", + "name": "@vscode/unocss", "displayName": "UnoCSS", "version": "0.41.2", "private": true, @@ -47,6 +47,11 @@ "type": "boolean", "default": true, "description": "Enable/disable underline decoration for class names" + }, + "unocss.colorPreview": { + "type": "boolean", + "default": true, + "description": "Enable/disable color preview decorations" } } } diff --git a/packages/vscode/src/annotation.ts b/packages/vscode/src/annotation.ts index efcee91ea3..b9810ec942 100644 --- a/packages/vscode/src/annotation.ts +++ b/packages/vscode/src/annotation.ts @@ -3,7 +3,7 @@ import type { DecorationOptions, ExtensionContext, StatusBarItem } from 'vscode' import { DecorationRangeBehavior, MarkdownString, Range, window, workspace } from 'vscode' import { INCLUDE_COMMENT_IDE, getMatchedPositions } from './integration' import { log } from './log' -import { getPrettiedMarkdown, isCssId, throttle } from './utils' +import { getColorsMap, getPrettiedMarkdown, isCssId, throttle } from './utils' import type { ContextLoader } from './contextLoader' export async function registerAnnotations( @@ -13,11 +13,16 @@ export async function registerAnnotations( ext: ExtensionContext, ) { let underline: boolean = workspace.getConfiguration().get('unocss.underline') ?? true + let colorPreview: boolean = workspace.getConfiguration().get('unocss.colorPreview') ?? true ext.subscriptions.push(workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration('unocss.underline')) { underline = workspace.getConfiguration().get('unocss.underline') ?? true updateAnnotation() } + if (event.affectsConfiguration('unocss.colorPreview')) { + colorPreview = workspace.getConfiguration().get('unocss.colorPreview') ?? true + updateAnnotation() + } })) workspace.onDidSaveTextDocument(async (doc) => { @@ -47,6 +52,26 @@ export async function registerAnnotations( rangeBehavior: DecorationRangeBehavior.ClosedClosed, }) + const colorDecoration = window.createTextEditorDecorationType({ + before: { + width: '0.9em', + height: '0.9em', + contentText: ' ', + border: '1px solid', + margin: 'auto 0.2em auto 0;vertical-align: middle;border-radius:50%;', + }, + dark: { + before: { + borderColor: '#eeeeee50', + }, + }, + light: { + before: { + borderColor: '#00000050', + }, + }, + }) + async function updateAnnotation(editor = window.activeTextEditor) { try { const doc = editor?.document @@ -67,10 +92,23 @@ export async function registerAnnotations( const result = await ctx.uno.generate(code, { id, preflights: false, minify: true }) + const colorsMap = getColorsMap(ctx.uno, result) + const colorRanges: DecorationOptions[] = [] + const _colorPositionsCache = new Map() // cache for avoid duplicated color ranges + const ranges: DecorationOptions[] = ( await Promise.all( getMatchedPositions(code, Array.from(result.matched)) .map(async (i): Promise => { + // side-effect: update colorRanges + if (colorPreview && colorsMap.has(i[2]) && !_colorPositionsCache.has(`${i[0]}:${i[1]}`)) { + _colorPositionsCache.set(`${i[0]}:${i[1]}`, i[2]) + colorRanges.push({ + range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])), + renderOptions: { before: { backgroundColor: colorsMap.get(i[2]) } }, + }) + } + try { const md = await getPrettiedMarkdown(ctx!.uno, i[2]) return { @@ -89,6 +127,9 @@ export async function registerAnnotations( ) ).filter(Boolean) + _colorPositionsCache.clear() + editor.setDecorations(colorDecoration, colorRanges) + if (underline) { editor.setDecorations(NoneDecoration, []) editor.setDecorations(UnderlineDecoration, ranges) @@ -105,6 +146,7 @@ export async function registerAnnotations( function reset() { editor?.setDecorations(UnderlineDecoration, []) editor?.setDecorations(NoneDecoration, []) + editor?.setDecorations(colorDecoration, []) status.hide() } } diff --git a/packages/vscode/src/utils.ts b/packages/vscode/src/utils.ts index 2d21151018..c20c8d9a4f 100644 --- a/packages/vscode/src/utils.ts +++ b/packages/vscode/src/utils.ts @@ -1,8 +1,11 @@ import path from 'path' -import type { UnoGenerator } from '@unocss/core' +import type { GenerateResult, UnoGenerator } from '@unocss/core' import { cssIdRE } from '@unocss/core' import prettier from 'prettier/standalone' import parserCSS from 'prettier/parser-postcss' +import type { Theme } from '@unocss/preset-mini' +import { parseColor } from '@unocss/preset-mini' +import { colorToString } from '@unocss/preset-mini/utils' export function throttle any)>(func: T, timeFrame: number): T { let lastTime = 0 @@ -37,6 +40,45 @@ export async function getPrettiedMarkdown(uno: UnoGenerator, util: string) { return `\`\`\`css\n${(await getPrettiedCSS(uno, util)).prettified}\n\`\`\`` } +const matchedAttributifyRE = /(?<=^\[.+~?=").*(?="\]$)/ +const _colorsMapCache = new Map() +export function getColorsMap(uno: UnoGenerator, result: GenerateResult) { + const theme = uno.config.theme as Theme + const themeColorNames = Object.keys(theme.colors ?? {}) + const colorNames = themeColorNames.concat(themeColorNames.map(colorName => colorName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase())) + const colorsMap = new Map() + + for (const i of result.matched) { + const _i = i.replace('~="', '="') + if (_colorsMapCache.get(_i)) { + colorsMap.set(_i, _colorsMapCache.get(_i)!) + continue + } + + const matchedAttr = i.match(matchedAttributifyRE) + const body = matchedAttr ? matchedAttr[0].split(':').at(-1) ?? '' : i // remove prefix e.g. `dark:` `hover:` + + for (const colorName of colorNames) { + const nameIndex = body.indexOf(colorName) + if (nameIndex > -1) { + const parsedResult = parseColor(body.substring(nameIndex), theme) + if (parsedResult?.cssColor) { + const color = colorToString(parsedResult.cssColor, parsedResult.alpha) + colorsMap.set(_i, color) + _colorsMapCache.set(_i, color) + } + + break + } + } + } + + if (_colorsMapCache.size > 5000) + _colorsMapCache.clear() + + return colorsMap +} + export function isCssId(id: string) { return cssIdRE.test(id) }