Skip to content

Commit

Permalink
implement command line history
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Oct 1, 2024
1 parent 1f2e9d1 commit 74be8c4
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 18 deletions.
70 changes: 55 additions & 15 deletions cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@ import (

// Cmdline implements editor.Cmdline
type Cmdline struct {
cmdline []rune
cursor int
completor *completor
typ rune
eventCh chan<- event.Event
cmdlineCh <-chan event.Event
redrawCh chan<- struct{}
mu *sync.Mutex
cmdline []rune
cursor int
completor *completor
typ rune
historyIndex int
history []string
histories map[bool][]string
eventCh chan<- event.Event
cmdlineCh <-chan event.Event
redrawCh chan<- struct{}
mu *sync.Mutex
}

// NewCmdline creates a new Cmdline.
func NewCmdline() *Cmdline {
return &Cmdline{
completor: newCompletor(&filesystem{}),
histories: map[bool][]string{false: {}, true: {}},
mu: new(sync.Mutex),
}
}
Expand All @@ -38,16 +42,17 @@ func (c *Cmdline) Run() {
c.mu.Lock()
switch e.Type {
case event.StartCmdlineCommand:
c.typ = ':'
c.start(e.Arg)
c.start(':', e.Arg)
case event.StartCmdlineSearchForward:
c.typ = '/'
c.clear()
c.start('/', "")
case event.StartCmdlineSearchBackward:
c.typ = '?'
c.clear()
c.start('?', "")
case event.ExitCmdline:
c.clear()
case event.CursorUp:
c.cursorUp()
case event.CursorDown:
c.cursorDown()
case event.CursorLeft:
c.cursorLeft()
case event.CursorRight:
Expand Down Expand Up @@ -80,6 +85,7 @@ func (c *Cmdline) Run() {
continue
case event.ExecuteCmdline:
c.execute()
c.saveHistory()
default:
c.mu.Unlock()
continue
Expand All @@ -90,6 +96,26 @@ func (c *Cmdline) Run() {
}
}

func (c *Cmdline) cursorUp() {
if c.historyIndex--; c.historyIndex >= 0 {
c.cmdline = []rune(c.history[c.historyIndex])
c.cursor = len(c.cmdline)
} else {
c.clear()
c.historyIndex = -1
}
}

func (c *Cmdline) cursorDown() {
if c.historyIndex++; c.historyIndex < len(c.history) {
c.cmdline = []rune(c.history[c.historyIndex])
c.cursor = len(c.cmdline)
} else {
c.clear()
c.historyIndex = len(c.history)
}
}

func (c *Cmdline) cursorLeft() {
c.cursor = max(0, c.cursor-1)
}
Expand Down Expand Up @@ -142,9 +168,12 @@ func isKeyword(c rune) bool {
return unicode.IsDigit(c) || unicode.IsLetter(c) || c == '_'
}

func (c *Cmdline) start(arg string) {
func (c *Cmdline) start(typ rune, arg string) {
c.typ = typ
c.cmdline = []rune(arg)
c.cursor = len(c.cmdline)
c.history = c.histories[typ == ':']
c.historyIndex = len(c.history)
}

func (c *Cmdline) clear() {
Expand Down Expand Up @@ -197,6 +226,17 @@ func (c *Cmdline) execute() {
}
}

func (c *Cmdline) saveHistory() {
cmdline := string(c.cmdline)
for i, h := range c.history {
if h == cmdline {
c.history = append(c.history[:i], c.history[i+1:]...)
break
}
}
c.histories[c.typ == ':'] = append(c.history, cmdline)
}

// Get returns the current state of cmdline.
func (c *Cmdline) Get() ([]rune, int, []string, int) {
c.mu.Lock()
Expand Down
169 changes: 167 additions & 2 deletions cmdline/cmdline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"runtime"
"strings"
"testing"
"time"

"github.com/itchyny/bed/event"
)
Expand Down Expand Up @@ -49,7 +48,6 @@ func TestCmdlineRun(t *testing.T) {
go func() {
for _, e := range events {
cmdlineCh <- e
time.Sleep(10 * time.Millisecond)
}
}()
for range len(events) - 4 {
Expand Down Expand Up @@ -661,3 +659,170 @@ func TestCmdlineSearch(t *testing.T) {
t.Errorf("cmdline should emit search event with Rune %q but got %q", '?', e.Rune)
}
}

func TestCmdlineHistory(t *testing.T) {
c := NewCmdline()
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
c.Init(eventCh, cmdlineCh, redrawCh)
go c.Run()
events0 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events1 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.Rune, Rune: 'v'},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events2 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events3 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.CursorDown},
{Type: event.ExecuteCmdline},
}
events4 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events5 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.Rune, Rune: 't'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 's'},
{Type: event.Rune, Rune: 't'},
{Type: event.ExecuteCmdline},
}
events6 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.CursorUp},
{Type: event.CursorDown},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events7 := []event.Event{
{Type: event.StartCmdlineSearchBackward},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events8 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events9 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
go func() {
for _, events := range [][]event.Event{
events0, events1, events2, events3, events4,
events5, events6, events7, events8, events9,
} {
for _, e := range events {
cmdlineCh <- e
}
}
}()
for range len(events0) - 1 {
<-redrawCh
}
e := <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e)
}
for range len(events1) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e)
}
for range len(events2) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e)
}
for range len(events3) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e.Type)
}
for range len(events4) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e.Type)
}
for range len(events5) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events6) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "new"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events7) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events8) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e.Type)
}
for range len(events9) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
<-redrawCh
}
6 changes: 5 additions & 1 deletion editor/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,13 @@ func defaultKeyManagers() map[mode.Mode]*key.Manager {
kms[mode.Visual] = km

km = key.NewManager(false)
km.Register(event.CursorUp, "up")
km.Register(event.CursorDown, "down")
km.Register(event.CursorLeft, "left")
km.Register(event.CursorLeft, "c-b")
km.Register(event.CursorRight, "right")
km.Register(event.CursorUp, "c-p")
km.Register(event.CursorDown, "c-n")
km.Register(event.CursorLeft, "c-b")
km.Register(event.CursorRight, "c-f")
km.Register(event.CursorHead, "home")
km.Register(event.CursorHead, "c-a")
Expand Down

0 comments on commit 74be8c4

Please sign in to comment.