newedit: Implement histlist mode.

This addresses #778.
This commit is contained in:
Qi Xiao 2019-04-22 23:39:51 +01:00
parent a46d854ab5
commit 5291dae422
10 changed files with 258 additions and 13 deletions

View File

@ -22,7 +22,8 @@ insert:binding = (binding-map [
&Ctrl-U= $kill-sol~
&Ctrl-K= $kill-eol~
&Alt-,= $lastcmd:start~
&Alt-,= $lastcmd:start~
&Ctrl-R= $histlist:start~
&Ctrl-D= $commit-eof~
&Default= $insert:default-handler~

View File

@ -1,8 +1,10 @@
package newedit
import (
"fmt"
"os"
"github.com/elves/elvish/edit/history/histutil"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vars"
"github.com/elves/elvish/newedit/core"
@ -41,11 +43,18 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
}).
AddGoFns("<edit>", bufferBuiltins(ed.State()))
// Add the builtin hook of appending history in after-readline.
ed.AddAfterReadline(func(code string) {
st.AddCmd(code)
// TODO: Log errors
})
histFuser, err := histutil.NewFuser(st)
if err == nil {
// Add the builtin hook of appending history in after-readline.
ed.AddAfterReadline(func(code string) {
err := histFuser.AddCmd(code)
if err != nil {
fmt.Fprintln(out, "failed to add command to history")
}
})
} else {
fmt.Fprintln(out, "failed to initialize history facilities")
}
// Elvish hook APIs
var beforeReadline func()
@ -67,9 +76,13 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
// Listing modes.
lsMode, lsBinding, lsNs := initListing(ed)
ns.AddNs("listing", lsNs)
lastcmdNs := initLastcmd(ed, ev, st, lsMode, lsBinding)
ns.AddNs("lastcmd", lastcmdNs)
histlistNs := initHistlist(ed, ev, histFuser.AllCmds, lsMode, lsBinding)
ns.AddNs("histlist", histlistNs)
// Evaluate default bindings.
evalDefaultBinding(ev, ns)

View File

@ -0,0 +1,81 @@
package histlist
import (
"fmt"
"strings"
"github.com/elves/elvish/edit/ui"
"github.com/elves/elvish/newedit/listing"
"github.com/elves/elvish/newedit/types"
"github.com/elves/elvish/styled"
)
// Mode represents the histlist mode. It implements the types.Mode interface by
// embedding a *listing.Mode.
type Mode struct {
*listing.Mode
KeyHandler func(ui.Key) types.HandlerAction
}
// Start starts the histlist mode.
func (m *Mode) Start(cmds []string) {
m.Mode.Start(listing.StartConfig{
Name: "HISTLIST",
KeyHandler: m.KeyHandler,
ItemsGetter: func(p string) listing.Items {
return getEntries(cmds, p)
},
StartFilter: true,
})
}
// Given all commands, and a pattern, returning all matching entries.
func getEntries(cmds []string, p string) items {
// TODO: Show the real in-storage IDs of cmds, not their in-memory indicies.
var entries []entry
for i, line := range cmds {
if strings.Contains(line, p) {
entries = append(entries, entry{line, i})
}
}
return entries
}
// A slice of entries, implementing the listing.Items interface.
type items []entry
// An entry to show, which is just a line plus its index.
type entry struct {
content string
index int
}
func (it items) Len() int {
return len(it)
}
func (it items) Show(i int) styled.Text {
// TODO: The alignment of the index works up to 10000 entries.
return styled.Unstyled(fmt.Sprintf("%4d %s", it[i].index+1, it[i].content))
}
func (it items) Accept(i int, st *types.State) {
st.Mutex.Lock()
defer st.Mutex.Unlock()
raw := &st.Raw
if raw.Code == "" {
insertAtDot(raw, it[i].content)
} else {
// TODO: This works well when the cursor is at the end, but can be
// unexpected when the cursor is in the middle.
insertAtDot(raw, "\n"+it[i].content)
}
}
func insertAtDot(raw *types.RawState, text string) {
// NOTE: This is an duplicate with (*types.State).InsertAtDot, without any
// locks because we accept RawState.
raw.Code = raw.Code[:raw.Dot] + text + raw.Code[raw.Dot:]
raw.Dot += len(text)
}

View File

@ -0,0 +1,57 @@
package histlist
import (
"testing"
"github.com/elves/elvish/newedit/listing"
"github.com/elves/elvish/newedit/types"
"github.com/elves/elvish/styled"
"github.com/elves/elvish/tt"
)
var Args = tt.Args
var testCmds = []string{}
func TestGetEntries(t *testing.T) {
cmds := []string{
"put 1",
"echo 2",
"print 3",
"repr 4",
}
tt.Test(t, tt.Fn("getEntries", getEntries), tt.Table{
// Show all commands.
Args(cmds, "").Rets(listing.MatchItems(
styled.Unstyled(" 1 put 1"),
styled.Unstyled(" 2 echo 2"),
styled.Unstyled(" 3 print 3"),
styled.Unstyled(" 4 repr 4"),
)),
// Filter.
Args(cmds, "pr").Rets(listing.MatchItems(
styled.Unstyled(" 3 print 3"),
styled.Unstyled(" 4 repr 4"),
)),
})
}
func TestAccept(t *testing.T) {
cmds := []string{
"put 1",
"echo 2",
}
entries := getEntries(cmds, "")
st := types.State{}
entries.Accept(0, &st)
if st.Code() != "put 1" {
t.Errorf("Accept doesn't insert command")
}
entries.Accept(1, &st)
if st.Code() != "put 1\necho 2" {
t.Errorf("Accept doesn't insert command with newline")
}
}

30
newedit/histlist_api.go Normal file
View File

@ -0,0 +1,30 @@
package newedit
import (
"github.com/elves/elvish/eval"
"github.com/elves/elvish/newedit/histlist"
"github.com/elves/elvish/newedit/listing"
)
// Initializes states for the histlist mode and its API.
func initHistlist(ed editor, ev *eval.Evaler, getCmds func() ([]string, error), lsMode *listing.Mode, lsBinding *bindingMap) eval.Ns {
binding := emptyBindingMap
mode := histlist.Mode{
Mode: lsMode,
KeyHandler: keyHandlerFromBindings(ed, ev, &binding, lsBinding),
}
ns := eval.Ns{}.
AddGoFn("<edit:histlist>", "start", func() {
startHistlist(ed, getCmds, &mode)
})
return ns
}
func startHistlist(ed editor, getCmds func() ([]string, error), mode *histlist.Mode) {
cmds, err := getCmds()
if err != nil {
ed.Notify("db error: " + err.Error())
}
mode.Start(cmds)
ed.State().SetMode(mode)
}

View File

@ -0,0 +1,44 @@
package newedit
import (
"reflect"
"testing"
"github.com/elves/elvish/edit/ui"
"github.com/elves/elvish/edit/history/histutil"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/newedit/listing"
"github.com/elves/elvish/newedit/types"
)
func TestHistlist_Start(t *testing.T) {
ed := &fakeEditor{}
ev := eval.NewEvaler()
lsMode := listing.Mode{}
lsBinding := emptyBindingMap
// TODO: Move this into common setup.
histFuser, err := histutil.NewFuser(testStore)
if err != nil {
panic(err)
}
ns := initHistlist(ed, ev, histFuser.AllCmds, &lsMode, &lsBinding)
// Call <edit:histlist>:start.
fm := eval.NewTopFrame(ev, eval.NewInternalSource("[test]"), nil)
fm.Call(getFn(ns, "start"), eval.NoArgs, eval.NoOpts)
// Verify that the current mode supports listing.
lister, ok := ed.state.Mode().(types.Lister)
if !ok {
t.Errorf("Mode is not Lister after <edit:histlist>:start")
}
// Verify the actual listing.
buf := ui.Render(lister.List(10), 30)
wantBuf := ui.NewBufferBuilder(30).
WriteString(" 1 echo hello world", "7").Buffer()
if !reflect.DeepEqual(buf, wantBuf) {
t.Errorf("Rendered listing is %v, want %v", buf, wantBuf)
}
}

View File

@ -25,7 +25,7 @@ func TestInitLastCmd_Start(t *testing.T) {
// Verify that the current mode supports listing.
lister, ok := ed.state.Mode().(types.Lister)
if !ok {
t.Errorf("Mode is not Lister after <edit:listing>:start")
t.Errorf("Mode is not Lister after <edit:lastcmd>:start")
}
// Verify the listing.
buf := ui.Render(lister.List(10), 20)

View File

@ -38,6 +38,7 @@ type StartConfig struct {
ItemsGetter func(filter string) Items
StartFilter bool
AutoAccept bool
SelectLast bool
}
// Items is an interface for accessing items to show in the listing mode.
@ -62,7 +63,8 @@ func (m *Mode) Start(cfg StartConfig) {
*m = Mode{
StartConfig: cfg,
state: State{
filtering: cfg.StartFilter, itemsGetter: cfg.ItemsGetter},
itemsGetter: cfg.ItemsGetter, selectLast: cfg.SelectLast,
filtering: cfg.StartFilter},
}
m.state.refilter("")
}

View File

@ -53,6 +53,16 @@ func TestModeRenderFlag(t *testing.T) {
}
}
func TestStart_SelectLast(t *testing.T) {
m := Mode{}
m.Start(StartConfig{ItemsGetter: func(string) Items {
return fakeItems{10}
}, SelectLast: true})
if m.state.selected != 9 {
t.Errorf("SelectLast did not cause the last item to be selected")
}
}
func TestHandleEvent_CallsKeyHandler(t *testing.T) {
m := Mode{}
key := ui.K('a')

View File

@ -3,11 +3,13 @@ package listing
// State keeps the state of the listing mode.
type State struct {
itemsGetter func(string) Items
filtering bool
filter string
items Items
first int
selected int
selectLast bool
filtering bool
filter string
items Items
first int
selected int
}
func (st *State) refilter(f string) {
@ -17,6 +19,11 @@ func (st *State) refilter(f string) {
} else {
st.items = st.itemsGetter(f)
}
if st.selectLast {
st.selected = st.items.Len() - 1
} else {
st.selected = 0
}
}
// Up moves the selection up.