mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-15 20:17:50 +08:00
151 lines
3.7 KiB
Go
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
|
|
}
|