Skip to content

Add tailwindCSS.classFunctions setting #1258

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
39b4a94
Added "tailwindCSS.experimental.classFunctions" option
Mar 6, 2025
baee7b5
Use optional chaining to access classFunctions
Mar 7, 2025
dc5a298
Added tests for "tailwindCSS.experimental.classFunctions"
Mar 7, 2025
0e227e9
Removed settings duplication: Created getDefaultTailwindSettings in @…
Mar 7, 2025
100cad7
Use getDefaultTailwindSettings in find.test.ts
Mar 7, 2025
41cc24a
Fixed findClassListsInHtmlRange state type & removed type casting in …
Mar 7, 2025
3b4e04c
Changed getDefaultTailwindSettings to return const object that satisf…
Mar 7, 2025
7d059da
Moved classFunctions option out of experimental
Mar 7, 2025
dc441bc
Added support for tagged template literals
Mar 7, 2025
43fab13
wip
thecrypticace Mar 12, 2025
9ee8b19
wip
thecrypticace Mar 12, 2025
94f1300
wip
thecrypticace Mar 12, 2025
e29481d
wip
thecrypticace Mar 12, 2025
23ccf9c
Move types
thecrypticace Mar 13, 2025
481d270
wip
thecrypticace Mar 13, 2025
b214092
Rewrite tests
thecrypticace Mar 13, 2025
878b18d
wip
thecrypticace Mar 13, 2025
a1375d8
wip
thecrypticace Mar 13, 2025
15ece2d
wip
thecrypticace Mar 13, 2025
9c9b63a
fix: DeepPartial type issues
Mar 16, 2025
9f2f840
fix: ts config issues in @tailwindcss/language-service
Mar 16, 2025
3baeb5b
fix: matchClassFunctions isClassFn RegExp flags
Mar 16, 2025
0977df1
feat: classFunctions & classProperties should not duplicate matches
Mar 16, 2025
8706e3a
feat: ensure same matches in a different spot are pushed to results i…
Mar 16, 2025
3514bb6
fix: use extended tsconfig instead of find *.test.d.ts -delete to rem…
Mar 16, 2025
86dace3
Remove `resolveRange` fn
thecrypticace Mar 13, 2025
44ba4b9
Rename config file
thecrypticace Mar 16, 2025
d2b0eed
Add test
thecrypticace Mar 16, 2025
a859e4b
Cleanup
thecrypticace Mar 16, 2025
0eb3712
Fix test name
thecrypticace Mar 19, 2025
58df6d9
Update readme
thecrypticace Mar 19, 2025
5902f5d
Limit class function matching to JS language boundaries
thecrypticace Mar 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/tailwindcss-language-service/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
})
},
},
Expand Down
8 changes: 4 additions & 4 deletions packages/tailwindcss-language-service/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export type DeepPartial<T> = {
[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<any> | Date
? T[P]
: T[P] extends (infer U)[]
? U[]
: T[P] extends object
? DeepPartial<T[P]>
: T[P]
Expand Down
101 changes: 84 additions & 17 deletions packages/tailwindcss-language-service/src/util/find.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
},
])
})

Expand Down Expand Up @@ -510,6 +494,90 @@ 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: {
classAttributes: ['className'],
classFunctions: ['cva', 'clsx'],
},
},
content: js`
const Component = ({ className }) => (
<div
className={clsx(
'relative flex',
'inset-0 md:h-[calc(100%-2rem)]',
clsx('rounded-none bg-blue-700', className),
)}
>
CONTENT
</div>
)
const OtherComponent = ({ className }) => (
<div
className={clsx(
'relative flex',
'inset-0 md:h-[calc(100%-2rem)]',
clsx('rounded-none bg-blue-700', className),
)}
>
CONTENT
</div>
)
`,
})

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 },
},
},
{
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 },
},
},
])
})

function createDocument({
name,
lang,
Expand All @@ -530,7 +598,6 @@ function createDocument({
let defaults = getDefaultTailwindSettings()
let state = createState({
editor: {
// @ts-ignore
getConfiguration: async () => ({
...defaults,
...settings,
Expand Down
86 changes: 47 additions & 39 deletions packages/tailwindcss-language-service/src/util/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand All @@ -208,7 +208,8 @@ export async function findClassListsInHtmlRange(
matches.push(...matchClassFunctions(text, settings.classFunctions))
}

const result: DocumentClassList[] = []
const existingResultSet = new Set<string>()
const results: DocumentClassList[] = []

matches.forEach((match) => {
const subtext = text.substr(match.index + match[0].length - 1)
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-service/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"include": ["src", "../../types"],
"exclude": ["src/**/*.test.ts"],
"compilerOptions": {
"module": "NodeNext",
"lib": ["ES2022"],
Expand All @@ -15,6 +14,7 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "NodeNext",
"skipLibCheck": true,
"jsx": "react",
"esModuleInterop": true
}
Expand Down