@@ -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 > c o n f i g | t h e m e | - - t h e m e | v a r ) (?< 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 > c o n f i g | t h e m e | - - t h e m e | v a r ) \( / 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
484582export function indexToPosition ( str : string , index : number ) : Position {
0 commit comments