diff --git a/internal/history/history.go b/internal/history/history.go index 8574f44..7ee40be 100644 --- a/internal/history/history.go +++ b/internal/history/history.go @@ -98,3 +98,19 @@ func (h *History) Count(project string) (int, error) { } return n, nil } + +// Counts returns the number of Visit lines recorded for every Project in +// one read of History, for the Picker's preview. Malformed lines are +// skipped silently. +func (h *History) Counts() (map[string]int, error) { + visits, err := readVisits(h.path) + if err != nil { + return nil, fmt.Errorf("history: counts: %w", err) + } + + counts := make(map[string]int, len(visits)) + for _, v := range visits { + counts[v.Project]++ + } + return counts, nil +} diff --git a/internal/history/history_test.go b/internal/history/history_test.go index cdff86d..584ab0b 100644 --- a/internal/history/history_test.go +++ b/internal/history/history_test.go @@ -138,6 +138,42 @@ func TestCount_PerProject(t *testing.T) { } } +func TestCounts_AllProjectsInOneRead(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "history") + + h, err := history.Open(path, 1000) + if err != nil { + t.Fatalf("Open: unexpected error: %v", err) + } + + for range 3 { + if err := h.Record("tools/cdd"); err != nil { + t.Fatalf("Record: unexpected error: %v", err) + } + } + if err := h.Record("tools/other"); err != nil { + t.Fatalf("Record: unexpected error: %v", err) + } + + counts, err := h.Counts() + if err != nil { + t.Fatalf("Counts: unexpected error: %v", err) + } + want := map[string]int{"tools/cdd": 3, "tools/other": 1} + if len(counts) != len(want) { + t.Fatalf("Counts: got %d Projects, want %d", len(counts), len(want)) + } + for project, n := range want { + if counts[project] != n { + t.Errorf("Counts[%q] = %d, want %d", project, counts[project], n) + } + } + if counts["never/visited"] != 0 { + t.Errorf("Counts of an unvisited Project = %d, want 0", counts["never/visited"]) + } +} + func TestReadVisits_SkipsMalformedLines(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "history") diff --git a/internal/jump/jump.go b/internal/jump/jump.go index 8434851..52f3c84 100644 --- a/internal/jump/jump.go +++ b/internal/jump/jump.go @@ -42,7 +42,12 @@ func Resolve(ctx context.Context, cfg config.Config, hist *history.History, quer return "", fmt.Errorf("jump: %w", err) } - rel, abs, err := choose(cfg, projects, latest, query, pick) + counts, err := hist.Counts() + if err != nil { + return "", fmt.Errorf("jump: %w", err) + } + + rel, abs, err := choose(cfg, projects, latest, counts, query, pick) if err != nil { return "", err } @@ -60,12 +65,12 @@ func Resolve(ctx context.Context, cfg config.Config, hist *history.History, quer // choose picks a Project either via the exact-match shortcut or by running // the Picker, and returns its Rel and absolute path. -func choose(cfg config.Config, projects []project.Project, latest []history.Visit, query string, pick PickFunc) (rel, abs string, err error) { +func choose(cfg config.Config, projects []project.Project, latest []history.Visit, counts map[string]int, query string, pick PickFunc) (rel, abs string, err error) { if p, ok := exactMatch(projects, query); ok { return p.Rel(), p.Abs(cfg.Root), nil } - rows := order(projects, latest, cfg.Root) + rows := order(projects, latest, counts, cfg.Root) status := func(c context.Context, dir string) git.Status { s, _ := git.GetStatus(c, dir) return s diff --git a/internal/jump/order.go b/internal/jump/order.go index b55a619..23b4196 100644 --- a/internal/jump/order.go +++ b/internal/jump/order.go @@ -16,9 +16,10 @@ import ( // Visits: Projects with a Visit come first, newest Visit first, ties break // alphabetically by Rel; never-visited Projects follow, alphabetically. A // Visit for a Project not present in projects (a Stale Visit) contributes -// no row. order is a pure function: it does not touch the filesystem or +// no row. counts supplies each Project's Visit total for the preview and +// may be nil. order is a pure function: it does not touch the filesystem or // History itself. -func order(projects []project.Project, latest []history.Visit, root string) []picker.Row { +func order(projects []project.Project, latest []history.Visit, counts map[string]int, root string) []picker.Row { lastVisit := make(map[string]time.Time, len(latest)) for _, v := range latest { lastVisit[v.Project] = v.At @@ -49,6 +50,7 @@ func order(projects []project.Project, latest []history.Visit, root string) []pi Path: p.Abs(root), }, LastVisit: lastVisit[p.Rel()], + Visits: counts[p.Rel()], } } return rows diff --git a/internal/jump/order_test.go b/internal/jump/order_test.go index b37172c..ac27544 100644 --- a/internal/jump/order_test.go +++ b/internal/jump/order_test.go @@ -27,7 +27,7 @@ func TestOrder_VisitedFirstNewestThenNeverVisited(t *testing.T) { {Project: "oss/lib", At: time.Unix(1000, 0)}, } - got := order(projects, latest, "/root") + got := order(projects, latest, nil, "/root") want := []string{ "work/api", // newest Visit @@ -54,7 +54,7 @@ func TestOrder_TiesBreakAlphabetically(t *testing.T) { {Project: "tools/alpha", At: tie}, } - got := order(projects, latest, "/root") + got := order(projects, latest, nil, "/root") want := []string{"tools/alpha", "tools/zeta"} assertRowOrder(t, got, want) @@ -69,7 +69,7 @@ func TestOrder_NeverVisitedAlphabetical(t *testing.T) { {Kind: "tools", Name: "alpha"}, } - got := order(projects, nil, "/root") + got := order(projects, nil, nil, "/root") want := []string{"archive/old", "tools/alpha", "tools/zeta"} assertRowOrder(t, got, want) @@ -88,7 +88,7 @@ func TestOrder_StaleVisitContributesNoRow(t *testing.T) { {Project: "gone/vanished", At: time.Unix(9000, 0)}, } - got := order(projects, latest, "/root") + got := order(projects, latest, nil, "/root") if len(got) != 1 { t.Fatalf("order: got %d rows, want 1", len(got)) @@ -113,3 +113,23 @@ func assertRowOrder(t *testing.T, got []picker.Row, want []string) { } } } + +// TestOrder_FillsVisitCounts checks that each row carries the Project's +// Visit total from counts, and zero for a Project counts does not know. +func TestOrder_FillsVisitCounts(t *testing.T) { + projects := []project.Project{ + {Kind: "tools", Name: "cdd"}, + {Kind: "tools", Name: "dotfiles"}, + } + latest := []history.Visit{{Project: "tools/cdd", At: time.Unix(3000, 0)}} + counts := map[string]int{"tools/cdd": 4} + + got := order(projects, latest, counts, "/root") + + if got[0].Visits != 4 { + t.Errorf("tools/cdd Visits = %d, want 4", got[0].Visits) + } + if got[1].Visits != 0 { + t.Errorf("tools/dotfiles Visits = %d, want 0", got[1].Visits) + } +}