Skip to content

Commit 060cb90

Browse files
committed
tweaking the add matches action. more robust implementation for wrapping/unwrapping optional values
1 parent 39a38b8 commit 060cb90

File tree

1 file changed

+149
-117
lines changed

1 file changed

+149
-117
lines changed

Diff for: server/src/codeActions.ts

+149-117
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// This file holds code actions derived from diagnostics. There are more code
22
// actions available in the extension, but they are derived via the analysis
33
// OCaml binary.
4+
import { match } from "assert";
45
import * as p from "vscode-languageserver-protocol";
56

67
export type filesCodeActions = {
@@ -82,6 +83,52 @@ let insertBeforeEndingChar = (
8283
];
8384
};
8485

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+
85132
export let findCodeActionsInDiagnosticsMessage = ({
86133
diagnostic,
87134
diagnosticMessage,
@@ -93,13 +140,12 @@ export let findCodeActionsInDiagnosticsMessage = ({
93140
// Because of how actions work, there can only be one per diagnostic. So,
94141
// halt whenever a code action has been found.
95142
let codeActionEtractors = [
143+
simpleTypeMismatches,
96144
didYouMeanAction,
97145
addUndefinedRecordFields,
98146
simpleConversion,
99147
applyUncurried,
100148
simpleAddMissingCases,
101-
simpleWrapOptionalWithSome,
102-
simpleUnwrapOptional,
103149
];
104150

105151
for (let extractCodeAction of codeActionEtractors) {
@@ -393,16 +439,6 @@ let applyUncurried: codeActionExtractor = ({
393439
return false;
394440
};
395441

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-
406442
// Untransformed is typically OCaml, and looks like these examples:
407443
//
408444
// `SomeVariantName
@@ -412,18 +448,37 @@ let isValidVariantCase = (text: string): boolean => {
412448
// ...and we'll need to transform this into proper ReScript. In the future, the
413449
// compiler itself should of course output real ReScript. But it currently does
414450
// 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, "#");
418467

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.
420477
if (text.includes(" ")) {
421478
let [variantText, payloadText] = text.split(" ");
422479

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) {
427482
return null;
428483
}
429484

@@ -461,6 +516,10 @@ let simpleAddMissingCases: codeActionExtractor = ({
461516
//
462517
// You forgot to handle a possible case here, for example:
463518
// AnotherValue
519+
//
520+
// You forgot to handle a possible case here, for example:
521+
// (`One _|`Two _|
522+
// `Three _)
464523

465524
if (
466525
line.startsWith("You forgot to handle a possible case here, for example:")
@@ -469,28 +528,36 @@ let simpleAddMissingCases: codeActionExtractor = ({
469528

470529
// This collects the rest of the fields if fields are printed on
471530
// 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);
492559
}
493-
});
560+
}
494561

495562
if (cases.length === 0) {
496563
return false;
@@ -541,7 +608,7 @@ let simpleAddMissingCases: codeActionExtractor = ({
541608
// This detects concrete variables or values put in a position which expects an
542609
// optional of that same type, and offers to wrap the value/variable in
543610
// `Some()`.
544-
let simpleWrapOptionalWithSome: codeActionExtractor = ({
611+
let simpleTypeMismatches: codeActionExtractor = ({
545612
line,
546613
codeActions,
547614
file,
@@ -559,91 +626,32 @@ let simpleWrapOptionalWithSome: codeActionExtractor = ({
559626
// 50 │
560627
// This has type: string
561628
// Somewhere wanted: option<string>
629+
//
630+
// ...but types etc can also be on multilines, so we need a good
631+
// amount of cleanup.
562632

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:";
570634

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:"
575639
);
640+
let somewhereWantedArr = array
641+
.slice(index + thisHasTypeArr.length)
642+
.map((line) => line.replace("Somewhere wanted:", ""));
576643

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);
604646

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}>`) {
641649
codeActions[file] = codeActions[file] || [];
642650

643651
// We can figure out default values for primitives etc.
644652
let defaultValue = "assert false";
645653

646-
switch (somewhereWantedText) {
654+
switch (somewhereWanted) {
647655
case "string": {
648656
defaultValue = `"-"`;
649657
break;
@@ -685,6 +693,30 @@ let simpleUnwrapOptional: codeActionExtractor = ({
685693

686694
return true;
687695
}
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+
}
688720
}
689721

690722
return false;

0 commit comments

Comments
 (0)