Skip to content

Commit 382bb95

Browse files
zthcristianoc
authored andcommitted
code action for inserting simple missing variant cases
1 parent 47c9f50 commit 382bb95

File tree

1 file changed

+161
-14
lines changed

1 file changed

+161
-14
lines changed

server/src/codeActions.ts

Lines changed: 161 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ let wrapRangeInText = (
6262
];
6363
};
6464

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+
6585
export let findCodeActionsInDiagnosticsMessage = ({
6686
diagnostic,
6787
diagnosticMessage,
@@ -78,6 +98,7 @@ export let findCodeActionsInDiagnosticsMessage = ({
7898
simpleConversion,
7999
topLevelUnitType,
80100
applyUncurried,
101+
simpleAddMissingCases,
81102
];
82103

83104
for (let action of actions) {
@@ -234,24 +255,11 @@ let addUndefinedRecordFields: codeActionExtractor = ({
234255
.join(", ");
235256
}
236257

237-
let beforeEndingRecordBraceLoc = {
238-
line: range.end.line,
239-
character: range.end.character - 1,
240-
};
241-
242258
let codeAction: p.CodeAction = {
243259
title: `Add missing record fields`,
244260
edit: {
245261
changes: {
246-
[file]: [
247-
{
248-
range: {
249-
start: beforeEndingRecordBraceLoc,
250-
end: beforeEndingRecordBraceLoc,
251-
},
252-
newText,
253-
},
254-
],
262+
[file]: insertBeforeEndingChar(range, newText),
255263
},
256264
},
257265
diagnostics: [diagnostic],
@@ -398,3 +406,142 @@ let topLevelUnitType: codeActionExtractor = ({
398406

399407
return false;
400408
};
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

Comments
 (0)