From 4c7a0cb0faa86217c6e5cb55d129f6bed2f25444 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 14 Mar 2025 10:45:56 -0400 Subject: [PATCH 1/5] Refactor --- .../src/util/getVariantsFromClassName.ts | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index b58bb29f..0a6d85c8 100644 --- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts +++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts @@ -1,5 +1,6 @@ import type { State } from './state' import * as jit from './jit' +import { segment } from './segment' export function getVariantsFromClassName( state: State, @@ -13,60 +14,39 @@ export function getVariantsFromClassName( } return [variant.name] }) - let variants = new Set() - let offset = 0 - let parts = splitAtTopLevelOnly(className, state.separator) + + let parts = segment(className, state.separator) if (parts.length < 2) { - return { variants: Array.from(variants), offset } + return { variants: [], offset: 0 } } + parts = parts.filter(Boolean) - for (let part of parts) { - if ( - allVariants.includes(part) || - (state.jit && - ((part.includes('[') && part.endsWith(']')) || part.includes('/')) && - jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0) - ) { - variants.add(part) - offset += part.length + state.separator.length - continue + function isValidVariant(part: string) { + if (allVariants.includes(part)) { + return true } - break - } - - return { variants: Array.from(variants), offset } -} - -// https://github.com/tailwindlabs/tailwindcss/blob/a8a2e2a7191fbd4bee044523aecbade5823a8664/src/util/splitAtTopLevelOnly.js -function splitAtTopLevelOnly(input: string, separator: string): string[] { - let stack: string[] = [] - let parts: string[] = [] - let lastPos = 0 + let className = `${part}${state.separator}[color:red]` - for (let idx = 0; idx < input.length; idx++) { - let char = input[idx] - - if (stack.length === 0 && char === separator[0]) { - if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) { - parts.push(input.slice(lastPos, idx)) - lastPos = idx + separator.length + if (state.jit) { + if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) { + return jit.generateRules(state, [className]).rules.length > 0 } } - if (char === '(' || char === '[' || char === '{') { - stack.push(char) - } else if ( - (char === ')' && stack[stack.length - 1] === '(') || - (char === ']' && stack[stack.length - 1] === '[') || - (char === '}' && stack[stack.length - 1] === '{') - ) { - stack.pop() - } + return false } - parts.push(input.slice(lastPos)) + let offset = 0 + let variants = new Set() - return parts + for (let part of parts) { + if (!isValidVariant(part)) break + + variants.add(part) + offset += part.length + state.separator!.length + } + + return { variants: Array.from(variants), offset } } From edcfadb9ed92b07d45d24c32403c8e7651af1cb5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 14 Mar 2025 10:47:18 -0400 Subject: [PATCH 2/5] Fix types --- .../tailwindcss-language-service/src/util/v4/design-system.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index cce64d4b..3fb3c401 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -44,6 +44,6 @@ export interface DesignSystem { export interface DesignSystem { dependencies(): Set - compile(classes: string[]): postcss.Root[] + compile(classes: string[]): (postcss.Root | null)[] toCss(nodes: postcss.Root | postcss.Node[]): string } From af01c9bd30790e5b699a2a6d7225cefaedf1c35f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:05:40 -0400 Subject: [PATCH 3/5] Cleanup This was just dead code b/c the version number is not 2.x --- .../src/completionProvider.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index fe43bb68..1579fd93 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -189,16 +189,8 @@ export function completionsFromClassList( }), ) } else { - let shouldSortVariants = !semver.gte(state.version, '2.99.0') let resultingVariants = [...existingVariants, variant.name] - if (shouldSortVariants) { - let allVariants = state.variants.map(({ name }) => name) - resultingVariants = resultingVariants.sort( - (a, b) => allVariants.indexOf(b) - allVariants.indexOf(a), - ) - } - let selectors: string[] = [] try { @@ -223,25 +215,6 @@ export function completionsFromClassList( .map((selector) => addPixelEquivalentsToMediaQuery(selector)) .join(', '), textEditText: resultingVariants[resultingVariants.length - 1] + sep, - additionalTextEdits: - shouldSortVariants && resultingVariants.length > 1 - ? [ - { - newText: - resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep, - range: { - start: { - ...classListRange.start, - character: classListRange.end.character - partialClassName.length, - }, - end: { - ...replacementRange.start, - character: replacementRange.start.character, - }, - }, - }, - ] - : [], }), ) } From 7545ccd85f56c95957ef2c6238815c375c13bf57 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 14 Mar 2025 16:09:26 -0400 Subject: [PATCH 4/5] v4: Show completions after any valid variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We weren’t handling the case of arbitrary variants, variants with an arbitrary value, and variants with bare values. We have to compile a dummy class temporarily to determine whether or not the class is valid --- .../tests/completions/completions.test.js | 70 ++++++++++++++++++- .../src/util/getVariantsFromClassName.ts | 13 ++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 2727fcb0..f5393a40 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -1,5 +1,7 @@ -import { test } from 'vitest' +import { test, expect, describe } from 'vitest' import { withFixture } from '../common' +import { css, defineTest } from '../../src/testing' +import { createClient } from '../utils/client' function buildCompletion(c) { return async function completion({ @@ -670,3 +672,69 @@ withFixture('v4/workspaces', (c) => { }) }) }) + +defineTest({ + name: 'v4: Completions show after a variant arbitrary value', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 23 }) + + expect(completion?.items.length).toBe(12289) + }, +}) + +defineTest({ + name: 'v4: Completions show after an arbitrary variant', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 22 }) + + expect(completion?.items.length).toBe(12289) + }, +}) + +defineTest({ + name: 'v4: Completions show after a variant with a bare value', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 31 }) + + expect(completion?.items.length).toBe(12289) + }, +}) diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index 0a6d85c8..c30e729a 100644 --- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts +++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts @@ -29,6 +29,19 @@ export function getVariantsFromClassName( let className = `${part}${state.separator}[color:red]` + if (state.v4) { + // NOTE: This should never happen + if (!state.designSystem) return false + + // We don't use `compile()` so there's no overhead from PostCSS + let compiled = state.designSystem.candidatesToCss([className]) + + // NOTE: This should never happen + if (compiled.length !== 1) return false + + return compiled[0] !== null + } + if (state.jit) { if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) { return jit.generateRules(state, [className]).rules.length > 0 From c660b19dfd4b75845d56a2aa0da806c3be1f9cb5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 09:59:23 -0400 Subject: [PATCH 5/5] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index dd3aa562..7b9e68fa 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -3,6 +3,7 @@ ## Prerelease - Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) +- v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) # 0.14.9