diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c161e571aa8..4d7a1fcbeb31 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 - Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888)) - Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885)) +- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900)) ## [4.1.13] - 2025-09-03 diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 32893b1a0c8e..5dbfc8c54c80 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -572,6 +572,39 @@ test('Custom functional @utility', async () => { expect(classMap.get('example-xs')?.modifiers).toEqual(['normal', 'foo', 'bar']) }) +test('Custom utilities sharing a root with built-in utilities should merge suggestions', async () => { + let input = css` + @import 'tailwindcss/utilities'; + @theme { + --font-sans: sans-serif; + } + + @theme { + --font-weight-custom: 1234; + --font-weight-bold: bold; /* Overlap with existing utility */ + } + + @utility font-* { + --my-font-weight: --value(--font-weight- *); + } + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + path: '', + base, + content: '@tailwind utilities;', + }), + }) + + let classMap = new Map(design.getClassList()) + let classNames = Array.from(classMap.keys()) + + expect(classNames).toContain('font-sans') // Existing font-family utility + expect(classNames).toContain('font-bold') // Existing font-family utility & custom font-weight utility + expect(classNames).toContain('font-custom') // Custom font-weight utility +}) + test('Theme keys with underscores are suggested with underscores', async () => { let input = css` @import 'tailwindcss/utilities'; diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 27db3a540bc3..e6db73b50f4a 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -55,6 +55,9 @@ export function getClassList(design: DesignSystem): ClassEntry[] { item.fraction ||= fraction item.modifiers.push(...group.modifiers) } + + // Deduplicate modifiers + item.modifiers = Array.from(new Set(item.modifiers)) } } } diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index fa97dbd5273a..9a6906bbd6eb 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -124,9 +124,12 @@ export class Utilities { } suggest(name: string, groups: () => SuggestionGroup[]) { - // TODO: We are calling this multiple times on purpose but ideally only ever - // once per utility root. - this.completions.set(name, groups) + let existingGroups = this.completions.get(name) + if (existingGroups) { + this.completions.set(name, () => [...existingGroups?.(), ...groups?.()]) + } else { + this.completions.set(name, groups) + } } keys(kind: 'static' | 'functional') {