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/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, - }, - }, - }, - ] - : [], }), ) } diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index b58bb29f..c30e729a 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,52 @@ 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 - } + let className = `${part}${state.separator}[color:red]` - return { variants: Array.from(variants), offset } -} + if (state.v4) { + // NOTE: This should never happen + if (!state.designSystem) return false -// 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 + // We don't use `compile()` so there's no overhead from PostCSS + let compiled = state.designSystem.candidatesToCss([className]) - for (let idx = 0; idx < input.length; idx++) { - let char = input[idx] + // NOTE: This should never happen + if (compiled.length !== 1) return false - 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 - } + return compiled[0] !== null } - 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() + if (state.jit) { + if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) { + return jit.generateRules(state, [className]).rules.length > 0 + } } + + 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 } } 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 } 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