1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078(*---------------------------------------------------------------------------
Copyright (c) 2017 The down programmers. All rights reserved.
Distributed under the ISC license, see terms at the end of the file.
---------------------------------------------------------------------------*)openDown_std(* Access toploop API functionality regardless of ocaml or ocamlnat.
Works around 4. in https://github.com/ocaml/ocaml/issues/7589 *)moduletypeTOP=sigvalread_interactive_input:(string->bytes->int->int*bool)refvaluse_file:Format.formatter->string->boolvaluse_silently:Format.formatter->string->boolendmoduleNil=structletnil()=invalid_arg"Down.Private.set_top not called"letread_interactive_input=ref(fun___->nil())letuse_file__=nil()letuse_silently__=nil()end(* Set to the right implementation by Down_top or Down_nattop *)lettop:(moduleTOP)ref=ref(moduleNil:TOP)letoriginal_ocaml_readline=ref(fun___->assertfalse)letuse_file?(silent=false)file=letmoduleTop=(val!top:TOP)inmatchsilentwith|true->Top.use_silentlyFormat.std_formatterfile|false->Top.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=substp.cursorp.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()=letmoduleTop=(val!top:TOP)inmatch!Top.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)inclear_uip;p.outputui;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=p.txt<-Pstring.subststart(start+String.lengthold)wp.txtinletword=Pstring.txt_rangestart(Pstring.cursorp.txt-1)p.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. *)Sys.set_signal(sigwinch())(Sys.Signal_handle(fun_->()))letpp_announceppf()=Fmt.pfppf"%a v0.0.4 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()inletmoduleTop=(val!top:TOP)inHistory.load();at_exitHistory.save;Session.load_unsaved_record();at_exitSession.save_unsaved_record;original_ocaml_readline:=!Top.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()inTop.read_interactive_input:=down_readlinep;install_sigwinch_interrupt();announce()|Errorerr->announce();log_warning"Down line edition disabled: %s"err(* Private *)modulePrivate=structmoduletypeTOP=TOPletset_topt=top:=t;install_down()letunicode_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(*---------------------------------------------------------------------------
Copyright (c) 2017 The down programmers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---------------------------------------------------------------------------*)