Skip to content

Commit ab5c8ba

Browse files
committed
Handle balanced parens when searching for helper functions
1 parent 583cbd4 commit ab5c8ba

File tree

1 file changed

+132
-34
lines changed
  • packages/tailwindcss-language-service/src/util

1 file changed

+132
-34
lines changed

packages/tailwindcss-language-service/src/util/find.ts

Lines changed: 132 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,12 @@ export function findHelperFunctionsInRange(
403403
doc: TextDocument,
404404
range?: Range,
405405
): DocumentHelperFunction[] {
406-
const text = getTextWithoutComments(doc, 'css', range)
407-
let matches = findAll(
408-
/(?<prefix>[\W])(?<helper>config|theme|--theme|var)(?<innerPrefix>\(\s*)(?<path>[^)]*?)\s*\)/g,
409-
text,
410-
)
406+
let text = getTextWithoutComments(doc, 'css', range)
407+
408+
// Find every instance of a helper function
409+
let matches = findAll(/\b(?<helper>config|theme|--theme|var)\(/g, text)
411410

412-
// Eliminate matches that are on an `@import`
411+
// Eliminate matches that are attached to an `@import`
413412
matches = matches.filter((match) => {
414413
// Scan backwards to see if we're in an `@import` statement
415414
for (let i = match.index - 1; i >= 0; i--) {
@@ -427,58 +426,157 @@ export function findHelperFunctionsInRange(
427426
return true
428427
})
429428

430-
return matches.map((match) => {
431-
let quotesBefore = ''
432-
let path = match.groups.path
433-
let commaIndex = getFirstCommaIndex(path)
434-
if (commaIndex !== null) {
435-
path = path.slice(0, commaIndex).trimEnd()
436-
}
437-
path = path.replace(/['"]+$/, '').replace(/^['"]+/, (m) => {
438-
quotesBefore = m
439-
return ''
440-
})
441-
let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/)
442-
if (matches) {
443-
path = matches[1]
429+
let fns: DocumentHelperFunction[] = []
430+
431+
// Collect the first argument of each fn accounting for balanced params
432+
const COMMA = 0x2c
433+
const SLASH = 0x2f
434+
const BACKSLASH = 0x5c
435+
const OPEN_PAREN = 0x28
436+
const CLOSE_PAREN = 0x29
437+
const DOUBLE_QUOTE = 0x22
438+
const SINGLE_QUOTE = 0x27
439+
440+
let len = text.length
441+
442+
for (let match of matches) {
443+
let argsStart = match.index + match[0].length
444+
let argsEnd = null
445+
let pathStart = argsStart
446+
let pathEnd = null
447+
let depth = 1
448+
449+
// Scan until we find a `,` or balanced `)` not in quotes
450+
for (let idx = argsStart; idx < len; ++idx) {
451+
let char = text.charCodeAt(idx)
452+
453+
if (char === BACKSLASH) {
454+
idx += 1
455+
}
456+
457+
//
458+
else if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE) {
459+
while (++idx < len) {
460+
let nextChar = text.charCodeAt(idx)
461+
if (nextChar === BACKSLASH) {
462+
idx += 1
463+
continue
464+
}
465+
if (nextChar === char) break
466+
}
467+
}
468+
469+
//
470+
else if (char === OPEN_PAREN) {
471+
depth += 1
472+
}
473+
474+
//
475+
else if (char === CLOSE_PAREN) {
476+
depth -= 1
477+
478+
if (depth === 0) {
479+
pathEnd ??= idx
480+
argsEnd = idx
481+
break
482+
}
483+
}
484+
485+
//
486+
else if (char === COMMA && depth === 1) {
487+
pathEnd ??= idx
488+
}
444489
}
445-
path = path.replace(/['"]*\s*$/, '')
446490

447-
let startIndex =
448-
match.index +
449-
match.groups.prefix.length +
450-
match.groups.helper.length +
451-
match.groups.innerPrefix.length
491+
if (argsEnd === null) continue
452492

453-
let helper: 'config' | 'theme' | 'var' = 'config'
493+
let helper: 'config' | 'theme' | 'var'
454494

455495
if (match.groups.helper === 'theme' || match.groups.helper === '--theme') {
456496
helper = 'theme'
457497
} else if (match.groups.helper === 'var') {
458498
helper = 'var'
499+
} else if (match.groups.helper === 'config') {
500+
helper = 'config'
501+
} else {
502+
continue
459503
}
460504

461-
return {
505+
let path = text.slice(pathStart, pathEnd)
506+
507+
// Skip leading/trailing whitespace
508+
pathStart += path.match(/^\s+/)?.length ?? 0
509+
pathEnd -= path.match(/\s+$/)?.length ?? 0
510+
511+
// Skip leading/trailing quotes
512+
let quoteStart = path.match(/^['"]+/)?.length ?? 0
513+
let quoteEnd = path.match(/['"]+$/)?.length ?? 0
514+
515+
if (quoteStart && quoteEnd) {
516+
pathStart += quoteStart
517+
pathEnd -= quoteEnd
518+
}
519+
520+
// Clip to the top-level slash
521+
depth = 1
522+
for (let idx = pathStart; idx < pathEnd; ++idx) {
523+
let char = text.charCodeAt(idx)
524+
if (char === BACKSLASH) {
525+
idx += 1
526+
} else if (char === OPEN_PAREN) {
527+
depth += 1
528+
} else if (char === CLOSE_PAREN) {
529+
depth -= 1
530+
} else if (char === SLASH && depth === 1) {
531+
pathEnd = idx
532+
}
533+
}
534+
535+
// Re-slice
536+
path = text.slice(pathStart, pathEnd)
537+
538+
// Skip leading/trailing whitespace
539+
//
540+
// This can happen if we've clipped the path down to before the `/`
541+
pathStart += path.match(/^\s+/)?.length ?? 0
542+
pathEnd -= path.match(/\s+$/)?.length ?? 0
543+
544+
// Re-slice
545+
path = text.slice(pathStart, pathEnd)
546+
547+
// Skip leading/trailing quotes
548+
quoteStart = path.match(/^['"]+/)?.length ?? 0
549+
quoteEnd = path.match(/['"]+$/)?.length ?? 0
550+
551+
pathStart += quoteStart
552+
pathEnd -= quoteEnd
553+
554+
// Re-slice
555+
path = text.slice(pathStart, pathEnd)
556+
557+
fns.push({
462558
helper,
463559
path,
464560
ranges: {
465561
full: absoluteRange(
466562
{
467-
start: indexToPosition(text, startIndex),
468-
end: indexToPosition(text, startIndex + match.groups.path.length),
563+
start: indexToPosition(text, argsStart),
564+
end: indexToPosition(text, argsEnd),
469565
},
470566
range,
471567
),
472568
path: absoluteRange(
473569
{
474-
start: indexToPosition(text, startIndex + quotesBefore.length),
475-
end: indexToPosition(text, startIndex + quotesBefore.length + path.length),
570+
start: indexToPosition(text, pathStart),
571+
end: indexToPosition(text, pathEnd),
476572
},
477573
range,
478574
),
479575
},
480-
}
481-
})
576+
})
577+
}
578+
579+
return fns
482580
}
483581

484582
export function indexToPosition(str: string, index: number): Position {

0 commit comments

Comments
 (0)