Skip to main content

zsh/zle/
utils.rs

1//! ZLE utility functions
2//!
3//! Direct port from zsh/Src/Zle/zle_utils.c
4//!
5//! Implements:
6//! - Line manipulation: setline, sizeline, spaceinline, shiftchars
7//! - Undo: initundo, freeundo, handleundo, mkundoent, undo, redo
8//! - Cut/paste: cut, cuttext, foredel, backdel, forekill, backkill
9//! - Cursor: findbol, findeol, findline
10//! - Conversion: zlelineasstring, stringaszleline, zlecharasstring
11//! - Display: showmsg, printbind, handlefeep
12//! - Position save/restore: zle_save_positions, zle_restore_positions
13
14use super::main::{Zle, ZleChar, ZleString};
15
16impl Zle {
17    /// Insert string at cursor position
18    pub fn insert_str(&mut self, s: &str) {
19        for c in s.chars() {
20            self.zleline.insert(self.zlecs, c);
21            self.zlecs += 1;
22            self.zlell += 1;
23        }
24        self.resetneeded = true;
25    }
26
27    /// Insert chars at cursor position
28    pub fn insert_chars(&mut self, chars: &[ZleChar]) {
29        for &c in chars {
30            self.zleline.insert(self.zlecs, c);
31            self.zlecs += 1;
32            self.zlell += 1;
33        }
34        self.resetneeded = true;
35    }
36
37    /// Delete n characters at cursor position
38    pub fn delete_chars(&mut self, n: usize) {
39        let n = n.min(self.zlell - self.zlecs);
40        for _ in 0..n {
41            if self.zlecs < self.zlell {
42                self.zleline.remove(self.zlecs);
43                self.zlell -= 1;
44            }
45        }
46        self.resetneeded = true;
47    }
48
49    /// Delete n characters before cursor
50    pub fn backspace_chars(&mut self, n: usize) {
51        let n = n.min(self.zlecs);
52        for _ in 0..n {
53            if self.zlecs > 0 {
54                self.zlecs -= 1;
55                self.zleline.remove(self.zlecs);
56                self.zlell -= 1;
57            }
58        }
59        self.resetneeded = true;
60    }
61
62    /// Get the line as a string
63    pub fn get_line(&self) -> String {
64        self.zleline.iter().collect()
65    }
66
67    /// Set the line from a string
68    pub fn set_line(&mut self, s: &str) {
69        self.zleline = s.chars().collect();
70        self.zlell = self.zleline.len();
71        self.zlecs = self.zlecs.min(self.zlell);
72        self.resetneeded = true;
73    }
74
75    /// Clear the line
76    pub fn clear_line(&mut self) {
77        self.zleline.clear();
78        self.zlell = 0;
79        self.zlecs = 0;
80        self.mark = 0;
81        self.resetneeded = true;
82    }
83
84    /// Get region between point and mark
85    pub fn get_region(&self) -> &[ZleChar] {
86        let (start, end) = if self.zlecs < self.mark {
87            (self.zlecs, self.mark)
88        } else {
89            (self.mark, self.zlecs)
90        };
91        &self.zleline[start..end]
92    }
93
94    /// Cut to named buffer
95    pub fn cut_to_buffer(&mut self, buf: usize, append: bool) {
96        if buf < self.vibuf.len() {
97            let (start, end) = if self.zlecs < self.mark {
98                (self.zlecs, self.mark)
99            } else {
100                (self.mark, self.zlecs)
101            };
102
103            let text: ZleString = self.zleline[start..end].to_vec();
104
105            if append {
106                self.vibuf[buf].extend(text);
107            } else {
108                self.vibuf[buf] = text;
109            }
110        }
111    }
112
113    /// Paste from named buffer
114    pub fn paste_from_buffer(&mut self, buf: usize, after: bool) {
115        if buf < self.vibuf.len() {
116            let text = self.vibuf[buf].clone();
117            if !text.is_empty() {
118                if after && self.zlecs < self.zlell {
119                    self.zlecs += 1;
120                }
121                self.insert_chars(&text);
122            }
123        }
124    }
125}
126
127/// Metafication helpers (for compatibility with zsh's metafied strings)
128pub fn metafy(s: &str) -> String {
129    // In zsh, Meta (0x83) is used to escape special bytes
130    // For Rust we typically don't need this, but provide for compatibility
131    s.to_string()
132}
133
134pub fn unmetafy(s: &str) -> String {
135    s.to_string()
136}
137
138/// String width calculation
139pub fn strwidth(s: &str) -> usize {
140    // TODO: use unicode-width for proper width calculation
141    s.chars().count()
142}
143
144/// Check if character is printable
145pub fn is_printable(c: char) -> bool {
146    !c.is_control() && c != '\x7f'
147}
148
149/// Escape special characters for display
150pub fn escape_for_display(c: char) -> String {
151    if c.is_control() {
152        if c as u32 <= 26 {
153            format!("^{}", (c as u8 + b'@') as char)
154        } else {
155            format!("\\x{:02x}", c as u32)
156        }
157    } else if c == '\x7f' {
158        "^?".to_string()
159    } else {
160        c.to_string()
161    }
162}
163
164/// Undo entry structure
165/// Port of struct change from zle_utils.c
166#[derive(Debug, Clone)]
167pub struct UndoEntry {
168    /// Start position of change
169    pub start: usize,
170    /// End position of change (original)
171    pub end: usize,
172    /// Inserted/deleted text
173    pub text: ZleString,
174    /// Cursor position before change
175    pub cursor: usize,
176    /// Whether this is the start of a group
177    pub group_start: bool,
178}
179
180/// Undo state
181#[derive(Debug, Default)]
182pub struct UndoState {
183    /// Undo history
184    pub history: Vec<UndoEntry>,
185    /// Current position in undo history
186    pub current: usize,
187    /// Undo limit (where to stop)
188    pub limit: usize,
189    /// Whether changes are being recorded
190    pub recording: bool,
191    /// Merge sequential inserts
192    pub merge_inserts: bool,
193}
194
195impl UndoState {
196    pub fn new() -> Self {
197        UndoState {
198            recording: true,
199            merge_inserts: true,
200            ..Default::default()
201        }
202    }
203
204    /// Initialize undo system
205    /// Port of initundo() from zle_utils.c
206    pub fn init(&mut self) {
207        self.history.clear();
208        self.current = 0;
209        self.limit = 0;
210        self.recording = true;
211    }
212
213    /// Free undo history
214    /// Port of freeundo() from zle_utils.c
215    pub fn free(&mut self) {
216        self.history.clear();
217        self.current = 0;
218    }
219
220    /// Create an undo entry
221    /// Port of mkundoent() from zle_utils.c
222    pub fn make_entry(&mut self, start: usize, end: usize, text: ZleString, cursor: usize) {
223        if !self.recording {
224            return;
225        }
226
227        // Remove any entries after current position (redo history)
228        self.history.truncate(self.current);
229
230        let entry = UndoEntry {
231            start,
232            end,
233            text,
234            cursor,
235            group_start: false,
236        };
237
238        self.history.push(entry);
239        self.current = self.history.len();
240    }
241
242    /// Split undo (start a new undo group)
243    /// Port of splitundo() from zle_utils.c
244    pub fn split(&mut self) {
245        if let Some(entry) = self.history.last_mut() {
246            entry.group_start = true;
247        }
248    }
249
250    /// Merge with previous undo entry
251    /// Port of mergeundo() from zle_utils.c
252    pub fn merge(&mut self) {
253        // For sequential character inserts, merge into one undo
254        if self.history.len() >= 2 {
255            let last = self.history.len() - 1;
256            let prev = last - 1;
257
258            // Check if mergeable (consecutive inserts at same position)
259            if self.history[prev].end == self.history[last].start
260                && self.history[prev].text.is_empty()
261                && self.history[last].text.is_empty()
262            {
263                self.history[prev].end = self.history[last].end;
264                self.history.pop();
265                self.current = self.history.len();
266            }
267        }
268    }
269
270    /// Get current change
271    /// Port of get_undo_current_change() from zle_utils.c
272    pub fn get_current(&self) -> Option<&UndoEntry> {
273        if self.current > 0 {
274            self.history.get(self.current - 1)
275        } else {
276            None
277        }
278    }
279
280    /// Set undo limit
281    /// Port of set_undo_limit_change() from zle_utils.c
282    pub fn set_limit(&mut self) {
283        self.limit = self.current;
284    }
285
286    /// Get undo limit
287    /// Port of get_undo_limit_change() from zle_utils.c
288    pub fn get_limit(&self) -> usize {
289        self.limit
290    }
291}
292
293impl Zle {
294    /// Apply an undo entry
295    /// Port of applychange() from zle_utils.c
296    fn apply_change(&mut self, entry: &UndoEntry, reverse: bool) {
297        if reverse {
298            // Redo: re-insert removed text
299            let removed: ZleString = self.zleline.drain(entry.start..entry.end).collect();
300            for (i, &c) in entry.text.iter().enumerate() {
301                self.zleline.insert(entry.start + i, c);
302            }
303            self.zlell = self.zleline.len();
304            self.zlecs = entry.cursor;
305
306            // Store for undo
307            let _ = removed;
308        } else {
309            // Undo: remove inserted text and restore old
310            let end = entry.start + entry.text.len();
311            self.zleline.drain(entry.start..end.min(self.zleline.len()));
312            for (i, &c) in entry.text.iter().enumerate() {
313                self.zleline.insert(entry.start + i, c);
314            }
315            self.zlell = self.zleline.len();
316            self.zlecs = entry.cursor;
317        }
318        self.resetneeded = true;
319    }
320
321    /// Find beginning of line from position
322    /// Port of findbol() from zle_utils.c
323    pub fn find_bol(&self, pos: usize) -> usize {
324        let mut p = pos;
325        while p > 0 && self.zleline.get(p - 1) != Some(&'\n') {
326            p -= 1;
327        }
328        p
329    }
330
331    /// Find end of line from position
332    /// Port of findeol() from zle_utils.c
333    pub fn find_eol(&self, pos: usize) -> usize {
334        let mut p = pos;
335        while p < self.zlell && self.zleline.get(p) != Some(&'\n') {
336            p += 1;
337        }
338        p
339    }
340
341    /// Find line number for position
342    /// Port of findline() from zle_utils.c
343    pub fn find_line(&self, pos: usize) -> usize {
344        self.zleline[..pos].iter().filter(|&&c| c == '\n').count()
345    }
346
347    /// Ensure line has enough space
348    /// Port of sizeline() from zle_utils.c
349    pub fn size_line(&mut self, needed: usize) {
350        if self.zleline.capacity() < needed {
351            self.zleline.reserve(needed - self.zleline.len());
352        }
353    }
354
355    /// Make space in line at position
356    /// Port of spaceinline() from zle_utils.c
357    pub fn space_in_line(&mut self, pos: usize, count: usize) {
358        for _ in 0..count {
359            self.zleline.insert(pos, ' ');
360        }
361        self.zlell += count;
362        if self.zlecs >= pos {
363            self.zlecs += count;
364        }
365    }
366
367    /// Shift characters in line
368    /// Port of shiftchars() from zle_utils.c
369    pub fn shift_chars(&mut self, from: usize, count: i32) {
370        if count > 0 {
371            for _ in 0..count {
372                self.zleline.insert(from, ' ');
373            }
374            self.zlell += count as usize;
375        } else if count < 0 {
376            let to_remove = (-count) as usize;
377            for _ in 0..to_remove.min(self.zlell - from) {
378                self.zleline.remove(from);
379            }
380            self.zlell = self.zleline.len();
381        }
382    }
383
384    /// Delete forward
385    /// Port of foredel() from zle_utils.c
386    pub fn fore_del(&mut self, count: usize, flags: CutFlags) {
387        let count = count.min(self.zlell - self.zlecs);
388        if count == 0 {
389            return;
390        }
391
392        // Save to kill ring if requested
393        if flags.contains(CutFlags::KILL) {
394            let text: ZleString = self.zleline[self.zlecs..self.zlecs + count].to_vec();
395            self.killring.push_front(text);
396            if self.killring.len() > self.killringmax {
397                self.killring.pop_back();
398            }
399        }
400
401        // Delete
402        for _ in 0..count {
403            self.zleline.remove(self.zlecs);
404        }
405        self.zlell -= count;
406        self.resetneeded = true;
407    }
408
409    /// Delete backward
410    /// Port of backdel() from zle_utils.c
411    pub fn back_del(&mut self, count: usize, flags: CutFlags) {
412        let count = count.min(self.zlecs);
413        if count == 0 {
414            return;
415        }
416
417        // Save to kill ring if requested
418        if flags.contains(CutFlags::KILL) {
419            let text: ZleString = self.zleline[self.zlecs - count..self.zlecs].to_vec();
420            self.killring.push_front(text);
421            if self.killring.len() > self.killringmax {
422                self.killring.pop_back();
423            }
424        }
425
426        // Delete
427        self.zlecs -= count;
428        for _ in 0..count {
429            self.zleline.remove(self.zlecs);
430        }
431        self.zlell -= count;
432        self.resetneeded = true;
433    }
434
435    /// Kill forward
436    /// Port of forekill() from zle_utils.c
437    pub fn fore_kill(&mut self, count: usize, append: bool) {
438        let count = count.min(self.zlell - self.zlecs);
439        if count == 0 {
440            return;
441        }
442
443        let text: ZleString = self.zleline[self.zlecs..self.zlecs + count].to_vec();
444
445        if append {
446            if let Some(front) = self.killring.front_mut() {
447                front.extend(text);
448            } else {
449                self.killring.push_front(text);
450            }
451        } else {
452            self.killring.push_front(text);
453        }
454
455        if self.killring.len() > self.killringmax {
456            self.killring.pop_back();
457        }
458
459        for _ in 0..count {
460            self.zleline.remove(self.zlecs);
461        }
462        self.zlell -= count;
463        self.resetneeded = true;
464    }
465
466    /// Kill backward
467    /// Port of backkill() from zle_utils.c
468    pub fn back_kill(&mut self, count: usize, append: bool) {
469        let count = count.min(self.zlecs);
470        if count == 0 {
471            return;
472        }
473
474        let text: ZleString = self.zleline[self.zlecs - count..self.zlecs].to_vec();
475
476        if append {
477            if let Some(front) = self.killring.front_mut() {
478                let mut new_text = text;
479                new_text.extend(front.iter());
480                *front = new_text;
481            } else {
482                self.killring.push_front(text);
483            }
484        } else {
485            self.killring.push_front(text);
486        }
487
488        if self.killring.len() > self.killringmax {
489            self.killring.pop_back();
490        }
491
492        self.zlecs -= count;
493        for _ in 0..count {
494            self.zleline.remove(self.zlecs);
495        }
496        self.zlell -= count;
497        self.resetneeded = true;
498    }
499
500    /// Cut text to buffer
501    /// Port of cut() / cuttext() from zle_utils.c
502    pub fn cut_text(&mut self, start: usize, end: usize, dir: CutDirection) {
503        if start >= end || end > self.zlell {
504            return;
505        }
506
507        let text: ZleString = self.zleline[start..end].to_vec();
508
509        match dir {
510            CutDirection::Front => {
511                self.killring.push_front(text);
512            }
513            CutDirection::Back => {
514                if let Some(front) = self.killring.front_mut() {
515                    front.extend(text);
516                } else {
517                    self.killring.push_front(text);
518                }
519            }
520        }
521
522        if self.killring.len() > self.killringmax {
523            self.killring.pop_back();
524        }
525    }
526
527    /// Set the last line (for history)
528    /// Port of setlastline() from zle_utils.c
529    pub fn set_last_line(&mut self) {
530        // Would store current line as last line
531    }
532
533    /// Show a message
534    /// Port of showmsg() from zle_utils.c
535    pub fn show_msg(&self, msg: &str) {
536        eprintln!("{}", msg);
537    }
538
539    /// Handle a feep (beep/error)
540    /// Port of handlefeep() from zle_utils.c
541    pub fn handle_feep(&self) {
542        print!("\x07"); // Bell
543    }
544
545    /// Add text to line at position
546    /// Port of zleaddtoline() from zle_utils.c
547    pub fn add_to_line(&mut self, pos: usize, text: &str) {
548        for (i, c) in text.chars().enumerate() {
549            self.zleline.insert(pos + i, c);
550        }
551        self.zlell += text.chars().count();
552        if self.zlecs >= pos {
553            self.zlecs += text.chars().count();
554        }
555        self.resetneeded = true;
556    }
557
558    /// Get line as string
559    /// Port of zlelineasstring() from zle_utils.c
560    pub fn line_as_string(&self) -> String {
561        self.zleline.iter().collect()
562    }
563
564    /// Set line from string
565    /// Port of stringaszleline() from zle_utils.c
566    pub fn string_as_line(&mut self, s: &str) {
567        self.zleline = s.chars().collect();
568        self.zlell = self.zleline.len();
569        if self.zlecs > self.zlell {
570            self.zlecs = self.zlell;
571        }
572        self.resetneeded = true;
573    }
574
575    /// Get ZLE line
576    /// Port of zlegetline() from zle_utils.c
577    pub fn get_zle_line(&self) -> &[ZleChar] {
578        &self.zleline
579    }
580
581    /// Get ZLE query (for menu selection etc)
582    /// Port of getzlequery() from zle_utils.c
583    pub fn get_zle_query(&self) -> Option<String> {
584        // Would prompt for input
585        None
586    }
587
588    /// Handle suffix (for completion)
589    /// Port of handlesuffix() from zle_utils.c
590    pub fn handle_suffix(&mut self) {
591        // Would handle completion suffix removal
592    }
593}
594
595/// Saved position state
596#[derive(Debug, Clone)]
597pub struct SavedPositions {
598    pub zlecs: usize,
599    pub zlell: usize,
600    pub mark: usize,
601}
602
603/// Position save/restore
604/// Port of zle_save_positions() / zle_restore_positions() from zle_utils.c
605impl Zle {
606    pub fn save_positions(&self) -> SavedPositions {
607        SavedPositions {
608            zlecs: self.zlecs,
609            zlell: self.zlell,
610            mark: self.mark,
611        }
612    }
613
614    pub fn restore_positions(&mut self, saved: &SavedPositions) {
615        self.zlecs = saved.zlecs.min(self.zlell);
616        self.mark = saved.mark.min(self.zlell);
617    }
618}
619
620bitflags::bitflags! {
621    /// Flags for cut operations
622    #[derive(Debug, Clone, Copy, Default)]
623    pub struct CutFlags: u32 {
624        const KILL = 1 << 0;   // Add to kill ring
625        const COPY = 1 << 1;   // Don't delete, just copy
626        const APPEND = 1 << 2; // Append to kill ring
627    }
628}
629
630/// Direction for cut operations
631#[derive(Debug, Clone, Copy)]
632pub enum CutDirection {
633    Front,
634    Back,
635}
636
637/// Print a key binding for display
638/// Port of printbind() from zle_utils.c
639pub fn print_bind(seq: &[u8]) -> String {
640    let mut result = String::new();
641
642    for &b in seq {
643        match b {
644            0x1b => result.push_str("^["),
645            0..=31 => {
646                result.push('^');
647                result.push((b + 64) as char);
648            }
649            127 => result.push_str("^?"),
650            128..=159 => {
651                result.push_str("^[^");
652                result.push((b - 64) as char);
653            }
654            _ => result.push(b as char),
655        }
656    }
657
658    result
659}
660
661/// Call ZLE hook
662/// Port of zlecallhook() from zle_utils.c  
663pub fn zle_call_hook(_name: &str, _args: &[&str]) -> i32 {
664    // Would call user-defined hook function
665    0
666}