Prevent duplicate suggestions when using @theme and @utility together (#17675)

Fixes
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/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.
This commit is contained in:
Jordan Pittman 2025-05-09 16:12:43 -04:00 committed by GitHub
parent 3386049b7b
commit f0986ce127
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 20 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow `_` before numbers during candidate extraction ([#17961](https://github.com/tailwindlabs/tailwindcss/pull/17961))
- Upgrade: Fix error when using `@import … source(…)` ([#17963](https://github.com/tailwindlabs/tailwindcss/pull/17963))
- Prevent duplicate suggestions when using `@theme` and `@utility` together ([#17675](https://github.com/tailwindlabs/tailwindcss/pull/17675))
## [4.1.6] - 2025-05-09

View File

@ -653,3 +653,25 @@ test('shadow utility default suggestions', async () => {
expect(classNames).toContain('inset-shadow')
expect(classNames).toContain('text-shadow')
})
test('Custom @utility and existing utility with names matching theme keys dont give duplicate results', async () => {
let input = css`
@theme reference {
--leading-sm: 0.25rem;
--text-header: 1.5rem;
}
@utility text-header {
text-transform: uppercase;
}
`
let design = await __unstable__loadDesignSystem(input)
let classList = design.getClassList()
let classMap = new Map(classList)
let matches = classList.filter(([className]) => className === 'text-header')
expect(matches).toHaveLength(1)
expect(classMap.get('text-header')?.modifiers).toEqual(['sm'])
})

View File

@ -20,16 +20,18 @@ export type ClassEntry = [string, ClassMetadata]
const IS_FRACTION = /^\d+\/\d+$/
export function getClassList(design: DesignSystem): ClassEntry[] {
let list: ClassItem[] = []
let items = new DefaultMap<string, ClassItem>((utility) => ({
name: utility,
utility,
fraction: false,
modifiers: [],
}))
// Static utilities only work as-is
for (let utility of design.utilities.keys('static')) {
list.push({
name: utility,
utility,
fraction: false,
modifiers: [],
})
let item = items.get(utility)
item.fraction = false
item.modifiers = []
}
// Functional utilities have their own list of completions
@ -42,28 +44,25 @@ export function getClassList(design: DesignSystem): ClassEntry[] {
let name = value === null ? utility : `${utility}-${value}`
list.push({
name,
utility,
fraction,
modifiers: group.modifiers,
})
let item = items.get(name)
item.utility = utility
item.fraction ||= fraction
item.modifiers.push(...group.modifiers)
if (group.supportsNegative) {
list.push({
name: `-${name}`,
utility: `-${utility}`,
fraction,
modifiers: group.modifiers,
})
let item = items.get(`-${name}`)
item.utility = `-${utility}`
item.fraction ||= fraction
item.modifiers.push(...group.modifiers)
}
}
}
}
if (list.length === 0) return []
if (items.size === 0) return []
// Sort utilities by their class name
let list = Array.from(items.values())
list.sort((a, b) => compare(a.name, b.name))
let entries = sortFractionsLast(list)