From a46bfe308945c12a3982b59ed4fb284f445d975a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 7 Mar 2025 11:55:42 -0500 Subject: [PATCH 1/6] =?UTF-8?q?Add=20support=20for=20`@source=20inline(?= =?UTF-8?q?=E2=80=A6)`=20and=20`@source=20not=20inline(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/completions/at-config.test.js | 26 +++++++ .../document-links/document-links.test.js | 12 ++++ .../tests/hover/hover.test.js | 68 +++++++++++++++++++ .../src/completions/file-paths.test.ts | 9 +++ .../src/completions/file-paths.ts | 2 + .../getInvalidSourceDiagnostics.ts | 7 +- .../src/hoverProvider.ts | 1 + 7 files changed, 124 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/tests/completions/at-config.test.js b/packages/tailwindcss-language-server/tests/completions/at-config.test.js index 181fb1367..60ee14952 100644 --- a/packages/tailwindcss-language-server/tests/completions/at-config.test.js +++ b/packages/tailwindcss-language-server/tests/completions/at-config.test.js @@ -368,6 +368,32 @@ withFixture('v4/dependencies', (c) => { }) }) + 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("', diff --git a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js index 5dd8b66ed..0fc76cb6a 100644 --- a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js +++ b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js @@ -157,6 +157,18 @@ withFixture('v4/basic', (c) => { ], }) + testDocumentLinks('@source inline(…)', { + text: '@source inline("m-{1,2,3}");', + lang: 'css', + expected: [], + }) + + testDocumentLinks('@source not inline(…)', { + text: '@source not inline("m-{1,2,3}");', + lang: 'css', + expected: [], + }) + testDocumentLinks('Directories in source(…) show links', { text: ` @import "tailwindcss" source("../../"); diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 1aaa356bb..ac97e414d 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -320,6 +320,74 @@ withFixture('v4/basic', (c) => { }, }) + testHover('css @source inline glob expansion', { + exact: true, + lang: 'css', + text: `@source inline("{hover:,active:,}m-{1,2,3}")`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- hover:m-1', + '- hover:m-2', + '- hover:m-3', + '- active:m-1', + '- active:m-2', + '- active:m-3', + '- m-1', + '- m-2', + '- m-3', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 15 }, + end: { line: 0, character: 43 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 15 }, + }, + }) + + testHover('css @source not inline glob expansion', { + exact: true, + lang: 'css', + text: `@source not inline("{hover:,active:,}m-{1,2,3}")`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- hover:m-1', + '- hover:m-2', + '- hover:m-3', + '- active:m-1', + '- active:m-2', + '- active:m-3', + '- m-1', + '- m-2', + '- m-3', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 19 }, + end: { line: 0, character: 47 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) + testHover('--theme() works inside @media queries', { lang: 'tailwindcss', text: `@media (width>=--theme(--breakpoint-xl)) { .foo { color: red; } }`, diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts index 6b9d8b7d2..ed5213923 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts @@ -61,3 +61,12 @@ test('Detecting v4 directives that point to files', async () => { suggest: 'directory', }) }) + +test('@source inline is ignored', async () => { + function find(text: string) { + return findFileDirective({ enabled: true, v4: true }, text) + } + + await expect(find('@source inline("')).resolves.toEqual(null) + await expect(find('@source not inline("')).resolves.toEqual(null) +}) diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts index ae455d432..acf4f9fa3 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -1,6 +1,8 @@ import type { State } from '../util/state' // @config, @plugin, @source +// - @source inline("…") is *not* a file directive +// - @source not inline("…") is *not* a file directive const PATTERN_CUSTOM_V4 = /@(?config|plugin|source)(?\s+not)?\s*(?'[^']*|"[^"]*)$/ const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index d2d191570..24fdcd9c3 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -14,7 +14,7 @@ const PATTERN_UTIL_SOURCE = // @source … const PATTERN_AT_SOURCE = - /(?:\s|^)@(?source)\s*(?not)?\s*(?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + /(?:\s|^)@(?source)\s*(?not)?\s*(?'[^']*'?|"[^"]*"?|[a-z]*(?:\([^)]+\))?|\)|;)/dg const HAS_DRIVE_LETTER = /^[A-Z]:/ @@ -135,6 +135,11 @@ export function getInvalidSourceDiagnostics( }) } + // `@source inline(…)` is fine + else if (directive === 'source' && source.startsWith('inline(')) { + // + } + // - `@import "tailwindcss" source(no)` // - `@tailwind utilities source('')` else if (directive === 'source' && source !== 'none' && !isQuoted) { diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 39ee35960..1db68bed8 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -170,6 +170,7 @@ async function provideSourceGlobHover( let patterns = [ /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/dg, + /@source(?:\s+not)?\s*inline\((?'[^']+'|"[^"]+")/dg, ] let matches = patterns.flatMap((pattern) => findAll(pattern, text)) From 93027cf819da527b0343c55bb83d7253416a7700 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:17:42 -0400 Subject: [PATCH 2/6] Emit code lenses from the language service --- .../tailwindcss-language-server/src/config.ts | 1 + .../tailwindcss-language-server/src/projects.ts | 15 +++++++++++++++ packages/tailwindcss-language-server/src/tw.ts | 13 +++++++++++++ .../tests/utils/client.ts | 16 ++++++++++++++++ .../src/codeLensProvider.ts | 13 +++++++++++++ .../src/util/state.ts | 1 + packages/vscode-tailwindcss/package.json | 6 ++++++ 7 files changed, 65 insertions(+) create mode 100644 packages/tailwindcss-language-service/src/codeLensProvider.ts diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts index d8364d061..1d183c1e0 100644 --- a/packages/tailwindcss-language-server/src/config.ts +++ b/packages/tailwindcss-language-server/src/config.ts @@ -16,6 +16,7 @@ function getDefaultSettings(): Settings { emmetCompletions: false, classAttributes: ['class', 'className', 'ngClass', 'class:list'], codeActions: true, + codeLens: true, hovers: true, suggestions: true, validate: true, diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index b55ee0781..3c885e291 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -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' @@ -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, @@ -110,6 +113,7 @@ export interface ProjectService { onColorPresentation(params: ColorPresentationParams): Promise onCodeAction(params: CodeActionParams): Promise onDocumentLinks(params: DocumentLinkParams): Promise + onCodeLens(params: CodeLensParams): Promise sortClassLists(classLists: string[]): string[] dependencies(): Iterable @@ -1177,6 +1181,17 @@ export async function createProjectService( return doHover(state, document, params.position) }, null) }, + async onCodeLens(params: CodeLensParams): Promise { + 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 { return withFallback(async () => { if (!state.enabled) return null diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 35da9386e..fc6a87a8f 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -19,6 +19,8 @@ import type { DocumentLink, InitializeResult, WorkspaceFolder, + CodeLensParams, + CodeLens, } from 'vscode-languageserver/node' import { CompletionRequest, @@ -30,6 +32,7 @@ import { FileChangeType, DocumentLinkRequest, TextDocumentSyncKind, + CodeLensRequest, } from 'vscode-languageserver/node' import { URI } from 'vscode-uri' import normalizePath from 'normalize-path' @@ -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)) } @@ -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, { @@ -931,6 +936,11 @@ export class TW { return this.getProject(params.textDocument)?.onCodeAction(params) ?? null } + async onCodeLens(params: CodeLensParams): Promise { + await this.init() + return this.getProject(params.textDocument)?.onCodeLens(params) ?? null + } + async onDocumentLinks(params: DocumentLinkParams): Promise { await this.init() return this.getProject(params.textDocument)?.onDocumentLinks(params) ?? null @@ -961,6 +971,9 @@ export class TW { hoverProvider: true, colorProvider: true, codeActionProvider: true, + codeLensProvider: { + resolveProvider: false, + }, documentLinkProvider: {}, completionProvider: { resolveProvider: true, diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index f7ee6e949..46d65eb07 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -1,6 +1,8 @@ import type { Settings } from '@tailwindcss/language-service/src/util/state' import { ClientCapabilities, + CodeLens, + CodeLensRequest, CompletionList, CompletionParams, Diagnostic, @@ -94,6 +96,11 @@ export interface ClientDocument { */ reopen(): Promise + /** + * Code lenses in the document + */ + codeLenses(): Promise + /** * The diagnostics for the current version of this document */ @@ -677,6 +684,14 @@ export async function createClientWorkspace({ return results } + async function codeLenses() { + return await conn.sendRequest(CodeLensRequest.type, { + textDocument: { + uri: uri.toString(), + }, + }) + } + return { uri, reopen, @@ -687,6 +702,7 @@ export async function createClientWorkspace({ symbols, completions, diagnostics, + codeLenses, } } diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts new file mode 100644 index 000000000..98cf4fb6a --- /dev/null +++ b/packages/tailwindcss-language-service/src/codeLensProvider.ts @@ -0,0 +1,13 @@ +import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { State } from './util/state' +import type { CodeLens } from 'vscode-languageserver' + +export async function getCodeLens(state: State, doc: TextDocument): Promise { + if (!state.enabled) return [] + + let groups: CodeLens[][] = await Promise.all([ + // + ]) + + return groups.flat() +} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 95afe8ec9..3499cab9e 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -48,6 +48,7 @@ export type TailwindCssSettings = { classAttributes: string[] suggestions: boolean hovers: boolean + codeLens: boolean codeActions: boolean validate: boolean showPixelEquivalents: boolean diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 08906fbde..71350b502 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -202,6 +202,12 @@ "markdownDescription": "Enable code actions.", "scope": "language-overridable" }, + "tailwindCSS.codeLens": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable code lens.", + "scope": "language-overridable" + }, "tailwindCSS.colorDecorators": { "type": "boolean", "default": true, From 75d56a0d3c0d9634e5fbf22b75541249bcebaf9a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:21:44 -0400 Subject: [PATCH 3/6] =?UTF-8?q?Show=20estimated=20number=20of=20classes=20?= =?UTF-8?q?generated=20above=20to=20`@source=20inline(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/package.json | 1 + .../tests/code-lens/source-inline.test.ts | 39 ++++++++++++++++++ .../tailwindcss-language-service/package.json | 1 + .../src/codeLensProvider.ts | 40 ++++++++++++++++++- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 16d4f73be..155842ad6 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -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", diff --git a/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts new file mode 100644 index 000000000..8823e77bc --- /dev/null +++ b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts @@ -0,0 +1,39 @@ +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 }), + }), + 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: '', + }, + }, + ]) + }, +}) diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 0cbdde252..61b455e89 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -40,6 +40,7 @@ "vscode-languageserver-textdocument": "1.0.11" }, "devDependencies": { + "@types/braces": "3.0.1", "@types/css.escape": "^1.5.2", "@types/line-column": "^1.0.2", "@types/node": "^18.19.33", diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts index 98cf4fb6a..820a21604 100644 --- a/packages/tailwindcss-language-service/src/codeLensProvider.ts +++ b/packages/tailwindcss-language-service/src/codeLensProvider.ts @@ -1,13 +1,51 @@ -import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { Range, TextDocument } from 'vscode-languageserver-textdocument' import type { State } from './util/state' import type { CodeLens } from 'vscode-languageserver' +import braces from 'braces' +import { findAll, indexToPosition } from './util/find' +import { absoluteRange } from './util/absoluteRange' export async function getCodeLens(state: State, doc: TextDocument): Promise { if (!state.enabled) return [] let groups: CodeLens[][] = await Promise.all([ // + sourceInlineCodeLens(state, doc), ]) return groups.flat() } + +const SOURCE_INLINE_PATTERN = /@source(?:\s+not)?\s*inline\((?'[^']+'|"[^"]+")/dg +async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise { + let text = doc.getText() + + let countFormatter = new Intl.NumberFormat('en', { + maximumFractionDigits: 2, + }) + + let lenses: CodeLens[] = [] + + for (let match of findAll(SOURCE_INLINE_PATTERN, text)) { + let glob = match.groups.glob.slice(1, -1) + + // Perform brace expansion + let expanded = new Set(braces.expand(glob)) + if (expanded.size < 2) continue + + let slice: Range = absoluteRange({ + start: indexToPosition(text, match.indices.groups.glob[0]), + end: indexToPosition(text, match.indices.groups.glob[1]), + }) + + lenses.push({ + range: slice, + command: { + title: `Generates ${countFormatter.format(expanded.size)} classes`, + command: '', + }, + }) + } + + return lenses +} From 0bbd1f6160d040760bb0d17aa5d8a30b316a4792 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:22:35 -0400 Subject: [PATCH 4/6] Show estimated size of generated classes --- .../tests/code-lens/source-inline.test.ts | 56 +++++++++++++++++++ .../src/codeLensProvider.ts | 25 +++++++++ .../src/util/estimated-class-size.ts | 35 ++++++++++++ .../src/util/format-bytes.ts | 11 ++++ 4 files changed, 127 insertions(+) create mode 100644 packages/tailwindcss-language-service/src/util/estimated-class-size.ts create mode 100644 packages/tailwindcss-language-service/src/util/format-bytes.ts diff --git a/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts index 8823e77bc..e169aac05 100644 --- a/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts +++ b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts @@ -37,3 +37,59 @@ defineTest({ ]) }, }) + +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 }), + }), + 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: '', + }, + }, + ]) + }, +}) diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts index 820a21604..178560d0c 100644 --- a/packages/tailwindcss-language-service/src/codeLensProvider.ts +++ b/packages/tailwindcss-language-service/src/codeLensProvider.ts @@ -4,6 +4,8 @@ import type { CodeLens } from 'vscode-languageserver' import braces from 'braces' import { findAll, indexToPosition } from './util/find' import { absoluteRange } from './util/absoluteRange' +import { formatBytes } from './util/format-bytes' +import { estimatedClassSize } from './util/estimated-class-size' export async function getCodeLens(state: State, doc: TextDocument): Promise { if (!state.enabled) return [] @@ -38,6 +40,11 @@ async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise= 1_000_000) { + lenses.push({ + range: slice, + command: { + title: `At least ${formatBytes(size)} of CSS`, + command: '', + }, + }) + + lenses.push({ + range: slice, + command: { + title: `This may slow down your bundler/browser`, + command: '', + }, + }) + } } return lenses diff --git a/packages/tailwindcss-language-service/src/util/estimated-class-size.ts b/packages/tailwindcss-language-service/src/util/estimated-class-size.ts new file mode 100644 index 000000000..57dc4235e --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/estimated-class-size.ts @@ -0,0 +1,35 @@ +import { segment } from './segment' + +/** + * Calculates the approximate size of a generated class + * + * This is meant to be a lower bound, as the actual size of a class can vary + * depending on the actual CSS properties and values, configured theme, etc… + */ +export function estimatedClassSize(className: string) { + let size = 0 + + // We estimate the size using the following structure which gives a reasonable + // lower bound on the size of the generated CSS: + // + // .class-name { + // &:variant-1 { + // &:variant-2 { + // … + // } + // } + // } + + // Class name + size += 1 + className.length + 3 + size += 2 + + // Variants + nesting + for (let [depth, variantName] of segment(className, ':').entries()) { + size += (depth + 1) * 2 + 2 + variantName.length + 3 + size += (depth + 1) * 2 + 2 + } + + // ~1.95x is a rough growth factor due to the actual properties being present + return size * 1.95 +} diff --git a/packages/tailwindcss-language-service/src/util/format-bytes.ts b/packages/tailwindcss-language-service/src/util/format-bytes.ts new file mode 100644 index 000000000..a7b0b050e --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/format-bytes.ts @@ -0,0 +1,11 @@ +const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'] + +export function formatBytes(n: number) { + let i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1000)) + return new Intl.NumberFormat('en', { + notation: 'compact', + style: 'unit', + unit: UNITS[i], + unitDisplay: 'narrow', + }).format(n / 1000 ** i) +} From 3a3688efe41d616b5e3d04524ec2f609e8bd521a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:22:43 -0400 Subject: [PATCH 5/6] Update lockfile --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1048aaaf..4e878e89b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: '@tailwindcss/typography': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.17) + '@types/braces': + specifier: 3.0.1 + version: 3.0.1 '@types/color-name': specifier: ^1.1.3 version: 1.1.4 @@ -297,6 +300,9 @@ importers: specifier: 1.0.11 version: 1.0.11 devDependencies: + '@types/braces': + specifier: 3.0.1 + version: 3.0.1 '@types/css.escape': specifier: ^1.5.2 version: 1.5.2 From 1b26cf387c00e2150ba41f22cf2bca0db29717f5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Mar 2025 09:38:31 -0400 Subject: [PATCH 6/6] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index f59826078..939dd84bb 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -6,6 +6,8 @@ - Cancel initial file search if it takes too long ([#1242](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1242)) - LSP: Don’t throw when the client does not provide `textDocument` in capabilities ([#1252](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1252)) - v4: Allow `*` anywhere in a CSS variable name ([#1256](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1256)) +- v4: Add support for `@source not` ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) +- v4: Add support for `@source inline(…)` ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) # 0.14.8