Skip to content

Commit f0986ce

Browse files
Prevent duplicate suggestions when using @theme and @utility together (#17675)
Fixes tailwindlabs/tailwindcss-intellisense#1313 Right now given this CSS: ```css @theme reference { --text-header: 1.5rem; } @Utility text-header { text-transform: uppercase; } ``` You'll see two entries for `text-header` in IntelliSense completions but we only want you to see one. This PR solves this by merging their modifier lists and de-duping by class name.
1 parent 3386049 commit f0986ce

File tree

3 files changed

+42
-20
lines changed

3 files changed

+42
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Allow `_` before numbers during candidate extraction ([#17961](https://github.com/tailwindlabs/tailwindcss/pull/17961))
1313
- Upgrade: Fix error when using `@import … source(…)` ([#17963](https://github.com/tailwindlabs/tailwindcss/pull/17963))
14+
- Prevent duplicate suggestions when using `@theme` and `@utility` together ([#17675](https://github.com/tailwindlabs/tailwindcss/pull/17675))
1415

1516
## [4.1.6] - 2025-05-09
1617

packages/tailwindcss/src/intellisense.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,25 @@ test('shadow utility default suggestions', async () => {
653653
expect(classNames).toContain('inset-shadow')
654654
expect(classNames).toContain('text-shadow')
655655
})
656+
657+
test('Custom @utility and existing utility with names matching theme keys dont give duplicate results', async () => {
658+
let input = css`
659+
@theme reference {
660+
--leading-sm: 0.25rem;
661+
--text-header: 1.5rem;
662+
}
663+
664+
@utility text-header {
665+
text-transform: uppercase;
666+
}
667+
`
668+
669+
let design = await __unstable__loadDesignSystem(input)
670+
671+
let classList = design.getClassList()
672+
let classMap = new Map(classList)
673+
let matches = classList.filter(([className]) => className === 'text-header')
674+
675+
expect(matches).toHaveLength(1)
676+
expect(classMap.get('text-header')?.modifiers).toEqual(['sm'])
677+
})

packages/tailwindcss/src/intellisense.ts

+19-20
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ export type ClassEntry = [string, ClassMetadata]
2020
const IS_FRACTION = /^\d+\/\d+$/
2121

2222
export function getClassList(design: DesignSystem): ClassEntry[] {
23-
let list: ClassItem[] = []
23+
let items = new DefaultMap<string, ClassItem>((utility) => ({
24+
name: utility,
25+
utility,
26+
fraction: false,
27+
modifiers: [],
28+
}))
2429

2530
// Static utilities only work as-is
2631
for (let utility of design.utilities.keys('static')) {
27-
list.push({
28-
name: utility,
29-
utility,
30-
fraction: false,
31-
modifiers: [],
32-
})
32+
let item = items.get(utility)
33+
item.fraction = false
34+
item.modifiers = []
3335
}
3436

3537
// Functional utilities have their own list of completions
@@ -42,28 +44,25 @@ export function getClassList(design: DesignSystem): ClassEntry[] {
4244

4345
let name = value === null ? utility : `${utility}-${value}`
4446

45-
list.push({
46-
name,
47-
utility,
48-
fraction,
49-
modifiers: group.modifiers,
50-
})
47+
let item = items.get(name)
48+
item.utility = utility
49+
item.fraction ||= fraction
50+
item.modifiers.push(...group.modifiers)
5151

5252
if (group.supportsNegative) {
53-
list.push({
54-
name: `-${name}`,
55-
utility: `-${utility}`,
56-
fraction,
57-
modifiers: group.modifiers,
58-
})
53+
let item = items.get(`-${name}`)
54+
item.utility = `-${utility}`
55+
item.fraction ||= fraction
56+
item.modifiers.push(...group.modifiers)
5957
}
6058
}
6159
}
6260
}
6361

64-
if (list.length === 0) return []
62+
if (items.size === 0) return []
6563

6664
// Sort utilities by their class name
65+
let list = Array.from(items.values())
6766
list.sort((a, b) => compare(a.name, b.name))
6867

6968
let entries = sortFractionsLast(list)

0 commit comments

Comments
 (0)