Skip to content

Commit cb4edfb

Browse files
committed
Merge branch 'filter-shares-search-history'
Fixes: #466
2 parents 9544d76 + 46adb4e commit cb4edfb

8 files changed

Lines changed: 333 additions & 44 deletions

‎internal/history-navigator.go‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package internal
2+
3+
// HistoryNavigator lets an InputBox-driven prompt (search, filter, ...) cycle
4+
// up and down through a shared SearchHistory without losing whatever text the
5+
// user is in the middle of typing.
6+
type HistoryNavigator struct {
7+
history *SearchHistory
8+
index int
9+
lastTypedText string
10+
}
11+
12+
// NewHistoryNavigator returns a HistoryNavigator positioned past the end of
13+
// history's entries, i.e. it will show whatever the user types rather than a
14+
// history entry until Move is called.
15+
func NewHistoryNavigator(history *SearchHistory) *HistoryNavigator {
16+
return &HistoryNavigator{
17+
history: history,
18+
index: len(history.entries),
19+
}
20+
}
21+
22+
// TextEdited must be called whenever the user changes the input box text by
23+
// hand, so that arrowing away and back finds that text again.
24+
func (n *HistoryNavigator) TextEdited(text string) {
25+
n.index = len(n.history.entries)
26+
n.lastTypedText = text
27+
}
28+
29+
// Move steps the history index by delta (negative towards older entries,
30+
// positive towards newer), clamping at both ends. It returns the text that
31+
// should now be shown, and whether there was anything to move to at all (an
32+
// empty history has nothing to navigate).
33+
func (n *HistoryNavigator) Move(delta int) (string, bool) {
34+
if len(n.history.entries) == 0 {
35+
return "", false
36+
}
37+
38+
n.index += delta
39+
if n.index < 0 {
40+
n.index = 0
41+
}
42+
if n.index > len(n.history.entries) {
43+
n.index = len(n.history.entries)
44+
}
45+
46+
if n.index == len(n.history.entries) {
47+
return n.lastTypedText, true
48+
}
49+
50+
return n.history.entries[n.index], true
51+
}
52+
53+
// Commit adds text as a new history entry. Call this when the user submits
54+
// or aborts the prompt.
55+
func (n *HistoryNavigator) Commit(text string) {
56+
n.history.addEntry(text)
57+
}

‎internal/history-navigator_test.go‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
)
8+
9+
func TestHistoryNavigator_MoveCyclesThroughEntriesNewestFirst(t *testing.T) {
10+
history := &SearchHistory{entries: []string{"a", "b", "c"}}
11+
navigator := NewHistoryNavigator(history)
12+
13+
text, ok := navigator.Move(-1)
14+
assert.Assert(t, ok)
15+
assert.Equal(t, "c", text)
16+
17+
text, ok = navigator.Move(-1)
18+
assert.Assert(t, ok)
19+
assert.Equal(t, "b", text)
20+
21+
text, ok = navigator.Move(-1)
22+
assert.Assert(t, ok)
23+
assert.Equal(t, "a", text)
24+
}
25+
26+
func TestHistoryNavigator_MoveClampsAtOldestEntry(t *testing.T) {
27+
history := &SearchHistory{entries: []string{"a", "b"}}
28+
navigator := NewHistoryNavigator(history)
29+
30+
navigator.Move(-1)
31+
navigator.Move(-1)
32+
text, ok := navigator.Move(-1)
33+
34+
assert.Assert(t, ok)
35+
assert.Equal(t, "a", text)
36+
}
37+
38+
func TestHistoryNavigator_MoveClampsAtNewestEntry(t *testing.T) {
39+
history := &SearchHistory{entries: []string{"a", "b"}}
40+
navigator := NewHistoryNavigator(history)
41+
42+
navigator.Move(-1)
43+
navigator.Move(1)
44+
text, ok := navigator.Move(1)
45+
46+
assert.Assert(t, ok)
47+
assert.Equal(t, "", text)
48+
}
49+
50+
func TestHistoryNavigator_MoveOnEmptyHistoryDoesNothing(t *testing.T) {
51+
history := &SearchHistory{}
52+
navigator := NewHistoryNavigator(history)
53+
54+
_, ok := navigator.Move(-1)
55+
56+
assert.Assert(t, !ok)
57+
}
58+
59+
func TestHistoryNavigator_TypedTextSurvivesUpThenDown(t *testing.T) {
60+
history := &SearchHistory{entries: []string{"a", "b"}}
61+
navigator := NewHistoryNavigator(history)
62+
63+
navigator.TextEdited("typing this")
64+
65+
text, ok := navigator.Move(-1)
66+
assert.Assert(t, ok)
67+
assert.Equal(t, "b", text)
68+
69+
text, ok = navigator.Move(1)
70+
assert.Assert(t, ok)
71+
assert.Equal(t, "typing this", text)
72+
}
73+
74+
func TestHistoryNavigator_MoveStepsThroughEveryEntryBothWays(t *testing.T) {
75+
history := &SearchHistory{entries: []string{"a", "b", "c"}}
76+
navigator := NewHistoryNavigator(history)
77+
78+
text, ok := navigator.Move(-1)
79+
assert.Assert(t, ok)
80+
assert.Equal(t, "c", text)
81+
82+
text, ok = navigator.Move(-1)
83+
assert.Assert(t, ok)
84+
assert.Equal(t, "b", text)
85+
86+
text, ok = navigator.Move(-1)
87+
assert.Assert(t, ok)
88+
assert.Equal(t, "a", text)
89+
90+
text, ok = navigator.Move(1)
91+
assert.Assert(t, ok)
92+
assert.Equal(t, "b", text)
93+
94+
text, ok = navigator.Move(1)
95+
assert.Assert(t, ok)
96+
assert.Equal(t, "c", text)
97+
98+
text, ok = navigator.Move(1)
99+
assert.Assert(t, ok)
100+
assert.Equal(t, "", text)
101+
}
102+
103+
func TestHistoryNavigator_CommitAddsEntryToHistory(t *testing.T) {
104+
history := &SearchHistory{entries: []string{"a"}}
105+
navigator := NewHistoryNavigator(history)
106+
107+
navigator.Commit("b")
108+
109+
assert.DeepEqual(t, []string{"a", "b"}, history.entries)
110+
}

‎internal/pagermode-filter.go‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import (
77
)
88

99
type PagerModeFilter struct {
10-
pager *Pager
11-
inputBox *InputBox
10+
pager *Pager
11+
initialScrollPosition scrollPosition // Pager position before filtering started
12+
inputBox *InputBox
13+
history *HistoryNavigator
1214
}
1315

14-
func NewPagerModeFilter(p *Pager) *PagerModeFilter {
16+
func NewPagerModeFilter(p *Pager, initialScrollPosition scrollPosition) *PagerModeFilter {
1517
m := &PagerModeFilter{
16-
pager: p,
18+
pager: p,
19+
initialScrollPosition: initialScrollPosition,
20+
history: NewHistoryNavigator(p.searchHistory),
1721
}
1822
m.inputBox = &InputBox{
1923
accept: INPUTBOX_ACCEPT_ALL,
@@ -35,21 +39,36 @@ func (m *PagerModeFilter) updateFilterPattern(text string) {
3539

3640
func (m *PagerModeFilter) onKey(key twin.KeyCode) {
3741
if m.inputBox.handleKey(key) {
42+
m.history.TextEdited(m.inputBox.text)
3843
return
3944
}
4045

4146
switch key {
4247
case twin.KeyEnter:
48+
m.history.Commit(m.inputBox.text)
4349
m.pager.mode = PagerModeViewing{pager: m.pager}
4450

4551
case twin.KeyEscape:
52+
m.history.Commit(m.inputBox.text)
4653
m.pager.mode = PagerModeViewing{pager: m.pager}
4754
m.pager.filter = search.Search{}
4855
m.pager.search.Clear()
56+
m.pager.scrollPosition = m.initialScrollPosition
4957

50-
case twin.KeyUp, twin.KeyDown, twin.KeyPgUp, twin.KeyPgDown:
51-
viewing := PagerModeViewing{pager: m.pager}
52-
viewing.onKey(key)
58+
case twin.KeyUp:
59+
if text, ok := m.history.Move(-1); ok {
60+
m.inputBox.setText(text)
61+
}
62+
63+
case twin.KeyDown:
64+
if text, ok := m.history.Move(1); ok {
65+
m.inputBox.setText(text)
66+
}
67+
68+
case twin.KeyPgUp, twin.KeyPgDown:
69+
m.history.Commit(m.inputBox.text)
70+
m.pager.mode = PagerModeViewing{pager: m.pager}
71+
m.pager.mode.onKey(key)
5372

5473
default:
5574
log.Debugf("Unhandled filter key event %v", key)
@@ -58,4 +77,5 @@ func (m *PagerModeFilter) onKey(key twin.KeyCode) {
5877

5978
func (m *PagerModeFilter) onRune(char rune) {
6079
m.inputBox.handleRune(char)
80+
m.history.TextEdited(m.inputBox.text)
6181
}

‎internal/pagermode-filter_test.go‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"github.com/walles/moor/v2/internal/linemetadata"
7+
"github.com/walles/twin"
8+
"gotest.tools/v3/assert"
9+
)
10+
11+
// Ref: https://github.com/walles/moor/issues/466
12+
func TestPagerModeFilter_UpArrowShowsMostRecentSearchHistoryEntry(t *testing.T) {
13+
pager := createThreeLinesPager(t)
14+
pager.searchHistory.entries = []string{"b", "d"}
15+
16+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
17+
filterMode.onKey(twin.KeyUp)
18+
19+
assert.Equal(t, "d", filterMode.inputBox.text)
20+
}
21+
22+
func TestPagerModeFilter_DownArrowStepsBackTowardsNewestEntry(t *testing.T) {
23+
pager := createThreeLinesPager(t)
24+
pager.searchHistory.entries = []string{"b", "d"}
25+
26+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
27+
filterMode.onKey(twin.KeyUp)
28+
filterMode.onKey(twin.KeyUp)
29+
filterMode.onKey(twin.KeyDown)
30+
31+
assert.Equal(t, "d", filterMode.inputBox.text)
32+
}
33+
34+
func TestPagerModeFilter_EnterPersistsTypedTextToSearchHistory(t *testing.T) {
35+
pager := createThreeLinesPager(t)
36+
pager.searchHistory = &SearchHistory{} // No file backing, keep this test disk-free
37+
38+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
39+
filterMode.inputBox.setText("abc")
40+
filterMode.onKey(twin.KeyEnter)
41+
42+
assert.DeepEqual(t, []string{"abc"}, pager.searchHistory.entries)
43+
}
44+
45+
// Search and filter modes each get their own HistoryNavigator, but both wrap
46+
// the same *SearchHistory on the pager, so an entry committed from one mode
47+
// must be visible when navigating history from the other.
48+
//
49+
// Ref: https://github.com/walles/moor/issues/466
50+
func TestPagerModeFilter_SharesSearchHistoryWithSearchMode(t *testing.T) {
51+
pager := createThreeLinesPager(t)
52+
pager.searchHistory = &SearchHistory{} // No file backing, keep this test disk-free
53+
54+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
55+
filterMode.inputBox.setText("from filter")
56+
filterMode.onKey(twin.KeyEnter)
57+
58+
searchMode := NewPagerModeSearch(pager, SearchDirectionForward, pager.scrollPosition)
59+
searchMode.onKey(twin.KeyUp)
60+
61+
assert.Equal(t, "from filter", searchMode.inputBox.text)
62+
}
63+
64+
// Pressing ESC should return the view to wherever it was scrolled to before
65+
// filtering started, the same way search's ESC does, even though filtering
66+
// out non-matching lines may have moved the pager's scroll position while
67+
// typing.
68+
//
69+
// Ref: https://github.com/walles/moor/issues/466
70+
func TestPagerModeFilter_EscapeRestoresScrollPositionFromBeforeFiltering(t *testing.T) {
71+
pager := createThreeLinesPager(t)
72+
pager.scrollPosition = NewScrollPositionFromIndex(linemetadata.IndexFromZeroBased(4), "before filtering")
73+
initialIndex := pager.lineIndex().Index()
74+
75+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
76+
filterMode.onRune('a') // Filters down to only the line containing "a"
77+
78+
// This mirrors what a real redraw does, and confirms filtering really did
79+
// move the scroll position as a side effect.
80+
assert.Assert(t, pager.lineIndex().Index() != initialIndex, "Filtering should have moved the scroll position")
81+
82+
filterMode.onKey(twin.KeyEscape)
83+
84+
assert.Equal(t, initialIndex, pager.lineIndex().Index())
85+
}
86+
87+
// Ref: https://github.com/walles/moor/issues/466
88+
func TestPagerModeFilter_PgDownPersistsTypedTextAndSwitchesToViewing(t *testing.T) {
89+
pager := createThreeLinesPager(t)
90+
pager.searchHistory = &SearchHistory{} // No file backing, keep this test disk-free
91+
92+
filterMode := NewPagerModeFilter(pager, pager.scrollPosition)
93+
pager.mode = filterMode
94+
filterMode.inputBox.setText("abc")
95+
filterMode.onKey(twin.KeyPgDown)
96+
97+
assert.DeepEqual(t, []string{"abc"}, pager.searchHistory.entries)
98+
assert.Equal(t, "Viewing", modeName(pager))
99+
}

0 commit comments

Comments
 (0)