Skip to content

Commit 05f9ec1

Browse files
authored
Merge pull request #654 from dgageot/simpler-history-2
Simpler code for history
2 parents 726d3c0 + 280e580 commit 05f9ec1

4 files changed

Lines changed: 66 additions & 128 deletions

File tree

pkg/history/history.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7-
"slices"
87
)
98

109
type History struct {
@@ -33,13 +32,18 @@ func New() (*History, error) {
3332
}
3433

3534
func (h *History) Add(message string) error {
36-
// Avoid duplicate messages
37-
if slices.Contains(h.Messages, message) {
38-
return nil
35+
// Add the message last but avoid duplicate messages
36+
var messages []string
37+
for _, msg := range h.Messages {
38+
if msg != message {
39+
messages = append(messages, msg)
40+
}
3941
}
42+
messages = append(messages, message)
4043

41-
h.Messages = append(h.Messages, message)
44+
h.Messages = messages
4245
h.current = len(h.Messages)
46+
4347
return h.save()
4448
}
4549

pkg/history/history_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,47 @@ func TestHistory_EdgeCases(t *testing.T) {
8888
assert.Equal(t, "only", h.Previous()) // Should stay at the beginning
8989
assert.Empty(t, h.Next()) // Should return empty when going past the end
9090
}
91+
92+
func TestHistory_StayAtTheBeginning(t *testing.T) {
93+
t.Setenv("HOME", t.TempDir())
94+
95+
h, err := New()
96+
require.NoError(t, err)
97+
98+
require.NoError(t, h.Add("first"))
99+
100+
assert.Equal(t, "first", h.Previous())
101+
assert.Equal(t, "first", h.Previous())
102+
}
103+
104+
func TestHistory_NoDuplicateMessages(t *testing.T) {
105+
t.Setenv("HOME", t.TempDir())
106+
107+
h, err := New()
108+
require.NoError(t, err)
109+
110+
require.NoError(t, h.Add("first"))
111+
require.NoError(t, h.Add("second"))
112+
require.NoError(t, h.Add("second"))
113+
114+
assert.Equal(t, "second", h.Previous())
115+
assert.Equal(t, "first", h.Previous())
116+
assert.Equal(t, "first", h.Previous())
117+
}
118+
119+
func TestHistory_MoveDuplicateLast(t *testing.T) {
120+
t.Setenv("HOME", t.TempDir())
121+
122+
h, err := New()
123+
require.NoError(t, err)
124+
125+
require.NoError(t, h.Add("first"))
126+
require.NoError(t, h.Add("second"))
127+
require.NoError(t, h.Add("third"))
128+
require.NoError(t, h.Add("first"))
129+
130+
assert.Equal(t, "first", h.Previous())
131+
assert.Equal(t, "third", h.Previous())
132+
assert.Equal(t, "second", h.Previous())
133+
assert.Equal(t, "second", h.Previous())
134+
}

pkg/tui/components/editor/editor.go

Lines changed: 12 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,32 @@ type SendMsg struct {
2222
Content string
2323
}
2424

25-
// historyNavigation describes which direction we want to pull from history.
26-
type historyNavigation int
27-
28-
const (
29-
NAVIGATEPREVIOUS historyNavigation = iota
30-
NAVIGATENEXT
31-
)
32-
3325
// Editor represents an input editor component
3426
type Editor interface {
3527
layout.Model
3628
layout.Sizeable
3729
layout.Focusable
3830
layout.Help
39-
SetHistory(hist *history.History)
4031
SetWorking(working bool) tea.Cmd
4132
}
4233

4334
// editor implements [Editor]
4435
type editor struct {
4536
textarea *textarea.Model
37+
hist *history.History
4638
width int
4739
height int
4840
working bool
41+
// completions are the available completions
42+
completions []completions.Completion
4943

50-
// history is the shared command store backing up/down navigation.
51-
hist *history.History
52-
// draftInput holds the user's unsent text while they browse history.
53-
draftInput string
54-
// historyBrowsing marks that we're currently showing history entries.
55-
historyBrowsing bool
5644
// completionWord stores the word being completed
57-
completionWord string
58-
// completions are the available completions
59-
completions []completions.Completion
45+
completionWord string
6046
currentCompletion completions.Completion
6147
}
6248

6349
// New creates a new editor component
64-
func New(a *app.App) Editor {
50+
func New(a *app.App, hist *history.History) Editor {
6551
ta := textarea.New()
6652
ta.SetStyles(styles.InputStyle)
6753
ta.Placeholder = "Type your message here..."
@@ -75,6 +61,7 @@ func New(a *app.App) Editor {
7561

7662
return &editor{
7763
textarea: ta,
64+
hist: hist,
7865
completions: completions.Completions(a),
7966
}
8067
}
@@ -123,24 +110,20 @@ func (e *editor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
123110
}
124111
value := e.textarea.Value()
125112
if value != "" && !e.working {
126-
// Treat enter as send: clear input and exit history browse state.
127113
e.textarea.Reset()
128-
e.endHistoryBrowse()
129114
return e, core.CmdHandler(SendMsg{Content: value})
130115
}
131116
return e, nil
132117
case "ctrl+c":
133118
return e, tea.Quit
134119
case "up":
135-
// Consume the key when we replace the buffer with an older command.
136-
if e.navigateHistory(NAVIGATEPREVIOUS) {
137-
return e, nil
138-
}
120+
e.textarea.SetValue(e.hist.Previous())
121+
e.textarea.MoveToEnd()
122+
return e, nil
139123
case "down":
140-
// Consume the key when we replace the buffer with a newer command.
141-
if e.navigateHistory(NAVIGATENEXT) {
142-
return e, nil
143-
}
124+
e.textarea.SetValue(e.hist.Next())
125+
e.textarea.MoveToEnd()
126+
return e, nil
144127
default:
145128
for _, completion := range e.completions {
146129
if msg.String() == completion.Trigger() {
@@ -150,11 +133,6 @@ func (e *editor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
150133
cmds = append(cmds, e.startCompletion(completion))
151134
}
152135
}
153-
154-
// Any other key exits history browsing so input becomes fresh text.
155-
if e.historyBrowsing {
156-
e.endHistoryBrowse()
157-
}
158136
}
159137
}
160138

@@ -244,88 +222,3 @@ func (e *editor) SetWorking(working bool) tea.Cmd {
244222
e.working = working
245223
return nil
246224
}
247-
248-
func (e *editor) SetHistory(hist *history.History) {
249-
e.hist = hist
250-
}
251-
252-
func (e *editor) navigateHistory(direction historyNavigation) bool {
253-
// Returning true tells Update to stop Bubble Tea's default cursor handling,
254-
// because we've already replaced the textarea content for this key press.
255-
if !e.canBrowseHistory() {
256-
return false
257-
}
258-
259-
if !e.historyBrowsing {
260-
e.beginHistoryBrowse()
261-
}
262-
263-
var entry string
264-
switch direction {
265-
case NAVIGATEPREVIOUS:
266-
// Up arrow walks toward older commands.
267-
entry = e.hist.Previous()
268-
case NAVIGATENEXT:
269-
// Down arrow walks toward newer commands.
270-
entry = e.hist.Next()
271-
if entry == "" {
272-
// Restore the draft when we step past the newest entry.
273-
e.restoreDraftFromHistory()
274-
return true
275-
}
276-
default:
277-
return false
278-
}
279-
280-
if entry == "" {
281-
return true
282-
}
283-
284-
// Replace the input with the selected history entry.
285-
e.textarea.SetValue(entry)
286-
// Place the cursor at the end so the user can immediately append or send.
287-
e.textarea.MoveToEnd()
288-
return true
289-
}
290-
291-
func (e *editor) canBrowseHistory() bool {
292-
// We only take over arrow keys when there's at least one history entry and
293-
// the textarea is a single line (multi-line inputs retain normal movement).
294-
return e.hist != nil && e.textarea.Value() == ""
295-
}
296-
297-
func (e *editor) beginHistoryBrowse() {
298-
if e.hist == nil {
299-
return
300-
}
301-
// Capture the in-progress text so we can restore it after browsing.
302-
e.draftInput = e.textarea.Value()
303-
e.historyBrowsing = true
304-
// Start from the newest entry so the first "up" pulls the latest command.
305-
e.moveHistoryCursorToLatest()
306-
}
307-
308-
func (e *editor) restoreDraftFromHistory() {
309-
e.textarea.SetValue(e.draftInput)
310-
e.textarea.MoveToEnd()
311-
e.endHistoryBrowse()
312-
}
313-
314-
func (e *editor) endHistoryBrowse() {
315-
e.historyBrowsing = false
316-
e.draftInput = ""
317-
if e.hist == nil {
318-
return
319-
}
320-
e.moveHistoryCursorToLatest()
321-
}
322-
323-
func (e *editor) moveHistoryCursorToLatest() {
324-
if e.hist == nil {
325-
return
326-
}
327-
// Advance until Next returns empty, which positions the cursor just after
328-
// the most recent saved command.
329-
for e.hist.Next() != "" {
330-
}
331-
}

pkg/tui/page/chat/chat.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,16 @@ func defaultKeyMap() KeyMap {
9595

9696
// New creates a new chat page
9797
func New(a *app.App) Page {
98-
ed := editor.New(a)
99-
10098
historyStore, err := history.New()
10199
if err != nil {
102100
fmt.Fprintf(os.Stderr, "failed to initialize command history: %v\n", err)
103101
}
104-
ed.SetHistory(historyStore)
105102

106103
return &chatPage{
107104
title: a.Title(),
108105
sidebar: sidebar.New(),
109106
messages: messages.New(a),
110-
editor: ed,
107+
editor: editor.New(a, historyStore),
111108
focusedPanel: PanelEditor,
112109
app: a,
113110
keyMap: defaultKeyMap(),

0 commit comments

Comments
 (0)