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 prevHeight := m.textarea.Height()
47 // Navigate to older history entry from cursor position (0,0).
48 if m.textarea.Length() == 0 || m.isAtEditorStart() {
49 if m.historyPrev() {
50 // we send this so that the textarea moves the view to the correct position
51 // without this the cursor will show up in the wrong place.
52 return m.updateTextareaWithPrevHeight(nil, prevHeight)
53 }
54 }
55
56 // First move cursor to start before entering history.
57 if m.textarea.Line() == 0 {
58 m.textarea.CursorStart()
59 return nil
60 }
61
62 // Let textarea handle normal cursor movement.
63 return m.updateTextarea(msg)
64}
65
66// handleHistoryDown handles down arrow for history navigation.
67func (m *UI) handleHistoryDown(msg tea.Msg) tea.Cmd {
68 prevHeight := m.textarea.Height()
69 // Navigate to newer history entry from end of text.
70 if m.isAtEditorEnd() {
71 if m.historyNext() {
72 // we send this so that the textarea moves the view to the correct position
73 // without this the cursor will show up in the wrong place.
74 return m.updateTextareaWithPrevHeight(nil, prevHeight)
75 }
76 }
77
78 // First move cursor to end before navigating history.
79 if m.textarea.Line() == max(m.textarea.LineCount()-1, 0) {
80 m.textarea.MoveToEnd()
81 return m.updateTextarea(nil)
82 }
83
84 // Let textarea handle normal cursor movement.
85 return m.updateTextarea(msg)
86}
87
88// handleHistoryEscape handles escape for exiting history navigation.
89func (m *UI) handleHistoryEscape(msg tea.Msg) tea.Cmd {
90 prevHeight := m.textarea.Height()
91 // Return to current draft when browsing history.
92 if m.promptHistory.index >= 0 {
93 m.promptHistory.index = -1
94 m.textarea.Reset()
95 m.textarea.InsertString(m.promptHistory.draft)
96 return m.updateTextareaWithPrevHeight(nil, prevHeight)
97 }
98
99 // Let textarea handle escape normally.
100 return m.updateTextarea(msg)
101}
102
103// updateHistoryDraft updates history state when text is modified.
104func (m *UI) updateHistoryDraft(oldValue string) {
105 if m.textarea.Value() != oldValue {
106 m.promptHistory.draft = m.textarea.Value()
107 m.promptHistory.index = -1
108 }
109}
110
111// historyPrev changes the text area content to the previous message in the history
112// it returns false if it could not find the previous message.
113func (m *UI) historyPrev() bool {
114 if len(m.promptHistory.messages) == 0 {
115 return false
116 }
117 if m.promptHistory.index == -1 {
118 m.promptHistory.draft = m.textarea.Value()
119 }
120 nextIndex := m.promptHistory.index + 1
121 if nextIndex >= len(m.promptHistory.messages) {
122 return false
123 }
124 m.promptHistory.index = nextIndex
125 m.textarea.Reset()
126 m.textarea.InsertString(m.promptHistory.messages[nextIndex])
127 m.textarea.MoveToBegin()
128 return true
129}
130
131// historyNext changes the text area content to the next message in the history
132// it returns false if it could not find the next message.
133func (m *UI) historyNext() bool {
134 if m.promptHistory.index < 0 {
135 return false
136 }
137 nextIndex := m.promptHistory.index - 1
138 if nextIndex < 0 {
139 m.promptHistory.index = -1
140 m.textarea.Reset()
141 m.textarea.InsertString(m.promptHistory.draft)
142 return true
143 }
144 m.promptHistory.index = nextIndex
145 m.textarea.Reset()
146 m.textarea.InsertString(m.promptHistory.messages[nextIndex])
147 return true
148}
149
150// historyReset resets the history, but does not clear the message
151// it just sets the current draft to empty and the position in the history.
152func (m *UI) historyReset() {
153 m.promptHistory.index = -1
154 m.promptHistory.draft = ""
155}
156
157// isAtEditorStart returns true if we are at the 0 line and 0 col in the textarea.
158func (m *UI) isAtEditorStart() bool {
159 return m.textarea.Line() == 0 && m.textarea.LineInfo().ColumnOffset == 0
160}
161
162// isAtEditorEnd returns true if we are in the last line and the last column in the textarea.
163func (m *UI) isAtEditorEnd() bool {
164 lineCount := m.textarea.LineCount()
165 if lineCount == 0 {
166 return true
167 }
168 if m.textarea.Line() != lineCount-1 {
169 return false
170 }
171 info := m.textarea.LineInfo()
172 return info.CharOffset >= info.CharWidth-1 || info.CharWidth == 0
173}