Skip to main content

zsh/zle/
hist.rs

1//! ZLE history operations
2//!
3//! Direct port from zsh/Src/Zle/zle_hist.c
4//!
5//! Implements all history navigation widgets:
6//! - up-line-or-history, down-line-or-history
7//! - history-search-backward, history-search-forward  
8//! - history-incremental-search-backward, history-incremental-search-forward
9//! - beginning-of-history, end-of-history
10//! - vi-fetch-history, vi-history-search-*
11//! - accept-line-and-down-history, accept-and-infer-next-history
12//! - insert-last-word, push-line, push-line-or-edit
13
14use super::main::{Zle, ZleString};
15
16/// History entry
17#[derive(Debug, Clone)]
18pub struct HistEntry {
19    /// The command line
20    pub line: String,
21    /// Event number
22    pub num: i64,
23    /// Timestamp (if available)
24    pub time: Option<i64>,
25}
26
27/// History state
28#[derive(Debug, Default)]
29pub struct History {
30    /// History entries (newest last)
31    pub entries: Vec<HistEntry>,
32    /// Current position in history
33    pub cursor: usize,
34    /// Maximum history size
35    pub max_size: usize,
36    /// Saved line when navigating history
37    pub saved_line: Option<ZleString>,
38    /// Saved cursor position
39    pub saved_cs: usize,
40    /// Search pattern
41    pub search_pattern: String,
42    /// Last search direction (true = backward)
43    pub search_backward: bool,
44}
45
46impl History {
47    pub fn new(max_size: usize) -> Self {
48        History {
49            entries: Vec::new(),
50            cursor: 0,
51            max_size,
52            saved_line: None,
53            saved_cs: 0,
54            search_pattern: String::new(),
55            search_backward: true,
56        }
57    }
58
59    /// Add entry to history
60    pub fn add(&mut self, line: String) {
61        // Don't add empty or duplicate entries
62        if line.is_empty() {
63            return;
64        }
65        if let Some(last) = self.entries.last() {
66            if last.line == line {
67                return;
68            }
69        }
70
71        self.entries.push(HistEntry {
72            line,
73            num: self.entries.len() as i64 + 1,
74            time: Some(
75                std::time::SystemTime::now()
76                    .duration_since(std::time::UNIX_EPOCH)
77                    .map(|d| d.as_secs() as i64)
78                    .unwrap_or(0),
79            ),
80        });
81
82        // Trim if over max size
83        while self.entries.len() > self.max_size {
84            self.entries.remove(0);
85        }
86
87        // Reset cursor to end
88        self.cursor = self.entries.len();
89    }
90
91    /// Get entry at cursor
92    pub fn get(&self, index: usize) -> Option<&HistEntry> {
93        self.entries.get(index)
94    }
95
96    /// Move cursor up (older)
97    pub fn up(&mut self) -> Option<&HistEntry> {
98        if self.cursor > 0 {
99            self.cursor -= 1;
100            self.entries.get(self.cursor)
101        } else {
102            None
103        }
104    }
105
106    /// Move cursor down (newer)
107    pub fn down(&mut self) -> Option<&HistEntry> {
108        if self.cursor < self.entries.len() {
109            self.cursor += 1;
110            self.entries.get(self.cursor)
111        } else {
112            None
113        }
114    }
115
116    /// Search backward for pattern
117    pub fn search_backward(&mut self, pattern: &str) -> Option<&HistEntry> {
118        let start = if self.cursor > 0 {
119            self.cursor - 1
120        } else {
121            return None;
122        };
123
124        for i in (0..=start).rev() {
125            if self.entries[i].line.contains(pattern) {
126                self.cursor = i;
127                return self.entries.get(i);
128            }
129        }
130
131        None
132    }
133
134    /// Search forward for pattern
135    pub fn search_forward(&mut self, pattern: &str) -> Option<&HistEntry> {
136        for i in (self.cursor + 1)..self.entries.len() {
137            if self.entries[i].line.contains(pattern) {
138                self.cursor = i;
139                return self.entries.get(i);
140            }
141        }
142
143        None
144    }
145
146    /// Reset cursor to end
147    pub fn reset(&mut self) {
148        self.cursor = self.entries.len();
149        self.saved_line = None;
150    }
151}
152
153impl Zle {
154    /// Initialize history for ZLE
155    pub fn init_history(&mut self, max_size: usize) {
156        // History would be stored externally and passed in
157        // This is just a stub for the interface
158        let _ = max_size;
159    }
160
161    /// Go to previous history entry
162    pub fn history_up(&mut self, hist: &mut History) {
163        if hist.saved_line.is_none() {
164            // Save current line
165            hist.saved_line = Some(self.zleline.clone());
166            hist.saved_cs = self.zlecs;
167        }
168
169        if let Some(entry) = hist.up() {
170            self.zleline = entry.line.chars().collect();
171            self.zlell = self.zleline.len();
172            self.zlecs = self.zlell;
173            self.resetneeded = true;
174        }
175    }
176
177    /// Go to next history entry
178    pub fn history_down(&mut self, hist: &mut History) {
179        if let Some(entry) = hist.down() {
180            self.zleline = entry.line.chars().collect();
181            self.zlell = self.zleline.len();
182            self.zlecs = self.zlell;
183            self.resetneeded = true;
184        } else if let Some(saved) = hist.saved_line.take() {
185            // Restore saved line
186            self.zleline = saved;
187            self.zlell = self.zleline.len();
188            self.zlecs = hist.saved_cs;
189            self.resetneeded = true;
190        }
191    }
192
193    /// Incremental search backward
194    pub fn history_isearch_backward(&mut self, hist: &mut History) {
195        hist.search_backward = true;
196        // TODO: implement full incremental search UI
197    }
198
199    /// Incremental search forward
200    pub fn history_isearch_forward(&mut self, hist: &mut History) {
201        hist.search_backward = false;
202        // TODO: implement full incremental search UI
203    }
204
205    /// Search history for prefix
206    pub fn history_search_prefix(&mut self, hist: &mut History) {
207        let prefix: String = self.zleline[..self.zlecs].iter().collect();
208
209        if let Some(entry) = hist.search_backward(&prefix) {
210            self.zleline = entry.line.chars().collect();
211            self.zlell = self.zleline.len();
212            self.resetneeded = true;
213        }
214    }
215
216    /// Beginning of history - go to first entry
217    /// Port of beginningofhistory() from zle_hist.c
218    pub fn beginning_of_history(&mut self, hist: &mut History) {
219        if hist.saved_line.is_none() {
220            hist.saved_line = Some(self.zleline.clone());
221            hist.saved_cs = self.zlecs;
222        }
223
224        if !hist.entries.is_empty() {
225            hist.cursor = 0;
226            if let Some(entry) = hist.entries.first() {
227                self.zleline = entry.line.chars().collect();
228                self.zlell = self.zleline.len();
229                self.zlecs = 0;
230                self.resetneeded = true;
231            }
232        }
233    }
234
235    /// End of history - go to last entry (current line)
236    /// Port of endofhistory() from zle_hist.c
237    pub fn end_of_history(&mut self, hist: &mut History) {
238        hist.cursor = hist.entries.len();
239
240        if let Some(saved) = hist.saved_line.take() {
241            self.zleline = saved;
242            self.zlell = self.zleline.len();
243            self.zlecs = hist.saved_cs;
244            self.resetneeded = true;
245        }
246    }
247
248    /// Up line or history - move up in multi-line buffer or go to previous history
249    /// Port of uplineorhistory() from zle_hist.c
250    pub fn up_line_or_history(&mut self, hist: &mut History) {
251        // For now, just do history (multi-line TODO)
252        self.history_up(hist);
253    }
254
255    /// Down line or history - move down in multi-line buffer or go to next history
256    /// Port of downlineorhistory() from zle_hist.c
257    pub fn down_line_or_history(&mut self, hist: &mut History) {
258        self.history_down(hist);
259    }
260
261    /// History search backward - search for entries starting with current prefix
262    /// Port of historysearchbackward() from zle_hist.c
263    pub fn history_search_backward(&mut self, hist: &mut History) {
264        let prefix: String = self.zleline[..self.zlecs.min(self.zleline.len())]
265            .iter()
266            .collect();
267
268        if hist.saved_line.is_none() {
269            hist.saved_line = Some(self.zleline.clone());
270            hist.saved_cs = self.zlecs;
271        }
272
273        hist.search_pattern = prefix.clone();
274        hist.search_backward = true;
275
276        let start = hist.cursor.saturating_sub(1);
277        for i in (0..=start).rev() {
278            if hist.entries[i].line.starts_with(&prefix) {
279                hist.cursor = i;
280                self.zleline = hist.entries[i].line.chars().collect();
281                self.zlell = self.zleline.len();
282                self.zlecs = prefix.len();
283                self.resetneeded = true;
284                return;
285            }
286        }
287    }
288
289    /// History search forward - search for entries starting with current prefix
290    /// Port of historysearchforward() from zle_hist.c
291    pub fn history_search_forward(&mut self, hist: &mut History) {
292        let prefix = &hist.search_pattern;
293        hist.search_backward = false;
294
295        for i in (hist.cursor + 1)..hist.entries.len() {
296            if hist.entries[i].line.starts_with(prefix) {
297                hist.cursor = i;
298                self.zleline = hist.entries[i].line.chars().collect();
299                self.zlell = self.zleline.len();
300                self.zlecs = prefix.len();
301                self.resetneeded = true;
302                return;
303            }
304        }
305
306        // Wrap to saved line
307        if let Some(ref saved) = hist.saved_line {
308            let saved_str: String = saved.iter().collect();
309            if saved_str.starts_with(prefix) {
310                hist.cursor = hist.entries.len();
311                self.zleline = saved.clone();
312                self.zlell = self.zleline.len();
313                self.zlecs = hist.saved_cs;
314                self.resetneeded = true;
315            }
316        }
317    }
318
319    /// Insert last word from previous history entry
320    /// Port of insertlastword() from zle_hist.c
321    pub fn insert_last_word(&mut self, hist: &History) {
322        if let Some(entry) = hist.entries.last() {
323            // Get the last word
324            if let Some(last_word) = entry.line.split_whitespace().last() {
325                // Insert at cursor
326                for c in last_word.chars() {
327                    self.zleline.insert(self.zlecs, c);
328                    self.zlecs += 1;
329                }
330                self.zlell = self.zleline.len();
331                self.resetneeded = true;
332            }
333        }
334    }
335
336    /// Push current line to buffer stack
337    /// Port of pushline() from zle_hist.c
338    pub fn push_line(&mut self) {
339        // Save line to a stack (not history)
340        let line: String = self.zleline.iter().collect();
341        if !line.is_empty() {
342            // Would push to buffer stack
343            // For now, just clear the line
344            self.zleline.clear();
345            self.zlell = 0;
346            self.zlecs = 0;
347            self.resetneeded = true;
348        }
349    }
350
351    /// Accept line and go to next history (for walking through history executing each)
352    /// Port of acceptlineanddownhistory() from zle_hist.c
353    pub fn accept_line_and_down_history(&mut self, hist: &mut History) -> Option<String> {
354        let line: String = self.zleline.iter().collect();
355
356        // Move to next history entry for next iteration
357        if hist.cursor < hist.entries.len() {
358            hist.cursor += 1;
359            if let Some(entry) = hist.entries.get(hist.cursor) {
360                self.zleline = entry.line.chars().collect();
361                self.zlell = self.zleline.len();
362                self.zlecs = self.zlell;
363            }
364        }
365
366        Some(line)
367    }
368
369    /// Vi fetch history - go to specific history entry by number
370    /// Port of vifetchhistory() from zle_hist.c
371    pub fn vi_fetch_history(&mut self, hist: &mut History, num: usize) {
372        if num > 0 && num <= hist.entries.len() {
373            if hist.saved_line.is_none() {
374                hist.saved_line = Some(self.zleline.clone());
375                hist.saved_cs = self.zlecs;
376            }
377
378            hist.cursor = num - 1;
379            if let Some(entry) = hist.entries.get(hist.cursor) {
380                self.zleline = entry.line.chars().collect();
381                self.zlell = self.zleline.len();
382                self.zlecs = 0;
383                self.resetneeded = true;
384            }
385        }
386    }
387
388    /// Vi history search backward
389    /// Port of vihistorysearchbackward() from zle_hist.c
390    pub fn vi_history_search_backward(&mut self, hist: &mut History, pattern: &str) {
391        hist.search_pattern = pattern.to_string();
392        hist.search_backward = true;
393
394        if let Some(entry) = hist.search_backward(pattern) {
395            self.zleline = entry.line.chars().collect();
396            self.zlell = self.zleline.len();
397            self.zlecs = 0;
398            self.resetneeded = true;
399        }
400    }
401
402    /// Vi history search forward
403    /// Port of vihistorysearchforward() from zle_hist.c
404    pub fn vi_history_search_forward(&mut self, hist: &mut History, pattern: &str) {
405        hist.search_pattern = pattern.to_string();
406        hist.search_backward = false;
407
408        if let Some(entry) = hist.search_forward(pattern) {
409            self.zleline = entry.line.chars().collect();
410            self.zlell = self.zleline.len();
411            self.zlecs = 0;
412            self.resetneeded = true;
413        }
414    }
415
416    /// Vi repeat search
417    /// Port of virepeatsearch() from zle_hist.c
418    pub fn vi_repeat_search(&mut self, hist: &mut History) {
419        let pattern = hist.search_pattern.clone();
420        if hist.search_backward {
421            self.vi_history_search_backward(hist, &pattern);
422        } else {
423            self.vi_history_search_forward(hist, &pattern);
424        }
425    }
426
427    /// Vi reverse repeat search
428    /// Port of virevrepeatsearch() from zle_hist.c
429    pub fn vi_rev_repeat_search(&mut self, hist: &mut History) {
430        let pattern = hist.search_pattern.clone();
431        if hist.search_backward {
432            self.vi_history_search_forward(hist, &pattern);
433        } else {
434            self.vi_history_search_backward(hist, &pattern);
435        }
436    }
437
438    /// Set local history mode
439    /// Port of setlocalhistory() from zle_hist.c
440    pub fn set_local_history(&mut self, _local: bool) {
441        // Local history restricts to current session
442        // TODO: implement session-based history filtering
443    }
444
445    /// Remember current line edits for history navigation
446    /// Port of remember_edits() from zle_hist.c
447    pub fn remember_edits(&mut self, hist: &mut History) {
448        if hist.cursor < hist.entries.len() {
449            // Store modified version of history entry
450            let line: String = self.zleline.iter().collect();
451            hist.entries[hist.cursor].line = line;
452        }
453    }
454
455    /// Forget remembered edits
456    /// Port of forget_edits() from zle_hist.c
457    pub fn forget_edits(&mut self, _hist: &mut History) {
458        // Would restore original history entries
459        // TODO: implement edit restoration
460    }
461}