···11+MIT License
22+33+Copyright (c) 2025 Arthur Wendling, Tarides
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+44
x-ocaml/README.md
···11+Embed OCaml notebooks in any web page thanks to WebComponents! Just copy and paste the following script in your html page source to load the integration:
22+33+```html
44+<script async
55+ src="https://cdn.jsdelivr.net/gh/art-w/x-ocaml.js@6/x-ocaml.js"
66+ src-worker="https://cdn.jsdelivr.net/gh/art-w/x-ocaml.js@6/x-ocaml.worker+effects.js"
77+ integrity="sha256-3ITn2LRgP/8Rz6oqP5ZQTysesNaSi6/iEdbDvBfyCSE="
88+ crossorigin="anonymous"
99+></script>
1010+```
1111+1212+This will introduce a new html tag `<x-ocaml>` to present OCaml code, for example:
1313+1414+```html
1515+<x-ocaml>let x = 42</x-ocaml>
1616+```
1717+1818+The script will initialize a CodeMirror editor integrated with the OCaml interpreter, Merlin and OCamlformat (all running in a web worker). [**Check out the online demo**](https://art-w.github.io/x-ocaml/) for more details, including how to load additional OCaml libraries and ppx in your page.
1919+2020+For an even easier integration, @patricoferris made a command-line tool [`xocmd`](https://github.com/patricoferris/xocmd) to convert markdown files to use `<x-ocaml>`!
2121+2222+## Compilation
2323+2424+To avoid relying on a public CDN and host your own copy of the `x-ocaml` scripts, you can reproduce the javascript files with:
2525+2626+```shell
2727+$ git clone --recursive https://github.com/art-w/x-ocaml
2828+$ cd x-ocaml
2929+3030+# Install the dependencies with either dune:
3131+x-ocaml/ $ dune pkg lock
3232+# Or with opam:
3333+x-ocaml/ $ opam update && opam install . --deps-only
3434+3535+# Make sure to use the release profile to optimize the js file size
3636+x-ocaml/ $ dune build --profile=release
3737+3838+x-ocaml/ $ ls *.js
3939+x-ocaml.js x-ocaml.worker+effects.js x-ocaml.worker.js
4040+```
4141+4242+## Acknowledgments
4343+4444+This project was heavily inspired by the amazing [`sketch.sh`](https://sketch.sh), [@jonludlam's notebooks in Odoc](https://jon.recoil.org/notebooks/foundations/foundations1.html#a-first-session-with-ocaml), [`blogaml` by @panglesd](https://github.com/panglesd/blogaml), and all the wonderful people who made [Try OCaml](https://try.ocamlpro.com/) and other online playgrounds! It was made possible thanks to the invaluable [`js_of_ocaml-toplevel`](https://github.com/ocsigen/js_of_ocaml) library, the magical [`merlin-js` by @voodoos](https://github.com/voodoos/merlin-js), the excellent [CodeMirror bindings by @patricoferris](https://github.com/patricoferris/jsoo-code-mirror/), the guidance of @Julow on `ocamlformat` and the javascript expertise of @xvw.
···11+let mapper _argv =
22+ let module Current_ast = Ppxlib_ast.Selected_ast in
33+ let structure s =
44+ match s with [] -> [] | _ -> Ppxlib.Driver.map_structure s
55+ in
66+ let structure _ st =
77+ Current_ast.of_ocaml Structure st
88+ |> structure
99+ |> Current_ast.to_ocaml Structure
1010+ in
1111+ let signature _ si =
1212+ Current_ast.of_ocaml Signature si
1313+ |> Ppxlib.Driver.map_signature
1414+ |> Current_ast.to_ocaml Signature
1515+ in
1616+ { Ast_mapper.default_mapper with structure; signature }
1717+1818+let () = Ast_mapper.register "ppxlib" mapper
···11+module Merlin_protocol = Protocol
22+33+type id = int
44+55+type request =
66+ | Merlin of id * Merlin_protocol.action
77+ | Eval of id * int * string
88+ | Format of id * string
99+ | Format_config of string
1010+ | Setup
1111+1212+type output =
1313+ | Stdout of string
1414+ | Stderr of string
1515+ | Meta of string
1616+ | Html of string
1717+1818+type response =
1919+ | Merlin_response of id * Merlin_protocol.answer
2020+ | Top_response of id * output list
2121+ | Top_response_at of id * int * output list
2222+ | Formatted_source of id * string
2323+2424+let req_to_bytes (req : request) = Marshal.to_bytes req []
2525+let resp_to_bytes (req : response) = Marshal.to_bytes req []
2626+let req_of_bytes req : request = Marshal.from_bytes req 0
2727+let resp_of_string resp : response = Marshal.from_string resp 0
+190
x-ocaml/src/cell.ml
···11+open Brr
22+33+type status = Not_run | Running | Run_ok | Request_run
44+55+type t = {
66+ id : int;
77+ mutable prev : t option;
88+ mutable next : t option;
99+ mutable status : status;
1010+ cm : Editor.t;
1111+ worker : Client.t;
1212+ merlin_worker : Merlin_ext.Client.worker;
1313+ run_on : [ `Click | `Load ];
1414+}
1515+1616+let id t = t.id
1717+1818+let pre_source t =
1919+ let rec go acc t =
2020+ match t.prev with
2121+ | None -> String.concat "\n" acc
2222+ | Some e -> go (Editor.source e.cm :: acc) e
2323+ in
2424+ let s = go [] t in
2525+ if s = "" then s else s ^ " ;;\n"
2626+2727+let rec invalidate_from ~editor =
2828+ editor.status <- Not_run;
2929+ Editor.clear editor.cm;
3030+ let count = Editor.nb_lines editor.cm in
3131+ match editor.next with
3232+ | None -> ()
3333+ | Some editor ->
3434+ Editor.set_previous_lines editor.cm count;
3535+ invalidate_from ~editor
3636+3737+let invalidate_after ~editor =
3838+ editor.status <- Not_run;
3939+ let count = Editor.nb_lines editor.cm in
4040+ match editor.next with
4141+ | None -> ()
4242+ | Some editor ->
4343+ Editor.set_previous_lines editor.cm count;
4444+ invalidate_from ~editor
4545+4646+let rec refresh_lines_from ~editor =
4747+ let count = Editor.nb_lines editor.cm in
4848+ match editor.next with
4949+ | None -> ()
5050+ | Some editor ->
5151+ Editor.set_previous_lines editor.cm count;
5252+ refresh_lines_from ~editor
5353+5454+let rec run editor =
5555+ if editor.status = Running then ()
5656+ else (
5757+ editor.status <- Request_run;
5858+ Editor.clear_messages editor.cm;
5959+ match editor.prev with
6060+ | Some e when e.status <> Run_ok -> run e
6161+ | _ ->
6262+ editor.status <- Running;
6363+ let code_txt = Editor.source editor.cm in
6464+ let line_number = 1 + Editor.get_previous_lines editor.cm in
6565+ Client.eval ~id:editor.id ~line_number editor.worker code_txt)
6666+6767+let set_prev ~prev t =
6868+ let () = match t.prev with None -> () | Some prev -> prev.next <- None in
6969+ t.prev <- prev;
7070+ match prev with
7171+ | None ->
7272+ Editor.set_previous_lines t.cm 0;
7373+ refresh_lines_from ~editor:t
7474+ | Some p ->
7575+ assert (p.next = None);
7676+ p.next <- Some t;
7777+ refresh_lines_from ~editor:p
7878+7979+let set_source_from_html editor this =
8080+ let doc = Webcomponent.text_content this in
8181+ let doc = String.trim doc in
8282+ Editor.set_source editor.cm doc;
8383+ invalidate_from ~editor;
8484+ Client.fmt ~id:editor.id editor.worker doc
8585+8686+let init_css shadow ~extra_style ~inline_style =
8787+ El.append_children shadow
8888+ [
8989+ El.style
9090+ (El.txt (Jstr.of_string [%blob "style.css"])
9191+ ::
9292+ (match inline_style with
9393+ | None -> []
9494+ | Some inline_style ->
9595+ [
9696+ El.txt
9797+ @@ Jstr.of_string (":host{" ^ Jstr.to_string inline_style ^ "}");
9898+ ]));
9999+ ];
100100+ match extra_style with
101101+ | None -> ()
102102+ | Some src_style ->
103103+ El.append_children shadow
104104+ [
105105+ El.link
106106+ ~at:
107107+ [
108108+ At.href src_style;
109109+ At.rel (Jstr.of_string "stylesheet");
110110+ At.type' (Jstr.of_string "text/css");
111111+ ]
112112+ ();
113113+ ]
114114+115115+let init ~id ~run_on ?extra_style ?inline_style worker this =
116116+ let shadow = Webcomponent.attach_shadow this in
117117+ init_css shadow ~extra_style ~inline_style;
118118+119119+ let run_btn = El.button [ El.txt (Jstr.of_string "Run") ] in
120120+ El.append_children shadow
121121+ [ El.div ~at:[ At.class' (Jstr.of_string "run_btn") ] [ run_btn ] ];
122122+123123+ let cm = Editor.make shadow in
124124+125125+ let merlin = Merlin_ext.make ~id worker in
126126+ let merlin_worker = Merlin_ext.Client.make_worker merlin in
127127+ let editor =
128128+ {
129129+ id;
130130+ status = Not_run;
131131+ cm;
132132+ prev = None;
133133+ next = None;
134134+ worker;
135135+ merlin_worker;
136136+ run_on;
137137+ }
138138+ in
139139+ Editor.on_change cm (fun () -> invalidate_after ~editor);
140140+ set_source_from_html editor this;
141141+142142+ Merlin_ext.set_context merlin (fun () -> pre_source editor);
143143+ Editor.configure_merlin cm (fun () -> Merlin_ext.extensions merlin_worker);
144144+145145+ let () =
146146+ Mutation_observer.observe ~target:(Webcomponent.as_target this)
147147+ @@ Mutation_observer.create (fun _ _ -> set_source_from_html editor this)
148148+ in
149149+150150+ let _ : Ev.listener =
151151+ Ev.listen Ev.click (fun _ev -> run editor) (El.as_target run_btn)
152152+ in
153153+154154+ editor
155155+156156+let set_source editor doc =
157157+ Editor.set_source editor.cm doc;
158158+ refresh_lines_from ~editor
159159+160160+let render_message msg =
161161+ let raw_html s =
162162+ let el = El.div [] in
163163+ let el_t = El.to_jv el in
164164+ Jv.set el_t "innerHTML" (Jv.of_jstr @@ Jstr.of_string s);
165165+ el
166166+ in
167167+ let kind, text =
168168+ match msg with
169169+ | X_protocol.Stdout str -> ("stdout", El.txt' str)
170170+ | Stderr str -> ("stderr", El.txt' str)
171171+ | Meta str -> ("meta", El.txt' str)
172172+ | Html str -> ("html", raw_html str)
173173+ in
174174+ El.pre ~at:[ At.class' (Jstr.of_string ("caml_" ^ kind)) ] [ text ]
175175+176176+let add_message t loc msg =
177177+ Editor.add_message t.cm loc (List.map render_message msg)
178178+179179+let completed_run ed msg =
180180+ (if msg <> [] then
181181+ let loc = String.length (Editor.source ed.cm) in
182182+ add_message ed loc msg);
183183+ ed.status <- Run_ok;
184184+ match ed.next with Some e when e.status = Request_run -> run e | _ -> ()
185185+186186+let receive_merlin t msg =
187187+ Merlin_ext.Client.on_message t.merlin_worker
188188+ (Merlin_ext.fix_answer ~pre:(pre_source t) ~doc:(Editor.source t.cm) msg)
189189+190190+let loadable t = t.run_on = `Load
+19
x-ocaml/src/cell.mli
···11+type t
22+33+val init :
44+ id:int ->
55+ run_on:[ `Click | `Load ] ->
66+ ?extra_style:Jstr.t ->
77+ ?inline_style:Jstr.t ->
88+ Client.t ->
99+ Webcomponent.t ->
1010+ t
1111+1212+val id : t -> int
1313+val set_source : t -> string -> unit
1414+val add_message : t -> int -> X_protocol.output list -> unit
1515+val completed_run : t -> X_protocol.output list -> unit
1616+val set_prev : prev:t option -> t -> unit
1717+val receive_merlin : t -> Protocol.answer -> unit
1818+val loadable : t -> bool
1919+val run : t -> unit
+63
x-ocaml/src/client.ml
···11+module Worker = Brr_webworkers.Worker
22+open Brr
33+44+type t = Worker.t
55+66+let current_url =
77+ let url = Window.location G.window in
88+ let path = Jstr.to_string (Uri.path url) in
99+ let url =
1010+ match List.rev (String.split_on_char '/' path) with
1111+ | [] | "" :: _ -> url
1212+ | _ :: rev_path -> (
1313+ let path = Jstr.of_string @@ String.concat "/" @@ List.rev rev_path in
1414+ match Uri.with_uri ~path ~query:Jstr.empty ~fragment:Jstr.empty url with
1515+ | Ok url -> url
1616+ | Error _ -> url)
1717+ in
1818+ Jstr.to_string (Uri.to_jstr url)
1919+2020+let absolute_url url =
2121+ if
2222+ not
2323+ (String.starts_with ~prefix:"http:" url
2424+ || String.starts_with ~prefix:"https:" url)
2525+ then current_url ^ url
2626+ else url
2727+2828+let wrap_url ?extra_load url =
2929+ let url = absolute_url url in
3030+ let extra =
3131+ match extra_load with
3232+ | None -> ""
3333+ | Some extra -> "','" ^ absolute_url extra
3434+ in
3535+ let script = "importScripts('" ^ url ^ extra ^ "');" in
3636+ let script = Jstr.of_string script in
3737+ let url =
3838+ match Base64.(encode (data_of_binary_jstr script)) with
3939+ | Ok data -> Jstr.to_string data
4040+ | Error _ -> assert false
4141+ in
4242+ "data:text/javascript;base64," ^ url
4343+4444+let make ?extra_load url =
4545+ Worker.create @@ Jstr.of_string @@ wrap_url ?extra_load url
4646+4747+let on_message t fn =
4848+ let fn m =
4949+ let m = Ev.as_type m in
5050+ let msg = Bytes.to_string @@ Brr_io.Message.Ev.data m in
5151+ fn (X_protocol.resp_of_string msg)
5252+ in
5353+ let _listener =
5454+ Ev.listen Brr_io.Message.Ev.message fn @@ Worker.as_target t
5555+ in
5656+ ()
5757+5858+let post worker msg = Worker.post worker (X_protocol.req_to_bytes msg)
5959+6060+let eval ~id ~line_number worker code =
6161+ post worker (Eval (id, line_number, code))
6262+6363+let fmt ~id worker code = post worker (Format (id, code))
+7
x-ocaml/src/client.mli
···11+type t
22+33+val make : ?extra_load:string -> string -> t
44+val on_message : t -> (X_protocol.response -> unit) -> unit
55+val post : t -> X_protocol.request -> unit
66+val eval : id:int -> line_number:int -> t -> string -> unit
77+val fmt : id:int -> t -> string -> unit
···11+type t = {
22+ view : Code_mirror.Editor.View.t;
33+ messages_comp : Code_mirror.Compartment.t;
44+ lines_comp : Code_mirror.Compartment.t;
55+ merlin_comp : Code_mirror.Compartment.t;
66+ mutable merlin_extension : unit -> Code_mirror.Extension.t list;
77+ changes : Code_mirror.Compartment.t;
88+ mutable previous_lines : int;
99+ mutable current_doc : string;
1010+ mutable messages : (int * Brr.El.t list) list;
1111+}
1212+1313+let find_line_ends at doc =
1414+ let rec go i =
1515+ if i >= String.length doc || doc.[i] = '\n' then i else go (i + 1)
1616+ in
1717+ go at
1818+1919+let render_messages cm =
2020+ let open Code_mirror.Editor in
2121+ let open Code_mirror.Decoration in
2222+ let (State.Facet ((module F), it)) = View.decorations () in
2323+ let doc = cm.current_doc in
2424+ let ranges =
2525+ Array.of_list
2626+ @@ List.map (fun (at, msg) ->
2727+ range ~from:at ~to_:at
2828+ @@ widget ~block:true ~side:99
2929+ @@ Widget.make (fun () -> msg))
3030+ @@ List.filter (fun (at, _) -> at <= String.length doc)
3131+ @@ List.map (fun (at, msg) ->
3232+ let at = find_line_ends at doc in
3333+ (at, msg))
3434+ @@ List.concat
3535+ @@ List.map (fun (loc, lst) -> List.map (fun m -> (loc, m)) lst)
3636+ @@ List.sort (fun (a, _) (b, _) -> Int.compare a b) cm.messages
3737+ in
3838+ F.of_ it (Range_set.of' ranges)
3939+4040+let refresh_messages ed =
4141+ Code_mirror.Editor.View.dispatch ed.view
4242+ (Code_mirror.Compartment.reconfigure ed.messages_comp
4343+ [ render_messages ed ])
4444+4545+let custom_ln editor =
4646+ Code_mirror.Editor.View.line_numbers (fun x ->
4747+ string_of_int (editor.previous_lines + x))
4848+4949+let refresh_lines ed =
5050+ Code_mirror.Editor.View.dispatch ed.view
5151+ @@ Code_mirror.Compartment.reconfigure ed.lines_comp [ custom_ln ed ]
5252+5353+let refresh_merlin ed =
5454+ Code_mirror.Editor.View.dispatch ed.view
5555+ @@ Code_mirror.Compartment.reconfigure ed.merlin_comp (ed.merlin_extension ())
5656+5757+let configure_merlin ed extension =
5858+ ed.merlin_extension <- extension;
5959+ refresh_merlin ed
6060+6161+let clear x =
6262+ x.messages <- [];
6363+ refresh_lines x;
6464+ refresh_messages x;
6565+ refresh_merlin x
6666+6767+let source_of_state s =
6868+ String.concat "\n" @@ Array.to_list @@ Array.map Jstr.to_string
6969+ @@ Code_mirror.Text.to_jstr_array
7070+ @@ Code_mirror.Editor.State.doc s
7171+7272+let source t = source_of_state @@ Code_mirror.Editor.View.state t.view
7373+7474+let prefix_length a b =
7575+ let rec go i =
7676+ if i >= String.length a || i >= String.length b || a.[i] <> b.[i] then i
7777+ else go (i + 1)
7878+ in
7979+ go 0
8080+8181+let basic_setup =
8282+ Jv.get Jv.global "__CM__basic_setup" |> Code_mirror.Extension.of_jv
8383+8484+let make parent =
8585+ let open Code_mirror.Editor in
8686+ let changes = Code_mirror.Compartment.make () in
8787+ let messages = Code_mirror.Compartment.make () in
8888+ let lines = Code_mirror.Compartment.make () in
8989+ let merlin = Code_mirror.Compartment.make () in
9090+ let extensions =
9191+ [|
9292+ basic_setup;
9393+ Code_mirror.Editor.View.line_wrapping ();
9494+ Code_mirror.Compartment.of' lines [];
9595+ Code_mirror.Compartment.of' messages [];
9696+ Code_mirror.Compartment.of' changes [];
9797+ Code_mirror.Compartment.of' merlin [];
9898+ |]
9999+ in
100100+ let config = State.Config.create ~doc:Jstr.empty ~extensions () in
101101+ let state = State.create ~config () in
102102+ let opts = View.opts ~state ~parent () in
103103+ let view = View.create ~opts () in
104104+ {
105105+ previous_lines = 0;
106106+ current_doc = "";
107107+ messages = [];
108108+ view;
109109+ messages_comp = messages;
110110+ lines_comp = lines;
111111+ merlin_comp = merlin;
112112+ merlin_extension = (fun () -> []);
113113+ changes;
114114+ }
115115+116116+let set_current_doc t new_doc =
117117+ let at = prefix_length t.current_doc new_doc in
118118+ t.current_doc <- new_doc;
119119+ t.messages <- List.filter (fun (loc, _) -> loc < at) t.messages;
120120+ refresh_messages t
121121+122122+let on_change cm fn =
123123+ let has_changed =
124124+ let open Code_mirror.Editor in
125125+ let (State.Facet ((module F), it)) = View.update_listener () in
126126+ F.of_ it (fun ev ->
127127+ if View.Update.doc_changed ev then
128128+ let new_doc = source_of_state (View.Update.state ev) in
129129+ if not (String.equal cm.current_doc new_doc) then (
130130+ set_current_doc cm new_doc;
131131+ fn ()))
132132+ in
133133+ Code_mirror.Editor.View.dispatch cm.view
134134+ @@ Code_mirror.Compartment.reconfigure cm.changes [ has_changed ]
135135+136136+let count_lines str =
137137+ if str = "" then 0
138138+ else
139139+ let nb = ref 1 in
140140+ for i = 0 to String.length str - 1 do
141141+ if str.[i] = '\n' then incr nb
142142+ done;
143143+ !nb
144144+145145+let nb_lines t = t.previous_lines + count_lines t.current_doc
146146+let get_previous_lines t = t.previous_lines
147147+148148+let set_previous_lines t nb =
149149+ t.previous_lines <- nb;
150150+ refresh_lines t
151151+152152+let set_messages t msg =
153153+ t.messages <- msg;
154154+ refresh_messages t
155155+156156+let clear_messages t = set_messages t []
157157+let add_message t loc msg = set_messages t ((loc, msg) :: t.messages)
158158+159159+let set_source t doc =
160160+ set_current_doc t doc;
161161+ Code_mirror.Editor.View.set_doc t.view (Jstr.of_string doc)
+13
x-ocaml/src/editor.mli
···11+type t
22+33+val make : Brr.El.t -> t
44+val source : t -> string
55+val set_source : t -> string -> unit
66+val clear : t -> unit
77+val nb_lines : t -> int
88+val get_previous_lines : t -> int
99+val set_previous_lines : t -> int -> unit
1010+val clear_messages : t -> unit
1111+val add_message : t -> int -> Brr.El.t list -> unit
1212+val on_change : t -> (unit -> unit) -> unit
1313+val configure_merlin : t -> (unit -> Code_mirror.Extension.t list) -> unit
+73
x-ocaml/src/merlin_ext.ml
···11+module Worker = Brr_webworkers.Worker
22+33+type t = { id : int; mutable context : unit -> string; client : Client.t }
44+55+let set_context t fn = t.context <- fn
66+77+let make ~id client =
88+ { id; context = (fun () -> failwith "Merlin_ext.context"); client }
99+1010+let fix_position pre_len = function
1111+ | `Offset at -> `Offset (at + pre_len)
1212+ | other -> other
1313+1414+let fix_loc pre_len ({ loc_start; loc_end; _ } as loc : Protocol.Location.t) =
1515+ {
1616+ loc with
1717+ loc_start = { loc_start with pos_cnum = loc_start.pos_cnum - pre_len };
1818+ loc_end = { loc_end with pos_cnum = loc_end.pos_cnum - pre_len };
1919+ }
2020+2121+let fix_request t msg =
2222+ let pre = t.context () in
2323+ let pre_len = String.length pre in
2424+ match msg with
2525+ | Protocol.Complete_prefix (src, position) ->
2626+ let position = fix_position pre_len position in
2727+ Protocol.Complete_prefix (pre ^ src, position)
2828+ | Protocol.Type_enclosing (src, position) ->
2929+ let position = fix_position pre_len position in
3030+ Protocol.Type_enclosing (pre ^ src, position)
3131+ | Protocol.All_errors src -> Protocol.All_errors (pre ^ src)
3232+ | Protocol.Add_cmis _ as other -> other
3333+3434+let fix_answer ~pre ~doc msg =
3535+ let pre_len = String.length pre in
3636+ match (msg : Protocol.answer) with
3737+ | Protocol.Errors errors ->
3838+ Protocol.Errors
3939+ (List.filter_map
4040+ (fun (e : Protocol.error) ->
4141+ let loc = fix_loc pre_len e.loc in
4242+ let from = loc.loc_start.pos_cnum in
4343+ let to_ = loc.loc_end.pos_cnum in
4444+ if from < 0 || to_ > String.length doc then None
4545+ else Some { e with loc })
4646+ errors)
4747+ | Protocol.Completions completions ->
4848+ Completions
4949+ {
5050+ completions with
5151+ from = completions.from - pre_len;
5252+ to_ = completions.to_ - pre_len;
5353+ }
5454+ | Protocol.Typed_enclosings typed_enclosings ->
5555+ Typed_enclosings
5656+ (List.map
5757+ (fun (loc, a, b) -> (fix_loc pre_len loc, a, b))
5858+ typed_enclosings)
5959+ | Protocol.Added_cmis -> msg
6060+6161+module Merlin_send = struct
6262+ type nonrec t = t
6363+6464+ let post t msg =
6565+ let msg = fix_request t msg in
6666+ Client.post t.client (Merlin (t.id, msg))
6767+end
6868+6969+module Client = Merlin_client.Make (Merlin_send)
7070+module Ed = Merlin_codemirror.Extensions (Merlin_send)
7171+7272+let extensions t =
7373+ Merlin_codemirror.ocaml :: Array.to_list (Ed.all_extensions t)
+21
x-ocaml/src/mutation_observer.ml
···11+open Brr
22+33+type t = Jv.t
44+55+let mutation_observer = Jv.get Jv.global "MutationObserver"
66+77+let create callback =
88+ let callback = Jv.callback ~arity:2 callback in
99+ Jv.new' mutation_observer [| callback |]
1010+1111+let disconnect t =
1212+ let _ : Jv.t = Jv.call t "disconnect" [||] in
1313+ ()
1414+1515+let observe t ~target =
1616+ let config =
1717+ Jv.obj
1818+ Jv.[| ("attributes", true'); ("childList", true'); ("subtree", true') |]
1919+ in
2020+ let _ : Jv.t = Jv.call t "observe" [| El.to_jv target; config |] in
2121+ ()
+7
x-ocaml/src/mutation_observer.mli
···11+open Brr
22+33+type t
44+55+val create : (Jv.t -> Jv.t -> unit) -> t
66+val observe : t -> target:El.t -> unit
77+val disconnect : t -> unit
···11+let id = ref (0, 0)
22+33+let output_html m =
44+ let id, loc = !id in
55+ Js_of_ocaml.Worker.post_message
66+ (X_protocol.resp_to_bytes
77+ (X_protocol.Top_response_at (id, loc, [ Html m ])));
88+ ()
+5
x-ocaml/worker-lib/x_ocaml_lib.mli
···11+val output_html : string -> unit
22+33+(**/**)
44+55+val id : (int * int) ref