Skip to content

Commit 0d9bd2e

Browse files
v4: Show completions after any valid variant (#1263)
Fixes #943 Fixes #1257 This doesn't ensure variants like `not-supports-*` are suggested because `not-supports:` isn't valid. Only something with a value is e.g. `not-supports-display`. Making that work (it really should) is a larger task and requires designing an interactive suggestion API for v4.
1 parent b68a681 commit 0d9bd2e

File tree

5 files changed

+104
-69
lines changed

5 files changed

+104
-69
lines changed

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { test } from 'vitest'
1+
import { test, expect, describe } from 'vitest'
22
import { withFixture } from '../common'
3+
import { css, defineTest } from '../../src/testing'
4+
import { createClient } from '../utils/client'
35

46
function buildCompletion(c) {
57
return async function completion({
@@ -670,3 +672,69 @@ withFixture('v4/workspaces', (c) => {
670672
})
671673
})
672674
})
675+
676+
defineTest({
677+
name: 'v4: Completions show after a variant arbitrary value',
678+
fs: {
679+
'app.css': css`
680+
@import 'tailwindcss';
681+
`,
682+
},
683+
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
684+
handle: async ({ client }) => {
685+
let document = await client.open({
686+
lang: 'html',
687+
text: '<div class="data-[foo]:">',
688+
})
689+
690+
// <div class="data-[foo]:">
691+
// ^
692+
let completion = await document.completions({ line: 0, character: 23 })
693+
694+
expect(completion?.items.length).toBe(12289)
695+
},
696+
})
697+
698+
defineTest({
699+
name: 'v4: Completions show after an arbitrary variant',
700+
fs: {
701+
'app.css': css`
702+
@import 'tailwindcss';
703+
`,
704+
},
705+
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
706+
handle: async ({ client }) => {
707+
let document = await client.open({
708+
lang: 'html',
709+
text: '<div class="[&:hover]:">',
710+
})
711+
712+
// <div class="[&:hover]:">
713+
// ^
714+
let completion = await document.completions({ line: 0, character: 22 })
715+
716+
expect(completion?.items.length).toBe(12289)
717+
},
718+
})
719+
720+
defineTest({
721+
name: 'v4: Completions show after a variant with a bare value',
722+
fs: {
723+
'app.css': css`
724+
@import 'tailwindcss';
725+
`,
726+
},
727+
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
728+
handle: async ({ client }) => {
729+
let document = await client.open({
730+
lang: 'html',
731+
text: '<div class="supports-not-hover:">',
732+
})
733+
734+
// <div class="supports-not-hover:">
735+
// ^
736+
let completion = await document.completions({ line: 0, character: 31 })
737+
738+
expect(completion?.items.length).toBe(12289)
739+
},
740+
})

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,8 @@ export function completionsFromClassList(
189189
}),
190190
)
191191
} else {
192-
let shouldSortVariants = !semver.gte(state.version, '2.99.0')
193192
let resultingVariants = [...existingVariants, variant.name]
194193

195-
if (shouldSortVariants) {
196-
let allVariants = state.variants.map(({ name }) => name)
197-
resultingVariants = resultingVariants.sort(
198-
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a),
199-
)
200-
}
201-
202194
let selectors: string[] = []
203195

204196
try {
@@ -223,25 +215,6 @@ export function completionsFromClassList(
223215
.map((selector) => addPixelEquivalentsToMediaQuery(selector))
224216
.join(', '),
225217
textEditText: resultingVariants[resultingVariants.length - 1] + sep,
226-
additionalTextEdits:
227-
shouldSortVariants && resultingVariants.length > 1
228-
? [
229-
{
230-
newText:
231-
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
232-
range: {
233-
start: {
234-
...classListRange.start,
235-
character: classListRange.end.character - partialClassName.length,
236-
},
237-
end: {
238-
...replacementRange.start,
239-
character: replacementRange.start.character,
240-
},
241-
},
242-
},
243-
]
244-
: [],
245218
}),
246219
)
247220
}
Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { State } from './state'
22
import * as jit from './jit'
3+
import { segment } from './segment'
34

45
export function getVariantsFromClassName(
56
state: State,
@@ -13,60 +14,52 @@ export function getVariantsFromClassName(
1314
}
1415
return [variant.name]
1516
})
16-
let variants = new Set<string>()
17-
let offset = 0
18-
let parts = splitAtTopLevelOnly(className, state.separator)
17+
18+
let parts = segment(className, state.separator)
1919
if (parts.length < 2) {
20-
return { variants: Array.from(variants), offset }
20+
return { variants: [], offset: 0 }
2121
}
22+
2223
parts = parts.filter(Boolean)
2324

24-
for (let part of parts) {
25-
if (
26-
allVariants.includes(part) ||
27-
(state.jit &&
28-
((part.includes('[') && part.endsWith(']')) || part.includes('/')) &&
29-
jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0)
30-
) {
31-
variants.add(part)
32-
offset += part.length + state.separator.length
33-
continue
25+
function isValidVariant(part: string) {
26+
if (allVariants.includes(part)) {
27+
return true
3428
}
3529

36-
break
37-
}
30+
let className = `${part}${state.separator}[color:red]`
3831

39-
return { variants: Array.from(variants), offset }
40-
}
32+
if (state.v4) {
33+
// NOTE: This should never happen
34+
if (!state.designSystem) return false
4135

42-
// https://github.com/tailwindlabs/tailwindcss/blob/a8a2e2a7191fbd4bee044523aecbade5823a8664/src/util/splitAtTopLevelOnly.js
43-
function splitAtTopLevelOnly(input: string, separator: string): string[] {
44-
let stack: string[] = []
45-
let parts: string[] = []
46-
let lastPos = 0
36+
// We don't use `compile()` so there's no overhead from PostCSS
37+
let compiled = state.designSystem.candidatesToCss([className])
4738

48-
for (let idx = 0; idx < input.length; idx++) {
49-
let char = input[idx]
39+
// NOTE: This should never happen
40+
if (compiled.length !== 1) return false
5041

51-
if (stack.length === 0 && char === separator[0]) {
52-
if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) {
53-
parts.push(input.slice(lastPos, idx))
54-
lastPos = idx + separator.length
55-
}
42+
return compiled[0] !== null
5643
}
5744

58-
if (char === '(' || char === '[' || char === '{') {
59-
stack.push(char)
60-
} else if (
61-
(char === ')' && stack[stack.length - 1] === '(') ||
62-
(char === ']' && stack[stack.length - 1] === '[') ||
63-
(char === '}' && stack[stack.length - 1] === '{')
64-
) {
65-
stack.pop()
45+
if (state.jit) {
46+
if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) {
47+
return jit.generateRules(state, [className]).rules.length > 0
48+
}
6649
}
50+
51+
return false
6752
}
6853

69-
parts.push(input.slice(lastPos))
54+
let offset = 0
55+
let variants = new Set<string>()
7056

71-
return parts
57+
for (let part of parts) {
58+
if (!isValidVariant(part)) break
59+
60+
variants.add(part)
61+
offset += part.length + state.separator!.length
62+
}
63+
64+
return { variants: Array.from(variants), offset }
7265
}

packages/tailwindcss-language-service/src/util/v4/design-system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ export interface DesignSystem {
4444

4545
export interface DesignSystem {
4646
dependencies(): Set<string>
47-
compile(classes: string[]): postcss.Root[]
47+
compile(classes: string[]): (postcss.Root | null)[]
4848
toCss(nodes: postcss.Root | postcss.Node[]): string
4949
}

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Prerelease
44

55
- Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258))
6+
- v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263))
67

78
# 0.14.9
89

0 commit comments

Comments
 (0)