From 39b4a94a3f6984044a3fd05ab7582c0baf372eaa Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Thu, 6 Mar 2025 20:07:06 +0200
Subject: [PATCH 01/32] Added "tailwindCSS.experimental.classFunctions" option
---
.../tailwindcss-language-server/src/config.ts | 1 +
.../src/completionProvider.ts | 6 +++++-
.../src/util/find.ts | 16 ++++++++++++----
.../src/util/lexers.ts | 9 +++++++++
.../src/util/state.ts | 1 +
packages/vscode-tailwindcss/package.json | 8 ++++++++
6 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts
index d8364d06..59e2a90e 100644
--- a/packages/tailwindcss-language-server/src/config.ts
+++ b/packages/tailwindcss-language-server/src/config.ts
@@ -37,6 +37,7 @@ function getDefaultSettings(): Settings {
experimental: {
classRegex: [],
configFile: null,
+ classFunctions: []
},
},
}
diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts
index c67eb0f5..9f0550b0 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -15,7 +15,7 @@ import removeMeta from './util/removeMeta'
import { formatColor, getColor, getColorFromValue } from './util/color'
import { isHtmlContext, isHtmlDoc, isVueDoc } from './util/html'
import { isCssContext } from './util/css'
-import { findLast, matchClassAttributes } from './util/find'
+import { findLast, matchClassAttributes, matchClassFunctions } from './util/find'
import { stringifyConfigValue, stringifyCss } from './util/stringify'
import { stringifyScreen, Screen } from './util/screens'
import isObject from './util/isObject'
@@ -747,6 +747,10 @@ async function provideClassAttributeCompletions(
let matches = matchClassAttributes(str, settings.classAttributes)
+ if (settings.experimental.classFunctions.length > 0) {
+ matches.push(...matchClassFunctions(str, settings.experimental.classFunctions))
+ }
+
if (matches.length === 0) {
return null
}
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index f38d7a16..1d3fac82 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -164,6 +164,12 @@ export function matchClassAttributes(text: string, attributes: string[]): RegExp
return findAll(new RegExp(re.source.replace('ATTRS', attrs.join('|')), 'gi'), text)
}
+export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatchArray[] {
+ const names = fnNames.filter((x) => typeof x === 'string')
+ const re = /\b(F_NAMES)\(/
+ return findAll(new RegExp(re.source.replace('F_NAMES', names.join('|')), 'gi'), text)
+}
+
export async function findClassListsInHtmlRange(
state: State,
doc: TextDocument,
@@ -172,10 +178,12 @@ export async function findClassListsInHtmlRange(
): Promise {
const text = getTextWithoutComments(doc, type, range)
- const matches = matchClassAttributes(
- text,
- (await state.editor.getConfiguration(doc.uri)).tailwindCSS.classAttributes,
- )
+ const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS
+ const matches = matchClassAttributes(text, settings.classAttributes)
+
+ if (settings.experimental.classFunctions.length > 0) {
+ matches.push(...matchClassFunctions(text, settings.experimental.classFunctions))
+ }
const result: DocumentClassList[] = []
diff --git a/packages/tailwindcss-language-service/src/util/lexers.ts b/packages/tailwindcss-language-service/src/util/lexers.ts
index 38ecc2d7..a8315f51 100644
--- a/packages/tailwindcss-language-service/src/util/lexers.ts
+++ b/packages/tailwindcss-language-service/src/util/lexers.ts
@@ -29,6 +29,14 @@ const classAttributeStates: () => { [x: string]: moo.Rules } = () => ({
rbrace: { match: new RegExp('(? {
start2: { match: "'", push: 'singleClassList' },
start3: { match: '{', push: 'interpBrace' },
start4: { match: '`', push: 'tickClassList' },
+ start5: { match: '(', push: 'interpParen' },
},
...classAttributeStates(),
})
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 95afe8ec..ffd741cc 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -66,6 +66,7 @@ export type TailwindCssSettings = {
experimental: {
classRegex: string[]
configFile: string | Record | null
+ classFunctions: string[]
}
files: {
exclude: string[]
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index 08906fbd..859e13b1 100644
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -304,6 +304,14 @@
"default": null,
"markdownDescription": "Manually specify the Tailwind config file or files that should be read to provide IntelliSense features. Can either be a single string value, or an object where each key is a config file path and each value is a glob or array of globs representing the set of files that the config file applies to."
},
+ "tailwindCSS.experimental.classFunctions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "default": [],
+ "markdownDescription": "The function names for which to provide class completions, hover previews, linting etc."
+ },
"tailwindCSS.showPixelEquivalents": {
"type": "boolean",
"default": true,
From baee7b5bf03da0d463a6361c9625e6e110082545 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 12:50:42 +0200
Subject: [PATCH 02/32] Use optional chaining to access classFunctions
---
packages/tailwindcss-language-service/src/completionProvider.ts | 2 +-
packages/tailwindcss-language-service/src/util/find.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts
index 9f0550b0..7050bf30 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -747,7 +747,7 @@ async function provideClassAttributeCompletions(
let matches = matchClassAttributes(str, settings.classAttributes)
- if (settings.experimental.classFunctions.length > 0) {
+ if (settings.experimental.classFunctions?.length) {
matches.push(...matchClassFunctions(str, settings.experimental.classFunctions))
}
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 1d3fac82..25afde92 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -181,7 +181,7 @@ export async function findClassListsInHtmlRange(
const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS
const matches = matchClassAttributes(text, settings.classAttributes)
- if (settings.experimental.classFunctions.length > 0) {
+ if (settings.experimental.classFunctions?.length) {
matches.push(...matchClassFunctions(text, settings.experimental.classFunctions))
}
From dc5a29830bfcbe46e44a28f010195859936743b6 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 12:53:18 +0200
Subject: [PATCH 03/32] Added tests for
"tailwindCSS.experimental.classFunctions"
---
.../src/util/find.test.ts | 235 +++++++++++++++++-
1 file changed, 233 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 90dbcfb3..c078afb2 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -1,9 +1,9 @@
-import type { State } from './state'
+import type { DocumentClassList, State } from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
-test('test', async ({ expect }) => {
+test('test astro', async ({ expect }) => {
let content = [
//
'',
@@ -79,3 +79,234 @@ test('test', async ({ expect }) => {
]
`)
})
+
+test('test simple classFunctions', async ({ expect }) => {
+ const state: State = {
+ blocklist: [],
+ editor: {
+ userLanguages: {},
+ getConfiguration: async () => ({
+ editor: {
+ tabSize: 1,
+ },
+ tailwindCSS: {
+ classAttributes: ['class'],
+ experimental: {
+ classFunctions: ['cva', 'cn'],
+ },
+ },
+ }),
+ },
+ } as any
+
+ const classList = `'pointer-events-auto relative flex bg-red-500',
+ 'items-center justify-between overflow-hidden',
+ 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm py-xs pl-md pr-xs gap-sm w-full',
+ 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]',
+ Date.now() > 15000 ? 'text-red-200' : 'text-red-700',
+ 'data-[swipe=move]:transition-none',
+ `
+
+ const expectedResult: DocumentClassList[] = [
+ {
+ classList: 'pointer-events-auto relative flex bg-red-500',
+ range: {
+ start: { character: 7, line: 2 },
+ end: { character: 51, line: 2 },
+ },
+ },
+ {
+ classList: 'items-center justify-between overflow-hidden',
+ range: {
+ start: { character: 7, line: 3 },
+ end: { character: 51, line: 3 },
+ },
+ },
+ {
+ classList: 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm py-xs pl-md pr-xs gap-sm w-full',
+ range: {
+ start: { character: 7, line: 4 },
+ end: { character: 83, line: 4 },
+ },
+ },
+ {
+ classList: 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]',
+ range: {
+ start: { line: 5, character: 7 },
+ end: { line: 5, character: 68 },
+ },
+ },
+ {
+ classList: 'text-red-200',
+ range: {
+ start: { line: 6, character: 28 },
+ end: { line: 6, character: 40 },
+ },
+ },
+ {
+ classList: 'text-red-700',
+ range: {
+ start: { line: 6, character: 45 },
+ end: { line: 6, character: 57 },
+ },
+ },
+ {
+ classList: 'data-[swipe=move]:transition-none',
+ range: {
+ start: { line: 7, character: 7 },
+ end: { line: 7, character: 40 },
+ },
+ },
+ ]
+
+ const cnContent = `
+ const classes = cn(
+ ${classList}
+ )
+ `
+
+ const cnDoc = TextDocument.create('file://file.html', 'html', 1, cnContent)
+ const cnClassLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+
+ expect(cnClassLists).toMatchObject(expectedResult)
+
+ const cvaContent = `
+ const classes = cva(
+ ${classList}
+ )
+ `
+
+ const cvaDoc = TextDocument.create('file://file.html', 'html', 1, cvaContent)
+ const cvaClassLists = await findClassListsInHtmlRange(state, cvaDoc, 'html')
+
+ expect(cvaClassLists).toMatchObject(expectedResult)
+
+ // Ensure another function name with the same layout doesn't match
+ const cmaContent = `
+ const classes = cma(
+ ${classList}
+ )
+ `
+
+ const cmaDoc = TextDocument.create('file://file.html', 'html', 1, cmaContent)
+ const cmaClassLists = await findClassListsInHtmlRange(state, cmaDoc, 'html')
+
+ expect(cmaClassLists).not.toMatchObject(expectedResult)
+})
+
+test('test nested classFunctions', async ({ expect }) => {
+ const state: State = {
+ blocklist: [],
+ editor: {
+ userLanguages: {},
+ getConfiguration: async () => ({
+ editor: {
+ tabSize: 1,
+ },
+ tailwindCSS: {
+ classAttributes: ['class'],
+ experimental: {
+ classFunctions: ['cva', 'cn'],
+ },
+ },
+ }),
+ },
+ } as any
+
+ const expectedResult: DocumentClassList[] = [
+ {
+ classList: 'fixed flex',
+ range: {
+ start: { line: 3, character: 9 },
+ end: { line: 3, character: 19 },
+ },
+ },
+ {
+ classList: 'md:h-[calc(100%-2rem)]',
+ range: {
+ start: { line: 4, character: 9 },
+ end: { line: 4, character: 31 },
+ },
+ },
+ {
+ classList: 'bg-red-700',
+ range: {
+ start: { line: 5, character: 9 },
+ end: { line: 5, character: 19 },
+ },
+ },
+ {
+ classList: 'bottom-0 left-0',
+ range: {
+ start: { line: 10, character: 22 },
+ end: { line: 10, character: 37 },
+ },
+ },
+ {
+ classList:
+ 'inset-0\n md:h-[calc(100%-2rem)]\n rounded-none\n ',
+ range: {
+ start: { line: 12, character: 14 },
+ end: { line: 14, character: 26 },
+ },
+ },
+ {
+ classList: 'default',
+ range: {
+ start: { line: 19, character: 19 },
+ end: { line: 19, character: 26 },
+ },
+ },
+ {
+ classList: 'fixed flex',
+ range: {
+ start: { line: 3, character: 9 },
+ end: { line: 3, character: 19 },
+ },
+ },
+ {
+ classList: 'md:h-[calc(100%-2rem)]',
+ range: {
+ start: { line: 4, character: 9 },
+ end: { line: 4, character: 31 },
+ },
+ },
+ {
+ classList: 'bg-red-700',
+ range: {
+ start: { line: 5, character: 9 },
+ end: { line: 5, character: 19 },
+ },
+ },
+ ]
+
+ const content = `
+ const variants = cva(
+ cn(
+ 'fixed flex',
+ 'md:h-[calc(100%-2rem)]',
+ 'bg-red-700',
+ ),
+ {
+ variants: {
+ mobile: {
+ default: 'bottom-0 left-0',
+ fullScreen: \`
+ inset-0
+ md:h-[calc(100%-2rem)]
+ rounded-none
+ \`,
+ },
+ },
+ defaultVariants: {
+ mobile: 'default',
+ },
+ },
+ )
+ `
+
+ const cnDoc = TextDocument.create('file://file.html', 'html', 1, content)
+ const classLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+
+ expect(classLists).toMatchObject(expectedResult)
+})
From 0e227e9a17ac6569f5370e18a92064eefd257b9f Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 15:09:54 +0200
Subject: [PATCH 04/32] Removed settings duplication: Created
getDefaultTailwindSettings in @tailwindcss/language-service
---
.../tailwindcss-language-server/src/config.ts | 42 +++----------------
.../tests/utils/configuration.ts | 41 +++---------------
.../src/util/state.ts | 35 ++++++++++++++++
3 files changed, 45 insertions(+), 73 deletions(-)
diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts
index 59e2a90e..a5e68f7d 100644
--- a/packages/tailwindcss-language-server/src/config.ts
+++ b/packages/tailwindcss-language-server/src/config.ts
@@ -1,6 +1,9 @@
import merge from 'deepmerge'
import { isObject } from './utils'
-import type { Settings } from '@tailwindcss/language-service/src/util/state'
+import {
+ getDefaultTailwindSettings,
+ type Settings,
+} from '@tailwindcss/language-service/src/util/state'
import type { Connection } from 'vscode-languageserver'
export interface SettingsCache {
@@ -8,41 +11,6 @@ export interface SettingsCache {
clear(): void
}
-function getDefaultSettings(): Settings {
- return {
- editor: { tabSize: 2 },
- tailwindCSS: {
- inspectPort: null,
- emmetCompletions: false,
- classAttributes: ['class', 'className', 'ngClass', 'class:list'],
- codeActions: true,
- hovers: true,
- suggestions: true,
- validate: true,
- colorDecorators: true,
- rootFontSize: 16,
- lint: {
- cssConflict: 'warning',
- invalidApply: 'error',
- invalidScreen: 'error',
- invalidVariant: 'error',
- invalidConfigPath: 'error',
- invalidTailwindDirective: 'error',
- invalidSourceDirective: 'error',
- recommendedVariantOrder: 'warning',
- },
- showPixelEquivalents: true,
- includeLanguages: {},
- files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] },
- experimental: {
- classRegex: [],
- configFile: null,
- classFunctions: []
- },
- },
- }
-}
-
export function createSettingsCache(connection: Connection): SettingsCache {
const cache: Map = new Map()
@@ -74,7 +42,7 @@ export function createSettingsCache(connection: Connection): SettingsCache {
tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {}
return merge(
- getDefaultSettings(),
+ getDefaultTailwindSettings(),
{ editor, tailwindCSS },
{ arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray },
)
diff --git a/packages/tailwindcss-language-server/tests/utils/configuration.ts b/packages/tailwindcss-language-server/tests/utils/configuration.ts
index 2b4d6fbc..8c08a518 100644
--- a/packages/tailwindcss-language-server/tests/utils/configuration.ts
+++ b/packages/tailwindcss-language-server/tests/utils/configuration.ts
@@ -1,4 +1,7 @@
-import type { Settings } from '@tailwindcss/language-service/src/util/state'
+import {
+ getDefaultTailwindSettings,
+ type Settings,
+} from '@tailwindcss/language-service/src/util/state'
import { URI } from 'vscode-uri'
import type { DeepPartial } from './types'
import { CacheMap } from '../../src/cache-map'
@@ -10,41 +13,7 @@ export interface Configuration {
}
export function createConfiguration(): Configuration {
- let defaults: Settings = {
- editor: {
- tabSize: 2,
- },
- tailwindCSS: {
- inspectPort: null,
- emmetCompletions: false,
- includeLanguages: {},
- classAttributes: ['class', 'className', 'ngClass', 'class:list'],
- suggestions: true,
- hovers: true,
- codeActions: true,
- validate: true,
- showPixelEquivalents: true,
- rootFontSize: 16,
- colorDecorators: true,
- lint: {
- cssConflict: 'warning',
- invalidApply: 'error',
- invalidScreen: 'error',
- invalidVariant: 'error',
- invalidConfigPath: 'error',
- invalidTailwindDirective: 'error',
- invalidSourceDirective: 'error',
- recommendedVariantOrder: 'warning',
- },
- experimental: {
- classRegex: [],
- configFile: {},
- },
- files: {
- exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'],
- },
- },
- }
+ let defaults = getDefaultTailwindSettings()
/**
* Settings per file or directory URI
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index ffd741cc..5fd6696c 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -172,3 +172,38 @@ export type ClassNameMeta = {
scope: string[]
context: string[]
}
+
+export function getDefaultTailwindSettings(): Settings {
+ return {
+ editor: { tabSize: 2 },
+ tailwindCSS: {
+ inspectPort: null,
+ emmetCompletions: false,
+ classAttributes: ['class', 'className', 'ngClass', 'class:list'],
+ codeActions: true,
+ hovers: true,
+ suggestions: true,
+ validate: true,
+ colorDecorators: true,
+ rootFontSize: 16,
+ lint: {
+ cssConflict: 'warning',
+ invalidApply: 'error',
+ invalidScreen: 'error',
+ invalidVariant: 'error',
+ invalidConfigPath: 'error',
+ invalidTailwindDirective: 'error',
+ invalidSourceDirective: 'error',
+ recommendedVariantOrder: 'warning',
+ },
+ showPixelEquivalents: true,
+ includeLanguages: {},
+ files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] },
+ experimental: {
+ classRegex: [],
+ configFile: null,
+ classFunctions: [],
+ },
+ },
+ }
+}
From 100cad726e06e4a856080ceb8b438f3101766c1a Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 15:14:50 +0200
Subject: [PATCH 05/32] Use getDefaultTailwindSettings in find.test.ts
---
.../src/util/find.test.ts | 81 +++++++++----------
.../src/util/state.ts | 2 +-
2 files changed, 38 insertions(+), 45 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index c078afb2..84b77812 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -1,4 +1,9 @@
-import type { DocumentClassList, State } from './state'
+import {
+ getDefaultTailwindSettings,
+ type DocumentClassList,
+ type EditorState,
+ type State,
+} from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
@@ -12,26 +17,27 @@ test('test astro', async ({ expect }) => {
].join('\n')
let doc = TextDocument.create('file://file.astro', 'astro', 1, content)
+ let defaultSettings = getDefaultTailwindSettings()
let state: State = {
blocklist: [],
+ enabled: true,
editor: {
userLanguages: {},
getConfiguration: async () => ({
- editor: {
- tabSize: 1,
- },
+ ...defaultSettings,
tailwindCSS: {
- classAttributes: ['class'],
+ ...defaultSettings.tailwindCSS,
experimental: {
+ ...defaultSettings.tailwindCSS.experimental,
classRegex: [
['cva\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
['cn\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
],
},
- } as any,
+ },
}),
- } as any,
- } as any
+ } as EditorState,
+ }
let classLists = await findClassListsInHtmlRange(state, doc, 'html')
@@ -81,24 +87,7 @@ test('test astro', async ({ expect }) => {
})
test('test simple classFunctions', async ({ expect }) => {
- const state: State = {
- blocklist: [],
- editor: {
- userLanguages: {},
- getConfiguration: async () => ({
- editor: {
- tabSize: 1,
- },
- tailwindCSS: {
- classAttributes: ['class'],
- experimental: {
- classFunctions: ['cva', 'cn'],
- },
- },
- }),
- },
- } as any
-
+ const state = getTailwindSettingsForClassFunctions()
const classList = `'pointer-events-auto relative flex bg-red-500',
'items-center justify-between overflow-hidden',
'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm py-xs pl-md pr-xs gap-sm w-full',
@@ -195,24 +184,7 @@ test('test simple classFunctions', async ({ expect }) => {
})
test('test nested classFunctions', async ({ expect }) => {
- const state: State = {
- blocklist: [],
- editor: {
- userLanguages: {},
- getConfiguration: async () => ({
- editor: {
- tabSize: 1,
- },
- tailwindCSS: {
- classAttributes: ['class'],
- experimental: {
- classFunctions: ['cva', 'cn'],
- },
- },
- }),
- },
- } as any
-
+ const state = getTailwindSettingsForClassFunctions()
const expectedResult: DocumentClassList[] = [
{
classList: 'fixed flex',
@@ -310,3 +282,24 @@ test('test nested classFunctions', async ({ expect }) => {
expect(classLists).toMatchObject(expectedResult)
})
+
+function getTailwindSettingsForClassFunctions(): State {
+ const defaultSettings = getDefaultTailwindSettings()
+ return {
+ blocklist: [],
+ enabled: true,
+ editor: {
+ userLanguages: {},
+ getConfiguration: async () => ({
+ ...defaultSettings,
+ tailwindCSS: {
+ ...defaultSettings.tailwindCSS,
+ experimental: {
+ ...defaultSettings.tailwindCSS.experimental,
+ classFunctions: ['cva', 'cn'],
+ },
+ },
+ }),
+ } as EditorState,
+ }
+}
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 5fd6696c..29db4c88 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -64,7 +64,7 @@ export type TailwindCssSettings = {
recommendedVariantOrder: DiagnosticSeveritySetting
}
experimental: {
- classRegex: string[]
+ classRegex: string[] | [string, string][]
configFile: string | Record | null
classFunctions: string[]
}
From 41cc24ac335db896a2627616a022f92d998b81d0 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 16:15:09 +0200
Subject: [PATCH 06/32] Fixed findClassListsInHtmlRange state type & removed
type casting in find.test.ts
---
.../src/util/find.test.ts | 24 ++++++-------------
.../src/util/find.ts | 4 +++-
2 files changed, 10 insertions(+), 18 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 84b77812..9a103cd8 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -1,9 +1,4 @@
-import {
- getDefaultTailwindSettings,
- type DocumentClassList,
- type EditorState,
- type State,
-} from './state'
+import { getDefaultTailwindSettings, type DocumentClassList } from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
@@ -18,15 +13,13 @@ test('test astro', async ({ expect }) => {
let doc = TextDocument.create('file://file.astro', 'astro', 1, content)
let defaultSettings = getDefaultTailwindSettings()
- let state: State = {
- blocklist: [],
- enabled: true,
+ let state: Parameters[0] = {
editor: {
- userLanguages: {},
getConfiguration: async () => ({
...defaultSettings,
tailwindCSS: {
...defaultSettings.tailwindCSS,
+ classAttributes: ['class'],
experimental: {
...defaultSettings.tailwindCSS.experimental,
classRegex: [
@@ -36,7 +29,7 @@ test('test astro', async ({ expect }) => {
},
},
}),
- } as EditorState,
+ },
}
let classLists = await findClassListsInHtmlRange(state, doc, 'html')
@@ -180,7 +173,7 @@ test('test simple classFunctions', async ({ expect }) => {
const cmaDoc = TextDocument.create('file://file.html', 'html', 1, cmaContent)
const cmaClassLists = await findClassListsInHtmlRange(state, cmaDoc, 'html')
- expect(cmaClassLists).not.toMatchObject(expectedResult)
+ expect(cmaClassLists).toMatchObject([])
})
test('test nested classFunctions', async ({ expect }) => {
@@ -283,13 +276,10 @@ test('test nested classFunctions', async ({ expect }) => {
expect(classLists).toMatchObject(expectedResult)
})
-function getTailwindSettingsForClassFunctions(): State {
+function getTailwindSettingsForClassFunctions(): Parameters[0] {
const defaultSettings = getDefaultTailwindSettings()
return {
- blocklist: [],
- enabled: true,
editor: {
- userLanguages: {},
getConfiguration: async () => ({
...defaultSettings,
tailwindCSS: {
@@ -300,6 +290,6 @@ function getTailwindSettingsForClassFunctions(): State {
},
},
}),
- } as EditorState,
+ },
}
}
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 25afde92..aa9ba3bc 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -171,7 +171,9 @@ export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatc
}
export async function findClassListsInHtmlRange(
- state: State,
+ // PickDeep from 'type-fest' package cannot be used here due to a circular reference issue in the package
+ // Fixes are underway (see: https://github.com/sindresorhus/type-fest/pull/1079)
+ state: { editor?: Pick, 'getConfiguration'> },
doc: TextDocument,
type: 'html' | 'js' | 'jsx',
range?: Range,
From 3b4e04c53ee6fb7d29f255dbb3e70c5e6839261f Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 16:31:48 +0200
Subject: [PATCH 07/32] Changed getDefaultTailwindSettings to return const
object that satisfies Settings
---
packages/tailwindcss-language-service/src/util/state.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 29db4c88..0c808060 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -173,7 +173,7 @@ export type ClassNameMeta = {
context: string[]
}
-export function getDefaultTailwindSettings(): Settings {
+export function getDefaultTailwindSettings() {
return {
editor: { tabSize: 2 },
tailwindCSS: {
@@ -205,5 +205,6 @@ export function getDefaultTailwindSettings(): Settings {
classFunctions: [],
},
},
- }
+ // Return this as const object that satisfies Settings to be able to see the exact default values we specify
+ } as const satisfies Settings
}
From 7d059da9cde2f39f195fa25d19a104d436fd4398 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 17:44:24 +0200
Subject: [PATCH 08/32] Moved classFunctions option out of experimental
---
.../src/completionProvider.ts | 4 ++--
.../src/util/find.test.ts | 5 +----
.../src/util/find.ts | 4 ++--
.../src/util/state.ts | 4 ++--
packages/vscode-tailwindcss/package.json | 16 ++++++++--------
5 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts
index 7050bf30..7343c1e9 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -747,8 +747,8 @@ async function provideClassAttributeCompletions(
let matches = matchClassAttributes(str, settings.classAttributes)
- if (settings.experimental.classFunctions?.length) {
- matches.push(...matchClassFunctions(str, settings.experimental.classFunctions))
+ if (settings.classFunctions?.length) {
+ matches.push(...matchClassFunctions(str, settings.classFunctions))
}
if (matches.length === 0) {
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 9a103cd8..977881f7 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -284,10 +284,7 @@ function getTailwindSettingsForClassFunctions(): Parameters
classAttributes: string[]
+ classFunctions: string[]
suggestions: boolean
hovers: boolean
codeActions: boolean
@@ -66,7 +67,6 @@ export type TailwindCssSettings = {
experimental: {
classRegex: string[] | [string, string][]
configFile: string | Record | null
- classFunctions: string[]
}
files: {
exclude: string[]
@@ -180,6 +180,7 @@ export function getDefaultTailwindSettings() {
inspectPort: null,
emmetCompletions: false,
classAttributes: ['class', 'className', 'ngClass', 'class:list'],
+ classFunctions: [],
codeActions: true,
hovers: true,
suggestions: true,
@@ -202,7 +203,6 @@ export function getDefaultTailwindSettings() {
experimental: {
classRegex: [],
configFile: null,
- classFunctions: [],
},
},
// Return this as const object that satisfies Settings to be able to see the exact default values we specify
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index 859e13b1..de73bfba 100644
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -184,6 +184,14 @@
],
"markdownDescription": "The HTML attributes for which to provide class completions, hover previews, linting etc."
},
+ "tailwindCSS.classFunctions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "default": [],
+ "markdownDescription": "The function names for which to provide class completions, hover previews, linting etc."
+ },
"tailwindCSS.suggestions": {
"type": "boolean",
"default": true,
@@ -304,14 +312,6 @@
"default": null,
"markdownDescription": "Manually specify the Tailwind config file or files that should be read to provide IntelliSense features. Can either be a single string value, or an object where each key is a config file path and each value is a glob or array of globs representing the set of files that the config file applies to."
},
- "tailwindCSS.experimental.classFunctions": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "default": [],
- "markdownDescription": "The function names for which to provide class completions, hover previews, linting etc."
- },
"tailwindCSS.showPixelEquivalents": {
"type": "boolean",
"default": true,
From dc441bc55387bea5fd98e5371a64fb6812b49b85 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Fri, 7 Mar 2025 18:12:43 +0200
Subject: [PATCH 09/32] Added support for tagged template literals
---
.../src/util/find.test.ts | 51 +++++++++++++++++++
.../src/util/find.ts | 2 +-
packages/vscode-tailwindcss/package.json | 2 +-
3 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 977881f7..ff4e85d3 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -276,6 +276,57 @@ test('test nested classFunctions', async ({ expect }) => {
expect(classLists).toMatchObject(expectedResult)
})
+test('test classFunctions with tagged template literals', async ({ expect }) => {
+ const state = getTailwindSettingsForClassFunctions()
+ const classList = `pointer-events-auto relative flex bg-red-500
+ items-center justify-between overflow-hidden
+ md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm pl-md py-xs pr-xs gap-sm w-full
+ data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]
+ md:h-[calc(100%-2rem)]
+ data-[swipe=move]:transition-none`
+
+ const expectedResult: DocumentClassList[] = [
+ {
+ classList,
+ range: {
+ start: { line: 2, character: 6 },
+ end: { line: 7, character: 37 },
+ },
+ },
+ ]
+
+ const cnContent = `
+ const tagged = cn\`
+ ${classList}\`
+ `
+ const cnDoc = TextDocument.create('file://file.html', 'html', 1, cnContent)
+ const cnClassLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+
+ console.log('cnClassLists', JSON.stringify(cnClassLists, null, 2))
+
+ expect(cnClassLists).toMatchObject(expectedResult)
+
+ const cvaContent = `
+ const tagged = cva\`
+ ${classList}\`
+ `
+ const cvaDoc = TextDocument.create('file://file.html', 'html', 1, cvaContent)
+ const cvaClassLists = await findClassListsInHtmlRange(state, cvaDoc, 'html')
+
+ expect(cvaClassLists).toMatchObject(expectedResult)
+
+ // Ensure another tag name with the same layout doesn't match
+ const cmaContent = `
+ const tagged = cma\`
+ ${classList}\`
+ `
+
+ const cmaDoc = TextDocument.create('file://file.html', 'html', 1, cmaContent)
+ const cmaClassLists = await findClassListsInHtmlRange(state, cmaDoc, 'html')
+
+ expect(cmaClassLists).toMatchObject([])
+})
+
function getTailwindSettingsForClassFunctions(): Parameters[0] {
const defaultSettings = getDefaultTailwindSettings()
return {
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 5fd93dd7..2adda7f7 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -166,7 +166,7 @@ export function matchClassAttributes(text: string, attributes: string[]): RegExp
export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatchArray[] {
const names = fnNames.filter((x) => typeof x === 'string')
- const re = /\b(F_NAMES)\(/
+ const re = /\b(F_NAMES)(\(|`)/
return findAll(new RegExp(re.source.replace('F_NAMES', names.join('|')), 'gi'), text)
}
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index de73bfba..9f218da6 100644
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -190,7 +190,7 @@
"type": "string"
},
"default": [],
- "markdownDescription": "The function names for which to provide class completions, hover previews, linting etc."
+ "markdownDescription": "The function or tagged template literal names for which to provide class completions, hover previews, linting etc."
},
"tailwindCSS.suggestions": {
"type": "boolean",
From 43fab13e0aa000d2a0b4e375d9dd3f10fe5a7471 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 12 Mar 2025 11:40:16 -0400
Subject: [PATCH 10/32] wip
---
.../src/util/find.ts | 24 ++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 2adda7f7..b9d1b999 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -165,9 +165,27 @@ export function matchClassAttributes(text: string, attributes: string[]): RegExp
}
export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatchArray[] {
- const names = fnNames.filter((x) => typeof x === 'string')
- const re = /\b(F_NAMES)(\(|`)/
- return findAll(new RegExp(re.source.replace('F_NAMES', names.join('|')), 'gi'), text)
+ // 1. Validate the list of function name patterns provided by the user
+ let names = fnNames.filter((x) => typeof x === 'string')
+ if (names.length === 0) return []
+
+ // 2. Extract function names in the document
+ // This is intentionally scoped to JS syntax for now but should be extended to
+ // other languages in the future
+ //
+ // This regex the JS pattern for an identifier + function call with some
+ // additional constraints:
+ //
+ // - It needs to be in an expression position — so it must be preceded by
+ // whitespace, parens, curlies, commas, whitespace, etc…
+ // - It must look like a fn call or a tagged template literal
+ let FN_NAMES = /(?<=^|[:=,;\s{()])([\p{ID_Start}$_][\p{ID_Continue}$_.]*)[(`]/dgu
+ let foundFns = findAll(FN_NAMES, text)
+
+ // 3. Match against the function names in the document
+ let re = /^(NAMES)$/
+ let isClassFn = new RegExp(re.source.replace('NAMES', names.join('|')), 'dgi')
+ return foundFns.filter((fn) => isClassFn.test(fn[1]))
}
export async function findClassListsInHtmlRange(
From 9ee8b19bc99a1439fc88959298fb0570d50b2b6f Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 12 Mar 2025 11:46:40 -0400
Subject: [PATCH 11/32] wip
---
.../src/util/find.test.ts | 21 -------------------
.../src/util/find.ts | 7 +++++--
2 files changed, 5 insertions(+), 23 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index ff4e85d3..41ffa77e 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -222,27 +222,6 @@ test('test nested classFunctions', async ({ expect }) => {
end: { line: 19, character: 26 },
},
},
- {
- classList: 'fixed flex',
- range: {
- start: { line: 3, character: 9 },
- end: { line: 3, character: 19 },
- },
- },
- {
- classList: 'md:h-[calc(100%-2rem)]',
- range: {
- start: { line: 4, character: 9 },
- end: { line: 4, character: 31 },
- },
- },
- {
- classList: 'bg-red-700',
- range: {
- start: { line: 5, character: 9 },
- end: { line: 5, character: 19 },
- },
- },
]
const content = `
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index b9d1b999..ba73e9da 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -179,13 +179,16 @@ export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatc
// - It needs to be in an expression position — so it must be preceded by
// whitespace, parens, curlies, commas, whitespace, etc…
// - It must look like a fn call or a tagged template literal
- let FN_NAMES = /(?<=^|[:=,;\s{()])([\p{ID_Start}$_][\p{ID_Continue}$_.]*)[(`]/dgu
+ let FN_NAMES = /(?<=^|[:=,;\s{()])([\p{ID_Start}$_][\p{ID_Continue}$_.]*)[(`]/dgiu
let foundFns = findAll(FN_NAMES, text)
// 3. Match against the function names in the document
let re = /^(NAMES)$/
let isClassFn = new RegExp(re.source.replace('NAMES', names.join('|')), 'dgi')
- return foundFns.filter((fn) => isClassFn.test(fn[1]))
+
+ let matches = foundFns.filter((fn) => isClassFn.test(fn[1]))
+
+ return matches
}
export async function findClassListsInHtmlRange(
From 94f130039252a04dd3f4f77dd07f27c513cbf210 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 12 Mar 2025 11:50:35 -0400
Subject: [PATCH 12/32] wip
---
packages/tailwindcss-language-service/src/util/find.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index ba73e9da..b0ab3bf3 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -192,13 +192,13 @@ export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatc
}
export async function findClassListsInHtmlRange(
- // PickDeep from 'type-fest' package cannot be used here due to a circular reference issue in the package
- // Fixes are underway (see: https://github.com/sindresorhus/type-fest/pull/1079)
- state: { editor?: Pick, 'getConfiguration'> },
+ state: State,
doc: TextDocument,
type: 'html' | 'js' | 'jsx',
range?: Range,
): Promise {
+ if (!state.editor) return []
+
const text = getTextWithoutComments(doc, type, range)
const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS
From e29481dc3687f6a1ba06e17ea5c79ff23412ec92 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 12 Mar 2025 12:02:04 -0400
Subject: [PATCH 13/32] wip
---
.../src/util/find.test.ts | 6 ++--
.../src/util/state.ts | 34 +++++++++++++++++--
2 files changed, 34 insertions(+), 6 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 41ffa77e..ab9bce59 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -1,4 +1,4 @@
-import { getDefaultTailwindSettings, type DocumentClassList } from './state'
+import { createState, getDefaultTailwindSettings, type DocumentClassList } from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
@@ -13,7 +13,7 @@ test('test astro', async ({ expect }) => {
let doc = TextDocument.create('file://file.astro', 'astro', 1, content)
let defaultSettings = getDefaultTailwindSettings()
- let state: Parameters[0] = {
+ let state = createState({
editor: {
getConfiguration: async () => ({
...defaultSettings,
@@ -30,7 +30,7 @@ test('test astro', async ({ expect }) => {
},
}),
},
- }
+ })
let classLists = await findClassListsInHtmlRange(state, doc, 'html')
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 68a199c3..26e84308 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -173,7 +173,7 @@ export type ClassNameMeta = {
context: string[]
}
-export function getDefaultTailwindSettings() {
+export function getDefaultTailwindSettings(): Settings {
return {
editor: { tabSize: 2 },
tailwindCSS: {
@@ -205,6 +205,34 @@ export function getDefaultTailwindSettings() {
configFile: null,
},
},
- // Return this as const object that satisfies Settings to be able to see the exact default values we specify
- } as const satisfies Settings
+ }
+}
+
+export function createState(
+ partial: Omit, 'editor'> & {
+ editor?: Partial
+ },
+): State {
+ return {
+ enabled: true,
+ ...partial,
+ editor: {
+ get connection(): Connection {
+ throw new Error('Not implemented')
+ },
+ folder: '/',
+ userLanguages: {},
+ capabilities: {
+ configuration: true,
+ diagnosticRelatedInformation: true,
+ itemDefaults: [],
+ },
+ getConfiguration: () => {
+ throw new Error('Not implemented')
+ },
+ getDocumentSymbols: async () => [],
+ readDirectory: async () => [],
+ ...partial.editor,
+ },
+ }
}
From 23ccf9c544d8a1011fd31583734577561ba0900b Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 12:50:06 -0400
Subject: [PATCH 14/32] Move types
---
packages/tailwindcss-language-server/tests/utils/client.ts | 2 +-
.../tests/utils => tailwindcss-language-service/src}/types.ts | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename packages/{tailwindcss-language-server/tests/utils => tailwindcss-language-service/src}/types.ts (100%)
diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts
index f7ee6e94..d6d317bb 100644
--- a/packages/tailwindcss-language-server/tests/utils/client.ts
+++ b/packages/tailwindcss-language-server/tests/utils/client.ts
@@ -44,7 +44,7 @@ import { createConfiguration, Configuration } from './configuration'
import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/util/getLanguageBoundaries'
import { DefaultMap } from '../../src/util/default-map'
import { connect, ConnectOptions } from './connection'
-import type { DeepPartial } from './types'
+import type { DeepPartial } from '@tailwindcss/language-service/src/types'
export interface DocumentDescriptor {
/**
diff --git a/packages/tailwindcss-language-server/tests/utils/types.ts b/packages/tailwindcss-language-service/src/types.ts
similarity index 100%
rename from packages/tailwindcss-language-server/tests/utils/types.ts
rename to packages/tailwindcss-language-service/src/types.ts
From 481d270a5a9898266e0c58d13563941056f79e97 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 12:50:11 -0400
Subject: [PATCH 15/32] wip
---
packages/tailwindcss-language-service/src/util/state.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 26e84308..d548b270 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -173,6 +173,9 @@ export type ClassNameMeta = {
context: string[]
}
+/**
+ * @internal
+ */
export function getDefaultTailwindSettings(): Settings {
return {
editor: { tabSize: 2 },
From b2140924aaf7ee4fe44dd44b268819582dbc33ca Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 12:50:23 -0400
Subject: [PATCH 16/32] Rewrite tests
---
.../tailwindcss-language-service/package.json | 2 +
.../src/util/find.test.ts | 620 +++++++++++-------
2 files changed, 401 insertions(+), 221 deletions(-)
diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json
index 0cbdde25..39feb660 100644
--- a/packages/tailwindcss-language-service/package.json
+++ b/packages/tailwindcss-language-service/package.json
@@ -41,9 +41,11 @@
},
"devDependencies": {
"@types/css.escape": "^1.5.2",
+ "@types/dedent": "^0.7.2",
"@types/line-column": "^1.0.2",
"@types/node": "^18.19.33",
"@types/stringify-object": "^4.0.5",
+ "dedent": "^1.5.3",
"esbuild": "^0.25.0",
"esbuild-node-externals": "^1.9.0",
"minimist": "^1.2.8",
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index ab9bce59..cc8ff013 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -1,322 +1,500 @@
-import { createState, getDefaultTailwindSettings, type DocumentClassList } from './state'
+import { createState, getDefaultTailwindSettings, Settings, type DocumentClassList } from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
+import type { DeepPartial } from '../types'
+import dedent from 'dedent'
-test('test astro', async ({ expect }) => {
- let content = [
- //
- '',
- ' ',
- '',
- ].join('\n')
+const js = dedent
- let doc = TextDocument.create('file://file.astro', 'astro', 1, content)
- let defaultSettings = getDefaultTailwindSettings()
- let state = createState({
- editor: {
- getConfiguration: async () => ({
- ...defaultSettings,
- tailwindCSS: {
- ...defaultSettings.tailwindCSS,
- classAttributes: ['class'],
- experimental: {
- ...defaultSettings.tailwindCSS.experimental,
- classRegex: [
- ['cva\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
- ['cn\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
- ],
- },
+test('class regex works in astro', async ({ expect }) => {
+ let { doc, state } = createDocument({
+ name: 'file.astro',
+ lang: 'astro',
+ settings: {
+ tailwindCSS: {
+ classAttributes: ['class'],
+ experimental: {
+ classRegex: [
+ ['cva\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
+ ['cn\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'],
+ ],
},
- }),
+ },
},
+ content: [
+ '',
+ ' ',
+ '',
+ ],
})
let classLists = await findClassListsInHtmlRange(state, doc, 'html')
- expect(classLists).toMatchInlineSnapshot(`
- [
- {
- "classList": "p-4 sm:p-2 $",
- "range": {
- "end": {
- "character": 22,
- "line": 0,
- },
- "start": {
- "character": 10,
- "line": 0,
- },
- },
+ expect(classLists).toEqual([
+ {
+ classList: 'p-4 sm:p-2 $',
+ range: {
+ start: { line: 0, character: 10 },
+ end: { line: 0, character: 22 },
},
- {
- "classList": "underline",
- "range": {
- "end": {
- "character": 42,
- "line": 0,
- },
- "start": {
- "character": 33,
- "line": 0,
- },
- },
+ },
+ {
+ classList: 'underline',
+ range: {
+ start: { line: 0, character: 33 },
+ end: { line: 0, character: 42 },
},
- {
- "classList": "line-through",
- "range": {
- "end": {
- "character": 58,
- "line": 0,
- },
- "start": {
- "character": 46,
- "line": 0,
- },
- },
+ },
+ {
+ classList: 'line-through',
+ range: {
+ start: { line: 0, character: 46 },
+ end: { line: 0, character: 58 },
},
- ]
- `)
+ },
+ ])
})
-test('test simple classFunctions', async ({ expect }) => {
- const state = getTailwindSettingsForClassFunctions()
- const classList = `'pointer-events-auto relative flex bg-red-500',
- 'items-center justify-between overflow-hidden',
- 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm py-xs pl-md pr-xs gap-sm w-full',
- 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]',
- Date.now() > 15000 ? 'text-red-200' : 'text-red-700',
- 'data-[swipe=move]:transition-none',
- `
-
- const expectedResult: DocumentClassList[] = [
+test('find class lists in functions', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
+ content: js`
+ // These should match
+ let classes = clsx(
+ 'flex p-4',
+ 'block sm:p-0',
+ Date.now() > 100 ? 'text-white' : 'text-black',
+ )
+
+ // These should match
+ let classes = cva(
+ 'flex p-4',
+ 'block sm:p-0',
+ Date.now() > 100 ? 'text-white' : 'text-black',
+ )
+ `,
+ })
+
+ let fileB = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
+ content: js`
+ let classes = cn(
+ 'flex p-4',
+ 'block sm:p-0',
+ Date.now() > 100 ? 'text-white' : 'text-black',
+ )
+ `,
+ })
+
+ let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+ let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js')
+
+ expect(classListsA).toEqual([
+ // from clsx(…)
{
- classList: 'pointer-events-auto relative flex bg-red-500',
+ classList: 'flex p-4',
range: {
- start: { character: 7, line: 2 },
- end: { character: 51, line: 2 },
+ start: { line: 2, character: 3 },
+ end: { line: 2, character: 11 },
},
},
{
- classList: 'items-center justify-between overflow-hidden',
+ classList: 'block sm:p-0',
range: {
- start: { character: 7, line: 3 },
- end: { character: 51, line: 3 },
+ start: { line: 3, character: 3 },
+ end: { line: 3, character: 15 },
},
},
{
- classList: 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm py-xs pl-md pr-xs gap-sm w-full',
+ classList: 'text-white',
range: {
- start: { character: 7, line: 4 },
- end: { character: 83, line: 4 },
+ start: { line: 4, character: 22 },
+ end: { line: 4, character: 32 },
},
},
{
- classList: 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]',
+ classList: 'text-black',
range: {
- start: { line: 5, character: 7 },
- end: { line: 5, character: 68 },
+ start: { line: 4, character: 37 },
+ end: { line: 4, character: 47 },
},
},
+
+ // from cva(…)
{
- classList: 'text-red-200',
+ classList: 'flex p-4',
range: {
- start: { line: 6, character: 28 },
- end: { line: 6, character: 40 },
+ start: { line: 9, character: 3 },
+ end: { line: 9, character: 11 },
},
},
{
- classList: 'text-red-700',
+ classList: 'block sm:p-0',
range: {
- start: { line: 6, character: 45 },
- end: { line: 6, character: 57 },
+ start: { line: 10, character: 3 },
+ end: { line: 10, character: 15 },
},
},
{
- classList: 'data-[swipe=move]:transition-none',
+ classList: 'text-white',
range: {
- start: { line: 7, character: 7 },
- end: { line: 7, character: 40 },
+ start: { line: 11, character: 22 },
+ end: { line: 11, character: 32 },
},
},
- ]
-
- const cnContent = `
- const classes = cn(
- ${classList}
- )
- `
-
- const cnDoc = TextDocument.create('file://file.html', 'html', 1, cnContent)
- const cnClassLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+ {
+ classList: 'text-black',
+ range: {
+ start: { line: 11, character: 37 },
+ end: { line: 11, character: 47 },
+ },
+ },
+ ])
- expect(cnClassLists).toMatchObject(expectedResult)
+ // none from cn(…) since it's not in the list of class functions
+ expect(classListsB).toEqual([])
+})
- const cvaContent = `
- const classes = cva(
- ${classList}
- )
- `
+test('find class lists in nested fn calls', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
- const cvaDoc = TextDocument.create('file://file.html', 'html', 1, cvaContent)
- const cvaClassLists = await findClassListsInHtmlRange(state, cvaDoc, 'html')
+ content: js`
+ // NOTE: All strings inside a matched class function will be treated as class lists
+ // TODO: Nested calls tha are *not* class functions should have their content ignored
+ let classes = clsx(
+ 'flex',
+ cn({
+ 'bg-red-500': true,
+ 'text-white': Date.now() > 100,
+ }),
+ clsx(
+ 'fixed',
+ 'absolute inset-0'
+ ),
+ cva(
+ ['bottom-0', 'border'],
+ {
+ variants: {
+ mobile: {
+ default: 'bottom-0 left-0',
+ large: \`
+ inset-0
+ rounded-none
+ \`,
+ },
+ }
+ }
+ )
+ )
+ `,
+ })
- expect(cvaClassLists).toMatchObject(expectedResult)
+ let classLists = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'html')
- // Ensure another function name with the same layout doesn't match
- const cmaContent = `
- const classes = cma(
- ${classList}
- )
- `
+ expect(classLists).toMatchObject([
+ {
+ classList: 'flex',
+ range: {
+ start: { line: 3, character: 3 },
+ end: { line: 3, character: 7 },
+ },
+ },
- const cmaDoc = TextDocument.create('file://file.html', 'html', 1, cmaContent)
- const cmaClassLists = await findClassListsInHtmlRange(state, cmaDoc, 'html')
+ // TODO: This should be ignored because they're inside cn(…)
+ {
+ classList: 'bg-red-500',
+ range: {
+ start: { line: 5, character: 5 },
+ end: { line: 5, character: 15 },
+ },
+ },
- expect(cmaClassLists).toMatchObject([])
-})
+ // TODO: This should be ignored because they're inside cn(…)
+ {
+ classList: 'text-white',
+ range: {
+ start: { line: 6, character: 5 },
+ end: { line: 6, character: 15 },
+ },
+ },
-test('test nested classFunctions', async ({ expect }) => {
- const state = getTailwindSettingsForClassFunctions()
- const expectedResult: DocumentClassList[] = [
{
- classList: 'fixed flex',
+ classList: 'fixed',
+ range: {
+ start: { line: 9, character: 5 },
+ end: { line: 9, character: 10 },
+ },
+ },
+ {
+ classList: 'absolute inset-0',
range: {
- start: { line: 3, character: 9 },
- end: { line: 3, character: 19 },
+ start: { line: 10, character: 5 },
+ end: { line: 10, character: 21 },
},
},
{
- classList: 'md:h-[calc(100%-2rem)]',
+ classList: 'bottom-0',
range: {
- start: { line: 4, character: 9 },
- end: { line: 4, character: 31 },
+ start: { line: 13, character: 6 },
+ end: { line: 13, character: 14 },
},
},
{
- classList: 'bg-red-700',
+ classList: 'border',
range: {
- start: { line: 5, character: 9 },
- end: { line: 5, character: 19 },
+ start: { line: 13, character: 18 },
+ end: { line: 13, character: 24 },
},
},
{
classList: 'bottom-0 left-0',
range: {
- start: { line: 10, character: 22 },
- end: { line: 10, character: 37 },
+ start: { line: 17, character: 20 },
+ end: { line: 17, character: 35 },
},
},
{
- classList:
- 'inset-0\n md:h-[calc(100%-2rem)]\n rounded-none\n ',
+ classList: `inset-0\n rounded-none\n `,
range: {
- start: { line: 12, character: 14 },
- end: { line: 14, character: 26 },
+ start: { line: 19, character: 12 },
+ // TODO: Fix the range calculation. Its wrong on this one
+ end: { line: 20, character: 24 },
},
},
+
+ // TODO: These duplicates are from matching nested clsx(…) and should be ignored
{
- classList: 'default',
+ classList: 'fixed',
range: {
- start: { line: 19, character: 19 },
- end: { line: 19, character: 26 },
- },
- },
- ]
-
- const content = `
- const variants = cva(
- cn(
- 'fixed flex',
- 'md:h-[calc(100%-2rem)]',
- 'bg-red-700',
- ),
- {
- variants: {
- mobile: {
- default: 'bottom-0 left-0',
- fullScreen: \`
- inset-0
- md:h-[calc(100%-2rem)]
- rounded-none
- \`,
- },
- },
- defaultVariants: {
- mobile: 'default',
- },
+ start: { line: 9, character: 5 },
+ end: { line: 9, character: 10 },
+ },
+ },
+ {
+ classList: 'absolute inset-0',
+ range: {
+ start: { line: 10, character: 5 },
+ end: { line: 10, character: 21 },
},
- )
- `
+ },
+ ])
+})
- const cnDoc = TextDocument.create('file://file.html', 'html', 1, content)
- const classLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+test('find class lists in nested fn calls (only nested matches)', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
- expect(classLists).toMatchObject(expectedResult)
-})
+ content: js`
+ let classes = cn(
+ 'flex',
+ cn({
+ 'bg-red-500': true,
+ 'text-white': Date.now() > 100,
+ }),
+ // NOTE: The only class lists appear inside this function because cn is
+ // not in the list of class functions
+ clsx(
+ 'fixed',
+ 'absolute inset-0'
+ ),
+ )
+ `,
+ })
-test('test classFunctions with tagged template literals', async ({ expect }) => {
- const state = getTailwindSettingsForClassFunctions()
- const classList = `pointer-events-auto relative flex bg-red-500
- items-center justify-between overflow-hidden
- md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm pl-md py-xs pr-xs gap-sm w-full
- data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]
- md:h-[calc(100%-2rem)]
- data-[swipe=move]:transition-none`
+ let classLists = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'html')
- const expectedResult: DocumentClassList[] = [
+ expect(classLists).toMatchObject([
{
- classList,
+ classList: 'fixed',
range: {
- start: { line: 2, character: 6 },
- end: { line: 7, character: 37 },
+ start: { line: 9, character: 5 },
+ end: { line: 9, character: 10 },
},
},
- ]
-
- const cnContent = `
- const tagged = cn\`
- ${classList}\`
- `
- const cnDoc = TextDocument.create('file://file.html', 'html', 1, cnContent)
- const cnClassLists = await findClassListsInHtmlRange(state, cnDoc, 'html')
+ {
+ classList: 'absolute inset-0',
+ range: {
+ start: { line: 10, character: 5 },
+ end: { line: 10, character: 21 },
+ },
+ },
+ ])
+})
- console.log('cnClassLists', JSON.stringify(cnClassLists, null, 2))
+test('find class lists in tagged template literals', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
+ content: js`
+ // These should match
+ let classes = clsx\`
+ flex p-4
+ block sm:p-0
+ \${Date.now() > 100 ? 'text-white' : 'text-black'}
+ \`
- expect(cnClassLists).toMatchObject(expectedResult)
+ // These should match
+ let classes = cva\`
+ flex p-4
+ block sm:p-0
+ \${Date.now() > 100 ? 'text-white' : 'text-black'}
+ \`
+ `,
+ })
- const cvaContent = `
- const tagged = cva\`
- ${classList}\`
- `
- const cvaDoc = TextDocument.create('file://file.html', 'html', 1, cvaContent)
- const cvaClassLists = await findClassListsInHtmlRange(state, cvaDoc, 'html')
+ let fileB = createDocument({
+ name: 'file.jsx',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['clsx', 'cva'],
+ },
+ },
+ content: js`
+ let classes = cn\`
+ flex p-4
+ block sm:p-0
+ \${Date.now() > 100 ? 'text-white' : 'text-black'}
+ \`
+ `,
+ })
- expect(cvaClassLists).toMatchObject(expectedResult)
+ let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+ let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js')
- // Ensure another tag name with the same layout doesn't match
- const cmaContent = `
- const tagged = cma\`
- ${classList}\`
- `
+ expect(classListsA).toEqual([
+ // from clsx`…`
+ {
+ classList: 'flex p-4\n block sm:p-0\n $',
+ range: {
+ start: { line: 2, character: 2 },
+ end: { line: 4, character: 3 },
+ },
+ },
+ {
+ classList: 'text-white',
+ range: {
+ start: { line: 4, character: 24 },
+ end: { line: 4, character: 34 },
+ },
+ },
+ {
+ classList: 'text-black',
+ range: {
+ start: { line: 4, character: 39 },
+ end: { line: 4, character: 49 },
+ },
+ },
- const cmaDoc = TextDocument.create('file://file.html', 'html', 1, cmaContent)
- const cmaClassLists = await findClassListsInHtmlRange(state, cmaDoc, 'html')
+ // from cva`…`
+ {
+ classList: 'flex p-4\n block sm:p-0\n $',
+ range: {
+ start: { line: 9, character: 2 },
+ end: { line: 11, character: 3 },
+ },
+ },
+ {
+ classList: 'text-white',
+ range: {
+ start: { line: 11, character: 24 },
+ end: { line: 11, character: 34 },
+ },
+ },
+ {
+ classList: 'text-black',
+ range: {
+ start: { line: 11, character: 39 },
+ end: { line: 11, character: 49 },
+ },
+ },
+ ])
- expect(cmaClassLists).toMatchObject([])
+ // none from cn`…` since it's not in the list of class functions
+ expect(classListsB).toEqual([])
})
-function getTailwindSettingsForClassFunctions(): Parameters[0] {
- const defaultSettings = getDefaultTailwindSettings()
- return {
+function createDocument({
+ name,
+ lang,
+ content,
+ settings,
+}: {
+ name: string
+ lang: string
+ content: string | string[]
+ settings: DeepPartial
+}) {
+ let doc = TextDocument.create(
+ `file://${name}`,
+ lang,
+ 1,
+ typeof content === 'string' ? content : content.join('\n'),
+ )
+ let defaults = getDefaultTailwindSettings()
+ let state = createState({
editor: {
+ // @ts-ignore
getConfiguration: async () => ({
- ...defaultSettings,
+ ...defaults,
+ ...settings,
tailwindCSS: {
- ...defaultSettings.tailwindCSS,
- classFunctions: ['cva', 'cn'],
+ ...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,
},
}),
},
+ })
+
+ return {
+ doc,
+ state,
}
}
From 878b18dcf3760167a13bdc75ff763e9c2f8b342a Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 12:50:29 -0400
Subject: [PATCH 17/32] wip
---
pnpm-lock.yaml | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f1048aaa..2378a376 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -300,6 +300,9 @@ importers:
'@types/css.escape':
specifier: ^1.5.2
version: 1.5.2
+ '@types/dedent':
+ specifier: ^0.7.2
+ version: 0.7.2
'@types/line-column':
specifier: ^1.0.2
version: 1.0.2
@@ -309,6 +312,9 @@ importers:
'@types/stringify-object':
specifier: ^4.0.5
version: 4.0.5
+ dedent:
+ specifier: ^1.5.3
+ version: 1.5.3
esbuild:
specifier: ^0.25.0
version: 0.25.0
@@ -969,6 +975,9 @@ packages:
'@types/debounce@1.2.0':
resolution: {integrity: sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==}
+ '@types/dedent@0.7.2':
+ resolution: {integrity: sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==}
+
'@types/dlv@1.1.4':
resolution: {integrity: sha512-m8KmImw4Jt+4rIgupwfivrWEOnj1LzkmKkqbh075uG13eTQ1ZxHWT6T0vIdSQhLIjQCiR0n0lZdtyDOPO1x2Mw==}
@@ -3227,6 +3236,8 @@ snapshots:
'@types/debounce@1.2.0': {}
+ '@types/dedent@0.7.2': {}
+
'@types/dlv@1.1.4': {}
'@types/estree@1.0.6': {}
From a1375d8cafa55002e139a1bde9ffa95eee738e91 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 12:52:39 -0400
Subject: [PATCH 18/32] wip
---
packages/tailwindcss-language-service/src/util/state.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index d548b270..3bdb1bc4 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -211,6 +211,9 @@ export function getDefaultTailwindSettings(): Settings {
}
}
+/**
+ * @internal
+ */
export function createState(
partial: Omit, 'editor'> & {
editor?: Partial
From 15ece2dfbaeef60a1d971aaa949708c8f11621cb Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 13:10:36 -0400
Subject: [PATCH 19/32] wip
---
.../src/util/find.test.ts | 77 +++++++++++++++++--
1 file changed, 71 insertions(+), 6 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index cc8ff013..c28a4277 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -59,7 +59,7 @@ test('class regex works in astro', async ({ expect }) => {
test('find class lists in functions', async ({ expect }) => {
let fileA = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -84,7 +84,7 @@ test('find class lists in functions', async ({ expect }) => {
let fileB = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -171,7 +171,7 @@ test('find class lists in functions', async ({ expect }) => {
test('find class lists in nested fn calls', async ({ expect }) => {
let fileA = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -303,7 +303,7 @@ test('find class lists in nested fn calls', async ({ expect }) => {
test('find class lists in nested fn calls (only nested matches)', async ({ expect }) => {
let fileA = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -350,7 +350,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec
test('find class lists in tagged template literals', async ({ expect }) => {
let fileA = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -375,7 +375,7 @@ test('find class lists in tagged template literals', async ({ expect }) => {
let fileB = createDocument({
name: 'file.jsx',
- lang: 'javascript',
+ lang: 'javascriptreact',
settings: {
tailwindCSS: {
classFunctions: ['clsx', 'cva'],
@@ -445,6 +445,71 @@ test('find class lists in tagged template literals', async ({ expect }) => {
expect(classListsB).toEqual([])
})
+test('classFunctions can be a regex', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascriptreact',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['tw\\.[a-z]+'],
+ },
+ },
+ content: js`
+ let classes = tw.div('flex p-4')
+ `,
+ })
+
+ let fileB = createDocument({
+ name: 'file.jsx',
+ lang: 'javascriptreact',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['tw\\.[a-z]+'],
+ },
+ },
+ content: js`
+ let classes = tw.div.foo('flex p-4')
+ `,
+ })
+
+ let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+ let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js')
+
+ expect(classListsA).toEqual([
+ {
+ classList: 'flex p-4',
+ range: {
+ start: { line: 0, character: 22 },
+ end: { line: 0, character: 30 },
+ },
+ },
+ ])
+
+ // none from tw.div.foo(`…`) since it does not match a class function
+ expect(classListsB).toEqual([])
+})
+
+test('classFunctions regexes only match on function names', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascriptreact',
+ settings: {
+ tailwindCSS: {
+ // A function name itself cannot contain a `:`
+ classFunctions: [':\\s*tw\\.[a-z]+'],
+ },
+ },
+ content: js`
+ let classes = tw.div('flex p-4')
+ let classes = { foo: tw.div('flex p-4') }
+ `,
+ })
+
+ let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+
+ expect(classListsA).toEqual([])
+})
+
function createDocument({
name,
lang,
From 9c9b63a438046da0827f08f4f3a7704bdf755b3b Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 16:28:26 +0200
Subject: [PATCH 20/32] fix: DeepPartial type issues
---
packages/tailwindcss-language-service/src/types.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/types.ts b/packages/tailwindcss-language-service/src/types.ts
index 6be62396..c84f0c7a 100644
--- a/packages/tailwindcss-language-service/src/types.ts
+++ b/packages/tailwindcss-language-service/src/types.ts
@@ -1,8 +1,8 @@
export type DeepPartial = {
- [P in keyof T]?: T[P] extends (infer U)[]
- ? U[]
- : T[P] extends (...args: any) => any
- ? T[P] | undefined
+ [P in keyof T]?: T[P] extends ((...args: any) => any) | ReadonlyArray | Date
+ ? T[P]
+ : T[P] extends (infer U)[]
+ ? U[]
: T[P] extends object
? DeepPartial
: T[P]
From 9f2f840c239f8ef749a62ed5955e2bc2c3eb62fd Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 16:31:12 +0200
Subject: [PATCH 21/32] fix: ts config issues in @tailwindcss/language-service
---
packages/tailwindcss-language-service/scripts/build.mjs | 8 +++++++-
.../tailwindcss-language-service/src/util/find.test.ts | 1 -
packages/tailwindcss-language-service/tsconfig.json | 2 +-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/packages/tailwindcss-language-service/scripts/build.mjs b/packages/tailwindcss-language-service/scripts/build.mjs
index 913debc3..76f4bef9 100644
--- a/packages/tailwindcss-language-service/scripts/build.mjs
+++ b/packages/tailwindcss-language-service/scripts/build.mjs
@@ -27,14 +27,20 @@ let build = await esbuild.context({
name: 'generate-types',
async setup(build) {
build.onEnd(async (result) => {
+ const distPath = path.resolve(__dirname, '../dist')
+
// Call the tsc command to generate the types
spawnSync(
'tsc',
- ['--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')],
+ ['--emitDeclarationOnly', '--outDir', distPath],
{
stdio: 'inherit',
},
)
+ // Remove all .test.d.ts file definitions
+ spawnSync('find', [distPath, '-name', '*.test.d.ts', '-delete'], {
+ stdio: 'inherit',
+ })
})
},
},
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index c28a4277..d568f0b6 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -530,7 +530,6 @@ function createDocument({
let defaults = getDefaultTailwindSettings()
let state = createState({
editor: {
- // @ts-ignore
getConfiguration: async () => ({
...defaults,
...settings,
diff --git a/packages/tailwindcss-language-service/tsconfig.json b/packages/tailwindcss-language-service/tsconfig.json
index 605ece3c..883356e7 100644
--- a/packages/tailwindcss-language-service/tsconfig.json
+++ b/packages/tailwindcss-language-service/tsconfig.json
@@ -1,6 +1,5 @@
{
"include": ["src", "../../types"],
- "exclude": ["src/**/*.test.ts"],
"compilerOptions": {
"module": "NodeNext",
"lib": ["ES2022"],
@@ -15,6 +14,7 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "NodeNext",
+ "skipLibCheck": true,
"jsx": "react",
"esModuleInterop": true
}
From 3baeb5b69a24f5e74a5e4af3ddfbab7bfb6a8a5f Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 16:35:36 +0200
Subject: [PATCH 22/32] fix: matchClassFunctions isClassFn RegExp flags
---
packages/tailwindcss-language-service/src/util/find.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index b0ab3bf3..d994c623 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -184,7 +184,7 @@ export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatc
// 3. Match against the function names in the document
let re = /^(NAMES)$/
- let isClassFn = new RegExp(re.source.replace('NAMES', names.join('|')), 'dgi')
+ let isClassFn = new RegExp(re.source.replace('NAMES', names.join('|')), 'i')
let matches = foundFns.filter((fn) => isClassFn.test(fn[1]))
From 0977df10148dbb973f399f4a9b50d45ba0e112a4 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 16:36:44 +0200
Subject: [PATCH 23/32] feat: classFunctions & classProperties should not
duplicate matches
---
.../src/util/find.test.ts | 67 +++++++++++----
.../src/util/find.ts | 84 ++++++++++---------
2 files changed, 97 insertions(+), 54 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index d568f0b6..662d8113 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -281,22 +281,6 @@ test('find class lists in nested fn calls', async ({ expect }) => {
end: { line: 20, character: 24 },
},
},
-
- // TODO: These duplicates are from matching nested clsx(…) and should be ignored
- {
- classList: 'fixed',
- range: {
- start: { line: 9, character: 5 },
- end: { line: 9, character: 10 },
- },
- },
- {
- classList: 'absolute inset-0',
- range: {
- start: { line: 10, character: 5 },
- end: { line: 10, character: 21 },
- },
- },
])
})
@@ -510,6 +494,57 @@ test('classFunctions regexes only match on function names', async ({ expect }) =
expect(classListsA).toEqual([])
})
+test('classFunctions & classProperties should not duplicate matches', async ({ expect }) => {
+ let fileA = createDocument({
+ name: 'file.jsx',
+ lang: 'javascriptreact',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['cva', 'clsx'],
+ },
+ },
+ content: js`
+ const Component = ({ className }) => (
+
+ CONTENT
+
+ )
+ `,
+ })
+
+ let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+
+ expect(classListsA).toEqual([
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 3, character: 7 },
+ end: { line: 3, character: 20 },
+ },
+ },
+ {
+ classList: 'inset-0 md:h-[calc(100%-2rem)]',
+ range: {
+ start: { line: 4, character: 7 },
+ end: { line: 4, character: 37 },
+ },
+ },
+ {
+ classList: 'rounded-none bg-blue-700',
+ range: {
+ start: { line: 5, character: 12 },
+ end: { line: 5, character: 36 },
+ },
+ },
+ ])
+})
+
function createDocument({
name,
lang,
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index d994c623..ec96a842 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -208,7 +208,8 @@ export async function findClassListsInHtmlRange(
matches.push(...matchClassFunctions(text, settings.classFunctions))
}
- const result: DocumentClassList[] = []
+ const existingResultSet = new Set()
+ const results: DocumentClassList[] = []
matches.forEach((match) => {
const subtext = text.substr(match.index + match[0].length - 1)
@@ -253,46 +254,53 @@ export async function findClassListsInHtmlRange(
})
}
- result.push(
- ...classLists
- .map(({ value, offset }) => {
- if (value.trim() === '') {
- return null
- }
+ classLists.forEach(({ value, offset }) => {
+ if (value.trim() === '') {
+ return null
+ }
- const before = value.match(/^\s*/)
- const beforeOffset = before === null ? 0 : before[0].length
- const after = value.match(/\s*$/)
- const afterOffset = after === null ? 0 : -after[0].length
-
- const start = indexToPosition(
- text,
- match.index + match[0].length - 1 + offset + beforeOffset,
- )
- const end = indexToPosition(
- text,
- match.index + match[0].length - 1 + offset + value.length + afterOffset,
- )
-
- return {
- 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,
- },
- },
- }
- })
- .filter((x) => x !== null),
- )
+ const before = value.match(/^\s*/)
+ const beforeOffset = before === null ? 0 : before[0].length
+ const after = value.match(/\s*$/)
+ const afterOffset = after === null ? 0 : -after[0].length
+
+ const start = indexToPosition(text, match.index + match[0].length - 1 + offset + beforeOffset)
+ const end = indexToPosition(
+ text,
+ match.index + match[0].length - 1 + offset + value.length + afterOffset,
+ )
+
+ 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,
+ },
+ },
+ }
+
+ const resultKey = [
+ result.classList,
+ result.range.start.line,
+ result.range.start.character,
+ result.range.end.line,
+ result.range.end.character,
+ ].join(':')
+
+ // No need to add the result if it was already matched
+ if (!existingResultSet.has(resultKey)) {
+ existingResultSet.add(resultKey)
+ results.push(result)
+ }
+ })
})
- return result
+ return results
}
export async function findClassListsInRange(
From 8706e3a5f24c6ca7c55f3bfaf526267a41d479b4 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 17:22:34 +0200
Subject: [PATCH 24/32] feat: ensure same matches in a different spot are
pushed to results in find.test.ts
---
.../src/util/find.test.ts | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index 662d8113..db6d7e34 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -500,6 +500,7 @@ test('classFunctions & classProperties should not duplicate matches', async ({ e
lang: 'javascriptreact',
settings: {
tailwindCSS: {
+ classAttributes: ['className'],
classFunctions: ['cva', 'clsx'],
},
},
@@ -515,6 +516,17 @@ test('classFunctions & classProperties should not duplicate matches', async ({ e
CONTENT
)
+ const OtherComponent = ({ className }) => (
+
+ CONTENT
+
+ )
`,
})
@@ -542,6 +554,27 @@ test('classFunctions & classProperties should not duplicate matches', async ({ e
end: { line: 5, character: 36 },
},
},
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 14, character: 7 },
+ end: { line: 14, character: 20 },
+ },
+ },
+ {
+ classList: 'inset-0 md:h-[calc(100%-2rem)]',
+ range: {
+ start: { line: 15, character: 7 },
+ end: { line: 15, character: 37 },
+ },
+ },
+ {
+ classList: 'rounded-none bg-blue-700',
+ range: {
+ start: { line: 16, character: 12 },
+ end: { line: 16, character: 36 },
+ },
+ },
])
})
From 3514bb6fc5c159fb9db77822ac25f9938131a9b7 Mon Sep 17 00:00:00 2001
From: Laurynas Grigutis
Date: Sun, 16 Mar 2025 20:06:52 +0200
Subject: [PATCH 25/32] fix: use extended tsconfig instead of find *.test.d.ts
-delete to remove definitions
---
packages/tailwindcss-language-service/scripts/build.mjs | 8 +-------
.../scripts/type-gen.tsconfig.json | 4 ++++
2 files changed, 5 insertions(+), 7 deletions(-)
create mode 100644 packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json
diff --git a/packages/tailwindcss-language-service/scripts/build.mjs b/packages/tailwindcss-language-service/scripts/build.mjs
index 76f4bef9..b0c462d8 100644
--- a/packages/tailwindcss-language-service/scripts/build.mjs
+++ b/packages/tailwindcss-language-service/scripts/build.mjs
@@ -27,20 +27,14 @@ let build = await esbuild.context({
name: 'generate-types',
async setup(build) {
build.onEnd(async (result) => {
- const distPath = path.resolve(__dirname, '../dist')
-
// Call the tsc command to generate the types
spawnSync(
'tsc',
- ['--emitDeclarationOnly', '--outDir', distPath],
+ ['-p', path.resolve(__dirname, './type-gen.tsconfig.json'), '--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')],
{
stdio: 'inherit',
},
)
- // Remove all .test.d.ts file definitions
- spawnSync('find', [distPath, '-name', '*.test.d.ts', '-delete'], {
- stdio: 'inherit',
- })
})
},
},
diff --git a/packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json b/packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json
new file mode 100644
index 00000000..e80bb38f
--- /dev/null
+++ b/packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../tsconfig.json",
+ "exclude": ["../src/**/*.test.ts"]
+}
\ No newline at end of file
From 86dace379465a9caaf27df229b0001ac78dc7179 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 13 Mar 2025 17:53:48 -0400
Subject: [PATCH 26/32] Remove `resolveRange` fn
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
It’s _identical_ to `absoluteRange` which is more used
---
.../src/util/find.ts | 6 +++---
.../src/util/resolveRange.ts | 16 ----------------
2 files changed, 3 insertions(+), 19 deletions(-)
delete mode 100644 packages/tailwindcss-language-service/src/util/resolveRange.ts
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index ec96a842..37a9c043 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -9,7 +9,7 @@ import { isJsxContext } from './js'
import { dedupeByRange, flatten } from './array'
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries'
-import { resolveRange } from './resolveRange'
+import { absoluteRange } from './absoluteRange'
import { getTextWithoutComments } from './doc'
import { isSemicolonlessCssLanguage } from './languages'
import { customClassesIn } from './classes'
@@ -446,14 +446,14 @@ export function findHelperFunctionsInRange(
helper,
path,
ranges: {
- full: resolveRange(
+ full: absoluteRange(
{
start: indexToPosition(text, startIndex),
end: indexToPosition(text, startIndex + match.groups.path.length),
},
range,
),
- path: resolveRange(
+ path: absoluteRange(
{
start: indexToPosition(text, startIndex + quotesBefore.length),
end: indexToPosition(text, startIndex + quotesBefore.length + path.length),
diff --git a/packages/tailwindcss-language-service/src/util/resolveRange.ts b/packages/tailwindcss-language-service/src/util/resolveRange.ts
deleted file mode 100644
index d90fa5b9..00000000
--- a/packages/tailwindcss-language-service/src/util/resolveRange.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Range } from 'vscode-languageserver'
-
-export function resolveRange(range: Range, relativeTo?: Range) {
- return {
- start: {
- line: (relativeTo?.start.line || 0) + range.start.line,
- character:
- (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) + range.start.character,
- },
- end: {
- line: (relativeTo?.start.line || 0) + range.end.line,
- character:
- (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) + range.end.character,
- },
- }
-}
From 44ba4b9bf9f188c01c2f1507ca7099d534fdc1a7 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Sun, 16 Mar 2025 15:23:47 -0400
Subject: [PATCH 27/32] Rename config file
---
packages/tailwindcss-language-service/scripts/build.mjs | 2 +-
.../scripts/{type-gen.tsconfig.json => tsconfig.build.json} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename packages/tailwindcss-language-service/scripts/{type-gen.tsconfig.json => tsconfig.build.json} (100%)
diff --git a/packages/tailwindcss-language-service/scripts/build.mjs b/packages/tailwindcss-language-service/scripts/build.mjs
index b0c462d8..128426be 100644
--- a/packages/tailwindcss-language-service/scripts/build.mjs
+++ b/packages/tailwindcss-language-service/scripts/build.mjs
@@ -30,7 +30,7 @@ let build = await esbuild.context({
// Call the tsc command to generate the types
spawnSync(
'tsc',
- ['-p', path.resolve(__dirname, './type-gen.tsconfig.json'), '--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')],
+ ['-p', path.resolve(__dirname, './tsconfig.build.json'), '--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')],
{
stdio: 'inherit',
},
diff --git a/packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json b/packages/tailwindcss-language-service/scripts/tsconfig.build.json
similarity index 100%
rename from packages/tailwindcss-language-service/scripts/type-gen.tsconfig.json
rename to packages/tailwindcss-language-service/scripts/tsconfig.build.json
From d2b0eed667caa04beaa866a76f79c0ee2ef71c71 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Sun, 16 Mar 2025 15:32:24 -0400
Subject: [PATCH 28/32] Add test
---
.../src/util/find.test.ts | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index db6d7e34..d89c6952 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -494,6 +494,51 @@ test('classFunctions regexes only match on function names', async ({ expect }) =
expect(classListsA).toEqual([])
})
+test('Finds consecutive instances of a class function', async ({ expect }) => {
+ let file = createDocument({
+ name: 'file.js',
+ lang: 'javascript',
+ settings: {
+ tailwindCSS: {
+ classFunctions: ['cn'],
+ },
+ },
+ content: js`
+ export const list = [
+ cn('relative flex bg-red-500'),
+ cn('relative flex bg-red-500'),
+ cn('relative flex bg-red-500'),
+ ]
+ `,
+ })
+
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js')
+
+ expect(classLists).toEqual([
+ {
+ classList: 'relative flex bg-red-500',
+ range: {
+ start: { line: 1, character: 6 },
+ end: { line: 1, character: 30 },
+ },
+ },
+ {
+ classList: 'relative flex bg-red-500',
+ range: {
+ start: { line: 2, character: 6 },
+ end: { line: 2, character: 30 },
+ },
+ },
+ {
+ classList: 'relative flex bg-red-500',
+ range: {
+ start: { line: 3, character: 6 },
+ end: { line: 3, character: 30 },
+ },
+ },
+ ])
+})
+
test('classFunctions & classProperties should not duplicate matches', async ({ expect }) => {
let fileA = createDocument({
name: 'file.jsx',
From a859e4b9445209c2cad365f2a247da8891db070d Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Sun, 16 Mar 2025 15:34:57 -0400
Subject: [PATCH 29/32] Cleanup
---
.../src/util/find.test.ts | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index d89c6952..c720e62f 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -8,7 +8,7 @@ import dedent from 'dedent'
const js = dedent
test('class regex works in astro', async ({ expect }) => {
- let { doc, state } = createDocument({
+ let file = createDocument({
name: 'file.astro',
lang: 'astro',
settings: {
@@ -29,7 +29,7 @@ test('class regex works in astro', async ({ expect }) => {
],
})
- let classLists = await findClassListsInHtmlRange(state, doc, 'html')
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html')
expect(classLists).toEqual([
{
@@ -169,7 +169,7 @@ test('find class lists in functions', async ({ expect }) => {
})
test('find class lists in nested fn calls', async ({ expect }) => {
- let fileA = createDocument({
+ let file = createDocument({
name: 'file.jsx',
lang: 'javascriptreact',
settings: {
@@ -209,7 +209,7 @@ test('find class lists in nested fn calls', async ({ expect }) => {
`,
})
- let classLists = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'html')
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html')
expect(classLists).toMatchObject([
{
@@ -285,7 +285,7 @@ test('find class lists in nested fn calls', async ({ expect }) => {
})
test('find class lists in nested fn calls (only nested matches)', async ({ expect }) => {
- let fileA = createDocument({
+ let file = createDocument({
name: 'file.jsx',
lang: 'javascriptreact',
settings: {
@@ -311,7 +311,7 @@ test('find class lists in nested fn calls (only nested matches)', async ({ expec
`,
})
- let classLists = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'html')
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html')
expect(classLists).toMatchObject([
{
@@ -474,7 +474,7 @@ test('classFunctions can be a regex', async ({ expect }) => {
})
test('classFunctions regexes only match on function names', async ({ expect }) => {
- let fileA = createDocument({
+ let file = createDocument({
name: 'file.jsx',
lang: 'javascriptreact',
settings: {
@@ -489,9 +489,9 @@ test('classFunctions regexes only match on function names', async ({ expect }) =
`,
})
- let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js')
- expect(classListsA).toEqual([])
+ expect(classLists).toEqual([])
})
test('Finds consecutive instances of a class function', async ({ expect }) => {
@@ -540,7 +540,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => {
})
test('classFunctions & classProperties should not duplicate matches', async ({ expect }) => {
- let fileA = createDocument({
+ let file = createDocument({
name: 'file.jsx',
lang: 'javascriptreact',
settings: {
@@ -575,9 +575,9 @@ test('classFunctions & classProperties should not duplicate matches', async ({ e
`,
})
- let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js')
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js')
- expect(classListsA).toEqual([
+ expect(classLists).toEqual([
{
classList: 'relative flex',
range: {
From 0eb371261e8240983c4cad671668bbfe99fd3f52 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 19 Mar 2025 09:52:31 -0400
Subject: [PATCH 30/32] Fix test name
---
packages/tailwindcss-language-service/src/util/find.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index c720e62f..ca839a58 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -539,7 +539,7 @@ test('Finds consecutive instances of a class function', async ({ expect }) => {
])
})
-test('classFunctions & classProperties should not duplicate matches', async ({ expect }) => {
+test('classFunctions & classAttributes should not duplicate matches', async ({ expect }) => {
let file = createDocument({
name: 'file.jsx',
lang: 'javascriptreact',
From 58df6d903e422893ff498796d38eed740555e6d0 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 19 Mar 2025 09:23:09 -0400
Subject: [PATCH 31/32] Update readme
---
packages/vscode-tailwindcss/README.md | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/packages/vscode-tailwindcss/README.md b/packages/vscode-tailwindcss/README.md
index 62165308..efc937cc 100644
--- a/packages/vscode-tailwindcss/README.md
+++ b/packages/vscode-tailwindcss/README.md
@@ -90,6 +90,26 @@ Enable completions when using [Emmet](https://emmet.io/)-style syntax, for examp
The HTML attributes for which to provide class completions, hover previews, linting etc. **Default: `class`, `className`, `ngClass`, `class:list`**
+### `tailwindCSS.classFunctions`
+
+Functions in which to provide completions, hover previews, linting etc. Currently, this works for both function calls and tagged template literals in JavaScript / TypeScript.
+
+Example:
+
+```json
+{
+ "tailwindCSS.classFunctions": ["tw", "clsx"]
+}
+```
+
+```javascript
+let classes = tw`flex bg-red-500`
+let classes2 = clsx([
+ "flex bg-red-500",
+ { "text-red-500": true }
+])
+```
+
### `tailwindCSS.colorDecorators`
Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions. **Default: `true`**
From 5902f5d0f96007cb7d2afa2213380de0a032ce98 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 19 Mar 2025 09:52:22 -0400
Subject: [PATCH 32/32] Limit class function matching to JS language boundaries
---
.../src/completionProvider.ts | 21 +++++-
.../src/util/find.test.ts | 66 +++++++++++++++++++
.../src/util/find.ts | 18 ++++-
3 files changed, 101 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts
index 7343c1e9..fe43bb68 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -45,6 +45,8 @@ import type { ThemeEntry } from './util/v4'
import { segment } from './util/segment'
import { resolveKnownThemeKeys, resolveKnownThemeNamespaces } from './util/v4/theme-keys'
import { SEARCH_RANGE } from './util/constants'
+import { getLanguageBoundaries } from './util/getLanguageBoundaries'
+import { isWithinRange } from './util/isWithinRange'
let isUtil = (className) =>
Array.isArray(className.__info)
@@ -747,8 +749,23 @@ async function provideClassAttributeCompletions(
let matches = matchClassAttributes(str, settings.classAttributes)
- if (settings.classFunctions?.length) {
- matches.push(...matchClassFunctions(str, settings.classFunctions))
+ let boundaries = getLanguageBoundaries(state, document)
+
+ for (let boundary of boundaries ?? []) {
+ let isJsContext = boundary.type === 'js' || boundary.type === 'jsx'
+ if (!isJsContext) continue
+ if (!settings.classFunctions?.length) continue
+ if (!isWithinRange(position, boundary.range)) continue
+
+ let str = document.getText(boundary.range)
+ let offset = document.offsetAt(boundary.range.start)
+ let fnMatches = matchClassFunctions(str, settings.classFunctions)
+
+ fnMatches.forEach((match) => {
+ if (match.index) match.index += offset
+ })
+
+ matches.push(...fnMatches)
}
if (matches.length === 0) {
diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts
index ca839a58..f9f17a96 100644
--- a/packages/tailwindcss-language-service/src/util/find.test.ts
+++ b/packages/tailwindcss-language-service/src/util/find.test.ts
@@ -6,6 +6,7 @@ import type { DeepPartial } from '../types'
import dedent from 'dedent'
const js = dedent
+const html = dedent
test('class regex works in astro', async ({ expect }) => {
let file = createDocument({
@@ -623,6 +624,71 @@ test('classFunctions & classAttributes should not duplicate matches', async ({ e
])
})
+test('classFunctions should only match in JS-like contexts', async ({ expect }) => {
+ let file = createDocument({
+ name: 'file.html',
+ lang: 'html',
+ settings: {
+ tailwindCSS: {
+ classAttributes: ['className'],
+ classFunctions: ['clsx'],
+ },
+ },
+ content: html`
+
+ clsx('relative flex') clsx('relative flex')
+
+
+
+
+
+ clsx('relative flex') clsx('relative flex')
+
+
+
+ `,
+ })
+
+ let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js')
+
+ expect(classLists).toEqual([
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 5, character: 16 },
+ end: { line: 5, character: 29 },
+ },
+ },
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 6, character: 16 },
+ end: { line: 6, character: 29 },
+ },
+ },
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 14, character: 16 },
+ end: { line: 14, character: 29 },
+ },
+ },
+ {
+ classList: 'relative flex',
+ range: {
+ start: { line: 15, character: 16 },
+ end: { line: 15, character: 29 },
+ },
+ },
+ ])
+})
+
function createDocument({
name,
lang,
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 37a9c043..03218798 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -204,8 +204,22 @@ export async function findClassListsInHtmlRange(
const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS
const matches = matchClassAttributes(text, settings.classAttributes)
- if (settings.classFunctions?.length) {
- matches.push(...matchClassFunctions(text, settings.classFunctions))
+ 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)
}
const existingResultSet = new Set()