src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/config_api.go (about) 1 package edit 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "src.elv.sh/pkg/cli" 9 "src.elv.sh/pkg/cli/histutil" 10 "src.elv.sh/pkg/diag" 11 "src.elv.sh/pkg/eval" 12 "src.elv.sh/pkg/eval/vals" 13 "src.elv.sh/pkg/eval/vars" 14 "src.elv.sh/pkg/store/storedefs" 15 ) 16 17 func initMaxHeight(appSpec *cli.AppSpec, nb eval.NsBuilder) { 18 maxHeight := newIntVar(-1) 19 appSpec.MaxHeight = func() int { return maxHeight.GetRaw().(int) } 20 nb.AddVar("max-height", maxHeight) 21 } 22 23 func initReadlineHooks(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 24 initBeforeReadline(appSpec, ev, nb) 25 initAfterReadline(appSpec, ev, nb) 26 } 27 28 func initBeforeReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 29 hook := newListVar(vals.EmptyList) 30 nb.AddVar("before-readline", hook) 31 appSpec.BeforeReadline = append(appSpec.BeforeReadline, func() { 32 eval.CallHook(ev, nil, "$<edit>:before-readline", hook.Get().(vals.List)) 33 }) 34 } 35 36 func initAfterReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 37 hook := newListVar(vals.EmptyList) 38 nb.AddVar("after-readline", hook) 39 appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { 40 eval.CallHook(ev, nil, "$<edit>:after-readline", hook.Get().(vals.List), code) 41 }) 42 } 43 44 func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder, s histutil.Store) { 45 ignoreLeadingSpace := eval.NewGoFn("<ignore-cmd-with-leading-space>", 46 func(s string) bool { return !strings.HasPrefix(s, " ") }) 47 filters := newListVar(vals.MakeList(ignoreLeadingSpace)) 48 nb.AddVar("add-cmd-filters", filters) 49 50 appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { 51 if code != "" && 52 callFilters(ev, "$<edit>:add-cmd-filters", 53 filters.Get().(vals.List), code) { 54 s.AddCmd(storedefs.Cmd{Text: code, Seq: -1}) 55 } 56 // TODO(xiaq): Handle the error. 57 }) 58 } 59 60 func initGlobalBindings(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { 61 bindingVar := newBindingVar(emptyBindingsMap) 62 appSpec.GlobalBindings = newMapBindings(nt, ev, bindingVar) 63 nb.AddVar("global-binding", bindingVar) 64 } 65 66 func callFilters(ev *eval.Evaler, name string, filters vals.List, args ...any) bool { 67 if filters.Len() == 0 { 68 return true 69 } 70 71 i := -1 72 for it := filters.Iterator(); it.HasElem(); it.Next() { 73 i++ 74 name := fmt.Sprintf("%s[%d]", name, i) 75 fn, ok := it.Elem().(eval.Callable) 76 if !ok { 77 complain("%s not function", name) 78 continue 79 } 80 81 port1, collect, err := eval.ValueCapturePort() 82 if err != nil { 83 complain("cannot create pipe to run filter") 84 return true 85 } 86 err = ev.Call(fn, eval.CallCfg{Args: args, From: name}, 87 // TODO: Supply the Chan component of port 2. 88 eval.EvalCfg{Ports: []*eval.Port{nil, port1, {File: os.Stderr}}}) 89 out := collect() 90 91 if err != nil { 92 complain("%s return error", name) 93 continue 94 } 95 if len(out) != 1 { 96 complain("filter %s should only return $true or $false", name) 97 continue 98 } 99 p, ok := out[0].(bool) 100 if !ok { 101 complain("filter %s should return bool", name) 102 continue 103 } 104 if !p { 105 return false 106 } 107 } 108 return true 109 } 110 111 // TODO: This is not testable as it depends on stderr. Make it testable. 112 func complain(format string, args ...any) { 113 diag.ShowError(os.Stderr, fmt.Errorf(format, args...)) 114 } 115 116 func newIntVar(i int) vars.PtrVar { return vars.FromPtr(&i) } 117 func newFloatVar(f float64) vars.PtrVar { return vars.FromPtr(&f) } 118 func newBoolVar(b bool) vars.PtrVar { return vars.FromPtr(&b) } 119 func newListVar(l vals.List) vars.PtrVar { return vars.FromPtr(&l) } 120 func newMapVar(m vals.Map) vars.PtrVar { return vars.FromPtr(&m) } 121 func newFnVar(c eval.Callable) vars.PtrVar { return vars.FromPtr(&c) } 122 func newBindingVar(b bindingsMap) vars.PtrVar { return vars.FromPtr(&b) }