From a0d3d5c77780aac4d18057e50496a037ff512150 Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Fri, 27 Oct 2023 06:13:31 -0400 Subject: [PATCH] feat: search based on location + tests --- pkg/mux/screen/replay/replay_test.go | 46 ++++++++- pkg/mux/screen/replay/screen.go | 96 +----------------- pkg/mux/screen/replay/search.go | 146 +++++++++++++++++++++++++++ pkg/mux/screen/replay/time.go | 22 ---- pkg/mux/screen/replay/view.go | 6 +- pkg/sessions/search/module.go | 31 ++---- 6 files changed, 203 insertions(+), 144 deletions(-) create mode 100644 pkg/mux/screen/replay/search.go diff --git a/pkg/mux/screen/replay/replay_test.go b/pkg/mux/screen/replay/replay_test.go index 394e8194..85c3b034 100644 --- a/pkg/mux/screen/replay/replay_test.go +++ b/pkg/mux/screen/replay/replay_test.go @@ -66,9 +66,49 @@ func createTest(events []sessions.Event) (*Replay, func(msgs ...interface{})) { } func TestSearch(t *testing.T) { - r, i := createTest(createTestSession()) - i(ActionBeginning, ActionSearchForward, "test", "enter") - require.Equal(t, 2, len(r.matches)) + s := sessions.NewSimulator(). + Add( + // TODO(cfoust): 10/27/23 why does R=2 lead to fewer matches? + geom.Size{R: 10, C: 10}, + "foo", + "bar blah", // 2 + "foo", + "foo", + "foo", + "foo", + "bar", // 7 + "foo", + "bar", // 9 + "foo", + ) + + r, i := createTest(s.Events()) + i(ActionBeginning, ActionSearchForward, "bar", "enter") + require.Equal(t, 3, len(r.matches)) + require.Equal(t, 2, r.location.Index) + i(ActionSearchAgain) + require.Equal(t, 7, r.location.Index) + i(ActionSearchReverse) + require.Equal(t, 2, r.location.Index) + // Loop back from beginning + i(ActionSearchReverse) + require.Equal(t, 9, r.location.Index) + // Loop over end + i(ActionSearchAgain) + require.Equal(t, 2, r.location.Index) + + // Ensure that the -1 rewriting works + r.gotoIndex(2, -1) + i(ActionSearchReverse) + require.Equal(t, 2, r.location.Index) + + // Now search backward + i(ActionEnd, ActionSearchBackward, "bar", "enter") + require.Equal(t, 9, r.location.Index) + i(ActionSearchAgain) + require.Equal(t, 7, r.location.Index) + i(ActionSearchReverse) + require.Equal(t, 9, r.location.Index) } func TestIndex(t *testing.T) { diff --git a/pkg/mux/screen/replay/screen.go b/pkg/mux/screen/replay/screen.go index 7f7a9e41..c5a74b28 100644 --- a/pkg/mux/screen/replay/screen.go +++ b/pkg/mux/screen/replay/screen.go @@ -64,7 +64,6 @@ type Replay struct { isForward bool isWaiting bool searchInput textinput.Model - matchIndex int matches []search.SearchResult } @@ -156,93 +155,11 @@ func (r *Replay) Update(msg tea.Msg) (taro.Model, tea.Cmd) { }, ) case SearchResultEvent: - r.isWaiting = false - - // TODO(cfoust): 10/13/23 handle error - - matches := msg.results - if len(matches) == 0 { - r.matches = matches - return r, nil - } - - origin := msg.origin - - if msg.isForward { - startIndex := 0 - for i, match := range matches { - begin := match.Begin - if begin.Index >= origin.Index && begin.Offset > origin.Offset { - startIndex = i - break - } - } - - matches = append(matches[startIndex:], matches[:startIndex]...) - } else { - reverse(matches) - - startIndex := 0 - for i, match := range matches { - begin := match.Begin - if begin.Index <= origin.Index && begin.Offset <= origin.Offset { - startIndex = i - break - } - } - - matches = append(matches[startIndex:], matches[:startIndex]...) - } - - r.matches = matches - - if len(r.matches) > 0 { - r.gotoMatch(0) - } - - return r, nil + return r.handleSearchResult(msg) } if r.isSearching { - switch msg := msg.(type) { - case ActionEvent: - switch msg.Type { - case ActionQuit: - r.isSearching = false - return r, nil - } - case taro.KeyMsg: - switch msg.Type { - case taro.KeyEnter: - value := r.searchInput.Value() - - r.searchInput.Reset() - r.isWaiting = true - r.isSearching = false - r.matches = make([]search.SearchResult, 0) - - location := r.location - isForward := r.isForward - events := r.events - - return r, func() tea.Msg { - res, err := search.Search(events, value) - return SearchResultEvent{ - isForward: isForward, - origin: location, - results: res, - err: err, - } - } - } - } - var cmd tea.Cmd - inputMsg := msg - if key, ok := msg.(taro.KeyMsg); ok { - inputMsg = key.ToTea() - } - r.searchInput, cmd = r.searchInput.Update(inputMsg) - return r, cmd + return r.handleSearchInput(msg) } // These events do not stop playback @@ -308,14 +225,7 @@ func (r *Replay) Update(msg tea.Msg) (taro.Model, tea.Cmd) { r.gotoIndex(-1, -1) } case ActionSearchAgain, ActionSearchReverse: - delta := 1 - if msg.Type == ActionSearchReverse { - delta = -1 - } - - if !r.isSelectionMode { - r.gotoMatchDelta(delta) - } + r.searchAgain(msg.Type != ActionSearchReverse) case ActionSearchForward, ActionSearchBackward: r.isSearching = true r.isForward = msg.Type == ActionSearchForward diff --git a/pkg/mux/screen/replay/search.go b/pkg/mux/screen/replay/search.go new file mode 100644 index 00000000..08836f7f --- /dev/null +++ b/pkg/mux/screen/replay/search.go @@ -0,0 +1,146 @@ +package replay + +import ( + "github.com/cfoust/cy/pkg/geom" + P "github.com/cfoust/cy/pkg/io/protocol" + "github.com/cfoust/cy/pkg/sessions/search" + "github.com/cfoust/cy/pkg/taro" + + tea "github.com/charmbracelet/bubbletea" +) + +func (r *Replay) gotoMatch(index int) { + if len(r.matches) == 0 { + return + } + + index = geom.Clamp(index, 0, len(r.matches)-1) + match := r.matches[index].Begin + r.gotoIndex(match.Index, match.Offset) +} + +func (r *Replay) searchAgain(isForward bool) { + if r.isSelectionMode { + return + } + + matches := r.matches + if len(matches) == 0 { + return + } + + if !r.isForward { + isForward = !isForward + } + + location := r.location + + firstMatch := matches[0].Begin + lastMatch := matches[len(matches) - 1].Begin + + if !isForward && (location.Before(firstMatch) || location.Equal(firstMatch)) { + location.Index = len(r.events) - 1 + location.Offset = -1 + } + + // In order for the comparison to work, we have to turn our special -1 + // offset into a real value + if location.Offset == -1 { + event := r.events[location.Index] + if output, ok := event.Message.(P.OutputMessage); ok { + location.Offset = len(output.Data) - 1 + } + } + + if isForward && (location.After(lastMatch) || location.Equal(lastMatch)) { + location.Index = 0 + location.Offset = -1 + } + + var initialIndex int + var other search.Address + if isForward { + for i, match := range matches { + other = match.Begin + if location.After(other) || location.Equal(other) { + continue + } + initialIndex = i + break + } + } else { + for i := len(matches) - 1; i >= 0; i-- { + other = matches[i].Begin + if location.Before(other) || location.Equal(other) { + continue + } + initialIndex = i + break + } + } + + r.gotoMatch(initialIndex) +} + +func (r *Replay) handleSearchResult(msg SearchResultEvent) (taro.Model, tea.Cmd) { + r.isWaiting = false + + // TODO(cfoust): 10/13/23 handle error + + matches := msg.results + if len(matches) == 0 { + r.matches = matches + return r, nil + } + + // TODO(cfoust): 10/27/23 y tho + reverse(matches) + + r.matches = matches + r.location = msg.origin + r.isForward = msg.isForward + r.searchAgain(true) + return r, nil +} + +func (r *Replay) handleSearchInput(msg tea.Msg) (taro.Model, tea.Cmd) { + switch msg := msg.(type) { + case ActionEvent: + switch msg.Type { + case ActionQuit: + r.isSearching = false + return r, nil + } + case taro.KeyMsg: + switch msg.Type { + case taro.KeyEnter: + value := r.searchInput.Value() + + r.searchInput.Reset() + r.isWaiting = true + r.isSearching = false + r.matches = make([]search.SearchResult, 0) + + location := r.location + isForward := r.isForward + events := r.events + + return r, func() tea.Msg { + res, err := search.Search(events, value) + return SearchResultEvent{ + isForward: isForward, + origin: location, + results: res, + err: err, + } + } + } + } + var cmd tea.Cmd + inputMsg := msg + if key, ok := msg.(taro.KeyMsg); ok { + inputMsg = key.ToTea() + } + r.searchInput, cmd = r.searchInput.Update(inputMsg) + return r, cmd +} diff --git a/pkg/mux/screen/replay/time.go b/pkg/mux/screen/replay/time.go index 4d381da3..86c891eb 100644 --- a/pkg/mux/screen/replay/time.go +++ b/pkg/mux/screen/replay/time.go @@ -88,28 +88,6 @@ func (r *Replay) gotoIndex(index, indexByte int) { r.setIndex(index, indexByte, true) } -func (r *Replay) gotoMatch(index int) { - if len(r.matches) == 0 { - return - } - - index = geom.Clamp(index, 0, len(r.matches)-1) - r.matchIndex = index - match := r.matches[index].Begin - r.gotoIndex(match.Index, match.Offset) -} - -func (r *Replay) gotoMatchDelta(delta int) { - numMatches := len(r.matches) - if numMatches == 0 { - return - } - - // Python-esque modulo behavior - index := (((r.matchIndex + delta) % numMatches) + numMatches) % numMatches - r.gotoMatch(index) -} - func (r *Replay) scheduleUpdate() (taro.Model, tea.Cmd) { since := time.Now() return r, func() tea.Msg { diff --git a/pkg/mux/screen/replay/view.go b/pkg/mux/screen/replay/view.go index acb7a675..0bef003e 100644 --- a/pkg/mux/screen/replay/view.go +++ b/pkg/mux/screen/replay/view.go @@ -18,13 +18,13 @@ func (r *Replay) drawMatches(state *tty.State) { } location := r.location - for i, match := range matches { + for _, match := range matches { // This match is not on the screen - if location.Compare(match.Begin) < 0 || location.Compare(match.End) >= 0 { + if location.Before(match.Begin) || location.After(match.End) { continue } - isSelected := i == r.matchIndex && location.Compare(match.Begin) == 0 + isSelected := location.Equal(match.Begin) from := r.termToViewport(match.From) to := r.termToViewport(match.To) if !r.isInViewport(from) || !r.isInViewport(to) { diff --git a/pkg/sessions/search/module.go b/pkg/sessions/search/module.go index c5b223b1..d01c18e9 100644 --- a/pkg/sessions/search/module.go +++ b/pkg/sessions/search/module.go @@ -19,31 +19,16 @@ type Address struct { Offset int } -// TODO(cfoust): 10/23/23 make this Before(), After(), Equal() to mimic Time API -func (a Address) Compare(other Address) int { - if a == other { - return 0 - } - - if a.Index < other.Index { - return -1 - } - - if a.Index > other.Index { - return 1 - } - - // index must be equal - - if a.Offset < other.Offset { - return -1 - } +func (a Address) Before(other Address) bool { + return a.Index < other.Index || (a.Index == other.Index && a.Offset < other.Offset) +} - if a.Offset > other.Offset { - return 1 - } +func (a Address) After(other Address) bool { + return a.Index > other.Index || (a.Index == other.Index && a.Offset > other.Offset) +} - return 0 +func (a Address) Equal(other Address) bool { + return a.Index == other.Index && a.Offset == other.Offset } type SearchResult struct {