123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371(*
* lTerm_read_line.ml
* ------------------
* Copyright : (c) 2011, Jeremie Dimino <jeremie@dimino.org>
* Licence : BSD3
*
* This file is a part of Lambda-Term.
*)openLwt_reactopenLTerm_geomopenLTerm_styleopenLTerm_keyletreturn,(>>=)=Lwt.return,Lwt.(>>=)typeprompt=LTerm_text.ttypehistory=Zed_string.tlist(* +-----------------------------------------------------------------+
| Completion |
+-----------------------------------------------------------------+ *)letcommon_prefix_oneab=letrecloopofs=ifofs=String.lengtha||ofs=String.lengthbthenString.suba0ofselseletch1,ofs1=Zed_utf8.unsafe_extract_nextaofsandch2,ofs2=Zed_utf8.unsafe_extract_nextbofsinifch1=ch2&&ofs1=ofs2thenloopofs1elseString.suba0ofsinloop0letcommon_prefix=function|[]->""|word::rest->List.fold_leftcommon_prefix_onewordrestletzed_common_prefix_oneab=letrecloopofs=ifofs=Zed_string.bytesa||ofs=Zed_string.bytesbthenZed_string.sub_ofs~ofs:0~len:ofsaelseletch1,ofs1=Zed_string.extract_nextaofsandch2,ofs2=Zed_string.extract_nextbofsinifch1=ch2&&ofs1=ofs2thenloopofs1elseZed_string.sub_ofs~ofs:0~len:ofsainloop0letzed_common_prefix=function|[]->Zed_string.empty()|word::rest->List.fold_leftzed_common_prefix_onewordrestletlookupwordwords=List.filter(funword'->Zed_utf8.starts_withword'word)wordsletlookup_assocwordwords=List.filter(fun(word',_)->Zed_utf8.starts_withword'word)wordsincludeLTerm_read_line_basemoduleBindings=Zed_input.Make(LTerm_key)letbindings=refBindings.emptyletbindseqactions=bindings:=Bindings.addseqactions!bindingsletunbindseq=bindings:=Bindings.removeseq!bindingslet()=bind[{control=false;meta=false;shift=false;code=Home}][Edit(LTerm_edit.ZedZed_edit.Goto_bot)];bind[{control=false;meta=false;shift=false;code=End}][Edit(LTerm_edit.ZedZed_edit.Goto_eot)];bind[{control=false;meta=false;shift=false;code=Up}][History_prev];bind[{control=false;meta=false;shift=false;code=Down}][History_next];bind[{control=false;meta=false;shift=false;code=Tab}][Complete];bind[{control=false;meta=false;shift=false;code=Enter}][Accept];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'b')}][Edit(LTerm_edit.ZedZed_edit.Prev_char)];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'f')}][Edit(LTerm_edit.ZedZed_edit.Next_char)];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'h')}][Edit(LTerm_edit.ZedZed_edit.Delete_prev_char)];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'c')}][Break];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'z')}][Suspend];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'm')}][Accept];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'l')}][Clear_screen];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'r')}][Prev_search];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char's')}][Next_search];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'd')}][Interrupt_or_delete_next_char];bind[{control=false;meta=true;shift=false;code=Char(Uchar.of_char'p')}][History_prev];bind[{control=false;meta=true;shift=false;code=Char(Uchar.of_char'n')}][History_next];bind[{control=false;meta=true;shift=false;code=Left}][Complete_bar_prev];bind[{control=false;meta=true;shift=false;code=Right}][Complete_bar_next];bind[{control=false;meta=true;shift=false;code=Home}][Complete_bar_first];bind[{control=false;meta=true;shift=false;code=End}][Complete_bar_last];bind[{control=false;meta=true;shift=false;code=Tab}][Complete_bar];bind[{control=false;meta=true;shift=false;code=Down}][Complete_bar];bind[{control=false;meta=true;shift=false;code=Enter}][Edit(LTerm_edit.ZedZed_edit.Newline)];bind[{control=false;meta=false;shift=false;code=Escape}][Cancel_search];bind[{control=true;meta=false;shift=false;code=Char(Uchar.of_char'x')};{control=true;meta=false;shift=false;code=Char(Uchar.of_char'e')}][Edit_with_external_editor](* +-----------------------------------------------------------------+
| The read-line engine |
+-----------------------------------------------------------------+ *)letis_prefix~prefixs=letprefix=Zed_string.to_utf8prefixands=Zed_string.to_utf8sinString.lengthprefix<=String.lengths&&(leti=ref0inwhile!i<String.lengthprefix&&s.[!i]=prefix.[!i]doincridone;!i=String.lengthprefix)lethistory_findpredicatehistory=letrechistory_find_skipped=function|[]->None|x::xs->ifpredicatexthenSome(skipped,x,xs)elsehistory_find_(x::skipped)xsinhistory_find_[]historyletsearch_stringstrsub=letstr=Zed_string.to_utf8strandsub=Zed_string.to_utf8subinletrecequal_atab=(b=String.lengthsub)||(String.unsafe_getstra=String.unsafe_getsubb)&&equal_at(a+1)(b+1)inletrecloopofsidx=ifofs+String.lengthsub>String.lengthstrthenNoneelseifequal_atofs0thenSomeidxelseloop(Zed_utf8.unsafe_nextstrofs)(idx+1)inloop00letmacro=Zed_macro.create[]typemode=|Edition|Search|Set_counter|Add_countertypecompletion_state={start:int;(* Beginning of the word being completed *)index:int;(* Index of the selected in [words] *)count:int;(* Length of [words] *)words:(Zed_string.t*Zed_string.t)list;}letno_completion={start=0;index=0;words=[];count=0;}typedirection=Forward|Backwardtypesearch_status={before:Zed_string.tlist;after:Zed_string.tlist;match_:(Zed_string.t*int)option}classvirtual['a]engine?(history=[])?(clipboard=LTerm_edit.clipboard)?(macro=macro)()=letedit:unitZed_edit.t=Zed_edit.create~clipboard()inletcontext=Zed_edit.contextedit(Zed_edit.new_cursoredit)inletmode,set_mode=S.createEditioninletuser_completion_state,set_completion_state=E.create()inletreset_completion_state=E.when_(S.map(funmode->mode=Edition)mode)(E.select[E.stamp(Zed_edit.changesedit)no_completion;E.stamp(S.changes(Zed_cursor.position(Zed_edit.cursorcontext)))no_completion;])inletcompletion_state=S.hold~eq:(==)no_completion(E.select[reset_completion_state;user_completion_state])inletcompletion_words=S.map~eq:(==)(func->c.words)completion_stateinletcompletion_index=S.map(func->c.index)completion_stateinlethistory,set_history=S.create(history,[])inletmessage,set_message=S.createNoneinlethistory_prefix,set_history_prefix=letev,send=E.create()inletedit_changes=Zed_edit.changeseditinletedit_changes=E.map(fun_->Zed_edit.textedit)edit_changesinletprefix=S.hold(Zed_rope.empty())(E.select[ev;edit_changes])inprefix,sendinobject(self)methodvirtualeval:'amethodedit=editmethodcontext=contextmethodshow_box=truemethodmode=modemethodhistory=historymethodmessage=messagemethodclipboard=clipboardmethodmacro=macrovalinterrupt:exnLwt_mvar.t=Lwt_mvar.create_empty()methodinterrupt=interrupt(* The event which occurs when completion need to be recomputed. *)valmutablecompletion_event=E.never(* Save for when setting the macro counter. *)valmutablesave=(0,Zed_rope.empty())methodset_completion?(index=0)startwords=letcount=List.lengthwordsinifindex<0||index>max0(count-1)theninvalid_arg"LTerm_read_line.set_completion: \
index out of bounds compared to words.";set_completion_state{start;index;count;words}initializercompletion_event<-(E.map(fun_->(* We can't execute it right now as the user might call [set_completion]
immediatly. *)Lwt.pause()>>=fun()->self#completion;Lwt.return_unit)reset_completion_state);self#completionmethodinput_prev=Zed_rope.before(Zed_edit.textedit)(Zed_edit.positioncontext)methodinput_next=Zed_rope.after(Zed_edit.textedit)(Zed_edit.positioncontext)methodcompletion_words=completion_wordsmethodcompletion_index=completion_indexmethodcompletion=self#set_completion0[]methodcomplete=letcomp=S.valuecompletion_stateinletprefix_length=Zed_edit.positioncontext-comp.startinmatchcomp.wordswith|[]->()|[(completion,suffix)]->Zed_edit.insertcontext(Zed_rope.of_string(Zed_string.subcompletion~pos:prefix_length~len:(Zed_string.lengthcompletion-prefix_length)));Zed_edit.insertcontext(Zed_rope.of_stringsuffix)|(completion,_suffix)::rest->letword=List.fold_left(funacc(word,_)->zed_common_prefix_oneaccword)completionrestinZed_edit.insertcontext(Zed_rope.of_string(Zed_string.subword~pos:prefix_length~len:(Zed_string.lengthword-prefix_length)))(* The event which search for the string in the history. *)valmutablesearch_event=E.nevervalmutablesearch_status=Noneinitializerletreset_search_=search_status<-None;self#searchBackwardinsearch_event<-E.mapreset_search(E.when_(S.map(funmode->mode=Search)mode)(Zed_edit.changesedit))methodprivatesearchdirection=letdo_searchdirection=letset_statusother_entriesentriesmatch_=letbefore,after=matchdirectionwith|Backward->(other_entries,entries)|Forward->(entries,other_entries)insearch_status<-Some{before;after;match_}inletinput=Zed_rope.to_string(Zed_edit.textedit)inletrecloopother_entriesentries=matchentrieswith|[]->set_statusother_entriesentriesNone;set_message(Some(LTerm_text.of_utf8"Reverse search: not found"))|entry::rest->matchsearch_stringentryinputwith|Somepos->beginmatchsearch_statuswith|Some{match_=Some(entry',_);_}whenentry=entry'->loop(entry::other_entries)rest|_->set_statusother_entriesrest(Some(entry,pos));lettxt=LTerm_text.of_stringentryinfori=postopos+Zed_rope.length(Zed_edit.textedit)-1doletch,style=txt.(i)intxt.(i)<-(ch,{stylewithunderline=Sometrue})done;set_message(Some(Array.append(LTerm_text.of_utf8"Reverse search: ")txt))end|None->loop(entry::other_entries)restinmatchsearch_statuswith|None->lethist=fst(S.valuehistory)inloop[](matchdirectionwith|Backward->hist|Forward->List.revhist)|Some{before;after;match_}->letother_entries,entries=matchdirectionwith|Backward->(before,after)|Forward->(after,before)inletother_entries=matchmatch_with|None->other_entries|Some(entry,_)->entry::other_entriesinloopother_entriesentriesinmatchS.valuemodewith|Search->do_searchdirection|Edition->lettext=Zed_edit.texteditinZed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.lengthtext);letprev,next=S.valuehistoryinset_history(Zed_rope.to_stringtext::(List.rev_appendnextprev),[]);search_status<-None;set_modeSearch;do_searchdirection|_->()methodinsertch=Zed_edit.insert_charcontextchmethodsend_actionaction=ifaction<>EditLTerm_edit.Stop_macrothenZed_macro.addmacroaction;matchactionwith|(Complete|Complete_bar|Accept)whenS.valuemode=Search->beginset_modeEdition;set_messageNone;matchsearch_statuswith|Some{match_=Some(entry,_pos);_}->search_status<-None;Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.length(Zed_edit.textedit));Zed_edit.insertcontext(Zed_rope.of_stringentry)|Some{match_=None;_}|None->()end|Edit(LTerm_edit.Zedaction)->Zed_edit.get_actionactioncontext|Interrupt_or_delete_next_char->ifZed_rope.is_empty(Zed_edit.textedit)thenLwt.async(fun()->Lwt_mvar.putinterruptInterrupt)elseZed_edit.delete_next_charcontext|CompletewhenS.valuemode=Edition->self#complete|Complete_bar_nextwhenS.valuemode=Edition->letcomp=S.valuecompletion_stateinifcomp.index<comp.count-1thenset_completion_state{compwithindex=comp.index+1}|Complete_bar_prevwhenS.valuemode=Edition->letcomp=S.valuecompletion_stateinifcomp.index>0thenset_completion_state{compwithindex=comp.index-1}|Complete_bar_firstwhenS.valuemode=Edition->letcomp=S.valuecompletion_stateinifcomp.index>0thenset_completion_state{compwithindex=0}|Complete_bar_lastwhenS.valuemode=Edition->letcomp=S.valuecompletion_stateinifcomp.index<comp.count-1thenset_completion_state{compwithindex=comp.count-1}|Complete_barwhenS.valuemode=Edition->letcomp=S.valuecompletion_stateinifcomp.words<>[]thenbeginletprefix_length=Zed_edit.positioncontext-comp.startinletcompletion,suffix=List.nthcomp.wordscomp.indexinZed_edit.insertcontext(Zed_rope.of_string(Zed_string.aftercompletionprefix_length));Zed_edit.insertcontext(Zed_rope.of_stringsuffix)end|History_prevwhenS.valuemode=Edition->beginletprev,next=S.valuehistoryinmatchprevwith|[]->()|line::rest->lettext=Zed_edit.texteditinset_history(rest,Zed_rope.to_stringtext::next);Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.lengthtext);Zed_edit.insertcontext(Zed_rope.of_stringline)end|History_nextwhenS.valuemode=Edition->beginletprev,next=S.valuehistoryinmatchnextwith|[]->()|line::rest->lettext=Zed_edit.texteditinset_history(Zed_rope.to_stringtext::prev,rest);Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.lengthtext);Zed_edit.insertcontext(Zed_rope.of_stringline)end|History_search_prevwhenS.valuemode=Edition->beginletprev,next=S.valuehistoryinlettext=Zed_rope.to_string@@Zed_edit.texteditinletprefix=S.valuehistory_prefixinmatchhistory_find(is_prefix~prefix:(Zed_rope.to_stringprefix))prevwith|None->()|Some(not_matched,line,rest)->set_history(rest,not_matched@text::next);Zed_edit.gotocontext0;Zed_edit.delete_next_linecontext;Zed_edit.insertcontext(Zed_rope.of_stringline);set_history_prefixprefixend|History_search_nextwhenS.valuemode=Edition->beginletprev,next=S.valuehistoryinletprefix=S.valuehistory_prefixinmatchhistory_find(is_prefix~prefix:(Zed_rope.to_stringprefix))nextwith|None->()|Some(not_matched,line,rest)->lettext=Zed_rope.to_string@@Zed_edit.texteditinset_history(not_matched@text::prev,rest);Zed_edit.gotocontext0;Zed_edit.delete_next_linecontext;Zed_edit.insertcontext(Zed_rope.of_stringline);set_history_prefixprefixend|Prev_search->self#searchBackward|Next_search->self#searchForward|Cancel_search->ifS.valuemode=Searchthenbeginset_modeEdition;set_messageNoneend|EditLTerm_edit.Start_macrowhenS.valuemode=Edition->Zed_macro.set_recordingmacrotrue|EditLTerm_edit.Stop_macro->Zed_macro.set_recordingmacrofalse|EditLTerm_edit.Cancel_macro->Zed_macro.cancelmacro|EditLTerm_edit.Play_macro->Zed_macro.cancelmacro;List.iterself#send_action(Zed_macro.contentsmacro)|EditLTerm_edit.Insert_macro_counter->Zed_edit.insertcontext(Zed_rope.of_string(Zed_string.unsafe_of_utf8(string_of_int(Zed_macro.get_countermacro))));Zed_macro.add_countermacro1|EditLTerm_edit.Set_macro_counterwhenS.valuemode=Edition->lettext=Zed_edit.texteditinsave<-(Zed_edit.positioncontext,text);Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.lengthtext);set_modeSet_counter;set_message(Some(LTerm_text.of_utf8"Enter a value for the macro counter."))|EditLTerm_edit.Add_macro_counterwhenS.valuemode=Edition->lettext=Zed_edit.texteditinsave<-(Zed_edit.positioncontext,text);Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.lengthtext);set_modeAdd_counter;set_message(Some(LTerm_text.of_utf8"Enter a value to add to the macro counter."))|Accept->beginmatchS.valuemodewith|Edition|Search->()|Set_counter->letpos,text=saveinsave<-(0,Zed_rope.empty());(tryZed_macro.set_countermacro(int_of_string(Zed_string.to_utf8(Zed_rope.to_string(Zed_edit.textedit))))withFailure_->());Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.length(Zed_edit.textedit));Zed_edit.insertcontexttext;Zed_edit.gotocontextpos;set_modeEdition;set_messageNone|Add_counter->letpos,text=saveinsave<-(0,Zed_rope.empty());(tryZed_macro.add_countermacro(int_of_string(Zed_string.to_utf8(Zed_rope.to_string(Zed_edit.textedit))))withFailure_->());Zed_edit.gotocontext0;Zed_edit.removecontext(Zed_rope.length(Zed_edit.textedit));Zed_edit.insertcontexttext;Zed_edit.gotocontextpos;set_modeEdition;set_messageNoneend|Break->raiseSys.Break|Edit(LTerm_edit.Customf)->f()|_->()methodstyliselast=lettxt=LTerm_text.of_rope(Zed_edit.textedit)inletpos=Zed_edit.positioncontextinifnotlast&&Zed_edit.get_selectioneditthenbeginletmark=Zed_cursor.get_position(Zed_edit.markedit)inleta=minposmarkandb=maxposmarkinfori=atob-1doletch,style=txt.(i)intxt.(i)<-(ch,{stylewithunderline=Sometrue})done;end;(txt,pos)endclassvirtual['a]abstract=objectmethodvirtualeval:'amethodvirtualsend_action:action->unitmethodvirtualinsert:Uchar.t->unitmethodvirtualedit:unitZed_edit.tmethodvirtualcontext:unitZed_edit.contextmethodvirtualclipboard:Zed_edit.clipboardmethodvirtualmacro:actionZed_macro.tmethodvirtualstylise:bool->LTerm_text.t*intmethodvirtualhistory:(Zed_string.tlist*Zed_string.tlist)signalmethodvirtualmessage:LTerm_text.toptionsignalmethodvirtualinput_prev:Zed_rope.tmethodvirtualinput_next:Zed_rope.tmethodvirtualcompletion_words:(Zed_string.t*Zed_string.t)listsignalmethodvirtualcompletion_index:intsignalmethodvirtualset_completion:?index:int->int->(Zed_string.t*Zed_string.t)list->unitmethodvirtualcompletion:unitmethodvirtualcomplete:unitmethodvirtualshow_box:boolmethodvirtualmode:modesignalmethodvirtualinterrupt:exnLwt_mvar.tend(* +-----------------------------------------------------------------+
| Predefined classes |
+-----------------------------------------------------------------+ *)classread_line?history()=object(self)inherit[Zed_string.t]engine?history()methodeval=Zed_rope.to_string(Zed_edit.textself#edit)endclassread_password()=object(self)inherit[Zed_string.t]engine()assupermethod!styliselast=lettext,pos=super#styliselastinfori=0toArray.lengthtext-1dolet_ch,style=text.(i)intext.(i)<-(Zed_char.unsafe_of_char'*',style)done;(text,pos)methodeval=Zed_rope.to_string(Zed_edit.textself#edit)method!show_box=falsemethod!send_action=function|Prev_search|Next_search->()|action->super#send_actionactionendtype'aread_keyword_result=|Rk_valueof'a|Rk_errorofZed_string.tclass['a]read_keyword?history()=object(self)inherit['aread_keyword_result]engine?history()methodkeywords=[]methodeval=letinput=Zed_rope.to_string(Zed_edit.textself#edit)intryRk_value(List.associnputself#keywords)withNot_found->Rk_errorinputmethod!completion=letword=Zed_rope.to_stringself#input_previnletkeywords=List.filter(fun(keyword,_value)->Zed_string.starts_with~prefix:wordkeyword)self#keywordsinself#set_completion0(List.map(fun(keyword,_value)->(keyword,Zed_string.empty()))keywords)end(* +-----------------------------------------------------------------+
| Running in a terminal |
+-----------------------------------------------------------------+ *)letnewline_uChar=Uchar.of_char'\n'letnewline=Zed_char.unsafe_of_uChar@@newline_uCharletvline=LTerm_draw.({top=Light;bottom=Light;left=Blank;right=Blank})letreverse_style={LTerm_style.nonewithLTerm_style.reverse=Sometrue}letdefault_prompt=LTerm_text.of_utf8"# "letrecdropcountl=ifcount<=0thenlelsematchlwith|[]->[]|_::l->drop(count-1)l(* Computes the position of the cursor after printing the given styled
string:
- [pos] is the current cursor position
(it may be at column [max-column + 1])
- [text] is the text to display
- [start] is the start of the chunk to display in [text]
- [stop] is the end of the chunk to display in [text]
*)letreccompute_positioncolspostextstartstop=ifstart=stopthenposelseletch,_style=text.(start)inifch=newlinethencompute_positioncols{row=pos.row+1;col=0}text(start+1)stopelseletwidth=Zed_char.widthchinifpos.col+width>colsthencompute_positioncols{row=pos.row+1;col=width}text(start+1)stopelsecompute_positioncols{poswithcol=pos.col+max0width}text(start+1)stop(* Return the "real" position of the cursor, i.e. on the screen. *)letreal_poscolspos=ifpos.col=colsthen{row=pos.row+1;col=0}elseposletrecget_index_of_last_displayed_wordcolumncolumnsindexwords=matchwordswith|[]->index-1|(word,_suffix)::words->letcolumn=column+Zed_string.lengthwordinifcolumn<=columns-1thenget_index_of_last_displayed_word(column+1)columns(index+1)wordselseindex-1(*let rec get_index_of_last_displayed_word_by_width column columns index words =
match words with
| [] ->
index - 1
| (word, _suffix) :: words ->
let column = column + Zed_string.(aval_width (width word)) in
if column <= columns - 1 then
get_index_of_last_displayed_word_by_width (column + 1) columns (index + 1) words
else
index - 1*)letdraw_styledctxrowcolstr=letsize=LTerm_draw.sizectxinletreclooprowcolidx=ifidx<Array.lengthstrthenbeginletch,style=Array.unsafe_getstridxinifch=newlinethenloop(row+1)0(idx+1)elsebeginletwidth=max1(Zed_char.widthch)inifcol+width>size.colsthenloop(row+1)0idxelsebeginLTerm_draw.draw_charctxrowcol~stylech;looprow(col+width)(idx+1)endendendinlooprowcol0letdraw_styled_with_newlinesmatrixcolsrowcolstr=letreclooprowcolidx=ifidx<Array.lengthstrthenbeginletch,style=Array.unsafe_getstridxinifch=newlinethenbeginLTerm_draw.draw_char_matrixmatrixrowcolnewline;loop(row+1)0(idx+1)endelsebeginletwidth=max1(Zed_char.widthch)inifcol+width>colsthenloop(row+1)0idxelsebeginLTerm_draw.draw_char_matrixmatrixrowcol~stylech;looprow(col+width)(idx+1)endendendinlooprowcol0letstyled_newline=[|(newline,LTerm_style.none)|]classvirtual['a]termterm=letsize,set_size=S.create(LTerm.sizeterm)inletevent,set_prompt=E.create()inleteditor_mode,set_editor_mode=S.createLTerm_editor.Defaultinletprompt=S.switch(S.hold~eq:(==)(S.constdefault_prompt)event)inletkey_sequence,set_key_sequence=S.create[]inobject(self)inherit['a]abstractmethodsize=sizemethodprompt=promptmethodset_promptprompt=set_promptpromptvalmutablevisible=true(* Whether the read-line instance is currently visible. *)valmutabledisplayed=false(* Whether the read-line instance is currently displayed on the
screen. *)valmutabledraw_queued=false(* Whether a draw operation has been queued, in which case it is
not necessary to redraw. *)valmutablecursor={row=0;col=0}(* The position of the cursor. *)valmutablecompletion_start=S.const0(* Index of the first displayed word in the completion bar. *)valmutableheight=0(* The height of the displayed material. *)valmutableresolver=None(* The current resolver for resolving input sequences. *)valmutablerunning=truevalvi_state=newLTerm_vi.statevalmutablevi_edit=Noneinitializercompletion_start<-(S.fold(funstart(words,index,columns)->ifindex<startthen(* The cursor is before the left margin. *)letcount=List.lengthwordsinletrev_index=count-index-1incount-get_index_of_last_displayed_word1columnsrev_index(droprev_index(List.revwords))-1elseifindex>get_index_of_last_displayed_word1columnsstart(dropstartwords)then(* The cursor is after the right margin. *)indexelsestart)0(S.changes(S.l3(funwordsindexsize->(words,index,size.cols))self#completion_wordsself#completion_indexsize)))methodeditor_mode=editor_modevalmutablevi_thread=Nonemethodvi_state=vi_statemethodset_editor_modemode=set_editor_modemode;matchmodewith|LTerm_editor.Default->vi_edit<-None;(matchvi_threadwith|Somethread->LTerm_vi.Concurrent.Thread.cancelthread;vi_thread<-None;|None->());|LTerm_editor.Vi->let_vi_edit=vi_state#vi_editinvi_edit<-Some_vi_edit;self#listen_vi_vi_editself#interruptmethodkey_sequence=key_sequencemethodcompletion_start=completion_startvaldraw_mutex=Lwt_mutex.create()methodprivatequeue_draw_update=ifdraw_queuedthenreturn()elsebegin(* Wait a bit in order not to draw too often. *)draw_queued<-true;Lwt.pause()>>=fun()->draw_queued<-false;Lwt_mutex.with_lockdraw_mutex(fun()->ifrunningthenself#draw_updateelsereturn())endmethoddraw_update=letsize=S.valuesizeinifvisible&&size.rows>0&&size.cols>0thenbeginletstyled,position=self#stylisefalseinletprompt=S.valuepromptin(* Compute the position of the cursor after displaying the
prompt. *)letpos_after_prompt=compute_positionsize.cols{row=0;col=0}prompt0(Array.lengthprompt)in(* Compute the position of the cursor after displaying the
input before the cursor. *)letpos_after_before=compute_positionsize.colspos_after_promptstyled0positionin(* Compute the position of the cursor after displaying the
input. *)letpos_after_styled=compute_positionsize.colspos_after_beforestyledposition(Array.lengthstyled)in(* Compute the position of the cursor after displaying the
newline used to end the input. *)letpos_after_newline=compute_positionsize.colspos_after_styledstyled_newline01in(* The real position of the cursor on the screen. *)letpos_cursor=real_possize.colspos_after_beforein(* Height of prompt+input. *)letprompt_input_height=max(pos_cursor.row+1)pos_after_newline.rowinletmatrix=ifself#show_box&&size.cols>2thenmatchS.valueself#messagewith|Somemsg->(* Compute the height of the message. *)letmessage_height=(compute_position(size.cols-2){row=0;col=0}msg0(Array.lengthmsg)).row+1in(* The total height of the displayed text. *)lettotal_height=prompt_input_height+message_height+2in(* Create the matrix for the rendering. *)letmatrix_size={cols=size.cols+1;rows=ifdisplayedthenmaxtotal_heightheightelsetotal_height}inletmatrix=LTerm_draw.make_matrixmatrix_sizein(* Update the height parameter. *)height<-total_height;(* Draw the prompt and the input. *)draw_styled_with_newlinesmatrixsize.cols00prompt;draw_styled_with_newlinesmatrixsize.colspos_after_prompt.rowpos_after_prompt.colstyled;draw_styled_with_newlinesmatrixsize.colspos_after_styled.rowpos_after_styled.colstyled_newline;letctx=LTerm_draw.sub(LTerm_draw.contextmatrixmatrix_size){row1=0;col1=0;row2=matrix_size.rows;col2=size.cols;}in(* Draw a frame for the message. *)LTerm_draw.draw_framectx{row1=prompt_input_height;col1=0;row2=total_height;col2=size.cols;}LTerm_draw.Light;forrow=prompt_input_heighttototal_height-1doLTerm_draw.draw_char_matrixmatrixrowsize.colsnewline;done;(* Draw the message. *)letctx=LTerm_draw.subctx{row1=prompt_input_height+1;col1=1;row2=total_height-1;col2=size.cols-1;}indraw_styledctx00msg;matrix|None->letcomp_start=S.valueself#completion_startinletcomp_index=S.valueself#completion_indexinletcomp_words=dropcomp_start(S.valueself#completion_words)in(* The total height of the displayed text. *)lettotal_height=prompt_input_height+3in(* Create the matrix for the rendering. *)letmatrix_size={cols=size.cols+1;rows=ifdisplayedthenmaxtotal_heightheightelsetotal_height}inletmatrix=LTerm_draw.make_matrixmatrix_sizein(* Update the height parameter. *)height<-total_height;(* Draw the prompt and the input. *)draw_styled_with_newlinesmatrixsize.cols00prompt;draw_styled_with_newlinesmatrixsize.colspos_after_prompt.rowpos_after_prompt.colstyled;draw_styled_with_newlinesmatrixsize.colspos_after_styled.rowpos_after_styled.colstyled_newline;letctx=LTerm_draw.sub(LTerm_draw.contextmatrixmatrix_size){row1=0;col1=0;row2=matrix_size.rows;col2=size.cols;}in(* Draw a frame for the completion. *)LTerm_draw.draw_framectx{row1=prompt_input_height;col1=0;row2=total_height;col2=size.cols;}LTerm_draw.Light;forrow=prompt_input_heighttototal_height-1doLTerm_draw.draw_char_matrixmatrixrowsize.colsnewline;done;(* Draw the completion. *)letctx=LTerm_draw.subctx{row1=prompt_input_height+1;col1=1;row2=total_height-1;col2=size.cols-1;}inletrecloopidxcol=function|[]->()|(word,_suffix)::words->letlen=Zed_string.lengthwordinLTerm_draw.draw_stringctx0colword;(* Apply the reverse style if this is the selected word. *)ifidx=comp_indexthenforcol=coltomin(col+len-1)(size.cols-2)doLTerm_draw.set_style(LTerm_draw.pointctx0col)reverse_styledone;(* Draw a separator. *)LTerm_draw.draw_piecectx0(col+len)vline;letcol=col+len+1inifcol<size.cols-2thenloop(idx+1)colwordsinloopcomp_start0comp_words;matrixelsebeginlettotal_height=prompt_input_heightinletmatrix_size={cols=size.cols+1;rows=ifdisplayedthenmaxtotal_heightheightelsetotal_height}inletmatrix=LTerm_draw.make_matrixmatrix_sizeinheight<-total_height;draw_styled_with_newlinesmatrixsize.cols00prompt;draw_styled_with_newlinesmatrixsize.colspos_after_prompt.rowpos_after_prompt.colstyled;matrixendinLTerm.hide_cursorterm>>=fun()->beginifdisplayedthen(* Go back to the beginning of displayed text. *)LTerm.moveterm(-cursor.row)(-cursor.col)elsereturn()end>>=fun()->(* Display everything. *)LTerm.print_box_with_newlinestermmatrix>>=fun()->(* Update the cursor. *)cursor<-pos_cursor;(* Move the cursor to the right position. *)LTerm.moveterm(cursor.row-Array.lengthmatrix+1)cursor.col>>=fun()->LTerm.show_cursorterm>>=fun()->LTerm.flushterm>>=fun()->displayed<-true;return()endelsereturn()methoddraw_success=letsize=S.valuesizeinifsize.rows>0&&size.cols>0thenbeginletstyled,_position=self#stylisetrueinletprompt=S.valuepromptin(ifdisplayedthenLTerm.moveterm(-cursor.row)(-cursor.col)>>=fun()->LTerm.clear_screen_nexttermelsereturn())>>=fun()->LTerm.fprintstermprompt>>=fun()->LTerm.fprintlstermstyledendelsereturn()methoddraw_failure=self#draw_successmethodhide=ifvisiblethenbeginvisible<-false;Lwt_mutex.lockdraw_mutex>>=fun()->Lwt.finalize(fun()->letsize=S.valuesizeinifdisplayed&&size.rows>0&&size.cols>0thenletmatrix_size={cols=size.cols+1;rows=height}inletmatrix=LTerm_draw.make_matrixmatrix_sizeinforrow=0toheight-1doLTerm_draw.draw_char_matrixmatrixrow0newline;done;LTerm.moveterm(-cursor.row)(-cursor.col)>>=fun()->LTerm.print_box_with_newlinestermmatrix>>=fun()->LTerm.moveterm(1-Array.lengthmatrix)0>>=fun()->cursor<-{row=0;col=0};height<-0;displayed<-false;return()elsereturn())(fun()->Lwt_mutex.unlockdraw_mutex;return())endelsereturn()methodshow=ifnotvisiblethenbeginvisible<-true;self#queue_draw_updateendelsereturn()valmutablemode=Nonevalmutablelocal_bindings=Bindings.emptymethodbindkeysactions=local_bindings<-Bindings.addkeysactionslocal_bindingsmethodprivatekeyseqkeys=matchkeyswith|[]->return(ContinueLoop[])|key::tl->letres=matchresolverwith|Someres->res|None->Bindings.resolver[Bindings.pack(funx->x)local_bindings;Bindings.pack(funx->x)!bindings;Bindings.pack(List.map(funx->Editx))!LTerm_edit.bindings]inmatchBindings.resolvekeyreswith|Bindings.Acceptedactions->resolver<-None;set_key_sequence[];self#exec~keys:tlactions|Bindings.Continueres->resolver<-Someres;set_key_sequence(S.valuekey_sequence@[key]);return(ContinueLooptl)|Bindings.Rejected->set_key_sequence[];ifresolver=Nonethenmatchkeywith|{control=false;meta=false;shift=false;code=Charch}->Zed_macro.addself#macro(Edit(LTerm_edit.Zed(Zed_edit.Insert(Zed_char.unsafe_of_uCharch))));self#insertch|{code=Charch;_}whenLTerm.windowsterm&&Uchar.to_intch>=32->(* Windows reports Shift+A for A, ... *)Zed_macro.addself#macro(Edit(LTerm_edit.Zed(Zed_edit.Insert(Zed_char.unsafe_of_uCharch))));self#insertch|_->()elseresolver<-None;return(ContinueLooptl)valresult=Lwt_mvar.create_empty()methodprivatelisten_vivi_editexnBox=letmsgBox=vi_edit#action_outputinletrecperform_actions=function|[]->return(ContinueLoop[])|action::tl->LTerm_vi.performvi_editself#contextself#execaction>>=function|Result_asr->returnr|ContinueLoop_->perform_actionstlinletreclisten()=set_key_sequence[];LTerm_vi.Concurrent.MsgBox.getmsgBox>>=(function|Bypasskeyseq->letkeyseq=List.mapLTerm_vi.of_vi_keykeyseqinself#process_keyskeyseq>>=(function|Resultr->Lwt_mvar.putresultr|ContinueLoop_->listen())|Dummy->listen()|Viactions->perform_actionsactions>>=function|ContinueLoop_->listen()|Resultr->Lwt_mvar.putresultr)inletthread=Lwt.catchlisten(funexn->Lwt_mvar.putexnBoxexn)invi_thread<-Some(thread)methodprivateprocess_keyskeys=self#keyseqkeys>>=function|Resultr->return(Resultr)|ContinueLoopkeys->matchkeyswith|[]->return(ContinueLoop[])|_->self#process_keyskeys(* The main loop. *)methodprivateloop=letread_event=matchvi_editwith|Some_->Lwt.pause()>>=fun()->Lwt.(>|=)(LTerm.read_eventterm)(funev->Evev)|None->Lwt.(>|=)(LTerm.read_eventterm)(funev->Evev)inLwt.pick[read_event;Lwt.(>|=)(Lwt_mvar.takeresult)(funr->Loop_resultr);Lwt.(>|=)(Lwt_mvar.takeself#interrupt)(fune->Interruptede);]>>=function|Loop_resultr->returnr|Interruptedexn->raiseexn|Evev->matchevwith|LTerm_event.Resizesize->set_sizesize;self#loop|LTerm_event.Keykey->(matchS.valueeditor_modewith|LTerm_editor.Default->self#process_keys[key]>>=(function|Resultr->returnr|ContinueLoop_->self#loop)|LTerm_editor.Vi->matchvi_editwith|Somevi_edit->set_key_sequence(S.valuekey_sequence@[key]);LTerm_vi.Concurrent.MsgBox.putvi_edit#i(LTerm_vi.of_lterm_keykey)>>=fun()->self#loop|None->self#process_keys[key]>>=(function|Resultr->returnr|ContinueLoop_->self#loop)(* falllback to the default mode *))|_->self#loopmethodcreate_temporary_file_for_external_editor=Filename.temp_file"lambda-term"".txt"methodexternal_editor=trySys.getenv"EDITOR"withNot_found->"vi"methodprivateexec?(keys=[])actions=matchactionswith|Accept::_whenS.valueself#mode=Edition->Zed_macro.addself#macroAccept;return(Resultself#eval)|Clear_screen::actions->Zed_macro.addself#macroClear_screen;LTerm.clear_screenterm>>=fun()->LTerm.gototerm{row=0;col=0}>>=fun()->displayed<-false;self#queue_draw_update>>=fun()->self#exec~keysactions|EditLTerm_edit.Play_macro::actions->Zed_macro.cancelself#macro;self#exec~keys(Zed_macro.contentsmacro@actions)|Suspend::actions->ifSys.win32thenself#exec~keysactionselsebeginletis_visible=visibleinself#hide>>=fun()->LTerm.flushterm>>=fun()->beginmatchmodewith|Somemode->LTerm.leave_raw_modetermmode|None->return()end>>=fun()->Unix.kill(Unix.getpid())Sys.sigtstp;beginmatchLTerm.is_a_ttytermwith|true->LTerm.enter_raw_modeterm>>=funm->mode<-Somem;return()|false->return()end>>=fun()->(ifis_visiblethenself#showelsereturn())>>=fun()->self#exec~keysactionsend|Edit_with_external_editor::actions->beginletis_visible=visibleinself#hide>>=fun()->LTerm.flushterm>>=fun()->beginmatchmodewith|Somemode->LTerm.leave_raw_modetermmode|None->return()end>>=fun()->lettemp_fn=self#create_temporary_file_for_external_editorinletinput=Zed_rope.to_string(Zed_edit.textself#edit)inLwt_io.with_file~mode:Outputtemp_fn(funoc->Lwt_io.write_lineoc(Zed_string.to_utf8input))>>=fun()->leteditor=self#external_editorinPrintf.ksprintfLwt_unix.system"%s %s"editor(Filename.quotetemp_fn)>>=funstatus->(ifstatus<>WEXITED0thenLwt_io.eprintf"`%s %s' exited with status %d\n"editortemp_fn(matchstatuswith|WEXITEDn->n|_->255)elseLwt.try_bind(fun()->Lwt_io.with_file~mode:Inputtemp_fnLwt_io.read)(funs->lets=Zed_utf8.rstripsinZed_edit.goto_botself#context;Zed_edit.replaceself#context(Zed_rope.length(Zed_edit.textself#edit))(Zed_rope.of_string(Zed_string.unsafe_of_utf8s));Lwt.return())(function|Unix.Unix_error(err,_,_)->Lwt_io.eprintf"%s: %s\n"temp_fn(Unix.error_messageerr)|exn->Lwt.failexn))>>=fun()->beginmatchLTerm.is_a_ttytermwith|true->LTerm.enter_raw_modeterm>>=funm->mode<-Somem;return()|false->return()end>>=fun()->(ifis_visiblethenself#showelsereturn())>>=fun()->self#exec~keysactionsend|action::actions->self#send_actionaction;self#exec~keysactions|[]->return(ContinueLoopkeys)methodrun=(* Update the size with the current size. *)set_size(LTerm.sizeterm);running<-true;(* Redraw everything when needed. *)letevent=E.map_p(fun()->ifrunningthenself#queue_draw_updateelsereturn())(E.select[E.stamp(S.changessize)();Zed_edit.updateself#edit[Zed_edit.cursorself#context];E.stamp(S.changesprompt)();E.stamp(S.changesself#completion_words)();E.stamp(S.changesself#completion_index)();E.stamp(S.changesself#completion_start)();E.stamp(S.changesself#message)();])inbeginmatchLTerm.is_a_ttytermwith|true->LTerm.enter_raw_modeterm>>=funm->mode<-Somem;return()|false->return()end>>=fun()->beginLwt.finalize(fun()->Lwt.catch(fun()->(* Go to the beginning of line otherwise all offset
calculation will be false. *)LTerm.fprintterm"\r">>=fun()->self#queue_draw_update>>=fun()->self#loop)(funexn->running<-false;E.stopevent;Lwt_mutex.with_lockdraw_mutex(fun()->self#draw_failure)>>=fun()->Lwt.failexn))(fun()->matchmodewith|Somemode->LTerm.leave_raw_modetermmode|None->return())end>>=funresult->running<-false;E.stopevent;Lwt_mutex.with_lockdraw_mutex(fun()->self#draw_success)>>=fun()->returnresultend