this repo has no description

feat(x-ocaml): add cell modes (interactive/exercise/test/hidden)

- Add mode type and data attributes (data-id, data-for, data-env) to cells
- Hidden cells: no editor/UI, still execute in linked list chain
- Exercise cells: editable (default CodeMirror behaviour)
- Interactive/Test cells: read-only via EditorState.readOnly facet
- Read mode attribute from <x-ocaml mode="..."> elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+155 -53
+116 -42
src/cell.ml
··· 1 1 open Brr 2 2 3 + type mode = Interactive | Exercise | Test | Hidden 4 + 5 + let mode_of_string = function 6 + | "exercise" -> Exercise 7 + | "test" -> Test 8 + | "hidden" -> Hidden 9 + | "interactive" | _ -> Interactive 10 + 3 11 type status = Not_run | Running | Run_ok | Request_run 4 12 5 13 type t = { 6 14 id : int; 15 + mode : mode; 16 + cell_id : string option; 17 + cell_for : string option; 18 + cell_env : string option; 7 19 mutable prev : t option; 8 20 mutable next : t option; 9 21 mutable status : status; 10 - cm : Editor.t; 22 + cm : Editor.t option; 23 + source_text : string ref; 11 24 eval_fn : id:int -> line_number:int -> string -> unit; 12 25 fmt_fn : id:int -> string -> unit; 13 - merlin_worker : Merlin_ext.Client.worker; 26 + merlin_worker : Merlin_ext.Client.worker option; 14 27 run_on : [ `Click | `Load ]; 15 28 filename : string option; 16 29 } 17 30 18 31 let id t = t.id 32 + let mode t = t.mode 33 + let cell_id t = t.cell_id 34 + 35 + let source t = 36 + match t.cm with 37 + | Some cm -> Editor.source cm 38 + | None -> !(t.source_text) 19 39 20 40 let pre_source t = 21 41 let rec go acc t = 22 42 match t.prev with 23 43 | None -> String.concat "\n" acc 24 - | Some e -> go (Editor.source e.cm :: acc) e 44 + | Some e -> go (source e :: acc) e 25 45 in 26 46 let s = go [] t in 27 47 if s = "" then s else s ^ " ;;\n" 48 + 49 + let with_cm t f = match t.cm with Some cm -> f cm | None -> () 28 50 29 51 let rec invalidate_from ~editor = 30 52 editor.status <- Not_run; 31 - Editor.clear editor.cm; 32 - let count = Editor.nb_lines editor.cm in 53 + with_cm editor Editor.clear; 54 + let count = match editor.cm with Some cm -> Editor.nb_lines cm | None -> 0 in 33 55 match editor.next with 34 56 | None -> () 35 57 | Some editor -> 36 - Editor.set_previous_lines editor.cm count; 58 + with_cm editor (fun cm -> Editor.set_previous_lines cm count); 37 59 invalidate_from ~editor 38 60 39 61 let invalidate_after ~editor = 40 62 editor.status <- Not_run; 41 - let count = Editor.nb_lines editor.cm in 63 + let count = match editor.cm with Some cm -> Editor.nb_lines cm | None -> 0 in 42 64 match editor.next with 43 65 | None -> () 44 66 | Some editor -> 45 - Editor.set_previous_lines editor.cm count; 67 + with_cm editor (fun cm -> Editor.set_previous_lines cm count); 46 68 invalidate_from ~editor 47 69 48 70 let rec refresh_lines_from ~editor = 49 - let count = Editor.nb_lines editor.cm in 71 + let count = match editor.cm with Some cm -> Editor.nb_lines cm | None -> 0 in 50 72 match editor.next with 51 73 | None -> () 52 74 | Some editor -> 53 - Editor.set_previous_lines editor.cm count; 75 + with_cm editor (fun cm -> Editor.set_previous_lines cm count); 54 76 refresh_lines_from ~editor 55 77 56 78 let rec run editor = 57 79 if editor.status = Running then () 58 80 else ( 59 81 editor.status <- Request_run; 60 - Editor.clear_messages editor.cm; 82 + with_cm editor Editor.clear_messages; 61 83 match editor.prev with 62 84 | Some e when e.status <> Run_ok -> run e 63 85 | _ -> 64 86 editor.status <- Running; 65 - let code_txt = Editor.source editor.cm in 66 - let line_number = 1 + Editor.get_previous_lines editor.cm in 87 + let code_txt = source editor in 88 + let line_number = 89 + 1 + (match editor.cm with 90 + | Some cm -> Editor.get_previous_lines cm 91 + | None -> 0) 92 + in 67 93 editor.eval_fn ~id:editor.id ~line_number code_txt) 68 94 69 95 let set_prev ~prev t = ··· 71 97 t.prev <- prev; 72 98 match prev with 73 99 | None -> 74 - Editor.set_previous_lines t.cm 0; 100 + with_cm t (fun cm -> Editor.set_previous_lines cm 0); 75 101 refresh_lines_from ~editor:t 76 102 | Some p -> 77 103 assert (p.next = None); ··· 81 107 let set_source_from_html editor this = 82 108 let doc = Webcomponent.text_content this in 83 109 let doc = String.trim doc in 84 - Editor.set_source editor.cm doc; 85 - invalidate_from ~editor; 86 - editor.fmt_fn ~id:editor.id doc 110 + editor.source_text := doc; 111 + match editor.cm with 112 + | Some cm -> 113 + Editor.set_source cm doc; 114 + invalidate_from ~editor; 115 + editor.fmt_fn ~id:editor.id doc 116 + | None -> 117 + invalidate_from ~editor 87 118 88 119 let init_css shadow ~extra_style ~inline_style = 89 120 El.append_children shadow ··· 114 145 (); 115 146 ] 116 147 117 - let init ~id ~run_on ?filename ?extra_style ?inline_style ~eval_fn ~fmt_fn ~post_fn this = 148 + let init ~id ~mode ~run_on ?cell_id ?cell_for ?cell_env ?filename 149 + ?extra_style ?inline_style ~eval_fn ~fmt_fn ~post_fn this = 118 150 let shadow = Webcomponent.attach_shadow this in 119 - init_css shadow ~extra_style ~inline_style; 151 + let is_hidden = mode = Hidden in 152 + let is_readonly = mode = Interactive || mode = Test in 120 153 121 - let run_btn = El.button [ El.txt (Jstr.of_string "Run") ] in 122 - El.append_children shadow 123 - [ El.div ~at:[ At.class' (Jstr.of_string "run_btn") ] [ run_btn ] ]; 154 + if not is_hidden then 155 + init_css shadow ~extra_style ~inline_style; 124 156 125 - let cm = Editor.make shadow in 157 + let cm, merlin_info = 158 + if is_hidden then (None, None) 159 + else begin 160 + let run_btn = El.button [ El.txt (Jstr.of_string "Run") ] in 161 + El.append_children shadow 162 + [ El.div ~at:[ At.class' (Jstr.of_string "run_btn") ] [ run_btn ] ]; 126 163 127 - let merlin = Merlin_ext.make ~id ?filename post_fn in 128 - let merlin_worker = Merlin_ext.Client.make_worker merlin in 164 + let cm = Editor.make ~read_only:is_readonly shadow in 165 + 166 + let merlin = Merlin_ext.make ~id ?filename post_fn in 167 + let merlin_worker = Merlin_ext.Client.make_worker merlin in 168 + 169 + let editor_ref = ref None in 170 + 171 + let () = 172 + Mutation_observer.observe ~target:(Webcomponent.as_target this) 173 + @@ Mutation_observer.create (fun _ _ -> 174 + match !editor_ref with 175 + | Some editor -> set_source_from_html editor this 176 + | None -> ()) 177 + in 178 + 179 + let _ : Ev.listener = 180 + Ev.listen Ev.click 181 + (fun _ev -> 182 + match !editor_ref with Some editor -> run editor | None -> ()) 183 + (El.as_target run_btn) 184 + in 185 + 186 + (Some cm, Some (merlin, merlin_worker, editor_ref)) 187 + end 188 + in 189 + 129 190 let editor = 130 191 { 131 192 id; 193 + mode; 194 + cell_id; 195 + cell_for; 196 + cell_env; 132 197 status = Not_run; 133 198 cm; 199 + source_text = ref ""; 134 200 prev = None; 135 201 next = None; 136 202 eval_fn; 137 203 fmt_fn; 138 - merlin_worker; 204 + merlin_worker = 205 + (match merlin_info with Some (_, w, _) -> Some w | None -> None); 139 206 run_on; 140 207 filename; 141 208 } 142 209 in 143 - Editor.on_change cm (fun () -> invalidate_after ~editor); 144 210 145 - Merlin_ext.set_context merlin (fun () -> pre_source editor); 146 - Editor.configure_merlin cm (fun () -> Merlin_ext.extensions merlin_worker); 211 + (match cm with 212 + | Some cm -> Editor.on_change cm (fun () -> invalidate_after ~editor) 213 + | None -> ()); 147 214 148 - let () = 149 - Mutation_observer.observe ~target:(Webcomponent.as_target this) 150 - @@ Mutation_observer.create (fun _ _ -> set_source_from_html editor this) 151 - in 152 - 153 - let _ : Ev.listener = 154 - Ev.listen Ev.click (fun _ev -> run editor) (El.as_target run_btn) 155 - in 215 + (match merlin_info with 216 + | Some (merlin, merlin_worker, editor_ref) -> 217 + editor_ref := Some editor; 218 + Merlin_ext.set_context merlin (fun () -> pre_source editor); 219 + Editor.configure_merlin (Option.get cm) (fun () -> 220 + Merlin_ext.extensions merlin_worker) 221 + | None -> ()); 156 222 157 223 editor 158 224 ··· 160 226 set_source_from_html editor this 161 227 162 228 let set_source editor doc = 163 - Editor.set_source editor.cm doc; 229 + editor.source_text := doc; 230 + (match editor.cm with 231 + | Some cm -> Editor.set_source cm doc 232 + | None -> ()); 164 233 refresh_lines_from ~editor 165 234 166 235 let render_message msg = ··· 180 249 El.pre ~at:[ At.class' (Jstr.of_string ("caml_" ^ kind)) ] [ text ] 181 250 182 251 let add_message t loc msg = 183 - Editor.add_message t.cm loc (List.map render_message msg) 252 + match t.cm with 253 + | Some cm -> Editor.add_message cm loc (List.map render_message msg) 254 + | None -> () 184 255 185 256 let completed_run ed msg = 186 257 (if msg <> [] then 187 - let loc = String.length (Editor.source ed.cm) in 258 + let loc = String.length (source ed) in 188 259 add_message ed loc msg); 189 260 ed.status <- Run_ok; 190 261 match ed.next with Some e when e.status = Request_run -> run e | _ -> () 191 262 192 263 let receive_merlin t msg = 193 - Merlin_ext.Client.on_message t.merlin_worker 194 - (Merlin_ext.fix_answer ~pre:(pre_source t) ~doc:(Editor.source t.cm) msg) 264 + match t.merlin_worker with 265 + | Some merlin_worker -> 266 + Merlin_ext.Client.on_message merlin_worker 267 + (Merlin_ext.fix_answer ~pre:(pre_source t) ~doc:(source t) msg) 268 + | None -> () 195 269 196 270 let loadable t = t.run_on = `Load
+10
src/cell.mli
··· 1 + type mode = Interactive | Exercise | Test | Hidden 2 + 3 + val mode_of_string : string -> mode 4 + 1 5 type t 2 6 3 7 val init : 4 8 id:int -> 9 + mode:mode -> 5 10 run_on:[ `Click | `Load ] -> 11 + ?cell_id:string -> 12 + ?cell_for:string -> 13 + ?cell_env:string -> 6 14 ?filename:string -> 7 15 ?extra_style:Jstr.t -> 8 16 ?inline_style:Jstr.t -> ··· 13 21 t 14 22 15 23 val id : t -> int 24 + val mode : t -> mode 25 + val cell_id : t -> string option 16 26 val set_source : t -> string -> unit 17 27 val add_message : t -> int -> X_protocol.output list -> unit 18 28 val completed_run : t -> X_protocol.output list -> unit
+16 -9
src/editor.ml
··· 81 81 let basic_setup = 82 82 Jv.get Jv.global "__CM__basic_setup" |> Code_mirror.Extension.of_jv 83 83 84 - let make parent = 84 + let read_only_extension () = 85 + let editor_state = Jv.get Jv.global "__CM__state" in 86 + let ro_facet = Jv.get editor_state "readOnly" in 87 + Jv.call ro_facet "of" [| Jv.of_bool true |] |> Code_mirror.Extension.of_jv 88 + 89 + let make ?(read_only = false) parent = 85 90 let open Code_mirror.Editor in 86 91 let changes = Code_mirror.Compartment.make () in 87 92 let messages = Code_mirror.Compartment.make () in 88 93 let lines = Code_mirror.Compartment.make () in 89 94 let merlin = Code_mirror.Compartment.make () in 90 95 let extensions = 91 - [| 92 - basic_setup; 93 - Code_mirror.Editor.View.line_wrapping (); 94 - Code_mirror.Compartment.of' lines []; 95 - Code_mirror.Compartment.of' messages []; 96 - Code_mirror.Compartment.of' changes []; 97 - Code_mirror.Compartment.of' merlin []; 98 - |] 96 + Array.append 97 + [| 98 + basic_setup; 99 + Code_mirror.Editor.View.line_wrapping (); 100 + Code_mirror.Compartment.of' lines []; 101 + Code_mirror.Compartment.of' messages []; 102 + Code_mirror.Compartment.of' changes []; 103 + Code_mirror.Compartment.of' merlin []; 104 + |] 105 + (if read_only then [| read_only_extension () |] else [||]) 99 106 in 100 107 let config = State.Config.create ~doc:Jstr.empty ~extensions () in 101 108 let state = State.create ~config () in
+1 -1
src/editor.mli
··· 1 1 type t 2 2 3 - val make : Brr.El.t -> t 3 + val make : ?read_only:bool -> Brr.El.t -> t 4 4 val source : t -> string 5 5 val set_source : t -> string -> unit 6 6 val clear : t -> unit
+12 -1
src/x_ocaml.ml
··· 62 62 in 63 63 let id = List.length !all in 64 64 let filename = Webcomponent.get_attribute this "data-filename" in 65 + let mode = 66 + Cell.mode_of_string 67 + (Option.value ~default:"interactive" 68 + (Webcomponent.get_attribute this "mode")) 69 + in 70 + let cell_id = Webcomponent.get_attribute this "data-id" in 71 + let cell_for = Webcomponent.get_attribute this "data-for" in 72 + let cell_env = Webcomponent.get_attribute this "data-env" in 65 73 let eval_fn ~id ~line_number code = Backend.eval ~id ~line_number backend code in 66 74 let fmt_fn ~id code = Backend.fmt ~id backend code in 67 75 let post_fn msg = Backend.post backend msg in 68 - let editor = Cell.init ~id ~run_on ?filename ?extra_style ?inline_style ~eval_fn ~fmt_fn ~post_fn this in 76 + let editor = 77 + Cell.init ~id ~mode ~run_on ?cell_id ?cell_for ?cell_env ?filename 78 + ?extra_style ?inline_style ~eval_fn ~fmt_fn ~post_fn this 79 + in 69 80 all := editor :: !all; 70 81 Cell.set_prev ~prev editor; 71 82 Cell.start editor this;