My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix RangeError: decoration positions exceeding document length

Three fixes for "Position N is out of range for changeset" errors:

1. js_top_worker: clamp output_at loc to input length — the ";;"
appended for parsing made pos_cnum extend past the original source

2. x-ocaml/editor: clamp decoration positions against CM document
length (Text.length) not OCaml String.length — they differ for
non-ASCII text (UTF-16 vs bytes). Also combine doc replacement +
decoration clear into a single transaction in set_source, and
read actual CM doc in build_range_set

3. jsoo-code-mirror/decoration: defensive safe_map that catches
JS RangeError in RangeSet.map and returns empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+56 -20
+8 -6
js_top_worker/lib/impl.cppo.ml
··· 618 618 Buffer.clear res_buff; 619 619 Buffer.clear stderr_buff; 620 620 Buffer.clear stdout_buff; 621 + let input_len = String.length phrase in 621 622 let phrase = 622 - let l = String.length phrase in 623 - if l >= 2 && String.sub phrase (l - 2) 2 = ";;" then phrase 623 + if input_len >= 2 && String.sub phrase (input_len - 2) 2 = ";;" then phrase 624 624 else phrase ^ ";;" 625 625 in 626 626 (* Bind the merlin-lib local store so Toploop.execute_phrase can access ··· 639 639 let phr = Toploop.preprocess_phrase Format.err_formatter phr in 640 640 ignore (Toploop.execute_phrase true pp_result phr : bool); 641 641 (* Get location from phrase AST — use the last 642 - structure item so output appears after all defs *) 642 + structure item so output appears after all defs. 643 + Clamp to input_len so positions from the appended 644 + ";;" suffix are never returned to the caller. *) 643 645 let loc = match phr with 644 646 | Parsetree.Ptop_def (_ :: _ as items) -> 645 647 let last = List.nth items (List.length items - 1) in 646 - last.pstr_loc.loc_end.pos_cnum 648 + min input_len last.pstr_loc.loc_end.pos_cnum 647 649 | Parsetree.Ptop_dir { pdir_loc; _ } -> 648 - pdir_loc.loc_end.pos_cnum 649 - | _ -> lb.lex_curr_p.pos_cnum 650 + min input_len pdir_loc.loc_end.pos_cnum 651 + | _ -> min input_len lb.lex_curr_p.pos_cnum 650 652 in 651 653 (* Flush and get current output *) 652 654 Format.pp_print_flush pp_result ();
+8 -1
jsoo-code-mirror/src/decoration.ml
··· 49 49 Jv.call range_set "of" [| Jv.of_array Range.to_jv ranges |] |> of_jv 50 50 51 51 let empty = Jv.get range_set "empty" |> of_jv 52 - let map t changes = Jv.call t "map" [| changes |] |> of_jv 52 + 53 + let safe_map = 54 + Jv.new' (Jv.get Jv.global "Function") 55 + [| Jv.of_string "rs"; Jv.of_string "ch"; Jv.of_string "em"; 56 + Jv.of_string "try { return rs.map(ch); } catch(e) { return em; }" |] 57 + 58 + let map t changes = 59 + Jv.call safe_map "call" [| Jv.null; t; changes; to_jv empty |] |> of_jv 53 60 end
+40 -13
x-ocaml/src/editor.ml
··· 38 38 in 39 39 Jv.call deco_facet "from" [| field |] |> Code_mirror.Extension.of_jv) 40 40 41 + let source_of_state s = 42 + String.concat "\n" @@ Array.to_list @@ Array.map Jstr.to_string 43 + @@ Code_mirror.Text.to_jstr_array 44 + @@ Code_mirror.Editor.State.doc s 45 + 46 + let source t = source_of_state @@ Code_mirror.Editor.View.state t.view 47 + 41 48 let build_range_set cm = 42 49 let open Code_mirror.Decoration in 43 50 let doc = cm.current_doc in 51 + (* Use the CM document length (in UTF-16 code units) for clamping, not 52 + String.length which counts bytes — they differ for non-ASCII text. *) 53 + let cm_len = 54 + Code_mirror.Text.length 55 + (Code_mirror.Editor.State.doc 56 + (Code_mirror.Editor.View.state cm.view)) 57 + in 44 58 let ranges = 45 59 Array.of_list 46 60 @@ List.map (fun (at, msg) -> ··· 51 65 @@ List.map (fun (at, msg) -> 52 66 let at = min at (String.length doc) in 53 67 let at = find_line_ends at doc in 54 - let at = min at (max 0 (String.length doc - 1)) in 68 + let at = min at (max 0 (cm_len - 1)) in 55 69 (at, msg)) 56 70 @@ List.concat 57 71 @@ List.map (fun (loc, lst) -> List.map (fun m -> (loc, m)) lst) ··· 92 106 refresh_lines x; 93 107 refresh_messages x; 94 108 refresh_merlin x 95 - 96 - let source_of_state s = 97 - String.concat "\n" @@ Array.to_list @@ Array.map Jstr.to_string 98 - @@ Code_mirror.Text.to_jstr_array 99 - @@ Code_mirror.Editor.State.doc s 100 - 101 - let source t = source_of_state @@ Code_mirror.Editor.View.state t.view 102 109 103 110 let prefix_length a b = 104 111 let rec go i = ··· 191 198 let add_message t loc msg = set_messages t ((loc, msg) :: t.messages) 192 199 193 200 let set_source t doc = 194 - Code_mirror.Editor.View.set_doc t.view (Jstr.of_string doc); 195 - (* Read back from CodeMirror to get the canonical form — the raw 196 - string may differ (e.g., HTML entity decoding, line ending 197 - normalization) from what CodeMirror stores internally. *) 201 + (* Clear decorations AND replace the document in a single transaction 202 + so CodeMirror never sees stale decoration positions. *) 203 + let state = Code_mirror.Editor.View.state t.view in 204 + let doc_len = 205 + Code_mirror.Text.length (Code_mirror.Editor.State.doc state) 206 + in 207 + t.messages <- []; 208 + let changes = 209 + Jv.obj 210 + [| ("from", Jv.of_int 0); 211 + ("to", Jv.of_int doc_len); 212 + ("insert", Jv.of_jstr (Jstr.of_string doc)) |] 213 + in 214 + let effect = 215 + Code_mirror.State_effect.of_ set_messages_effect 216 + (Code_mirror.Decoration.Range_set.to_jv 217 + (Code_mirror.Decoration.Range_set.empty)) 218 + in 219 + let txn = 220 + Jv.obj [| ("changes", changes); ("effects", effect) |] 221 + |> Code_mirror.Editor.View.Transaction.of_jv 222 + in 223 + Code_mirror.Editor.View.dispatch t.view txn; 224 + (* Read back canonical form *) 198 225 let canonical = source t in 199 - set_current_doc t canonical 226 + t.current_doc <- canonical