cliedit: Consolidate files and add tests.

This commit is contained in:
Qi Xiao 2019-08-26 18:19:34 +01:00
parent 85a75c7722
commit 795281efdf
7 changed files with 227 additions and 249 deletions

116
cliedit/api.go Normal file
View File

@ -0,0 +1,116 @@
package cliedit
import (
"fmt"
"os"
"github.com/elves/elvish/cli"
"github.com/elves/elvish/diag"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
"github.com/elves/elvish/eval/vars"
"github.com/xiaq/persistent/hashmap"
)
func initAPI(app *cli.App, ev *eval.Evaler, ns eval.Ns) {
initMaxHeight(app, ns)
initBeforeReadline(app, ev, ns)
initAfterReadline(app, ev, ns)
initInsert(app, ev, ns)
}
func initMaxHeight(app *cli.App, ns eval.Ns) {
maxHeight := -1
maxHeightVar := vars.FromPtr(&maxHeight)
app.Config.MaxHeight = func() int { return maxHeightVar.Get().(int) }
ns.Add("max-height", maxHeightVar)
}
func initBeforeReadline(app *cli.App, ev *eval.Evaler, ns eval.Ns) {
hook := vals.EmptyList
hookVar := vars.FromPtr(&hook)
ns["before-readline"] = hookVar
app.Config.BeforeReadline = func() {
i := -1
hook := hookVar.Get().(vals.List)
for it := hook.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("$before-readline[%d]", i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf("%s not function", name)
continue
}
// TODO(xiaq): This should use stdPorts, but stdPorts is currently
// unexported from eval.
ports := []*eval.Port{
{File: os.Stdin}, {File: os.Stdout}, {File: os.Stderr}}
fm := eval.NewTopFrame(ev, eval.NewInternalSource(name), ports)
fm.Call(fn, eval.NoArgs, eval.NoOpts)
}
}
}
func initAfterReadline(app *cli.App, ev *eval.Evaler, ns eval.Ns) {
hook := vals.EmptyList
hookVar := vars.FromPtr(&hook)
ns["after-readline"] = hookVar
app.Config.AfterReadline = func(code string) {
i := -1
hook := hookVar.Get().(vals.List)
for it := hook.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("$after-readline[%d]", i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf("%s not function", name)
continue
}
// TODO(xiaq): This should use stdPorts, but stdPorts is currently
// unexported from eval.
ports := []*eval.Port{
{File: os.Stdin}, {File: os.Stdout}, {File: os.Stderr}}
fm := eval.NewTopFrame(ev, eval.NewInternalSource(name), ports)
fm.Call(fn, []interface{}{code}, eval.NoOpts)
}
}
}
func initInsert(app *cli.App, ev *eval.Evaler, ns eval.Ns) {
abbr := vals.EmptyMap
abbrVar := vars.FromPtr(&abbr)
app.CodeArea.Abbreviations = makeMapIterator(abbrVar)
// TODO(xiaq): Synchronize properly.
binding := emptyBindingMap
bindingVar := vars.FromPtr(&binding)
app.CodeArea.OverlayHandler = newMapBinding(app, ev, &binding)
quotePaste := false
quotePasteVar := vars.FromPtr(&quotePaste)
app.CodeArea.QuotePaste = func() bool { return quotePasteVar.Get().(bool) }
ns.AddNs("insert", eval.Ns{
"abbr": abbrVar,
"binding": bindingVar,
"quote-paste": quotePasteVar,
})
}
func makeMapIterator(mv vars.Var) func(func(a, b string)) {
return func(f func(a, b string)) {
for it := mv.Get().(hashmap.Map).Iterator(); it.HasElem(); it.Next() {
k, v := it.Elem()
ks, kok := k.(string)
vs, vok := v.(string)
if !kok || !vok {
continue
}
f(ks, vs)
}
}
}

110
cliedit/api_test.go Normal file
View File

@ -0,0 +1,110 @@
package cliedit
import (
"testing"
"github.com/elves/elvish/cli"
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/cli/term"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
"github.com/elves/elvish/eval/vars"
)
func setupAPI() (*cli.App, *eval.Evaler, eval.Ns) {
app := cli.NewApp(cli.NewStdTTY())
ev := eval.NewEvaler()
ns := eval.Ns{}
initAPI(app, ev, ns)
return app, ev, ns
}
func TestInitAPI_BeforeReadline(t *testing.T) {
app, _, ns := setupAPI()
var called int
ns["before-readline"].Set(vals.MakeList(eval.NewGoFn("[test]", func() {
called++
})))
app.Config.BeforeReadline()
if called != 1 {
t.Errorf("before-readline called %d times, want once", called)
}
}
func TestInitAPI_AfterReadline(t *testing.T) {
app, _, ns := setupAPI()
var called int
var calledWith string
ns["after-readline"].Set(vals.MakeList(eval.NewGoFn("[test]", func(s string) {
called++
calledWith = s
})))
app.Config.AfterReadline("code")
if called != 1 {
t.Errorf("after-readline called %d times, want once", called)
}
if calledWith != "code" {
t.Errorf("after-readline called with %q, want %q", calledWith, "code")
}
}
func TestInitAPI_Insert_Abbr(t *testing.T) {
app, _, ns := setupAPI()
m := vals.MakeMap("xx", "xx full", "yy", "yy full")
getNs(ns, "insert")["abbr"].Set(m)
collected := vals.EmptyMap
app.CodeArea.Abbreviations(func(a, f string) {
collected = collected.Assoc(a, f)
})
if !vals.Equal(m, collected) {
t.Errorf("Callback collected %v, var set %v", collected, m)
}
}
func TestInitAPI_Insert_Binding(t *testing.T) {
app, _, ns := setupAPI()
testKeyBinding(t, getNs(ns, "insert")["binding"], app.CodeArea.OverlayHandler)
}
func TestInitAPI_Insert_QuotePaste(t *testing.T) {
app, _, ns := setupAPI()
for _, quote := range []bool{false, true} {
getNs(ns, "insert")["quote-paste"].Set(quote)
if got := app.CodeArea.QuotePaste(); got != quote {
t.Errorf("quote paste = %v, want %v", got, quote)
}
}
}
func testKeyBinding(t *testing.T, v vars.Var, h clitypes.Handler) {
t.Helper()
var called int
binding, err := emptyBindingMap.Assoc(
"a", eval.NewGoFn("[binding]", func() { called++ }))
if err != nil {
panic(err)
}
v.Set(binding)
handled := h.Handle(term.K('a'))
if !handled {
t.Errorf("handled = false, want true")
}
if called != 1 {
t.Errorf("handler called %d times, want once", called)
}
}
func getNs(ns eval.Ns, name string) eval.Ns {
return ns[name+eval.NsSuffix].Get().(eval.Ns)
}
func getFn(ns eval.Ns, name string) eval.Callable {
return ns[name+eval.FnSuffix].Get().(eval.Callable)
}

View File

@ -7,7 +7,6 @@ import (
"github.com/elves/elvish/cli"
"github.com/elves/elvish/cli/histutil"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vars"
"github.com/elves/elvish/parse"
"github.com/elves/elvish/store/storedefs"
)
@ -52,13 +51,9 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
ns := eval.NewNs()
app := cli.NewApp(cli.NewTTY(in, out))
initAPI(app, ev, ns)
app.Config.Highlighter = makeHighlighter(ev)
maxHeight := -1
maxHeightVar := vars.FromPtr(&maxHeight)
app.Config.MaxHeight = func() int { return maxHeightVar.Get().(int) }
ns.Add("max-height", maxHeightVar)
// TODO: BindingMap should pass event context to event handlers
ns.AddGoFns("<edit>", map[string]interface{}{
"binding-map": makeBindingMap,
@ -67,22 +62,10 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
// "reset-mode": cli.ResetMode,
}).AddGoFns("<edit>", bufferBuiltins(app))
// Elvish hook APIs
var beforeReadline func()
ns["before-readline"], beforeReadline = initBeforeReadline(ev)
var afterReadline func(string)
ns["after-readline"], afterReadline = initAfterReadline(ev)
app.Config.BeforeReadline = beforeReadline
app.Config.AfterReadline = afterReadline
// Prompts
app.Config.Prompt = makePrompt(app, ev, ns, defaultPrompt, "prompt")
app.Config.RPrompt = makePrompt(app, ev, ns, defaultRPrompt, "rprompt")
// Insert mode
insertNs := initInsert(ev, app)
ns.AddNs("insert", insertNs)
// Listing modes.
lsBinding, lsNs := initListing()
ns.AddNs("listing", lsNs)

View File

@ -1,59 +0,0 @@
package cliedit
import (
"fmt"
"os"
"github.com/elves/elvish/diag"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
"github.com/elves/elvish/eval/vars"
)
func initBeforeReadline(ev *eval.Evaler) (vars.Var, func()) {
hook := vals.EmptyList
return vars.FromPtr(&hook), func() {
i := -1
for it := hook.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("$before-readline[%d]", i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf("%s not function", name)
continue
}
// TODO(xiaq): This should use stdPorts, but stdPorts is currently
// unexported from eval.
ports := []*eval.Port{
{File: os.Stdin}, {File: os.Stdout}, {File: os.Stderr}}
fm := eval.NewTopFrame(ev, eval.NewInternalSource(name), ports)
fm.Call(fn, eval.NoArgs, eval.NoOpts)
}
}
}
func initAfterReadline(ev *eval.Evaler) (vars.Var, func(string)) {
hook := vals.EmptyList
return vars.FromPtr(&hook), func(code string) {
i := -1
for it := hook.Iterator(); it.HasElem(); it.Next() {
i++
name := fmt.Sprintf("$after-readline[%d]", i)
fn, ok := it.Elem().(eval.Callable)
if !ok {
// TODO(xiaq): This is not testable as it depends on stderr.
// Make it testable.
diag.Complainf("%s not function", name)
continue
}
// TODO(xiaq): This should use stdPorts, but stdPorts is currently
// unexported from eval.
ports := []*eval.Port{
{File: os.Stdin}, {File: os.Stdout}, {File: os.Stderr}}
fm := eval.NewTopFrame(ev, eval.NewInternalSource(name), ports)
fm.Call(fn, []interface{}{code}, eval.NoOpts)
}
}
}

View File

@ -1,39 +0,0 @@
package cliedit
import (
"testing"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
)
func TestInitBeforeReadline(t *testing.T) {
variable, cb := initBeforeReadline(eval.NewEvaler())
called := 0
variable.Set(vals.EmptyList.Cons(eval.NewGoFn("[test]", func() {
called++
})))
cb()
if called != 1 {
t.Errorf("Called %d times, want once", called)
}
// TODO: Test input and output
}
func TestInitAfterReadline(t *testing.T) {
variable, cb := initAfterReadline(eval.NewEvaler())
called := 0
calledWith := ""
variable.Set(vals.EmptyList.Cons(eval.NewGoFn("[test]", func(s string) {
called++
calledWith = s
})))
cb("code")
if called != 1 {
t.Errorf("Called %d times, want once", called)
}
if calledWith != "code" {
t.Errorf("Called with %q, want %q", calledWith, "code")
}
// TODO: Test input and output
}

View File

@ -1,41 +0,0 @@
package cliedit
import (
"github.com/elves/elvish/cli"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
"github.com/elves/elvish/eval/vars"
"github.com/xiaq/persistent/hashmap"
)
func initInsert(ev *eval.Evaler, app *cli.App) eval.Ns {
abbr := vals.EmptyMap
app.CodeArea.Abbreviations = makeMapIterator(&abbr)
binding := emptyBindingMap
app.CodeArea.OverlayHandler = newMapBinding(app, ev, &binding)
quotePaste := false
quotePasteVar := vars.FromPtr(&quotePaste)
app.CodeArea.QuotePaste = func() bool { return quotePasteVar.Get().(bool) }
return eval.Ns{
"abbr": vars.FromPtr(&abbr),
"binding": vars.FromPtr(&binding),
"quote-paste": quotePasteVar,
}
}
func makeMapIterator(m *hashmap.Map) func(func(a, b string)) {
return func(f func(a, b string)) {
for it := (*m).Iterator(); it.HasElem(); it.Next() {
k, v := it.Elem()
ks, kok := k.(string)
vs, vok := v.(string)
if !kok || !vok {
continue
}
f(ks, vs)
}
}
}

View File

@ -1,92 +0,0 @@
package cliedit
import (
"github.com/elves/elvish/eval"
)
var abbrData = [][2]string{{"xx", "xx full"}, {"yy", "yy full"}}
/*
func TestInitInsert_Abbr(t *testing.T) {
m, ns := initInsert(&fakeApp{}, eval.NewEvaler())
abbrValue := vals.EmptyMap
for _, pair := range abbrData {
abbrValue = abbrValue.Assoc(pair[0], pair[1])
}
ns["abbr"].Set(abbrValue)
var cbData [][2]string
m.AbbrIterate(func(a, f string) {
cbData = append(cbData, [2]string{a, f})
})
if !reflect.DeepEqual(cbData, abbrData) {
t.Errorf("Callback called with %v, want %v", cbData, abbrData)
}
}
func TestInitInsert_Binding(t *testing.T) {
m, ns := initInsert(&fakeApp{}, eval.NewEvaler())
called := 0
binding, err := emptyBindingMap.Assoc("a",
eval.NewGoFn("test binding", func() { called++ }))
if err != nil {
panic(err)
}
ns["binding"].Set(binding)
m.HandleEvent(tty.KeyEvent{Rune: 'a'}, &clitypes.State{})
if called != 1 {
t.Errorf("Handler called %d times, want once", called)
}
}
func TestInitInsert_QuotePaste(t *testing.T) {
m, ns := initInsert(&fakeApp{}, eval.NewEvaler())
ns["quote-paste"].Set(true)
if !m.Config.QuotePaste() {
t.Errorf("QuotePaste not set via namespae")
}
}
func TestInitInsert_Start(t *testing.T) {
ed := &fakeApp{}
ev := eval.NewEvaler()
m, ns := initInsert(ed, ev)
fm := eval.NewTopFrame(ev, eval.NewInternalSource("[test]"), nil)
fm.Call(getFn(ns, "start"), nil, eval.NoOpts)
if ed.state.Mode() != m {
t.Errorf("state is not insert mode after calling start")
}
}
func TestInitInsert_DefaultHandler(t *testing.T) {
ed := &fakeApp{}
ev := eval.NewEvaler()
_, ns := initInsert(ed, ev)
// Pretend that we are executing a binding for "a".
ed.state.SetBindingKey(ui.Key{Rune: 'a'})
// Call <edit:insert>:default-binding.
fm := eval.NewTopFrame(ev, eval.NewInternalSource("[test]"), nil)
fm.Call(getFn(ns, "default-handler"), nil, eval.NoOpts)
// Verify that the default handler has executed, inserting "a".
if ed.state.Raw.Code != "a" {
t.Errorf("state.Raw.Code = %q, want %q", ed.state.Raw.Code, "a")
}
}
*/
func getFn(ns eval.Ns, name string) eval.Callable {
return ns[name+eval.FnSuffix].Get().(eval.Callable)
}