1// Package edit implements the line editor for Elvish. 2// 3// The line editor is based on the cli package, which implements a general, 4// Elvish-agnostic line editor, and multiple "addon" packages. This package 5// glues them together and provides Elvish bindings for them. 6package edit 7 8import ( 9 _ "embed" 10 "fmt" 11 "sync" 12 13 "src.elv.sh/pkg/cli" 14 "src.elv.sh/pkg/eval" 15 "src.elv.sh/pkg/eval/vals" 16 "src.elv.sh/pkg/eval/vars" 17 "src.elv.sh/pkg/parse" 18 "src.elv.sh/pkg/store/storedefs" 19) 20 21// Editor is the interactive line editor for Elvish. 22type Editor struct { 23 app cli.App 24 ns *eval.Ns 25 26 excMutex sync.RWMutex 27 excList vals.List 28 29 // Maybe move this to another type that represents the REPL cycle as a whole, not just the 30 // read/edit portion represented by the Editor type. 31 AfterCommand []func(src parse.Source, duration float64, err error) 32} 33 34// An interface that wraps notifyf and notifyError. It is only implemented by 35// the *Editor type; functions may take a notifier instead of *Editor argument 36// to make it clear that they do not depend on other parts of *Editor. 37type notifier interface { 38 notifyf(format string, args ...interface{}) 39 notifyError(ctx string, e error) 40} 41 42// NewEditor creates a new editor. The TTY is used for input and output. The 43// Evaler is used for syntax highlighting, completion, and calling callbacks. 44// The Store is used for saving and retrieving command and directory history. 45func NewEditor(tty cli.TTY, ev *eval.Evaler, st storedefs.Store) *Editor { 46 // Declare the Editor with a nil App first; some initialization functions 47 // require a notifier as an argument, but does not use it immediately. 48 ed := &Editor{excList: vals.EmptyList} 49 nb := eval.BuildNsNamed("edit") 50 appSpec := cli.AppSpec{TTY: tty} 51 52 hs, err := newHistStore(st) 53 if err != nil { 54 _ = err // TODO(xiaq): Report the error. 55 } 56 57 initHighlighter(&appSpec, ev) 58 initMaxHeight(&appSpec, nb) 59 initReadlineHooks(&appSpec, ev, nb) 60 initAddCmdFilters(&appSpec, ev, nb, hs) 61 initGlobalBindings(&appSpec, ed, ev, nb) 62 initInsertAPI(&appSpec, ed, ev, nb) 63 initPrompts(&appSpec, ed, ev, nb) 64 ed.app = cli.NewApp(appSpec) 65 66 initExceptionsAPI(ed, nb) 67 initVarsAPI(ed, nb) 68 initCommandAPI(ed, ev, nb) 69 initListings(ed, ev, st, hs, nb) 70 initNavigation(ed, ev, nb) 71 initCompletion(ed, ev, nb) 72 initHistWalk(ed, ev, hs, nb) 73 initInstant(ed, ev, nb) 74 initMinibuf(ed, ev, nb) 75 76 initRepl(ed, ev, nb) 77 initBufferBuiltins(ed.app, nb) 78 initTTYBuiltins(ed.app, tty, nb) 79 initMiscBuiltins(ed.app, nb) 80 initStateAPI(ed.app, nb) 81 initStoreAPI(ed.app, nb, hs) 82 83 ed.ns = nb.Ns() 84 initElvishState(ev, ed.ns) 85 86 return ed 87} 88 89//elvdoc:var exceptions 90// 91// A list of exceptions thrown from callbacks such as prompts. Useful for 92// examining tracebacks and other metadata. 93 94func initExceptionsAPI(ed *Editor, nb eval.NsBuilder) { 95 nb.AddVar("exceptions", vars.FromPtrWithMutex(&ed.excList, &ed.excMutex)) 96} 97 98//go:embed init.elv 99var initElv string 100 101// Initialize the `edit` module by executing the pre-defined Elvish code for the module. 102func initElvishState(ev *eval.Evaler, ns *eval.Ns) { 103 src := parse.Source{Name: "[init.elv]", Code: initElv} 104 err := ev.Eval(src, eval.EvalCfg{Global: ns}) 105 if err != nil { 106 panic(err) 107 } 108} 109 110// ReadCode reads input from the user. 111func (ed *Editor) ReadCode() (string, error) { 112 return ed.app.ReadCode() 113} 114 115// Notify adds a note to the notification buffer. 116func (ed *Editor) Notify(note string) { 117 ed.app.Notify(note) 118} 119 120// RunAfterCommandHooks runs callbacks involving the interactive completion of a command line. 121func (ed *Editor) RunAfterCommandHooks(src parse.Source, duration float64, err error) { 122 for _, f := range ed.AfterCommand { 123 f(src, duration, err) 124 } 125} 126 127// Ns returns a namespace for manipulating the editor from Elvish code. 128// 129// See https://elv.sh/ref/edit.html for the Elvish API. 130func (ed *Editor) Ns() *eval.Ns { 131 return ed.ns 132} 133 134func (ed *Editor) notifyf(format string, args ...interface{}) { 135 ed.app.Notify(fmt.Sprintf(format, args...)) 136} 137 138func (ed *Editor) notifyError(ctx string, e error) { 139 if exc, ok := e.(eval.Exception); ok { 140 ed.excMutex.Lock() 141 defer ed.excMutex.Unlock() 142 ed.excList = ed.excList.Cons(exc) 143 ed.notifyf("[%v error] %v\n"+ 144 `see stack trace with "show $edit:exceptions[%d]"`, 145 ctx, e, ed.excList.Len()-1) 146 } else { 147 ed.notifyf("[%v error] %v", ctx, e) 148 } 149} 150