github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/binding.go (about)

     1  package edit
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  
    10  	"github.com/elves/elvish/eval"
    11  	"github.com/elves/elvish/parse"
    12  )
    13  
    14  // Errors thrown to Evaler.
    15  var (
    16  	ErrTakeNoArg       = errors.New("editor builtins take no arguments")
    17  	ErrEditorInactive  = errors.New("editor inactive")
    18  	ErrKeyMustBeString = errors.New("key must be string")
    19  )
    20  
    21  // BindingTable adapts a binding table to eval.IndexSetter, so that it can be
    22  // manipulated in elvish script.
    23  type BindingTable struct {
    24  	inner map[Key]BoundFunc
    25  }
    26  
    27  func (BindingTable) Kind() string {
    28  	return "map"
    29  }
    30  
    31  func (bt BindingTable) Repr(indent int) string {
    32  	var builder eval.MapReprBuilder
    33  	builder.Indent = indent
    34  	for k, v := range bt.inner {
    35  		builder.WritePair(parse.Quote(k.String()), v.Repr(eval.IncIndent(indent, 1)))
    36  	}
    37  	return builder.String()
    38  }
    39  
    40  func (bt BindingTable) IndexOne(idx eval.Value) eval.Value {
    41  	key := keyIndex(idx)
    42  	switch f := bt.inner[key].(type) {
    43  	case Builtin:
    44  		return eval.String(f.name)
    45  	case FnAsBoundFunc:
    46  		return f.Fn
    47  	}
    48  	throw(errors.New("bug"))
    49  	panic("unreachable")
    50  }
    51  
    52  func (bt BindingTable) IndexSet(idx, v eval.Value) {
    53  	key := keyIndex(idx)
    54  
    55  	var f BoundFunc
    56  	switch v := v.(type) {
    57  	case eval.String:
    58  		builtin, ok := builtinMap[string(v)]
    59  		if !ok {
    60  			throw(fmt.Errorf("no builtin named %s", v.Repr(eval.NoPretty)))
    61  		}
    62  		f = builtin
    63  	case eval.FnValue:
    64  		f = FnAsBoundFunc{v}
    65  	default:
    66  		throw(fmt.Errorf("bad function type %s", v.Kind()))
    67  	}
    68  
    69  	bt.inner[key] = f
    70  }
    71  
    72  func keyIndex(idx eval.Value) Key {
    73  	skey, ok := idx.(eval.String)
    74  	if !ok {
    75  		throw(ErrKeyMustBeString)
    76  	}
    77  	key, err := parseKey(string(skey))
    78  	if err != nil {
    79  		throw(err)
    80  	}
    81  	return key
    82  }
    83  
    84  // BuiltinAsFnValue adapts a Builtin to satisfy eval.FnValue, so that it can be
    85  // called from elvish script.
    86  type BuiltinAsFnValue struct {
    87  	b  Builtin
    88  	ed *Editor
    89  }
    90  
    91  func (*BuiltinAsFnValue) Kind() string {
    92  	return "fn"
    93  }
    94  
    95  func (eb *BuiltinAsFnValue) Repr(int) string {
    96  	return "<editor builtin " + eb.b.name + ">"
    97  }
    98  
    99  func (eb *BuiltinAsFnValue) Call(ec *eval.EvalCtx, args []eval.Value) {
   100  	if len(args) > 0 {
   101  		throw(ErrTakeNoArg)
   102  	}
   103  	if !eb.ed.active {
   104  		throw(ErrEditorInactive)
   105  	}
   106  	eb.b.impl(eb.ed)
   107  }
   108  
   109  // BoundFunc is a function bound to a key. It is either a Builtin or an
   110  // FnAsBoundFunc.
   111  type BoundFunc interface {
   112  	eval.Reprer
   113  	Call(ed *Editor)
   114  }
   115  
   116  func (b Builtin) Repr(int) string {
   117  	return b.name
   118  }
   119  
   120  func (b Builtin) Call(ed *Editor) {
   121  	b.impl(ed)
   122  }
   123  
   124  // FnAsBoundFunc adapts eval.Fn to BoundFunc, so that functions in elvish
   125  // script can be bound to keys.
   126  type FnAsBoundFunc struct {
   127  	Fn eval.FnValue
   128  }
   129  
   130  func (c FnAsBoundFunc) Repr(indent int) string {
   131  	return c.Fn.Repr(indent)
   132  }
   133  
   134  func (c FnAsBoundFunc) Call(ed *Editor) {
   135  	rout, chanOut, ports, err := makePorts()
   136  	if err != nil {
   137  		return
   138  	}
   139  
   140  	// Goroutines to collect output.
   141  	var wg sync.WaitGroup
   142  	wg.Add(2)
   143  	go func() {
   144  		rd := bufio.NewReader(rout)
   145  		for {
   146  			line, err := rd.ReadString('\n')
   147  			if err != nil {
   148  				break
   149  			}
   150  			// XXX notify is not concurrency-safe.
   151  			ed.notify("[bound fn bytes] %s", line[:len(line)-1])
   152  		}
   153  		rout.Close()
   154  		wg.Done()
   155  	}()
   156  	go func() {
   157  		for v := range chanOut {
   158  			ed.notify("[bound fn value] %s", v.Repr(eval.NoPretty))
   159  		}
   160  		wg.Done()
   161  	}()
   162  
   163  	// XXX There is no source to pass to NewTopEvalCtx.
   164  	ec := eval.NewTopEvalCtx(ed.evaler, "[editor]", "", ports)
   165  	ex := ec.PCall(c.Fn, []eval.Value{})
   166  	if ex != nil {
   167  		ed.notify("function error: %s", ex.Error())
   168  	}
   169  
   170  	eval.ClosePorts(ports)
   171  	wg.Wait()
   172  	ed.refresh(true, true)
   173  }
   174  
   175  // makePorts connects stdin to /dev/null and a closed channel, identifies
   176  // stdout and stderr and connects them to a pipe and channel. It returns the
   177  // other end of stdout and the resulting []*eval.Port. The caller is
   178  // responsible for closing the returned file and calling eval.ClosePorts on the
   179  // ports.
   180  func makePorts() (*os.File, chan eval.Value, []*eval.Port, error) {
   181  	in, err := makeClosedStdin()
   182  	if err != nil {
   183  		return nil, nil, nil, err
   184  	}
   185  
   186  	// Output
   187  	rout, out, err := os.Pipe()
   188  	if err != nil {
   189  		Logger.Println(err)
   190  		return nil, nil, nil, err
   191  	}
   192  	chanOut := make(chan eval.Value)
   193  
   194  	return rout, chanOut, []*eval.Port{
   195  		in,
   196  		{File: out, CloseFile: true, Chan: chanOut, CloseChan: true},
   197  		{File: out, Chan: chanOut},
   198  	}, nil
   199  }
   200  
   201  func makeClosedStdin() (*eval.Port, error) {
   202  	// Input
   203  	devnull, err := os.Open("/dev/null")
   204  	if err != nil {
   205  		Logger.Println(err)
   206  		return nil, err
   207  	}
   208  	in := make(chan eval.Value)
   209  	close(in)
   210  	return &eval.Port{File: devnull, CloseFile: true, Chan: in}, nil
   211  }