Skip to content

Commit 9f9208a

Browse files
Add support for @source not and @source inline(…) (#1262)
IntelliSense counterpart of tailwindlabs/tailwindcss#17147
1 parent 0d9bd2e commit 9f9208a

23 files changed

+624
-21
lines changed

packages/tailwindcss-language-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@tailwindcss/line-clamp": "0.4.2",
4343
"@tailwindcss/oxide": "^4.0.0-alpha.19",
4444
"@tailwindcss/typography": "0.5.7",
45+
"@types/braces": "3.0.1",
4546
"@types/color-name": "^1.1.3",
4647
"@types/culori": "^2.1.0",
4748
"@types/debounce": "1.2.0",

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import type {
1515
Disposable,
1616
DocumentLinkParams,
1717
DocumentLink,
18+
CodeLensParams,
19+
CodeLens,
1820
} from 'vscode-languageserver/node'
1921
import { FileChangeType } from 'vscode-languageserver/node'
2022
import type { TextDocument } from 'vscode-languageserver-textdocument'
@@ -35,6 +37,7 @@ import stackTrace from 'stack-trace'
3537
import extractClassNames from './lib/extractClassNames'
3638
import { klona } from 'klona/full'
3739
import { doHover } from '@tailwindcss/language-service/src/hoverProvider'
40+
import { getCodeLens } from '@tailwindcss/language-service/src/codeLensProvider'
3841
import { Resolver } from './resolver'
3942
import {
4043
doComplete,
@@ -110,6 +113,7 @@ export interface ProjectService {
110113
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
111114
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
112115
onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]>
116+
onCodeLens(params: CodeLensParams): Promise<CodeLens[]>
113117
sortClassLists(classLists: string[]): string[]
114118

115119
dependencies(): Iterable<string>
@@ -212,6 +216,7 @@ export async function createProjectService(
212216

213217
let state: State = {
214218
enabled: false,
219+
features: [],
215220
completionItemData: {
216221
_projectKey: projectKey,
217222
},
@@ -462,6 +467,14 @@ export async function createProjectService(
462467
// and this should be determined there and passed in instead
463468
let features = supportedFeatures(tailwindcssVersion, tailwindcss)
464469
log(`supported features: ${JSON.stringify(features)}`)
470+
state.features = features
471+
472+
if (params.initializationOptions?.testMode) {
473+
state.features = [
474+
...state.features,
475+
...(params.initializationOptions.additionalFeatures ?? []),
476+
]
477+
}
465478

466479
if (!features.includes('css-at-theme')) {
467480
tailwindcss = tailwindcss.default ?? tailwindcss
@@ -688,6 +701,15 @@ export async function createProjectService(
688701
state.v4 = true
689702
state.v4Fallback = true
690703
state.jit = true
704+
state.features = features
705+
706+
if (params.initializationOptions?.testMode) {
707+
state.features = [
708+
...state.features,
709+
...(params.initializationOptions.additionalFeatures ?? []),
710+
]
711+
}
712+
691713
state.modules = {
692714
tailwindcss: { version: tailwindcssVersion, module: tailwindcss },
693715
postcss: { version: null, module: null },
@@ -1150,7 +1172,7 @@ export async function createProjectService(
11501172
},
11511173
tryInit,
11521174
async dispose() {
1153-
state = { enabled: false }
1175+
state = { enabled: false, features: [] }
11541176
for (let disposable of disposables) {
11551177
;(await disposable).dispose()
11561178
}
@@ -1177,6 +1199,17 @@ export async function createProjectService(
11771199
return doHover(state, document, params.position)
11781200
}, null)
11791201
},
1202+
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
1203+
return withFallback(async () => {
1204+
if (!state.enabled) return null
1205+
let document = documentService.getDocument(params.textDocument.uri)
1206+
if (!document) return null
1207+
let settings = await state.editor.getConfiguration(document.uri)
1208+
if (!settings.tailwindCSS.codeLens) return null
1209+
if (await isExcluded(state, document)) return null
1210+
return getCodeLens(state, document)
1211+
}, null)
1212+
},
11801213
async onCompletion(params: CompletionParams): Promise<CompletionList> {
11811214
return withFallback(async () => {
11821215
if (!state.enabled) return null

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type {
1919
DocumentLink,
2020
InitializeResult,
2121
WorkspaceFolder,
22+
CodeLensParams,
23+
CodeLens,
2224
} from 'vscode-languageserver/node'
2325
import {
2426
CompletionRequest,
@@ -30,6 +32,7 @@ import {
3032
FileChangeType,
3133
DocumentLinkRequest,
3234
TextDocumentSyncKind,
35+
CodeLensRequest,
3336
} from 'vscode-languageserver/node'
3437
import { URI } from 'vscode-uri'
3538
import normalizePath from 'normalize-path'
@@ -757,6 +760,7 @@ export class TW {
757760
this.connection.onDocumentColor(this.onDocumentColor.bind(this))
758761
this.connection.onColorPresentation(this.onColorPresentation.bind(this))
759762
this.connection.onCodeAction(this.onCodeAction.bind(this))
763+
this.connection.onCodeLens(this.onCodeLens.bind(this))
760764
this.connection.onDocumentLinks(this.onDocumentLinks.bind(this))
761765
this.connection.onRequest(this.onRequest.bind(this))
762766
}
@@ -809,6 +813,7 @@ export class TW {
809813
capabilities.add(HoverRequest.type, { documentSelector: null })
810814
capabilities.add(DocumentColorRequest.type, { documentSelector: null })
811815
capabilities.add(CodeActionRequest.type, { documentSelector: null })
816+
capabilities.add(CodeLensRequest.type, { documentSelector: null })
812817
capabilities.add(DocumentLinkRequest.type, { documentSelector: null })
813818

814819
capabilities.add(CompletionRequest.type, {
@@ -931,6 +936,11 @@ export class TW {
931936
return this.getProject(params.textDocument)?.onCodeAction(params) ?? null
932937
}
933938

939+
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
940+
await this.init()
941+
return this.getProject(params.textDocument)?.onCodeLens(params) ?? null
942+
}
943+
934944
async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
935945
await this.init()
936946
return this.getProject(params.textDocument)?.onDocumentLinks(params) ?? null
@@ -961,6 +971,9 @@ export class TW {
961971
hoverProvider: true,
962972
colorProvider: true,
963973
codeActionProvider: true,
974+
codeLensProvider: {
975+
resolveProvider: false,
976+
},
964977
documentLinkProvider: {},
965978
completionProvider: {
966979
resolveProvider: true,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { expect } from 'vitest'
2+
import { css, defineTest } from '../../src/testing'
3+
import { createClient } from '../utils/client'
4+
5+
defineTest({
6+
name: 'Code lenses are displayed for @source inline(…)',
7+
fs: {
8+
'app.css': css`
9+
@import 'tailwindcss';
10+
`,
11+
},
12+
prepare: async ({ root }) => ({
13+
client: await createClient({
14+
root,
15+
features: ['source-inline'],
16+
}),
17+
}),
18+
handle: async ({ client }) => {
19+
let document = await client.open({
20+
lang: 'css',
21+
text: css`
22+
@import 'tailwindcss';
23+
@source inline("{,{hover,focus}:}{flex,underline,bg-red-{50,{100..900.100},950}}");
24+
`,
25+
})
26+
27+
let lenses = await document.codeLenses()
28+
29+
expect(lenses).toEqual([
30+
{
31+
range: {
32+
start: { line: 1, character: 15 },
33+
end: { line: 1, character: 81 },
34+
},
35+
command: {
36+
title: 'Generates 15 classes',
37+
command: '',
38+
},
39+
},
40+
])
41+
},
42+
})
43+
44+
defineTest({
45+
name: 'The user is warned when @source inline(…) generates a lerge amount of CSS',
46+
fs: {
47+
'app.css': css`
48+
@import 'tailwindcss';
49+
`,
50+
},
51+
prepare: async ({ root }) => ({
52+
client: await createClient({
53+
root,
54+
features: ['source-inline'],
55+
}),
56+
}),
57+
handle: async ({ client }) => {
58+
let document = await client.open({
59+
lang: 'css',
60+
text: css`
61+
@import 'tailwindcss';
62+
@source inline("{,dark:}{,{sm,md,lg,xl,2xl}:}{,{hover,focus,active}:}{flex,underline,bg-red-{50,{100..900.100},950}{,/{0..100}}}");
63+
`,
64+
})
65+
66+
let lenses = await document.codeLenses()
67+
68+
expect(lenses).toEqual([
69+
{
70+
range: {
71+
start: { line: 1, character: 15 },
72+
end: { line: 1, character: 129 },
73+
},
74+
command: {
75+
title: 'Generates 14,784 classes',
76+
command: '',
77+
},
78+
},
79+
{
80+
range: {
81+
start: { line: 1, character: 15 },
82+
end: { line: 1, character: 129 },
83+
},
84+
command: {
85+
title: 'At least 3MB of CSS',
86+
command: '',
87+
},
88+
},
89+
{
90+
range: {
91+
start: { line: 1, character: 15 },
92+
end: { line: 1, character: 129 },
93+
},
94+
command: {
95+
title: 'This may slow down your bundler/browser',
96+
command: '',
97+
},
98+
},
99+
])
100+
},
101+
})

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,51 @@ withFixture('v4/dependencies', (c) => {
271271
})
272272
})
273273

274+
test.concurrent('@source not', async ({ expect }) => {
275+
let result = await completion({
276+
text: '@source not "',
277+
lang: 'css',
278+
position: {
279+
line: 0,
280+
character: 13,
281+
},
282+
})
283+
284+
expect(result).toEqual({
285+
isIncomplete: false,
286+
items: [
287+
{
288+
label: 'index.html',
289+
kind: 17,
290+
data: expect.anything(),
291+
textEdit: {
292+
newText: 'index.html',
293+
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
294+
},
295+
},
296+
{
297+
label: 'sub-dir/',
298+
kind: 19,
299+
command: { command: 'editor.action.triggerSuggest', title: '' },
300+
data: expect.anything(),
301+
textEdit: {
302+
newText: 'sub-dir/',
303+
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
304+
},
305+
},
306+
{
307+
label: 'tailwind.config.js',
308+
kind: 17,
309+
data: expect.anything(),
310+
textEdit: {
311+
newText: 'tailwind.config.js',
312+
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
313+
},
314+
},
315+
],
316+
})
317+
})
318+
274319
test.concurrent('@source directory', async ({ expect }) => {
275320
let result = await completion({
276321
text: '@source "./sub-dir/',
@@ -297,6 +342,58 @@ withFixture('v4/dependencies', (c) => {
297342
})
298343
})
299344

345+
test.concurrent('@source not directory', async ({ expect }) => {
346+
let result = await completion({
347+
text: '@source not "./sub-dir/',
348+
lang: 'css',
349+
position: {
350+
line: 0,
351+
character: 23,
352+
},
353+
})
354+
355+
expect(result).toEqual({
356+
isIncomplete: false,
357+
items: [
358+
{
359+
label: 'colors.js',
360+
kind: 17,
361+
data: expect.anything(),
362+
textEdit: {
363+
newText: 'colors.js',
364+
range: { start: { line: 0, character: 23 }, end: { line: 0, character: 23 } },
365+
},
366+
},
367+
],
368+
})
369+
})
370+
371+
test.concurrent('@source inline(…)', async ({ expect }) => {
372+
let result = await completion({
373+
text: '@source inline("',
374+
lang: 'css',
375+
position: {
376+
line: 0,
377+
character: 16,
378+
},
379+
})
380+
381+
expect(result).toEqual(null)
382+
})
383+
384+
test.concurrent('@source not inline(…)', async ({ expect }) => {
385+
let result = await completion({
386+
text: '@source not inline("',
387+
lang: 'css',
388+
position: {
389+
line: 0,
390+
character: 20,
391+
},
392+
})
393+
394+
expect(result).toEqual(null)
395+
})
396+
300397
test.concurrent('@import "…" source(…)', async ({ expect }) => {
301398
let result = await completion({
302399
text: '@import "tailwindcss" source("',

0 commit comments

Comments
 (0)