diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b08ea20a1c6..ac13cd20e1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 04a6ed7961b9..f5902a2613df 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -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']) +}) diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index ca9c1a386099..27db3a540bc3 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -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((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)