1package model
2
3import (
4 "context"
5 "log/slog"
6
7 tea "charm.land/bubbletea/v2"
8
9 "github.com/charmbracelet/crush/internal/message"
10)
11
12// promptHistoryLoadedMsg is sent when prompt history is loaded.
13type promptHistoryLoadedMsg struct {
14 messages []string
15}
16
17// loadPromptHistory loads user messages for history navigation.
18func (m *UI) loadPromptHistory() tea.Cmd {
19 return func() tea.Msg {
20 ctx := context.Background()
21 var messages []message.Message
22 var err error
23
24 if m.session != nil {
25 messages, err = m.com.App.Messages.ListUserMessages(ctx, m.session.ID)
26 } else {
27 messages, err = m.com.App.Messages.ListAllUserMessages(ctx)
28 }
29 if err != nil {
30 slog.Error("Failed to load prompt history", "error", err)
31 return promptHistoryLoadedMsg{messages: nil}
32 }
33
34 texts := make([]string, 0, len(messages))
35 for _, msg := range messages {
36 if text := msg.Content().Text; text != "" {
37 texts = append(texts, text)
38 }
39 }
40 return promptHistoryLoadedMsg{messages: texts}
41 }
42}
43
44// handleHistoryUp handles up arrow for history navigation.
45func (m *UI) handleHistoryUp(msg tea.Msg) tea.Cmd {
46 // Navigate to older history entry from cursor position (0,0).
47 if m.textarea.Length() == 0 || m.isAtEditorStart() {
48 if m.historyPrev() {
49 // we send this so that the textarea moves the view to the correct position
50 // without this the cursor will show up in the wrong place.
51 ta, cmd := m.textarea.Update(nil)
52 m.textarea = ta
53 return cmd
54 }
55 }
56
57 // First move cursor to start before entering history.
58 if m.textarea.Line() == 0 {
59 m.textarea.CursorStart()
60 return nil
61 }
62
63 // Let textarea handle normal cursor movement.
64 ta, cmd := m.textarea.Update(msg)
65 m.textarea = ta
66 return cmd
67}
68
69// handleHistoryDown handles down arrow for history navigation.
70func (m *UI) handleHistoryDown(msg tea.Msg) tea.Cmd {
71 // Navigate to newer history entry from end of text.
72 if m.isAtEditorEnd() {
73 if m.historyNext() {
74 // we send this so that the textarea moves the view to the correct position
75 // without this the cursor will show up in the wrong place.
76 ta, cmd := m.textarea.Update(nil)
77 m.textarea = ta
78 return cmd
79 }
80 }
81
82 // First move cursor to end before navigating history.
83 if m.textarea.Line() == max(m.textarea.LineCount()-1, 0) {
84 m.textarea.MoveToEnd()
85 ta, cmd := m.textarea.Update(nil)
86 m.textarea = ta
87 return cmd
88 }
89
90 // Let textarea handle normal cursor movement.
91 ta, cmd := m.textarea.Update(msg)
92 m.textarea = ta
93 return cmd
94}
95
96// handleHistoryEscape handles escape for exiting history navigation.
97func (m *UI) handleHistoryEscape(msg tea.Msg) tea.Cmd {
98 // Return to current draft when browsing history.
99 if m.promptHistory.index >= 0 {
100 m.promptHistory.index = -1
101 m.textarea.Reset()
102 m.textarea.InsertString(m.promptHistory.draft)
103 ta, cmd := m.textarea.Update(nil)
104 m.textarea = ta
105 return cmd
106 }
107
108 // Let textarea handle escape normally.
109 ta, cmd := m.textarea.Update(msg)
110 m.textarea = ta
111 return cmd
112}
113
114// updateHistoryDraft updates history state when text is modified.
115func (m *UI) updateHistoryDraft(oldValue string) {
116 if m.textarea.Value() != oldValue {
117 m.promptHistory.draft = m.textarea.Value()
118 m.promptHistory.index = -1
119 }
120}
121
122// historyPrev changes the text area content to the previous message in the history
123// it returns false if it could not find the previous message.
124func (m *UI) historyPrev() bool {
125 if len(m.promptHistory.messages) == 0 {
126 return false
127 }
128 if m.promptHistory.index == -1 {
129 m.promptHistory.draft = m.textarea.Value()
130 }
131 nextIndex := m.promptHistory.index + 1
132 if nextIndex >= len(m.promptHistory.messages) {
133 return false
134 }
135 m.promptHistory.index = nextIndex
136 m.textarea.Reset()
137 m.textarea.InsertString(m.promptHistory.messages[nextIndex])
138 m.textarea.MoveToBegin()
139 return true
140}
141
142// historyNext changes the text area content to the next message in the history
143// it returns false if it could not find the next message.
144func (m *UI) historyNext() bool {
145 if m.promptHistory.index < 0 {
146 return false
147 }
148 nextIndex := m.promptHistory.index - 1
149 if nextIndex < 0 {
150 m.promptHistory.index = -1
151 m.textarea.Reset()
152 m.textarea.InsertString(m.promptHistory.draft)
153 return true
154 }
155 m.promptHistory.index = nextIndex
156 m.textarea.Reset()
157 m.textarea.InsertString(m.promptHistory.messages[nextIndex])
158 return true
159}
160
161// historyReset resets the history, but does not clear the message
162// it just sets the current draft to empty and the position in the history.
163func (m *UI) historyReset() {
164 m.promptHistory.index = -1
165 m.promptHistory.draft = ""
166}
167
168// isAtEditorStart returns true if we are at the 0 line and 0 col in the textarea.
169func (m *UI) isAtEditorStart() bool {
170 return m.textarea.Line() == 0 && m.textarea.LineInfo().ColumnOffset == 0
171}
172
173// isAtEditorEnd returns true if we are in the last line and the last column in the textarea.
174func (m *UI) isAtEditorEnd() bool {
175 lineCount := m.textarea.LineCount()
176 if lineCount == 0 {
177 return true
178 }
179 if m.textarea.Line() != lineCount-1 {
180 return false
181 }
182 info := m.textarea.LineInfo()
183 return info.CharOffset >= info.CharWidth-1 || info.CharWidth == 0
184}