@@ -403,13 +403,12 @@ export function findHelperFunctionsInRange(
403
403
doc : TextDocument ,
404
404
range ?: Range ,
405
405
) : 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 )
411
410
412
- // Eliminate matches that are on an `@import`
411
+ // Eliminate matches that are attached to an `@import`
413
412
matches = matches . filter ( ( match ) => {
414
413
// Scan backwards to see if we're in an `@import` statement
415
414
for ( let i = match . index - 1 ; i >= 0 ; i -- ) {
@@ -427,58 +426,157 @@ export function findHelperFunctionsInRange(
427
426
return true
428
427
} )
429
428
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
+ }
444
489
}
445
- path = path . replace ( / [ ' " ] * \s * $ / , '' )
446
490
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
452
492
453
- let helper : 'config' | 'theme' | 'var' = 'config'
493
+ let helper : 'config' | 'theme' | 'var'
454
494
455
495
if ( match . groups . helper === 'theme' || match . groups . helper === '--theme' ) {
456
496
helper = 'theme'
457
497
} else if ( match . groups . helper === 'var' ) {
458
498
helper = 'var'
499
+ } else if ( match . groups . helper === 'config' ) {
500
+ helper = 'config'
501
+ } else {
502
+ continue
459
503
}
460
504
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 ( {
462
558
helper,
463
559
path,
464
560
ranges : {
465
561
full : absoluteRange (
466
562
{
467
- start : indexToPosition ( text , startIndex ) ,
468
- end : indexToPosition ( text , startIndex + match . groups . path . length ) ,
563
+ start : indexToPosition ( text , argsStart ) ,
564
+ end : indexToPosition ( text , argsEnd ) ,
469
565
} ,
470
566
range ,
471
567
) ,
472
568
path : absoluteRange (
473
569
{
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 ) ,
476
572
} ,
477
573
range ,
478
574
) ,
479
575
} ,
480
- }
481
- } )
576
+ } )
577
+ }
578
+
579
+ return fns
482
580
}
483
581
484
582
export function indexToPosition ( str : string , index : number ) : Position {
0 commit comments