Support filtering in listing mode.

This commit is contained in:
Qi Xiao 2019-03-12 21:43:08 +00:00
parent 06fae6494a
commit e3de2eb8b1
7 changed files with 121 additions and 11 deletions

View File

@ -29,13 +29,18 @@ insert:binding = (binding-map [
])
listing:binding = (binding-map [
&Ctrl-'['= $reset-mode~
&Up= $listing:up~
&Down= $listing:down~
&Tab= $listing:down-cycle~
&Shift-Tab= $listing:up-cycle~
&Enter= $listing:accept-close~
&Ctrl-F= $listing:toggle-filtering~
&Alt-Enter= $listing:accept~
&Enter= $listing:accept-close~
&Ctrl-'['= $reset-mode~
&Default= $listing:default~
])
`

View File

@ -24,9 +24,9 @@ func (m *Mode) Start(line string, words []string) {
Name: "LASTCMD",
KeyHandler: m.KeyHandler,
ItemsGetter: itemsGetter(line, words),
StartFilter: true,
// TODO: Uncomment
// AutoAccept: true,
// StartFiltering: true,
})
}

View File

@ -15,6 +15,8 @@ package listing
import (
"sync"
"unicode"
"unicode/utf8"
"github.com/elves/elvish/edit/tty"
"github.com/elves/elvish/edit/ui"
@ -34,9 +36,9 @@ type StartConfig struct {
Name string
KeyHandler func(ui.Key) types.HandlerAction
ItemsGetter func(filter string) Items
StartFilter bool
// TODO(xiaq): Support the following config options.
// AutoAccept bool
// StartFiltering bool
}
// Items is an interface for accessing items to show in the listing mode.
@ -64,15 +66,21 @@ func (m *Mode) Start(cfg StartConfig) {
} else {
m.state.items = sliceItems{}
}
m.state.filtering = cfg.StartFilter
}
// ModeLine returns a modeline showing the specified name of the mode.
func (m *Mode) ModeLine() ui.Renderer {
return ui.NewModeLineRenderer(" "+m.Name+" ", "")
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
return ui.NewModeLineRenderer(" "+m.Name+" ", m.state.filter)
}
// ModeRenderFlag always returns 0.
// ModeRenderFlag returns CursorOnModeLine if filtering, or 0 otherwise.
func (m *Mode) ModeRenderFlag() types.ModeRenderFlag {
if m.state.filtering {
return types.CursorOnModeLine
}
return 0
}
@ -83,7 +91,7 @@ func (m *Mode) HandleEvent(e tty.Event, st *types.State) types.HandlerAction {
if m.KeyHandler == nil {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
return defaultHandler(ui.Key(e), st, &m.state)
return defaultBinding(ui.Key(e), st, &m.state)
}
return m.KeyHandler(ui.Key(e))
default:
@ -91,7 +99,14 @@ func (m *Mode) HandleEvent(e tty.Event, st *types.State) types.HandlerAction {
}
}
func defaultHandler(k ui.Key, st *types.State, mst *State) types.HandlerAction {
// DefaultHandler handles keys when filtering, and resets the mode when not.
func (m *Mode) DefaultHandler(st *types.State) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
defaultHandler(st.BindingKey(), st, &m.state)
}
func defaultBinding(k ui.Key, st *types.State, mst *State) types.HandlerAction {
switch k {
case ui.K('[', ui.Ctrl):
// TODO(xiaq): Go back to previous mode instead of the initial mode.
@ -104,10 +119,38 @@ func defaultHandler(k ui.Key, st *types.State, mst *State) types.HandlerAction {
mst.DownCycle()
case ui.K(ui.Tab, ui.Shift):
mst.UpCycle()
case ui.K('F', ui.Ctrl):
mst.ToggleFiltering()
default:
return defaultHandler(k, st, mst)
}
return 0
}
func defaultHandler(k ui.Key, st *types.State, mst *State) types.HandlerAction {
if mst.filtering {
filter := mst.filter
if k == ui.K(ui.Backspace) {
_, size := utf8.DecodeLastRuneInString(filter)
if size > 0 {
mst.filter = filter[:len(filter)-size]
}
} else if likeChar(k) {
mst.filter += string(k.Rune)
} else {
st.AddNote("Unbound: " + k.String())
}
return 0
}
st.SetMode(nil)
// TODO: Return ReprocessEvent
return 0
}
func likeChar(k ui.Key) bool {
return k.Mod == 0 && k.Rune > 0 && unicode.IsGraphic(k.Rune)
}
// MutateStates mutates the states using the given function, guarding the
// mutation with the mutex.
func (m *Mode) MutateStates(f func(*State)) {

View File

@ -39,7 +39,8 @@ func (it fakeAcceptableItems) Accept(i int, st *types.State) {
func TestModeLine(t *testing.T) {
m := Mode{}
m.Start(StartConfig{Name: "LISTING"})
wantRenderer := ui.NewModeLineRenderer(" LISTING ", "")
m.state.filter = "filter"
wantRenderer := ui.NewModeLineRenderer(" LISTING ", "filter")
if renderer := m.ModeLine(); !reflect.DeepEqual(renderer, wantRenderer) {
t.Errorf("m.ModeLine() = %v, want %v", renderer, wantRenderer)
}
@ -69,7 +70,7 @@ func TestHandleEvent_CallsKeyHandler(t *testing.T) {
}
}
func TestHandleEvent_DefaultHandler(t *testing.T) {
func TestHandleEvent_DefaultBinding(t *testing.T) {
m := Mode{}
m.Start(StartConfig{ItemsGetter: func(string) Items {
return fakeItems{10}
@ -112,12 +113,62 @@ func TestHandleEvent_DefaultHandler(t *testing.T) {
t.Errorf("Shift-Tab did not move selection up")
}
m.HandleEvent(tty.KeyEvent{'F', ui.Ctrl}, &st)
if !m.state.filtering {
t.Errorf("Ctrl-F does not enable filtering")
}
m.HandleEvent(tty.KeyEvent{'[', ui.Ctrl}, &st)
if st.Mode() != nil {
t.Errorf("Ctrl-[ did not set mode to nil")
}
}
func TestDefaultHandler_Filtering(t *testing.T) {
m := Mode{}
m.Start(StartConfig{ItemsGetter: func(f string) Items {
return fakeItems{10}
}})
m.state.filtering = true
st := types.State{}
st.SetMode(&m)
st.SetBindingKey(ui.K('a'))
m.DefaultHandler(&st)
if m.state.filter != "a" {
t.Errorf("Printable key did not append to filter")
}
m.state.filter = "hello world"
st.SetBindingKey(ui.K(ui.Backspace))
m.DefaultHandler(&st)
if m.state.filter != "hello worl" {
t.Errorf("Backspace did not remove last char of filter")
}
st.SetBindingKey(ui.K('A', ui.Ctrl))
m.DefaultHandler(&st)
wantNotes := []string{"Unbound: Ctrl-A"}
if !reflect.DeepEqual(st.Raw.Notes, wantNotes) {
t.Errorf("Unbound key made notes %v, want %v", st.Raw.Notes, wantNotes)
}
}
func TestDefaultHandler_NotFiltering(t *testing.T) {
m := Mode{}
m.Start(StartConfig{ItemsGetter: func(f string) Items {
return fakeItems{10}
}})
st := types.State{}
st.SetMode(&m)
st.SetBindingKey(ui.K('a'))
m.DefaultHandler(&st)
if st.Mode() != nil {
t.Errorf("Mode not reset")
}
}
func TestHandleEvent_NonKeyEvent(t *testing.T) {
m := Mode{}
a := m.HandleEvent(tty.MouseEvent{}, &types.State{})

View File

@ -42,3 +42,8 @@ func (st *State) DownCycle() {
st.selected = 0
}
}
// ToggleFiltering toggles the filtering status of the state.
func (st *State) ToggleFiltering() {
st.filtering = !st.filtering
}

View File

@ -17,8 +17,12 @@ func initListing(ed editor) (*listing.Mode, *BindingMap, eval.Ns) {
"up-cycle": func() { mode.MutateStates((*listing.State).UpCycle) },
"down-cycle": func() { mode.MutateStates((*listing.State).DownCycle) },
"toggle-filtering": func() { mode.MutateStates((*listing.State).ToggleFiltering) },
"accept": func() { mode.AcceptItem(ed.State()) },
"accept-close": func() { mode.AcceptItemAndClose(ed.State()) },
"default": func() { mode.DefaultHandler(ed.State()) },
})
return mode, &binding, ns
}

View File

@ -13,4 +13,6 @@ func TestInitListing_Binding(t *testing.T) {
}
}
// TODO: Test the builtin functions
// TODO: Test the builtin functions. As a prerequisite, we need to make listing
// mode's state observable, and expose fakeItems and fakeAcceptableItems of the
// listing package.