From 2e35be27fec9af28b0cb8446a610cbf16ec2186e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 11:45:19 -0500 Subject: [PATCH 01/17] Add `pug` to html-like language list --- packages/tailwindcss-language-service/src/util/languages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tailwindcss-language-service/src/util/languages.ts b/packages/tailwindcss-language-service/src/util/languages.ts index 29cfb48f..15be45b9 100644 --- a/packages/tailwindcss-language-service/src/util/languages.ts +++ b/packages/tailwindcss-language-service/src/util/languages.ts @@ -23,6 +23,7 @@ export const htmlLanguages: string[] = [ 'html-eex', 'htmldjango', 'jade', + 'pug', 'latte', 'leaf', 'liquid', From 94ba3b909e6351b0f63b062e100327ad8434f401 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:08:13 -0500 Subject: [PATCH 02/17] Tweak invalid apply diagnostic We replace `findClassNamesInRange` with a two step process we use elsewhere: 1. find all class lists in the document; 2. find classes in each class list --- .../src/diagnostics/getInvalidApplyDiagnostics.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts index ca9a2d82..7768c150 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts @@ -1,8 +1,9 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' -import { findClassNamesInRange } from '../util/find' +import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' import { type InvalidApplyDiagnostic, DiagnosticKind } from './types' import type { Settings, State } from '../util/state' import { validateApply } from '../util/validateApply' +import { isCssDoc } from '../util/css' export async function getInvalidApplyDiagnostics( state: State, @@ -11,8 +12,12 @@ export async function getInvalidApplyDiagnostics( ): Promise { let severity = settings.tailwindCSS.lint.invalidApply if (severity === 'ignore') return [] + if (!isCssDoc(state, document)) return [] - const classNames = await findClassNamesInRange(state, document, undefined, 'css', false) + const classLists = await findClassListsInDocument(state, document) + const classNames = classLists.flatMap((classList) => + getClassNamesInClassList(classList, state.blocklist), + ) let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => { let result = validateApply(state, className.className) From 9dd5f63e3e401e71181691e7714dcc078facd220 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 12:49:31 -0500 Subject: [PATCH 03/17] Tweak helper function finding in hover provider --- packages/tailwindcss-language-service/src/hoverProvider.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 1b42e0e0..9cbb0452 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -6,7 +6,7 @@ import { isCssContext } from './util/css' import { findAll, findClassNameAtPosition, - findHelperFunctionsInRange, + findHelperFunctionsInDocument, indexToPosition, } from './util/find' import { validateApply } from './util/validateApply' @@ -46,10 +46,7 @@ async function provideCssHelperHover( const settings = await state.editor.getConfiguration(document.uri) - let helperFns = findHelperFunctionsInRange(document, { - start: { line: position.line, character: 0 }, - end: { line: position.line + 1, character: 0 }, - }) + let helperFns = findHelperFunctionsInDocument(state, document) for (let helperFn of helperFns) { if (!isWithinRange(position, helperFn.ranges.path)) continue From 4c4667ba1c6779225cb52786f26ec7940b7e4c23 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 00:15:27 -0500 Subject: [PATCH 04/17] Sort class list matches --- .../tailwindcss-language-service/src/util/find.test.ts | 8 ++++---- packages/tailwindcss-language-service/src/util/find.ts | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 77414250..8ee896cc 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -1114,15 +1114,15 @@ test('class functions work inside astro code fences', async ({ expect }) => { { classList: 'relative flex bg-red-500', range: { - start: { line: 3, character: 12 }, - end: { line: 3, character: 36 }, + start: { line: 1, character: 14 }, + end: { line: 1, character: 38 }, }, }, { classList: 'relative flex bg-red-500', range: { - start: { line: 1, character: 14 }, - end: { line: 1, character: 38 }, + start: { line: 3, character: 12 }, + end: { line: 3, character: 36 }, }, }, ]) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index ab1df1f6..2355a8d6 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -225,6 +225,8 @@ export async function findClassListsInHtmlRange( const existingResultSet = new Set() const results: DocumentClassList[] = [] + matches.sort((a, b) => a.index - b.index) + matches.forEach((match) => { const subtext = text.substr(match.index + match[0].length - 1) From bd5538a32edd792fe6dee66f66f41afe4c0cba3b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 11:24:21 -0500 Subject: [PATCH 05/17] Test document-level class finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of dropping down to the individual “range” helpers we should test things at the document level since everything should go through those --- .../src/util/find.test.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 8ee896cc..2d9019f4 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest' import { - findClassListsInHtmlRange, + findClassListsInDocument, findClassNameAtPosition, findHelperFunctionsInDocument, } from './find' @@ -34,7 +34,7 @@ test('class regex works in astro', async ({ expect }) => { ], }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -104,8 +104,8 @@ test('find class lists in functions', async ({ expect }) => { `, }) - let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') - let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) + let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) expect(classListsA).toEqual([ // from clsx(…) @@ -214,7 +214,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toMatchObject([ { @@ -316,7 +316,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toMatchObject([ { @@ -379,8 +379,8 @@ test('find class lists in tagged template literals', async ({ expect }) => { `, }) - let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') - let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) + let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) expect(classListsA).toEqual([ // from clsx`…` @@ -461,8 +461,8 @@ test('classFunctions can be a regex', async ({ expect }) => { `, }) - let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') - let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) + let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) expect(classListsA).toEqual([ { @@ -494,7 +494,7 @@ test('classFunctions regexes only match on function names', async ({ expect }) = `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([]) }) @@ -517,7 +517,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => { `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -580,7 +580,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -659,7 +659,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -719,7 +719,7 @@ test('classAttributes find class lists inside variables in JS(X)/TS(X)', async ( `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -760,7 +760,7 @@ test('classAttributes find class lists inside pug', async ({ expect }) => { `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -789,7 +789,7 @@ test('classAttributes find class lists inside Vue bindings', async ({ expect }) `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -1108,7 +1108,7 @@ test('class functions work inside astro code fences', async ({ expect }) => { `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { @@ -1144,7 +1144,7 @@ test('classFunctions are detected inside of arrays in javascript just after open `, }) - let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + let classLists = await findClassListsInDocument(file.state, file.doc) expect(classLists).toEqual([ { From bceea2e9b048916cac5e0ee144234cb8cb6b4e18 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:17:07 -0500 Subject: [PATCH 06/17] Verify class functions arent matched inside comments This was already the case because the class attribute matching was slicing on text with comments already removed. --- .../src/util/find.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 2d9019f4..c8f5de0e 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -104,8 +104,34 @@ test('find class lists in functions', async ({ expect }) => { `, }) + let fileC = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + content: js` + // These should not match (commented) + // let classes = clsx( + // 'flex p-4', + // 'block sm:p-0', + // Date.now() > 100 ? 'text-white' : 'text-black', + // ) + + // These should not match (commented) + // let classes = cva( + // 'flex p-4', + // 'block sm:p-0', + // Date.now() > 100 ? 'text-white' : 'text-black', + // ) + `, + }) + let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) + let classListsC = await findClassListsInDocument(fileC.state, fileC.doc) expect(classListsA).toEqual([ // from clsx(…) @@ -171,6 +197,9 @@ test('find class lists in functions', async ({ expect }) => { // none from cn(…) since it's not in the list of class functions expect(classListsB).toEqual([]) + + // none from commented code + expect(classListsC).toEqual([]) }) test('find class lists in nested fn calls', async ({ expect }) => { From 3c3b2e59418286fbcf84f34bc40df3197e8e9ecd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:10:44 -0500 Subject: [PATCH 07/17] Refactor `findClassListsInDocument` This simplifies some of the code making it more linearly scannable --- .../src/util/find.ts | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 2355a8d6..0b2a2151 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -347,24 +347,46 @@ export async function findClassListsInDocument( return findClassListsInCssRange(state, doc) } + let settings = await state.editor.getConfiguration(doc.uri) + + let classLists: DocumentClassList[] = [] + let boundaries = getLanguageBoundaries(state, doc) if (!boundaries) return [] - return dedupeByRange( - flatten([ - ...(await Promise.all( - boundaries - .filter((b) => b.type === 'html' || b.type === 'jsx') - .map(({ type, range }) => - findClassListsInHtmlRange(state, doc, type === 'html' ? 'html' : 'jsx', range), - ), - )), - ...boundaries - .filter((b) => b.type === 'css') - .map(({ range, lang }) => findClassListsInCssRange(state, doc, range, lang)), - await findCustomClassLists(state, doc), - ]), - ) + for (let b of boundaries) { + if (b.type === 'html') { + classLists.push( + ...(await findClassListsInHtmlRange( + state, + doc, + b.type === 'html' ? 'html' : 'jsx', + b.range, + )), + ) + } + + if (b.type === 'js' || b.type === 'jsx') { + classLists.push( + ...(await findClassListsInHtmlRange( + state, + doc, + b.type === 'html' ? 'html' : 'jsx', + b.range, + )), + ) + } + + if (b.type === 'css') { + classLists.push(...findClassListsInCssRange(state, doc, b.range, b.lang)) + } + } + + classLists.push(...(await findCustomClassLists(state, doc))) + + classLists = dedupeByRange(classLists) + + return classLists } export function findHelperFunctionsInDocument( From 873902adc282b52950799061422c26f424726d2c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:33:33 -0500 Subject: [PATCH 08/17] Use document level class lists in `findClassNameAtPosition` This guarantees class hovers work identically to things like color decorators that already use whole document class lists --- .../src/util/find.ts | 59 +++---------------- 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 0b2a2151..0a25ad5e 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -2,10 +2,8 @@ import { type Range, type Position, LRUCache } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { DocumentClassName, DocumentClassList, State, DocumentHelperFunction } from './state' import lineColumn from 'line-column' -import { isCssContext, isCssDoc } from './css' -import { isHtmlContext, isVueDoc } from './html' +import { isCssDoc } from './css' import { isWithinRange } from './isWithinRange' -import { isJsContext } from './js' import { dedupeByRange, flatten } from './array' import { getClassAttributeLexer, getComputedClassAttributeLexer } from './lexers' import { getLanguageBoundaries } from './getLanguageBoundaries' @@ -13,7 +11,6 @@ import { absoluteRange } from './absoluteRange' import { getTextWithoutComments } from './doc' import { isSemicolonlessCssLanguage } from './languages' import { customClassesIn } from './classes' -import { SEARCH_RANGE } from './constants' export function findAll(re: RegExp, str: string): RegExpMatchArray[] { let match: RegExpMatchArray @@ -637,54 +634,12 @@ export async function findClassNameAtPosition( doc: TextDocument, position: Position, ): Promise { - let classNames: DocumentClassName[] = [] - const positionOffset = doc.offsetAt(position) - const searchRange: Range = { - start: doc.positionAt(0), - end: doc.positionAt(positionOffset + SEARCH_RANGE), - } - - if (isVueDoc(doc)) { - let boundaries = getLanguageBoundaries(state, doc) - - let groups = await Promise.all( - boundaries.map(async ({ type, range, lang }) => { - if (type === 'css') { - return await findClassListsInRange(state, doc, range, 'css', true, lang) - } - - if (type === 'html') { - return await findClassListsInRange(state, doc, range, 'html') - } - - if (type === 'js' || type === 'jsx') { - return await findClassListsInRange(state, doc, range, 'jsx') - } - - return [] - }), - ) - - classNames = dedupeByRange(flatten(groups)).flatMap((classList) => - getClassNamesInClassList(classList, state.blocklist), - ) - } else if (isCssContext(state, doc, position)) { - classNames = await findClassNamesInRange(state, doc, searchRange, 'css') - } else if (isHtmlContext(state, doc, position)) { - classNames = await findClassNamesInRange(state, doc, searchRange, 'html') - } else if (isJsContext(state, doc, position)) { - classNames = await findClassNamesInRange(state, doc, searchRange, 'jsx') - } else { - classNames = await findClassNamesInRange(state, doc, searchRange) - } - - if (classNames.length === 0) { - return null - } - - const className = classNames.find(({ range }) => isWithinRange(position, range)) + let classLists = await findClassListsInDocument(state, doc) + let classNames = classLists.flatMap((classList) => + getClassNamesInClassList(classList, state.blocklist), + ) - if (!className) return null + let className = classNames.find((className) => isWithinRange(position, className.range)) - return className + return className ?? null } From 1b7d7eb85a1687b15394fdf927a9ff72fc635fc3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:21:21 -0500 Subject: [PATCH 09/17] Simplify `findClassListsInHtmlRange` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document boundaries are handled at a level above this now so we can guarantee that this will only be called for individual document parts — including JSX. Because of this we can simplify how we do class function matching. --- .../src/util/find.ts | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 0a25ad5e..89ad5f4e 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -196,27 +196,15 @@ export async function findClassListsInHtmlRange( ): Promise { if (!state.editor) return [] - const text = getTextWithoutComments(doc, type, range) - - const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS - const matches = matchClassAttributes(text, settings.classAttributes) - - let boundaries = getLanguageBoundaries(state, doc) - - for (let boundary of boundaries ?? []) { - let isJsContext = boundary.type === 'js' || boundary.type === 'jsx' - if (!isJsContext) continue - if (!settings.classFunctions?.length) continue - - let str = doc.getText(boundary.range) - let offset = doc.offsetAt(boundary.range.start) - let fnMatches = matchClassFunctions(str, settings.classFunctions) - - fnMatches.forEach((match) => { - if (match.index) match.index += offset - }) - - matches.push(...fnMatches) + let settings = await state.editor.getConfiguration(doc.uri) + let text = getTextWithoutComments(doc, type, range) + let matches = matchClassAttributes(text, settings.tailwindCSS.classAttributes) + + // For JS/TS contexts we want to look inside specific function calls and + // tagged template literals for class lists + let fnNames = settings.tailwindCSS.classFunctions ?? [] + if (type === 'jsx' && fnNames.length) { + matches.push(...matchClassFunctions(text, fnNames)) } const existingResultSet = new Set() From 31faa1953189a8fd262350e97649bc7e106fb670 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 12:43:16 -0500 Subject: [PATCH 10/17] Make `findClassListsInHtmlRange` sync --- .../src/util/find.ts | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 89ad5f4e..74ff2839 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -188,22 +188,19 @@ export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatc return matches } -export async function findClassListsInHtmlRange( - state: State, +function findClassListsInHtmlRange( doc: TextDocument, - type: 'html' | 'js' | 'jsx', - range?: Range, -): Promise { - if (!state.editor) return [] - - let settings = await state.editor.getConfiguration(doc.uri) + type: 'html' | 'js', + range: Range, + settings: Settings, +): DocumentClassList[] { let text = getTextWithoutComments(doc, type, range) let matches = matchClassAttributes(text, settings.tailwindCSS.classAttributes) // For JS/TS contexts we want to look inside specific function calls and // tagged template literals for class lists let fnNames = settings.tailwindCSS.classFunctions ?? [] - if (type === 'jsx' && fnNames.length) { + if (type === 'js' && fnNames.length) { matches.push(...matchClassFunctions(text, fnNames)) } @@ -341,25 +338,11 @@ export async function findClassListsInDocument( for (let b of boundaries) { if (b.type === 'html') { - classLists.push( - ...(await findClassListsInHtmlRange( - state, - doc, - b.type === 'html' ? 'html' : 'jsx', - b.range, - )), - ) + classLists.push(...findClassListsInHtmlRange(doc, 'html', b.range, settings)) } if (b.type === 'js' || b.type === 'jsx') { - classLists.push( - ...(await findClassListsInHtmlRange( - state, - doc, - b.type === 'html' ? 'html' : 'jsx', - b.range, - )), - ) + classLists.push(...findClassListsInHtmlRange(doc, 'js', b.range, settings)) } if (b.type === 'css') { From dbf24ee85a406482bf966ce7b34cebe4c322e21f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:35:17 -0500 Subject: [PATCH 11/17] Make `findCustomClassLists` sync --- .../src/util/find.ts | 28 ++++++++++--------- .../src/util/state.ts | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 74ff2839..5b99fa9a 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -1,6 +1,12 @@ import { type Range, type Position, LRUCache } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver-textdocument' -import type { DocumentClassName, DocumentClassList, State, DocumentHelperFunction } from './state' +import type { + DocumentClassName, + DocumentClassList, + State, + DocumentHelperFunction, + Settings, +} from './state' import lineColumn from 'line-column' import { isCssDoc } from './css' import { isWithinRange } from './isWithinRange' @@ -124,17 +130,14 @@ export function findClassListsInCssRange( }) } -async function findCustomClassLists( - state: State, - doc: TextDocument, - range?: Range, -): Promise { - const settings = await state.editor.getConfiguration(doc.uri) - const regexes = settings.tailwindCSS.experimental.classRegex - if (!Array.isArray(regexes) || regexes.length === 0) return [] +function findCustomClassLists(doc: TextDocument, settings: Settings): DocumentClassList[] { + let regexes = settings.tailwindCSS.experimental.classRegex + if (!Array.isArray(regexes) || regexes.length === 0) { + return [] + } - const text = doc.getText(range ? { ...range, start: doc.positionAt(0) } : undefined) - const result: DocumentClassList[] = [] + let text = doc.getText() + let result: DocumentClassList[] = [] try { for (let match of customClassesIn({ text, filters: regexes })) { @@ -350,8 +353,7 @@ export async function findClassListsInDocument( } } - classLists.push(...(await findCustomClassLists(state, doc))) - + classLists.push(...findCustomClassLists(doc, settings)) classLists = dedupeByRange(classLists) return classLists diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 10fe81a7..75e940a0 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -69,7 +69,7 @@ export type TailwindCssSettings = { suggestCanonicalClasses: DiagnosticSeveritySetting } experimental: { - classRegex: string[] | [string, string][] + classRegex: (string | [string] | [string, string])[] configFile: string | Record | null } files: { From c73c50dec1d44918d1ac07ce0d42ce1f1ddec84f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 18:32:11 -0500 Subject: [PATCH 12/17] Only split class lists by ASCII whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the code a bit more linear. Using `.split(…)` is still optimal for performance in V8 and JSC when splitting larger class lists. This also refactors the code a bit for clarity. --- .../src/util/find.ts | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 5b99fa9a..4b57c9b5 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -35,42 +35,60 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray { return matches[matches.length - 1] } +const BY_ASCII_WHITESPACE = /([\t\n\f\r ]+)/ + +/** + * Extract class names from a list separated by whitespace + * + * The HTML spec separates classes by ASCII whitespace: + * - U+0009 TAB + * - U+000A LF + * - U+000C FF + * - U+000D CR + * - U+0020 SPACE + * + * The CSS spec also effectively uses the above definition for whitespace. + * + * @see {@link https://dom.spec.whatwg.org/#concept-getelementsbyclassname} + * @see {@link https://dom.spec.whatwg.org/#concept-ordered-set-parser} + * @see {@link https://infra.spec.whatwg.org/#ascii-whitespace} + * @see {@link https://www.w3.org/TR/css-syntax-3/#whitespace} + */ export function getClassNamesInClassList( - { classList, range, important }: DocumentClassList, + classList: DocumentClassList, blocklist: State['blocklist'], ): DocumentClassName[] { - const parts = classList.split(/(\s+)/) - const names: DocumentClassName[] = [] + let input = classList.classList + let parts = input.split(BY_ASCII_WHITESPACE) + let names: DocumentClassName[] = [] let index = 0 + for (let i = 0; i < parts.length; i++) { - if (i % 2 === 0 && !blocklist.includes(parts[i])) { - const start = indexToPosition(classList, index) - const end = indexToPosition(classList, index + parts[i].length) - names.push({ - className: parts[i], - classList: { - classList, - range, - important, - }, - relativeRange: { - start, - end, - }, - range: { - start: { - line: range.start.line + start.line, - character: (end.line === 0 ? range.start.character : 0) + start.character, - }, - end: { - line: range.start.line + end.line, - character: (end.line === 0 ? range.start.character : 0) + end.character, - }, - }, - }) + let isWhitespace = i % 2 === 1 + let className = parts[i] + + let start = index + let end = start + className.length + index += className.length + + if (isWhitespace) continue + if (blocklist.includes(className)) continue + + let relativeRange = { + start: indexToPosition(input, start), + end: indexToPosition(input, end), } - index += parts[i].length + + let range = absoluteRange(relativeRange, classList.range) + + names.push({ + className, + classList, + relativeRange, + range, + }) } + return names } From 1147fa33d399dc17bc81c1c486ad8d01f8386133 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 14:11:40 -0500 Subject: [PATCH 13/17] Cleanup --- .../src/util/doc.ts | 8 +- .../src/util/find.ts | 73 ++-------------- .../src/util/language-blocks.ts | 86 ++++++++++++------- .../src/util/test-utils.ts | 60 +++++++------ 4 files changed, 100 insertions(+), 127 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/doc.ts b/packages/tailwindcss-language-service/src/util/doc.ts index 7c73f9d3..8cf0dd66 100644 --- a/packages/tailwindcss-language-service/src/util/doc.ts +++ b/packages/tailwindcss-language-service/src/util/doc.ts @@ -5,19 +5,19 @@ import { spliceChangesIntoString, StringChange } from './splice-changes-into-str export function getTextWithoutComments( doc: TextDocument, - type: 'html' | 'js' | 'jsx' | 'css', + type: 'html' | 'js' | 'css', range?: Range, ): string -export function getTextWithoutComments(text: string, type: 'html' | 'js' | 'jsx' | 'css'): string +export function getTextWithoutComments(text: string, type: 'html' | 'js' | 'css'): string export function getTextWithoutComments( docOrText: TextDocument | string, - type: 'html' | 'js' | 'jsx' | 'css', + type: 'html' | 'js' | 'css', range?: Range, ): string { let text = typeof docOrText === 'string' ? docOrText : docOrText.getText(range) - if (type === 'js' || type === 'jsx') { + if (type === 'js') { return getJsWithoutComments(text) } diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 4b57c9b5..62fb4e0e 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -92,30 +92,7 @@ export function getClassNamesInClassList( return names } -export async function findClassNamesInRange( - state: State, - doc: TextDocument, - range?: Range, - mode?: 'html' | 'css' | 'jsx', - includeCustom: boolean = true, -): Promise { - const classLists = await findClassListsInRange(state, doc, range, mode, includeCustom) - return flatten( - classLists.map((classList) => getClassNamesInClassList(classList, state.blocklist)), - ) -} - -export async function findClassNamesInDocument( - state: State, - doc: TextDocument, -): Promise { - const classLists = await findClassListsInDocument(state, doc) - return flatten( - classLists.map((classList) => getClassNamesInClassList(classList, state.blocklist)), - ) -} - -export function findClassListsInCssRange( +function findClassListsInCssRange( state: State, doc: TextDocument, range?: Range, @@ -240,7 +217,6 @@ function findClassListsInHtmlRange( lexer.reset(subtext) let classLists: { value: string; offset: number }[] = [] - let token: moo.Token let currentClassList: { value: string; offset: number } try { @@ -322,26 +298,6 @@ function findClassListsInHtmlRange( return results } -export async function findClassListsInRange( - state: State, - doc: TextDocument, - range?: Range, - mode?: 'html' | 'css' | 'jsx', - includeCustom: boolean = true, - lang?: string, -): Promise { - let classLists: DocumentClassList[] = [] - if (mode === 'css') { - classLists = findClassListsInCssRange(state, doc, range, lang) - } else if (mode === 'html' || mode === 'jsx') { - classLists = await findClassListsInHtmlRange(state, doc, mode, range) - } - return dedupeByRange([ - ...classLists, - ...(includeCustom ? await findCustomClassLists(state, doc, range) : []), - ]) -} - export async function findClassListsInDocument( state: State, doc: TextDocument, @@ -395,26 +351,7 @@ export function findHelperFunctionsInDocument( ) } -function getFirstCommaIndex(str: string): number | null { - let quoteChar: string | undefined - for (let i = 0; i < str.length; i++) { - let char = str[i] - if (char === ',' && !quoteChar) { - return i - } - if (!quoteChar && (char === '"' || char === "'")) { - quoteChar = char - } else if (char === quoteChar) { - quoteChar = undefined - } - } - return null -} - -export function findHelperFunctionsInRange( - doc: TextDocument, - range?: Range, -): DocumentHelperFunction[] { +function findHelperFunctionsInRange(doc: TextDocument, range?: Range): DocumentHelperFunction[] { let text = getTextWithoutComments(doc, 'css', range) // Find every instance of a helper function @@ -456,9 +393,9 @@ export function findHelperFunctionsInRange( for (let match of matches) { let argsStart = match.index + match[0].length - let argsEnd = null + let argsEnd: number | null = null let pathStart = argsStart - let pathEnd = null + let pathEnd: number | null = null let depth = 1 // Scan until we find a `,` or balanced `)` not in quotes @@ -503,7 +440,7 @@ export function findHelperFunctionsInRange( } } - if (argsEnd === null) continue + if (argsEnd === null || pathEnd === null) continue let helper: 'config' | 'theme' | 'var' diff --git a/packages/tailwindcss-language-service/src/util/language-blocks.ts b/packages/tailwindcss-language-service/src/util/language-blocks.ts index 5fbc038c..1f5ed97d 100644 --- a/packages/tailwindcss-language-service/src/util/language-blocks.ts +++ b/packages/tailwindcss-language-service/src/util/language-blocks.ts @@ -1,45 +1,73 @@ import type { State } from '../util/state' -import type { Range } from 'vscode-languageserver' -import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { TextDocument, Range } from 'vscode-languageserver-textdocument' import { getLanguageBoundaries } from '../util/getLanguageBoundaries' import { isCssDoc } from '../util/css' import { getTextWithoutComments } from './doc' +import { isHtmlDoc } from './html' +import { isJsDoc } from './js' export interface LanguageBlock { - document: TextDocument + context: 'html' | 'js' | 'css' | 'other' range: Range | undefined lang: string - readonly text: string + text: string } -export function* getCssBlocks( - state: State, - document: TextDocument, -): Iterable { - if (isCssDoc(state, document)) { - yield { - document, - range: undefined, - lang: document.languageId, - get text() { - return getTextWithoutComments(document, 'css') - }, - } - } else { - let boundaries = getLanguageBoundaries(state, document) - if (!boundaries) return [] +export function getDocumentBlocks(state: State, doc: TextDocument): LanguageBlock[] { + let text = doc.getText() - for (let boundary of boundaries) { - if (boundary.type !== 'css') continue + let boundaries = getLanguageBoundaries(state, doc, text) + if (boundaries && boundaries.length > 0) { + return boundaries.map((boundary) => { + let context: 'html' | 'js' | 'css' | 'other' + + if (boundary.type === 'html') { + context = 'html' + } else if (boundary.type === 'css') { + context = 'css' + } else if (boundary.type === 'js' || boundary.type === 'jsx') { + context = 'js' + } else { + context = 'other' + } - yield { - document, + let text = doc.getText(boundary.range) + + return { + context, range: boundary.range, - lang: boundary.lang ?? document.languageId, - get text() { - return getTextWithoutComments(document, 'css', boundary.range) - }, + lang: boundary.lang ?? doc.languageId, + text: context === 'other' ? text : getTextWithoutComments(text, context), } - } + }) } + + // If we get here we most likely have non-HTML document in a single language + let context: 'html' | 'js' | 'css' | 'other' + + if (isHtmlDoc(state, doc)) { + context = 'html' + } else if (isCssDoc(state, doc)) { + context = 'css' + } else if (isJsDoc(state, doc)) { + context = 'js' + } else { + context = 'other' + } + + return [ + { + context, + range: { + start: doc.positionAt(0), + end: doc.positionAt(text.length), + }, + lang: doc.languageId, + text: context === 'other' ? text : getTextWithoutComments(text, context), + }, + ] +} + +export function getCssBlocks(state: State, document: TextDocument): LanguageBlock[] { + return getDocumentBlocks(state, document).filter((block) => block.context === 'css') } diff --git a/packages/tailwindcss-language-service/src/util/test-utils.ts b/packages/tailwindcss-language-service/src/util/test-utils.ts index 5b47bc96..1d1a9d01 100644 --- a/packages/tailwindcss-language-service/src/util/test-utils.ts +++ b/packages/tailwindcss-language-service/src/util/test-utils.ts @@ -12,6 +12,35 @@ export const html: Dedent = dedent export const astro: Dedent = dedent export const pug: Dedent = dedent +export function createSettings(settings: DeepPartial): Settings { + let defaults = getDefaultTailwindSettings() + settings ??= {} + + return { + ...defaults, + ...settings, + tailwindCSS: { + ...defaults.tailwindCSS, + ...settings.tailwindCSS, + lint: { + ...defaults.tailwindCSS.lint, + ...(settings.tailwindCSS?.lint ?? {}), + }, + experimental: { + ...defaults.tailwindCSS.experimental, + ...(settings.tailwindCSS?.experimental ?? {}), + }, + files: { + ...defaults.tailwindCSS.files, + ...(settings.tailwindCSS?.files ?? {}), + }, + }, + editor: { + ...defaults.editor, + ...settings.editor, + }, + } +} export function createDocument({ name, lang, @@ -29,34 +58,13 @@ export function createDocument({ 1, typeof content === 'string' ? content : content.join('\n'), ) - let defaults = getDefaultTailwindSettings() - settings ??= {} + + let documentSettings = createSettings(settings ?? {}) + let state = createState({ editor: { - getConfiguration: async () => ({ - ...defaults, - ...settings, - tailwindCSS: { - ...defaults.tailwindCSS, - ...settings.tailwindCSS, - lint: { - ...defaults.tailwindCSS.lint, - ...(settings.tailwindCSS?.lint ?? {}), - }, - experimental: { - ...defaults.tailwindCSS.experimental, - ...(settings.tailwindCSS?.experimental ?? {}), - }, - files: { - ...defaults.tailwindCSS.files, - ...(settings.tailwindCSS?.files ?? {}), - }, - }, - editor: { - ...defaults.editor, - ...settings.editor, - }, - }), + userLanguages: documentSettings.tailwindCSS.includeLanguages ?? {}, + getConfiguration: async () => documentSettings, }, }) From 2f2703d49bdd596cf473b6aa68289d15c053ef9f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 29 Dec 2025 19:11:57 -0500 Subject: [PATCH 14/17] Simplify code a bit --- .../src/util/find.ts | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 62fb4e0e..99371436 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -108,19 +108,11 @@ function findClassListsInCssRange( return matches.map((match) => { const start = indexToPosition(text, match.index + match[1].length) const end = indexToPosition(text, match.index + match[1].length + match.groups.classList.length) + const range = absoluteRange({ start, end }, { start: globalStart, end: globalStart }) return { classList: match.groups.classList, important: Boolean(match.groups.important), - range: { - start: { - line: globalStart.line + start.line, - character: (end.line === 0 ? globalStart.character : 0) + start.character, - }, - end: { - line: globalStart.line + end.line, - character: (end.line === 0 ? globalStart.character : 0) + end.character, - }, - }, + range, } }) } @@ -264,19 +256,11 @@ function findClassListsInHtmlRange( text, match.index + match[0].length - 1 + offset + value.length + afterOffset, ) + const resultRange = absoluteRange({ start, end }, range) const result: DocumentClassList = { classList: value.substr(beforeOffset, value.length + afterOffset), - range: { - start: { - line: (range?.start.line || 0) + start.line, - character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character, - }, - end: { - line: (range?.start.line || 0) + end.line, - character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character, - }, - }, + range: resultRange, } const resultKey = [ From 02440779cda88cf054c8b61ef19206d81d683173 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 31 Dec 2025 09:19:12 -0500 Subject: [PATCH 15/17] Cleanup --- packages/tailwindcss-language-service/src/util/find.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 99371436..382030fb 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -49,10 +49,10 @@ const BY_ASCII_WHITESPACE = /([\t\n\f\r ]+)/ * * The CSS spec also effectively uses the above definition for whitespace. * - * @see {@link https://dom.spec.whatwg.org/#concept-getelementsbyclassname} - * @see {@link https://dom.spec.whatwg.org/#concept-ordered-set-parser} - * @see {@link https://infra.spec.whatwg.org/#ascii-whitespace} - * @see {@link https://www.w3.org/TR/css-syntax-3/#whitespace} + * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname + * @see https://dom.spec.whatwg.org/#concept-ordered-set-parser + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @see https://www.w3.org/TR/css-syntax-3/#whitespace */ export function getClassNamesInClassList( classList: DocumentClassList, From 7b256f32868d0e22f8d6676daba2edddd0b459cf Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 31 Dec 2025 20:33:10 -0500 Subject: [PATCH 16/17] Pass settings into `findClassListsInDocument` --- .../src/diagnostics/canonical-classes.ts | 6 +-- .../src/diagnostics/diagnosticsProvider.ts | 10 ++--- .../diagnostics/getCssConflictDiagnostics.ts | 6 +-- .../diagnostics/getInvalidApplyDiagnostics.ts | 6 +-- .../getRecommendedVariantOrderDiagnostics.ts | 6 +-- .../getUsedBlocklistedClassDiagnostics.ts | 6 +-- .../src/documentColorProvider.ts | 2 +- .../src/util/find.test.ts | 38 +++++++++---------- .../src/util/find.ts | 10 ++--- .../src/util/test-utils.ts | 3 +- 10 files changed, 47 insertions(+), 46 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts b/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts index d7e3dfe9..2a35a782 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts @@ -3,11 +3,11 @@ import type { State, Settings } from '../util/state' import { type SuggestCanonicalClassesDiagnostic, DiagnosticKind } from './types' import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' -export async function getSuggestCanonicalClassesDiagnostics( +export function getSuggestCanonicalClassesDiagnostics( state: State, document: TextDocument, settings: Settings, -): Promise { +): SuggestCanonicalClassesDiagnostic[] { if (!state.v4) return [] if (!state.designSystem.canonicalizeCandidates) return [] @@ -16,7 +16,7 @@ export async function getSuggestCanonicalClassesDiagnostics( let diagnostics: SuggestCanonicalClassesDiagnostic[] = [] - let classLists = await findClassListsInDocument(state, document) + let classLists = findClassListsInDocument(state, document, settings) for (let classList of classLists) { let classNames = getClassNamesInClassList(classList, []) diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts index 1244b181..96d77610 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts @@ -33,10 +33,10 @@ export async function doValidate( return settings.tailwindCSS.validate ? [ ...(only.includes(DiagnosticKind.CssConflict) - ? await getCssConflictDiagnostics(state, document, settings) + ? getCssConflictDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.InvalidApply) - ? await getInvalidApplyDiagnostics(state, document, settings) + ? getInvalidApplyDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.InvalidScreen) ? getInvalidScreenDiagnostics(state, document, settings) @@ -54,13 +54,13 @@ export async function doValidate( ? getInvalidSourceDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.RecommendedVariantOrder) - ? await getRecommendedVariantOrderDiagnostics(state, document, settings) + ? getRecommendedVariantOrderDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.UsedBlocklistedClass) - ? await getUsedBlocklistedClassDiagnostics(state, document, settings) + ? getUsedBlocklistedClassDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.SuggestCanonicalClasses) - ? await getSuggestCanonicalClassesDiagnostics(state, document, settings) + ? getSuggestCanonicalClassesDiagnostics(state, document, settings) : []), ] : [] diff --git a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts index 065de72c..7cd2adc2 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts @@ -41,16 +41,16 @@ function getRuleProperties(rule: Rule): string[] { return properties } -export async function getCssConflictDiagnostics( +export function getCssConflictDiagnostics( state: State, document: TextDocument, settings: Settings, -): Promise { +): CssConflictDiagnostic[] { let severity = settings.tailwindCSS.lint.cssConflict if (severity === 'ignore') return [] let diagnostics: CssConflictDiagnostic[] = [] - const classLists = await findClassListsInDocument(state, document) + const classLists = findClassListsInDocument(state, document, settings) classLists.forEach((classList) => { const classNames = getClassNamesInClassList(classList, state.blocklist) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts index 7768c150..4482f70b 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts @@ -5,16 +5,16 @@ import type { Settings, State } from '../util/state' import { validateApply } from '../util/validateApply' import { isCssDoc } from '../util/css' -export async function getInvalidApplyDiagnostics( +export function getInvalidApplyDiagnostics( state: State, document: TextDocument, settings: Settings, -): Promise { +): InvalidApplyDiagnostic[] { let severity = settings.tailwindCSS.lint.invalidApply if (severity === 'ignore') return [] if (!isCssDoc(state, document)) return [] - const classLists = await findClassListsInDocument(state, document) + const classLists = findClassListsInDocument(state, document, settings) const classNames = classLists.flatMap((classList) => getClassNamesInClassList(classList, state.blocklist), ) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getRecommendedVariantOrderDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getRecommendedVariantOrderDiagnostics.ts index 959bb87f..c2d54732 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getRecommendedVariantOrderDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getRecommendedVariantOrderDiagnostics.ts @@ -7,11 +7,11 @@ import { getVariantsFromClassName } from '../util/getVariantsFromClassName' import { equalExact } from '../util/array' import * as semver from '../util/semver' -export async function getRecommendedVariantOrderDiagnostics( +export function getRecommendedVariantOrderDiagnostics( state: State, document: TextDocument, settings: Settings, -): Promise { +): RecommendedVariantOrderDiagnostic[] { if (state.v4) return [] if (!state.jit) return [] @@ -21,7 +21,7 @@ export async function getRecommendedVariantOrderDiagnostics( if (severity === 'ignore') return [] let diagnostics: RecommendedVariantOrderDiagnostic[] = [] - const classLists = await findClassListsInDocument(state, document) + const classLists = findClassListsInDocument(state, document, settings) classLists.forEach((classList) => { const classNames = getClassNamesInClassList(classList, state.blocklist) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getUsedBlocklistedClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getUsedBlocklistedClassDiagnostics.ts index a0743eae..f6fd22b9 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getUsedBlocklistedClassDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getUsedBlocklistedClassDiagnostics.ts @@ -3,11 +3,11 @@ import type { State, Settings } from '../util/state' import { type UsedBlocklistedClassDiagnostic, DiagnosticKind } from './types' import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' -export async function getUsedBlocklistedClassDiagnostics( +export function getUsedBlocklistedClassDiagnostics( state: State, document: TextDocument, settings: Settings, -): Promise { +): UsedBlocklistedClassDiagnostic[] { if (!state.v4) return [] if (!state.blocklist?.length) return [] @@ -17,7 +17,7 @@ export async function getUsedBlocklistedClassDiagnostics( let blocklist = new Set(state.blocklist ?? []) let diagnostics: UsedBlocklistedClassDiagnostic[] = [] - let classLists = await findClassListsInDocument(state, document) + let classLists = findClassListsInDocument(state, document, settings) for (let classList of classLists) { let classNames = getClassNamesInClassList(classList, []) diff --git a/packages/tailwindcss-language-service/src/documentColorProvider.ts b/packages/tailwindcss-language-service/src/documentColorProvider.ts index cef0729f..c8ecec6e 100644 --- a/packages/tailwindcss-language-service/src/documentColorProvider.ts +++ b/packages/tailwindcss-language-service/src/documentColorProvider.ts @@ -21,7 +21,7 @@ export async function getDocumentColors( let settings = await state.editor.getConfiguration(document.uri) if (settings.tailwindCSS.colorDecorators === false) return colors - let classLists = await findClassListsInDocument(state, document) + let classLists = findClassListsInDocument(state, document, settings) classLists.forEach((classList) => { let classNames = getClassNamesInClassList(classList, state.blocklist) classNames.forEach((className) => { diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index c8f5de0e..2df0d208 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -34,7 +34,7 @@ test('class regex works in astro', async ({ expect }) => { ], }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -129,9 +129,9 @@ test('find class lists in functions', async ({ expect }) => { `, }) - let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) - let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) - let classListsC = await findClassListsInDocument(fileC.state, fileC.doc) + let classListsA = findClassListsInDocument(fileA.state, fileA.doc, fileA.settings) + let classListsB = findClassListsInDocument(fileB.state, fileB.doc, fileB.settings) + let classListsC = findClassListsInDocument(fileC.state, fileC.doc, fileC.settings) expect(classListsA).toEqual([ // from clsx(…) @@ -243,7 +243,7 @@ test('find class lists in nested fn calls', async ({ expect }) => { `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toMatchObject([ { @@ -345,7 +345,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toMatchObject([ { @@ -408,8 +408,8 @@ test('find class lists in tagged template literals', async ({ expect }) => { `, }) - let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) - let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) + let classListsA = findClassListsInDocument(fileA.state, fileA.doc, fileA.settings) + let classListsB = findClassListsInDocument(fileB.state, fileB.doc, fileB.settings) expect(classListsA).toEqual([ // from clsx`…` @@ -490,8 +490,8 @@ test('classFunctions can be a regex', async ({ expect }) => { `, }) - let classListsA = await findClassListsInDocument(fileA.state, fileA.doc) - let classListsB = await findClassListsInDocument(fileB.state, fileB.doc) + let classListsA = findClassListsInDocument(fileA.state, fileA.doc, fileA.settings) + let classListsB = findClassListsInDocument(fileB.state, fileB.doc, fileB.settings) expect(classListsA).toEqual([ { @@ -523,7 +523,7 @@ test('classFunctions regexes only match on function names', async ({ expect }) = `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([]) }) @@ -546,7 +546,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => { `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -609,7 +609,7 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -688,7 +688,7 @@ test('classFunctions should only match in JS-like contexts', async ({ expect }) `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -748,7 +748,7 @@ test('classAttributes find class lists inside variables in JS(X)/TS(X)', async ( `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -789,7 +789,7 @@ test('classAttributes find class lists inside pug', async ({ expect }) => { `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -818,7 +818,7 @@ test('classAttributes find class lists inside Vue bindings', async ({ expect }) `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -1137,7 +1137,7 @@ test('class functions work inside astro code fences', async ({ expect }) => { `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { @@ -1173,7 +1173,7 @@ test('classFunctions are detected inside of arrays in javascript just after open `, }) - let classLists = await findClassListsInDocument(file.state, file.doc) + let classLists = findClassListsInDocument(file.state, file.doc, file.settings) expect(classLists).toEqual([ { diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 382030fb..6fe724a2 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -282,16 +282,15 @@ function findClassListsInHtmlRange( return results } -export async function findClassListsInDocument( +export function findClassListsInDocument( state: State, doc: TextDocument, -): Promise { + settings: Settings, +): DocumentClassList[] { if (isCssDoc(state, doc)) { return findClassListsInCssRange(state, doc) } - let settings = await state.editor.getConfiguration(doc.uri) - let classLists: DocumentClassList[] = [] let boundaries = getLanguageBoundaries(state, doc) @@ -546,7 +545,8 @@ export async function findClassNameAtPosition( doc: TextDocument, position: Position, ): Promise { - let classLists = await findClassListsInDocument(state, doc) + let settings = await state.editor.getConfiguration(doc.uri) + let classLists = findClassListsInDocument(state, doc, settings) let classNames = classLists.flatMap((classList) => getClassNamesInClassList(classList, state.blocklist), ) diff --git a/packages/tailwindcss-language-service/src/util/test-utils.ts b/packages/tailwindcss-language-service/src/util/test-utils.ts index 1d1a9d01..8034c193 100644 --- a/packages/tailwindcss-language-service/src/util/test-utils.ts +++ b/packages/tailwindcss-language-service/src/util/test-utils.ts @@ -51,7 +51,7 @@ export function createDocument({ lang: string content: string | string[] settings?: DeepPartial -}): { doc: TextDocument; state: State } { +}): { doc: TextDocument; state: State; settings: Settings } { let doc = TextDocument.create( `file://${name}`, lang, @@ -71,5 +71,6 @@ export function createDocument({ return { doc, state, + settings: documentSettings, } } From 6b58f5ee33306b4b8f4d3985b1176e7b8019896c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 31 Dec 2025 20:34:41 -0500 Subject: [PATCH 17/17] Pass settings into `findClassNameAtPosition` --- packages/tailwindcss-language-service/src/hoverProvider.ts | 7 +++---- .../tailwindcss-language-service/src/util/find.test.ts | 4 ++-- packages/tailwindcss-language-service/src/util/find.ts | 6 +++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 9cbb0452..4ffd63db 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -1,4 +1,4 @@ -import type { State } from './util/state' +import type { Settings, State } from './util/state' import type { Hover, MarkupContent, Position, Range } from 'vscode-languageserver' import { stringifyCss, stringifyConfigValue } from './util/stringify' import dlv from 'dlv' @@ -94,7 +94,8 @@ async function provideClassNameHover( document: TextDocument, position: Position, ): Promise { - let className = await findClassNameAtPosition(state, document, position) + let settings = await state.editor.getConfiguration(document.uri) + let className = findClassNameAtPosition(state, document, settings, position) if (className === null) return null if (state.v4) { @@ -136,8 +137,6 @@ async function provideClassNameHover( } } - const settings = await state.editor.getConfiguration(document.uri) - const css = stringifyCss( className.className, dlv(state.classNames.classNames, [...parts, '__info']), diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 2df0d208..3e6d91a2 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -847,7 +847,7 @@ test('Can find class name inside JS/TS functions in