Skip to content

Add support for @source not and @source inline(…) #1262

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/tailwindcss-language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@tailwindcss/line-clamp": "0.4.2",
"@tailwindcss/oxide": "^4.0.0-alpha.19",
"@tailwindcss/typography": "0.5.7",
"@types/braces": "3.0.1",
"@types/color-name": "^1.1.3",
"@types/culori": "^2.1.0",
"@types/debounce": "1.2.0",
Expand Down
35 changes: 34 additions & 1 deletion packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {
Disposable,
DocumentLinkParams,
DocumentLink,
CodeLensParams,
CodeLens,
} from 'vscode-languageserver/node'
import { FileChangeType } from 'vscode-languageserver/node'
import type { TextDocument } from 'vscode-languageserver-textdocument'
Expand All @@ -35,6 +37,7 @@ import stackTrace from 'stack-trace'
import extractClassNames from './lib/extractClassNames'
import { klona } from 'klona/full'
import { doHover } from '@tailwindcss/language-service/src/hoverProvider'
import { getCodeLens } from '@tailwindcss/language-service/src/codeLensProvider'
import { Resolver } from './resolver'
import {
doComplete,
Expand Down Expand Up @@ -110,6 +113,7 @@ export interface ProjectService {
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]>
onCodeLens(params: CodeLensParams): Promise<CodeLens[]>
sortClassLists(classLists: string[]): string[]

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

let state: State = {
enabled: false,
features: [],
completionItemData: {
_projectKey: projectKey,
},
Expand Down Expand Up @@ -462,6 +467,14 @@ export async function createProjectService(
// and this should be determined there and passed in instead
let features = supportedFeatures(tailwindcssVersion, tailwindcss)
log(`supported features: ${JSON.stringify(features)}`)
state.features = features

if (params.initializationOptions?.testMode) {
state.features = [
...state.features,
...(params.initializationOptions.additionalFeatures ?? []),
]
}

if (!features.includes('css-at-theme')) {
tailwindcss = tailwindcss.default ?? tailwindcss
Expand Down Expand Up @@ -688,6 +701,15 @@ export async function createProjectService(
state.v4 = true
state.v4Fallback = true
state.jit = true
state.features = features

if (params.initializationOptions?.testMode) {
state.features = [
...state.features,
...(params.initializationOptions.additionalFeatures ?? []),
]
}

state.modules = {
tailwindcss: { version: tailwindcssVersion, module: tailwindcss },
postcss: { version: null, module: null },
Expand Down Expand Up @@ -1150,7 +1172,7 @@ export async function createProjectService(
},
tryInit,
async dispose() {
state = { enabled: false }
state = { enabled: false, features: [] }
for (let disposable of disposables) {
;(await disposable).dispose()
}
Expand All @@ -1177,6 +1199,17 @@ export async function createProjectService(
return doHover(state, document, params.position)
}, null)
},
async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
return withFallback(async () => {
if (!state.enabled) return null
let document = documentService.getDocument(params.textDocument.uri)
if (!document) return null
let settings = await state.editor.getConfiguration(document.uri)
if (!settings.tailwindCSS.codeLens) return null
if (await isExcluded(state, document)) return null
return getCodeLens(state, document)
}, null)
},
async onCompletion(params: CompletionParams): Promise<CompletionList> {
return withFallback(async () => {
if (!state.enabled) return null
Expand Down
13 changes: 13 additions & 0 deletions packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
DocumentLink,
InitializeResult,
WorkspaceFolder,
CodeLensParams,
CodeLens,
} from 'vscode-languageserver/node'
import {
CompletionRequest,
Expand All @@ -30,6 +32,7 @@ import {
FileChangeType,
DocumentLinkRequest,
TextDocumentSyncKind,
CodeLensRequest,
} from 'vscode-languageserver/node'
import { URI } from 'vscode-uri'
import normalizePath from 'normalize-path'
Expand Down Expand Up @@ -757,6 +760,7 @@ export class TW {
this.connection.onDocumentColor(this.onDocumentColor.bind(this))
this.connection.onColorPresentation(this.onColorPresentation.bind(this))
this.connection.onCodeAction(this.onCodeAction.bind(this))
this.connection.onCodeLens(this.onCodeLens.bind(this))
this.connection.onDocumentLinks(this.onDocumentLinks.bind(this))
this.connection.onRequest(this.onRequest.bind(this))
}
Expand Down Expand Up @@ -809,6 +813,7 @@ export class TW {
capabilities.add(HoverRequest.type, { documentSelector: null })
capabilities.add(DocumentColorRequest.type, { documentSelector: null })
capabilities.add(CodeActionRequest.type, { documentSelector: null })
capabilities.add(CodeLensRequest.type, { documentSelector: null })
capabilities.add(DocumentLinkRequest.type, { documentSelector: null })

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

async onCodeLens(params: CodeLensParams): Promise<CodeLens[]> {
await this.init()
return this.getProject(params.textDocument)?.onCodeLens(params) ?? null
}

async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
await this.init()
return this.getProject(params.textDocument)?.onDocumentLinks(params) ?? null
Expand Down Expand Up @@ -961,6 +971,9 @@ export class TW {
hoverProvider: true,
colorProvider: true,
codeActionProvider: true,
codeLensProvider: {
resolveProvider: false,
},
documentLinkProvider: {},
completionProvider: {
resolveProvider: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { expect } from 'vitest'
import { css, defineTest } from '../../src/testing'
import { createClient } from '../utils/client'

defineTest({
name: 'Code lenses are displayed for @source inline(…)',
fs: {
'app.css': css`
@import 'tailwindcss';
`,
},
prepare: async ({ root }) => ({
client: await createClient({
root,
features: ['source-inline'],
}),
}),
handle: async ({ client }) => {
let document = await client.open({
lang: 'css',
text: css`
@import 'tailwindcss';
@source inline("{,{hover,focus}:}{flex,underline,bg-red-{50,{100..900.100},950}}");
`,
})

let lenses = await document.codeLenses()

expect(lenses).toEqual([
{
range: {
start: { line: 1, character: 15 },
end: { line: 1, character: 81 },
},
command: {
title: 'Generates 15 classes',
command: '',
},
},
])
},
})

defineTest({
name: 'The user is warned when @source inline(…) generates a lerge amount of CSS',
fs: {
'app.css': css`
@import 'tailwindcss';
`,
},
prepare: async ({ root }) => ({
client: await createClient({
root,
features: ['source-inline'],
}),
}),
handle: async ({ client }) => {
let document = await client.open({
lang: 'css',
text: css`
@import 'tailwindcss';
@source inline("{,dark:}{,{sm,md,lg,xl,2xl}:}{,{hover,focus,active}:}{flex,underline,bg-red-{50,{100..900.100},950}{,/{0..100}}}");
`,
})

let lenses = await document.codeLenses()

expect(lenses).toEqual([
{
range: {
start: { line: 1, character: 15 },
end: { line: 1, character: 129 },
},
command: {
title: 'Generates 14,784 classes',
command: '',
},
},
{
range: {
start: { line: 1, character: 15 },
end: { line: 1, character: 129 },
},
command: {
title: 'At least 3MB of CSS',
command: '',
},
},
{
range: {
start: { line: 1, character: 15 },
end: { line: 1, character: 129 },
},
command: {
title: 'This may slow down your bundler/browser',
command: '',
},
},
])
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,51 @@ withFixture('v4/dependencies', (c) => {
})
})

test.concurrent('@source not', async ({ expect }) => {
let result = await completion({
text: '@source not "',
lang: 'css',
position: {
line: 0,
character: 13,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [
{
label: 'index.html',
kind: 17,
data: expect.anything(),
textEdit: {
newText: 'index.html',
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
},
},
{
label: 'sub-dir/',
kind: 19,
command: { command: 'editor.action.triggerSuggest', title: '' },
data: expect.anything(),
textEdit: {
newText: 'sub-dir/',
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
},
},
{
label: 'tailwind.config.js',
kind: 17,
data: expect.anything(),
textEdit: {
newText: 'tailwind.config.js',
range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } },
},
},
],
})
})

test.concurrent('@source directory', async ({ expect }) => {
let result = await completion({
text: '@source "./sub-dir/',
Expand All @@ -297,6 +342,58 @@ withFixture('v4/dependencies', (c) => {
})
})

test.concurrent('@source not directory', async ({ expect }) => {
let result = await completion({
text: '@source not "./sub-dir/',
lang: 'css',
position: {
line: 0,
character: 23,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [
{
label: 'colors.js',
kind: 17,
data: expect.anything(),
textEdit: {
newText: 'colors.js',
range: { start: { line: 0, character: 23 }, end: { line: 0, character: 23 } },
},
},
],
})
})

test.concurrent('@source inline(…)', async ({ expect }) => {
let result = await completion({
text: '@source inline("',
lang: 'css',
position: {
line: 0,
character: 16,
},
})

expect(result).toEqual(null)
})

test.concurrent('@source not inline(…)', async ({ expect }) => {
let result = await completion({
text: '@source not inline("',
lang: 'css',
position: {
line: 0,
character: 20,
},
})

expect(result).toEqual(null)
})

test.concurrent('@import "…" source(…)', async ({ expect }) => {
let result = await completion({
text: '@import "tailwindcss" source("',
Expand Down
Loading