Skip to content

Commit 74be8c4

Browse files
committed
implement command line history
1 parent 1f2e9d1 commit 74be8c4

File tree

3 files changed

+227
-18
lines changed

3 files changed

+227
-18
lines changed

cmdline/cmdline.go

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,24 @@ import (
99

1010
// Cmdline implements editor.Cmdline
1111
type Cmdline struct {
12-
cmdline []rune
13-
cursor int
14-
completor *completor
15-
typ rune
16-
eventCh chan<- event.Event
17-
cmdlineCh <-chan event.Event
18-
redrawCh chan<- struct{}
19-
mu *sync.Mutex
12+
cmdline []rune
13+
cursor int
14+
completor *completor
15+
typ rune
16+
historyIndex int
17+
history []string
18+
histories map[bool][]string
19+
eventCh chan<- event.Event
20+
cmdlineCh <-chan event.Event
21+
redrawCh chan<- struct{}
22+
mu *sync.Mutex
2023
}
2124

2225
// NewCmdline creates a new Cmdline.
2326
func NewCmdline() *Cmdline {
2427
return &Cmdline{
2528
completor: newCompletor(&filesystem{}),
29+
histories: map[bool][]string{false: {}, true: {}},
2630
mu: new(sync.Mutex),
2731
}
2832
}
@@ -38,16 +42,17 @@ func (c *Cmdline) Run() {
3842
c.mu.Lock()
3943
switch e.Type {
4044
case event.StartCmdlineCommand:
41-
c.typ = ':'
42-
c.start(e.Arg)
45+
c.start(':', e.Arg)
4346
case event.StartCmdlineSearchForward:
44-
c.typ = '/'
45-
c.clear()
47+
c.start('/', "")
4648
case event.StartCmdlineSearchBackward:
47-
c.typ = '?'
48-
c.clear()
49+
c.start('?', "")
4950
case event.ExitCmdline:
5051
c.clear()
52+
case event.CursorUp:
53+
c.cursorUp()
54+
case event.CursorDown:
55+
c.cursorDown()
5156
case event.CursorLeft:
5257
c.cursorLeft()
5358
case event.CursorRight:
@@ -80,6 +85,7 @@ func (c *Cmdline) Run() {
8085
continue
8186
case event.ExecuteCmdline:
8287
c.execute()
88+
c.saveHistory()
8389
default:
8490
c.mu.Unlock()
8591
continue
@@ -90,6 +96,26 @@ func (c *Cmdline) Run() {
9096
}
9197
}
9298

99+
func (c *Cmdline) cursorUp() {
100+
if c.historyIndex--; c.historyIndex >= 0 {
101+
c.cmdline = []rune(c.history[c.historyIndex])
102+
c.cursor = len(c.cmdline)
103+
} else {
104+
c.clear()
105+
c.historyIndex = -1
106+
}
107+
}
108+
109+
func (c *Cmdline) cursorDown() {
110+
if c.historyIndex++; c.historyIndex < len(c.history) {
111+
c.cmdline = []rune(c.history[c.historyIndex])
112+
c.cursor = len(c.cmdline)
113+
} else {
114+
c.clear()
115+
c.historyIndex = len(c.history)
116+
}
117+
}
118+
93119
func (c *Cmdline) cursorLeft() {
94120
c.cursor = max(0, c.cursor-1)
95121
}
@@ -142,9 +168,12 @@ func isKeyword(c rune) bool {
142168
return unicode.IsDigit(c) || unicode.IsLetter(c) || c == '_'
143169
}
144170

145-
func (c *Cmdline) start(arg string) {
171+
func (c *Cmdline) start(typ rune, arg string) {
172+
c.typ = typ
146173
c.cmdline = []rune(arg)
147174
c.cursor = len(c.cmdline)
175+
c.history = c.histories[typ == ':']
176+
c.historyIndex = len(c.history)
148177
}
149178

150179
func (c *Cmdline) clear() {
@@ -197,6 +226,17 @@ func (c *Cmdline) execute() {
197226
}
198227
}
199228

229+
func (c *Cmdline) saveHistory() {
230+
cmdline := string(c.cmdline)
231+
for i, h := range c.history {
232+
if h == cmdline {
233+
c.history = append(c.history[:i], c.history[i+1:]...)
234+
break
235+
}
236+
}
237+
c.histories[c.typ == ':'] = append(c.history, cmdline)
238+
}
239+
200240
// Get returns the current state of cmdline.
201241
func (c *Cmdline) Get() ([]rune, int, []string, int) {
202242
c.mu.Lock()

cmdline/cmdline_test.go

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"runtime"
66
"strings"
77
"testing"
8-
"time"
98

109
"github.com/itchyny/bed/event"
1110
)
@@ -49,7 +48,6 @@ func TestCmdlineRun(t *testing.T) {
4948
go func() {
5049
for _, e := range events {
5150
cmdlineCh <- e
52-
time.Sleep(10 * time.Millisecond)
5351
}
5452
}()
5553
for range len(events) - 4 {
@@ -661,3 +659,170 @@ func TestCmdlineSearch(t *testing.T) {
661659
t.Errorf("cmdline should emit search event with Rune %q but got %q", '?', e.Rune)
662660
}
663661
}
662+
663+
func TestCmdlineHistory(t *testing.T) {
664+
c := NewCmdline()
665+
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
666+
c.Init(eventCh, cmdlineCh, redrawCh)
667+
go c.Run()
668+
events0 := []event.Event{
669+
{Type: event.StartCmdlineCommand},
670+
{Type: event.Rune, Rune: 'n'},
671+
{Type: event.Rune, Rune: 'e'},
672+
{Type: event.Rune, Rune: 'w'},
673+
{Type: event.ExecuteCmdline},
674+
}
675+
events1 := []event.Event{
676+
{Type: event.StartCmdlineCommand},
677+
{Type: event.Rune, Rune: 'v'},
678+
{Type: event.Rune, Rune: 'n'},
679+
{Type: event.Rune, Rune: 'e'},
680+
{Type: event.Rune, Rune: 'w'},
681+
{Type: event.ExecuteCmdline},
682+
}
683+
events2 := []event.Event{
684+
{Type: event.StartCmdlineCommand},
685+
{Type: event.CursorUp},
686+
{Type: event.ExecuteCmdline},
687+
}
688+
events3 := []event.Event{
689+
{Type: event.StartCmdlineCommand},
690+
{Type: event.CursorUp},
691+
{Type: event.CursorUp},
692+
{Type: event.CursorUp},
693+
{Type: event.CursorDown},
694+
{Type: event.ExecuteCmdline},
695+
}
696+
events4 := []event.Event{
697+
{Type: event.StartCmdlineCommand},
698+
{Type: event.CursorUp},
699+
{Type: event.ExecuteCmdline},
700+
}
701+
events5 := []event.Event{
702+
{Type: event.StartCmdlineSearchForward},
703+
{Type: event.Rune, Rune: 't'},
704+
{Type: event.Rune, Rune: 'e'},
705+
{Type: event.Rune, Rune: 's'},
706+
{Type: event.Rune, Rune: 't'},
707+
{Type: event.ExecuteCmdline},
708+
}
709+
events6 := []event.Event{
710+
{Type: event.StartCmdlineSearchForward},
711+
{Type: event.CursorUp},
712+
{Type: event.CursorDown},
713+
{Type: event.Rune, Rune: 'n'},
714+
{Type: event.Rune, Rune: 'e'},
715+
{Type: event.Rune, Rune: 'w'},
716+
{Type: event.ExecuteCmdline},
717+
}
718+
events7 := []event.Event{
719+
{Type: event.StartCmdlineSearchBackward},
720+
{Type: event.CursorUp},
721+
{Type: event.CursorUp},
722+
{Type: event.ExecuteCmdline},
723+
}
724+
events8 := []event.Event{
725+
{Type: event.StartCmdlineCommand},
726+
{Type: event.CursorUp},
727+
{Type: event.CursorUp},
728+
{Type: event.ExecuteCmdline},
729+
}
730+
events9 := []event.Event{
731+
{Type: event.StartCmdlineSearchForward},
732+
{Type: event.CursorUp},
733+
{Type: event.ExecuteCmdline},
734+
}
735+
go func() {
736+
for _, events := range [][]event.Event{
737+
events0, events1, events2, events3, events4,
738+
events5, events6, events7, events8, events9,
739+
} {
740+
for _, e := range events {
741+
cmdlineCh <- e
742+
}
743+
}
744+
}()
745+
for range len(events0) - 1 {
746+
<-redrawCh
747+
}
748+
e := <-eventCh
749+
if e.Type != event.New {
750+
t.Errorf("cmdline should emit New event but got %v", e)
751+
}
752+
for range len(events1) {
753+
<-redrawCh
754+
}
755+
e = <-eventCh
756+
if e.Type != event.Vnew {
757+
t.Errorf("cmdline should emit Vnew event but got %v", e)
758+
}
759+
for range len(events2) {
760+
<-redrawCh
761+
}
762+
e = <-eventCh
763+
if e.Type != event.Vnew {
764+
t.Errorf("cmdline should emit Vnew event but got %v", e)
765+
}
766+
for range len(events3) {
767+
<-redrawCh
768+
}
769+
e = <-eventCh
770+
if e.Type != event.New {
771+
t.Errorf("cmdline should emit New event but got %v", e.Type)
772+
}
773+
for range len(events4) {
774+
<-redrawCh
775+
}
776+
e = <-eventCh
777+
if e.Type != event.New {
778+
t.Errorf("cmdline should emit New event but got %v", e.Type)
779+
}
780+
for range len(events5) {
781+
<-redrawCh
782+
}
783+
e = <-eventCh
784+
if e.Type != event.ExecuteSearch {
785+
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
786+
}
787+
if expected := "test"; e.Arg != expected {
788+
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
789+
}
790+
for range len(events6) {
791+
<-redrawCh
792+
}
793+
e = <-eventCh
794+
if e.Type != event.ExecuteSearch {
795+
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
796+
}
797+
if expected := "new"; e.Arg != expected {
798+
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
799+
}
800+
for range len(events7) {
801+
<-redrawCh
802+
}
803+
e = <-eventCh
804+
if e.Type != event.ExecuteSearch {
805+
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
806+
}
807+
if expected := "test"; e.Arg != expected {
808+
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
809+
}
810+
for range len(events8) {
811+
<-redrawCh
812+
}
813+
e = <-eventCh
814+
if e.Type != event.Vnew {
815+
t.Errorf("cmdline should emit Vnew event but got %v", e.Type)
816+
}
817+
for range len(events9) {
818+
<-redrawCh
819+
}
820+
e = <-eventCh
821+
if e.Type != event.ExecuteSearch {
822+
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
823+
}
824+
if expected := "test"; e.Arg != expected {
825+
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
826+
}
827+
<-redrawCh
828+
}

editor/key.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,13 @@ func defaultKeyManagers() map[mode.Mode]*key.Manager {
111111
kms[mode.Visual] = km
112112

113113
km = key.NewManager(false)
114+
km.Register(event.CursorUp, "up")
115+
km.Register(event.CursorDown, "down")
114116
km.Register(event.CursorLeft, "left")
115-
km.Register(event.CursorLeft, "c-b")
116117
km.Register(event.CursorRight, "right")
118+
km.Register(event.CursorUp, "c-p")
119+
km.Register(event.CursorDown, "c-n")
120+
km.Register(event.CursorLeft, "c-b")
117121
km.Register(event.CursorRight, "c-f")
118122
km.Register(event.CursorHead, "home")
119123
km.Register(event.CursorHead, "c-a")

0 commit comments

Comments
 (0)