Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Allow `_` before numbers during candidate extraction ([#17961](https://github.com/tailwindlabs/tailwindcss/pull/17961))
- Prevent duplicate suggestions when using `@theme` and `@utility` together ([#17675](https://github.com/tailwindlabs/tailwindcss/pull/17675))

## [4.1.6] - 2025-05-09

Expand Down
22 changes: 22 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
})
39 changes: 19 additions & 20 deletions packages/tailwindcss/src/intellisense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down