Skip to content

Commit 8cbe6af

Browse files
committed
feat(vscode): introduce strictAnnotationMatch and turn on by default, close #3278
1 parent 532cc8a commit 8cbe6af

File tree

5 files changed

+223
-35
lines changed

5 files changed

+223
-35
lines changed

packages/shared-common/src/index.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,48 @@ export function getPlainClassMatchedPositionsForPug(codeSplit: string, matchedPl
8585
return result
8686
}
8787

88+
export interface GetMatchedPositionsOptions {
89+
isPug?: boolean
90+
/**
91+
* Regex to only limit the matched positions for certain code
92+
*/
93+
includeRegex?: RegExp[]
94+
/**
95+
* Regex to exclude the matched positions for certain code, excludeRegex has higher priority than includeRegex
96+
*/
97+
excludeRegex?: RegExp[]
98+
}
99+
88100
export function getMatchedPositions(
89101
code: string,
90102
matched: string[],
91103
extraAnnotations: HighlightAnnotation[] = [],
92-
isPug = false,
104+
options: GetMatchedPositionsOptions = {},
93105
) {
94106
const result: (readonly [start: number, end: number, text: string])[] = []
95107
const attributify: RegExpMatchArray[] = []
96108
const plain = new Set<string>()
97109

110+
const includeRanges: [number, number][] = []
111+
const excludeRanges: [number, number][] = []
112+
113+
if (options.includeRegex) {
114+
for (const regex of options.includeRegex) {
115+
for (const match of code.matchAll(regex))
116+
includeRanges.push([match.index!, match.index! + match[0].length])
117+
}
118+
}
119+
else {
120+
includeRanges.push([0, code.length])
121+
}
122+
123+
if (options.excludeRegex) {
124+
for (const regex of options.excludeRegex) {
125+
for (const match of code.matchAll(regex))
126+
excludeRanges.push([match.index!, match.index! + match[0].length])
127+
}
128+
}
129+
98130
Array.from(matched)
99131
.forEach((v) => {
100132
const match = isAttributifySelector(v)
@@ -106,7 +138,9 @@ export function getMatchedPositions(
106138
highlightLessGreaterThanSign(match[1])
107139
plain.add(match[1])
108140
}
109-
else { attributify.push(match) }
141+
else {
142+
attributify.push(match)
143+
}
110144
})
111145

112146
// highlight classes that includes `><`
@@ -124,7 +158,7 @@ export function getMatchedPositions(
124158
let start = 0
125159
code.split(splitWithVariantGroupRE).forEach((i) => {
126160
const end = start + i.length
127-
if (isPug) {
161+
if (options.isPug) {
128162
result.push(...getPlainClassMatchedPositionsForPug(i, plain, start))
129163
}
130164
else {
@@ -172,9 +206,18 @@ export function getMatchedPositions(
172206
})
173207
})
174208

175-
result.push(...extraAnnotations.map(i => [i.offset, i.offset + i.length, i.className] as const))
209+
result
210+
.push(...extraAnnotations.map(i => [i.offset, i.offset + i.length, i.className] as const))
176211

177-
return result.sort((a, b) => a[0] - b[0])
212+
return result
213+
.filter(([start, end]) => {
214+
if (excludeRanges.some(([s, e]) => start >= s && end <= e))
215+
return false
216+
if (includeRanges.some(([s, e]) => start >= s && end <= e))
217+
return true
218+
return false
219+
})
220+
.sort((a, b) => a[0] - b[0])
178221
}
179222

180223
// remove @unocss/transformer-directives transformer to get matched result from source code
@@ -183,7 +226,12 @@ const ignoreTransformers = [
183226
'@unocss/transformer-compile-class',
184227
]
185228

186-
export async function getMatchedPositionsFromCode(uno: UnoGenerator, code: string, id = '') {
229+
export async function getMatchedPositionsFromCode(
230+
uno: UnoGenerator,
231+
code: string,
232+
id = '',
233+
options: GetMatchedPositionsOptions = {},
234+
) {
187235
const s = new MagicString(code)
188236
const tokens = new Set()
189237
const ctx = { uno, tokens } as any
@@ -201,5 +249,8 @@ export async function getMatchedPositionsFromCode(uno: UnoGenerator, code: strin
201249

202250
const { pug, code: pugCode } = await isPug(uno, s.toString(), id)
203251
const result = await uno.generate(pug ? pugCode : s.toString(), { preflights: false })
204-
return getMatchedPositions(code, [...result.matched], annotations, pug)
252+
return getMatchedPositions(code, [...result.matched], annotations, {
253+
isPug: pug,
254+
...options,
255+
})
205256
}

packages/shared-integration/src/defaults.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@ export const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|
88
export const defaultFilesystemGlobs = [
99
'**/*.{html,js,ts,jsx,tsx,vue,svelte,astro,elm,php,phtml,mdx,md}',
1010
]
11+
12+
/**
13+
* Default match includes in getMatchedPositions for IDE
14+
*/
15+
export const defaultIdeMatchInclude: RegExp[] = [
16+
// String literals
17+
/(['"`])[^\1]*?\1/g,
18+
// HTML tags
19+
/<[^>]+?>/g,
20+
// CSS directives
21+
/(@apply|--uno|--at-apply)[^;]*?;/g,
22+
]
23+
24+
/**
25+
* Default match includes in getMatchedPositions for IDE
26+
*/
27+
export const defaultIdeMatchExclude: RegExp[] = [
28+
]

packages/vscode/src/annotation.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path'
22
import type { DecorationOptions, Disposable, ExtensionContext, StatusBarItem, TextEditor } from 'vscode'
33
import { DecorationRangeBehavior, MarkdownString, Range, window, workspace } from 'vscode'
4-
import { INCLUDE_COMMENT_IDE, getMatchedPositionsFromCode, isCssId } from './integration'
4+
import { INCLUDE_COMMENT_IDE, defaultIdeMatchExclude, defaultIdeMatchInclude, getMatchedPositionsFromCode, isCssId } from './integration'
55
import { log } from './log'
66
import { getColorString, getPrettiedMarkdown, throttle } from './utils'
77
import type { ContextLoader } from './contextLoader'
@@ -14,7 +14,8 @@ export async function registerAnnotations(
1414
) {
1515
const { configuration, watchChanged, disposable } = useConfigurations(ext)
1616
const disposals: Disposable[] = []
17-
watchChanged(['underline', 'colorPreview', 'remToPxPreview', 'remToPxRatio'], () => {
17+
18+
watchChanged(['underline', 'colorPreview', 'remToPxPreview', 'remToPxRatio', 'strictAnnotationMatch'], () => {
1819
updateAnnotation()
1920
})
2021

@@ -103,35 +104,42 @@ export async function registerAnnotations(
103104
? configuration.remToPxRatio
104105
: -1
105106

107+
const options = configuration.strictAnnotationMatch
108+
? {
109+
includeRegex: defaultIdeMatchInclude,
110+
excludeRegex: defaultIdeMatchExclude,
111+
}
112+
: undefined
113+
114+
const positions = await getMatchedPositionsFromCode(ctx.uno, code, id, options)
115+
106116
const ranges: DecorationOptions[] = (
107-
await Promise.all(
108-
(await getMatchedPositionsFromCode(ctx.uno, code))
109-
.map(async (i): Promise<DecorationOptions> => {
110-
try {
111-
const md = await getPrettiedMarkdown(ctx!.uno, i[2], remToPxRatio)
112-
113-
if (configuration.colorPreview) {
114-
const color = getColorString(md)
115-
if (color && !colorRanges.find(r => r.range.start.isEqual(doc.positionAt(i[0])))) {
116-
colorRanges.push({
117-
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
118-
renderOptions: { before: { backgroundColor: color } },
119-
})
120-
}
121-
}
122-
return {
117+
await Promise.all(positions.map(async (i): Promise<DecorationOptions> => {
118+
try {
119+
const md = await getPrettiedMarkdown(ctx!.uno, i[2], remToPxRatio)
120+
121+
if (configuration.colorPreview) {
122+
const color = getColorString(md)
123+
if (color && !colorRanges.find(r => r.range.start.isEqual(doc.positionAt(i[0])))) {
124+
colorRanges.push({
123125
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
124-
get hoverMessage() {
125-
return new MarkdownString(md)
126-
},
127-
}
128-
}
129-
catch (e: any) {
130-
log.appendLine(`⚠️ Failed to parse ${i[2]}`)
131-
log.appendLine(String(e.stack ?? e))
132-
return undefined!
126+
renderOptions: { before: { backgroundColor: color } },
127+
})
133128
}
134-
}),
129+
}
130+
return {
131+
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
132+
get hoverMessage() {
133+
return new MarkdownString(md)
134+
},
135+
}
136+
}
137+
catch (e: any) {
138+
log.appendLine(`⚠️ Failed to parse ${i[2]}`)
139+
log.appendLine(String(e.stack ?? e))
140+
return undefined!
141+
}
142+
}),
135143
)
136144
).filter(Boolean)
137145

packages/vscode/src/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function useConfigurations(ext: ExtensionContext) {
9292
remToPxRatio: 16,
9393
underline: true,
9494
selectionStyle: true,
95+
strictAnnotationMatch: true,
9596
},
9697
alias: {
9798
matchType: 'autocomplete.matchType',

test/pos.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getMatchedPositionsFromCode as match } from '@unocss/shared-common'
77
import transformerVariantGroup from '@unocss/transformer-variant-group'
88
import cssDirectives from '@unocss/transformer-directives'
99
import extractorPug from '@unocss/extractor-pug'
10+
import { defaultIdeMatchExclude, defaultIdeMatchInclude } from '@unocss/shared-integration'
1011

1112
describe('matched-positions', async () => {
1213
it('attributify', async () => {
@@ -248,6 +249,115 @@ describe('matched-positions', async () => {
248249
]
249250
`)
250251
})
252+
253+
it('with include and exclude', async () => {
254+
const uno = createGenerator({
255+
presets: [
256+
presetUno(),
257+
],
258+
})
259+
260+
const code = `
261+
<script setup>
262+
let transition = 'ease-in-out duration-300'
263+
</script>
264+
265+
<template>
266+
<div class="h-1 text-red" />
267+
</template>
268+
269+
<style>
270+
.css {
271+
transform: translateX(0);
272+
@apply: text-blue;
273+
--uno:
274+
text-purple;
275+
}
276+
</style>
277+
`
278+
279+
expect(await match(uno, code))
280+
.toMatchInlineSnapshot(`
281+
[
282+
[
283+
20,
284+
30,
285+
"transition",
286+
],
287+
[
288+
34,
289+
45,
290+
"ease-in-out",
291+
],
292+
[
293+
46,
294+
58,
295+
"duration-300",
296+
],
297+
[
298+
96,
299+
99,
300+
"h-1",
301+
],
302+
[
303+
100,
304+
108,
305+
"text-red",
306+
],
307+
[
308+
144,
309+
153,
310+
"transform",
311+
],
312+
[
313+
180,
314+
189,
315+
"text-blue",
316+
],
317+
[
318+
204,
319+
215,
320+
"text-purple",
321+
],
322+
]
323+
`)
324+
325+
expect(await match(uno, code, undefined, { includeRegex: defaultIdeMatchInclude, excludeRegex: defaultIdeMatchExclude }))
326+
.toMatchInlineSnapshot(`
327+
[
328+
[
329+
34,
330+
45,
331+
"ease-in-out",
332+
],
333+
[
334+
46,
335+
58,
336+
"duration-300",
337+
],
338+
[
339+
96,
340+
99,
341+
"h-1",
342+
],
343+
[
344+
100,
345+
108,
346+
"text-red",
347+
],
348+
[
349+
180,
350+
189,
351+
"text-blue",
352+
],
353+
[
354+
204,
355+
215,
356+
"text-purple",
357+
],
358+
]
359+
`)
360+
})
251361
})
252362

253363
describe('matched-positions-pug', async () => {

0 commit comments

Comments
 (0)