Skip to content

Commit ade6991

Browse files
committed
Add support for @custom-variant
1 parent 4d9917c commit ade6991

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

packages/tailwindcss-language-server/src/language/cssServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ async function validateTextDocument(textDocument: TextDocument): Promise<void> {
396396
.filter((diagnostic) => {
397397
if (
398398
diagnostic.code === 'unknownAtRules' &&
399-
/Unknown at rule @(tailwind|apply|config|theme|plugin|source|utility|variant)/.test(
399+
/Unknown at rule @(tailwind|apply|config|theme|plugin|source|utility|variant|custom-variant)/.test(
400400
diagnostic.message,
401401
)
402402
) {

packages/tailwindcss-language-server/tests/completions/completions.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -477,15 +477,15 @@ withFixture('v4/basic', (c) => {
477477
expect(result.items.filter((item) => item.label.startsWith('--')).length).toBe(23)
478478
})
479479

480-
test.concurrent('@slot is suggeted inside @variant', async ({ expect }) => {
480+
test.concurrent('@slot is suggeted inside @custom-variant', async ({ expect }) => {
481481
let result = await completion({
482482
lang: 'css',
483483
text: '@',
484484
position: { line: 0, character: 1 },
485485
})
486486

487487
// Make sure `@slot` is NOT suggested by default
488-
expect(result.items.length).toBe(10)
488+
expect(result.items.length).toBe(11)
489489
expect(result.items).not.toEqual(
490490
expect.arrayContaining([
491491
expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }),
@@ -494,12 +494,12 @@ withFixture('v4/basic', (c) => {
494494

495495
result = await completion({
496496
lang: 'css',
497-
text: '@variant foo {\n@',
497+
text: '@custom-variant foo {\n@',
498498
position: { line: 1, character: 1 },
499499
})
500500

501501
// Make sure `@slot` is suggested
502-
expect(result.items.length).toBe(11)
502+
expect(result.items.length).toBe(12)
503503
expect(result.items).toEqual(
504504
expect.arrayContaining([
505505
expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }),

packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This is invalid in this position because some `@import`s are not at the top of the file.
55
* We don't want project discovery to fail so we hoist them up and then warn in the console.
66
*/
7-
@variant dark (&:where(.dark, .dark *));
7+
@custom-variant dark (&:where(.dark, .dark *));
88

99
@import './a.css';
1010
@import './b.css';

packages/tailwindcss-language-service/src/completionProvider.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,66 @@ function provideVariantsDirectiveCompletions(
12751275
)
12761276
}
12771277

1278+
function provideVariantDirectiveCompletions(
1279+
state: State,
1280+
document: TextDocument,
1281+
position: Position,
1282+
): CompletionList {
1283+
if (!state.v4) return null
1284+
if (!isCssContext(state, document, position)) return null
1285+
1286+
let text = document.getText({
1287+
start: { line: position.line, character: 0 },
1288+
end: position,
1289+
})
1290+
1291+
let match = text.match(/^\s*@variant\s+(?<partial>[^}]*)$/i)
1292+
if (match === null) return null
1293+
1294+
let partial = match.groups.partial.trim()
1295+
1296+
// We only allow one variant `@variant` call
1297+
if (/\s/.test(partial)) return null
1298+
1299+
// We don't allow applying stacked variants so don't suggest them
1300+
if (/:/.test(partial)) return null
1301+
1302+
let possibleVariants = state.variants.flatMap((variant) => {
1303+
if (variant.values.length) {
1304+
return variant.values.map((value) =>
1305+
value === 'DEFAULT' ? variant.name : `${variant.name}${variant.hasDash ? '-' : ''}${value}`,
1306+
)
1307+
}
1308+
1309+
return [variant.name]
1310+
})
1311+
1312+
return withDefaults(
1313+
{
1314+
isIncomplete: false,
1315+
items: possibleVariants.map((variant, index, variants) => ({
1316+
label: variant,
1317+
kind: 21,
1318+
sortText: naturalExpand(index, variants.length),
1319+
})),
1320+
},
1321+
{
1322+
data: {
1323+
...(state.completionItemData ?? {}),
1324+
_type: 'variant',
1325+
},
1326+
range: {
1327+
start: {
1328+
line: position.line,
1329+
character: position.character,
1330+
},
1331+
end: position,
1332+
},
1333+
},
1334+
state.editor.capabilities.itemDefaults,
1335+
)
1336+
}
1337+
12781338
function provideLayerDirectiveCompletions(
12791339
state: State,
12801340
document: TextDocument,
@@ -1423,6 +1483,8 @@ function provideCssDirectiveCompletions(
14231483

14241484
if (match === null) return null
14251485

1486+
let isNested = isInsideNesting(document, position)
1487+
14261488
let items: CompletionItem[] = []
14271489

14281490
items.push({
@@ -1535,12 +1597,12 @@ function provideCssDirectiveCompletions(
15351597
})
15361598

15371599
items.push({
1538-
label: '@variant',
1600+
label: '@custom-variant',
15391601
documentation: {
15401602
kind: 'markdown' as typeof MarkupKind.Markdown,
1541-
value: `Use the \`@variant\` directive to define a custom variant or override an existing one.\n\n[Tailwind CSS Documentation](${docsUrl(
1603+
value: `Use the \`@custom-variant\` directive to define a custom variant or override an existing one.\n\n[Tailwind CSS Documentation](${docsUrl(
15421604
state.version,
1543-
'functions-and-directives/#variant',
1605+
'functions-and-directives/#custom-variant',
15441606
)})`,
15451607
},
15461608
})
@@ -1566,9 +1628,22 @@ function provideCssDirectiveCompletions(
15661628
)})`,
15671629
},
15681630
})
1631+
}
15691632

1570-
// If we're inside an @variant directive, also add `@slot`
1571-
if (isInsideAtRule('variant', document, position)) {
1633+
if (state.v4 && isNested) {
1634+
items.push({
1635+
label: '@variant',
1636+
documentation: {
1637+
kind: 'markdown' as typeof MarkupKind.Markdown,
1638+
value: `Use the \`@variant\` directive to use a variant in CSS.\n\n[Tailwind CSS Documentation](${docsUrl(
1639+
state.version,
1640+
'functions-and-directives/variant',
1641+
)})`,
1642+
},
1643+
})
1644+
1645+
// If we're inside an @custom-variant directive, also add `@slot`
1646+
if (isInsideAtRule('custom-variant', document, position)) {
15721647
items.push({
15731648
label: '@slot',
15741649
documentation: {
@@ -1611,20 +1686,29 @@ function provideCssDirectiveCompletions(
16111686
}
16121687

16131688
function isInsideAtRule(name: string, document: TextDocument, position: Position) {
1614-
// 1. Get all text up to the current position
16151689
let text = document.getText({
16161690
start: { line: 0, character: 0 },
16171691
end: position,
16181692
})
16191693

1620-
// 2. Find the last instance of the at-rule
1694+
// Find the last instance of the at-rule
16211695
let block = text.lastIndexOf(`@${name}`)
16221696
if (block === -1) return false
16231697

1624-
// 4. Count the number of open and close braces following the rule to determine if we're inside it
1698+
// Check if we're inside it by counting the number of still-open braces
16251699
return braceLevel(text.slice(block)) > 0
16261700
}
16271701

1702+
function isInsideNesting(document: TextDocument, position: Position) {
1703+
let text = document.getText({
1704+
start: { line: 0, character: 0 },
1705+
end: position,
1706+
})
1707+
1708+
// Check if we're inside a rule by counting the number of still-open braces
1709+
return braceLevel(text) > 0
1710+
}
1711+
16281712
// Provide completions for directives that take file paths
16291713
const PATTERN_AT_THEME = /@(?<directive>theme)\s+(?:(?<parts>[^{]+)\s$|$)/
16301714
const PATTERN_IMPORT_THEME = /@(?<directive>import)\s*[^;]+?theme\((?:(?<parts>[^)]+)\s$|$)/
@@ -1874,6 +1958,7 @@ export async function doComplete(
18741958
provideCssHelperCompletions(state, document, position) ||
18751959
provideCssDirectiveCompletions(state, document, position) ||
18761960
provideScreenDirectiveCompletions(state, document, position) ||
1961+
provideVariantDirectiveCompletions(state, document, position) ||
18771962
provideVariantsDirectiveCompletions(state, document, position) ||
18781963
provideTailwindDirectiveCompletions(state, document, position) ||
18791964
provideLayerDirectiveCompletions(state, document, position) ||

0 commit comments

Comments
 (0)