From 2f1cbbfed28729798eebdaa57935e8f7b0c622e1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 8 Sep 2025 12:18:30 +0200 Subject: [PATCH] Merge suggestions when using `@utility` (#18900) This PR fixes a bug where custom `@utility` implementations with a name that match an existing utility would override the existing suggestions even though we generate both utilities. With this, we want to make sure that both the custom and the built-in utilities are suggested. We also want to make sure that we don't get duplicate suggestions. E.g.: - `font-` would suggest: - 'font-black' - 'font-bold' - 'font-extrabold' - 'font-extralight' - 'font-light' - 'font-medium' - 'font-mono' - 'font-normal' - 'font-sans' - 'font-semibold' - 'font-serif' - 'font-thin' But if you introduce this little custom utility: ```css @theme { --custom-font-weights-foo: 123; } @utility font-* { --my-weight: --value(--custom-font-weights- *); } ``` - `font-` would suggest: - 'font-foo' With this fix, we would suggest: - `font-` would suggest: - 'font-black' - 'font-bold' - 'font-extrabold' - 'font-extralight' - 'font-foo' // This is now added - 'font-light' - 'font-medium' - 'font-mono' - 'font-normal' - 'font-sans' - 'font-semibold' - 'font-serif' - 'font-thin' We also make sure that they are unique, so if you have a custom utility that happens to match another existing utility (e.g. `font-bold`), you won't see `font-bold` twice in the suggestions. ```css @theme { --custom-font-weights-bold: bold; --custom-font-weights-normal: normal; --custom-font-weights-foo: 1234; } @utility font-* { --my-weight: --value(--custom-font-weights-*); } ``` - `font-` would suggest: - 'font-black' - 'font-bold' // Overlaps with existing utility - 'font-extrabold' - 'font-extralight' - 'font-foo' // This is now added - 'font-light' - 'font-medium' - 'font-mono' - 'font-normal' // Overlaps with existing utility - 'font-sans' - 'font-semibold' - 'font-serif' - 'font-thin' --- CHANGELOG.md | 1 + packages/tailwindcss/src/intellisense.test.ts | 33 +++++++++++++++++++ packages/tailwindcss/src/intellisense.ts | 3 ++ packages/tailwindcss/src/utilities.ts | 9 +++-- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c161e571..4d7a1fcbe 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 32893b1a0..5dbfc8c54 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 27db3a540..e6db73b50 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 fa97dbd52..9a6906bbd 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') {