github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/edit/config_api.go (about)

     1  package edit
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/markusbkk/elvish/pkg/cli"
     9  	"github.com/markusbkk/elvish/pkg/cli/histutil"
    10  	"github.com/markusbkk/elvish/pkg/diag"
    11  	"github.com/markusbkk/elvish/pkg/eval"
    12  	"github.com/markusbkk/elvish/pkg/eval/vals"
    13  	"github.com/markusbkk/elvish/pkg/eval/vars"
    14  	"github.com/markusbkk/elvish/pkg/store/storedefs"
    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.AddVar("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.AddVar("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.AddVar("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.AddVar("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(storedefs.Cmd{Text: code, Seq: -1})
    84  		}
    85  		// TODO(xiaq): Handle the error.
    86  	})
    87  }
    88  
    89  //elvdoc:var global-binding
    90  //
    91  // Global keybindings, consulted for keys not handled by mode-specific bindings.
    92  //
    93  // See [Keybindings](#keybindings).
    94  
    95  func initGlobalBindings(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) {
    96  	bindingVar := newBindingVar(emptyBindingsMap)
    97  	appSpec.GlobalBindings = newMapBindings(nt, ev, bindingVar)
    98  	nb.AddVar("global-binding", bindingVar)
    99  }
   100  
   101  func callHooks(ev *eval.Evaler, name string, hook vals.List, args ...interface{}) {
   102  	if hook.Len() == 0 {
   103  		return
   104  	}
   105  
   106  	ports, cleanup := eval.PortsFromStdFiles(ev.ValuePrefix())
   107  	evalCfg := eval.EvalCfg{Ports: ports[:]}
   108  	defer cleanup()
   109  
   110  	i := -1
   111  	for it := hook.Iterator(); it.HasElem(); it.Next() {
   112  		i++
   113  		name := fmt.Sprintf("%s[%d]", name, i)
   114  		fn, ok := it.Elem().(eval.Callable)
   115  		if !ok {
   116  			// TODO(xiaq): This is not testable as it depends on stderr.
   117  			// Make it testable.
   118  			diag.Complainf(os.Stderr, "%s not function", name)
   119  			continue
   120  		}
   121  
   122  		err := ev.Call(fn, eval.CallCfg{Args: args, From: name}, evalCfg)
   123  		if err != nil {
   124  			diag.ShowError(os.Stderr, err)
   125  		}
   126  	}
   127  }
   128  
   129  func callFilters(ev *eval.Evaler, name string, filters vals.List, args ...interface{}) bool {
   130  	if filters.Len() == 0 {
   131  		return true
   132  	}
   133  
   134  	i := -1
   135  	for it := filters.Iterator(); it.HasElem(); it.Next() {
   136  		i++
   137  		name := fmt.Sprintf("%s[%d]", name, i)
   138  		fn, ok := it.Elem().(eval.Callable)
   139  		if !ok {
   140  			// TODO(xiaq): This is not testable as it depends on stderr.
   141  			// Make it testable.
   142  			diag.Complainf(os.Stderr, "%s not function", name)
   143  			continue
   144  		}
   145  
   146  		port1, collect, err := eval.CapturePort()
   147  		if err != nil {
   148  			diag.Complainf(os.Stderr, "cannot create pipe to run filter")
   149  			return true
   150  		}
   151  		err = ev.Call(fn, eval.CallCfg{Args: args, From: name},
   152  			// TODO: Supply the Chan component of port 2.
   153  			eval.EvalCfg{Ports: []*eval.Port{nil, port1, {File: os.Stderr}}})
   154  		out := collect()
   155  
   156  		if err != nil {
   157  			diag.Complainf(os.Stderr, "%s return error", name)
   158  			continue
   159  		}
   160  		if len(out) != 1 {
   161  			diag.Complainf(os.Stderr, "filter %s should only return $true or $false", name)
   162  			continue
   163  		}
   164  		p, ok := out[0].(bool)
   165  		if !ok {
   166  			diag.Complainf(os.Stderr, "filter %s should return bool", name)
   167  			continue
   168  		}
   169  		if !p {
   170  			return false
   171  		}
   172  	}
   173  	return true
   174  }
   175  
   176  func newIntVar(i int) vars.PtrVar             { return vars.FromPtr(&i) }
   177  func newFloatVar(f float64) vars.PtrVar       { return vars.FromPtr(&f) }
   178  func newBoolVar(b bool) vars.PtrVar           { return vars.FromPtr(&b) }
   179  func newListVar(l vals.List) vars.PtrVar      { return vars.FromPtr(&l) }
   180  func newMapVar(m vals.Map) vars.PtrVar        { return vars.FromPtr(&m) }
   181  func newFnVar(c eval.Callable) vars.PtrVar    { return vars.FromPtr(&c) }
   182  func newBindingVar(b bindingsMap) vars.PtrVar { return vars.FromPtr(&b) }