From ac60dac880439d8318a0d243d70aea35f39ac693 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 7 Mar 2025 11:44:25 -0500 Subject: [PATCH 01/15] Add support for `@source not` --- .../tests/completions/at-config.test.js | 71 +++++++++++++++++++ .../tests/completions/completions.test.js | 2 +- .../document-links/document-links.test.js | 26 +++++++ .../tests/hover/hover.test.js | 27 +++++++ .../src/completionProvider.ts | 11 +++ .../src/completions/file-paths.test.ts | 7 ++ .../src/completions/file-paths.ts | 8 ++- .../getInvalidSourceDiagnostics.ts | 2 +- .../src/documentLinksProvider.ts | 2 +- .../src/hoverProvider.ts | 2 +- 10 files changed, 153 insertions(+), 5 deletions(-) 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 15d99ac6..181fb136 100644 --- a/packages/tailwindcss-language-server/tests/completions/at-config.test.js +++ b/packages/tailwindcss-language-server/tests/completions/at-config.test.js @@ -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/', @@ -297,6 +342,32 @@ 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('@import "…" source(…)', async ({ expect }) => { let result = await completion({ text: '@import "tailwindcss" source("', diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index f5393a40..8d1253e3 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -487,7 +487,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is NOT suggested by default - expect(result.items.length).toBe(7) + expect(result.items.length).toBe(8) expect(result.items).not.toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), 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 861f74c9..5dd8b66e 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 @@ -131,6 +131,32 @@ withFixture('v4/basic', (c) => { ], }) + testDocumentLinks('source not: file exists', { + text: '@source not "index.html";', + lang: 'css', + expected: [ + { + target: `file://${path + .resolve('./tests/fixtures/v4/basic/index.html') + .replace(/@/g, '%40')}`, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 24 } }, + }, + ], + }) + + testDocumentLinks('source not: file does not exist', { + text: '@source not "does-not-exist.html";', + lang: 'css', + expected: [ + { + target: `file://${path + .resolve('./tests/fixtures/v4/basic/does-not-exist.html') + .replace(/@/g, '%40')}`, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 33 } }, + }, + ], + }) + 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 5c8bcb7f..1aaa356b 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -293,6 +293,33 @@ withFixture('v4/basic', (c) => { }, }) + testHover('css @source not glob expansion', { + exact: true, + lang: 'css', + text: `@source not "../{app,components}/**/*.jsx"`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- ../app/**/*.jsx', + '- ../components/**/*.jsx', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 42 }, + }, + }, + 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/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 1579fd93..678ee109 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1866,6 +1866,17 @@ function provideCssDirectiveCompletions( }, }) + items.push({ + label: '@source not', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@source not\` directive to ignore files when scanning.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#source', + )})`, + }, + }) + items.push({ label: '@plugin', documentation: { 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 c79fcbdd..6b9d8b7d 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts @@ -15,6 +15,7 @@ test('Detecting v3 directives that point to files', async () => { // The following are not supported in v3 await expect(find('@plugin "./')).resolves.toEqual(null) await expect(find('@source "./')).resolves.toEqual(null) + await expect(find('@source not "./')).resolves.toEqual(null) await expect(find('@import "tailwindcss" source("./')).resolves.toEqual(null) await expect(find('@tailwind utilities source("./')).resolves.toEqual(null) }) @@ -42,6 +43,12 @@ test('Detecting v4 directives that point to files', async () => { suggest: 'source', }) + await expect(find('@source not "./')).resolves.toEqual({ + directive: 'source', + partial: './', + suggest: 'source', + }) + await expect(find('@import "tailwindcss" source("./')).resolves.toEqual({ directive: 'import', partial: './', diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts index a99325be..ae455d43 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -1,7 +1,8 @@ import type { State } from '../util/state' // @config, @plugin, @source -const PATTERN_CUSTOM_V4 = /@(?config|plugin|source)\s*(?'[^']*|"[^"]*)$/ +const PATTERN_CUSTOM_V4 = + /@(?config|plugin|source)(?\s+not)?\s*(?'[^']*|"[^"]*)$/ const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ // @import … source('…') @@ -26,6 +27,7 @@ export async function findFileDirective(state: State, text: string): Promise 0 let directive = match.groups.directive let partial = match.groups.partial?.slice(1) ?? '' // remove leading quote @@ -40,6 +42,7 @@ export async function findFileDirective(state: State, text: string): Promise 0 + if (isNot) return null + let directive = match.groups.directive let partial = match.groups.partial.slice(1) // remove leading quote diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 2ac52a08..d2d19157 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*(?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + /(?:\s|^)@(?source)\s*(?not)?\s*(?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg const HAS_DRIVE_LETTER = /^[A-Z]:/ diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 76e39ed7..ea2e7bd9 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -18,7 +18,7 @@ export function getDocumentLinks( if (state.v4) { patterns.push( /@plugin\s*(?'[^']+'|"[^"]+")/g, - /@source\s*(?'[^']+'|"[^"]+")/g, + /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/g, /@import\s*('[^']*'|"[^"]*")\s*(layer\([^)]+\)\s*)?source\((?'[^']*'?|"[^"]*"?)/g, /@reference\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, /@tailwind\s*utilities\s*source\((?'[^']*'?|"[^"]*"?)/g, diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index eb5a7d1c..0a110bd7 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -168,7 +168,7 @@ async function provideSourceGlobHover( let text = getTextWithoutComments(document, 'css', range) - let pattern = /@source\s*(?'[^']+'|"[^"]+")/dg + let pattern = /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/dg for (let match of findAll(pattern, text)) { let path = match.groups.path.slice(1, -1) From 53095e4bd6687fbca6a22b4ca897342514ba4e17 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 7 Mar 2025 12:03:39 -0500 Subject: [PATCH 02/15] Refactor --- .../src/hoverProvider.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 0a110bd7..39ee3596 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -168,19 +168,23 @@ async function provideSourceGlobHover( let text = getTextWithoutComments(document, 'css', range) - let pattern = /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/dg + let patterns = [ + /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/dg, + ] - for (let match of findAll(pattern, text)) { - let path = match.groups.path.slice(1, -1) + let matches = patterns.flatMap((pattern) => findAll(pattern, text)) - // Ignore paths that don't need brace expansion - if (!path.includes('{') || !path.includes('}')) continue + for (let match of matches) { + let glob = match.groups.glob.slice(1, -1) + + // Ignore globs that don't need brace expansion + if (!glob.includes('{') || !glob.includes('}')) continue - // Ignore paths that don't contain the current position + // Ignore glob that don't contain the current position let slice: Range = absoluteRange( { - start: indexToPosition(text, match.indices.groups.path[0]), - end: indexToPosition(text, match.indices.groups.path[1]), + start: indexToPosition(text, match.indices.groups.glob[0]), + end: indexToPosition(text, match.indices.groups.glob[1]), }, range, ) @@ -188,8 +192,8 @@ async function provideSourceGlobHover( if (!isWithinRange(position, slice)) continue // Perform brace expansion - let paths = new Set(braces.expand(path)) - if (paths.size < 2) continue + let expanded = new Set(braces.expand(glob)) + if (expanded.size < 2) continue return { range: slice, @@ -197,7 +201,7 @@ async function provideSourceGlobHover( // '**Expansion**', '```plaintext', - ...Array.from(paths, (path) => `- ${path}`), + ...Array.from(expanded, (entry) => `- ${entry}`), '```', ]), } From 7032385397f8f7d99414cc609ae1f90bc6f2d948 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 7 Mar 2025 11:55:42 -0500 Subject: [PATCH 03/15] =?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 181fb136..60ee1495 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 5dd8b66e..0fc76cb6 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 1aaa356b..ac97e414 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 6b9d8b7d..ed521392 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 ae455d43..acf4f9fa 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 d2d19157..24fdcd9c 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 39ee3596..1db68bed 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 a96c96525857e563253f6a5698daa3d0a7ab26f0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:17:42 -0400 Subject: [PATCH 04/15] Emit code lenses from the language service --- .../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 | 2 ++ packages/vscode-tailwindcss/package.json | 6 ++++++ 6 files changed, 65 insertions(+) create mode 100644 packages/tailwindcss-language-service/src/codeLensProvider.ts diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index b55ee078..3c885e29 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 35da9386..fc6a87a8 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 d6d317bb..fcd0387d 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 00000000..98cf4fb6 --- /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 3bdb1bc4..723ffacb 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -49,6 +49,7 @@ export type TailwindCssSettings = { classFunctions: string[] suggestions: boolean hovers: boolean + codeLens: boolean codeActions: boolean validate: boolean showPixelEquivalents: boolean @@ -185,6 +186,7 @@ export function getDefaultTailwindSettings(): Settings { classAttributes: ['class', 'className', 'ngClass', 'class:list'], classFunctions: [], codeActions: true, + codeLens: true, hovers: true, suggestions: true, validate: true, diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 036ca936..876a661b 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -210,6 +210,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 7bd975fe6e628599918c380613e729fd01cb15fc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:21:44 -0400 Subject: [PATCH 05/15] =?UTF-8?q?Show=20estimated=20number=20of=20classes?= =?UTF-8?q?=20generated=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 5ed431fb..fa0a7e91 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 00000000..8823e77b --- /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 d5fddaaa..e73f6ca3 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/dedent": "^0.7.2", "@types/line-column": "^1.0.2", diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts index 98cf4fb6..820a2160 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 1f84960d832be74be045d2a6ea303c4c91e08ff7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:22:35 -0400 Subject: [PATCH 06/15] 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 8823e77b..e169aac0 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 820a2160..178560d0 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 00000000..57dc4235 --- /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 00000000..a7b0b050 --- /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 ca626d4c7de6a7d051a0bc35105fed89211d4c22 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 18 Mar 2025 13:22:43 -0400 Subject: [PATCH 07/15] Update lockfile --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2378a376..67d58759 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 33a5320e45ecb375290d7d8b967a68977feda824 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Mar 2025 09:38:31 -0400 Subject: [PATCH 08/15] 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 7b9e68fa..d961fd78 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,6 +4,8 @@ - Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) - v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) +- 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.9 From 4377591d5e8966be3cec398be7592e63a22f9af8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:12:08 -0400 Subject: [PATCH 09/15] Refactor --- .../tailwindcss-language-service/src/features.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-service/src/features.ts b/packages/tailwindcss-language-service/src/features.ts index 60181bf7..cf60ebef 100644 --- a/packages/tailwindcss-language-service/src/features.ts +++ b/packages/tailwindcss-language-service/src/features.ts @@ -42,12 +42,14 @@ export function supportedFeatures(version: string, mod?: unknown): Feature[] { return ['css-at-theme', 'layer:base', 'content-list'] } - if (!isInsidersV3 && semver.gte(version, '4.0.0-alpha.1')) { - return ['css-at-theme', 'layer:base', 'content-list'] - } + if (!isInsidersV3) { + if (semver.gte(version, '4.0.0-alpha.1')) { + return ['css-at-theme', 'layer:base', 'content-list'] + } - if (!isInsidersV3 && version.startsWith('0.0.0-oxide')) { - return ['css-at-theme', 'layer:base', 'content-list'] + if (version.startsWith('0.0.0-oxide')) { + return ['css-at-theme', 'layer:base', 'content-list'] + } } if (semver.gte(version, '0.99.0')) { From df7958d52eed90cc259191e9054f57c99c700ff9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:12:31 -0400 Subject: [PATCH 10/15] Add `features` to state --- packages/tailwindcss-language-server/src/projects.ts | 5 ++++- packages/tailwindcss-language-service/src/util/state.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 3c885e29..8f6c8ec4 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -216,6 +216,7 @@ export async function createProjectService( let state: State = { enabled: false, + features: [], completionItemData: { _projectKey: projectKey, }, @@ -466,6 +467,7 @@ 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 (!features.includes('css-at-theme')) { tailwindcss = tailwindcss.default ?? tailwindcss @@ -692,6 +694,7 @@ export async function createProjectService( state.v4 = true state.v4Fallback = true state.jit = true + state.features = features state.modules = { tailwindcss: { version: tailwindcssVersion, module: tailwindcss }, postcss: { version: null, module: null }, @@ -1154,7 +1157,7 @@ export async function createProjectService( }, tryInit, async dispose() { - state = { enabled: false } + state = { enabled: false, features: [] } for (let disposable of disposables) { ;(await disposable).dispose() } diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 723ffacb..119e5f59 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -4,6 +4,7 @@ import type { Postcss } from 'postcss' import type { KeywordColor } from './color' import type * as culori from 'culori' import type { DesignSystem } from './v4' +import type { Feature } from '../features' export type ClassNamesTree = { [key: string]: ClassNamesTree @@ -142,6 +143,7 @@ export interface State { classListContainsMetadata?: boolean pluginVersions?: string completionItemData?: Record + features: Feature[] // postcssPlugins?: { before: any[]; after: any[] } } @@ -223,6 +225,7 @@ export function createState( ): State { return { enabled: true, + features: [], ...partial, editor: { get connection(): Connection { From 3d57bc6bc53a74982407fb4afbbd513484d3f6c6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:15:06 -0400 Subject: [PATCH 11/15] Only show `@source not` suggestion for newer Tailwind CSS versions --- .../src/completionProvider.ts | 22 ++++++++++--------- .../src/features.ts | 8 ++++++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 678ee109..3cdd511a 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1866,16 +1866,18 @@ function provideCssDirectiveCompletions( }, }) - items.push({ - label: '@source not', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@source not\` directive to ignore files when scanning.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#source', - )})`, - }, - }) + if (state.features.includes('source-not')) { + items.push({ + label: '@source not', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@source not\` directive to ignore files when scanning.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#source', + )})`, + }, + }) + } items.push({ label: '@plugin', diff --git a/packages/tailwindcss-language-service/src/features.ts b/packages/tailwindcss-language-service/src/features.ts index cf60ebef..6aa9236e 100644 --- a/packages/tailwindcss-language-service/src/features.ts +++ b/packages/tailwindcss-language-service/src/features.ts @@ -15,6 +15,8 @@ export type Feature = | 'jit' | 'separator:root' | 'separator:options' + | 'source-not' + | 'source-inline' /** * Determine a list of features that are supported by the given Tailwind CSS version @@ -39,10 +41,14 @@ export function supportedFeatures(version: string, mod?: unknown): Feature[] { } if (isInsidersV4) { - return ['css-at-theme', 'layer:base', 'content-list'] + return ['css-at-theme', 'layer:base', 'content-list', 'source-inline', 'source-not'] } if (!isInsidersV3) { + if (semver.gte(version, '4.1.0')) { + return ['css-at-theme', 'layer:base', 'content-list', 'source-inline', 'source-not'] + } + if (semver.gte(version, '4.0.0-alpha.1')) { return ['css-at-theme', 'layer:base', 'content-list'] } From c68e08b746db7bde271b0e822d273979c29005d6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:15:50 -0400 Subject: [PATCH 12/15] Update test --- .../tests/completions/completions.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 8d1253e3..f5393a40 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -487,7 +487,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is NOT suggested by default - expect(result.items.length).toBe(8) + expect(result.items.length).toBe(7) expect(result.items).not.toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), From 58d3b613d889bf33d92272c79db6cabc40c3cdc4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:23:17 -0400 Subject: [PATCH 13/15] Allow test harness to force-enable additional features --- .../tailwindcss-language-server/src/projects.ts | 15 +++++++++++++++ .../tests/utils/client.ts | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 8f6c8ec4..afc4515f 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -469,6 +469,13 @@ export async function createProjectService( 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 } @@ -695,6 +702,14 @@ export async function createProjectService( 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 }, diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index fcd0387d..3a860230 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -47,6 +47,7 @@ import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/ import { DefaultMap } from '../../src/util/default-map' import { connect, ConnectOptions } from './connection' import type { DeepPartial } from '@tailwindcss/language-service/src/types' +import type { Feature } from '@tailwindcss/language-service/src/features' export interface DocumentDescriptor { /** @@ -170,6 +171,14 @@ export interface ClientOptions extends ConnectOptions { * Settings to provide the server immediately when it starts */ settings?: DeepPartial + + /** + * Additional features to force-enable + * + * These should normally be enabled by the server based on the project + * and the Tailwind CSS version it detects + */ + features?: Feature[] } export interface Client extends ClientWorkspace { @@ -394,6 +403,7 @@ export async function createClient(opts: ClientOptions): Promise { workspaceFolders, initializationOptions: { testMode: true, + additionalFeatures: opts.features, ...opts.options, }, }) From 5a021ad6e5a2b8dbca01e2e35c571654ec3851f0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:24:35 -0400 Subject: [PATCH 14/15] Only enable `@source inline` code lenses on Tailwind CSS v4.1 --- .../tests/code-lens/source-inline.test.ts | 10 ++++++++-- .../src/codeLensProvider.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) 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 e169aac0..55f6f22e 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 @@ -10,7 +10,10 @@ defineTest({ `, }, prepare: async ({ root }) => ({ - client: await createClient({ root }), + client: await createClient({ + root, + features: ['source-inline'], + }), }), handle: async ({ client }) => { let document = await client.open({ @@ -46,7 +49,10 @@ defineTest({ `, }, prepare: async ({ root }) => ({ - client: await createClient({ root }), + client: await createClient({ + root, + features: ['source-inline'], + }), }), handle: async ({ client }) => { let document = await client.open({ diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts index 178560d0..b00df983 100644 --- a/packages/tailwindcss-language-service/src/codeLensProvider.ts +++ b/packages/tailwindcss-language-service/src/codeLensProvider.ts @@ -20,6 +20,8 @@ export async function getCodeLens(state: State, doc: TextDocument): Promise'[^']+'|"[^"]+")/dg async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise { + if (!state.features.includes('source-inline')) return [] + let text = doc.getText() let countFormatter = new Intl.NumberFormat('en', { From 8b18f8fab2f546c2bf8ac72afad1b7a9e41bc579 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:24:40 -0400 Subject: [PATCH 15/15] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index d961fd78..faf20065 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,8 +4,8 @@ - Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) - v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) -- 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)) +- v4: Add support for upcoming `@source not` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) +- v4: Add support for upcoming `@source inline(…)` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) # 0.14.9