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