Skip to content

Commit e601610

Browse files
Improve support for new v4 at rules (#1045)
1 parent 6134ab7 commit e601610

File tree

16 files changed

+890
-107
lines changed

16 files changed

+890
-107
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ async function validateTextDocument(textDocument: TextDocument): Promise<void> {
381381
.filter((diagnostic) => {
382382
if (
383383
diagnostic.code === 'unknownAtRules' &&
384-
/Unknown at rule @(tailwind|apply|config|theme|plugin|source)/.test(diagnostic.message)
384+
/Unknown at rule @(tailwind|apply|config|theme|plugin|source|utility|variant)/.test(diagnostic.message)
385385
) {
386386
return false
387387
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,18 +223,22 @@ export async function createProjectService(
223223
try {
224224
directory = path.resolve(path.dirname(getFileFsPath(document.uri)), directory)
225225
let dirents = await fs.promises.readdir(directory, { withFileTypes: true })
226+
226227
let result: Array<[string, { isDirectory: boolean }] | null> = await Promise.all(
227228
dirents.map(async (dirent) => {
228229
let isDirectory = dirent.isDirectory()
229-
return (await isExcluded(
230+
let shouldRemove = await isExcluded(
230231
state,
231232
document,
232233
path.join(directory, dirent.name, isDirectory ? '/' : ''),
233-
))
234-
? null
235-
: [dirent.name, { isDirectory }]
234+
)
235+
236+
if (shouldRemove) return null
237+
238+
return [dirent.name, { isDirectory }]
236239
}),
237240
)
241+
238242
return result.filter((item) => item !== null)
239243
} catch {
240244
return []

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

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,218 @@ withFixture('dependencies', (c) => {
8282
})
8383
})
8484
})
85+
86+
withFixture('v4/dependencies', (c) => {
87+
async function completion({
88+
lang,
89+
text,
90+
position,
91+
context = {
92+
triggerKind: 1,
93+
},
94+
settings,
95+
}) {
96+
let textDocument = await c.openDocument({ text, lang, settings })
97+
98+
return c.sendRequest('textDocument/completion', {
99+
textDocument,
100+
position,
101+
context,
102+
})
103+
}
104+
105+
test.concurrent('@config', async ({ expect }) => {
106+
let result = await completion({
107+
text: '@config "',
108+
lang: 'css',
109+
position: {
110+
line: 0,
111+
character: 9,
112+
},
113+
})
114+
115+
expect(result).toEqual({
116+
isIncomplete: false,
117+
items: [
118+
{
119+
label: 'sub-dir/',
120+
kind: 19,
121+
command: { command: 'editor.action.triggerSuggest', title: '' },
122+
data: expect.anything(),
123+
textEdit: {
124+
newText: 'sub-dir/',
125+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
126+
},
127+
},
128+
{
129+
label: 'tailwind.config.js',
130+
kind: 17,
131+
data: expect.anything(),
132+
textEdit: {
133+
newText: 'tailwind.config.js',
134+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
135+
},
136+
},
137+
],
138+
})
139+
})
140+
141+
test.concurrent('@config directory', async ({ expect }) => {
142+
let result = await completion({
143+
text: '@config "./sub-dir/',
144+
lang: 'css',
145+
position: {
146+
line: 0,
147+
character: 19,
148+
},
149+
})
150+
151+
expect(result).toEqual({
152+
isIncomplete: false,
153+
items: [
154+
{
155+
label: 'colors.js',
156+
kind: 17,
157+
data: expect.anything(),
158+
textEdit: {
159+
newText: 'colors.js',
160+
range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } },
161+
},
162+
},
163+
],
164+
})
165+
})
166+
167+
test.concurrent('@plugin', async ({ expect }) => {
168+
let result = await completion({
169+
text: '@plugin "',
170+
lang: 'css',
171+
position: {
172+
line: 0,
173+
character: 9,
174+
},
175+
})
176+
177+
expect(result).toEqual({
178+
isIncomplete: false,
179+
items: [
180+
{
181+
label: 'sub-dir/',
182+
kind: 19,
183+
command: { command: 'editor.action.triggerSuggest', title: '' },
184+
data: expect.anything(),
185+
textEdit: {
186+
newText: 'sub-dir/',
187+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
188+
},
189+
},
190+
{
191+
label: 'tailwind.config.js',
192+
kind: 17,
193+
data: expect.anything(),
194+
textEdit: {
195+
newText: 'tailwind.config.js',
196+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
197+
},
198+
},
199+
],
200+
})
201+
})
202+
203+
test.concurrent('@plugin directory', async ({ expect }) => {
204+
let result = await completion({
205+
text: '@plugin "./sub-dir/',
206+
lang: 'css',
207+
position: {
208+
line: 0,
209+
character: 19,
210+
},
211+
})
212+
213+
expect(result).toEqual({
214+
isIncomplete: false,
215+
items: [
216+
{
217+
label: 'colors.js',
218+
kind: 17,
219+
data: expect.anything(),
220+
textEdit: {
221+
newText: 'colors.js',
222+
range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } },
223+
},
224+
},
225+
],
226+
})
227+
})
228+
229+
test.concurrent('@source', async ({ expect }) => {
230+
let result = await completion({
231+
text: '@source "',
232+
lang: 'css',
233+
position: {
234+
line: 0,
235+
character: 9,
236+
},
237+
})
238+
239+
expect(result).toEqual({
240+
isIncomplete: false,
241+
items: [
242+
{
243+
label: 'index.html',
244+
kind: 17,
245+
data: expect.anything(),
246+
textEdit: {
247+
newText: 'index.html',
248+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
249+
},
250+
},
251+
{
252+
label: 'sub-dir/',
253+
kind: 19,
254+
command: { command: 'editor.action.triggerSuggest', title: '' },
255+
data: expect.anything(),
256+
textEdit: {
257+
newText: 'sub-dir/',
258+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
259+
},
260+
},
261+
{
262+
label: 'tailwind.config.js',
263+
kind: 17,
264+
data: expect.anything(),
265+
textEdit: {
266+
newText: 'tailwind.config.js',
267+
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
268+
},
269+
},
270+
],
271+
})
272+
})
273+
274+
test.concurrent('@source directory', async ({ expect }) => {
275+
let result = await completion({
276+
text: '@source "./sub-dir/',
277+
lang: 'css',
278+
position: {
279+
line: 0,
280+
character: 19,
281+
},
282+
})
283+
284+
expect(result).toEqual({
285+
isIncomplete: false,
286+
items: [
287+
{
288+
label: 'colors.js',
289+
kind: 17,
290+
data: expect.anything(),
291+
textEdit: {
292+
newText: 'colors.js',
293+
range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } },
294+
},
295+
},
296+
],
297+
})
298+
})
299+
})

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,36 @@ 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 }) => {
481+
let result = await completion({
482+
lang: 'css',
483+
text: '@',
484+
position: { line: 0, character: 1 },
485+
})
486+
487+
// Make sure `@slot` is NOT suggested by default
488+
expect(result.items.length).toBe(10)
489+
expect(result.items).not.toEqual(
490+
expect.arrayContaining([
491+
expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }),
492+
]),
493+
)
494+
495+
result = await completion({
496+
lang: 'css',
497+
text: '@variant foo {\n@',
498+
position: { line: 1, character: 1 },
499+
})
500+
501+
// Make sure `@slot` is suggested
502+
expect(result.items.length).toBe(11)
503+
expect(result.items).toEqual(
504+
expect.arrayContaining([
505+
expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }),
506+
]),
507+
)
508+
})
509+
480510
test.concurrent('resolve', async ({ expect }) => {
481511
let result = await completion({
482512
text: '<div class="">',

packages/tailwindcss-language-server/tests/document-links/document-links.test.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,94 @@ withFixture('basic', (c) => {
4040
],
4141
})
4242
})
43+
44+
withFixture('v4/basic', (c) => {
45+
async function testDocumentLinks(name, { text, lang, expected }) {
46+
test.concurrent(name, async ({ expect }) => {
47+
let textDocument = await c.openDocument({ text, lang })
48+
let res = await c.sendRequest('textDocument/documentLink', {
49+
textDocument,
50+
})
51+
52+
expect(res).toEqual(expected)
53+
})
54+
}
55+
56+
testDocumentLinks('config: file exists', {
57+
text: '@config "tailwind.config.js";',
58+
lang: 'css',
59+
expected: [
60+
{
61+
target: `file://${path
62+
.resolve('./tests/fixtures/v4/basic/tailwind.config.js')
63+
.replace(/@/g, '%40')}`,
64+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 28 } },
65+
},
66+
],
67+
})
68+
69+
testDocumentLinks('config: file does not exist', {
70+
text: '@config "does-not-exist.js";',
71+
lang: 'css',
72+
expected: [
73+
{
74+
target: `file://${path
75+
.resolve('./tests/fixtures/v4/basic/does-not-exist.js')
76+
.replace(/@/g, '%40')}`,
77+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 27 } },
78+
},
79+
],
80+
})
81+
82+
testDocumentLinks('plugin: file exists', {
83+
text: '@plugin "plugin.js";',
84+
lang: 'css',
85+
expected: [
86+
{
87+
target: `file://${path
88+
.resolve('./tests/fixtures/v4/basic/plugin.js')
89+
.replace(/@/g, '%40')}`,
90+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 19 } },
91+
},
92+
],
93+
})
94+
95+
testDocumentLinks('plugin: file does not exist', {
96+
text: '@plugin "does-not-exist.js";',
97+
lang: 'css',
98+
expected: [
99+
{
100+
target: `file://${path
101+
.resolve('./tests/fixtures/v4/basic/does-not-exist.js')
102+
.replace(/@/g, '%40')}`,
103+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 27 } },
104+
},
105+
],
106+
})
107+
108+
testDocumentLinks('source: file exists', {
109+
text: '@source "index.html";',
110+
lang: 'css',
111+
expected: [
112+
{
113+
target: `file://${path
114+
.resolve('./tests/fixtures/v4/basic/index.html')
115+
.replace(/@/g, '%40')}`,
116+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 20 } },
117+
},
118+
],
119+
})
120+
121+
testDocumentLinks('source: file does not exist', {
122+
text: '@source "does-not-exist.html";',
123+
lang: 'css',
124+
expected: [
125+
{
126+
target: `file://${path
127+
.resolve('./tests/fixtures/v4/basic/does-not-exist.html')
128+
.replace(/@/g, '%40')}`,
129+
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 29 } },
130+
},
131+
],
132+
})
133+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import 'tailwindcss';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="underline">foo</div>

0 commit comments

Comments
 (0)