123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Rich.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)typet={(* the characters, the caret, the selection *)edit:Text_edit.t;(* the looks: (length in bytes, look), in order, covering the text *)runs:(int*Style.t)list;(* a look set with nothing selected, waiting for the next character *)pending:Style.toption;(* what an empty text looks like, so that its first character has a
look to take *)base:Style.t;}letof_string?(style=Style.plain)s={edit=Text_edit.of_strings;runs=(ifs=""then[]else[(String.lengths,style)]);pending=None;base=style;}letto_stringt=Text_edit.to_stringt.editletlengtht=Text_edit.lengtht.editleteditt=t.edit(*****************************************************************************)(* The run table *)(*****************************************************************************)(* everything before [pos] and everything after, splitting the run it
* falls inside -- the piece table's own surgery, on looks *)letsplitrunspos=letrecgoaccpos=function|[]->(List.revacc,[])|((len,st)asr)::rest->ifpos<=0then(List.revacc,r::rest)elseifpos>=lenthengo(r::acc)(pos-len)restelse(List.rev((pos,st)::acc),(len-pos,st)::rest)ingo[]posruns(* neighbours that have come to look the same are one run again: the
* table stays as short as the text's looks really are *)letmergeruns=letrecgo=function|(l1,s1)::(l2,s2)::restwhens1=s2->go((l1+l2,s1)::rest)|r::rest->r::gorest|[]->[]ingo(List.filter(fun(l,_)->l>0)runs)letcutrunsab=letbefore,rest=splitrunsainlet_,after=splitrest(b-a)inmerge(before@after)letputrunsalenst=letbefore,after=splitrunsainmerge(before@[(len,st)]@after)letrunst=let_,out=List.fold_left(fun(pos,acc)(len,st)->(pos+len,(pos,len,st)::acc))(0,[])t.runsinList.revoutletstyle_atti=letrecgopos=function|[]->t.base|[(_,st)]->st|(len,st)::rest->ifi<pos+lenthenstelsego(pos+len)restingo0t.runs(*****************************************************************************)(* The caret and the selection *)(*****************************************************************************)letcarett=Text_edit.carett.editletranget=Text_edit.ranget.edit(* moving the caret forgets a look that was waiting for it *)letatpost={twithedit=Text_edit.atpost.edit;pending=None}letselect~anchor~carett={twithedit=Text_edit.select~anchor~carett.edit;pending=None}letto_post={twithedit=Text_edit.to_post.edit;pending=None}lettyping_stylet=matcht.pendingwith|Somest->st|None->leta,b=rangetiniflengtht=0thent.baseelseifa<>bthenstyle_atta(* over a selection: its first character *)elseifa>0thenstyle_att(a-1)(* what is before the caret *)elsestyle_att0(* at the very start: what comes after *)(*****************************************************************************)(* Editing *)(*****************************************************************************)letinsertst=letstyle=typing_styletinleta,b=rangetinletruns=put(cutt.runsab)a(String.lengths)stylein{twithedit=Text_edit.insertst.edit;runs;pending=None}(* the same bytes Text_edit is about to delete, so both tables lose the
* same stretch *)letdelete_backwardt=leta,b=rangetinletfrom,upto=ifa<>bthen(a,b)elseifa=0then(0,0)else(Text.prev_char(to_stringt)a,a)iniffrom=uptothentelse{twithedit=Text_edit.delete_backwardt.edit;runs=cutt.runsfromupto;pending=None}letdelete_forwardt=leta,b=rangetinlets=to_stringtinletfrom,upto=ifa<>bthen(a,b)elseifa>=String.lengthsthen(a,a)else(a,Text.next_charsa)iniffrom=uptothentelse{twithedit=Text_edit.delete_forwardt.edit;runs=cutt.runsfromupto;pending=None}(*****************************************************************************)(* Looks *)(*****************************************************************************)letrestyleft=leta,b=rangetinifa=bthen{twithpending=Some(f(typing_stylet))}elseletbefore,rest=splitt.runsainletmid,after=splitrest(b-a)in{twithruns=merge(before@List.map(fun(l,st)->(l,fst))mid@after)}