Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure the CSS `theme()` function resolves to the right value in some compatibility situations ([#14614](https://github.com/tailwindlabs/tailwindcss/pull/14614))
- Fix issue that could cause the CLI to crash when files are deleted while watching ([#14616](https://github.com/tailwindlabs/tailwindcss/pull/14616))
- Ensure custom variants using the JS API have access to modifiers ([#14637](https://github.com/tailwindlabs/tailwindcss/pull/14637))
- Ensure auto complete suggestions work when using `matchUtilities` ([#14589](https://github.com/tailwindlabs/tailwindcss/pull/14589))
- _Upgrade (experimental)_: Ensure CSS before a layer stays unlayered when running codemods ([#14596](https://github.com/tailwindlabs/tailwindcss/pull/14596))
- _Upgrade (experimental)_: Resolve issues where some prefixed candidates were not properly migrated ([#14600](https://github.com/tailwindlabs/tailwindcss/pull/14600))

Expand Down
26 changes: 26 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,32 @@ export function buildPluginApi(
designSystem.utilities.functional(name, compileFn, {
types,
})

designSystem.utilities.suggest(name, () => {
let values = options?.values ?? {}
let valueKeys = new Set<string | null>(Object.keys(values))

// The `__BARE_VALUE__` key is a special key used to ensure bare values
// work even with legacy configs and plugins
valueKeys.delete('__BARE_VALUE__')

// The `DEFAULT` key is represented as `null` in the utility API
if (valueKeys.has('DEFAULT')) {
valueKeys.delete('DEFAULT')
valueKeys.add(null)
}

let modifiers = options?.modifiers ?? {}
let modifierKeys = modifiers === 'any' ? [] : Object.keys(modifiers)

return [
{
supportsNegative: options?.supportsNegativeValues ?? false,
values: Array.from(valueKeys),
modifiers: modifierKeys,
},
]
})
}
},

Expand Down
176 changes: 176 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { buildDesignSystem } from './design-system'
import plugin from './plugin'
import { Theme } from './theme'

const css = String.raw
Expand Down Expand Up @@ -174,3 +175,178 @@ test('Utilities, when marked as important, show as important in intellisense', a
]
`)
})

test('Static utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
`

let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: plugin(({ addUtilities }) => {
addUtilities({
'.custom-utility': {
color: 'red',
},
})
}),
}),
})

expect(design.candidatesToCss(['custom-utility'])).toMatchInlineSnapshot(`
[
".custom-utility {
color: red;
}
",
]
`)

expect(design.getClassList().map((entry) => entry[0])).toContain('custom-utility')
})

test('Functional utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
`

let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: plugin(({ matchUtilities }) => {
matchUtilities(
{
'custom-1': (value) => ({
color: value,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
},
)

matchUtilities(
{
'custom-2': (value, { modifier }) => ({
color: `${value} / ${modifier ?? '0%'}`,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
modifiers: {
'50': '50%',
'75': '75%',
},
},
)

matchUtilities(
{
'custom-3': (value, { modifier }) => ({
color: `${value} / ${modifier ?? '0%'}`,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
modifiers: 'any',
},
)
}),
}),
})

expect(design.candidatesToCss(['custom-1-red', 'custom-1-green', 'custom-1-unknown']))
.toMatchInlineSnapshot(`
[
".custom-1-red {
color: #ff0000;
}
",
".custom-1-green {
color: #ff0000;
}
",
null,
]
`)

expect(design.candidatesToCss(['custom-2-red', 'custom-2-green', 'custom-2-unknown']))
.toMatchInlineSnapshot(`
[
".custom-2-red {
color: #ff0000 / 0%;
}
",
".custom-2-green {
color: #ff0000 / 0%;
}
",
null,
]
`)

expect(design.candidatesToCss(['custom-2-red/50', 'custom-2-red/75', 'custom-2-red/unknown']))
.toMatchInlineSnapshot(`
[
".custom-2-red\\/50 {
color: #ff0000 / 50%;
}
",
".custom-2-red\\/75 {
color: #ff0000 / 75%;
}
",
null,
]
`)

let classMap = new Map(design.getClassList())
let classNames = Array.from(classMap.keys())

// matchUtilities without modifiers
expect(classNames).toContain('custom-1-red')
expect(classMap.get('custom-1-red')?.modifiers).toEqual([])

expect(classNames).toContain('custom-1-green')
expect(classMap.get('custom-1-green')?.modifiers).toEqual([])

expect(classNames).not.toContain('custom-1-unknown')

// matchUtilities with a set list of modifiers
expect(classNames).toContain('custom-2-red')
expect(classMap.get('custom-2-red')?.modifiers).toEqual(['50', '75'])

expect(classNames).toContain('custom-2-green')
expect(classMap.get('custom-2-green')?.modifiers).toEqual(['50', '75'])

expect(classNames).not.toContain('custom-2-unknown')

// matchUtilities with a any modifiers
expect(classNames).toContain('custom-3-red')
expect(classMap.get('custom-3-red')?.modifiers).toEqual([])

expect(classNames).toContain('custom-3-green')
expect(classMap.get('custom-3-green')?.modifiers).toEqual([])

expect(classNames).not.toContain('custom-3-unknown')
})