@@ -62,6 +62,26 @@ let wrapRangeInText = (
62
62
] ;
63
63
} ;
64
64
65
+ let insertBeforeEndingChar = (
66
+ range : p . Range ,
67
+ newText : string
68
+ ) : p . TextEdit [ ] => {
69
+ let beforeEndingChar = {
70
+ line : range . end . line ,
71
+ character : range . end . character - 1 ,
72
+ } ;
73
+
74
+ return [
75
+ {
76
+ range : {
77
+ start : beforeEndingChar ,
78
+ end : beforeEndingChar ,
79
+ } ,
80
+ newText,
81
+ } ,
82
+ ] ;
83
+ } ;
84
+
65
85
export let findCodeActionsInDiagnosticsMessage = ( {
66
86
diagnostic,
67
87
diagnosticMessage,
@@ -78,6 +98,7 @@ export let findCodeActionsInDiagnosticsMessage = ({
78
98
simpleConversion ,
79
99
topLevelUnitType ,
80
100
applyUncurried ,
101
+ simpleAddMissingCases ,
81
102
] ;
82
103
83
104
for ( let action of actions ) {
@@ -234,24 +255,11 @@ let addUndefinedRecordFields: codeActionExtractor = ({
234
255
. join ( ", " ) ;
235
256
}
236
257
237
- let beforeEndingRecordBraceLoc = {
238
- line : range . end . line ,
239
- character : range . end . character - 1 ,
240
- } ;
241
-
242
258
let codeAction : p . CodeAction = {
243
259
title : `Add missing record fields` ,
244
260
edit : {
245
261
changes : {
246
- [ file ] : [
247
- {
248
- range : {
249
- start : beforeEndingRecordBraceLoc ,
250
- end : beforeEndingRecordBraceLoc ,
251
- } ,
252
- newText,
253
- } ,
254
- ] ,
262
+ [ file ] : insertBeforeEndingChar ( range , newText ) ,
255
263
} ,
256
264
} ,
257
265
diagnostics : [ diagnostic ] ,
@@ -398,3 +406,142 @@ let topLevelUnitType: codeActionExtractor = ({
398
406
399
407
return false ;
400
408
} ;
409
+
410
+ // This protects against the fact that the compiler currently returns most
411
+ // text in OCaml. It also ensures that we only return simple constructors.
412
+ let isValidVariantCase = ( text : string ) : boolean => {
413
+ if ( text . startsWith ( "(" ) || text . includes ( "," ) ) {
414
+ return false ;
415
+ }
416
+
417
+ return true ;
418
+ } ;
419
+
420
+ // Untransformed is typically OCaml, and looks like these examples:
421
+ //
422
+ // `SomeVariantName
423
+ //
424
+ // SomeVariantWithPayload _
425
+ //
426
+ // ...and we'll need to transform this into proper ReScript. In the future, the
427
+ // compiler itself should of course output real ReScript. But it currently does
428
+ // not.
429
+ let transformVariant = ( variant : string ) : string | null => {
430
+ // Convert old polyvariant notation to new
431
+ let text = variant . replace ( / ` / g, "#" ) ;
432
+
433
+ // Fix payloads
434
+ if ( text . includes ( " " ) ) {
435
+ let [ variantText , payloadText ] = text . split ( " " ) ;
436
+
437
+ // If the payload itself starts with (, it's another variant with a
438
+ // constructor. We bail in that case, for now at least. We'll be able to
439
+ // revisit this in the future when the compiler prints real ReScript syntax.
440
+ if ( payloadText . startsWith ( "(" ) ) {
441
+ return null ;
442
+ }
443
+
444
+ text = `${ variantText } (${ payloadText } )` ;
445
+ }
446
+
447
+ return text ;
448
+ } ;
449
+
450
+ let simpleAddMissingCases : codeActionExtractor = ( {
451
+ line,
452
+ codeActions,
453
+ file,
454
+ range,
455
+ diagnostic,
456
+ array,
457
+ index,
458
+ } ) => {
459
+ // Examples:
460
+ //
461
+ // You forgot to handle a possible case here, for example:
462
+ // (AnotherValue|Third|Fourth)
463
+ //
464
+ // You forgot to handle a possible case here, for example:
465
+ // (`AnotherValue|`Third|`Fourth)
466
+ //
467
+ // You forgot to handle a possible case here, for example:
468
+ // `AnotherValue
469
+ //
470
+ // You forgot to handle a possible case here, for example:
471
+ // AnotherValue
472
+
473
+ if (
474
+ line . startsWith ( "You forgot to handle a possible case here, for example:" )
475
+ ) {
476
+ let cases : string [ ] = [ ] ;
477
+
478
+ // This collects the rest of the fields if fields are printed on
479
+ // multiple lines.
480
+ array . slice ( index + 1 ) . forEach ( ( line ) => {
481
+ let theLine = line . trim ( ) ;
482
+
483
+ let hasMultipleCases = theLine . includes ( "|" ) ;
484
+
485
+ if ( hasMultipleCases ) {
486
+ cases . push (
487
+ ...( theLine
488
+ // Remove leading and ending parens
489
+ . slice ( 1 , theLine . length - 1 )
490
+ . split ( "|" )
491
+ . filter ( isValidVariantCase )
492
+ . map ( transformVariant )
493
+ . filter ( Boolean ) as string [ ] )
494
+ ) ;
495
+ } else {
496
+ let transformed = transformVariant ( theLine ) ;
497
+ if ( isValidVariantCase ( theLine ) && transformed != null ) {
498
+ cases . push ( transformed ) ;
499
+ }
500
+ }
501
+ } ) ;
502
+
503
+ if ( cases . length === 0 ) {
504
+ return false ;
505
+ }
506
+
507
+ // The end char is the closing brace. In switches, the leading `|` always
508
+ // has the same left padding as the end brace.
509
+ let paddingContentSwitchCase = Array . from ( {
510
+ length : range . end . character ,
511
+ } ) . join ( " " ) ;
512
+
513
+ let newText = cases
514
+ . map ( ( variantName , index ) => {
515
+ // The first case will automatically be padded because we're inserting
516
+ // it where the end brace is currently located.
517
+ let padding = index === 0 ? "" : paddingContentSwitchCase ;
518
+ return `${ padding } | ${ variantName } => assert false` ;
519
+ } )
520
+ . join ( "\n" ) ;
521
+
522
+ // Let's put the end brace back where it was (we still have it to the direct right of us).
523
+ newText += `\n${ paddingContentSwitchCase } ` ;
524
+
525
+ codeActions [ file ] = codeActions [ file ] || [ ] ;
526
+ let codeAction : p . CodeAction = {
527
+ title : `Insert missing cases` ,
528
+ edit : {
529
+ changes : {
530
+ [ file ] : insertBeforeEndingChar ( range , newText ) ,
531
+ } ,
532
+ } ,
533
+ diagnostics : [ diagnostic ] ,
534
+ kind : p . CodeActionKind . QuickFix ,
535
+ isPreferred : true ,
536
+ } ;
537
+
538
+ codeActions [ file ] . push ( {
539
+ range,
540
+ codeAction,
541
+ } ) ;
542
+
543
+ return true ;
544
+ }
545
+
546
+ return false ;
547
+ } ;
0 commit comments