elvish/pkg/edit/config_api.go
Qi Xiao 6e75f3468c Move API of pkg/store into pkg/storedefs.
This makes the nodaemon entrypoint not depend on boltdb at all, reducing its
size by 100KB (on darwin-arm64).
2021-06-19 01:54:00 +01:00

183 lines
5.6 KiB
Go

package edit
import (
"fmt"
"os"
"strings"
"src.elv.sh/pkg/cli"
"src.elv.sh/pkg/cli/histutil"
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/eval/vars"
"src.elv.sh/pkg/store/storedefs"
)
//elvdoc:var max-height
//
// Maximum height the editor is allowed to use, defaults to `+Inf`.
//
// By default, the height of the editor is only restricted by the terminal
// height. Some modes like location mode can use a lot of lines; as a result,
// it can often occupy the entire terminal, and push up your scrollback buffer.
// Change this variable to a finite number to restrict the height of the editor.
func initMaxHeight(appSpec *cli.AppSpec, nb eval.NsBuilder) {
maxHeight := newIntVar(-1)
appSpec.MaxHeight = func() int { return maxHeight.GetRaw().(int) }
nb.Add("max-height", maxHeight)
}
func initReadlineHooks(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) {
initBeforeReadline(appSpec, ev, nb)
initAfterReadline(appSpec, ev, nb)
}
//elvdoc:var before-readline
//
// A list of functions to call before each readline cycle. Each function is
// called without any arguments.
func initBeforeReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) {
hook := newListVar(vals.EmptyList)
nb["before-readline"] = hook
appSpec.BeforeReadline = append(appSpec.BeforeReadline, func() {
callHooks(ev, "$<edit>:before-readline", hook.Get().(vals.List))
})
}
//elvdoc:var after-readline
//
// A list of functions to call after each readline cycle. Each function is
// called with a single string argument containing the code that has been read.
func initAfterReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) {
hook := newListVar(vals.EmptyList)
nb["after-readline"] = hook
appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) {
callHooks(ev, "$<edit>:after-readline", hook.Get().(vals.List), code)
})
}
//elvdoc:var add-cmd-filters
//
// List of filters to run before adding a command to history.
//
// A filter is a function that takes a command as argument and outputs
// a boolean value. If any of the filters outputs `$false`, the
// command is not saved to history, and the rest of the filters are
// not run. The default value of this list contains a filter which
// ignores command starts with space.
func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder, s histutil.Store) {
ignoreLeadingSpace := eval.NewGoFn("<ignore-cmd-with-leading-space>",
func(s string) bool { return !strings.HasPrefix(s, " ") })
filters := newListVar(vals.MakeList(ignoreLeadingSpace))
nb["add-cmd-filters"] = filters
appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) {
if code != "" &&
callFilters(ev, "$<edit>:add-cmd-filters",
filters.Get().(vals.List), code) {
s.AddCmd(storedefs.Cmd{Text: code, Seq: -1})
}
// TODO(xiaq): Handle the error.
})
}
//elvdoc:var global-binding
//
// Global keybindings, consulted for keys not handled by mode-specific bindings.
//
// See [Keybindings](#keybindings).
func initGlobalBindings(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) {
bindingVar := newBindingVar(emptyBindingsMap)
appSpec.GlobalBindings = newMapBindings(nt, ev, bindingVar)
nb.Add("global-binding", bindingVar)
}
func callHooks(ev *eval.Evaler, name string, hook vals.List, args ...interface{}) {
if hook.Len() == 0 {
return
}
ports, cleanup := eval.PortsFromStdFiles(ev.ValuePrefix())
evalCfg := eval.EvalCfg{Ports: ports[:]}
defer cleanup()
i := -1
for it := hook.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("%s[%d]", name, i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf(os.Stderr, "%s not function", name)
continue
}
err := ev.Call(fn, eval.CallCfg{Args: args, From: name}, evalCfg)
if err != nil {
diag.ShowError(os.Stderr, err)
}
}
}
func callFilters(ev *eval.Evaler, name string, filters vals.List, args ...interface{}) bool {
if filters.Len() == 0 {
return true
}
i := -1
for it := filters.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("%s[%d]", name, i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf(os.Stderr, "%s not function", name)
continue
}
port1, collect, err := eval.CapturePort()
if err != nil {
diag.Complainf(os.Stderr, "cannot create pipe to run filter")
return true
}
err = ev.Call(fn, eval.CallCfg{Args: args, From: name},
// TODO: Supply the Chan component of port 2.
eval.EvalCfg{Ports: []*eval.Port{nil, port1, {File: os.Stderr}}})
out := collect()
if err != nil {
diag.Complainf(os.Stderr, "%s return error", name)
continue
}
if len(out) != 1 {
diag.Complainf(os.Stderr, "filter %s should only return $true or $false", name)
continue
}
p, ok := out[0].(bool)
if !ok {
diag.Complainf(os.Stderr, "filter %s should return bool", name)
continue
}
if !p {
return false
}
}
return true
}
func newIntVar(i int) vars.PtrVar { return vars.FromPtr(&i) }
func newFloatVar(f float64) vars.PtrVar { return vars.FromPtr(&f) }
func newBoolVar(b bool) vars.PtrVar { return vars.FromPtr(&b) }
func newListVar(l vals.List) vars.PtrVar { return vars.FromPtr(&l) }
func newMapVar(m vals.Map) vars.PtrVar { return vars.FromPtr(&m) }
func newFnVar(c eval.Callable) vars.PtrVar { return vars.FromPtr(&c) }
func newBindingVar(b bindingsMap) vars.PtrVar { return vars.FromPtr(&b) }