elvish/cliedit/key_binding.go
2019-08-25 18:13:25 +01:00

121 lines
2.7 KiB
Go

package cliedit
import (
"bufio"
"io"
"os"
"sync"
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/cli/term"
"github.com/elves/elvish/edit/eddefs"
"github.com/elves/elvish/edit/ui"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
)
// TODO(xiaq): Move the implementation into this package.
// A specialized map type for key bindings.
type bindingMap = eddefs.BindingMap
// An empty binding map. It is useful for building binding maps.
var emptyBindingMap = eddefs.EmptyBindingMap
type mapBinding struct {
nt notifier
ev *eval.Evaler
maps []*bindingMap
}
func newMapBinding(nt notifier, ev *eval.Evaler, maps ...*bindingMap) clitypes.Handler {
return mapBinding{nt, ev, maps}
}
func (b mapBinding) Handle(e term.Event) bool {
k, ok := e.(term.KeyEvent)
if !ok {
return false
}
f := indexLayeredBindings(ui.Key(k), b.maps...)
if f == nil {
return false
}
callBinding(b.nt, b.ev, f)
return true
}
// Indexes a series of layered bindings. Returns nil if none of the bindings
// have the required key or a default.
func indexLayeredBindings(k ui.Key, bindings ...*bindingMap) eval.Callable {
for _, binding := range bindings {
if binding.HasKey(k) {
return binding.GetKey(k)
}
}
for _, binding := range bindings {
if binding.HasKey(ui.Default) {
return binding.GetKey(ui.Default)
}
}
return nil
}
var bindingSource = eval.NewInternalSource("[editor binding]")
func callBinding(nt notifier, ev *eval.Evaler, f eval.Callable) {
// TODO(xiaq): Use CallWithOutputCallback when it supports redirecting the
// stderr port.
notifyPort, cleanup := makeNotifyPort(nt.Notify)
defer cleanup()
ports := []*eval.Port{eval.DevNullClosedChan, notifyPort, notifyPort}
frame := eval.NewTopFrame(ev, bindingSource, ports)
err := frame.Call(f, nil, eval.NoOpts)
if err != nil {
// TODO(xiaq): Make the stack trace available.
nt.Notify("[binding error] " + err.Error())
}
}
func makeNotifyPort(notify func(string)) (*eval.Port, func()) {
ch := make(chan interface{})
r, w, err := os.Pipe()
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
// Relay value outputs
for v := range ch {
notify("[value out] " + vals.Repr(v, vals.NoPretty))
}
wg.Done()
}()
go func() {
// Relay byte outputs
reader := bufio.NewReader(r)
for {
line, err := reader.ReadString('\n')
if err != nil {
if line != "" {
notify("[bytes out] " + line)
}
if err != io.EOF {
notify("[bytes error] " + err.Error())
}
break
}
notify("[bytes out] " + line[:len(line)-1])
}
wg.Done()
}()
port := &eval.Port{Chan: ch, File: w, CloseChan: true, CloseFile: true}
cleanup := func() {
port.Close()
wg.Wait()
}
return port, cleanup
}