Ver Fonte

Rewrite pkg/cli/histutil.

- Organize code around two interfaces, Store and Cursor.

- Ensure that a session store is always available, even if the DB is not
  accessible.

This fixes #909.
Qi Xiao há 3 anos atrás
pai
commit
3501b945aa

+ 1 - 2
.gitignore

@@ -22,7 +22,6 @@ _testmain.go
 *.exe
 
 # Project specific
-/t/ # Small applications for manual testing go inside this directory
-/cover/
+cover
 /_bin/
 /elvish

+ 9 - 11
pkg/cli/addons/histlist/histlist_test.go

@@ -39,10 +39,9 @@ func TestStart_OK(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	st := histutil.NewMemoryStore()
-	st.AddCmd(store.Cmd{Text: "foo", Seq: 0})
-	st.AddCmd(store.Cmd{Text: "bar", Seq: 1})
-	st.AddCmd(store.Cmd{Text: "baz", Seq: 2})
+	st := histutil.NewMemStore(
+		// 0    1      2
+		"foo", "bar", "baz")
 	Start(f.App, Config{Store: st})
 
 	// Test UI.
@@ -78,10 +77,9 @@ func TestStart_Dedup(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	st := histutil.NewMemoryStore()
-	st.AddCmd(store.Cmd{Text: "ls", Seq: 0})
-	st.AddCmd(store.Cmd{Text: "echo", Seq: 1})
-	st.AddCmd(store.Cmd{Text: "ls", Seq: 2})
+	st := histutil.NewMemStore(
+		// 0    1      2
+		"ls", "echo", "ls")
 
 	// No dedup
 	Start(f.App, Config{Store: st, Dedup: func() bool { return false }})
@@ -106,9 +104,9 @@ func TestStart_CaseSensitive(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	st := histutil.NewMemoryStore()
-	st.AddCmd(store.Cmd{Text: "ls", Seq: 0})
-	st.AddCmd(store.Cmd{Text: "LS", Seq: 1})
+	st := histutil.NewMemStore(
+		// 0  1
+		"ls", "LS")
 
 	// Case sensitive
 	Start(f.App, Config{Store: st, CaseSensitive: func() bool { return true }})

+ 24 - 17
pkg/cli/addons/histwalk/histwalk.go

@@ -16,18 +16,21 @@ var ErrHistWalkInactive = errors.New("the histwalk addon is not active")
 type Config struct {
 	// Keybinding.
 	Binding cli.Handler
-	// The history walker.
-	Walker histutil.Walker
+	// History store to walk.
+	Store histutil.Store
+	// Only walk through items with this prefix.
+	Prefix string
 }
 
 type widget struct {
-	app cli.App
+	app    cli.App
+	cursor histutil.Cursor
 	Config
 }
 
 func (w *widget) Render(width, height int) *term.Buffer {
-	content := cli.ModeLine(
-		fmt.Sprintf(" HISTORY #%d ", w.Walker.CurrentSeq()), false)
+	cmd, _ := w.cursor.Get()
+	content := cli.ModeLine(fmt.Sprintf(" HISTORY #%d ", cmd.Seq), false)
 	buf := term.NewBufferBuilder(width).WriteStyled(content).Buffer()
 	buf.TrimToLines(0, height)
 	return buf
@@ -45,31 +48,32 @@ func (w *widget) Handle(event term.Event) bool {
 func (w *widget) Focus() bool { return false }
 
 func (w *widget) onWalk() {
-	prefix := w.Walker.Prefix()
+	cmd, _ := w.cursor.Get()
 	w.app.CodeArea().MutateState(func(s *cli.CodeAreaState) {
 		s.Pending = cli.PendingCode{
-			From: len(prefix), To: len(s.Buffer.Content),
-			Content: w.Walker.CurrentCmd()[len(prefix):],
+			From: len(w.Prefix), To: len(s.Buffer.Content),
+			Content: cmd.Text[len(w.Prefix):],
 		}
 	})
 }
 
 // Start starts the histwalk addon.
 func Start(app cli.App, cfg Config) {
-	if cfg.Walker == nil {
-		app.Notify("no history walker")
+	if cfg.Store == nil {
+		app.Notify("no history store")
 		return
 	}
 	if cfg.Binding == nil {
 		cfg.Binding = cli.DummyHandler{}
 	}
-	walker := cfg.Walker
-	err := walker.Prev()
+	cursor := histutil.NewDedupCursor(cfg.Store.Cursor(cfg.Prefix))
+	cursor.Prev()
+	_, err := cursor.Get()
 	if err != nil {
 		app.Notify(err.Error())
 		return
 	}
-	w := widget{app: app, Config: cfg}
+	w := widget{app: app, Config: cfg, cursor: cursor}
 	w.onWalk()
 	app.MutateState(func(s *cli.State) { s.Addon = &w })
 	app.Redraw()
@@ -79,14 +83,14 @@ func Start(app cli.App, cfg Config) {
 // if the histwalk addon is not active, and histutil.ErrEndOfHistory if it would
 // go over the end.
 func Prev(app cli.App) error {
-	return walk(app, func(w *widget) error { return w.Walker.Prev() })
+	return walk(app, histutil.Cursor.Prev, histutil.Cursor.Next)
 }
 
 // Next walks to the next entry in history. It returns ErrHistWalkInactive if
 // the histwalk addon is not active, and histutil.ErrEndOfHistory if it would go
 // over the end.
 func Next(app cli.App) error {
-	return walk(app, func(w *widget) error { return w.Walker.Next() })
+	return walk(app, histutil.Cursor.Next, histutil.Cursor.Prev)
 }
 
 // Close closes the histwalk addon. It does nothing if the histwalk addon is not
@@ -121,14 +125,17 @@ func closeAddon(app cli.App) bool {
 	return closed
 }
 
-func walk(app cli.App, f func(*widget) error) error {
+func walk(app cli.App, f func(histutil.Cursor), undo func(histutil.Cursor)) error {
 	w, ok := getWidget(app)
 	if !ok {
 		return ErrHistWalkInactive
 	}
-	err := f(w)
+	f(w.cursor)
+	_, err := w.cursor.Get()
 	if err == nil {
 		w.onWalk()
+	} else if err == histutil.ErrEndOfHistory {
+		undo(w.cursor)
 	}
 	return err
 }

+ 10 - 14
pkg/cli/addons/histwalk/histwalk_test.go

@@ -20,12 +20,12 @@ func TestHistWalk(t *testing.T) {
 	f.TTY.TestBuffer(t, buf0)
 
 	getCfg := func() Config {
-		db := &histutil.TestDB{
-			//                 0       1        2         3        4         5
-			AllCmds: []string{"echo", "ls -l", "echo a", "ls -a", "echo a", "ls -a"},
-		}
+		store := histutil.NewMemStore(
+			// 0       1        2         3        4         5
+			"echo", "ls -l", "echo a", "ls -a", "echo a", "ls -a")
 		return Config{
-			Walker: histutil.NewWalker(db, -1, nil, "ls"),
+			Store:  store,
+			Prefix: "ls",
 			Binding: cli.MapHandler{
 				term.K(ui.Up):        func() { Prev(f.App) },
 				term.K(ui.Down):      func() { Next(f.App) },
@@ -72,7 +72,7 @@ func TestHistWalk_NoWalker(t *testing.T) {
 	defer f.Stop()
 
 	Start(f.App, Config{})
-	f.TestTTYNotes(t, "no history walker")
+	f.TestTTYNotes(t, "no history store")
 }
 
 func TestHistWalk_NoMatch(t *testing.T) {
@@ -84,8 +84,8 @@ func TestHistWalk_NoMatch(t *testing.T) {
 	buf0 := f.MakeBuffer("ls", term.DotHere)
 	f.TTY.TestBuffer(t, buf0)
 
-	db := &histutil.TestDB{AllCmds: []string{"echo 1", "echo 2"}}
-	cfg := Config{Walker: histutil.NewWalker(db, -1, nil, "ls")}
+	store := histutil.NewMemStore("echo 1", "echo 2")
+	cfg := Config{Store: store, Prefix: "ls"}
 	Start(f.App, cfg)
 	// Test that an error message has been written to the notes buffer.
 	f.TestTTYNotes(t, "end of history")
@@ -97,12 +97,8 @@ func TestHistWalk_FallbackHandler(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	db := &histutil.TestDB{
-		AllCmds: []string{"ls"},
-	}
-	Start(f.App, Config{
-		Walker: histutil.NewWalker(db, -1, nil, ""),
-	})
+	store := histutil.NewMemStore("ls")
+	Start(f.App, Config{Store: store, Prefix: ""})
 	f.TestTTY(t,
 		"ls", Styles,
 		"__", term.DotHere, "\n",

+ 4 - 3
pkg/cli/addons/lastcmd/lastcmd.go

@@ -9,7 +9,6 @@ import (
 
 	"github.com/elves/elvish/pkg/cli"
 	"github.com/elves/elvish/pkg/cli/histutil"
-	"github.com/elves/elvish/pkg/store"
 	"github.com/elves/elvish/pkg/ui"
 )
 
@@ -25,7 +24,7 @@ type Config struct {
 
 // Store wraps the LastCmd method. It is a subset of histutil.Store.
 type Store interface {
-	LastCmd() (store.Cmd, error)
+	Cursor(prefix string) histutil.Cursor
 }
 
 var _ = Store(histutil.Store(nil))
@@ -36,7 +35,9 @@ func Start(app cli.App, cfg Config) {
 		app.Notify("no history store")
 		return
 	}
-	cmd, err := cfg.Store.LastCmd()
+	c := cfg.Store.Cursor("")
+	c.Prev()
+	cmd, err := c.Get()
 	if err != nil {
 		app.Notify("db error: " + err.Error())
 		return

+ 9 - 3
pkg/cli/addons/lastcmd/lastcmd_test.go

@@ -33,7 +33,14 @@ func TestStart_StoreError(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	Start(f.App, Config{Store: faultyStore{}})
+	db := histutil.NewFaultyInMemoryDB()
+	store, err := histutil.NewDBStore(db)
+	if err != nil {
+		panic(err)
+	}
+	db.SetOneOffError(mockError)
+
+	Start(f.App, Config{Store: store})
 	f.TestTTYNotes(t, "db error: mock error")
 }
 
@@ -41,8 +48,7 @@ func TestStart_OK(t *testing.T) {
 	f := Setup()
 	defer f.Stop()
 
-	st := histutil.NewMemoryStore()
-	st.AddCmd(store.Cmd{Text: "foo,bar,baz", Seq: 0})
+	st := histutil.NewMemStore("foo,bar,baz")
 	Start(f.App, Config{
 		Store: st,
 		Wordifier: func(cmd string) []string {

+ 65 - 23
pkg/cli/histutil/db.go

@@ -12,52 +12,94 @@ type DB interface {
 	AddCmd(cmd string) (int, error)
 	CmdsWithSeq(from, upto int) ([]store.Cmd, error)
 	PrevCmd(upto int, prefix string) (store.Cmd, error)
+	NextCmd(from int, prefix string) (store.Cmd, error)
 }
 
-// TestDB is an implementation of the DB interface that can be used for testing.
-type TestDB struct {
-	AllCmds []string
+// FaultyInMemoryDB is an in-memory DB implementation that can be injected
+// one-off errors. It is useful in tests.
+type FaultyInMemoryDB interface {
+	DB
+	// SetOneOffError causes the next operation on the database to return the
+	// given error.
+	SetOneOffError(err error)
+}
+
+// NewFaultyInMemoryDB creates a new FaultyInMemoryDB with the given commands.
+func NewFaultyInMemoryDB(cmds ...string) FaultyInMemoryDB {
+	return &testDB{cmds: cmds}
+}
+
+// Implementation of FaultyInMemoryDB.
+type testDB struct {
+	cmds        []string
+	oneOffError error
+}
 
-	OneOffError error
+func (s *testDB) SetOneOffError(err error) {
+	s.oneOffError = err
 }
 
-func (s *TestDB) error() error {
-	err := s.OneOffError
-	s.OneOffError = nil
+func (s *testDB) error() error {
+	err := s.oneOffError
+	s.oneOffError = nil
 	return err
 }
 
-func (s *TestDB) NextCmdSeq() (int, error) {
-	return len(s.AllCmds), s.error()
+func (s *testDB) NextCmdSeq() (int, error) {
+	return len(s.cmds), s.error()
 }
 
-func (s *TestDB) AddCmd(cmd string) (int, error) {
-	if s.OneOffError != nil {
+func (s *testDB) AddCmd(cmd string) (int, error) {
+	if s.oneOffError != nil {
 		return -1, s.error()
 	}
-	s.AllCmds = append(s.AllCmds, cmd)
-	return len(s.AllCmds) - 1, nil
+	s.cmds = append(s.cmds, cmd)
+	return len(s.cmds) - 1, nil
 }
 
-func (s *TestDB) CmdsWithSeq(from, upto int) ([]store.Cmd, error) {
+func (s *testDB) CmdsWithSeq(from, upto int) ([]store.Cmd, error) {
+	if err := s.error(); err != nil {
+		return nil, err
+	}
+	if from < 0 {
+		from = 0
+	}
+	if upto < 0 || upto > len(s.cmds) {
+		upto = len(s.cmds)
+	}
 	var cmds []store.Cmd
 	for i := from; i < upto; i++ {
-		cmds = append(cmds, store.Cmd{Text: s.AllCmds[i], Seq: i})
+		cmds = append(cmds, store.Cmd{Text: s.cmds[i], Seq: i})
 	}
-	return cmds, s.error()
+	return cmds, nil
 }
 
-func (s *TestDB) PrevCmd(upto int, prefix string) (store.Cmd, error) {
-	if s.OneOffError != nil {
+func (s *testDB) PrevCmd(upto int, prefix string) (store.Cmd, error) {
+	if s.oneOffError != nil {
 		return store.Cmd{}, s.error()
 	}
-	if upto < 0 || upto > len(s.AllCmds) {
-		upto = len(s.AllCmds)
+	if upto < 0 || upto > len(s.cmds) {
+		upto = len(s.cmds)
 	}
 	for i := upto - 1; i >= 0; i-- {
-		if strings.HasPrefix(s.AllCmds[i], prefix) {
-			return store.Cmd{Text: s.AllCmds[i], Seq: i}, nil
+		if strings.HasPrefix(s.cmds[i], prefix) {
+			return store.Cmd{Text: s.cmds[i], Seq: i}, nil
+		}
+	}
+	return store.Cmd{}, store.ErrNoMatchingCmd
+}
+
+func (s *testDB) NextCmd(from int, prefix string) (store.Cmd, error) {
+	if s.oneOffError != nil {
+		return store.Cmd{}, s.error()
+	}
+	if from < 0 {
+		from = 0
+	}
+	for i := from; i < len(s.cmds); i++ {
+		if strings.HasPrefix(s.cmds[i], prefix) {
+			return store.Cmd{Text: s.cmds[i], Seq: i}, nil
 		}
 	}
-	return store.Cmd{}, ErrEndOfHistory
+	return store.Cmd{}, store.ErrNoMatchingCmd
 }

+ 78 - 0
pkg/cli/histutil/db_store.go

@@ -0,0 +1,78 @@
+package histutil
+
+import "github.com/elves/elvish/pkg/store"
+
+// Returns a Store backed by a database, with the view of all commands frozen at
+// creation.
+func NewDBStore(db DB) (Store, error) {
+	upper, err := db.NextCmdSeq()
+	if err != nil {
+		return nil, err
+	}
+	return dbStore{db, upper}, nil
+}
+
+type dbStore struct {
+	db    DB
+	upper int
+}
+
+func (s dbStore) AllCmds() ([]store.Cmd, error) {
+	return s.db.CmdsWithSeq(0, s.upper)
+}
+
+func (s dbStore) AddCmd(cmd store.Cmd) (int, error) {
+	return s.db.AddCmd(cmd.Text)
+}
+
+func (s dbStore) Cursor(prefix string) Cursor {
+	return &dbStoreCursor{
+		s.db, prefix, s.upper, store.Cmd{Seq: s.upper}, ErrEndOfHistory}
+}
+
+type dbStoreCursor struct {
+	db     DB
+	prefix string
+	upper  int
+	cmd    store.Cmd
+	err    error
+}
+
+func (c *dbStoreCursor) Prev() {
+	if c.cmd.Seq < 0 {
+		return
+	}
+	cmd, err := c.db.PrevCmd(c.cmd.Seq, c.prefix)
+	c.set(cmd, err, -1)
+}
+
+func (c *dbStoreCursor) Next() {
+	if c.cmd.Seq >= c.upper {
+		return
+	}
+	cmd, err := c.db.NextCmd(c.cmd.Seq+1, c.prefix)
+	if cmd.Seq < c.upper {
+		c.set(cmd, err, c.upper)
+	}
+	if cmd.Seq >= c.upper {
+		c.cmd = store.Cmd{Seq: c.upper}
+		c.err = ErrEndOfHistory
+	}
+}
+
+func (c *dbStoreCursor) set(cmd store.Cmd, err error, endSeq int) {
+	if err == nil {
+		c.cmd = cmd
+		c.err = nil
+	} else if err.Error() == store.ErrNoMatchingCmd.Error() {
+		c.cmd = store.Cmd{Seq: endSeq}
+		c.err = ErrEndOfHistory
+	} else {
+		// Don't change c.cmd
+		c.err = err
+	}
+}
+
+func (c *dbStoreCursor) Get() (store.Cmd, error) {
+	return c.cmd, c.err
+}

+ 51 - 0
pkg/cli/histutil/db_store_test.go

@@ -0,0 +1,51 @@
+package histutil
+
+import (
+	"testing"
+
+	"github.com/elves/elvish/pkg/store"
+)
+
+func TestDBStore_Cursor(t *testing.T) {
+	db := NewFaultyInMemoryDB("+ 1", "- 2", "+ 3")
+	s, err := NewDBStore(db)
+	if err != nil {
+		panic(err)
+	}
+
+	testCursorIteration(t, s.Cursor("+"), []store.Cmd{
+		{Text: "+ 1", Seq: 0},
+		{Text: "+ 3", Seq: 2},
+	})
+
+	// Test error conditions.
+	c := s.Cursor("+")
+
+	expect := func(wantCmd store.Cmd, wantErr error) {
+		t.Helper()
+		cmd, err := c.Get()
+		if cmd != wantCmd {
+			t.Errorf("Get -> %v, want %v", cmd, wantCmd)
+		}
+		if err != wantErr {
+			t.Errorf("Get -> error %v, want %v", err, wantErr)
+		}
+	}
+
+	db.SetOneOffError(mockError)
+	c.Prev()
+	expect(store.Cmd{Seq: 3}, mockError)
+
+	c.Prev()
+	expect(store.Cmd{Text: "+ 3", Seq: 2}, nil)
+
+	db.SetOneOffError(mockError)
+	c.Prev()
+	expect(store.Cmd{Text: "+ 3", Seq: 2}, mockError)
+
+	db.SetOneOffError(mockError)
+	c.Next()
+	expect(store.Cmd{Text: "+ 3", Seq: 2}, mockError)
+}
+
+// Remaing methods tested with HybridStore

+ 53 - 0
pkg/cli/histutil/dedup_cursor.go

@@ -0,0 +1,53 @@
+package histutil
+
+import "github.com/elves/elvish/pkg/store"
+
+// NewDedupCursor returns a cursor that skips over all duplicate entries.
+func NewDedupCursor(c Cursor) Cursor {
+	return &dedupCursor{c, 0, nil, make(map[string]bool)}
+}
+
+type dedupCursor struct {
+	c       Cursor
+	current int
+	stack   []store.Cmd
+	occ     map[string]bool
+}
+
+func (c *dedupCursor) Prev() {
+	if c.current < len(c.stack)-1 {
+		c.current++
+		return
+	}
+	for {
+		c.c.Prev()
+		cmd, err := c.c.Get()
+		if err != nil {
+			c.current = len(c.stack)
+			break
+		}
+		if !c.occ[cmd.Text] {
+			c.current = len(c.stack)
+			c.stack = append(c.stack, cmd)
+			c.occ[cmd.Text] = true
+			break
+		}
+	}
+}
+
+func (c *dedupCursor) Next() {
+	if c.current >= 0 {
+		c.current--
+	}
+}
+
+func (c *dedupCursor) Get() (store.Cmd, error) {
+	switch {
+	case c.current < 0:
+		return store.Cmd{}, ErrEndOfHistory
+	case c.current < len(c.stack):
+		return c.stack[c.current], nil
+	default:
+		return c.c.Get()
+	}
+}

+ 34 - 0
pkg/cli/histutil/dedup_cursor_test.go

@@ -0,0 +1,34 @@
+package histutil
+
+import (
+	"testing"
+
+	"github.com/elves/elvish/pkg/store"
+)
+
+func TestDedupCursor(t *testing.T) {
+	s := NewMemStore("0", "1", "2")
+	c := NewDedupCursor(s.Cursor(""))
+
+	wantCmds := []store.Cmd{
+		{Text: "0", Seq: 0},
+		{Text: "1", Seq: 1},
+		{Text: "2", Seq: 2}}
+
+	testCursorIteration(t, c, wantCmds)
+	// Go back again, this time with a full stack
+	testCursorIteration(t, c, wantCmds)
+
+	c = NewDedupCursor(s.Cursor(""))
+	// Should be a no-op
+	c.Next()
+	testCursorIteration(t, c, wantCmds)
+
+	c = NewDedupCursor(s.Cursor(""))
+	c.Prev()
+	c.Next()
+	_, err := c.Get()
+	if err != ErrEndOfHistory {
+		t.Errorf("Get -> error %v, want ErrEndOfHistory", err)
+	}
+}

+ 0 - 102
pkg/cli/histutil/fuser.go

@@ -1,102 +0,0 @@
-package histutil
-
-import (
-	"sync"
-
-	"github.com/elves/elvish/pkg/store"
-)
-
-// Fuser provides a view of command history that is fused from the shared
-// storage-backed command history and per-session history.
-type Fuser struct {
-	mutex sync.RWMutex
-
-	shared  Store
-	session Store
-
-	// Only used in FastForward.
-	db DB
-}
-
-// NewFuser returns a new Fuser from a database.
-func NewFuser(db DB) (*Fuser, error) {
-	shared, session, err := initStores(db)
-	if err != nil {
-		return nil, err
-	}
-	return &Fuser{shared: shared, session: session, db: db}, nil
-}
-
-func initStores(db DB) (shared, session Store, err error) {
-	shared, err = NewDBStoreFrozen(db)
-	if err != nil {
-		return nil, nil, err
-	}
-	return shared, NewMemoryStore(), nil
-}
-
-// FastForward fast-forwards the view of command history, so that commands added
-// by other sessions since the start of the current session are available.
-func (f *Fuser) FastForward() error {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	shared, session, err := initStores(f.db)
-	if err != nil {
-		return err
-	}
-	f.shared, f.session = shared, session
-	return nil
-}
-
-// AddCmd adds a command to both the database and the per-session history.
-func (f *Fuser) AddCmd(cmd string) (int, error) {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	seq, err := f.shared.AddCmd(store.Cmd{Text: cmd})
-	if err != nil {
-		return -1, err
-	}
-	f.session.AddCmd(store.Cmd{Text: cmd, Seq: seq})
-	return seq, nil
-}
-
-// AllCmds returns all visible commands, consisting of commands that were
-// already in the database at startup, plus the per-session history.
-func (f *Fuser) AllCmds() ([]store.Cmd, error) {
-	f.mutex.RLock()
-	defer f.mutex.RUnlock()
-
-	sharedCmds, err := f.shared.AllCmds()
-	if err != nil {
-		return nil, err
-	}
-	sessionCmds, _ := f.session.AllCmds()
-	return append(sharedCmds, sessionCmds...), nil
-}
-
-// LastCmd returns the last command within the fused view.
-func (f *Fuser) LastCmd() (store.Cmd, error) {
-	cmd, err := f.session.LastCmd()
-	if err != errStoreIsEmpty {
-		return cmd, err
-	}
-	return f.shared.LastCmd()
-}
-
-// SessionCmds returns the per-session history.
-func (f *Fuser) SessionCmds() []store.Cmd {
-	cmds, _ := f.session.AllCmds()
-	return cmds
-}
-
-// Walker returns a walker for the fused command history.
-func (f *Fuser) Walker(prefix string) Walker {
-	f.mutex.RLock()
-	defer f.mutex.RUnlock()
-
-	sessionCmds, _ := f.session.AllCmds()
-	// TODO: Avoid the type cast.
-	return NewWalker(f.db, f.shared.(dbStore).upper, sessionCmds, prefix)
-}

+ 0 - 181
pkg/cli/histutil/fuser_test.go

@@ -1,181 +0,0 @@
-package histutil
-
-import (
-	"errors"
-	"reflect"
-	"testing"
-
-	"github.com/elves/elvish/pkg/store"
-)
-
-var mockError = errors.New("mock error")
-
-func TestNewFuser_Error(t *testing.T) {
-	_, err := NewFuser(&TestDB{OneOffError: mockError})
-	if err != mockError {
-		t.Errorf("NewFuser -> error %v, want %v", err, mockError)
-	}
-}
-
-func TestFusuer_AddCmd(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-	f.AddCmd("session 1")
-
-	wantDBCmds := []string{"store 1", "session 1"}
-	wantSessionCmds := []store.Cmd{{"session 1", 1}}
-	if !reflect.DeepEqual(db.AllCmds, wantDBCmds) {
-		t.Errorf("DB commands = %v, want %v", db.AllCmds, wantDBCmds)
-	}
-	sessionCmds := f.SessionCmds()
-	if !reflect.DeepEqual(sessionCmds, wantSessionCmds) {
-		t.Errorf("Session commands = %v, want %v", sessionCmds, wantSessionCmds)
-	}
-}
-
-func TestFuser_AddCmd_Error(t *testing.T) {
-	db := &TestDB{}
-	f := mustNewFuser(db)
-	db.OneOffError = mockError
-
-	_, err := f.AddCmd("haha")
-
-	if err != mockError {
-		t.Errorf("AddCmd -> error %v, want %v", err, mockError)
-	}
-	if len(f.SessionCmds()) != 0 {
-		t.Errorf("AddCmd adds to session commands when erroring")
-	}
-	if len(f.SessionCmds()) != 0 {
-		t.Errorf("AddCmd adds to all commands when erroring")
-	}
-}
-
-func TestFuser_AllCmds(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-
-	// Simulate adding commands from both the current session and other sessions.
-	f.AddCmd("session 1")
-	db.AddCmd("other session 1")
-	db.AddCmd("other session 2")
-	f.AddCmd("session 2")
-	db.AddCmd("other session 3")
-
-	// AllCmds should return all commands from the storage when the Fuser was
-	// created, plus session commands. The session commands should have sequence
-	// numbers consistent with the DB.
-	cmds, err := f.AllCmds()
-	if err != nil {
-		t.Errorf("AllCmds -> error %v, want nil", err)
-	}
-	wantCmds := []store.Cmd{
-		{"store 1", 0}, {"session 1", 1}, {"session 2", 4}}
-	if !reflect.DeepEqual(cmds, wantCmds) {
-		t.Errorf("AllCmds -> %v, want %v", cmds, wantCmds)
-	}
-}
-
-func TestFuser_AllCmds_Error(t *testing.T) {
-	db := &TestDB{}
-	f := mustNewFuser(db)
-	db.OneOffError = mockError
-
-	_, err := f.AllCmds()
-
-	if err != mockError {
-		t.Errorf("AllCmds -> error %v, want %v", err, mockError)
-	}
-}
-
-func TestFuser_LastCmd_FromDB(t *testing.T) {
-	f := mustNewFuser(&TestDB{AllCmds: []string{"store 1"}})
-
-	cmd, _ := f.LastCmd()
-
-	wantCmd := store.Cmd{"store 1", 0}
-	if cmd != wantCmd {
-		t.Errorf("LastCmd -> %v, want %v", cmd, wantCmd)
-	}
-}
-
-func TestFuser_LastCmd_FromDB_Error(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-
-	db.OneOffError = mockError
-	_, err := f.LastCmd()
-
-	if err != mockError {
-		t.Errorf("LastCmd -> error %v, want %v", err, mockError)
-	}
-}
-
-func TestFuser_LastCmd_FromSession(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-	f.AddCmd("session 1")
-
-	// LastCmd does not use DB when there are any session commands.
-	db.OneOffError = mockError
-	cmd, _ := f.LastCmd()
-
-	wantCmd := store.Cmd{"session 1", 1}
-	if cmd != wantCmd {
-		t.Errorf("LastCmd -> %v, want %v", cmd, wantCmd)
-	}
-}
-
-func TestFuser_FastForward(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-
-	// Simulate adding commands from both the current session and other sessions.
-	f.AddCmd("session 1")
-	db.AddCmd("other session 1")
-	db.AddCmd("other session 2")
-	f.AddCmd("session 2")
-	db.AddCmd("other session 3")
-
-	f.FastForward()
-	db.AddCmd("other session 4")
-
-	wantCmds := []store.Cmd{
-		{"store 1", 0}, {"session 1", 1}, {"other session 1", 2},
-		{"other session 2", 3}, {"session 2", 4}, {"other session 3", 5}}
-	cmds, _ := f.AllCmds()
-	if !reflect.DeepEqual(cmds, wantCmds) {
-		t.Errorf("AllCmds -> %v, want %v", cmds, wantCmds)
-	}
-}
-
-func TestFuser_Walker(t *testing.T) {
-	db := &TestDB{AllCmds: []string{"store 1"}}
-	f := mustNewFuser(db)
-
-	// Simulate adding commands from both the current session and other sessions.
-	f.AddCmd("session 1")
-	db.AddCmd("other session 1")
-	db.AddCmd("other session 2")
-	f.AddCmd("session 2")
-	db.AddCmd("other session 3")
-
-	// Walker should return a walker that walks through shared and session
-	// commands.
-	w := f.Walker("")
-	w.Prev()
-	checkWalkerCurrent(t, w, 4, "session 2")
-	w.Prev()
-	checkWalkerCurrent(t, w, 1, "session 1")
-	w.Prev()
-	checkWalkerCurrent(t, w, 0, "store 1")
-	checkError(t, w.Prev(), ErrEndOfHistory)
-}
-
-func mustNewFuser(db DB) *Fuser {
-	f, err := NewFuser(db)
-	if err != nil {
-		panic(err)
-	}
-	return f
-}

+ 80 - 0
pkg/cli/histutil/hybrid_store.go

@@ -0,0 +1,80 @@
+package histutil
+
+import "github.com/elves/elvish/pkg/store"
+
+// NewHybridStore returns a store that provides a view of all the commands that
+// exists in the database, plus a in-memory session history.
+func NewHybridStore(db DB) (Store, error) {
+	if db == nil {
+		return NewMemStore(), nil
+	}
+	dbStore, err := NewDBStore(db)
+	if err != nil {
+		return NewMemStore(), err
+	}
+	return hybridStore{dbStore, NewMemStore()}, nil
+}
+
+type hybridStore struct {
+	shared, session Store
+}
+
+func (s hybridStore) AddCmd(cmd store.Cmd) (int, error) {
+	seq, err := s.shared.AddCmd(cmd)
+	s.session.AddCmd(store.Cmd{Text: cmd.Text, Seq: seq})
+	return seq, err
+}
+
+func (s hybridStore) AllCmds() ([]store.Cmd, error) {
+	shared, err := s.shared.AllCmds()
+	session, err2 := s.session.AllCmds()
+	if err == nil {
+		err = err2
+	}
+	if len(shared) == 0 {
+		return session, err
+	}
+	return append(shared, session...), err
+}
+
+func (s hybridStore) Cursor(prefix string) Cursor {
+	return &hybridStoreCursor{
+		s.shared.Cursor(prefix), s.session.Cursor(prefix), false}
+}
+
+type hybridStoreCursor struct {
+	shared    Cursor
+	session   Cursor
+	useShared bool
+}
+
+func (c *hybridStoreCursor) Prev() {
+	if c.useShared {
+		c.shared.Prev()
+		return
+	}
+	c.session.Prev()
+	if _, err := c.session.Get(); err == ErrEndOfHistory {
+		c.useShared = true
+		c.shared.Prev()
+	}
+}
+
+func (c *hybridStoreCursor) Next() {
+	if !c.useShared {
+		c.session.Next()
+		return
+	}
+	c.shared.Next()
+	if _, err := c.shared.Get(); err == ErrEndOfHistory {
+		c.useShared = false
+		c.session.Next()
+	}
+}
+
+func (c *hybridStoreCursor) Get() (store.Cmd, error) {
+	if c.useShared {
+		return c.shared.Get()
+	}
+	return c.session.Get()
+}

+ 202 - 0
pkg/cli/histutil/hybrid_store_test.go

@@ -0,0 +1,202 @@
+package histutil
+
+import (
+	"errors"
+	"reflect"
+	"testing"
+
+	"github.com/elves/elvish/pkg/store"
+)
+
+var mockError = errors.New("mock error")
+
+func TestNewHybridStore_ReturnsMemStoreIfDBIsNil(t *testing.T) {
+	store, err := NewHybridStore(nil)
+	if _, ok := store.(*memStore); !ok {
+		t.Errorf("NewHybridStore -> %v, want memStore", store)
+	}
+	if err != nil {
+		t.Errorf("NewHybridStore -> error %v, want nil", err)
+	}
+}
+
+func TestNewHybridStore_ReturnsMemStoreOnDBError(t *testing.T) {
+	db := NewFaultyInMemoryDB()
+	db.SetOneOffError(mockError)
+	store, err := NewHybridStore(db)
+	if _, ok := store.(*memStore); !ok {
+		t.Errorf("NewHybridStore -> %v, want memStore", store)
+	}
+	if err != mockError {
+		t.Errorf("NewHybridStore -> error %v, want %v", err, mockError)
+	}
+}
+
+func TestFusuer_AddCmd_AddsBothToDBAndSession(t *testing.T) {
+	db := NewFaultyInMemoryDB("shared 1")
+	f := mustNewHybridStore(db)
+
+	f.AddCmd(store.Cmd{Text: "session 1"})
+
+	wantDBCmds := []store.Cmd{
+		{Text: "shared 1", Seq: 0}, {Text: "session 1", Seq: 1}}
+	if dbCmds, _ := db.CmdsWithSeq(-1, -1); !reflect.DeepEqual(dbCmds, wantDBCmds) {
+		t.Errorf("DB commands = %v, want %v", dbCmds, wantDBCmds)
+	}
+
+	allCmds, err := f.AllCmds()
+	if err != nil {
+		panic(err)
+	}
+	wantAllCmds := []store.Cmd{
+		{Text: "shared 1", Seq: 0},
+		{Text: "session 1", Seq: 1}}
+	if !reflect.DeepEqual(allCmds, wantAllCmds) {
+		t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds)
+	}
+}
+
+func TestHybridStore_AddCmd_AddsToSessionEvenIfDBErrors(t *testing.T) {
+	db := NewFaultyInMemoryDB()
+	f := mustNewHybridStore(db)
+	db.SetOneOffError(mockError)
+
+	_, err := f.AddCmd(store.Cmd{Text: "haha"})
+	if err != mockError {
+		t.Errorf("AddCmd -> error %v, want %v", err, mockError)
+	}
+
+	allCmds, err := f.AllCmds()
+	if err != nil {
+		panic(err)
+	}
+	wantAllCmds := []store.Cmd{{Text: "haha", Seq: 1}}
+	if !reflect.DeepEqual(allCmds, wantAllCmds) {
+		t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds)
+	}
+}
+
+func TestHybridStore_AllCmds_IncludesFrozenSharedAndNewlyAdded(t *testing.T) {
+	db := NewFaultyInMemoryDB("shared 1")
+	f := mustNewHybridStore(db)
+
+	// Simulate adding commands from both the current session and other sessions.
+	f.AddCmd(store.Cmd{Text: "session 1"})
+	db.AddCmd("other session 1")
+	db.AddCmd("other session 2")
+	f.AddCmd(store.Cmd{Text: "session 2"})
+	db.AddCmd("other session 3")
+
+	// AllCmds should return all commands from the storage when the HybridStore
+	// was created, plus session commands. The session commands should have
+	// sequence numbers consistent with the DB.
+	allCmds, err := f.AllCmds()
+	if err != nil {
+		t.Errorf("AllCmds -> error %v, want nil", err)
+	}
+	wantAllCmds := []store.Cmd{
+		{Text: "shared 1", Seq: 0},
+		{Text: "session 1", Seq: 1},
+		{Text: "session 2", Seq: 4}}
+	if !reflect.DeepEqual(allCmds, wantAllCmds) {
+		t.Errorf("AllCmds -> %v, want %v", allCmds, wantAllCmds)
+	}
+}
+
+func TestHybridStore_AllCmds_ReturnsSessionIfDBErrors(t *testing.T) {
+	db := NewFaultyInMemoryDB("shared 1")
+	f := mustNewHybridStore(db)
+	f.AddCmd(store.Cmd{Text: "session 1"})
+	db.SetOneOffError(mockError)
+
+	allCmds, err := f.AllCmds()
+	if err != mockError {
+		t.Errorf("AllCmds -> error %v, want %v", err, mockError)
+	}
+	wantAllCmds := []store.Cmd{{Text: "session 1", Seq: 1}}
+	if !reflect.DeepEqual(allCmds, wantAllCmds) {
+		t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds)
+	}
+}
+
+func TestHybridStore_Cursor_OnlySession(t *testing.T) {
+	db := NewFaultyInMemoryDB()
+	f := mustNewHybridStore(db)
+	db.AddCmd("+ other session")
+	f.AddCmd(store.Cmd{Text: "+ session 1"})
+	f.AddCmd(store.Cmd{Text: "- no match"})
+
+	testCursorIteration(t, f.Cursor("+"), []store.Cmd{{Text: "+ session 1", Seq: 1}})
+}
+
+func TestHybridStore_Cursor_OnlyShared(t *testing.T) {
+	db := NewFaultyInMemoryDB("- no match", "+ shared 1")
+	f := mustNewHybridStore(db)
+	db.AddCmd("+ other session")
+	f.AddCmd(store.Cmd{Text: "- no match"})
+
+	testCursorIteration(t, f.Cursor("+"), []store.Cmd{{Text: "+ shared 1", Seq: 1}})
+}
+
+func TestHybridStore_Cursor_SharedAndSession(t *testing.T) {
+	db := NewFaultyInMemoryDB("- no match", "+ shared 1")
+	f := mustNewHybridStore(db)
+	db.AddCmd("+ other session")
+	db.AddCmd("- no match")
+	f.AddCmd(store.Cmd{Text: "+ session 1"})
+	f.AddCmd(store.Cmd{Text: "- no match"})
+
+	testCursorIteration(t, f.Cursor("+"), []store.Cmd{
+		{Text: "+ shared 1", Seq: 1},
+		{Text: "+ session 1", Seq: 4}})
+}
+
+func testCursorIteration(t *testing.T, cursor Cursor, wantCmds []store.Cmd) {
+	expectEndOfHistory := func() {
+		t.Helper()
+		if _, err := cursor.Get(); err != ErrEndOfHistory {
+			t.Errorf("Get -> error %v, want ErrEndOfHistory", err)
+		}
+	}
+	expectCmd := func(i int) {
+		t.Helper()
+		wantCmd := wantCmds[i]
+		cmd, err := cursor.Get()
+		if cmd != wantCmd {
+			t.Errorf("Get -> %v, want %v", cmd, wantCmd)
+		}
+		if err != nil {
+			t.Errorf("Get -> error %v, want nil", err)
+		}
+	}
+
+	expectEndOfHistory()
+
+	for i := len(wantCmds) - 1; i >= 0; i-- {
+		cursor.Prev()
+		expectCmd(i)
+	}
+
+	cursor.Prev()
+	expectEndOfHistory()
+	cursor.Prev()
+	expectEndOfHistory()
+
+	for i := range wantCmds {
+		cursor.Next()
+		expectCmd(i)
+	}
+
+	cursor.Next()
+	expectEndOfHistory()
+	cursor.Next()
+	expectEndOfHistory()
+}
+
+func mustNewHybridStore(db DB) Store {
+	f, err := NewHybridStore(db)
+	if err != nil {
+		panic(err)
+	}
+	return f
+}

+ 69 - 0
pkg/cli/histutil/mem_store.go

@@ -0,0 +1,69 @@
+package histutil
+
+import (
+	"strings"
+
+	"github.com/elves/elvish/pkg/store"
+)
+
+// NewMemStore returns a Store that stores command history in memory.
+func NewMemStore(texts ...string) Store {
+	cmds := make([]store.Cmd, len(texts))
+	for i, text := range texts {
+		cmds[i] = store.Cmd{Text: text, Seq: i}
+	}
+	return &memStore{cmds}
+}
+
+type memStore struct{ cmds []store.Cmd }
+
+func (s *memStore) AllCmds() ([]store.Cmd, error) {
+	return s.cmds, nil
+}
+
+func (s *memStore) AddCmd(cmd store.Cmd) (int, error) {
+	if cmd.Seq < 0 {
+		cmd.Seq = len(s.cmds) + 1
+	}
+	s.cmds = append(s.cmds, cmd)
+	return cmd.Seq, nil
+}
+
+func (s *memStore) Cursor(prefix string) Cursor {
+	return &memStoreCursor{s.cmds, prefix, len(s.cmds)}
+}
+
+type memStoreCursor struct {
+	cmds   []store.Cmd
+	prefix string
+	index  int
+}
+
+func (c *memStoreCursor) Prev() {
+	if c.index < 0 {
+		return
+	}
+	for c.index--; c.index >= 0; c.index-- {
+		if strings.HasPrefix(c.cmds[c.index].Text, c.prefix) {
+			return
+		}
+	}
+}
+
+func (c *memStoreCursor) Next() {
+	if c.index >= len(c.cmds) {
+		return
+	}
+	for c.index++; c.index < len(c.cmds); c.index++ {
+		if strings.HasPrefix(c.cmds[c.index].Text, c.prefix) {
+			return
+		}
+	}
+}
+
+func (c *memStoreCursor) Get() (store.Cmd, error) {
+	if c.index < 0 || c.index >= len(c.cmds) {
+		return store.Cmd{}, ErrEndOfHistory
+	}
+	return c.cmds[c.index], nil
+}

+ 17 - 0
pkg/cli/histutil/mem_store_test.go

@@ -0,0 +1,17 @@
+package histutil
+
+import (
+	"testing"
+
+	"github.com/elves/elvish/pkg/store"
+)
+
+func TestMemStore_Cursor(t *testing.T) {
+	s := NewMemStore("+ 0", "- 1", "+ 2")
+	testCursorIteration(t, s.Cursor("+"), []store.Cmd{
+		{Text: "+ 0", Seq: 0},
+		{Text: "+ 2", Seq: 2},
+	})
+}
+
+// Remaining methods tested along with HybridStore

+ 0 - 63
pkg/cli/histutil/simple_walker.go

@@ -1,63 +0,0 @@
-package histutil
-
-import "strings"
-
-// A simple implementation of Walker.
-type simpleWalker struct {
-	cmds   []string
-	prefix string
-	index  int
-	// Index of the last occurrence of an element in cmds. Built on demand and
-	// used for skipping duplicate entries.
-	occ map[string]int
-}
-
-// NewSimpleWalker returns a Walker, given the slice of all commands and the prefix.
-func NewSimpleWalker(cmds []string, prefix string) Walker {
-	return &simpleWalker{cmds, prefix, len(cmds), make(map[string]int)}
-}
-
-func (w *simpleWalker) Prefix() string  { return w.prefix }
-func (w *simpleWalker) CurrentSeq() int { return w.index }
-
-func (w *simpleWalker) CurrentCmd() string {
-	if w.index < len(w.cmds) {
-		return w.cmds[w.index]
-	}
-	return ""
-}
-
-func (w *simpleWalker) Prev() error {
-	for i := w.index - 1; i >= 0; i-- {
-		cmd := w.cmds[i]
-		if !strings.HasPrefix(cmd, w.prefix) {
-			continue
-		}
-		j, ok := w.occ[cmd]
-		if j == i || !ok {
-			if !ok {
-				w.occ[cmd] = i
-			}
-			w.index = i
-			return nil
-		}
-	}
-	return ErrEndOfHistory
-}
-
-func (w *simpleWalker) Next() error {
-	if w.index >= len(w.cmds) {
-		return ErrEndOfHistory
-	}
-	for w.index++; w.index < len(w.cmds); w.index++ {
-		cmd := w.cmds[w.index]
-		if !strings.HasPrefix(cmd, w.prefix) {
-			continue
-		}
-		j, ok := w.occ[cmd]
-		if ok && j == w.index {
-			return nil
-		}
-	}
-	return ErrEndOfHistory
-}

+ 0 - 91
pkg/cli/histutil/simple_walker_test.go

@@ -1,91 +0,0 @@
-package histutil
-
-import "testing"
-
-type step struct {
-	f       func(Walker) error
-	wantSeq int
-	wantCmd string
-	wantErr error
-}
-
-var (
-	prev = Walker.Prev
-	next = Walker.Next
-)
-
-var simpleWalkerTests = []struct {
-	prefix string
-	cmds   []string
-	steps  []step
-}{
-	{
-		"",
-		[]string{},
-		[]step{
-			{next, 0, "", ErrEndOfHistory},
-			{prev, 0, "", ErrEndOfHistory},
-		},
-	},
-	{
-		"",
-		//       0        1        2         3        4         5
-		[]string{"ls -l", "ls -l", "echo a", "ls -a", "echo a", "ls a"},
-		[]step{
-			{prev, 5, "ls a", nil},
-			{next, 6, "", ErrEndOfHistory}, // Next does not stop at border
-			{prev, 5, "ls a", nil},
-			{prev, 4, "echo a", nil},
-			{prev, 3, "ls -a", nil},
-			{prev, 1, "ls -l", nil},             // skips 2; dup with 4
-			{prev, 1, "ls -l", ErrEndOfHistory}, // Prev stops at border
-			{next, 3, "ls -a", nil},
-			{next, 4, "echo a", nil},
-			{next, 5, "ls a", nil},
-		},
-	},
-	{
-		"e",
-		//       0         1        2         3        4         5
-		[]string{"echo a", "ls -l", "echo a", "ls -a", "echo a", "ls a"},
-		[]step{
-			{prev, 4, "echo a", nil},
-			{prev, 4, "echo a", ErrEndOfHistory},
-			{next, 6, "", ErrEndOfHistory},
-		},
-	},
-	{
-		"l",
-		//       0         1        2         3        4         5
-		[]string{"echo a", "ls -l", "echo a", "ls -a", "echo a", "ls a"},
-		[]step{
-			{prev, 5, "ls a", nil},
-			{prev, 3, "ls -a", nil},
-			{prev, 1, "ls -l", nil},
-			{prev, 1, "ls -l", ErrEndOfHistory},
-			{next, 3, "ls -a", nil},
-			{next, 5, "ls a", nil},
-			{next, 6, "", ErrEndOfHistory},
-		},
-	},
-}
-
-func TestSimpleWalker(t *testing.T) {
-	for _, test := range simpleWalkerTests {
-		w := NewSimpleWalker(test.cmds, test.prefix)
-		for _, step := range test.steps {
-			err := step.f(w)
-			if err != step.wantErr {
-				t.Errorf("Got error %v, want %v", err, step.wantErr)
-			}
-			seq := w.CurrentSeq()
-			if seq != step.wantSeq {
-				t.Errorf("Got seq %v, want %v", seq, step.wantSeq)
-			}
-			cmd := w.CurrentCmd()
-			if cmd != step.wantCmd {
-				t.Errorf("Got cmd %v, want %v", cmd, step.wantCmd)
-			}
-		}
-	}
-}

+ 21 - 60
pkg/cli/histutil/store.go

@@ -6,8 +6,6 @@ import (
 	"github.com/elves/elvish/pkg/store"
 )
 
-var errStoreIsEmpty = errors.New("store is empty")
-
 // Store is an abstract interface for history store.
 type Store interface {
 	// AddCmd adds a new command history entry and returns its sequence number.
@@ -16,61 +14,24 @@ type Store interface {
 	AddCmd(cmd store.Cmd) (int, error)
 	// AllCmds returns all commands kept in the store.
 	AllCmds() ([]store.Cmd, error)
-	// LastCmd returns the last command in the store.
-	LastCmd() (store.Cmd, error)
-}
-
-// NewMemoryStore returns a Store that stores command history in memory.
-func NewMemoryStore() Store {
-	return &memoryStore{}
-}
-
-type memoryStore struct{ cmds []store.Cmd }
-
-func (s *memoryStore) AllCmds() ([]store.Cmd, error) {
-	return s.cmds, nil
-}
-
-func (s *memoryStore) AddCmd(cmd store.Cmd) (int, error) {
-	s.cmds = append(s.cmds, cmd)
-	return cmd.Seq, nil
-}
-
-func (s *memoryStore) LastCmd() (store.Cmd, error) {
-	if len(s.cmds) == 0 {
-		return store.Cmd{}, errStoreIsEmpty
-	}
-	return s.cmds[len(s.cmds)-1], nil
-}
-
-// NewDBStore returns a Store backed by a database.
-func NewDBStore(db DB) Store {
-	return dbStore{db, -1}
-}
-
-// NewDBStoreFrozen returns a Store backed by a database, with the view of all
-// commands frozen at creation.
-func NewDBStoreFrozen(db DB) (Store, error) {
-	upper, err := db.NextCmdSeq()
-	if err != nil {
-		return nil, err
-	}
-	return dbStore{db, upper}, nil
-}
-
-type dbStore struct {
-	db    DB
-	upper int
-}
-
-func (s dbStore) AllCmds() ([]store.Cmd, error) {
-	return s.db.CmdsWithSeq(0, s.upper)
-}
-
-func (s dbStore) AddCmd(cmd store.Cmd) (int, error) {
-	return s.db.AddCmd(cmd.Text)
-}
-
-func (s dbStore) LastCmd() (store.Cmd, error) {
-	return s.db.PrevCmd(s.upper, "")
-}
+	// Cursor returns a cursor that iterating through commands with the given
+	// prefix. The cursor is initially placed just after the last command in the
+	// store.
+	Cursor(prefix string) Cursor
+}
+
+// Cursor is used to navigate a Store.
+type Cursor interface {
+	// Prev moves the cursor to the previous command.
+	Prev()
+	// Next moves the cursor to the next command.
+	Next()
+	// Get returns the command the cursor is currently at, or any error if the
+	// cursor is in an invalid state. If the cursor is "over the edge", the
+	// error is ErrEndOfHistory.
+	Get() (store.Cmd, error)
+}
+
+// ErrEndOfHistory is returned by Cursor.Get if the cursor is currently over the
+// edge.
+var ErrEndOfHistory = errors.New("end of history")

+ 0 - 120
pkg/cli/histutil/walker.go

@@ -1,120 +0,0 @@
-package histutil
-
-import (
-	"errors"
-	"strings"
-
-	"github.com/elves/elvish/pkg/store"
-)
-
-var ErrEndOfHistory = errors.New("end of history")
-
-// Walker is used for walking through history entries with a given (possibly
-// empty) prefix, skipping duplicates entries.
-type Walker interface {
-	Prefix() string
-	CurrentSeq() int
-	CurrentCmd() string
-	Prev() error
-	Next() error
-}
-
-type walker struct {
-	store       DB
-	storeUpper  int
-	sessionCmds []store.Cmd
-	prefix      string
-
-	// The next element to fetch from the session history. If equal to -1, the
-	// next element comes from the storage backend.
-	sessionIdx int
-	// Index of the next element in the stack that Prev will return on next
-	// call. If equal to len(stack), the next element needs to be fetched,
-	// either from the session history or the storage backend.
-	top     int
-	stack   []string
-	seq     []int
-	inStack map[string]bool
-}
-
-func NewWalker(store DB, upper int, cmds []store.Cmd, prefix string) Walker {
-	return &walker{store, upper, cmds, prefix,
-		len(cmds) - 1, 0, nil, nil, map[string]bool{}}
-}
-
-// Prefix returns the prefix of the commands that the walker walks through.
-func (w *walker) Prefix() string {
-	return w.prefix
-}
-
-// CurrentSeq returns the sequence number of the current entry.
-func (w *walker) CurrentSeq() int {
-	if len(w.seq) > 0 && w.top <= len(w.seq) && w.top > 0 {
-		return w.seq[w.top-1]
-	}
-	return -1
-}
-
-// CurrentSeq returns the content of the current entry.
-func (w *walker) CurrentCmd() string {
-	if len(w.stack) > 0 && w.top <= len(w.stack) && w.top > 0 {
-		return w.stack[w.top-1]
-	}
-	return ""
-}
-
-// Prev walks to the previous matching history entry, skipping all duplicates.
-func (w *walker) Prev() error {
-	// store.Cmd comes from the stack.
-	if w.top < len(w.stack) {
-		w.top++
-		return nil
-	}
-
-	// Find the entry in the session part.
-	for i := w.sessionIdx; i >= 0; i-- {
-		cmd := w.sessionCmds[i]
-		if strings.HasPrefix(cmd.Text, w.prefix) && !w.inStack[cmd.Text] {
-			w.push(cmd.Text, cmd.Seq)
-			w.sessionIdx = i - 1
-			return nil
-		}
-	}
-	// Not found in the session part.
-	w.sessionIdx = -1
-
-	seq := w.storeUpper
-	if len(w.seq) > 0 && seq > w.seq[len(w.seq)-1] {
-		seq = w.seq[len(w.seq)-1]
-	}
-	for {
-		cmd, err := w.store.PrevCmd(seq, w.prefix)
-		seq = cmd.Seq
-		if err != nil {
-			if err.Error() == store.ErrNoMatchingCmd.Error() {
-				err = ErrEndOfHistory
-			}
-			return err
-		}
-		if !w.inStack[cmd.Text] {
-			w.push(cmd.Text, seq)
-			return nil
-		}
-	}
-}
-
-func (w *walker) push(cmd string, seq int) {
-	w.inStack[cmd] = true
-	w.stack = append(w.stack, cmd)
-	w.seq = append(w.seq, seq)
-	w.top++
-}
-
-// Next reverses Prev.
-func (w *walker) Next() error {
-	if w.top <= 1 {
-		return ErrEndOfHistory
-	}
-	w.top--
-	return nil
-}

+ 0 - 130
pkg/cli/histutil/walker_test.go

@@ -1,130 +0,0 @@
-package histutil
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/elves/elvish/pkg/store"
-)
-
-func TestWalker(t *testing.T) {
-	mockError := errors.New("mock error")
-	walkerStore := &TestDB{
-		//              0       1        2         3        4         5
-		AllCmds: []string{"echo", "ls -l", "echo a", "ls -a", "echo a", "ls a"},
-	}
-
-	var w Walker
-	wantCurrent := func(i int, s string) { t.Helper(); checkWalkerCurrent(t, w, i, s) }
-	wantErr := func(e, f error) { t.Helper(); checkError(t, e, f) }
-	wantOK := func(e error) { t.Helper(); checkError(t, e, nil) }
-
-	// Going back and forth.
-	w = NewWalker(walkerStore, -1, nil, "")
-	wantCurrent(-1, "")
-	wantOK(w.Prev())
-	wantCurrent(5, "ls a")
-	wantErr(w.Next(), ErrEndOfHistory)
-	wantCurrent(5, "ls a")
-
-	wantOK(w.Prev())
-	wantCurrent(4, "echo a")
-	wantOK(w.Next())
-	wantCurrent(5, "ls a")
-	wantOK(w.Prev())
-	wantCurrent(4, "echo a")
-
-	wantOK(w.Prev())
-	wantCurrent(3, "ls -a")
-	// "echo a" should be skipped
-	wantOK(w.Prev())
-	wantCurrent(1, "ls -l")
-	wantOK(w.Prev())
-	wantCurrent(0, "echo")
-	wantErr(w.Prev(), ErrEndOfHistory)
-
-	// With an upper bound on the storage.
-	w = NewWalker(walkerStore, 2, nil, "")
-	wantOK(w.Prev())
-	wantCurrent(1, "ls -l")
-	wantOK(w.Prev())
-	wantCurrent(0, "echo")
-	wantErr(w.Prev(), ErrEndOfHistory)
-
-	// Prefix matching 1.
-	w = NewWalker(walkerStore, -1, nil, "echo")
-	if w.Prefix() != "echo" {
-		t.Errorf("got prefix %q, want %q", w.Prefix(), "echo")
-	}
-	wantOK(w.Prev())
-	wantCurrent(4, "echo a")
-	wantOK(w.Prev())
-	wantCurrent(0, "echo")
-	wantErr(w.Prev(), ErrEndOfHistory)
-
-	// Prefix matching 2.
-	w = NewWalker(walkerStore, -1, nil, "ls")
-	wantOK(w.Prev())
-	wantCurrent(5, "ls a")
-	wantOK(w.Prev())
-	wantCurrent(3, "ls -a")
-	wantOK(w.Prev())
-	wantCurrent(1, "ls -l")
-	wantErr(w.Prev(), ErrEndOfHistory)
-
-	// Walker with session history.
-	w = NewWalker(walkerStore, -1,
-		[]store.Cmd{{"ls -l", 7}, {"ls -v", 10}, {"echo haha", 12}}, "ls")
-	wantOK(w.Prev())
-	wantCurrent(10, "ls -v")
-
-	wantOK(w.Prev())
-	wantCurrent(7, "ls -l")
-	wantOK(w.Next())
-	wantCurrent(10, "ls -v")
-	wantOK(w.Prev())
-	wantCurrent(7, "ls -l")
-
-	wantOK(w.Prev())
-	wantCurrent(5, "ls a")
-	wantOK(w.Next())
-	wantCurrent(7, "ls -l")
-	wantOK(w.Prev())
-	wantCurrent(5, "ls a")
-
-	wantOK(w.Prev())
-	wantCurrent(3, "ls -a")
-	wantErr(w.Prev(), ErrEndOfHistory)
-
-	// Backend error.
-	w = NewWalker(walkerStore, -1, nil, "")
-	wantOK(w.Prev())
-	wantCurrent(5, "ls a")
-	wantOK(w.Prev())
-	wantCurrent(4, "echo a")
-	walkerStore.OneOffError = mockError
-	wantErr(w.Prev(), mockError)
-
-	// store.ErrNoMatchingCmd is turned into ErrEndOfHistory.
-	w = NewWalker(walkerStore, -1, nil, "")
-	walkerStore.OneOffError = store.ErrNoMatchingCmd
-	wantErr(w.Prev(), ErrEndOfHistory)
-}
-
-func checkWalkerCurrent(t *testing.T, w Walker, wantSeq int, wantCurrent string) {
-	t.Helper()
-	seq, cmd := w.CurrentSeq(), w.CurrentCmd()
-	if seq != wantSeq {
-		t.Errorf("got seq %d, want %d", seq, wantSeq)
-	}
-	if cmd != wantCurrent {
-		t.Errorf("got cmd %q, want %q", cmd, wantCurrent)
-	}
-}
-
-func checkError(t *testing.T, err, want error) {
-	t.Helper()
-	if err != want {
-		t.Errorf("got err %v, want %v", err, want)
-	}
-}

+ 3 - 2
pkg/edit/config_api.go

@@ -12,6 +12,7 @@ import (
 	"github.com/elves/elvish/pkg/eval/vals"
 	"github.com/elves/elvish/pkg/eval/vars"
 	"github.com/elves/elvish/pkg/parse"
+	"github.com/elves/elvish/pkg/store"
 )
 
 //elvdoc:var max-height
@@ -70,7 +71,7 @@ func initAfterReadline(appSpec *cli.AppSpec, ev *eval.Evaler, ns eval.Ns) {
 // not run. The default value of this list contains a filter which
 // ignores command starts with space.
 
-func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, ns eval.Ns, fuser *histutil.Fuser) {
+func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, ns eval.Ns, s histutil.Store) {
 	ignoreLeadingSpace := eval.NewGoFn("<ignore-cmd-with-leading-space>",
 		func(s string) bool { return !strings.HasPrefix(s, " ") })
 	filters := newListVar(vals.MakeList(ignoreLeadingSpace))
@@ -80,7 +81,7 @@ func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, ns eval.Ns, fuser
 		if code != "" &&
 			callFilters(ev, "$<edit>:add-cmd-filters",
 				filters.Get().(vals.List), code) {
-			fuser.AddCmd(code)
+			s.AddCmd(store.Cmd{Text: code, Seq: -1})
 		}
 		// TODO(xiaq): Handle the error.
 	})

+ 5 - 8
pkg/edit/editor.go

@@ -11,7 +11,6 @@ import (
 	"sync"
 
 	"github.com/elves/elvish/pkg/cli"
-	"github.com/elves/elvish/pkg/cli/histutil"
 	"github.com/elves/elvish/pkg/eval"
 	"github.com/elves/elvish/pkg/eval/vals"
 	"github.com/elves/elvish/pkg/eval/vars"
@@ -43,7 +42,7 @@ func NewEditor(tty cli.TTY, ev *eval.Evaler, st store.Store) *Editor {
 	ed := &Editor{ns: eval.Ns{}, excList: vals.EmptyList}
 	appSpec := cli.AppSpec{TTY: tty}
 
-	fuser, err := histutil.NewFuser(st)
+	hs, err := newHistStore(st)
 	if err != nil {
 		// TODO(xiaq): Report the error.
 	}
@@ -51,19 +50,17 @@ func NewEditor(tty cli.TTY, ev *eval.Evaler, st store.Store) *Editor {
 	initHighlighter(&appSpec, ev)
 	initMaxHeight(&appSpec, ed.ns)
 	initReadlineHooks(&appSpec, ev, ed.ns)
-	if fuser != nil {
-		initAddCmdFilters(&appSpec, ev, ed.ns, fuser)
-	}
+	initAddCmdFilters(&appSpec, ev, ed.ns, hs)
 	initInsertAPI(&appSpec, ed, ev, ed.ns)
 	initPrompts(&appSpec, ed, ev, ed.ns)
 	ed.app = cli.NewApp(appSpec)
 
 	initExceptionsAPI(ed)
 	initCommandAPI(ed, ev)
-	initListings(ed, ev, st, fuser)
+	initListings(ed, ev, st, hs)
 	initNavigation(ed, ev)
 	initCompletion(ed, ev)
-	initHistWalk(ed, ev, fuser)
+	initHistWalk(ed, ev, hs)
 	initInstant(ed, ev)
 	initMinibuf(ed, ev)
 
@@ -71,7 +68,7 @@ func NewEditor(tty cli.TTY, ev *eval.Evaler, st store.Store) *Editor {
 	initTTYBuiltins(ed.app, tty, ed.ns)
 	initMiscBuiltins(ed.app, ed.ns)
 	initStateAPI(ed.app, ed.ns)
-	initStoreAPI(ed.app, ed.ns, fuser)
+	initStoreAPI(ed.app, ed.ns, hs)
 	evalDefaultBinding(ev, ed.ns)
 
 	return ed

+ 47 - 0
pkg/edit/hist_store.go

@@ -0,0 +1,47 @@
+package edit
+
+import (
+	"sync"
+
+	"github.com/elves/elvish/pkg/cli/histutil"
+	"github.com/elves/elvish/pkg/store"
+)
+
+// A wrapper of histutil.Store that is concurrency-safe and supports an
+// additional FastForward method.
+type histStore struct {
+	m  sync.Mutex
+	db store.Store
+	hs histutil.Store
+}
+
+func newHistStore(db store.Store) (*histStore, error) {
+	hs, err := histutil.NewHybridStore(db)
+	return &histStore{db: db, hs: hs}, err
+}
+
+func (s *histStore) AddCmd(cmd store.Cmd) (int, error) {
+	s.m.Lock()
+	defer s.m.Unlock()
+	return s.hs.AddCmd(cmd)
+}
+
+func (s *histStore) AllCmds() ([]store.Cmd, error) {
+	s.m.Lock()
+	defer s.m.Unlock()
+	return s.hs.AllCmds()
+}
+
+func (s *histStore) Cursor(prefix string) histutil.Cursor {
+	s.m.Lock()
+	defer s.m.Unlock()
+	return s.hs.Cursor(prefix)
+}
+
+func (s *histStore) FastForward() error {
+	s.m.Lock()
+	defer s.m.Unlock()
+	hs, err := histutil.NewHybridStore(s.db)
+	s.hs = hs
+	return err
+}

+ 6 - 6
pkg/edit/histwalk.go

@@ -12,7 +12,7 @@ import (
 // Import command history entries that happened after the current session
 // started.
 
-func initHistWalk(ed *Editor, ev *eval.Evaler, fuser *histutil.Fuser) {
+func initHistWalk(ed *Editor, ev *eval.Evaler, hs *histStore) {
 	bindingVar := newBindingVar(EmptyBindingMap)
 	binding := newMapBinding(ed, ev, bindingVar)
 	app := ed.app
@@ -20,7 +20,7 @@ func initHistWalk(ed *Editor, ev *eval.Evaler, fuser *histutil.Fuser) {
 		eval.Ns{
 			"binding": bindingVar,
 		}.AddGoFns("<edit:history>", map[string]interface{}{
-			"start": func() { histWalkStart(app, fuser, binding) },
+			"start": func() { histWalkStart(app, hs, binding) },
 			"up":    func() { notifyIfError(app, histwalk.Prev(app)) },
 			"down":  func() { notifyIfError(app, histwalk.Next(app)) },
 			"down-or-quit": func() {
@@ -34,14 +34,14 @@ func initHistWalk(ed *Editor, ev *eval.Evaler, fuser *histutil.Fuser) {
 			"accept": func() { histwalk.Accept(app) },
 			"close":  func() { histwalk.Close(app) },
 
-			"fast-forward": fuser.FastForward,
+			"fast-forward": hs.FastForward,
 		}))
 }
 
-func histWalkStart(app cli.App, fuser *histutil.Fuser, binding cli.Handler) {
+func histWalkStart(app cli.App, hs *histStore, binding cli.Handler) {
 	buf := app.CodeArea().CopyState().Buffer
-	walker := fuser.Walker(buf.Content[:buf.Dot])
-	histwalk.Start(app, histwalk.Config{Binding: binding, Walker: walker})
+	histwalk.Start(app, histwalk.Config{
+		Binding: binding, Store: hs, Prefix: buf.Content[:buf.Dot]})
 }
 
 func notifyIfError(app cli.App, err error) {

+ 1 - 17
pkg/edit/listing.go

@@ -15,7 +15,7 @@ import (
 	"github.com/xiaq/persistent/hashmap"
 )
 
-func initListings(ed *Editor, ev *eval.Evaler, st store.Store, fuser *histutil.Fuser) {
+func initListings(ed *Editor, ev *eval.Evaler, st store.Store, histStore histutil.Store) {
 	bindingVar := newBindingVar(EmptyBindingMap)
 	app := ed.app
 	ed.ns.AddNs("listing",
@@ -39,11 +39,6 @@ func initListings(ed *Editor, ev *eval.Evaler, st store.Store, fuser *histutil.F
 			*/
 		}))
 
-	var histStore histutil.Store
-	if fuser != nil {
-		histStore = fuserWrapper{fuser}
-	}
-
 	initHistlist(ed, ev, histStore, bindingVar)
 	initLastcmd(ed, ev, histStore, bindingVar)
 	initLocation(ed, ev, st, bindingVar)
@@ -256,17 +251,6 @@ func adaptToIterateStringPair(variable vars.Var) func(func(string, string) bool)
 	}
 }
 
-// Wraps the histutil.Fuser interface to implement histutil.Store. This is a
-// bandaid as we cannot change the implementation of Fuser without breaking its
-// other users. Eventually Fuser should implement Store directly.
-type fuserWrapper struct {
-	*histutil.Fuser
-}
-
-func (f fuserWrapper) AddCmd(cmd store.Cmd) (int, error) {
-	return f.Fuser.AddCmd(cmd.Text)
-}
-
 // Wraps an Evaler to implement the cli.DirStore interface.
 type dirStore struct {
 	ev *eval.Evaler

+ 6 - 7
pkg/edit/store_api.go

@@ -26,7 +26,7 @@ var errStoreOffline = errors.New("store offline")
 // edit:command-history | put [(all)][-1][cmd]
 // ```
 
-func commandHistory(fuser *histutil.Fuser, ch chan<- interface{}) error {
+func commandHistory(fuser histutil.Store, ch chan<- interface{}) error {
 	if fuser == nil {
 		return errStoreOffline
 	}
@@ -44,11 +44,10 @@ func commandHistory(fuser *histutil.Fuser, ch chan<- interface{}) error {
 //
 // Inserts the last word of the last command.
 
-func insertLastWord(app cli.App, fuser *histutil.Fuser) error {
-	if fuser == nil {
-		return errStoreOffline
-	}
-	cmd, err := fuser.LastCmd()
+func insertLastWord(app cli.App, histStore histutil.Store) error {
+	c := histStore.Cursor("")
+	c.Prev()
+	cmd, err := c.Get()
 	if err != nil {
 		return err
 	}
@@ -61,7 +60,7 @@ func insertLastWord(app cli.App, fuser *histutil.Fuser) error {
 	return nil
 }
 
-func initStoreAPI(app cli.App, ns eval.Ns, fuser *histutil.Fuser) {
+func initStoreAPI(app cli.App, ns eval.Ns, fuser histutil.Store) {
 	ns.AddGoFns("<edit>", map[string]interface{}{
 		"command-history": func(fm *eval.Frame) error {
 			return commandHistory(fuser, fm.OutputChan())