diff --git a/newedit/default_bindings.go b/newedit/default_bindings.go index 6a2dfe19..d2e00624 100644 --- a/newedit/default_bindings.go +++ b/newedit/default_bindings.go @@ -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~ ]) ` diff --git a/newedit/lastcmd/lastcmd.go b/newedit/lastcmd/lastcmd.go index d6063c92..288f18d1 100644 --- a/newedit/lastcmd/lastcmd.go +++ b/newedit/lastcmd/lastcmd.go @@ -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, }) } diff --git a/newedit/listing/listing.go b/newedit/listing/listing.go index 1d958603..7f3edc6a 100644 --- a/newedit/listing/listing.go +++ b/newedit/listing/listing.go @@ -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)) { diff --git a/newedit/listing/listing_test.go b/newedit/listing/listing_test.go index c25e72b7..c44762ba 100644 --- a/newedit/listing/listing_test.go +++ b/newedit/listing/listing_test.go @@ -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{}) diff --git a/newedit/listing/state.go b/newedit/listing/state.go index 23f9e801..53c59965 100644 --- a/newedit/listing/state.go +++ b/newedit/listing/state.go @@ -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 +} diff --git a/newedit/listing_api.go b/newedit/listing_api.go index 61f5efc6..01036228 100644 --- a/newedit/listing_api.go +++ b/newedit/listing_api.go @@ -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 } diff --git a/newedit/listing_api_test.go b/newedit/listing_api_test.go index b000bce6..8e772633 100644 --- a/newedit/listing_api_test.go +++ b/newedit/listing_api_test.go @@ -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.