github.com/elves/elvish@v0.15.0/pkg/edit/config_api.go (about) 1 package edit 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "github.com/elves/elvish/pkg/cli" 9 "github.com/elves/elvish/pkg/cli/histutil" 10 "github.com/elves/elvish/pkg/diag" 11 "github.com/elves/elvish/pkg/eval" 12 "github.com/elves/elvish/pkg/eval/vals" 13 "github.com/elves/elvish/pkg/eval/vars" 14 "github.com/elves/elvish/pkg/store" 15 ) 16 17 //elvdoc:var max-height 18 // 19 // Maximum height the editor is allowed to use, defaults to `+Inf`. 20 // 21 // By default, the height of the editor is only restricted by the terminal 22 // height. Some modes like location mode can use a lot of lines; as a result, 23 // it can often occupy the entire terminal, and push up your scrollback buffer. 24 // Change this variable to a finite number to restrict the height of the editor. 25 26 func initMaxHeight(appSpec *cli.AppSpec, nb eval.NsBuilder) { 27 maxHeight := newIntVar(-1) 28 appSpec.MaxHeight = func() int { return maxHeight.GetRaw().(int) } 29 nb.Add("max-height", maxHeight) 30 } 31 32 func initReadlineHooks(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 33 initBeforeReadline(appSpec, ev, nb) 34 initAfterReadline(appSpec, ev, nb) 35 } 36 37 //elvdoc:var before-readline 38 // 39 // A list of functions to call before each readline cycle. Each function is 40 // called without any arguments. 41 42 func initBeforeReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 43 hook := newListVar(vals.EmptyList) 44 nb["before-readline"] = hook 45 appSpec.BeforeReadline = append(appSpec.BeforeReadline, func() { 46 callHooks(ev, "$<edit>:before-readline", hook.Get().(vals.List)) 47 }) 48 } 49 50 //elvdoc:var after-readline 51 // 52 // A list of functions to call after each readline cycle. Each function is 53 // called with a single string argument containing the code that has been read. 54 55 func initAfterReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { 56 hook := newListVar(vals.EmptyList) 57 nb["after-readline"] = hook 58 appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { 59 callHooks(ev, "$<edit>:after-readline", hook.Get().(vals.List), code) 60 }) 61 } 62 63 //elvdoc:var add-cmd-filters 64 // 65 // List of filters to run before adding a command to history. 66 // 67 // A filter is a function that takes a command as argument and outputs 68 // a boolean value. If any of the filters outputs `$false`, the 69 // command is not saved to history, and the rest of the filters are 70 // not run. The default value of this list contains a filter which 71 // ignores command starts with space. 72 73 func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder, s histutil.Store) { 74 ignoreLeadingSpace := eval.NewGoFn("<ignore-cmd-with-leading-space>", 75 func(s string) bool { return !strings.HasPrefix(s, " ") }) 76 filters := newListVar(vals.MakeList(ignoreLeadingSpace)) 77 nb["add-cmd-filters"] = filters 78 79 appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { 80 if code != "" && 81 callFilters(ev, "$<edit>:add-cmd-filters", 82 filters.Get().(vals.List), code) { 83 s.AddCmd(store.Cmd{Text: code, Seq: -1}) 84 } 85 // TODO(xiaq): Handle the error. 86 }) 87 } 88 89 func callHooks(ev *eval.Evaler, name string, hook vals.List, args ...interface{}) { 90 if hook.Len() == 0 { 91 return 92 } 93 94 ports, cleanup := eval.PortsFromStdFiles(ev.ValuePrefix()) 95 evalCfg := eval.EvalCfg{Ports: ports[:]} 96 defer cleanup() 97 98 i := -1 99 for it := hook.Iterator(); it.HasElem(); it.Next() { 100 i++ 101 name := fmt.Sprintf("%s[%d]", name, i) 102 fn, ok := it.Elem().(eval.Callable) 103 if !ok { 104 // TODO(xiaq): This is not testable as it depends on stderr. 105 // Make it testable. 106 diag.Complainf(os.Stderr, "%s not function", name) 107 continue 108 } 109 110 err := ev.Call(fn, eval.CallCfg{Args: args, From: name}, evalCfg) 111 if err != nil { 112 diag.ShowError(os.Stderr, err) 113 } 114 } 115 } 116 117 func callFilters(ev *eval.Evaler, name string, filters vals.List, args ...interface{}) bool { 118 if filters.Len() == 0 { 119 return true 120 } 121 122 i := -1 123 for it := filters.Iterator(); it.HasElem(); it.Next() { 124 i++ 125 name := fmt.Sprintf("%s[%d]", name, i) 126 fn, ok := it.Elem().(eval.Callable) 127 if !ok { 128 // TODO(xiaq): This is not testable as it depends on stderr. 129 // Make it testable. 130 diag.Complainf(os.Stderr, "%s not function", name) 131 continue 132 } 133 134 port1, collect, err := eval.CapturePort() 135 if err != nil { 136 diag.Complainf(os.Stderr, "cannot create pipe to run filter") 137 return true 138 } 139 err = ev.Call(fn, eval.CallCfg{Args: args, From: name}, 140 // TODO: Supply the Chan component of port 2. 141 eval.EvalCfg{Ports: []*eval.Port{nil, port1, {File: os.Stderr}}}) 142 out := collect() 143 144 if err != nil { 145 diag.Complainf(os.Stderr, "%s return error", name) 146 continue 147 } 148 if len(out) != 1 { 149 diag.Complainf(os.Stderr, "filter %s should only return $true or $false", name) 150 continue 151 } 152 p, ok := out[0].(bool) 153 if !ok { 154 diag.Complainf(os.Stderr, "filter %s should return bool", name) 155 continue 156 } 157 if !p { 158 return false 159 } 160 } 161 return true 162 } 163 164 func newIntVar(i int) vars.PtrVar { return vars.FromPtr(&i) } 165 func newFloatVar(f float64) vars.PtrVar { return vars.FromPtr(&f) } 166 func newBoolVar(b bool) vars.PtrVar { return vars.FromPtr(&b) } 167 func newListVar(l vals.List) vars.PtrVar { return vars.FromPtr(&l) } 168 func newMapVar(m vals.Map) vars.PtrVar { return vars.FromPtr(&m) } 169 func newFnVar(c eval.Callable) vars.PtrVar { return vars.FromPtr(&c) } 170 func newBindingVar(b BindingMap) vars.PtrVar { return vars.FromPtr(&b) }