Skip to content

Commit

Permalink
feat(vscode): add rem to px preview (#2808)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
chizukicn and antfu committed Jul 8, 2023
1 parent 7daa253 commit aa9bb4f
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 45 deletions.
10 changes: 10 additions & 0 deletions packages/vscode/package.json
Expand Up @@ -67,6 +67,16 @@
"type": "boolean",
"default": true,
"description": "Enable/disable color preview decorations"
},
"unocss.remToPxPreview": {

Check failure on line 71 in packages/vscode/package.json

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 8 spaces but found 9
"type": "boolean",
"default": false,
"description": "Enable/disable rem to px preview in hover"
},
"unocss.remToPxRatio": {
"type": "number",
"default": 16,
"description": "Ratio of rem to px"
},
"unocss.selectionStyle": {
"type": "boolean",
Expand Down
27 changes: 12 additions & 15 deletions packages/vscode/src/annotation.ts
Expand Up @@ -5,26 +5,19 @@ import { INCLUDE_COMMENT_IDE, getMatchedPositionsFromCode, isCssId } from './int
import { log } from './log'
import { getColorString, getPrettiedMarkdown, isSubdir, throttle } from './utils'
import type { ContextLoader } from './contextLoader'
import { useConfigurations } from './configuration'

export async function registerAnnotations(
cwd: string,
contextLoader: ContextLoader,
status: StatusBarItem,
ext: ExtensionContext,
) {
let underline: boolean = workspace.getConfiguration().get('unocss.underline') ?? true
let colorPreview: boolean = workspace.getConfiguration().get('unocss.colorPreview') ?? true
const { configuration, watchChanged } = useConfigurations(ext)

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()
}
}))
watchChanged(['underline', 'colorPreview', 'remToPxPreview', 'remToPxRatio'], () => {
updateAnnotation()
})

workspace.onDidSaveTextDocument(async (doc) => {
const id = doc.uri.fsPath
Expand Down Expand Up @@ -105,14 +98,18 @@ export async function registerAnnotations(

const colorRanges: DecorationOptions[] = []

const remToPxRatio = configuration.remToPxPreview
? configuration.remToPxRatio
: -1

const ranges: DecorationOptions[] = (
await Promise.all(
(await getMatchedPositionsFromCode(ctx.uno, code))
.map(async (i): Promise<DecorationOptions> => {
try {
const md = await getPrettiedMarkdown(ctx!.uno, i[2])
const md = await getPrettiedMarkdown(ctx!.uno, i[2], remToPxRatio)

if (colorPreview) {
if (configuration.colorPreview) {
const color = getColorString(md)
if (color && !colorRanges.find(r => r.range.start.isEqual(doc.positionAt(i[0])))) {
colorRanges.push({
Expand All @@ -139,7 +136,7 @@ export async function registerAnnotations(

editor.setDecorations(colorDecoration, colorRanges)

if (underline) {
if (configuration.underline) {
editor.setDecorations(NoneDecoration, [])
editor.setDecorations(UnderlineDecoration, ranges)
}
Expand Down
49 changes: 24 additions & 25 deletions packages/vscode/src/autocomplete.ts
@@ -1,4 +1,4 @@
import type { AutoCompleteMatchType, UnocssAutocomplete } from '@unocss/autocomplete'
import type { 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 All @@ -7,6 +7,7 @@ import { getCSS, getColorString, getPrettiedCSS, getPrettiedMarkdown, isSubdir }
import { log } from './log'
import type { ContextLoader } from './contextLoader'
import { isCssId } from './integration'
import { useConfigurations } from './configuration'

const defaultLanguageIds = [
'erb',
Expand Down Expand Up @@ -59,25 +60,23 @@ 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)
const { configuration, watchChanged } = useConfigurations(ext)

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

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

autoCompletes.set(ctx, autocomplete)
return autocomplete
}

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

function validateLanguages(targets: string[]) {
Expand Down Expand Up @@ -124,7 +123,7 @@ export async function registerAutoComplete(

const completionItems: UnoCompletionItem[] = []

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

for (const [value, label] of suggestions) {
const css = await getCSS(ctx!.uno, value)
Expand Down Expand Up @@ -153,10 +152,11 @@ export async function registerAutoComplete(
},

async resolveCompletionItem(item) {
const remToPxRatio = configuration.remToPxRatio ? configuration.remToPxRatio : -1
if (item.kind === CompletionItemKind.Color)
item.detail = await (await getPrettiedCSS(item.uno, item.value)).prettified
item.detail = await (await getPrettiedCSS(item.uno, item.value, remToPxRatio)).prettified
else
item.documentation = await getMarkdown(item.uno, item.value)
item.documentation = await getMarkdown(item.uno, item.value, remToPxRatio)
return item
},
}
Expand All @@ -178,21 +178,20 @@ export async function registerAutoComplete(
return completeUnregister
}

ext.subscriptions.push(workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration('unocss.languageIds')) {
ext.subscriptions.push(
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)
}
}))
watchChanged(['languagesIds'], () => {
ext.subscriptions.push(
registerProvider(),
)
})

watchChanged([
'matchType',
'maxItems',
'remToPxRatio',
'remToPxPreview',
], () => {
autoCompletes.clear()
})

ext.subscriptions.push(
registerProvider(),
Expand Down
100 changes: 100 additions & 0 deletions packages/vscode/src/configuration.ts
@@ -0,0 +1,100 @@
import { toArray } from '@unocss/core'
import type { ExtensionContext } from 'vscode'
import { workspace } from 'vscode'
import type { AutoCompleteMatchType } from '@unocss/autocomplete'
import { createNanoEvents } from '../../core/src/utils/events'

export interface UseConfigurationOptions<Init> {
ext?: ExtensionContext
scope?: string
initialValue: Init
alias?: Partial<Record<keyof Init, string>>
}

export type ConfigurationListenerMap<Init> = Map<keyof Init, WatchConfigurationHandler<Init, keyof Init>>

export type WatchConfigurationHandler<Init, K extends keyof Init> = (value: Init[K]) => void

export function getConfigurations<Init extends Record<string, unknown>>(options: UseConfigurationOptions<Init>) {
const { initialValue, alias, scope, ext } = options
const configuration = {} as Init

const getConfigurationKey = (key: keyof Init) => {
key = alias?.[key] ?? key
return [scope, key].filter(Boolean).join('.')
}

const reload = () => {
const _config = workspace.getConfiguration()
for (const key in initialValue) {
const configurationKey = getConfigurationKey(key)
configuration[key] = _config.get(configurationKey, initialValue[key])
}
}

const reset = () => {
const _config = workspace.getConfiguration()
for (const key in initialValue) {
configuration[key] = initialValue[key]
const configurationKey = getConfigurationKey(key)
_config.update(configurationKey, initialValue[key], true)
}
}

reload()

const emitter = createNanoEvents()

const watchChanged = <K extends keyof Init>(key: K | K[], fn: WatchConfigurationHandler<Init, K>) => {
const keys = toArray(key)
const unsubscribes = keys.map(key => emitter.on(`update:${String(key)}`, fn))
return () => unsubscribes.forEach(fn => fn())
}

const disposable = workspace.onDidChangeConfiguration((e) => {
const _config = workspace.getConfiguration()
const changedKeys = new Set<keyof Init>()

for (const key in initialValue) {
const configurationKey = getConfigurationKey(key)
if (e.affectsConfiguration(configurationKey)) {
const value = _config.get(configurationKey, initialValue[key])
configuration[key as keyof Init] = value as Init[keyof Init]
changedKeys.add(key)
}
}
for (const key of changedKeys)
emitter.emit(`update:${String(key)}`, configuration[key])
})

if (ext)
ext.subscriptions.push(disposable)

return {
configuration,
watchChanged,
disposable,
reload,
reset,
}
}

export function useConfigurations(ext: ExtensionContext) {
return getConfigurations({
ext,
scope: 'unocss',
initialValue: {
colorPreview: true,
languagesIds: <string[]>[],
matchType: <AutoCompleteMatchType>'prefix',
maxItems: 1000,
remToPxPreview: false,
remToPxRatio: 16,
underline: true,
},
alias: {
matchType: 'autocomplete.matchType',
maxItems: 'autocomplete.maxItems',
},
})
}
38 changes: 34 additions & 4 deletions packages/vscode/src/utils.ts
Expand Up @@ -24,9 +24,39 @@ export async function getCSS(uno: UnoGenerator, utilName: string) {
return css
}

export async function getPrettiedCSS(uno: UnoGenerator, util: string) {
/**
*
* Credit to [@voorjaar](https://github.com/voorjaar)
* @see https://github.com/windicss/windicss-intellisense/issues/13
* @param str
* @returns
*/
export function addRemToPxComment(str?: string, remToPixel = 16) {
if (!str)
return ''
if (remToPixel < 1)
return str
let index = 0
const output: string[] = []

while (index < str.length) {
const rem = str.slice(index).match(/-?[\d.]+rem;/)
if (!rem || !rem.index)
break
const px = ` /* ${Number.parseFloat(rem[0].slice(0, -4)) * remToPixel}px */`
const end = index + rem.index + rem[0].length
output.push(str.slice(index, end))
output.push(px)
index = end
}
output.push(str.slice(index))
return output.join('')
}

export async function getPrettiedCSS(uno: UnoGenerator, util: string, remToPxRatio: number) {
const result = (await uno.generate(new Set([util]), { preflights: false, safelist: false }))
const prettified = prettier.format(result.css, {
const css = addRemToPxComment(result.css, remToPxRatio)
const prettified = prettier.format(css, {
parser: 'css',
plugins: [parserCSS],
})
Expand All @@ -37,8 +67,8 @@ export async function getPrettiedCSS(uno: UnoGenerator, util: string) {
}
}

export async function getPrettiedMarkdown(uno: UnoGenerator, util: string) {
return `\`\`\`css\n${(await getPrettiedCSS(uno, util)).prettified}\n\`\`\``
export async function getPrettiedMarkdown(uno: UnoGenerator, util: string, remToPxRatio: number) {
return `\`\`\`css\n${(await getPrettiedCSS(uno, util, remToPxRatio)).prettified}\n\`\`\``
}

function getCssVariables(code: string) {
Expand Down
21 changes: 20 additions & 1 deletion test/utils.test.ts
@@ -1,7 +1,7 @@
import { mergeDeep } from '@unocss/core'
import { getComponent } from '@unocss/preset-mini/utils'
import { expect, it } from 'vitest'
import { getColorString } from '@unocss/vscode/utils'
import { addRemToPxComment, getColorString } from '@unocss/vscode/utils'

it('mergeDeep', () => {
expect(mergeDeep<any>({
Expand Down Expand Up @@ -95,3 +95,22 @@ it('getColorString', () => {
expect(getColorString(bgAmber)).eql('rgba(251, 191, 36, 1)')
expect(getColorString(bgAmberImportant)).eql('rgba(251, 191, 36, 1)')
})

it('addRemToPxComment', () => {
const text = `
/* layer: default */
.m-9 {
margin: 2.25rem;
}`

for (let i = 10; i < 32; i += 1) {
expect(addRemToPxComment(text, i)).eql(`
/* layer: default */
.m-9 {
margin: 2.25rem; /* ${i / 4 * 9}px */
}`)
}

expect(addRemToPxComment(text, 0)).eql(text)
expect(addRemToPxComment(text, -1)).eql(text)
})

0 comments on commit aa9bb4f

Please sign in to comment.