newedit: Support accepting in listing mode. Add tests.

This commit is contained in:
Qi Xiao 2019-03-11 12:25:36 +00:00
parent acb95dac00
commit 06fae6494a
11 changed files with 163 additions and 30 deletions

View File

@ -34,6 +34,8 @@ listing:binding = (binding-map [
&Down= $listing:down~
&Tab= $listing:down-cycle~
&Shift-Tab= $listing:up-cycle~
&Enter= $listing:accept-close~
&Alt-Enter= $listing:accept~
])
`

View File

@ -54,7 +54,7 @@ func NewEditor(in, out *os.File, ev *eval.Evaler) *Editor {
ns.AddNs("insert", insertNs)
// Listing modes.
lsMode, lsBinding, lsNs := initListing()
lsMode, lsBinding, lsNs := initListing(ed)
ns.AddNs("listing", lsNs)
lastcmdNs := initLastcmd(ed, ev, lsMode, lsBinding)
ns.AddNs("lastcmd", lastcmdNs)

View File

@ -113,7 +113,7 @@ func (m *Mode) handlePasteEnd(st *types.State) {
if m.paste == quotePaste {
text = parse.Quote(text)
}
insert(st, text)
st.InsertAtDot(text)
m.pastes = nil
m.paste = noPaste
}
@ -165,11 +165,3 @@ func (m *Mode) handleKey(k ui.Key, st *types.State) types.HandlerAction {
}
return action
}
func insert(st *types.State, text string) {
st.Mutex.Lock()
defer st.Mutex.Unlock()
raw := &st.Raw
raw.Code = raw.Code[:raw.Dot] + text + raw.Code[raw.Dot:]
raw.Dot += len(text)
}

View File

@ -94,3 +94,7 @@ func (it items) Show(i int) styled.Text {
}
return styled.Unstyled(fmt.Sprintf("%3s %s", index, entry.content))
}
func (it items) Accept(i int, st *types.State) {
st.InsertAtDot(it.entries[i].content)
}

View File

@ -35,7 +35,6 @@ type StartConfig struct {
KeyHandler func(ui.Key) types.HandlerAction
ItemsGetter func(filter string) Items
// TODO(xiaq): Support the following config options.
// AcceptItem func(i int)
// AutoAccept bool
// StartFiltering bool
}
@ -44,6 +43,7 @@ type StartConfig struct {
type Items interface {
Len() int
Show(int) styled.Text
Accept(int, *types.State)
}
// SliceItems returns an Items consisting of the given texts.
@ -51,13 +51,19 @@ func SliceItems(texts ...styled.Text) Items { return sliceItems{texts} }
type sliceItems struct{ texts []styled.Text }
func (it sliceItems) Len() int { return len(it.texts) }
func (it sliceItems) Show(i int) styled.Text { return it.texts[i] }
func (it sliceItems) Len() int { return len(it.texts) }
func (it sliceItems) Show(i int) styled.Text { return it.texts[i] }
func (it sliceItems) Accept(int, *types.State) {}
// Start starts the listing mode, using the given config and resetting all
// states.
func (m *Mode) Start(cfg StartConfig) {
*m = Mode{StartConfig: cfg}
if cfg.ItemsGetter != nil {
m.state.items = cfg.ItemsGetter("")
} else {
m.state.items = sliceItems{}
}
}
// ModeLine returns a modeline showing the specified name of the mode.
@ -110,6 +116,22 @@ func (m *Mode) MutateStates(f func(*State)) {
f(&m.state)
}
// AcceptItem accepts the currently selected item.
func (m *Mode) AcceptItem(st *types.State) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
m.state.items.Accept(m.state.selected, st)
}
// AcceptItemAndClose accepts the currently selected item and closes the listing
// mode.
func (m *Mode) AcceptItemAndClose(st *types.State) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
m.state.items.Accept(m.state.selected, st)
st.SetMode(nil)
}
// The number of lines the listing mode keeps between the current selected item
// and the top and bottom edges of the window, unless the available height is
// too small or if the selected item is near the top or bottom of the list.
@ -126,10 +148,6 @@ func (m *Mode) List(maxHeight int) ui.Renderer {
defer m.stateMutex.Unlock()
st := &m.state
if st.items == nil {
// This is the first time List is called, get initial items.
st.items = m.ItemsGetter(st.filter)
}
n := st.items.Len()
if n == 0 {
// No result.

View File

@ -12,6 +12,30 @@ import (
"github.com/elves/elvish/tt"
)
// Implementation of Items that emulates a list of numbers from 0 to n-1.
type fakeItems struct{ n int }
func (it fakeItems) Len() int { return it.n }
func (it fakeItems) Show(i int) styled.Text {
return styled.Unstyled(strconv.Itoa(i))
}
func (it fakeItems) Accept(int, *types.State) {}
// Implementation of Items that emulate 10 empty texts, but can be accepted.
type fakeAcceptableItems struct{ accept func(int, *types.State) }
func (it fakeAcceptableItems) Len() int { return 10 }
func (it fakeAcceptableItems) Show(int) styled.Text {
return styled.Unstyled("")
}
func (it fakeAcceptableItems) Accept(i int, st *types.State) {
it.accept(i, st)
}
func TestModeLine(t *testing.T) {
m := Mode{}
m.Start(StartConfig{Name: "LISTING"})
@ -47,11 +71,50 @@ func TestHandleEvent_CallsKeyHandler(t *testing.T) {
func TestHandleEvent_DefaultHandler(t *testing.T) {
m := Mode{}
m.Start(StartConfig{ItemsGetter: func(string) Items {
return fakeItems{10}
}})
st := types.State{}
st.SetMode(&m)
m.HandleEvent(tty.KeyEvent{ui.Down, 0}, &st)
if m.state.selected != 1 {
t.Errorf("Down did not move selection down")
}
m.HandleEvent(tty.KeyEvent{ui.Up, 0}, &st)
if m.state.selected != 0 {
t.Errorf("Up did not move selection up")
}
m.HandleEvent(tty.KeyEvent{ui.Up, 0}, &st)
if m.state.selected != 0 {
t.Errorf("Up did not stop at first item")
}
m.HandleEvent(tty.KeyEvent{ui.Tab, ui.Shift}, &st)
if m.state.selected != 9 {
t.Errorf("Shift-Tab did not wrap to last item")
}
m.HandleEvent(tty.KeyEvent{ui.Tab, 0}, &st)
if m.state.selected != 0 {
t.Errorf("Tab did not wrap to first item")
}
m.HandleEvent(tty.KeyEvent{ui.Tab, 0}, &st)
if m.state.selected != 1 {
t.Errorf("Tab did not move selection down")
}
m.HandleEvent(tty.KeyEvent{ui.Tab, ui.Shift}, &st)
if m.state.selected != 0 {
t.Errorf("Shift-Tab did not move selection up")
}
m.HandleEvent(tty.KeyEvent{'[', ui.Ctrl}, &st)
if st.Mode() != nil {
t.Errorf("C-[ of the default handler did not set mode to nil")
t.Errorf("Ctrl-[ did not set mode to nil")
}
}
@ -63,12 +126,45 @@ func TestHandleEvent_NonKeyEvent(t *testing.T) {
}
}
type fakeItems struct{ n int }
func TestMutateState(t *testing.T) {
m := Mode{}
m.MutateStates(func(st *State) {
st.selected = 10
})
if m.state.selected != 10 {
t.Errorf("state not mutated")
}
}
func (it fakeItems) Len() int { return it.n }
func TestAcceptItem(t *testing.T) {
m := Mode{}
accepted := -1
m.Start(StartConfig{ItemsGetter: func(string) Items {
return fakeAcceptableItems{func(i int, st *types.State) { accepted = i }}
}})
m.state.selected = 7
m.AcceptItem(&types.State{})
if accepted != 7 {
t.Errorf("accept called with %v, want 7", accepted)
}
}
func (it fakeItems) Show(i int) styled.Text {
return styled.Unstyled(strconv.Itoa(i))
func TestAcceptItemAndClose(t *testing.T) {
m := Mode{}
accepted := -1
m.Start(StartConfig{ItemsGetter: func(string) Items {
return fakeAcceptableItems{func(i int, st *types.State) { accepted = i }}
}})
m.state.selected = 7
st := &types.State{}
st.SetMode(&m)
m.AcceptItemAndClose(st)
if accepted != 7 {
t.Errorf("accept called with %v, want 7", accepted)
}
if st.Raw.Mode != nil {
t.Errorf("mode not reset")
}
}
func TestList_Normal(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"github.com/elves/elvish/newedit/listing"
)
func initListing() (*listing.Mode, *BindingMap, eval.Ns) {
func initListing(ed editor) (*listing.Mode, *BindingMap, eval.Ns) {
mode := &listing.Mode{}
binding := EmptyBindingMap
ns := eval.Ns{
@ -16,6 +16,9 @@ func initListing() (*listing.Mode, *BindingMap, eval.Ns) {
"down": func() { mode.MutateStates((*listing.State).Down) },
"up-cycle": func() { mode.MutateStates((*listing.State).UpCycle) },
"down-cycle": func() { mode.MutateStates((*listing.State).DownCycle) },
"accept": func() { mode.AcceptItem(ed.State()) },
"accept-close": func() { mode.AcceptItemAndClose(ed.State()) },
})
return mode, &binding, ns
}

View File

@ -7,7 +7,7 @@ import (
func TestInitListing_Binding(t *testing.T) {
// Test that the binding variable in the returned namespace indeed refers to
// the BindingMap returned.
_, binding, ns := initListing()
_, binding, ns := initListing(&fakeEditor{})
if ns["binding"].Get() != *binding {
t.Errorf("The binding var in the ns is not the same as the BindingMap")
}

View File

@ -1,6 +0,0 @@
package types
import "testing"
func TestTODO(t *testing.T) {
}

View File

@ -75,6 +75,15 @@ func (s *State) CodeAfterDot() string {
return s.Raw.Code[s.Raw.Dot:]
}
// InsertAtDot inserts the given text at the dot.
func (s *State) InsertAtDot(text string) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
raw := &s.Raw
raw.Code = raw.Code[:raw.Dot] + text + raw.Code[raw.Dot:]
raw.Dot += len(text)
}
// AddNote adds a note.
func (s *State) AddNote(note string) {
s.Mutex.Lock()

View File

@ -0,0 +1,15 @@
package types
import (
"reflect"
"testing"
)
func TestInsertAtDot(t *testing.T) {
st := &State{Raw: RawState{Code: "ab", Dot: 1}}
st.InsertAtDot("xy")
wantRawState := RawState{Code: "axyb", Dot: 3}
if !reflect.DeepEqual(st.Raw, wantRawState) {
t.Errorf("got raw state %v, want %v", st.Raw, wantRawState)
}
}