Skip to content

Commit 8694d75

Browse files
committed
Refactor
1 parent 6e04329 commit 8694d75

File tree

4 files changed

+130
-151
lines changed

4 files changed

+130
-151
lines changed

packages/tailwindcss-language-service/src/completionProvider.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { flagEnabled } from './util/flagEnabled'
3131
import * as jit from './util/jit'
3232
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
3333
import * as culori from 'culori'
34-
import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib'
3534
import {
3635
addPixelEquivalentsToMediaQuery,
3736
addPixelEquivalentsToValue,
@@ -504,19 +503,18 @@ async function provideCustomClassNameCompletions(
504503
context?: CompletionContext
505504
): Promise<CompletionList> {
506505
const settings = await state.editor.getConfiguration(document.uri)
507-
const regexes = settings.tailwindCSS.experimental.classRegex
508-
if (regexes.length === 0) return null
506+
const filters = settings.tailwindCSS.experimental.classRegex
507+
if (filters.length === 0) return null
509508

510509
const cursor = document.offsetAt(position)
511510

512-
let str = document.getText({
511+
let text = document.getText({
513512
start: document.positionAt(0),
514513
end: document.positionAt(cursor + 2000),
515514
})
516515

517516
// Get completions from the first matching regex or regex pair
518-
let match = customClassesIn(str, cursor, regexes)
519-
if (match) {
517+
for (let match of customClassesIn({ text, cursor, filters })) {
520518
return completionsFromClassList(
521519
state,
522520
match.classList,
Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,173 @@
11
import { expect, test } from 'vitest'
2-
import { ClassRegexEntry, customClassesIn } from './classes'
2+
import { ClassMatch, ClassRegexFilter, customClassesIn } from './classes'
33

44
interface TestRecord {
55
name: string,
6-
input: string,
7-
cursor: number,
8-
regexes: ClassRegexEntry[],
9-
expected: { classList: string } | null
6+
text: string,
7+
cursor: number | null,
8+
filters: ClassRegexFilter[],
9+
expected: ClassMatch[]
1010
}
1111

1212
let table: TestRecord[] = [
1313
{
1414
name: 'empty',
15-
input: 'test ',
15+
text: 'test ',
1616
cursor: 0,
17-
regexes: [],
18-
expected: null
17+
filters: [],
18+
expected: [],
1919
},
2020

2121
// Container regex only
2222
{
2323
name: 'simple (single, matches: yes)',
24-
input: 'test ""',
24+
text: 'test ""',
2525
cursor: 5,
26-
regexes: [['test (\\S*)']],
27-
expected: { classList: '' }
26+
filters: [['test (\\S*)']],
27+
expected: [{ classList: '', range: [5, 7] }],
2828
},
2929

3030
{
3131
name: 'simple (single, matches: no)',
32-
input: 'tron ""',
32+
text: 'tron ""',
3333
cursor: 5,
34-
regexes: [['test (\\S*)']],
35-
expected: null
34+
filters: [['test (\\S*)']],
35+
expected: []
3636
},
3737

3838
{
3939
name: 'simple (multiple, matches: yes)',
40-
input: 'tron ""',
40+
text: 'tron ""',
4141
cursor: 5,
42-
regexes: [['test (\\S*)'], ['tron (\\S*)']],
43-
expected: { classList: '' }
42+
filters: [['test (\\S*)'], ['tron (\\S*)']],
43+
expected: [{ classList: '', range: [5, 7] }],
4444
},
4545

4646
{
4747
name: 'simple (multiple, matches: no)',
48-
input: 'nope ""',
48+
text: 'nope ""',
4949
cursor: 5,
50-
regexes: [['test (\\S*)'], ['tron (\\S*)']],
51-
expected: null
50+
filters: [['test (\\S*)'], ['tron (\\S*)']],
51+
expected: []
5252
},
5353

5454
// Container + class regex
5555
{
5656
name: 'nested (single, matches: yes)',
57-
input: 'test ""',
57+
text: 'test ""',
5858
cursor: 6,
59-
regexes: [['test (\\S*)', '"([^"]*)"']],
60-
expected: { classList: '' }
59+
filters: [['test (\\S*)', '"([^"]*)"']],
60+
expected: [{ classList: '', range: [6, 6] }],
6161
},
6262

6363
{
6464
name: 'nested (single, matches: no)',
65-
input: 'tron ""',
65+
text: 'tron ""',
6666
cursor: 6,
67-
regexes: [['test (\\S*)', '"([^"]*)"']],
68-
expected: null
67+
filters: [['test (\\S*)', '"([^"]*)"']],
68+
expected: []
6969
},
7070

7171
{
7272
name: 'nested (multiple, matches: yes)',
73-
input: 'tron ""',
73+
text: 'tron ""',
7474
cursor: 6,
75-
regexes: [['test (\\S*)', '"([^"]*)"'], ['tron (\\S*)', '"([^"]*)"']],
76-
expected: { classList: '' }
75+
filters: [['test (\\S*)', '"([^"]*)"'], ['tron (\\S*)', '"([^"]*)"']],
76+
expected: [{ classList: '', range: [6, 6] }],
7777
},
7878

7979
{
8080
name: 'nested (multiple, matches: no)',
81-
input: 'nope ""',
81+
text: 'nope ""',
8282
cursor: 6,
83-
regexes: [['test (\\S*)', '"([^"]*)"'], ['tron (\\S*)', '"([^"]*)"']],
84-
expected: null
83+
filters: [['test (\\S*)', '"([^"]*)"'], ['tron (\\S*)', '"([^"]*)"']],
84+
expected: []
8585
},
8686

8787
// Cursor position validation
8888
{
8989
name: 'cursor, container: inside #1',
90-
input: `<div class="text-" /> <div class="bg-" />`,
90+
text: `<div class="text-" /> <div class="bg-" />`,
9191
cursor: 17,
92-
regexes: [['class="([^"]*)"']],
93-
expected: { classList: 'text-' }
92+
filters: [['class="([^"]*)"']],
93+
expected: [{ classList: 'text-', range: [12, 17] }],
9494
},
9595

9696
{
9797
name: 'cursor, container: inside #2',
98-
input: `<div class="text-" /> <div class="bg-" />`,
98+
text: `<div class="text-" /> <div class="bg-" />`,
9999
cursor: 37,
100-
regexes: [['class="([^"]*)"']],
101-
expected: { classList: 'bg-' }
100+
filters: [['class="([^"]*)"']],
101+
expected: [{ classList: 'bg-', range: [34, 37] }],
102102
},
103103

104104
{
105105
name: 'cursor, container: outside',
106-
input: `<div class="text-" /> <div class="bg-" />`,
106+
text: `<div class="text-" /> <div class="bg-" />`,
107107
cursor: 11,
108-
regexes: [['class="([^"]*)"']],
109-
expected: null
108+
filters: [['class="([^"]*)"']],
109+
expected: []
110110
},
111111

112112
{
113113
name: 'cursor, container: inside #1, class: inside #1',
114-
input: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
114+
text: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
115115
cursor: 23,
116-
regexes: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
117-
expected: { classList: 'text-' }
116+
filters: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
117+
expected: [{ classList: 'text-', range: [18, 23] }],
118118
},
119119

120120
{
121121
name: 'cursor, container: inside #1, class: inside #2',
122-
input: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
122+
text: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
123123
cursor: 38,
124-
regexes: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
125-
expected: { classList: 'decoration-' }
124+
filters: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
125+
expected: [{ classList: 'decoration-', range: [27, 38] }],
126126
},
127127

128128
{
129129
name: 'cursor, container: inside #2, class: inside #1',
130-
input: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
130+
text: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
131131
cursor: 66,
132-
regexes: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
133-
expected: { classList: 'bg-' }
132+
filters: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
133+
expected: [{ classList: 'bg-', range: [63, 66] }],
134134
},
135135

136136
{
137137
name: 'cursor, container: inside #1, class: outside',
138-
input: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
138+
text: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
139139
cursor: 17,
140-
regexes: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
141-
expected: null,
140+
filters: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
141+
expected: [],
142142
},
143143

144144
{
145145
name: 'cursor, container: inside #2, class: outside',
146-
input: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
146+
text: `<div class={clsx("text-", "decoration-")} /> <div class={clsx("bg-")} />`,
147147
cursor: 62,
148-
regexes: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
149-
expected: null,
148+
filters: [['clsx\\(([^)]*)\\)', '"([^"]*)"']],
149+
expected: [],
150+
},
151+
152+
// No cursor = multiple results
153+
{
154+
name: 'cursor, container: inside #1',
155+
text: `<div class="text-" /> <div class="bg-" />`,
156+
cursor: null,
157+
filters: [['class="([^"]*)"']],
158+
expected: [{ classList: 'text-', range: [12, 17] }, { classList: 'bg-', range: [34, 37] }],
150159
},
151160

152161
// Edge cases
153162
{
154163
name: 'regex matches empty string',
155-
input: `let _ = ""`,
164+
text: `let _ = ""`,
156165
cursor: 9,
157-
regexes: [['(?<=")(\\w*)(?=")']],
158-
expected: { classList: '' },
166+
filters: [['(?<=")(\\w*)(?=")']],
167+
expected: [{ classList: '', range: [9, 9] }],
159168
},
160169
]
161170

162-
test.each(table)('customClassesIn: $name', ({ input, cursor, regexes, expected }) => {
163-
expect(customClassesIn(input, cursor, regexes)).toStrictEqual(expected)
171+
test.each(table)('customClassesIn: $name', ({ text, cursor, filters, expected }) => {
172+
expect(Array.from(customClassesIn({ text, filters, cursor }))).toStrictEqual(expected)
164173
})
Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,73 @@
1-
2-
export type ClassRegexEntry = string | [string] | [string, string]
3-
export type ClassRegex = [container: RegExp, cls?: RegExp]
1+
export type ClassRegexFilter = string | [string] | [string, string]
42
export interface ClassMatch {
53
classList: string
4+
range: [start: number, end: number]
65
}
76

8-
export function customClassesIn(
9-
str: string,
10-
cursor: number,
11-
patterns: ClassRegexEntry[],
12-
): ClassMatch | null {
13-
for (let pattern of patterns) {
14-
let normalized = Array.isArray(pattern)
15-
? pattern
16-
: [pattern]
17-
18-
let regexes = normalized.map((pattern) => new RegExp(pattern, 'gd'))
19-
20-
let match = firstMatchIn(str, cursor, regexes as ClassRegex)
21-
22-
if (match) {
23-
return match
7+
export function *customClassesIn({
8+
text,
9+
filters,
10+
cursor = null,
11+
} : {
12+
text: string,
13+
filters: ClassRegexFilter[],
14+
cursor?: number | null,
15+
}): Iterable<ClassMatch> {
16+
for (let filter of filters) {
17+
let [containerPattern, classPattern] = Array.isArray(filter)
18+
? filter
19+
: [filter]
20+
21+
let containerRegex = new RegExp(containerPattern, 'gd')
22+
let classRegex = classPattern ? new RegExp(classPattern, 'gd') : undefined
23+
24+
for (let match of matchesIn(text, containerRegex, classRegex, cursor)) {
25+
yield match
2426
}
2527
}
26-
27-
return null
2828
}
2929

30-
function firstMatchIn(
31-
str: string,
32-
cursor: number,
33-
[containerRegex, classRegex]: ClassRegex,
34-
): ClassMatch | null {
35-
let containerMatch: ReturnType<RegExp['exec']>
36-
37-
while ((containerMatch = containerRegex.exec(str)) !== null) {
30+
function *matchesIn(
31+
text: string,
32+
containerRegex: RegExp,
33+
classRegex: RegExp | undefined,
34+
cursor: number | null,
35+
): Iterable<ClassMatch> {
36+
for (let containerMatch of text.matchAll(containerRegex)) {
3837
const matchStart = containerMatch.indices[1][0]
3938
const matchEnd = matchStart + containerMatch[1].length
4039

4140
// Cursor is outside of the match
42-
if (cursor < matchStart || cursor > matchEnd) {
41+
if (cursor !== null && (cursor < matchStart || cursor > matchEnd)) {
4342
continue
4443
}
4544

4645
if (! classRegex) {
47-
return {
48-
classList: containerMatch[1].slice(0, cursor - matchStart)
46+
yield {
47+
classList: cursor !== null
48+
? containerMatch[1].slice(0, cursor - matchStart)
49+
: containerMatch[1],
50+
range: [matchStart, matchEnd],
4951
}
52+
continue
5053
}
5154

5255
// Handle class matches inside the "container"
53-
let classMatch: ReturnType<RegExp['exec']>
54-
55-
while ((classMatch = classRegex.exec(containerMatch[1])) !== null) {
56+
for (let classMatch of containerMatch[1].matchAll(classRegex)) {
5657
const classMatchStart = matchStart + classMatch.indices[1][0]
5758
const classMatchEnd = classMatchStart + classMatch[1].length
5859

5960
// Cursor is outside of the match
60-
if (cursor < classMatchStart || cursor > classMatchEnd) {
61+
if (cursor !== null && (cursor < classMatchStart || cursor > classMatchEnd)) {
6162
continue
6263
}
6364

64-
return {
65-
classList: classMatch[1].slice(0, cursor - classMatchStart),
65+
yield {
66+
classList: cursor !== null
67+
? classMatch[1].slice(0, cursor - classMatchStart)
68+
: classMatch[1],
69+
range: [classMatchStart, classMatchEnd],
6670
}
6771
}
68-
69-
return null
7072
}
71-
72-
return null
7373
}

0 commit comments

Comments
 (0)