diff --git a/jscomp/bsc/rescript_compiler_main.ml b/jscomp/bsc/rescript_compiler_main.ml
index 16e927f42b..369abd3fcb 100644
--- a/jscomp/bsc/rescript_compiler_main.ml
+++ b/jscomp/bsc/rescript_compiler_main.ml
@@ -273,6 +273,9 @@ let buckle_script_flags : (string * Bsc_args.spec * string) array =
     "-bs-ast", unit_call(fun _ ->  Js_config.binary_ast := true; Js_config.syntax_only := true),
     "*internal* Generate binary .mli_ast and ml_ast and stop";
 
+    "-code-action-data", unit_call(fun _ ->  Js_config.code_action_data := true),
+    "*internal* Emit code action data";
+
     "-bs-syntax-only", set Js_config.syntax_only,
     "*internal* Only check syntax";
 
diff --git a/jscomp/build_tests/editor_tooling_enhancements/.gitignore b/jscomp/build_tests/editor_tooling_enhancements/.gitignore
new file mode 100644
index 0000000000..e0473fd9cf
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/.gitignore
@@ -0,0 +1 @@
+fixtures/*.js
diff --git a/jscomp/build_tests/editor_tooling_enhancements/README.md b/jscomp/build_tests/editor_tooling_enhancements/README.md
new file mode 100644
index 0000000000..12ae608779
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/README.md
@@ -0,0 +1,5 @@
+Special tests for the editor tooling enhancements mode.
+
+Follow CONTRIBUTING.md and build the project, then run `node ./jscomp/build_tests/editor_tooling_enhancements/input.js` at the root of the project to check the tests against previous snapshots.
+
+Run `node ./jscomp/build_tests/editor_tooling_enhancements/input.js update` to update the snapshots.
diff --git a/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected b/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected
new file mode 100644
index 0000000000..780d265269
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected
@@ -0,0 +1,16 @@
+
+  We've found a bug for you!
+  /.../fixtures/replace_name.res:8:3-6
+
+   6 │ let x: x = {
+   7 │   name: "hello",
+   8 │   agee: 10,
+   9 │ }
+  10 │ 
+
+  The field agee does not belong to type x
+  
+  This record expression is expected to have type x
+Hint: Did you mean age?
+=== CODE ACTIONS ===
+[{"title": "Replace with `agee`", "kind": "quickfix" "loc": {"start": {"line": 8, "col": 74},"end": {"line": 8, "col": 78}}, "type": "replaceWith", "replaceWith": "agee"}]
\ No newline at end of file
diff --git a/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected b/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected
new file mode 100644
index 0000000000..64a3618793
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected
@@ -0,0 +1,16 @@
+
+  We've found a bug for you!
+  /.../fixtures/wrap_option_in_some.res:2:3
+
+  1 │ switch Some(1) {
+  2 │ | 1 => ()
+  3 │ | _ => ()
+  4 │ }
+
+  This pattern matches values of type int
+  but a pattern was expected which matches values of type option<int>
+  
+  The value you're pattern matching on here is wrapped in an option, but you're trying to match on the actual value.
+  Wrap the highlighted pattern in Some() to make it work.
+=== CODE ACTIONS ===
+[{"title": "Wrap in `Some()`", "kind": "quickfix" "loc": {"start": {"line": 2, "col": 19},"end": {"line": 2, "col": 20}}, "type": "wrapWith", "wrapLeft": "Some(", "wrapRight": ")"}]
\ No newline at end of file
diff --git a/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res b/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res
new file mode 100644
index 0000000000..b154fbaf76
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res
@@ -0,0 +1,9 @@
+type x = {
+  name: string,
+  age: int,
+}
+
+let x: x = {
+  name: "hello",
+  agee: 10,
+}
diff --git a/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res b/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res
new file mode 100644
index 0000000000..1674431e5c
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res
@@ -0,0 +1,4 @@
+switch Some(1) {
+| 1 => ()
+| _ => ()
+}
diff --git a/jscomp/build_tests/editor_tooling_enhancements/input.js b/jscomp/build_tests/editor_tooling_enhancements/input.js
new file mode 100644
index 0000000000..64d8890b56
--- /dev/null
+++ b/jscomp/build_tests/editor_tooling_enhancements/input.js
@@ -0,0 +1,64 @@
+const fs = require("fs");
+const path = require("path");
+const child_process = require("child_process");
+
+const { bsc_exe: bsc } = require("#cli/bin_path");
+
+const expectedDir = path.join(__dirname, "expected");
+
+const fixtures = fs
+  .readdirSync(path.join(__dirname, "fixtures"))
+  .filter((fileName) => path.extname(fileName) === ".res");
+
+// const runtime = path.join(__dirname, '..', '..', 'runtime')
+const prefix = `${bsc} -w +A -code-action-data`;
+
+const updateTests = process.argv[2] === "update";
+
+function postProcessErrorOutput(output) {
+  output = output.trimRight();
+  output = output.replace(
+    /\/[^ ]+?jscomp\/build_tests\/editor_tooling_enhancements\//g,
+    "/.../"
+  );
+  return output;
+}
+
+let doneTasksCount = 0;
+let atLeastOneTaskFailed = false;
+
+fixtures.forEach((fileName) => {
+  const fullFilePath = path.join(__dirname, "fixtures", fileName);
+  const command = `${prefix} -color always ${fullFilePath}`;
+  console.log(`running ${command}`);
+  child_process.exec(command, (err, stdout, stderr) => {
+    doneTasksCount++;
+    // careful of:
+    // - warning test that actually succeeded in compiling (warning's still in stderr, so the code path is shared here)
+    // - accidentally succeeding tests (not likely in this context),
+    // actual, correctly erroring test case
+    const actualErrorOutput = postProcessErrorOutput(stderr.toString());
+    const expectedFilePath = path.join(expectedDir, fileName + ".expected");
+    if (updateTests) {
+      fs.writeFileSync(expectedFilePath, actualErrorOutput);
+    } else {
+      const expectedErrorOutput = postProcessErrorOutput(
+        fs.readFileSync(expectedFilePath, { encoding: "utf-8" })
+      );
+      if (expectedErrorOutput !== actualErrorOutput) {
+        console.error(
+          `The old and new error output for the test ${fullFilePath} aren't the same`
+        );
+        console.error("\n=== Old:");
+        console.error(expectedErrorOutput);
+        console.error("\n=== New:");
+        console.error(actualErrorOutput);
+        atLeastOneTaskFailed = true;
+      }
+
+      if (doneTasksCount === fixtures.length && atLeastOneTaskFailed) {
+        process.exit(1);
+      }
+    }
+  });
+});
diff --git a/jscomp/common/js_config.ml b/jscomp/common/js_config.ml
index 84f4e22f68..c5854cc3b1 100644
--- a/jscomp/common/js_config.ml
+++ b/jscomp/common/js_config.ml
@@ -57,6 +57,7 @@ let all_module_aliases = ref false
 let no_stdlib = ref false
 let no_export = ref false
 let as_ppx = ref false
+let code_action_data = ref false
 
 let int_of_jsx_version = function
   | Jsx_v3 -> 3
diff --git a/jscomp/common/js_config.mli b/jscomp/common/js_config.mli
index 31855eaca7..58253c291a 100644
--- a/jscomp/common/js_config.mli
+++ b/jscomp/common/js_config.mli
@@ -112,3 +112,5 @@ val as_pp : bool ref
 val self_stack : string Stack.t
 
 val modules : bool ref
+
+val code_action_data : bool ref
diff --git a/jscomp/ml/code_action_data.ml b/jscomp/ml/code_action_data.ml
new file mode 100644
index 0000000000..aa8cc91dc7
--- /dev/null
+++ b/jscomp/ml/code_action_data.ml
@@ -0,0 +1,89 @@
+type code_action_type = WrapWith of {left: string; right: string} | ReplaceWith of string
+type code_action_style = Regular | QuickFix
+type code_action = {
+  style: code_action_style;
+  type_: code_action_type;
+  title: string;
+}
+
+let code_actions_enabled = ref true
+
+let code_action_data = ref []
+let add_code_action (data : code_action) =
+  code_action_data := data :: !code_action_data
+let get_code_action_data () = !code_action_data
+
+let escape text =
+  let ln = String.length text in
+  let buf = Buffer.create ln in
+  let rec loop i =
+    if i < ln then (
+      (match text.[i] with
+      | '\012' -> Buffer.add_string buf "\\f"
+      | '\\' -> Buffer.add_string buf "\\\\"
+      | '"' -> Buffer.add_string buf "\\\""
+      | '\n' -> Buffer.add_string buf "\\n"
+      | '\b' -> Buffer.add_string buf "\\b"
+      | '\r' -> Buffer.add_string buf "\\r"
+      | '\t' -> Buffer.add_string buf "\\t"
+      | c -> Buffer.add_char buf c);
+      loop (i + 1))
+  in
+  loop 0;
+  Buffer.contents buf
+
+let loc_to_json loc =
+  Printf.sprintf
+    "{\"start\": {\"line\": %s, \"col\": %s},\"end\": {\"line\": %s, \"col\": \
+     %s}}"
+    (loc.Location.loc_start.pos_lnum |> string_of_int)
+    (loc.loc_start.pos_cnum |> string_of_int)
+    (loc.loc_end.pos_lnum |> string_of_int)
+    (loc.loc_end.pos_cnum |> string_of_int)
+
+let code_action_type_to_json = function
+  | WrapWith {left; right} ->
+    Printf.sprintf "\"type\": \"wrapWith\", \"wrapLeft\": \"%s\", \"wrapRight\": \"%s\""
+      (escape left) (escape right)
+  | ReplaceWith text ->
+    Printf.sprintf "\"type\": \"replaceWith\", \"replaceWith\": \"%s\""
+      (escape text)
+
+let emit_code_actions_data loc ppf =
+  match !code_action_data with
+  | [] -> ()
+  | code_actions ->
+    Format.fprintf ppf "@\n=== CODE ACTIONS ===@\n[";
+    Format.fprintf ppf "%s"
+      (code_actions
+      |> List.map (fun data ->
+            Format.sprintf
+              "{\"title\": \"%s\", \"kind\": \"%s\" \"loc\": %s, %s}"
+              (escape data.title)
+              (match data.style with
+              | Regular -> "regular"
+              | QuickFix -> "quickfix")
+              (loc_to_json loc)
+              (code_action_type_to_json data.type_))
+      |> String.concat ",");
+    Format.fprintf ppf "]"
+
+
+module Actions = struct
+  let add_replace_with name =
+    if !code_actions_enabled then
+      add_code_action
+        {
+          style = QuickFix;
+          type_ = ReplaceWith name;
+          title = "Replace with `" ^ name ^ "`";
+        }
+  let add_wrap_in_constructor name =
+    if !code_actions_enabled then
+      add_code_action
+        {
+          style = QuickFix;
+          type_ = WrapWith {left = name ^ "("; right = ")"};
+          title = "Wrap in `" ^ name ^ "()`";
+        }
+end
\ No newline at end of file
diff --git a/jscomp/ml/code_action_data.mli b/jscomp/ml/code_action_data.mli
new file mode 100644
index 0000000000..eca254d803
--- /dev/null
+++ b/jscomp/ml/code_action_data.mli
@@ -0,0 +1,17 @@
+type code_action_type = WrapWith of {left: string; right: string} | ReplaceWith of string
+type code_action_style = Regular | QuickFix
+type code_action = {
+    style: code_action_style;
+    type_: code_action_type;
+    title: string;
+}
+
+val add_code_action: code_action -> unit
+val get_code_action_data: unit -> code_action list
+
+val emit_code_actions_data: Location.t -> Format.formatter -> unit
+
+module Actions : sig
+  val add_replace_with: string -> unit
+  val add_wrap_in_constructor: string -> unit
+end
\ No newline at end of file
diff --git a/jscomp/ml/error_message_utils.ml b/jscomp/ml/error_message_utils.ml
index d178f2129f..e332820874 100644
--- a/jscomp/ml/error_message_utils.ml
+++ b/jscomp/ml/error_message_utils.ml
@@ -234,6 +234,7 @@ let print_contextual_unification_error ppf t1 t2 =
   | Tconstr (p1, _, _), Tconstr (p2, _, _)
     when Path.same p2 Predef.path_option
           && Path.same p1 Predef.path_option <> true ->
+    Code_action_data.Actions.add_wrap_in_constructor "Some";
     fprintf ppf
       "@,@\n\
         @[<v 0>The value you're pattern matching on here is wrapped in an \
diff --git a/jscomp/ml/typecore.ml b/jscomp/ml/typecore.ml
index 564a828db4..43a5871f60 100644
--- a/jscomp/ml/typecore.ml
+++ b/jscomp/ml/typecore.ml
@@ -636,6 +636,14 @@ let simple_conversions = [
 let print_simple_conversion ppf (actual, expected) =
   try (
     let converter = List.assoc (actual, expected) simple_conversions in
+    Code_action_data.add_code_action {
+      Code_action_data.style = QuickFix; 
+      type_ = WrapWith {
+        left = converter ^ "("; 
+        right = ")"
+      };
+      title = Printf.sprintf "Convert %s to %s" actual expected
+    };
     fprintf ppf "@,@,@[<v 2>You can convert @{<info>%s@} to @{<info>%s@} with @{<info>%s@}.@]" actual expected converter
   ) with | Not_found -> ()
   
@@ -3746,6 +3754,7 @@ let type_expression env sexp =
 (* Error report *)
 
 let spellcheck ppf unbound_name valid_names =
+  Code_action_data.Actions.add_replace_with unbound_name;
   Misc.did_you_mean ppf (fun () ->
     Misc.spellcheck valid_names unbound_name
   )
@@ -4045,14 +4054,14 @@ let report_error env ppf = function
 let super_report_error_no_wrap_printing_env = report_error
 
 
-let report_error env ppf err =
-  Printtyp.wrap_printing_env env (fun () -> report_error env ppf err)
+let report_error loc env ppf err =
+  Printtyp.wrap_printing_env env (fun () -> report_error env ppf err; Code_action_data.emit_code_actions_data loc ppf;)
 
 let () =
   Location.register_error_of_exn
     (function
       | Error (loc, env, err) ->
-        Some (Location.error_of_printer loc (report_error env) err)
+        Some (Location.error_of_printer loc (report_error loc env) err)
       | Error_forward err ->
         Some err
       | _ ->
diff --git a/jscomp/ml/typecore.mli b/jscomp/ml/typecore.mli
index 4b1fcc5368..3ba25b1b3e 100644
--- a/jscomp/ml/typecore.mli
+++ b/jscomp/ml/typecore.mli
@@ -114,7 +114,7 @@ exception Error_forward of Location.error
 val super_report_error_no_wrap_printing_env: Env.t -> formatter -> error -> unit
 
 
-val report_error: Env.t -> formatter -> error -> unit
+val report_error: Location.t -> Env.t -> formatter -> error -> unit
  (* Deprecated.  Use Location.{error_of_exn, report_error}. *)
 
 (* Forward declaration, to be filled in by Typemod.type_module *)