mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 09:57:51 +08:00
Support filtering in listing mode.
This commit is contained in:
parent
06fae6494a
commit
e3de2eb8b1
|
@ -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~
|
||||
])
|
||||
`
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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{})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user