Repository: elm-tooling/elm-language-server (ts)

Issue:
Missing action to add missing branches in case statements using nVim + Coc
<!--- Provide a general summary of the issue in the Title above -->

## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
When creating a case statement on a custom type, and only implementing one of the possible types, the code actions for the case line only display the **Add _ branch** action.

![image](https://user-images.githubusercontent.com/1090272/107941170-97a04d80-6f89-11eb-875a-4a78e6a9c4d3.png)


## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
It should display an **Add missing branches** action, which implements placeholders for the missing values.

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. Using nVim + Coc
2. Declare a custom type `Msg` with multiple values.
3. Implement a case statement for `Msg`, fulfilling only one branch.
4. Display code actions for the case statement line.

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used: 2.0.2
* Editor used: nVim
* Environment name and version (e.g. node.js 5.4): v12.15.0
* Operating System and version: macOS Big Sur



Pull request:
Add diagnostics and code actions for missing case patterns
Fixes #520. Part of #406. This seems to work well so far. I will test this at work and add more tests for pattern matching if needed. Feel free to adjust wording of diagnostic or code action.

Patch:
diff --git a/src/compiler/diagnostics.ts b/src/compiler/diagnostics.ts
index fa09ead..b4ed1e1 100644
--- a/src/compiler/diagnostics.ts
+++ b/src/compiler/diagnostics.ts
@@ -3,6 +3,7 @@ import { DiagnosticSeverity, Range } from "vscode-languageserver";
 import { SyntaxNode } from "web-tree-sitter";
 import { PositionUtil } from "../positionUtil";
 import { DiagnosticSource } from "../providers/diagnostics/diagnosticSource";
+import { getSpaces } from "../util/refactorEditUtils";
 
 export interface Diagnostic {
   code: string;
@@ -117,6 +118,16 @@ export const Diagnostics = {
     "The `{0}` type alias cannot be followed by (..).",
     DiagnosticSeverity.Error,
   ),
+  IncompleteCasePattern: (n: number): IDiagnosticMessage =>
+    diag(
+      "incomplete_case_pattern",
+      `This \`case\` does not have branches for all possibilities.\nMissing possibilities include:\n${getSpaces(
+        2,
+      )}${Array.from(Array(n))
+        .map((_, i) => `{${i}}`)
+        .join(`\n${getSpaces(2)}`)}\n`,
+      DiagnosticSeverity.Error,
+    ),
   InvalidPattern: diag(
     "invalid_pattern",
     "Invalid pattern error.\nExpected: `{0}`\nFound: `{1}`",
@@ -199,6 +210,11 @@ export const Diagnostics = {
     "A value named `{0}` is already defined.",
     DiagnosticSeverity.Error,
   ),
+  RedundantPattern: diag(
+    "redundant_pattern",
+    "The {0} pattern is redundant",
+    DiagnosticSeverity.Error,
+  ),
   TypeArgumentCount: diag(
     "type_argument_count",
     "The type expected {0} arguments, but got {1} instead.",
diff --git a/src/compiler/patternMatches.ts b/src/compiler/patternMatches.ts
new file mode 100644
index 0000000..8457666
--- /dev/null
+++ b/src/compiler/patternMatches.ts
@@ -0,0 +1,603 @@
+import { SyntaxNode } from "web-tree-sitter";
+import { TreeUtils } from "../util/treeUtils";
+import { Utils } from "../util/utils";
+import { Diagnostic, Diagnostics, error } from "./diagnostics";
+import { ISourceFile } from "./forest";
+import { IProgram } from "./program";
+
+type Union = {
+  alts: CanCtor[];
+  numAlts: number;
+};
+
+type LiteralType = "Int" | "Str" | "Chr";
+
+type Literal = {
+  type: "Literal";
+  literalType: LiteralType;
+  value: string;
+};
+
+type Anything = {
+  type: "Anything";
+};
+
+type Ctor = {
+  type: "Ctor";
+  union: Union;
+  name: string;
+  args: Pattern[];
+};
+
+type Pattern = Anything | Literal | Ctor;
+
+const Anything: Pattern = { type: "Anything" };
+const Literal = (literalType: LiteralType, value: string): Pattern => ({
+  type: "Literal",
+  literalType,
+  value,
+});
+const Ctor = (union: Union, name: string, args: Pattern[]): Pattern => ({
+  type: "Ctor",
+  union,
+  name,
+  args,
+});
+
+type CanCtor = {
+  name: string;
+  arity: number;
+};
+
+function nodeToCanCtor(node: SyntaxNode): CanCtor {
+  return {
+    name: node.firstNamedChild!.text,
+    arity: node.namedChildren
+      .slice(1)
+      .filter(
+        (n) => n.type !== "left_parenthesis" && n.type !== "right_parenthesis",
+      ).length,
+  };
+}
+
+const unitName = "#0";
+const pairName = "#2";
+const tripleName = "#3";
+const consName = "::";
+const nilName = "[]";
+
+const unit: Union = { alts: [{ name: unitName, arity: 0 }], numAlts: 1 };
+const pair: Union = { alts: [{ name: pairName, arity: 2 }], numAlts: 1 };
+const triple: Union = { alts: [{ name: tripleName, arity: 3 }], numAlts: 1 };
+const list: Union = {
+  alts: [
+    { name: nilName, arity: 0 },
+    { name: consName, arity: 2 },
+  ],
+  numAlts: 2,
+};
+
+const nil: Pattern = Ctor(list, nilName, []);
+
+export class PatternMatches {
+  constructor(private program: IProgram, private sourceFile: ISourceFile) {}
+
+  public static check(
+    region: SyntaxNode,
+    patterns: SyntaxNode[],
+    program: IProgram,
+  ): Diagnostic[] {
+    return new PatternMatches(
+      program,
+      program.getSourceFile(patterns[0].tree.uri)!,
+    ).checkPatterns(region, patterns);
+  }
+
+  public static missing(patterns: SyntaxNode[], program: IProgram): string[] {
+    return new PatternMatches(
+      program,
+      program.getSourceFile(patterns[0].tree.uri)!,
+    ).getMissing(patterns);
+  }
+
+  private getMissing(patterns: SyntaxNode[]): string[] {
+    const result = this.toNonRedundantRows(patterns[0], patterns);
+
+    if (!Array.isArray(result)) {
+      return [];
+    } else {
+      return this.isExhaustive(result, 1).map((p) =>
+        patternToDoc(p[0], "Unambiguous"),
+      );
+    }
+  }
+
+  private checkPatterns(
+    region: SyntaxNode,
+    patterns: SyntaxNode[],
+  ): Diagnostic[] {
+    const result = this.toNonRedundantRows(region, patterns);
+
+    if (!Array.isArray(result)) {
+      return [result];
+    } else {
+      const badPatterns = this.isExhaustive(result, 1);
+      if (badPatterns.length === 0) {
+        return [];
+      } else {
+        // TODO: Handle other incomplete patterns
+        const badPatternsText = badPatterns.map((p) =>
+          patternToDoc(p[0], "Unambiguous"),
+        );
+        return [
+          error(
+            region,
+            Diagnostics.IncompleteCasePattern(badPatternsText.length),
+            ...badPatternsText,
+          ),
+        ];
+      }
+    }
+  }
+
+  private isExhaustive(matrix: Pattern[][], n: number): Pattern[][] {
+    if (matrix.length === 0) {
+      return [replicate(Anything, n)];
+    } else {
+      if (n === 0) {
+        return [];
+      } else {
+        const ctors = this.collectCtors(matrix);
+        const numSeen = ctors.size;
+
+        if (numSeen === 0) {
+          return this.isExhaustive(
+            matrix
+              .map(this.specializeRowByAnything.bind(this))
+              .filter(Utils.notUndefined.bind(this)),
+            n - 1,
+          ).map((result) => [Anything, ...result]);
+        } else {
+          const alts = findMin(ctors)[1];
+
+          if (numSeen < alts.numAlts) {
+            const missing: Pattern[] = alts.alts
+              .map((alt) => isMissing(alts, ctors, alt))
+              .filter(Utils.notUndefined.bind(this));
+            return this.isExhaustive(
+              matrix
+                .map(this.specializeRowByAnything.bind(this))
+                .filter(Utils.notUndefined.bind(this)),
+              n - 1,
+            ).flatMap((ex) => missing.map((m) => [m, ...ex]));
+          } else {
+            const isAltExhaustive = (ctor: CanCtor): Pattern[][] => {
+              return this.isExhaustive(
+                matrix
+                  .map((row) =>
+                    this.specializeRowByCtor(ctor.name, ctor.arity, row),
+                  )
+                  .filter(Utils.notUndefined.bind(this)),
+                ctor.arity + n - 1,
+              ).map((patterns) =>
+                recoverCtor(alts, ctor.name, ctor.arity, patterns),
+              );
+            };
+
+            return alts.alts.flatMap(isAltExhaustive);
+          }
+        }
+      }
+    }
+  }
+
+  private toNonRedundantRows(
+    region: SyntaxNode,
+    patterns: SyntaxNode[],
+  ): Pattern[][] | Diagnostic {
+    return this.toSimplifiedUsefulRows(region, [], patterns);
+  }
+
+  private toSimplifiedUsefulRows(
+    overalRegion: SyntaxNode,
+    checkedRows: Pattern[][],
+    uncheckedPatterns: SyntaxNode[],
+  ): Pattern[][] | Diagnostic {
+    if (uncheckedPatterns.length === 0) {
+      return checkedRows;
+    } else {
+      const nextRow = [this.simplify(uncheckedPatterns[0])];
+      if (this.isUseful(checkedRows, nextRow)) {
+        return this.toSimplifiedUsefulRows(
+          overalRegion,
+          [nextRow, ...checkedRows],
+          uncheckedPatterns.slice(1),
+        );
+      } else {
+        return error(
+          uncheckedPatterns[0],
+          Diagnostics.RedundantPattern,
+          checkedRows.length + 1,
+        );
+      }
+    }
+  }
+
+  private isUseful(matrix: Pattern[][], vector: Pattern[]): boolean {
+    if (matrix.length === 0) {
+      return true;
+    } else {
+      if (vector.length === 0) {
+        return false;
+      } else {
+        const patterns = vector.slice(1);
+        switch (vector[0].type) {
+          case "Ctor": {
+            const args = vector[0].args;
+            const name = vector[0].name;
+            return this.isUseful(
+              matrix
+                .map((row) => this.specializeRowByCtor(name, args.length, row))
+                .filter(Utils.notUndefined.bind(this)),
+              [...args, ...patterns],
+            );
+          }
+
+          case "Anything": {
+            const alts = this.isComplete(matrix);
+
+            if (!alts) {
+              return this.isUseful(
+                matrix
+                  .map(this.specializeRowByAnything.bind(this))
+                  .filter(Utils.notUndefined.bind(this)),
+                patterns,
+              );
+            } else {
+              return alts.some((alt) => {
+                return this.isUseful(
+                  matrix
+                    .map((row) =>
+                      this.specializeRowByCtor(alt.name, alt.arity, row),
+                    )
+                    .filter(Utils.notUndefined.bind(this)),
+                  [...replicate(Anything, alt.arity), ...patterns],
+                );
+              });
+            }
+          }
+
+          case "Literal": {
+            const literal = vector[0];
+            return this.isUseful(
+              matrix
+                .map((row) => this.specializeRowByLiteral(literal, row))
+                .filter(Utils.notUndefined.bind(this)),
+              patterns,
+            );
+          }
+        }
+      }
+    }
+  }
+
+  private specializeRowByCtor(
+    name: string,
+    arity: number,
+    row: Pattern[],
+  ): Pattern[] | undefined {
+    const patterns = row.slice(1);
+    switch (row[0].type) {
+      case "Ctor":
+        if (row[0].name === name) {
+          return [...row[0].args, ...patterns];
+        } else {
+          return;
+        }
+
+      case "Anything":
+        return [...replicate(Anything, arity), ...patterns];
+
+      default:
+        throw new Error("Compiler bug");
+    }
+  }
+
+  private specializeRowByLiteral(
+    literal: Literal,
+    row: Pattern[],
+  ): Pattern[] | undefined {
+    const patterns = row.slice(1);
+    switch (row[0].type) {
+      case "Literal":
+        if (
+          row[0].literalType === literal.literalType &&
+          row[0].value === literal.value
+        ) {
+          return patterns;
+        } else {
+          return;
+        }
+
+      case "Anything":
+        return patterns;
+
+      default:
+        throw new Error("Compiler bug");
+    }
+  }
+
+  private specializeRowByAnything(row: Pattern[]): Pattern[] | undefined {
+    if (row.length === 0) {
+      return;
+    }
+
+    switch (row[0].type) {
+      case "Ctor":
+      case "Literal":
+        return;
+
+      case "Anything":
+        return row.slice(1);
+    }
+  }
+
+  private isComplete(matrix: Pattern[][]): CanCtor[] | undefined {
+    const ctors = this.collectCtors(matrix);
+    const numSeen = ctors.size;
+
+    if (numSeen === 0) {
+      return;
+    } else {
+      const union = findMin(ctors)[1];
+
+      if (numSeen === union.numAlts) {
+        return union.alts;
+      } else {
+        return;
+      }
+    }
+  }
+
+  private collectCtors(matrix: Pattern[][]): Map<string, Union> {
+    const ctors = new Map<string, Union>();
+    matrix.forEach((row) => {
+      if (row[0]?.type === "Ctor") {
+        ctors.set(row[0].name, row[0].union);
+      }
+    });
+    return ctors;
+  }
+
+  private cons(head: SyntaxNode, tail: Pattern): Pattern {
+    return Ctor(list, consName, [this.simplify(head), tail]);
+  }
+
+  private simplify(pattern: SyntaxNode): Pattern {
+    const patternAs = pattern.childForFieldName("patternAs");
+    if (patternAs) {
+      return this.simplify(patternAs);
+    }
+
+    switch (pattern.type) {
+      case "anything_pattern":
+      case "lower_pattern":
+      case "record_pattern":
+        return Anything;
+
+      case "unit_expr":
+        return Ctor(unit, unitName, []);
+
+      case "tuple_pattern": {
+        const patterns = pattern.children
+          .filter((n) => n.type === "pattern")
+          .map((n) => n.childForFieldName("child")!);
+
+        if (patterns.length === 3) {
+          return Ctor(
+            triple,
+            tripleName,
+            patterns.map(this.simplify.bind(this)),
+          );
+        } else {
+          return Ctor(pair, pairName, patterns.map(this.simplify.bind(this)));
+        }
+      }
+
+      case "union_pattern":
+      case "nullary_constructor_argument_pattern": {
+        const ctor =
+          pattern.childForFieldName("constructor")?.lastNamedChild ??
+          pattern.firstNamedChild!;
+        const definition = this.program
+          .getTypeChecker()
+          .findDefinition(ctor, this.sourceFile);
+
+        const unionVariants = definition.symbol
+          ? TreeUtils.findParentOfType(
+              "type_declaration",
+              definition.symbol.node,
+            )
+              ?.namedChildren.filter((n) => n.type === "union_variant")
+              .map(nodeToCanCtor) ?? []
+          : [];
+
+        const numAlts = unionVariants.length;
+
+        return Ctor(
+          { alts: unionVariants, numAlts },
+          ctor.text,
+          pattern.namedChildren
+            .filter(
+              (n) =>
+                n.type !== "left_parenthesis" && n.type !== "right_parenthesis",
+            )
+            .slice(1)
+            .map(this.simplify.bind(this)),
+        );
+      }
+
+      case "list_pattern":
+        return foldr(
+          this.cons.bind(this),
+          nil,
+          pattern.namedChildren.filter((n) => n.type === "pattern"),
+        );
+
+      case "cons_pattern": {
+        // TODO: Fix how cons is parsed
+        const patterns = pattern.namedChildren.filter(
+          (n) =>
+            n.type.includes("pattern") ||
+            n.type.includes("constant") ||
+            n.type === "unit_expr",
+        );
+        return this.cons(patterns[0], this.simplify(patterns[1]));
+      }
+
+      case "pattern":
+        return this.simplify(pattern.childForFieldName("child")!);
+
+      case "char_constant_expr":
+        return Literal("Chr", pattern.text);
+
+      case "string_constant_expr":
+        return Literal("Str", pattern.text);
+
+      case "number_constant_expr":
+        return Literal("Int", pattern.text);
+
+      default:
+        throw new Error("Unknown pattern type");
+    }
+  }
+}
+
+function isMissing(
+  union: Union,
+  ctors: Map<string, Union>,
+  ctor: CanCtor,
+): Pattern | undefined {
+  if (ctors.has(ctor.name)) {
+    return;
+  } else {
+    return Ctor(union, ctor.name, replicate(Anything, ctor.arity));
+  }
+}
+
+function recoverCtor(
+  union: Union,
+  name: string,
+  arity: number,
+  patterns: Pattern[],
+): Pattern[] {
+  const args = patterns.slice(0, arity);
+  const rest = patterns.slice(arity);
+
+  return [Ctor(union, name, args), ...rest];
+}
+
+function findMin(ctors: Map<string, Union>): [string, Union] {
+  return Array.from(ctors.entries()).sort(([a], [b]) =>
+    a < b ? -1 : a > b ? 1 : 0,
+  )[0];
+}
+
+function patternToDoc(
+  pattern: Pattern,
+  context: "Arg" | "Head" | "Unambiguous",
+): string {
+  const result = delist(pattern, []);
+
+  if (result.type === "NonList") {
+    if (result.pattern.type === "Anything") {
+      return "_";
+    } else if (result.pattern.type === "Literal") {
+      return result.pattern.value;
+    } else {
+      if (result.pattern.name === "#0") {
+        return "()";
+      } else if (result.pattern.name === "#2") {
+        return `( ${patternToDoc(
+          result.pattern.args[0],
+          "Unambiguous",
+        )}, ${patternToDoc(result.pattern.args[1], "Unambiguous")} )`;
+      } else if (result.pattern.name === "#3") {
+        return `( ${patternToDoc(
+          result.pattern.args[0],
+          "Unambiguous",
+        )}, ${patternToDoc(
+          result.pattern.args[1],
+          "Unambiguous",
+        )}, ${patternToDoc(result.pattern.args[2], "Unambiguous")} )`;
+      } else {
+        const ctorDoc = `${result.pattern.name}${
+          result.pattern.args.length > 0 ? " " : ""
+        }${result.pattern.args
+          .map((arg) => patternToDoc(arg, "Arg"))
+          .join(" ")}`;
+
+        if (context === "Arg" && result.pattern.args.length > 0) {
+          return `(${ctorDoc})`;
+        } else {
+          return ctorDoc;
+        }
+      }
+    }
+  } else if (result.type === "FiniteList") {
+    if (result.entries.length === 0) {
+      return "[]";
+    } else {
+      return `[${result.entries
+        .map((entry) => patternToDoc(entry, "Unambiguous"))
+        .join(", ")}]`;
+    }
+  } else {
+    const consDoc = foldr(
+      (hd, tl) => `${patternToDoc(hd, "Head")} :: ${tl}`,
+      patternToDoc(result.finalPattern, "Unambiguous"),
+      result.conses,
+    );
+
+    if (context === "Unambiguous") {
+      return consDoc;
+    } else {
+      return `(${consDoc})`;
+    }
+  }
+}
+
+type Structure =
+  | { type: "FiniteList"; entries: Pattern[] }
+  | { type: "Conses"; conses: Pattern[]; finalPattern: Pattern }
+  | { type: "NonList"; pattern: Pattern };
+
+function delist(pattern: Pattern, revEntries: Pattern[]): Structure {
+  if (pattern.type === "Ctor" && pattern.name === "[]") {
+    return { type: "FiniteList", entries: revEntries };
+  } else if (pattern.type === "Ctor" && pattern.name === "::") {
+    return delist(pattern.args[1], [pattern.args[0], ...revEntries]);
+  } else {
+    if (revEntries.length === 0) {
+      return { type: "NonList", pattern };
+    } else {
+      return {
+        type: "Conses",
+        conses: revEntries.reverse(),
+        finalPattern: pattern,
+      };
+    }
+  }
+}
+
+function replicate<T>(value: T, length: number): T[] {
+  return Array.from(Array(length)).map(() => value);
+}
+
+function foldl<A, B>(func: (b: B, a: A) => B, start: B, array: A[]): B {
+  return array.reduce(func, start);
+}
+
+function foldr<A, B>(func: (a: A, b: B) => B, start: B, array: A[]): B {
+  return array.reduceRight((prev, cur) => func(cur, prev), start);
+}
diff --git a/src/compiler/typeInference.ts b/src/compiler/typeInference.ts
index 286ff29..88e7b7b 100644
--- a/src/compiler/typeInference.ts
+++ b/src/compiler/typeInference.ts
@@ -54,6 +54,7 @@ import {
 } from "./diagnostics";
 import fromEntries from "fromentries";
 import { isKernelProject, nameIsKernel } from "./utils/elmUtils";
+import { PatternMatches } from "./patternMatches";
 
 export let inferTime = 0;
 export function resetInferTime(): void {
@@ -1533,6 +1534,14 @@ export class InferenceScope {
       }
     });
 
+    this.diagnostics.push(
+      ...PatternMatches.check(
+        caseOfExpr,
+        caseOfExpr.branches.map((branch) => branch.pattern),
+        this.program,
+      ),
+    );
+
     return type ?? TUnknown;
   }
 
diff --git a/src/providers/codeAction/addMissingCaseBranches.ts b/src/providers/codeAction/addMissingCaseBranches.ts
new file mode 100644
index 0000000..9781216
--- /dev/null
+++ b/src/providers/codeAction/addMissingCaseBranches.ts
@@ -0,0 +1,71 @@
+import { CodeAction, Range, TextEdit } from "vscode-languageserver";
+import { CodeActionProvider } from "..";
+import { getSpaces } from "../../util/refactorEditUtils";
+import { TreeUtils } from "../../util/treeUtils";
+import { Diagnostics } from "../../compiler/diagnostics";
+import { ICodeActionParams } from "../paramsExtensions";
+import { Utils } from "../../util/utils";
+import { PatternMatches } from "../../compiler/patternMatches";
+import { PositionUtil } from "../../positionUtil";
+
+const errorCodes = [Diagnostics.IncompleteCasePattern(0).code];
+const fixId = "add_missing_case_branches";
+
+CodeActionProvider.registerCodeAction({
+  errorCodes,
+  fixId,
+  getCodeActions: (params: ICodeActionParams) => {
+    const edits = getEdits(params, params.range);
+
+    return [
+      CodeActionProvider.getCodeAction(
+        params,
+        "Add missing case branches",
+        edits,
+      ),
+    ];
+  },
+  getFixAllCodeAction: (params: ICodeActionParams): CodeAction | undefined => {
+    return CodeActionProvider.getFixAllCodeAction(
+      "Add all missing case branches",
+      params,
+      errorCodes,
+      fixId,
+      (edits, diagnostic) => {
+        edits.push(...getEdits(params, diagnostic.range));
+      },
+    );
+  },
+});
+
+function getEdits(params: ICodeActionParams, range: Range): TextEdit[] {
+  const nodeAtPosition = TreeUtils.getNamedDescendantForRange(
+    params.sourceFile,
+    range,
+  );
+
+  if (nodeAtPosition.type === "case_of_expr") {
+    const patterns = nodeAtPosition.namedChildren
+      .filter((n) => n.type === "case_of_branch")
+      .map((branch) => branch.childForFieldName("pattern"))
+      .filter(Utils.notUndefinedOrNull.bind(getEdits));
+
+    const indent = getSpaces(nodeAtPosition.startPosition.column);
+
+    const edit = PatternMatches.missing(patterns, params.program).reduce(
+      (edit, missing) => edit + `\n\n${indent}\t${missing} ->\n\t\t${indent}\t`,
+      "",
+    );
+
+    return [
+      TextEdit.insert(
+        PositionUtil.FROM_TS_POSITION(
+          nodeAtPosition.endPosition,
+        ).toVSPosition(),
+        edit,
+      ),
+    ];
+  }
+
+  return [];
+}
diff --git a/src/providers/codeAction/index.ts b/src/providers/codeAction/index.ts
index 22fa282..2691949 100644
--- a/src/providers/codeAction/index.ts
+++ b/src/providers/codeAction/index.ts
@@ -6,3 +6,4 @@ import "./exposeUnexposeCodeAction";
 import "./moveFunctionCodeAction";
 import "./installPackageCodeAction";
 import "./addMissingRecordFieldCodeAction";
+import "./addMissingCaseBranches";
diff --git a/src/providers/diagnostics/elmDiagnosticsHelper.ts b/src/providers/diagnostics/elmDiagnosticsHelper.ts
index 75997cd..92df95e 100644
--- a/src/providers/diagnostics/elmDiagnosticsHelper.ts
+++ b/src/providers/diagnostics/elmDiagnosticsHelper.ts
@@ -59,6 +59,10 @@ export class ElmDiagnosticsHelper {
       code = Diagnostics.ImportMissing.code;
     }
 
+    if (issue.overview.startsWith("MISSING PATTERNS - This `case`")) {
+      code = Diagnostics.IncompleteCasePattern(0).code;
+    }
+
     return {
       range: lineRange,
       message: `${messagePrefix}${issue.details.replace(/\[\d+m/g, "")}`,
diff --git a/src/providers/diagnostics/elmMakeDiagnostics.ts b/src/providers/diagnostics/elmMakeDiagnostics.ts
index 701a51e..60b38a9 100644
--- a/src/providers/diagnostics/elmMakeDiagnostics.ts
+++ b/src/providers/diagnostics/elmMakeDiagnostics.ts
@@ -14,13 +14,11 @@ import { ISourceFile } from "../../compiler/forest";
 import * as utils from "../../compiler/utils/elmUtils";
 import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher";
 import { Settings } from "../../util/settings";
-import { TreeUtils } from "../../util/treeUtils";
-import { NonEmptyArray, Utils } from "../../util/utils";
+import { NonEmptyArray } from "../../util/utils";
 import { IDiagnostic, IElmIssue } from "./diagnosticsProvider";
 import { ElmDiagnosticsHelper } from "./elmDiagnosticsHelper";
 import execa = require("execa");
 import { ElmToolingJsonManager } from "../../elmToolingJsonManager";
-import { IProgram } from "../../compiler/program";
 
 const ELM_MAKE = "Elm";
 export const NAMING_ERROR = "NAMING ERROR";
@@ -114,11 +112,6 @@ export class ElmMakeDiagnostics {
   ): CodeAction[] {
     const result: CodeAction[] = [];
 
-    const program = this.elmWorkspaceMatcher.getProgramFor(URI.parse(uri));
-    const forest = program.getForest();
-
-    const sourceTree = forest.getByUri(uri);
-
     diagnostics.forEach((diagnostic) => {
       if (
         diagnostic.message.startsWith(NAMING_ERROR) ||
@@ -168,145 +161,11 @@ export class ElmMakeDiagnostics {
             ),
           );
         }
-      } else if (diagnostic.message.startsWith("UNFINISHED CASE")) {
-        // Offer the case completion only if we're at the `of`
-        const regex = /^\d+\|\s*.* of\s+\s+#\^#/gm;
-
-        const matches = regex.exec(diagnostic.message);
-        if (matches !== null) {
-          result.push(
-            ...this.addCaseQuickfixes(sourceTree, diagnostic, uri, program),
-          );
-        }
-      } else if (
-        diagnostic.message.startsWith("MISSING PATTERNS - This `case`")
-      ) {
-        result.push(
-          ...this.addCaseQuickfixes(sourceTree, diagnostic, uri, program),
-        );
       }
     });
     return result;
   }
 
-  private addCaseQuickfixes(
-    sourceFile: ISourceFile | undefined,
-    diagnostic: IDiagnostic,
-    uri: string,
-    program: IProgram,
-  ): CodeAction[] {
-    const result = [];
-    const valueNode = sourceFile?.tree.rootNode.namedDescendantForPosition(
-      {
-        column: diagnostic.range.start.character,
-        row: diagnostic.range.start.line,
-      },
-      {
-        column: diagnostic.range.end.character,
-        row: diagnostic.range.end.line,
-      },
-    );
-
-    if (valueNode) {
-      if (
-        valueNode.firstNamedChild?.type === "case" &&
-        valueNode.namedChildren.length > 1 &&
-        valueNode.namedChildren[1].type === "value_expr"
-      ) {
-        const indent = "    ".repeat(
-          (valueNode.firstNamedChild?.startPosition.column % 4) + 1,
-        );
-
-        const typeDeclarationNode = TreeUtils.getTypeAliasOfCase(
-          valueNode.namedChildren[1].firstNamedChild!.firstNamedChild!,
-          sourceFile!,
-          program,
-        );
-
-        if (typeDeclarationNode) {
-          const fields = TreeUtils.findAllNamedChildrenOfType(
-            "union_variant",
-            typeDeclarationNode,
-          );
-
-          const alreadyAvailableBranches = TreeUtils.findAllNamedChildrenOfType(
-            "case_of_branch",
-            valueNode,
-          )
-            ?.map(
-              (a) => a.firstNamedChild?.firstNamedChild?.firstNamedChild?.text,
-            )
-            .filter(Utils.notUndefined.bind(this));
-
-          let edit = "";
-          fields?.forEach((unionVariant) => {
-            if (
-              !alreadyAvailableBranches?.includes(
-                unionVariant.firstNamedChild!.text,
-              )
-            ) {
-              const parameters = TreeUtils.findAllNamedChildrenOfType(
-                "type_ref",
-                unionVariant,
-              );
-
-              const caseBranch = `${[
-                unionVariant.firstNamedChild!.text,
-                parameters
-                  ?.map((a) =>
-                    a.firstNamedChild?.lastNamedChild?.text.toLowerCase(),
-                  )
-                  .join(" "),
-              ].join(" ")}`;
-
-              edit += `\n${indent}    ${caseBranch} ->\n${indent}        \n`;
-            }
-          });
-
-          result.push(
-            this.createCaseQuickFix(
-              uri,
-              edit,
-              diagnostic,
-              `Add missing case branches`,
-            ),
-          );
-        }
-      }
-
-      result.push(
-        this.createCaseQuickFix(
-          uri,
-          "\n\n        _ ->\n    ",
-          diagnostic,
-          `Add \`_\` branch`,
-        ),
-      );
-    }
-    return result;
-  }
-
-  private createCaseQuickFix(
-    uri: string,
-    replaceWith: string,
-    diagnostic: IDiagnostic,
-    title: string,
-  ): CodeAction {
-    const map: {
-      [uri: string]: TextEdit[];
-    } = {};
-    if (!map[uri]) {
-      map[uri] = [];
-    }
-    map[uri].push(TextEdit.insert(diagnostic.range.end, replaceWith));
-    return {
-      diagnostics: [diagnostic],
-      edit: { changes: map },
-      kind: CodeActionKind.QuickFix,
-      title,
-    };
-  }
-
   private createQuickFix(
     uri: string,
     replaceWith: string,


Test patch:
diff --git a/test/diagnosticTests/elmDiagnostics.test.ts b/test/diagnosticTests/elmDiagnostics.test.ts
index 8806de4..6dc2660 100644
--- a/test/diagnosticTests/elmDiagnostics.test.ts
+++ b/test/diagnosticTests/elmDiagnostics.test.ts
@@ -119,8 +119,11 @@ describe("test elm diagnostics", () => {
       );
     }
 
-    // Adjust for Type(..)
-    if (nodeAtPosition?.nextNamedSibling?.text === "(..)") {
+    // Adjust for Type(..) or case
+    if (
+      nodeAtPosition?.nextNamedSibling?.text === "(..)" ||
+      nodeAtPosition?.type === "case"
+    ) {
       nodeAtPosition = nodeAtPosition.parent ?? nodeAtPosition;
     }
 
@@ -1268,4 +1271,139 @@ bar = ""
 `;
     await testTypeInference(basicsSources + stringSources + source2, [], true);
   });
+
+  test("test missing case patterns should have an error - ctor and tuple", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+type Result e a = Ok a | Error e
+
+func result =
+    case result of
+    --^
+        Ok _ ->
+            ""
+  `;
+    await testTypeInference(
+      basicsSources + source,
+      [{ message: Diagnostics.IncompleteCasePattern(1), args: ["Error _"] }],
+      true,
+    );
+
+    const source2 = `
+--@ Test.elm
+module Test exposing (..)
+
+type Maybe a = Just a | Nothing
+
+func result =
+    case result of
+    --^
+        (Just _, _) ->
+            ""
+  `;
+    await testTypeInference(
+      basicsSources + source2,
+      [
+        {
+          message: Diagnostics.IncompleteCasePattern(1),
+          args: ["( Nothing, _ )"],
+        },
+      ],
+      true,
+    );
+
+    const source3 = `
+--@ Test.elm
+module Test exposing (..)
+
+type Msg = One | Two | Three Int Int | Four
+
+func result =
+    case result of
+    --^
+        One ->
+            ""
+  `;
+    await testTypeInference(
+      basicsSources + source3,
+      [
+        {
+          message: Diagnostics.IncompleteCasePattern(3),
+          args: ["Two", "Three _ _", "Four"],
+        },
+      ],
+      true,
+    );
+
+    const source4 = `
+--@ Test.elm
+module Test exposing (..)
+
+type Maybe a = Just a | Nothing
+
+type Msg = One | Two | Three (Maybe Int) | Four
+
+func result =
+    case result of
+    --^
+        One ->
+            ""
+  `;
+    await testTypeInference(
+      basicsSources + source4,
+      [
+        {
+          message: Diagnostics.IncompleteCasePattern(3),
+          args: ["Two", "Three _", "Four"],
+        },
+      ],
+      true,
+    );
+
+    const source5 = `
+--@ Test.elm
+module Test exposing (..)
+
+type Maybe a = Just a | Nothing
+
+func result =
+    case result of
+    --^
+        Nothing ->
+            ""
+        
+        Just (Just a) ->
+            a
+  `;
+    await testTypeInference(
+      basicsSources + source5,
+      [
+        {
+          message: Diagnostics.IncompleteCasePattern(1),
+          args: ["Just Nothing"],
+        },
+      ],
+      true,
+    );
+  });
+
+  test("test missing case patterns should have an error - cons and list", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func result =
+    case result of
+    --^
+        a :: b ->
+            ""
+  `;
+    await testTypeInference(
+      basicsSources + source,
+      [{ message: Diagnostics.IncompleteCasePattern(1), args: ["[]"] }],
+      true,
+    );
+  });
 });