Skip to content

Commit 0977df1

Browse files
author
Laurynas Grigutis
committed
feat: classFunctions & classProperties should not duplicate matches
1 parent 3baeb5b commit 0977df1

File tree

2 files changed

+97
-54
lines changed

2 files changed

+97
-54
lines changed

packages/tailwindcss-language-service/src/util/find.test.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -281,22 +281,6 @@ test('find class lists in nested fn calls', async ({ expect }) => {
281281
end: { line: 20, character: 24 },
282282
},
283283
},
284-
285-
// TODO: These duplicates are from matching nested clsx(…) and should be ignored
286-
{
287-
classList: 'fixed',
288-
range: {
289-
start: { line: 9, character: 5 },
290-
end: { line: 9, character: 10 },
291-
},
292-
},
293-
{
294-
classList: 'absolute inset-0',
295-
range: {
296-
start: { line: 10, character: 5 },
297-
end: { line: 10, character: 21 },
298-
},
299-
},
300284
])
301285
})
302286

@@ -510,6 +494,57 @@ test('classFunctions regexes only match on function names', async ({ expect }) =
510494
expect(classListsA).toEqual([])
511495
})
512496

497+
test('classFunctions & classProperties should not duplicate matches', async ({ expect }) => {
498+
let fileA = createDocument({
499+
name: 'file.jsx',
500+
lang: 'javascriptreact',
501+
settings: {
502+
tailwindCSS: {
503+
classFunctions: ['cva', 'clsx'],
504+
},
505+
},
506+
content: js`
507+
const Component = ({ className }) => (
508+
<div
509+
className={clsx(
510+
'relative flex',
511+
'inset-0 md:h-[calc(100%-2rem)]',
512+
clsx('rounded-none bg-blue-700', className),
513+
)}
514+
>
515+
CONTENT
516+
</div>
517+
)
518+
`,
519+
})
520+
521+
let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
522+
523+
expect(classListsA).toEqual([
524+
{
525+
classList: 'relative flex',
526+
range: {
527+
start: { line: 3, character: 7 },
528+
end: { line: 3, character: 20 },
529+
},
530+
},
531+
{
532+
classList: 'inset-0 md:h-[calc(100%-2rem)]',
533+
range: {
534+
start: { line: 4, character: 7 },
535+
end: { line: 4, character: 37 },
536+
},
537+
},
538+
{
539+
classList: 'rounded-none bg-blue-700',
540+
range: {
541+
start: { line: 5, character: 12 },
542+
end: { line: 5, character: 36 },
543+
},
544+
},
545+
])
546+
})
547+
513548
function createDocument({
514549
name,
515550
lang,

packages/tailwindcss-language-service/src/util/find.ts

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ export async function findClassListsInHtmlRange(
208208
matches.push(...matchClassFunctions(text, settings.classFunctions))
209209
}
210210

211-
const result: DocumentClassList[] = []
211+
const existingResultSet = new Set<string>()
212+
const results: DocumentClassList[] = []
212213

213214
matches.forEach((match) => {
214215
const subtext = text.substr(match.index + match[0].length - 1)
@@ -253,46 +254,53 @@ export async function findClassListsInHtmlRange(
253254
})
254255
}
255256

256-
result.push(
257-
...classLists
258-
.map(({ value, offset }) => {
259-
if (value.trim() === '') {
260-
return null
261-
}
257+
classLists.forEach(({ value, offset }) => {
258+
if (value.trim() === '') {
259+
return null
260+
}
262261

263-
const before = value.match(/^\s*/)
264-
const beforeOffset = before === null ? 0 : before[0].length
265-
const after = value.match(/\s*$/)
266-
const afterOffset = after === null ? 0 : -after[0].length
267-
268-
const start = indexToPosition(
269-
text,
270-
match.index + match[0].length - 1 + offset + beforeOffset,
271-
)
272-
const end = indexToPosition(
273-
text,
274-
match.index + match[0].length - 1 + offset + value.length + afterOffset,
275-
)
276-
277-
return {
278-
classList: value.substr(beforeOffset, value.length + afterOffset),
279-
range: {
280-
start: {
281-
line: (range?.start.line || 0) + start.line,
282-
character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character,
283-
},
284-
end: {
285-
line: (range?.start.line || 0) + end.line,
286-
character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character,
287-
},
288-
},
289-
}
290-
})
291-
.filter((x) => x !== null),
292-
)
262+
const before = value.match(/^\s*/)
263+
const beforeOffset = before === null ? 0 : before[0].length
264+
const after = value.match(/\s*$/)
265+
const afterOffset = after === null ? 0 : -after[0].length
266+
267+
const start = indexToPosition(text, match.index + match[0].length - 1 + offset + beforeOffset)
268+
const end = indexToPosition(
269+
text,
270+
match.index + match[0].length - 1 + offset + value.length + afterOffset,
271+
)
272+
273+
const result: DocumentClassList = {
274+
classList: value.substr(beforeOffset, value.length + afterOffset),
275+
range: {
276+
start: {
277+
line: (range?.start.line || 0) + start.line,
278+
character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character,
279+
},
280+
end: {
281+
line: (range?.start.line || 0) + end.line,
282+
character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character,
283+
},
284+
},
285+
}
286+
287+
const resultKey = [
288+
result.classList,
289+
result.range.start.line,
290+
result.range.start.character,
291+
result.range.end.line,
292+
result.range.end.character,
293+
].join(':')
294+
295+
// No need to add the result if it was already matched
296+
if (!existingResultSet.has(resultKey)) {
297+
existingResultSet.add(resultKey)
298+
results.push(result)
299+
}
300+
})
293301
})
294302

295-
return result
303+
return results
296304
}
297305

298306
export async function findClassListsInRange(

0 commit comments

Comments
 (0)