11package history
22
33import (
4+ "bytes"
45 "encoding/json"
56 "os"
67 "path/filepath"
78 "slices"
8- "strconv"
99 "strings"
1010)
1111
@@ -86,55 +86,40 @@ func (h *History) migrateOldHistory(homeDir string) error {
8686}
8787
8888func (h * History ) Add (message string ) error {
89- // Update in-memory list: remove duplicate and append to end
90- h .Messages = slices .DeleteFunc (h .Messages , func (m string ) bool {
91- return m == message
92- })
93- h .Messages = append (h .Messages , message )
89+ h .addInMemory (message )
9490 h .current = len (h .Messages )
95-
9691 return h .append (message )
9792}
9893
9994func (h * History ) Previous () string {
10095 if len (h .Messages ) == 0 {
10196 return ""
10297 }
103-
104- // If we're at -1 (initial state), start from the end
105- if h .current == - 1 {
98+ switch {
99+ case h .current == - 1 :
106100 h .current = len (h .Messages ) - 1
107- return h .Messages [h .current ]
108- }
109-
110- // If we're at the beginning, stay there
111- if h .current <= 0 {
112- return h .Messages [0 ]
101+ case h .current > 0 :
102+ h .current --
113103 }
114-
115- h .current --
116104 return h .Messages [h .current ]
117105}
118106
119107func (h * History ) Next () string {
120108 if len (h .Messages ) == 0 {
121109 return ""
122110 }
123-
124111 if h .current >= len (h .Messages )- 1 {
125112 h .current = len (h .Messages )
126113 return ""
127114 }
128-
129115 h .current ++
130116 return h .Messages [h .current ]
131117}
132118
133119// LatestMatch returns the most recent history entry that extends the provided
134- // prefix, or the latest message when no prefix is supplied .
120+ // prefix, or an empty string when none does .
135121func (h * History ) LatestMatch (prefix string ) string {
136- for i := len (h .Messages ) - 1 ; i >= 0 ; i -- {
137- msg := h .Messages [i ]
122+ for _ , msg := range slices .Backward (h .Messages ) {
138123 if strings .HasPrefix (msg , prefix ) && len (msg ) > len (prefix ) {
139124 return msg
140125 }
@@ -147,46 +132,42 @@ func (h *History) LatestMatch(prefix string) string {
147132// Returns the matched message, its index, and whether a match was found.
148133// An empty query matches any entry.
149134func (h * History ) FindPrevContains (query string , from int ) (msg string , idx int , ok bool ) {
150- if len (h .Messages ) == 0 {
151- return "" , - 1 , false
152- }
153-
154- start := min (from - 1 , len (h .Messages )- 1 )
155-
156135 query = strings .ToLower (query )
157- for i := start ; i >= 0 ; i -- {
136+ for i := min ( from - 1 , len ( h . Messages ) - 1 ) ; i >= 0 ; i -- {
158137 if query == "" || strings .Contains (strings .ToLower (h .Messages [i ]), query ) {
159138 return h .Messages [i ], i , true
160139 }
161140 }
162-
163141 return "" , - 1 , false
164142}
165143
166144// FindNextContains searches forward through history for a message containing query.
167145// from is an exclusive lower bound index. Pass -1 to start from the oldest.
168146// Returns the matched message, its index, and whether a match was found.
147+ // An empty query matches any entry.
169148func (h * History ) FindNextContains (query string , from int ) (msg string , idx int , ok bool ) {
170- if len (h .Messages ) == 0 {
171- return "" , - 1 , false
172- }
173-
174- start := max (from + 1 , 0 )
175-
176149 query = strings .ToLower (query )
177- for i := start ; i < len (h .Messages ); i ++ {
150+ for i := max ( from + 1 , 0 ) ; i < len (h .Messages ); i ++ {
178151 if query == "" || strings .Contains (strings .ToLower (h .Messages [i ]), query ) {
179152 return h .Messages [i ], i , true
180153 }
181154 }
182-
183155 return "" , - 1 , false
184156}
185157
186158func (h * History ) SetCurrent (i int ) {
187159 h .current = i
188160}
189161
162+ // addInMemory removes any prior occurrence of message and appends it as the
163+ // most recent entry.
164+ func (h * History ) addInMemory (message string ) {
165+ h .Messages = slices .DeleteFunc (h .Messages , func (m string ) bool {
166+ return m == message
167+ })
168+ h .Messages = append (h .Messages , message )
169+ }
170+
190171func (h * History ) append (message string ) error {
191172 if err := os .MkdirAll (filepath .Dir (h .path ), 0o755 ); err != nil {
192173 return err
@@ -213,51 +194,19 @@ func (h *History) load() error {
213194 return err
214195 }
215196
216- // Count lines to pre-size the slice.
217- n := 0
218- for _ , b := range data {
219- if b == '\n' {
220- n ++
221- }
222- }
223-
224- // Parse all lines. Each line is a JSON-encoded string (e.g. "hello").
225- // strconv.Unquote handles the same escape sequences as JSON and is
226- // much faster than json.Unmarshal for quoted strings.
227- all := make ([]string , 0 , n )
228- s := string (data )
229- for s != "" {
230- i := strings .IndexByte (s , '\n' )
231- var line string
232- if i < 0 {
233- line = s
234- s = ""
235- } else {
236- line = s [:i ]
237- s = s [i + 1 :]
238- }
239- if line == "" {
197+ // The file is append-only with one JSON-encoded string per line.
198+ // Replaying each entry through addInMemory naturally deduplicates,
199+ // keeping the latest occurrence of each message.
200+ for line := range bytes .Lines (data ) {
201+ line = bytes .TrimRight (line , "\n " )
202+ if len (line ) == 0 {
240203 continue
241204 }
242-
243- message , err := strconv .Unquote (line )
244- if err != nil {
205+ var msg string
206+ if err := json .Unmarshal (line , & msg ); err != nil {
245207 continue
246208 }
247- all = append ( all , message )
209+ h . addInMemory ( msg )
248210 }
249-
250- // Deduplicate keeping the latest occurrence of each message.
251- seen := make (map [string ]struct {}, len (all ))
252- h .Messages = make ([]string , 0 , len (all ))
253- for i := len (all ) - 1 ; i >= 0 ; i -- {
254- if _ , dup := seen [all [i ]]; dup {
255- continue
256- }
257- seen [all [i ]] = struct {}{}
258- h .Messages = append (h .Messages , all [i ])
259- }
260- slices .Reverse (h .Messages )
261-
262211 return nil
263212}
0 commit comments