1
1
// This file holds code actions derived from diagnostics. There are more code
2
2
// actions available in the extension, but they are derived via the analysis
3
3
// OCaml binary.
4
+ import { match } from "assert" ;
4
5
import * as p from "vscode-languageserver-protocol" ;
5
6
6
7
export type filesCodeActions = {
@@ -82,6 +83,52 @@ let insertBeforeEndingChar = (
82
83
] ;
83
84
} ;
84
85
86
+ let removeTrailingComma = ( text : string ) : string => {
87
+ let str = text . trim ( ) ;
88
+ if ( str . endsWith ( "," ) ) {
89
+ return str . slice ( 0 , str . length - 1 ) ;
90
+ }
91
+
92
+ return str ;
93
+ } ;
94
+
95
+ let extractTypename = ( lines : string [ ] ) : string => {
96
+ let arrFiltered : string [ ] = [ ] ;
97
+
98
+ for ( let i = 0 ; i <= lines . length - 1 ; i += 1 ) {
99
+ let line = lines [ i ] ;
100
+ if ( line . includes ( "(defined as" ) ) {
101
+ let [ typeStr , _ ] = line . split ( "(defined as" ) ;
102
+ arrFiltered . push ( removeTrailingComma ( typeStr ) ) ;
103
+ break ;
104
+ } else {
105
+ arrFiltered . push ( removeTrailingComma ( line ) ) ;
106
+ }
107
+ }
108
+
109
+ return arrFiltered . join ( "" ) . trim ( ) ;
110
+ } ;
111
+
112
+ let takeUntil = ( array : string [ ] , startsWith : string ) : string [ ] => {
113
+ let res : string [ ] = [ ] ;
114
+ let arr = array . slice ( ) ;
115
+
116
+ let matched = false ;
117
+ arr . forEach ( ( line ) => {
118
+ if ( matched ) {
119
+ return ;
120
+ }
121
+
122
+ if ( line . startsWith ( startsWith ) ) {
123
+ matched = true ;
124
+ } else {
125
+ res . push ( line ) ;
126
+ }
127
+ } ) ;
128
+
129
+ return res ;
130
+ } ;
131
+
85
132
export let findCodeActionsInDiagnosticsMessage = ( {
86
133
diagnostic,
87
134
diagnosticMessage,
@@ -93,13 +140,12 @@ export let findCodeActionsInDiagnosticsMessage = ({
93
140
// Because of how actions work, there can only be one per diagnostic. So,
94
141
// halt whenever a code action has been found.
95
142
let codeActionEtractors = [
143
+ simpleTypeMismatches ,
96
144
didYouMeanAction ,
97
145
addUndefinedRecordFields ,
98
146
simpleConversion ,
99
147
applyUncurried ,
100
148
simpleAddMissingCases ,
101
- simpleWrapOptionalWithSome ,
102
- simpleUnwrapOptional ,
103
149
] ;
104
150
105
151
for ( let extractCodeAction of codeActionEtractors ) {
@@ -393,16 +439,6 @@ let applyUncurried: codeActionExtractor = ({
393
439
return false ;
394
440
} ;
395
441
396
- // This protects against the fact that the compiler currently returns most
397
- // text in OCaml. It also ensures that we only return simple constructors.
398
- let isValidVariantCase = ( text : string ) : boolean => {
399
- if ( text . startsWith ( "(" ) || text . includes ( "," ) ) {
400
- return false ;
401
- }
402
-
403
- return true ;
404
- } ;
405
-
406
442
// Untransformed is typically OCaml, and looks like these examples:
407
443
//
408
444
// `SomeVariantName
@@ -412,18 +448,37 @@ let isValidVariantCase = (text: string): boolean => {
412
448
// ...and we'll need to transform this into proper ReScript. In the future, the
413
449
// compiler itself should of course output real ReScript. But it currently does
414
450
// not.
415
- let transformVariant = ( variant : string ) : string | null => {
416
- // Convert old polyvariant notation to new
417
- let text = variant . replace ( / ` / g, "#" ) ;
451
+ //
452
+ // Note that we're trying to not be too clever here, so we'll only try to
453
+ // convert the very simplest cases - single variant/polyvariant, with single
454
+ // payloads. No records, tuples etc yet. We can add those when the compiler
455
+ // outputs them in proper ReScript.
456
+ let transformMatchPattern = ( matchPattern : string ) : string | null => {
457
+ if (
458
+ // Parens means tuples or more complicated payloads. Bail.
459
+ matchPattern . includes ( "(" ) ||
460
+ // Braces means records. Bail.
461
+ matchPattern . includes ( "{" )
462
+ ) {
463
+ return null ;
464
+ }
465
+
466
+ let text = matchPattern . replace ( / ` / g, "#" ) ;
418
467
419
- // Fix payloads
468
+ let payloadRegexp = / / g;
469
+ let matched = text . match ( payloadRegexp ) ;
470
+
471
+ // Constructors are preceded by a single space. Bail if there's more than 1.
472
+ if ( matched != null && matched . length > 2 ) {
473
+ return null ;
474
+ }
475
+
476
+ // Fix payloads if they can be fixed. If not, bail.
420
477
if ( text . includes ( " " ) ) {
421
478
let [ variantText , payloadText ] = text . split ( " " ) ;
422
479
423
- // If the payload itself starts with (, it's another variant with a
424
- // constructor. We bail in that case, for now at least. We'll be able to
425
- // revisit this in the future when the compiler prints real ReScript syntax.
426
- if ( payloadText . startsWith ( "(" ) ) {
480
+ let transformedPayloadText = transformMatchPattern ( payloadText ) ;
481
+ if ( transformedPayloadText == null ) {
427
482
return null ;
428
483
}
429
484
@@ -461,6 +516,10 @@ let simpleAddMissingCases: codeActionExtractor = ({
461
516
//
462
517
// You forgot to handle a possible case here, for example:
463
518
// AnotherValue
519
+ //
520
+ // You forgot to handle a possible case here, for example:
521
+ // (`One _|`Two _|
522
+ // `Three _)
464
523
465
524
if (
466
525
line . startsWith ( "You forgot to handle a possible case here, for example:" )
@@ -469,28 +528,36 @@ let simpleAddMissingCases: codeActionExtractor = ({
469
528
470
529
// This collects the rest of the fields if fields are printed on
471
530
// multiple lines.
472
- array . slice ( index + 1 ) . forEach ( ( line ) => {
473
- let theLine = line . trim ( ) ;
474
-
475
- let hasMultipleCases = theLine . includes ( "|" ) ;
476
-
477
- if ( hasMultipleCases ) {
478
- cases . push (
479
- ...( theLine
480
- // Remove leading and ending parens
481
- . slice ( 1 , theLine . length - 1 )
482
- . split ( "|" )
483
- . filter ( isValidVariantCase )
484
- . map ( transformVariant )
485
- . filter ( Boolean ) as string [ ] )
486
- ) ;
487
- } else {
488
- let transformed = transformVariant ( theLine ) ;
489
- if ( isValidVariantCase ( theLine ) && transformed != null ) {
490
- cases . push ( transformed ) ;
491
- }
531
+ let allCasesAsOneLine = array
532
+ . slice ( index + 1 )
533
+ . join ( "" )
534
+ . trim ( ) ;
535
+
536
+ // Remove leading and ending parens
537
+ allCasesAsOneLine = allCasesAsOneLine . slice (
538
+ 1 ,
539
+ allCasesAsOneLine . length - 1
540
+ ) ;
541
+
542
+ // Any parens left means this is a more complex match. Bail.
543
+ if ( allCasesAsOneLine . includes ( "(" ) ) {
544
+ return false ;
545
+ }
546
+
547
+ let hasMultipleCases = allCasesAsOneLine . includes ( "|" ) ;
548
+ if ( hasMultipleCases ) {
549
+ cases . push (
550
+ ...( allCasesAsOneLine
551
+ . split ( "|" )
552
+ . map ( transformMatchPattern )
553
+ . filter ( Boolean ) as string [ ] )
554
+ ) ;
555
+ } else {
556
+ let transformed = transformMatchPattern ( allCasesAsOneLine ) ;
557
+ if ( transformed != null ) {
558
+ cases . push ( transformed ) ;
492
559
}
493
- } ) ;
560
+ }
494
561
495
562
if ( cases . length === 0 ) {
496
563
return false ;
@@ -541,7 +608,7 @@ let simpleAddMissingCases: codeActionExtractor = ({
541
608
// This detects concrete variables or values put in a position which expects an
542
609
// optional of that same type, and offers to wrap the value/variable in
543
610
// `Some()`.
544
- let simpleWrapOptionalWithSome : codeActionExtractor = ( {
611
+ let simpleTypeMismatches : codeActionExtractor = ( {
545
612
line,
546
613
codeActions,
547
614
file,
@@ -559,91 +626,32 @@ let simpleWrapOptionalWithSome: codeActionExtractor = ({
559
626
// 50 │
560
627
// This has type: string
561
628
// Somewhere wanted: option<string>
629
+ //
630
+ // ...but types etc can also be on multilines, so we need a good
631
+ // amount of cleanup.
562
632
563
- if ( line . startsWith ( "Somewhere wanted: option<" ) ) {
564
- let somewhereWantedLine = line ;
565
- let thisHasTypeLine = array [ index - 1 ] ;
566
- let hasTypeText = thisHasTypeLine . split ( "This has type: " ) [ 1 ] . trim ( ) ;
567
- let somewhereWantedText = somewhereWantedLine
568
- . split ( "Somewhere wanted: option<" ) [ 1 ]
569
- . trim ( ) ;
633
+ let lookFor = "This has type:" ;
570
634
571
- // Remove ending `>` so we can compare the underlying types
572
- somewhereWantedText = somewhereWantedText . slice (
573
- 0 ,
574
- somewhereWantedText . length - 1
635
+ if ( line . startsWith ( lookFor ) ) {
636
+ let thisHasTypeArr = takeUntil (
637
+ [ line . slice ( lookFor . length ) , ... array . slice ( index + 1 ) ] ,
638
+ "Somewhere wanted:"
575
639
) ;
640
+ let somewhereWantedArr = array
641
+ . slice ( index + thisHasTypeArr . length )
642
+ . map ( ( line ) => line . replace ( "Somewhere wanted:" , "" ) ) ;
576
643
577
- // We only trigger the code action if the thing that's already there is the
578
- // exact same type.
579
- if ( hasTypeText === somewhereWantedText ) {
580
- codeActions [ file ] = codeActions [ file ] || [ ] ;
581
- let codeAction : p . CodeAction = {
582
- title : `Wrap value in Some` ,
583
- edit : {
584
- changes : {
585
- [ file ] : wrapRangeInText ( range , "Some(" , ")" ) ,
586
- } ,
587
- } ,
588
- diagnostics : [ diagnostic ] ,
589
- kind : p . CodeActionKind . QuickFix ,
590
- isPreferred : true ,
591
- } ;
592
-
593
- codeActions [ file ] . push ( {
594
- range,
595
- codeAction,
596
- } ) ;
597
-
598
- return true ;
599
- }
600
- }
601
-
602
- return false ;
603
- } ;
644
+ let thisHasType = extractTypename ( thisHasTypeArr ) ;
645
+ let somewhereWanted = extractTypename ( somewhereWantedArr ) ;
604
646
605
- // This detects when an optional is passed into a non optional slot, and offers
606
- // to insert a switch for unwrapping that optional.
607
- let simpleUnwrapOptional : codeActionExtractor = ( {
608
- line,
609
- codeActions,
610
- file,
611
- range,
612
- diagnostic,
613
- array,
614
- index,
615
- } ) => {
616
- // Examples:
617
- //
618
- // 47 │
619
- // 48 │ let as_ = {
620
- // 49 │ someProp: optional,
621
- // 50 │ another: Some("123"),
622
- // 51 │ }
623
-
624
- // This has type: option<string>
625
- // Somewhere wanted: string
626
-
627
- if ( line . startsWith ( "This has type: option<" ) ) {
628
- let thisHasTypeLine = line ;
629
- let hasTypeText = thisHasTypeLine . split ( "This has type: option<" ) [ 1 ] . trim ( ) ;
630
- // Remove ending `>` so we can compare the underlying types
631
- hasTypeText = hasTypeText . slice ( 0 , hasTypeText . length - 1 ) ;
632
-
633
- let somewhereWantedLine = array [ index + 1 ] ;
634
- let somewhereWantedText = somewhereWantedLine
635
- . split ( "Somewhere wanted: " ) [ 1 ]
636
- . trim ( ) ;
637
-
638
- // We only trigger the code action if the thing that's already there is the
639
- // exact same type.
640
- if ( hasTypeText === somewhereWantedText ) {
647
+ // Switching over an option
648
+ if ( thisHasType === `option<${ somewhereWanted } >` ) {
641
649
codeActions [ file ] = codeActions [ file ] || [ ] ;
642
650
643
651
// We can figure out default values for primitives etc.
644
652
let defaultValue = "assert false" ;
645
653
646
- switch ( somewhereWantedText ) {
654
+ switch ( somewhereWanted ) {
647
655
case "string" : {
648
656
defaultValue = `"-"` ;
649
657
break ;
@@ -685,6 +693,30 @@ let simpleUnwrapOptional: codeActionExtractor = ({
685
693
686
694
return true ;
687
695
}
696
+
697
+ // Wrapping a non-optional in Some
698
+ if ( `option<${ thisHasType } >` === somewhereWanted ) {
699
+ codeActions [ file ] = codeActions [ file ] || [ ] ;
700
+
701
+ let codeAction : p . CodeAction = {
702
+ title : `Wrap value in Some` ,
703
+ edit : {
704
+ changes : {
705
+ [ file ] : wrapRangeInText ( range , "Some(" , ")" ) ,
706
+ } ,
707
+ } ,
708
+ diagnostics : [ diagnostic ] ,
709
+ kind : p . CodeActionKind . QuickFix ,
710
+ isPreferred : true ,
711
+ } ;
712
+
713
+ codeActions [ file ] . push ( {
714
+ range,
715
+ codeAction,
716
+ } ) ;
717
+
718
+ return true ;
719
+ }
688
720
}
689
721
690
722
return false ;
0 commit comments