From 1bb224d2e48a2e809210d0fc49135d15c17e3a44 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 20 Jan 2025 20:31:07 -0500 Subject: [PATCH 1/3] Add support for `@custom-variant` --- .../src/language/cssServer.ts | 2 +- .../tests/completions/completions.test.js | 8 +- .../v4/invalid-import-order/tailwind.css | 2 +- .../src/completionProvider.ts | 101 ++++++++++++++++-- 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index fc37d854..bb228b19 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -396,7 +396,7 @@ async function validateTextDocument(textDocument: TextDocument): Promise { .filter((diagnostic) => { if ( diagnostic.code === 'unknownAtRules' && - /Unknown at rule @(tailwind|apply|config|theme|plugin|source|utility|variant)/.test( + /Unknown at rule @(tailwind|apply|config|theme|plugin|source|utility|variant|custom-variant)/.test( diagnostic.message, ) ) { diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index f6b4ed2c..6a353974 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -477,7 +477,7 @@ withFixture('v4/basic', (c) => { expect(result.items.filter((item) => item.label.startsWith('--')).length).toBe(23) }) - test.concurrent('@slot is suggeted inside @variant', async ({ expect }) => { + test.concurrent('@slot is suggeted inside @custom-variant', async ({ expect }) => { let result = await completion({ lang: 'css', text: '@', @@ -485,7 +485,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is NOT suggested by default - expect(result.items.length).toBe(10) + expect(result.items.length).toBe(11) expect(result.items).not.toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), @@ -494,12 +494,12 @@ withFixture('v4/basic', (c) => { result = await completion({ lang: 'css', - text: '@variant foo {\n@', + text: '@custom-variant foo {\n@', position: { line: 1, character: 1 }, }) // Make sure `@slot` is suggested - expect(result.items.length).toBe(11) + expect(result.items.length).toBe(12) expect(result.items).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css index e3d2eb5e..3dcea6ae 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css @@ -4,7 +4,7 @@ * This is invalid in this position because some `@import`s are not at the top of the file. * We don't want project discovery to fail so we hoist them up and then warn in the console. */ -@variant dark (&:where(.dark, .dark *)); +@custom-variant dark (&:where(.dark, .dark *)); @import './a.css'; @import './b.css'; diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 458c2c90..658f6f8c 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1275,6 +1275,66 @@ function provideVariantsDirectiveCompletions( ) } +function provideVariantDirectiveCompletions( + state: State, + document: TextDocument, + position: Position, +): CompletionList { + if (!state.v4) return null + if (!isCssContext(state, document, position)) return null + + let text = document.getText({ + start: { line: position.line, character: 0 }, + end: position, + }) + + let match = text.match(/^\s*@variant\s+(?[^}]*)$/i) + if (match === null) return null + + let partial = match.groups.partial.trim() + + // We only allow one variant `@variant` call + if (/\s/.test(partial)) return null + + // We don't allow applying stacked variants so don't suggest them + if (/:/.test(partial)) return null + + let possibleVariants = state.variants.flatMap((variant) => { + if (variant.values.length) { + return variant.values.map((value) => + value === 'DEFAULT' ? variant.name : `${variant.name}${variant.hasDash ? '-' : ''}${value}`, + ) + } + + return [variant.name] + }) + + return withDefaults( + { + isIncomplete: false, + items: possibleVariants.map((variant, index, variants) => ({ + label: variant, + kind: 21, + sortText: naturalExpand(index, variants.length), + })), + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: 'variant', + }, + range: { + start: { + line: position.line, + character: position.character, + }, + end: position, + }, + }, + state.editor.capabilities.itemDefaults, + ) +} + function provideLayerDirectiveCompletions( state: State, document: TextDocument, @@ -1423,6 +1483,8 @@ function provideCssDirectiveCompletions( if (match === null) return null + let isNested = isInsideNesting(document, position) + let items: CompletionItem[] = [] items.push({ @@ -1535,12 +1597,12 @@ function provideCssDirectiveCompletions( }) items.push({ - label: '@variant', + label: '@custom-variant', documentation: { kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@variant\` directive to define a custom variant or override an existing one.\n\n[Tailwind CSS Documentation](${docsUrl( + value: `Use the \`@custom-variant\` directive to define a custom variant or override an existing one.\n\n[Tailwind CSS Documentation](${docsUrl( state.version, - 'functions-and-directives/#variant', + 'functions-and-directives/#custom-variant', )})`, }, }) @@ -1566,9 +1628,22 @@ function provideCssDirectiveCompletions( )})`, }, }) + } - // If we're inside an @variant directive, also add `@slot` - if (isInsideAtRule('variant', document, position)) { + if (state.v4 && isNested) { + items.push({ + label: '@variant', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@variant\` directive to use a variant in CSS.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/variant', + )})`, + }, + }) + + // If we're inside an @custom-variant directive, also add `@slot` + if (isInsideAtRule('custom-variant', document, position)) { items.push({ label: '@slot', documentation: { @@ -1611,20 +1686,29 @@ function provideCssDirectiveCompletions( } function isInsideAtRule(name: string, document: TextDocument, position: Position) { - // 1. Get all text up to the current position let text = document.getText({ start: { line: 0, character: 0 }, end: position, }) - // 2. Find the last instance of the at-rule + // Find the last instance of the at-rule let block = text.lastIndexOf(`@${name}`) if (block === -1) return false - // 4. Count the number of open and close braces following the rule to determine if we're inside it + // Check if we're inside it by counting the number of still-open braces return braceLevel(text.slice(block)) > 0 } +function isInsideNesting(document: TextDocument, position: Position) { + let text = document.getText({ + start: { line: 0, character: 0 }, + end: position, + }) + + // Check if we're inside a rule by counting the number of still-open braces + return braceLevel(text) > 0 +} + // Provide completions for directives that take file paths const PATTERN_AT_THEME = /@(?theme)\s+(?:(?[^{]+)\s$|$)/ const PATTERN_IMPORT_THEME = /@(?import)\s*[^;]+?theme\((?:(?[^)]+)\s$|$)/ @@ -1874,6 +1958,7 @@ export async function doComplete( provideCssHelperCompletions(state, document, position) || provideCssDirectiveCompletions(state, document, position) || provideScreenDirectiveCompletions(state, document, position) || + provideVariantDirectiveCompletions(state, document, position) || provideVariantsDirectiveCompletions(state, document, position) || provideTailwindDirectiveCompletions(state, document, position) || provideLayerDirectiveCompletions(state, document, position) || From 8356f427be3cbec904974fd6dd2fe19fdec21042 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 20 Jan 2025 20:33:37 -0500 Subject: [PATCH 2/3] Suggest more appropriate at-rules based on context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v4 doesn’t have `@screen` - `@apply` doesn’t make sense to suggest top-level - `@tailwind` doesn’t make sense to suggest in v4 — we want people to use the imports instead - Things like `@config`, `@plugin`, `@source` etc… all must be top-level --- .../tests/completions/completions.test.js | 4 +- .../src/completionProvider.ts | 85 +++++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 6a353974..84b5c269 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -485,7 +485,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is NOT suggested by default - expect(result.items.length).toBe(11) + expect(result.items.length).toBe(7) expect(result.items).not.toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), @@ -499,7 +499,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is suggested - expect(result.items.length).toBe(12) + expect(result.items.length).toBe(4) expect(result.items).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 658f6f8c..f411ba22 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1353,10 +1353,16 @@ function provideLayerDirectiveCompletions( if (match === null) return null + let layerNames = ['base', 'components', 'utilities'] + + if (state.v4) { + layerNames = ['theme', 'base', 'components', 'utilities'] + } + return withDefaults( { isIncomplete: false, - items: ['base', 'components', 'utilities'].map((layer, index, layers) => ({ + items: layerNames.map((layer, index, layers) => ({ label: layer, kind: 21, sortText: naturalExpand(index, layers.length), @@ -1487,40 +1493,49 @@ function provideCssDirectiveCompletions( let items: CompletionItem[] = [] - items.push({ - label: '@tailwind', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@tailwind\` directive to insert Tailwind’s \`base\`, \`components\`, \`utilities\` and \`${ - state.jit && semver.gte(state.version, '2.1.99') ? 'variants' : 'screens' - }\` styles into your CSS.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind', - )})`, - }, - }) + if (state.v4) { + // We don't suggest @tailwind anymore in v4 because we prefer that people + // use the imports instead + } else { + items.push({ + label: '@tailwind', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@tailwind\` directive to insert Tailwind’s \`base\`, \`components\`, \`utilities\` and \`${ + state.jit && semver.gte(state.version, '2.1.99') ? 'variants' : 'screens' + }\` styles into your CSS.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind', + )})`, + }, + }) + } - items.push({ - label: '@screen', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `The \`@screen\` directive allows you to create media queries that reference your breakpoints by name instead of duplicating their values in your own CSS.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#screen', - )})`, - }, - }) + if (!state.v4) { + items.push({ + label: '@screen', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `The \`@screen\` directive allows you to create media queries that reference your breakpoints by name instead of duplicating their values in your own CSS.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#screen', + )})`, + }, + }) + } - items.push({ - label: '@apply', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use \`@apply\` to inline any existing utility classes into your own custom CSS.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#apply', - )})`, - }, - }) + if (isNested) { + items.push({ + label: '@apply', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use \`@apply\` to inline any existing utility classes into your own custom CSS.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#apply', + )})`, + }, + }) + } if (semver.gte(state.version, '1.8.0')) { items.push({ @@ -1560,7 +1575,7 @@ function provideCssDirectiveCompletions( }) } - if (semver.gte(state.version, '3.2.0')) { + if (semver.gte(state.version, '3.2.0') && !isNested) { items.push({ label: '@config', documentation: { @@ -1573,7 +1588,7 @@ function provideCssDirectiveCompletions( }) } - if (state.v4) { + if (state.v4 && !isNested) { items.push({ label: '@theme', documentation: { From a7e06a62bee1c7645a33f1ede1c66d7f53e08a1b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 20 Jan 2025 20:40:48 -0500 Subject: [PATCH 3/3] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index d59e1e84..733d92f1 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -5,6 +5,9 @@ - Don't break when importing missing CSS files ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) - Resolve CSS imports as relative first ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) - Add TypeScript config path support in v4 CSS files ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) +- Add support for `@custom-variant` ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) +- Add variant suggestions to `@variant` ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) +- Don't suggest at-rules when nested that cannot be used in a nested context ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) ## 0.12.18