elvish/edit/call.go
2016-09-16 13:43:49 +02:00

151 lines
3.7 KiB
Go

package edit
import (
"bufio"
"errors"
"os"
"sync"
"github.com/elves/elvish/eval"
)
var (
DevNull *os.File
ClosedChan chan eval.Value
NullClosedInput *eval.Port
)
func init() {
var err error
DevNull, err = os.Open("/dev/null")
if err != nil {
os.Stderr.WriteString("cannot open /dev/null, shell might not function normally")
}
ClosedChan = make(chan eval.Value)
close(ClosedChan)
NullClosedInput = &eval.Port{File: DevNull, Chan: ClosedChan}
}
// CallFn calls an Fn, displaying its outputs and possible errors as editor
// notifications. It is the preferred way to call a Fn while the editor is
// active.
func (ed *Editor) CallFn(fn eval.FnValue, args ...eval.Value) {
if b, ok := fn.(*BuiltinFn); ok {
// Builtin function: quick path.
b.impl(ed)
return
}
rout, chanOut, ports, err := makePorts()
if err != nil {
return
}
// Goroutines to collect output.
var wg sync.WaitGroup
wg.Add(2)
go func() {
rd := bufio.NewReader(rout)
for {
line, err := rd.ReadString('\n')
if err != nil {
break
}
ed.Notify("[bytes output] %s", line[:len(line)-1])
}
rout.Close()
wg.Done()
}()
go func() {
for v := range chanOut {
ed.Notify("[value output] %s", v.Repr(eval.NoPretty))
}
wg.Done()
}()
// XXX There is no source to pass to NewTopEvalCtx.
ec := eval.NewTopEvalCtx(ed.evaler, "[editor]", "", ports)
ex := ec.PCall(fn, args, eval.NoOpts)
if ex != nil {
ed.Notify("function error: %s", ex.Error())
}
eval.ClosePorts(ports)
wg.Wait()
ed.refresh(true, true)
}
// makePorts connects stdin to /dev/null and a closed channel, identifies
// stdout and stderr and connects them to a pipe and channel. It returns the
// other end of stdout and the resulting []*eval.Port. The caller is
// responsible for closing the returned file and calling eval.ClosePorts on the
// ports.
func makePorts() (*os.File, chan eval.Value, []*eval.Port, error) {
// Output
rout, out, err := os.Pipe()
if err != nil {
Logger.Println(err)
return nil, nil, nil, err
}
chanOut := make(chan eval.Value)
return rout, chanOut, []*eval.Port{
NullClosedInput,
{File: out, CloseFile: true, Chan: chanOut, CloseChan: true},
{File: out, Chan: chanOut},
}, nil
}
// callFnAsPrompt calls a Fn with closed input, captures its output and convert
// the output to a slice of *styled's.
func callFnForPrompt(ed *Editor, fn eval.Fn) []*styled {
ports := []*eval.Port{NullClosedInput, &eval.Port{File: os.Stdout}, &eval.Port{File: os.Stderr}}
// XXX There is no source to pass to NewTopEvalCtx.
ec := eval.NewTopEvalCtx(ed.evaler, "[editor prompt]", "", ports)
values, err := ec.PCaptureOutput(fn, nil, eval.NoOpts)
if err != nil {
ed.Notify("prompt function error: %v", err)
return nil
}
var ss []*styled
for _, v := range values {
if s, ok := v.(*styled); ok {
ss = append(ss, s)
} else {
ss = append(ss, &styled{eval.ToString(v), ""})
}
}
return ss
}
func callFnForCandidates(fn eval.FnValue, ev *eval.Evaler, args []string) ([]*candidate, error) {
ports := []*eval.Port{NullClosedInput, &eval.Port{File: os.Stdout}, &eval.Port{File: os.Stderr}}
argValues := make([]eval.Value, len(args))
for i, arg := range args {
argValues[i] = eval.String(arg)
}
// XXX There is no source to pass to NewTopEvalCtx.
ec := eval.NewTopEvalCtx(ev, "[editor completer]", "", ports)
values, err := ec.PCaptureOutput(fn, argValues, eval.NoOpts)
if err != nil {
return nil, errors.New("completer error: " + err.Error())
}
cands := make([]*candidate, len(values))
for i, v := range values {
switch v := v.(type) {
case eval.String:
cands[i] = &candidate{text: string(v)}
case *candidate:
cands[i] = v
default:
return nil, errors.New("completer must output string or candidate")
}
}
return cands, nil
}