12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043(*---------------------------------------------------------------------------
Copyright (c) 2017 The down programmers. All rights reserved.
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)openDown_stdletoriginal_ocaml_readline=ref(fun___->assertfalse)letuse_file?(silent=false)file=matchsilentwith|true->Toploop.use_silentlyFormat.std_formatter(Toploop.Filefile)|false->Toploop.use_fileFormat.std_formatterfile(* Logging and formatting styles *)letfaint=reftrueletadd_faintacc=if!faintthen`Faint::accelseacclettty_no_faint()=faint:=falseletstyle_err=[`Bold;`Fg`Red](* match ocaml *)letstyle_warn=[`Bold;`Fg`Magenta](* match ocaml *)letstyle_doc_section=[`Fg`Yellow]letstyle_code=[`Bold]letstyle_id_complete_suff=[`Fg(`Hi`Cyan)]letstyle_id_complete_info()=add_faint[`Italic]letstyle_id_info_id=[`Fg(`Hi`Cyan)]letstyle_id_info_type=[`Italic]letstyle_last_indicator=[`Fg`Yellow]letstyle_key=[`Bold]letstyle_prompt=[`Fg(`Hi`Green)]letstyle_prompt_inactive()=(add_faint[`Fg`Green])letstyle_prompt_recording=[`Fg(`Hi`Magenta)]letstyle_prompt_recording_inactive()=(add_faint[`Fg`Magenta])letpp_error=Fmt.ttystyle_errFmt.stringletpp_warn=Fmt.ttystyle_warnFmt.string(* match ocaml *)letpp_doc_section=Fmt.ttystyle_doc_sectionFmt.stringletpp_code=Fmt.ttystyle_codeFmt.stringletpp_faint()=Fmt.tty(add_faint[])Fmt.stringletlog_errorfmt=Fmt.pr("%a: "^^fmt^^"@.")pp_error"Error"letlog_warningfmt=Fmt.pr("%a: "^^fmt^^"@.")pp_warn"Warning"letlog_on_error~use=functionErrore->log_error"%s"e;use|Okv->v(* Prompt history *)modulePhistory=structtypet={prev:stringlist;focus:string;next:stringlist;}letvprev=letaddacce=matchString.trimewith""->acc|e->e::accin{prev=List.rev(List.fold_leftadd[]prev);focus="";next=[]}letempty=v[]letpushees=ife=""theneselsematcheswith|e'::_whenString.equalee'->es|es->e::esletentriesh=letnext=List.filter(funs->not(String.equals""))h.nextinList.rev_append(pushh.focusnext)h.prevletaddhe=matchString.trimewith""->h|e->v(pushe(entriesh))letrestarth=v(entriesh)letprevhcurrent=matchh.prevwith|[]->None|p::ps->letnext=push(String.trimcurrent)(pushh.focush.next)inletnext=ifnext=[]then[""](* bottom can be empty *)elsenextinSome({prev=ps;focus=p;next;},p)letnexthcurrent=matchh.nextwith|[]->None|n::ns->letprev=push(String.trimcurrent)(pushh.focush.prev)inSome({prev;focus=n;next=ns},n)letto_string~seph=Txt_entries.to_string~sep(entriesh)letof_string~seps=v(Txt_entries.of_string~seps)end(* String editor *)modulePstring=structtypet={s:string;cursor:int;mark:intoption}letv?(cursor=0)?marks={s;cursor;mark}letempty=v""letis_emptyp=String.equalp.s""lettxtp=p.slettxt_range~first:r0~last:r1p=String.subp.sr0(r1-r0+1)letcursorp=p.cursorletset_cursorcursorp=ifp.cursor=cursorthenpelse{pwithcursor}letset_markmarkp={pwithmark}letswap_cursor_and_markp=matchp.markwith|None->p|Somemwhenm>String.lengthp.s->{pwithmark=None}|Somem->{pwithcursor=m;mark=Somep.cursor}letsoip=set_cursor0pleteoip=set_cursor(String.lengthp.s)pletsolp=set_cursor(Txt.find_prev_solp.s~start:(p.cursor-1))pleteolp=set_cursor(Txt.find_next_eolp.s~start:p.cursor)pletnext_charp=set_cursor(Txt.find_next_gcp.s~after:p.cursor)pletprev_charp=set_cursor(Txt.find_prev_gcp.s~before:p.cursor)pletnext_wordp=set_cursor(Txt.find_next_after_eowp.s~start:p.cursor)pletprev_wordp=set_cursor(Txt.find_prev_sowp.s~start:(p.cursor-1))pletprev_linep=letl,w=Txt.find_prev_eol_and_tty_widthp.s~before:p.cursorinletpl=Txt.find_prev_eolp.s~start:(l-1)inletstart=ifpl=l||not(Txt.is_eolp.s.[pl])then0elsepl+1inletcursor=Txt.find_next_tty_width_or_eolp.s~start~winset_cursorcursorpletnext_linep=letl,w=Txt.find_prev_eol_and_tty_widthp.s~before:p.cursorinletstart=ifp.cursor=0&&w=0then0elsel+1inletnl=Txt.find_next_eolp.s~startinifnl=String.lengthp.sthenpelseletcursor=Txt.find_next_tty_width_or_eolp.s~start:(nl+1)~winset_cursorcursorpletsubst~start:r0~stop:r1bytesp=letslen=String.lengthp.sinletblen=String.lengthbytesinifr0=slenthen{pwiths=p.s^bytes;cursor=slen+blen}elseletb=String.subp.s0r0inleta=String.subp.sr1(slen-r1)inlets=String.concat""[b;bytes;a]inletmark=matchp.markwith|None->None|Somemwhenm<=slen+blen->p.mark|Some_->Nonein{s;cursor=r0+blen;mark}letinsertbytesp=subst~start:p.cursor~stop:p.cursorbytespletdelete_next_charp=ifp.cursor=String.lengthp.sthenpelseletstop=Txt.find_next_gcp.s~after:p.cursorinsubst~start:p.cursor~stop""pletdelete_prev_charp=ifp.cursor=0thenpelseletprev=Txt.find_prev_gcp.s~before:p.cursorinsubst~start:prev~stop:p.cursor""pletkill_next_wordp=letstop=Txt.find_next_after_eowp.s~start:p.cursorinifstop=p.cursorthenp,Noneelseletkill=txt_range~first:p.cursor~last:(stop-1)pinsubst~start:p.cursor~stop""p,Somekillletkill_prev_wordp=ifp.cursor=0thenp,Noneelseletprev=Txt.find_prev_sowp.s~start:(p.cursor-1)inletkill=txt_range~first:prev~last:(p.cursor-1)pinsubst~start:prev~stop:p.cursor""p,Somekillletkill_to_solp=letstart=Txt.find_prev_solp.s~start:(p.cursor-1)inletstart=ifstart=p.cursorthenp.cursor-1elsestartinletlast=p.cursor-1inifstart<0||last<0thenp,Noneelseletkill=txt_range~first:start~lastpinsubst~start~stop:p.cursor""p,Somekillletkill_to_eolp=letstop=Txt.find_next_eolp.s~start:p.cursorinletstop=ifstop=p.cursorthenstop+1elsestopinifstop>String.lengthp.sthenp,Noneelseletkill=txt_range~first:p.cursor~last:(stop-1)pinsubst~start:p.cursor~stop""p,Somekillletkill_regionp=matchp.markwith|None->p,None|Somemark->ifmark>String.lengthp.sthen{pwithmark=None},Noneelseifmark=p.cursorthenp,Noneelseletmin,max=matchp.cursor<markwith|true->p.cursor,mark-1|false->mark,p.cursor-1inletkill=txt_range~first:min~last:maxpinletcursor=minandmark=Someminin{(subst~start:min~stop:(max+1)""p)withcursor;mark},Somekillletgeometry~tty_w~margin_wp=(* Returns [rmax], (cr, cc, c_nl). [rmax] and [cr] are zero-based
row indexes relative to prompt row for: the maximal row and the
cursor row. [cc] is the cursor column and [c_nl] indicates if a
newline has to be added for a wrapping cursor. *)letrecloopsmaxcursorcrccrci=matchi>maxwith|true->ifcursor<>String.lengthsthenr,(cr,cc,false)elseifcmodtty_w=0thenr+1,(r+1,0,true)elser,(r,c,false)|false->letr,c=ifcmodtty_w=0then(* wrap *)r+1,0elser,cinletcr,cc=ifi=cursorthen(r,c)else(cr,cc)inletnl=s.[i]='\n'inleti,gc_w=Txt.find_next_gc_and_tty_widths~after:iinletr,c=ifnlthenr+1,margin_welser,c+gc_winloopsmaxcursorcrccrciinloopp.s(String.lengthp.s-1)p.cursor0margin_w0margin_w0end(* OCaml history *)moduleHistory=structletsep="(**)"leth=ref(Phistory.v[])letaddtxt=h:=Phistory.add!htxtletrestart()=h:=Phistory.restart!hletprevcurrent=matchPhistory.prev!hcurrentwith|None->None|Some(h',txt)->h:=h';Sometxtletnextcurrent=matchPhistory.next!hcurrentwith|None->None|Some(h',txt)->h:=h';Sometxtletfile()=Result.bind(Dir.config())@@fundir->Ok(Filename.concatdir(Filename.concat"ocaml""history.ml"))letload()=log_on_error~use:()@@Result.map_error(Fmt.str"history load failed: %s")@@Result.bind(file())@@funfile->Result.bind(File.existsfile)@@function|false->Ok()|true->Result.bind(File.readfile)@@funhstr->Ok(h:=Phistory.of_string~sephstr;)letsave()=log_on_error~use:()@@Result.map_error(Fmt.str"history save failed: %s")@@Result.bind(file())@@funfile->File.set_content~file(Phistory.to_string~sep!h)letedit()=log_on_error~use:()@@Result.map_error(Fmt.str"history edit failed: %s")@@Result.bind(file())@@funfile->lethstr=Phistory.to_string~sep!hinResult.bind(File.set_content~filehstr)@@fun()->Result.bind(Editor.edit_filefile)@@fun()->Result.bind(File.readfile)@@funhstr->Ok(h:=Phistory.of_string~sephstr)letclear()=h:=Phistory.v[];save()end(* Sessions *)moduleSession=structtypename=stringletdir()=Result.bind(Dir.config())@@fundir->Ok(Filename.concatdir(Filename.concat"ocaml""session"))letdir_filefn=Result.bind(dir())@@fund->Ok(Filename.concatdfn)letlast_session_file()=dir_file"last"letunsaved_file()=dir_file"unsaved"letsession_file=function|""->Error"Session name cannot be empty."|n->dir_file(n^".ml")letsession_of_pathp=ifnot(Filename.check_suffixp".ml")thenNoneelseSome(Filename.(basename(chop_extensionp)),p)letsessions_of_dirdir=Result.bind(Dir.existsdir)@@function|false->Ok[]|true->Result.bind(Dir.contentsdir)@@funpaths->letrecadd_sessionaccp=matchsession_of_pathpwith|None->acc|Some(n,p)->(n,p)::accinOk(List.sortcompare(List.fold_leftadd_session[]paths))letlast_session()=Result.bind(last_session_file())@@funfile->Result.bind(File.existsfile)@@function|false->OkNone|true->Result.bind(File.readfile)@@funn->letname=String.trimninResult.bind(session_filename)@@funfile->Result.bind(File.existsfile)@@function|false->OkNone|true->Ok(Some(name,file))letset_last_sessionn=Result.bind(last_session_file())@@funfile->File.set_content~filenletget_sessionn=letfind_sessionn=matchnwith|""->last_session()|n->Result.bind(session_filen)@@funf->Ok(Some(n,f))inResult.bind(find_sessionn)@@function|None->Error"No existing last session found."|Some(name,file)->log_on_error~use:()(set_last_sessionname);Ok(name,file)letget_existing_sessionn=Result.bind(get_sessionn)@@fun(n,fileass)->Result.bind(File.existsfile)@@function|true->Oks|false->Error(Fmt.str"No session %a found, \
see '%a'"pp_codenpp_code"Down.Session.list ()")letlast_name()=log_on_error~use:None@@Result.bind(last_session())@@function|None->OkNone|Some(n,_)->Ok(Somen)letlist()=letpp_lastppflast=ifnotlastthen()elseFmt.pfppf"(%a) "(Fmt.ttystyle_last_indicatorFmt.string)"last"inletpp_session~lastppf(n,p)=Fmt.pfppf"@[<h>%a %a%a@]"pp_codenpp_last(last=n)(pp_faint())pinletpp_session_list~lastppfss=Fmt.pfppf" @[<v>@,%a@,@]"(Fmt.list(pp_session~last))ssinletpp_noneppfdir=Fmt.pfppf" @[<v>@,No session found in %a@,@]"(pp_faint())dirinlog_on_error~use:()@@Result.bind(dir())@@fundir->Result.bind(last_session())@@funlast->Result.bind(sessions_of_dirdir)@@function|[]->Ok(Fmt.pr"%a@."pp_nonedir)|ss->letlast=matchlastwithNone->""|Some(last,_)->lastinOk(Fmt.pr"%a@."(pp_session_list~last)ss)letload?silentn=log_on_error~use:()@@Result.bind(get_existing_sessionn)@@fun(n,file)->if(use_file?silentfile)thenOk()elseError(Fmt.str"Use '%a' to correct errors."pp_code"Down.Session.edit \"\"")leteditn=log_on_error~use:()@@Result.bind(get_sessionn)@@fun(_,file)->Result.bind(File.existsfile)@@function|true->Editor.edit_filefile|false->(* create path *)Result.bind(File.set_content~file"")@@fun()->Editor.edit_filefileleterr_existsn=Fmt.str"Session %a exists, specify '%a' to overwrite."pp_codenpp_code"~replace:true"letof_file?(replace=false)~filen=log_on_error~use:()@@Result.bind(get_sessionn)@@fun(n,session_file)->Result.bind(File.existssession_file)@@function|truewhennotreplace->Error(err_existsn)|true|false->Result.bind(File.readfile)@@funcontents->File.set_content~file:session_filecontentsletdeleten=log_on_error~use:()@@Result.bind(get_existing_sessionn)@@fun(_,f)->File.deletef(* Recording. *)letsep="(**)"letto_stringphrases=Txt_entries.to_string~sepphrasesletof_strings=(Txt_entries.of_string~seps)letrecording:boolref=reffalseletset_recordingv=recording:=vletrecording()=!recordingletrecord:stringlistref=ref[]letset_recordphrases=record:=List.revphrasesletrem_last_recorded()=match!recordwith|[]->()|_::rs->record:=rsletadd_recordeds=matchString.trimswith|swhenString.lengths>=5&&String.subs05="#quit"->()|s->record:=s::!recordletadd_if_recordings=ifrecording()thenadd_recordedsletrecorded()=List.rev!recordletrecord()=match!Toploop.read_interactive_input==!original_ocaml_readlinewith|true->log_error"Sorry, recording needs Down's line edition."|false->(* That could still not be down's readline but, unlikely *)ifrecording()thenrem_last_recorded()elseset_recordingtrueletstop()=ifrecording()then(rem_last_recorded();set_recordingfalse)letrevise()=ifrecording()thenrem_last_recorded();log_on_error~use:()@@lets=to_string(recorded())inResult.bind(Editor.edit_string~ext:".ml"s)@@funs->Ok(set_record(of_strings))letsave?(replace=false)n=stop();log_on_error~use:()@@Result.bind(get_sessionn)@@fun(n,file)->matchrecorded()with|[]->Error"No phrase to save."|ps->Result.bind(File.existsfile)@@function|truewhennotreplace->Error(err_existsn)|true|false->Result.bind(File.set_content~file(to_stringps))@@fun()->Ok(set_record[])letappendn=stop();log_on_error~use:()@@Result.bind(get_sessionn)@@fun(_,file)->matchrecorded()with|[]->Error"No phrase to append."|ps->Result.bind(File.existsfile)@@function|false->Result.bind(File.set_content~file(to_stringps))@@fun()->Ok(set_record[])|true->Result.bind(File.readfile)@@funcontents->letps=of_stringcontents@psinResult.bind(File.set_content~file(to_stringps))@@fun()->Ok(set_record[])(* The idea of the following is to avoid a dialog to confirm losing
existing recorded phrases. Though similar to history, it may be
confusing on parallel ocaml processes. *)letload_unsaved_record()=log_on_error~use:()@@Result.bind(unsaved_file())@@funfile->Result.bind(File.existsfile)@@function|false->Ok()|true->Result.bind(File.readfile)@@funcontents->set_record(of_stringcontents);File.set_content~file""letsave_unsaved_record()=log_on_error~use:()@@matchrecorded()with|[]->Ok()|ps->Result.bind(unsaved_file())@@funfile->Result.bind(File.existsfile)@@function|false->File.set_content~file(to_stringps)|true->Result.bind(File.readfile)@@funcontents->(* Another toplevel process may have written meanwhile... *)File.set_content~file(to_string(of_stringcontents@ps))(* Stepping. *)letsteps:stringarrayref=ref[||]letstep=ref(-1)letstep_next()=match!stepwith|swhens>=Array.length!steps-1->None|s->incrstep;(Some!steps.(!step))letstep_prev()=match!stepwith|swhens<=0->None|s->decrstep;(Some!steps.(!step))letstepsn=log_on_error~use:()@@Result.bind(get_existing_sessionn)@@fun(_,file)->Result.bind(File.readfile)@@funcontents->steps:=Array.of_list(of_stringcontents);step:=(-1);Ok()letnext_step()=ignore(step_next())letprev_step()=ignore(step_prev())end(* Ocaml syntax munging *)moduleOcaml=structletid_path_char=function|'0'..'9'|'A'..'Z'|'a'..'z'|'.'|'\''|'_'->true|_->falseletid_spans~start=ifs=""thenNoneelseletid_char=Txt.find_prev~sat:id_path_chars~startinifnot(id_path_chars.[id_char])thenNoneelseletkeep_prev=Txt.keep_prev_len~sat:id_path_chars~start:id_charinletid_start=id_char-keep_prev+1inletid_len=Txt.keep_next_len~sat:id_path_chars~start:id_startinletid_end=id_start+id_len-1inifid_end<id_startthenNoneelseSome(String.subsid_start(id_end-id_start+1))end(* Prompting *)modulePrompt=structmoduleI=structtypet=Tty.inputletcompare=compareendmoduleItrie=Trie.Make(I)typecmd=t->[`Eoi|`Kont|`Break]andt={mutabletty_w:int;readc:unit->intoption;output:string->unit;has_answer:Tty.input->t->stringoption;id_complete:string->(string*stringlist,string)result;id_info:string->((string*string*string)option,string)result;mutableclipboard:string;mutabletxt:Pstring.t;(* These are zero-based rows relat. to the prompt line for clearing. *)mutablelast_cr:int;(* last cursor render row *)mutablelast_max_r:int;(* last max render row *)}lethas_answerinputp=(* FIXME ocaml and utop are a bit weird. Try to sort that out.
Notably is there does seem to be any good reason not to input
successive ;; separated phrases, ocaml does that on .ml files.
Cf. https://github.com/ocaml/ocaml/issues/8813 *)letends_with_semisemis=letrecloopsi=matchi<0with|true->false|false->ifTxt.is_whites.[i]thenloops(i-1)elseifs.[i]<>';'thenfalseelseifi=0thenfalseelses.[i-1]=';'inloops(String.lengths-1)inmatchinputwith|`Enter->lettxt=Pstring.txtp.txtinifends_with_semisemitxtthenSome((String.trimtxt)^"\n")(* trim is for a cleaner hist. *)elseNone|_->Noneletcreate?(id_complete=funw->Ok(w,[]))?(id_info=funid->OkNone)?(output=Tty.output)~readc()={tty_w=80;readc;output;has_answer;id_complete;id_info;clipboard="";txt=Pstring.empty;last_cr=0;last_max_r=0;}(* Rendering *)letdingp=p.outputTty.dingletnewlinep=p.outputTty.newlineleterrorpfmt=leterror=Tty.styled_strTty.capstyle_err"Error"inletkmsg=p.output(Printf.sprintf"\r\n%s: %s\r\n"errormsg)inFormat.kasprintfkfmtletclear_screenp=p.output(String.concat""Tty.[clear_screen;cursor_origin])letclear_uip=(* Go from cursor to max row and back to prompt row clearing lines. *)letrecgoacc=function|0->acc|up->go(Tty.clear_row::Tty.cursor_up1::acc)(up-1)inletdown=p.last_max_r-p.last_crandup=p.last_max_rinletacc=go(Tty.clear_row::Tty.cursor_downdown::[])upinp.output(String.concat""(List.rev("\r"::acc)));p.last_cr<-0;p.last_max_r<-0letprompt="# "letmargin=" "letnl_margin="\r\n "letrender_prompt~active=letstyle=matchSession.recording()with|false->ifactivethenstyle_promptelsestyle_prompt_inactive()|true->ifactivethenstyle_prompt_recordingelsestyle_prompt_recording_inactive()inTty.styled_strTty.capstylepromptletrender_ui?(active=true)p=lettty_w=p.tty_wandmargin_w=String.lengthmargininletmax_r,(cr,cc,c_nl)=Pstring.geometry~tty_w~margin_wp.txtinletadd_lineaccl=nl_margin::l::accinletacc=[render_prompt~active]inletacc=List.fold_leftadd_lineacc(Txt.lines(Pstring.txtp.txt))inletacc="\r"::List.tlacc(* remove exceeding nl_margin *)inletacc=ifc_nl(* cursor wrapped *)then"\n"::accelseaccinletacc=matchactivewith|true->Tty.cursor_forwardcc::Tty.cursor_up(max_r-cr)::acc|false->accinletui=String.concat""(List.revacc)inp.outputTty.cursor_hide;clear_uip;p.outputui;p.outputTty.cursor_show;ifactivethen(p.last_cr<-cr;p.last_max_r<-max_r)letrender_id_completepprefixcandidates=letrender_candidateprefixc=matchString.lengthcwith|0->""|blen->letstyledstys=Tty.styled_strTty.capstysinmatchc.[0]=' 'with|true->(* Hackish. E.g. we don't actually get candidates one per line
in case of module types. *)styled(style_id_complete_info())c|false->matchString.indexc'\t'with|exceptionNot_found->c(* should not happen but be robust *)|tab->letrst_start=tab+1inletsuf_start=String.lengthprefixinletpre=String.subc0suf_startinletsuf=String.subcsuf_start(tab-suf_start)inletrst=":"^String.subcrst_start(blen-rst_start)inPrintf.sprintf" %s%s%s"pre(styledstyle_id_complete_suffsuf)(styled(style_id_complete_info())rst)inletcandidates=List.map(render_candidateprefix)candidatesinrender_ui~active:falsep;newlinep;p.output(String.concatTty.newlinecandidates);newlinepletrender_id_infopidtypdoc=letrender_idid=Tty.styled_strTty.cap(style_id_info_id)idinletrender_typet=Tty.styled_strTty.cap(style_id_info_type)tinlettyp=matchTxt.linestypwith|[""]|[]->""|t::ts->Printf.sprintf":%s"@@render_type(String.concatTty.newline@@t::List.map(Printf.sprintf"%s%s"margin)ts)inletacc=[Printf.sprintf"\r\n %s%s"(render_idid)typ]inletacc=List.rev_append(Txt.linesdoc)accinletacc=List.rev_map(Printf.sprintf"%s%s"margin)accinrender_ui~active:falsep;newlinep;p.output(String.concatTty.newlineacc);newlinep(* Commands *)letset_txt_valueptxt=p.txt<-Pstring.eoi(Pstring.vtxt)letset_txt_with_historyopp=matchop(Pstring.txtp.txt)with|None->dingp|Sometxt->set_txt_valueptxtletupdateopp=lettxt=opp.txtiniftxt==p.txt(* bof *)thendingpelsep.txt<-txtletupdate_with_killopp=lettxt,kill=opp.txtinletclipboard=matchkillwithNone->dingp;p.clipboard|Somes->sinp.txt<-txt;p.clipboard<-clipboardletsoi=updatePstring.soileteoi=updatePstring.eoiletsol=updatePstring.solleteol=updatePstring.eolletprev_char=updatePstring.prev_charletnext_char=updatePstring.next_charletprev_word=updatePstring.prev_wordletnext_word=updatePstring.next_wordletprev_line=updatePstring.prev_lineletnext_line=updatePstring.next_lineletinsertbytes=update(Pstring.insertbytes)letdelete_next_char=updatePstring.delete_next_charletdelete_prev_char=updatePstring.delete_prev_charletset_mark=update(funp->Pstring.set_mark(Some(Pstring.cursorp))p)letswap_cursor_and_mark=updatePstring.swap_cursor_and_markletkill_prev_word=update_with_killPstring.kill_prev_wordletkill_next_word=update_with_killPstring.kill_next_wordletkill_to_sol=update_with_killPstring.kill_to_solletkill_to_eol=update_with_killPstring.kill_to_eolletkill_region=update_with_killPstring.kill_regionletyankp=ifp.clipboard=""thendingpelseletmark=Some(Pstring.cursorp.txt)andclip=p.clipboardinupdate(funp->Pstring.set_markmark(Pstring.insertclipp))pleteditp=matchEditor.edit_string~ext:".ml"(Pstring.txtp.txt)with|Errore->errorp"%s"e|Oktxt->set_txt_valueptxtletid_completep=letcompletion_startp=letrecloopsi=ifi>=0&&Ocaml.id_path_chars.[i]thenloops(i-1)elseletret=i+1inifret=(Pstring.cursorp)thenNoneelseSomeretinloop(Pstring.txtp)(Pstring.cursorp-1)inmatchcompletion_startp.txtwith|None->dingp|Somestart->letset_substpstartoldw=letstop=start+String.lengtholdinp.txt<-Pstring.subst~start~stopwp.txtinletlast=Pstring.cursorp.txt-1inletword=Pstring.txt_range~first:start~lastp.txtinmatchp.id_completewordwith|Errore->errorp"%s"e|Ok(_,[])->dingp|Ok(w,[_])->set_substpstartwordw|Ok(w,cs)->render_id_completepwcs;set_substpstartwordwletid_infop=matchOcaml.id_span(Pstring.txtp.txt)~start:(Pstring.cursorp.txt)with|None->dingp|Someid->matchp.id_infoidwith|Errore->errorp"%s"e|OkNone->dingp;|Ok(Some(id,typ,doc))->render_id_infopidtypdocletctrl_dp=ifPstring.txtp.txt=""then`Eoielse(delete_next_charp;`Kont)letsession_next_stepp=matchSession.step_next()with|None->dingp|Somes->set_txt_valuepsletsession_prev_stepp=matchSession.step_prev()with|None->dingp|Somes->set_txt_valuepsletprev_history=set_txt_with_historyHistory.prevletnext_history=set_txt_with_historyHistory.nextletbreakp=`Breakletkontfp=fp;`Kontletcmds:(Tty.inputlist*cmd*string)list=[(**)[`Home],kontsoi,"move to start of input";[`End],konteoi,"move to end of input";(**)[`Ctrl(`Key0x61)(* a *)],kontsol,"move to start of line";[`Ctrl(`Key0x65)(* e *)],konteol,"move to end of line";(**)[`Ctrl(`Key0x62)(* b *)],kontprev_char,"move to previous character";[`Ctrl(`Key0x66)(* f *)],kontnext_char,"move to next character";[`Arrow`Left],kontprev_char,"move to previous character";[`Arrow`Right],kontnext_char,"move to next character";(**)[`Meta0x62(* b *)],kontprev_word,"move to start of previous word";[`Meta0x66(* f *)],kontnext_word,"move after the end of next word";[`Ctrl(`Arrow`Left)],kontprev_word,"move to start of previous word";[`Ctrl(`Arrow`Right)],kontnext_word,"move after the end of next word";(**)[`Ctrl(`Key0x70)(* p *)],kontprev_line,"move to previous line";[`Ctrl(`Key0x6E)(* n *)],kontnext_line,"move to next line";(**)[`Arrow`Up],kontprev_history,"previous history entry";[`Arrow`Down],kontnext_history,"next history entry";(**)[`Backspace],kontdelete_prev_char,"delete previous character";[`Delete],kontdelete_next_char,"delete next character";[`Ctrl(`Key0x64)(* d *)],ctrl_d,"delete next character or exit if input is empty";[`Ctrl(`Key0x63)(* c *)],break,"abandon input";(**)[`Ctrl(`Key0x60)(* space ? *)],kontset_mark,"set the mark";[`Ctrl(`Key0x78)(* x *);`Ctrl(`Key0x78)(* x *)],kontswap_cursor_and_mark,"swap cursor and mark";[`Ctrl(`Key0x79)(* y *)],kontyank,"yank";(**)[`Ctrl(`Key0x6B)(* k *)],kontkill_to_eol,"kill to end of line";[`Ctrl(`Key0x75)(* k *)],kontkill_to_sol,"kill to start of line";[`Meta0x7F],kontkill_prev_word,"kill to start of previous word";[`Meta0x64(* d *)],kontkill_next_word,"kill to end of next word";[`Ctrl(`Key0x77)(* w *)],kontkill_region,"kill region";(**)[`Shift(`Arrow`Up)],kontsession_prev_step,"previous session step";[`Shift(`Arrow`Down)],kontsession_next_step,"next session step";[`Ctrl(`Key0x78)(* x *);`Ctrl(`Key0x70)(* p *)],kontsession_prev_step,"previous session step";[`Ctrl(`Key0x78)(* x *);`Ctrl(`Key0x6E)(* n *)],kontsession_next_step,"next session step";(**)[`Ctrl(`Key0x6C)(* l *)],kontclear_screen,"clear screen";[`Tab],kontid_complete,"complete identifier";[`Ctrl(`Key0x74)(* t *)],kontid_info,"show identifier type and documentation";[`Ctrl(`Key0x78)(* x *);`Ctrl(`Key0x65)(* e *)],kontedit,"edit input with external program (VISUAL or EDITOR env var)"]letpp_cmdppf(is,_,doc)=letpp_is=Fmt.ttystyle_key(Fmt.(list~sep:spTty.pp_input))inFmt.pfppf"@[%a @[%a@]@]"pp_isisFmt.textdocletcmd_trie=letaddt(is,cmd,_doc)=Itrie.addis(Somecmd)tinList.fold_leftaddItrie.emptycmds(* Event loop *)letaskp=letresetp=p.last_cr<-0;p.last_max_r<-0;p.txt<-Pstring.emptyinletresizep=p.tty_w<-Tty.widthp.readcinletreturnp=render_ui~active:falsep;newlinepinletreclooppinput_state=render_uip;matchTty.inputp.readcwith|None->(* EINTR (and thus SIGWINCH) *)resizep;looppinput_state|Somei->matchp.has_answeripwith|Somea->(History.adda;Session.add_if_recordinga;returnp;`Answera)|None->letinput_state=Itrie.find[i]input_stateinmatchItrie.valueinput_statewith|NonewhenItrie.is_emptyinput_state->beginmatchiwith|`Bytesbytes->insertbytesp;looppcmd_trie|`Enter->insert"\n"p;looppcmd_trie|_->dingp;looppcmd_trieend|None->looppinput_state|Somecmd->matchcmdpwith|`Kont->looppcmd_trie|`Break->History.restart();returnp;`Break|`Eoi->`Eoiin(resetp;resizep;looppcmd_trie)end(* Help *)lethelp()=letpp_manualppf()=Fmt.pfppf"@[Consult '%a' for the manual and API.@]"pp_code"odig doc down"inletpp_sessionppf()=Fmt.pfppf"%a:@,Support for sessions is in the %a module.@,\
Use '%a' to list sessions."pp_doc_section"Sessions"pp_code"Down.Session"pp_code"Down.Session.list ()"inletpp_key_bindingsppf()=Fmt.pfppf"%a:@,@[<v>%a@]"pp_doc_section"Key bindings"(Fmt.listPrompt.pp_cmd)Prompt.cmdsinletpp_helpppf()=Fmt.pfppf" @[<v>@,%a@,%a@,@,%a@,@,%a@,@]@."pp_doc_section"Welcome to Down!"pp_manual()pp_session()pp_key_bindings()inpp_helpFormat.std_formatter()(* Completion and doc lookup via ocp-index *)moduleOcp_index=struct(* FIXME. POC hack via ocp-index, we likely want to that ourselves since we
also need to peek in the OCaml toplevel symtable to be able to
complete what the user defined and keep track of [open]s. *)moduleCtrie=Trie.Make(Char)letstring_to_lists=letrecgosiacc=ifi<0thenaccelsegos(i-1)(s.[i]::acc)ingos(String.lengths-1)[]letstring_of_listl=letb=Buffer.create255inList.iter(Buffer.add_charb)l;(Buffer.contentsb)lethas_ocp_index=lazybeginmatchCmd.exists["ocp-index"]with|Error_ase->e|Oktrue->Ok()|Okfalse->Error(Fmt.str"Completion and doc lookup needs ocp-index. Try '%a'."pp_code"opam install ocp-index")endletcomplete_cmdtoken=["ocp-index";"complete";"-f";"%q \t %t";token]letcomplete_wordwordresults=letadd_idaccr=letid=String.trim(List.hd(String.split_on_char'\t'r))inifid=""thenaccelseCtrie.add(string_to_listid)(Some())accinletids=List.fold_leftadd_idCtrie.emptyresultsinletword,_=Ctrie.find_fork(string_to_listword)idsin(string_of_listword,results)letfinish_single_complete=function|""->""|wwhenw.[String.lengthw-1]='.'->w|w->letpath_start=matchString.rindexw'.'with|exceptionNot_found->0|i->i+1inifChar.uppercase_asciiw.[path_start]=w.[path_start]thenw^"."(* Likely Module name path segment. *)elsew^" "(* Likely Module structure item segment. *)letid_complete=function|""->Ok("",[])|w->Result.bind(Lazy.forcehas_ocp_index)@@fun()->Result.bind(Result.map_errorsnd@@Cmd.read(complete_cmdw))@@funs->matchList.rev(Txt.liness)with|[]|[""]|["";""]->Ok(w,[])|""::rlines|rlines->matchcomplete_wordw(List.revrlines)with|w,([_]ascs)->Ok(finish_single_completew,cs)|_asret->Okretletprint_cmdid=["ocp-index";"print";id;"%q \\t %t\\n(**)\\n%d"]letparse_id_info=function|""->None|o->matchTxt_entries.of_string~sep:"(**)"owith|[]->None|[v]->Some(v,"","")|(id_sig::doc::_)->matchString.indexid_sig'\t'with|exceptionNot_found->Some(o,"",doc)|i->letlen=String.lengthid_siginletid=String.subid_sig0iinlettyp=ifi+1=lenthen""elseString.subid_sig(i+1)(len-(i+1))inSome(id,typ,doc)letid_info=function|""->OkNone|id->Result.bind(Lazy.forcehas_ocp_index)@@fun()->match(Cmd.read(print_cmdid))with|Error(2,_)->OkNone|Error(n,e)->Errore|Oko->Ok(parse_id_infoo)end(* Toplevel readline *)letblit_toploop_bufsibblen=letslen=String.lengthsinletslen_to_write=slen-iinletlen=minblenslen_to_writeinletsnext=i+leninBytes.blit_stringsib0len;len,(ifsnext<slenthenSome(s,snext)elseNone)letdown_readlinep=letrem=refNoneinfunpromptblen->match!remwith|Some(s,i)->letlen,rem'=blit_toploop_bufsibleninrem:=rem';(len,false)|None->letrecloopp=matchPrompt.askpwith|`Eoi->(0,true)|`Break->Tty.output"Interrupted.\r\n";loopp|`Answerans->letlen,rem'=blit_toploop_bufans0bleninrem:=rem';(len,false)inmatchStdin.set_raw_modetruewith|false->!original_ocaml_readlinepromptblen|true->letr=looppinignore(Stdin.set_raw_modefalse);rexternalsigwinch:unit->int="ocaml_down_sigwinch"letinstall_sigwinch_interrupt()=(* Sufficient to interrupt the event loop on window size changes. *)ifSys.unixthenSys.set_signal(sigwinch())(Sys.Signal_handle(fun_->()))letpp_announceppf()=Fmt.pfppf"%a v0.4.0 loaded. Type %a for more info."pp_doc_section"Down"pp_code"Down.help ()"leterr_no_ansi="no ANSI terminal capability detected."leterr_no_raw="failed to set stdin in raw mode."letinstall_down()=letline_edition=matchTty.capwith|`None->Errorerr_no_ansi|`Ansi->matchStdin.set_raw_modetruewith|false->Errorerr_no_raw|true->ignore(Stdin.set_raw_modefalse);Ok()inletannounce()=Fmt.pr"%a@."pp_announce()inHistory.load();at_exitHistory.save;Session.load_unsaved_record();at_exitSession.save_unsaved_record;original_ocaml_readline:=!Toploop.read_interactive_input;matchline_editionwith|Ok()->letid_complete=Ocp_index.id_completeinletid_info=Ocp_index.id_infoinletp=Prompt.create~id_complete~id_info~readc:Stdin.readc()inToploop.read_interactive_input:=down_readlinep;install_sigwinch_interrupt();Stdin.enable_bracketed_pastestdout;announce()|Errorerr->announce();log_warning"Down line edition disabled: %s"errlet()=if!Sys.interactivetheninstall_down()(* Private *)modulePrivate=structletunicode_version=Down_tty_width.unicode_versionlettty_test()=matchTty.capwith|`None->print_endlineerr_no_ansi|`Ansi->matchStdin.set_raw_modetruewith|false->print_endlineerr_no_raw|true->letw=Tty.widthStdin.readcinletwelcome=Fmt.str"\r\nWelcome to Down's TTY test! Your width is %d. Ding!\r\n\
Hit your keyboard. C-{c,d} stops the test.\r\n\r\n"winTty.outputwelcome;Tty.outputTty.ding;letrecloop()=matchTty.inputStdin.readcwith|None->letw=Tty.widthStdin.readcinTty.output(Fmt.str"Your width is %d.\r\n"w);loop()|Somei->matchiwith|`Ctrl(`Key0x63)(* c *)->print_endline"Bye.\r"|`Ctrl(`Key0x64)(* d *)->print_endline"EOF Bye.\r"|_->print_endline(Format.asprintf"%a\r"Tty.pp_inputi);loop()inloop()end