Skip to main content

sveltui_code/
buffer.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use std::cell::RefCell;
3use std::collections::HashMap;
4use sveltui::prelude::FocusId;
5
6thread_local! {
7    static CODE_BUFFER_MAP: RefCell<HashMap<FocusId, CodeBuffer>> = RefCell::new(HashMap::new());
8}
9
10pub fn with_code_buffer<R>(id: FocusId, f: impl FnOnce(&CodeBuffer) -> R) -> R {
11    CODE_BUFFER_MAP.with(|m| {
12        let mut map = m.borrow_mut();
13        let buf = map.entry(id).or_default();
14        f(buf)
15    })
16}
17
18pub fn with_code_buffer_mut<R>(id: FocusId, f: impl FnOnce(&mut CodeBuffer) -> R) -> R {
19    CODE_BUFFER_MAP.with(|m| {
20        let mut map = m.borrow_mut();
21        let buf = map.entry(id).or_default();
22        f(buf)
23    })
24}
25
26pub fn get_code_buffer(id: FocusId) -> CodeBuffer {
27    with_code_buffer(id, |b| b.clone())
28}
29
30pub fn set_code_buffer(id: FocusId, buf: CodeBuffer) {
31    CODE_BUFFER_MAP.with(|m| {
32        m.borrow_mut().insert(id, buf);
33    });
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37struct HistoryEntry {
38    lines: Vec<String>,
39    cursor_row: usize,
40    cursor_col: usize,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct CodeBuffer {
45    lines: Vec<String>,
46    pub cursor_row: usize,
47    pub cursor_col: usize,
48    pub scroll_row: usize,
49    pub scroll_col: usize,
50    preferred_col: Option<usize>,
51    pub tab_size: usize,
52    undo_stack: Vec<HistoryEntry>,
53    redo_stack: Vec<HistoryEntry>,
54}
55
56impl Default for CodeBuffer {
57    fn default() -> Self {
58        Self::new("")
59    }
60}
61
62pub fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
63    s.char_indices()
64        .nth(char_idx)
65        .map(|(b, _)| b)
66        .unwrap_or(s.len())
67}
68
69impl CodeBuffer {
70    pub fn new(content: impl Into<String>) -> Self {
71        let s = content.into();
72        let lines: Vec<String> = if s.is_empty() {
73            vec![String::new()]
74        } else {
75            let mut l: Vec<String> = s.lines().map(|line| line.to_string()).collect();
76            if s.ends_with('\n') {
77                l.push(String::new());
78            }
79            if l.is_empty() { vec![String::new()] } else { l }
80        };
81
82        let cursor_row = lines.len().saturating_sub(1);
83        let cursor_col = lines[cursor_row].chars().count();
84
85        Self {
86            lines,
87            cursor_row,
88            cursor_col,
89            scroll_row: 0,
90            scroll_col: 0,
91            preferred_col: None,
92            tab_size: 4,
93            undo_stack: Vec::new(),
94            redo_stack: Vec::new(),
95        }
96    }
97
98    fn push_undo(&mut self) {
99        if self.undo_stack.len() >= 100 {
100            self.undo_stack.remove(0);
101        }
102        self.undo_stack.push(HistoryEntry {
103            lines: self.lines.clone(),
104            cursor_row: self.cursor_row,
105            cursor_col: self.cursor_col,
106        });
107        self.redo_stack.clear();
108    }
109
110    pub fn undo(&mut self) -> bool {
111        let Some(prev) = self.undo_stack.pop() else {
112            return false;
113        };
114        self.redo_stack.push(HistoryEntry {
115            lines: self.lines.clone(),
116            cursor_row: self.cursor_row,
117            cursor_col: self.cursor_col,
118        });
119        self.lines = prev.lines;
120        self.cursor_row = prev.cursor_row.min(self.lines.len().saturating_sub(1));
121        self.cursor_col = prev.cursor_col.min(self.line_len(self.cursor_row));
122        self.preferred_col = None;
123        true
124    }
125
126    pub fn redo(&mut self) -> bool {
127        let Some(next) = self.redo_stack.pop() else {
128            return false;
129        };
130        self.undo_stack.push(HistoryEntry {
131            lines: self.lines.clone(),
132            cursor_row: self.cursor_row,
133            cursor_col: self.cursor_col,
134        });
135        self.lines = next.lines;
136        self.cursor_row = next.cursor_row.min(self.lines.len().saturating_sub(1));
137        self.cursor_col = next.cursor_col.min(self.line_len(self.cursor_row));
138        self.preferred_col = None;
139        true
140    }
141
142    pub fn content(&self) -> String {
143        self.lines.join("\n")
144    }
145
146    pub fn set_content(&mut self, text: impl Into<String>) {
147        let s = text.into();
148        self.lines = if s.is_empty() {
149            vec![String::new()]
150        } else {
151            let mut l: Vec<String> = s.lines().map(|line| line.to_string()).collect();
152            if s.ends_with('\n') {
153                l.push(String::new());
154            }
155            if l.is_empty() { vec![String::new()] } else { l }
156        };
157        let max_row = self.lines.len().saturating_sub(1);
158        self.cursor_row = self.cursor_row.min(max_row);
159        self.cursor_col = self.cursor_col.min(self.line_len(self.cursor_row));
160        self.preferred_col = None;
161    }
162
163    pub fn lines(&self) -> &[String] {
164        &self.lines
165    }
166
167    pub fn line(&self, row: usize) -> Option<&str> {
168        self.lines.get(row).map(|s| s.as_str())
169    }
170
171    pub fn line_count(&self) -> usize {
172        self.lines.len()
173    }
174
175    pub fn line_len(&self, row: usize) -> usize {
176        self.lines.get(row).map(|l| l.chars().count()).unwrap_or(0)
177    }
178
179    pub fn is_empty(&self) -> bool {
180        self.lines.is_empty() || (self.lines.len() == 1 && self.lines[0].is_empty())
181    }
182
183    pub fn cursor_pos(&self) -> (usize, usize) {
184        (self.cursor_row, self.cursor_col)
185    }
186
187    pub fn set_cursor_pos(&mut self, row: usize, col: usize) {
188        let max_row = self.lines.len().saturating_sub(1);
189        self.cursor_row = row.min(max_row);
190        self.cursor_col = col.min(self.line_len(self.cursor_row));
191        self.preferred_col = None;
192    }
193
194    // --- Cursor Navigation ---
195
196    pub fn move_left(&mut self) {
197        if self.cursor_col > 0 {
198            self.cursor_col -= 1;
199            self.preferred_col = None;
200        } else if self.cursor_row > 0 {
201            self.cursor_row -= 1;
202            self.cursor_col = self.line_len(self.cursor_row);
203            self.preferred_col = None;
204        }
205    }
206
207    pub fn move_right(&mut self) {
208        let cur_line_len = self.line_len(self.cursor_row);
209        if self.cursor_col < cur_line_len {
210            self.cursor_col += 1;
211            self.preferred_col = None;
212        } else if self.cursor_row + 1 < self.lines.len() {
213            self.cursor_row += 1;
214            self.cursor_col = 0;
215            self.preferred_col = None;
216        }
217    }
218
219    pub fn move_up(&mut self) {
220        if self.cursor_row > 0 {
221            let target_col = self.preferred_col.unwrap_or(self.cursor_col);
222            self.preferred_col = Some(target_col);
223            self.cursor_row -= 1;
224            self.cursor_col = target_col.min(self.line_len(self.cursor_row));
225        }
226    }
227
228    pub fn move_down(&mut self) {
229        if self.cursor_row + 1 < self.lines.len() {
230            let target_col = self.preferred_col.unwrap_or(self.cursor_col);
231            self.preferred_col = Some(target_col);
232            self.cursor_row += 1;
233            self.cursor_col = target_col.min(self.line_len(self.cursor_row));
234        }
235    }
236
237    pub fn move_home(&mut self) {
238        // Smart home: toggle between first non-whitespace character and column 0
239        let cur_line = &self.lines[self.cursor_row];
240        let first_non_ws = cur_line.chars().take_while(|c| c.is_whitespace()).count();
241
242        if self.cursor_col == first_non_ws {
243            self.cursor_col = 0;
244        } else {
245            self.cursor_col = first_non_ws;
246        }
247        self.preferred_col = None;
248    }
249
250    pub fn move_end(&mut self) {
251        self.cursor_col = self.line_len(self.cursor_row);
252        self.preferred_col = None;
253    }
254
255    pub fn move_top(&mut self) {
256        self.cursor_row = 0;
257        self.cursor_col = 0;
258        self.preferred_col = None;
259    }
260
261    pub fn move_bottom(&mut self) {
262        self.cursor_row = self.lines.len().saturating_sub(1);
263        self.cursor_col = self.line_len(self.cursor_row);
264        self.preferred_col = None;
265    }
266
267    pub fn page_up(&mut self, page_size: usize) {
268        let target_col = self.preferred_col.unwrap_or(self.cursor_col);
269        self.preferred_col = Some(target_col);
270        self.cursor_row = self.cursor_row.saturating_sub(page_size);
271        self.cursor_col = target_col.min(self.line_len(self.cursor_row));
272    }
273
274    pub fn page_down(&mut self, page_size: usize) {
275        let target_col = self.preferred_col.unwrap_or(self.cursor_col);
276        self.preferred_col = Some(target_col);
277        let max_row = self.lines.len().saturating_sub(1);
278        self.cursor_row = (self.cursor_row + page_size).min(max_row);
279        self.cursor_col = target_col.min(self.line_len(self.cursor_row));
280    }
281
282    pub fn move_word_left(&mut self) {
283        if self.cursor_col == 0 {
284            if self.cursor_row > 0 {
285                self.move_left();
286            }
287            return;
288        }
289        let line = &self.lines[self.cursor_row];
290        let chars: Vec<char> = line.chars().collect();
291        let mut idx = self.cursor_col;
292
293        while idx > 0 && chars[idx - 1].is_whitespace() {
294            idx -= 1;
295        }
296        while idx > 0 && !chars[idx - 1].is_whitespace() {
297            idx -= 1;
298        }
299        self.cursor_col = idx;
300        self.preferred_col = None;
301    }
302
303    pub fn move_word_right(&mut self) {
304        let cur_len = self.line_len(self.cursor_row);
305        if self.cursor_col >= cur_len {
306            if self.cursor_row + 1 < self.lines.len() {
307                self.move_right();
308            }
309            return;
310        }
311        let line = &self.lines[self.cursor_row];
312        let chars: Vec<char> = line.chars().collect();
313        let mut idx = self.cursor_col;
314
315        while idx < cur_len && !chars[idx].is_whitespace() {
316            idx += 1;
317        }
318        while idx < cur_len && chars[idx].is_whitespace() {
319            idx += 1;
320        }
321        self.cursor_col = idx;
322        self.preferred_col = None;
323    }
324
325    // --- Edits ---
326
327    pub fn insert_char(&mut self, c: char) {
328        self.push_undo();
329        if c == '\n' {
330            self.insert_newline();
331        } else {
332            let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
333            self.lines[self.cursor_row].insert(byte_pos, c);
334            self.cursor_col += 1;
335            self.preferred_col = None;
336        }
337    }
338
339    pub fn insert_str(&mut self, s: &str) {
340        self.push_undo();
341        for c in s.chars() {
342            if c == '\n' {
343                let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
344                let rest = self.lines[self.cursor_row][byte_pos..].to_string();
345                self.lines[self.cursor_row].truncate(byte_pos);
346                self.cursor_row += 1;
347                self.lines.insert(self.cursor_row, rest);
348                self.cursor_col = 0;
349            } else {
350                let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
351                self.lines[self.cursor_row].insert(byte_pos, c);
352                self.cursor_col += 1;
353            }
354        }
355        self.preferred_col = None;
356    }
357
358    pub fn insert_tab(&mut self) {
359        let spaces = " ".repeat(self.tab_size);
360        self.insert_str(&spaces);
361    }
362
363    pub fn insert_newline(&mut self) {
364        let curr_line = &self.lines[self.cursor_row];
365        let byte_pos = char_to_byte_index(curr_line, self.cursor_col);
366
367        let before_cursor = &curr_line[..byte_pos];
368        let rest = curr_line[byte_pos..].to_string();
369
370        // Calculate leading indentation of the current line
371        let mut indent: String = before_cursor
372            .chars()
373            .take_while(|c| *c == ' ' || *c == '\t')
374            .collect();
375
376        // If the line ended in opening bracket, increase indentation
377        let trimmed_before = before_cursor.trim_end();
378        if trimmed_before.ends_with('{')
379            || trimmed_before.ends_with('(')
380            || trimmed_before.ends_with('[')
381            || trimmed_before.ends_with(':')
382        {
383            indent.push_str(&" ".repeat(self.tab_size));
384        }
385
386        self.lines[self.cursor_row].truncate(byte_pos);
387        self.cursor_row += 1;
388        let mut new_line = indent;
389        let new_col = new_line.chars().count();
390        new_line.push_str(&rest);
391        self.lines.insert(self.cursor_row, new_line);
392        self.cursor_col = new_col;
393        self.preferred_col = None;
394    }
395
396    pub fn backspace(&mut self) -> bool {
397        self.push_undo();
398        if self.cursor_col > 0 {
399            let line = &self.lines[self.cursor_row];
400            let is_in_leading_ws = line[..char_to_byte_index(line, self.cursor_col)]
401                .chars()
402                .all(|c| c == ' ');
403
404            // If we are in leading spaces and on a tab stop boundary, unindent
405            let del_chars = if is_in_leading_ws
406                && self.cursor_col >= self.tab_size
407                && (self.cursor_col % self.tab_size == 0)
408            {
409                self.tab_size
410            } else {
411                1
412            };
413
414            let del_idx = self.cursor_col.saturating_sub(del_chars);
415            let start_byte = char_to_byte_index(&self.lines[self.cursor_row], del_idx);
416            let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
417            self.lines[self.cursor_row].drain(start_byte..end_byte);
418            self.cursor_col = del_idx;
419            self.preferred_col = None;
420            true
421        } else if self.cursor_row > 0 {
422            let curr_line = self.lines.remove(self.cursor_row);
423            self.cursor_row -= 1;
424            self.cursor_col = self.line_len(self.cursor_row);
425            self.lines[self.cursor_row].push_str(&curr_line);
426            self.preferred_col = None;
427            true
428        } else {
429            false
430        }
431    }
432
433    pub fn delete(&mut self) -> bool {
434        self.push_undo();
435        let cur_len = self.line_len(self.cursor_row);
436        if self.cursor_col < cur_len {
437            let start_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
438            let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col + 1);
439            self.lines[self.cursor_row].drain(start_byte..end_byte);
440            self.preferred_col = None;
441            true
442        } else if self.cursor_row + 1 < self.lines.len() {
443            let next_line = self.lines.remove(self.cursor_row + 1);
444            self.lines[self.cursor_row].push_str(&next_line);
445            self.preferred_col = None;
446            true
447        } else {
448            false
449        }
450    }
451
452    pub fn delete_word_left(&mut self) -> bool {
453        if self.cursor_col > 0 {
454            self.push_undo();
455            let old_col = self.cursor_col;
456            self.move_word_left();
457            let new_col = self.cursor_col;
458            let start_byte = char_to_byte_index(&self.lines[self.cursor_row], new_col);
459            let end_byte = char_to_byte_index(&self.lines[self.cursor_row], old_col);
460            self.lines[self.cursor_row].drain(start_byte..end_byte);
461            self.preferred_col = None;
462            true
463        } else if self.cursor_row > 0 {
464            self.backspace()
465        } else {
466            false
467        }
468    }
469
470    pub fn delete_to_end(&mut self) -> bool {
471        let cur_len = self.line_len(self.cursor_row);
472        if self.cursor_col < cur_len {
473            self.push_undo();
474            let start_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
475            self.lines[self.cursor_row].truncate(start_byte);
476            self.preferred_col = None;
477            true
478        } else if self.cursor_row + 1 < self.lines.len() {
479            self.delete()
480        } else {
481            false
482        }
483    }
484
485    pub fn delete_to_start(&mut self) -> bool {
486        if self.cursor_col > 0 {
487            self.push_undo();
488            let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
489            self.lines[self.cursor_row].drain(0..end_byte);
490            self.cursor_col = 0;
491            self.preferred_col = None;
492            true
493        } else {
494            false
495        }
496    }
497
498    pub fn duplicate_line(&mut self) {
499        self.push_undo();
500        let current_line = self.lines[self.cursor_row].clone();
501        self.cursor_row += 1;
502        self.lines.insert(self.cursor_row, current_line);
503        self.preferred_col = None;
504    }
505
506    pub fn clear(&mut self) {
507        self.push_undo();
508        self.lines = vec![String::new()];
509        self.cursor_row = 0;
510        self.cursor_col = 0;
511        self.scroll_row = 0;
512        self.scroll_col = 0;
513        self.preferred_col = None;
514    }
515
516    pub fn update_viewport(&mut self, visible_width: usize, visible_height: usize) {
517        if visible_height > 0 {
518            if self.cursor_row < self.scroll_row {
519                self.scroll_row = self.cursor_row;
520            } else if self.cursor_row >= self.scroll_row + visible_height {
521                self.scroll_row = self.cursor_row.saturating_sub(visible_height) + 1;
522            }
523        }
524        if visible_width > 0 {
525            if self.cursor_col < self.scroll_col {
526                self.scroll_col = self.cursor_col;
527            } else if self.cursor_col >= self.scroll_col + visible_width {
528                self.scroll_col = self.cursor_col.saturating_sub(visible_width) + 1;
529            }
530        }
531    }
532
533    // --- Key Event Handling ---
534
535    pub fn accepts_key(&self, key: &KeyEvent) -> bool {
536        let control = key.modifiers.contains(KeyModifiers::CONTROL);
537        let alt = key.modifiers.contains(KeyModifiers::ALT);
538        match key.code {
539            KeyCode::Char(c) if control => {
540                matches!(c, 'w' | 'k' | 'u' | 'a' | 'e' | 'h' | 'd' | 'z' | 'y')
541            }
542            KeyCode::Char(_) => !alt,
543            KeyCode::Enter
544            | KeyCode::Tab
545            | KeyCode::BackTab
546            | KeyCode::Backspace
547            | KeyCode::Delete
548            | KeyCode::Left
549            | KeyCode::Right
550            | KeyCode::Up
551            | KeyCode::Down
552            | KeyCode::PageUp
553            | KeyCode::PageDown
554            | KeyCode::Home
555            | KeyCode::End => true,
556            _ => false,
557        }
558    }
559
560    pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
561        let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
562        let is_alt = key.modifiers.contains(KeyModifiers::ALT);
563        let is_shift = key.modifiers.contains(KeyModifiers::SHIFT);
564
565        match key.code {
566            KeyCode::Char('z') if is_ctrl => {
567                if is_shift {
568                    self.redo()
569                } else {
570                    self.undo()
571                }
572            }
573            KeyCode::Char('y') if is_ctrl => self.redo(),
574            KeyCode::Char('w') if is_ctrl => self.delete_word_left(),
575            KeyCode::Char('k') if is_ctrl => self.delete_to_end(),
576            KeyCode::Char('u') if is_ctrl => self.delete_to_start(),
577            KeyCode::Char('d') if is_ctrl => {
578                self.duplicate_line();
579                true
580            }
581            KeyCode::Char('a') if is_ctrl => {
582                self.move_home();
583                true
584            }
585            KeyCode::Char('e') if is_ctrl => {
586                self.move_end();
587                true
588            }
589            KeyCode::Char('h') if is_ctrl => self.backspace(),
590            KeyCode::Char(c) if !is_ctrl && !is_alt => {
591                self.insert_char(c);
592                true
593            }
594            KeyCode::Enter => {
595                self.push_undo();
596                self.insert_newline();
597                true
598            }
599            KeyCode::Tab => {
600                self.push_undo();
601                self.insert_tab();
602                true
603            }
604            KeyCode::BackTab => {
605                // Shift+Tab unindents current line
606                let line = &self.lines[self.cursor_row];
607                let leading_spaces = line.chars().take_while(|c| *c == ' ').count();
608                if leading_spaces > 0 {
609                    let remove_count = leading_spaces.min(self.tab_size);
610                    self.lines[self.cursor_row].drain(0..remove_count);
611                    self.cursor_col = self.cursor_col.saturating_sub(remove_count);
612                    true
613                } else {
614                    false
615                }
616            }
617            KeyCode::Backspace => {
618                if is_alt {
619                    self.delete_word_left()
620                } else {
621                    self.backspace()
622                }
623            }
624            KeyCode::Delete => self.delete(),
625            KeyCode::Left => {
626                if is_ctrl || is_alt {
627                    self.move_word_left();
628                } else {
629                    self.move_left();
630                }
631                true
632            }
633            KeyCode::Right => {
634                if is_ctrl || is_alt {
635                    self.move_word_right();
636                } else {
637                    self.move_right();
638                }
639                true
640            }
641            KeyCode::Up => {
642                if is_ctrl {
643                    self.move_top();
644                } else {
645                    self.move_up();
646                }
647                true
648            }
649            KeyCode::Down => {
650                if is_ctrl {
651                    self.move_bottom();
652                } else {
653                    self.move_down();
654                }
655                true
656            }
657            KeyCode::PageUp => {
658                self.page_up(10);
659                true
660            }
661            KeyCode::PageDown => {
662                self.page_down(10);
663                true
664            }
665            KeyCode::Home => {
666                if is_ctrl {
667                    self.move_top();
668                } else {
669                    self.move_home();
670                }
671                true
672            }
673            KeyCode::End => {
674                if is_ctrl {
675                    self.move_bottom();
676                } else {
677                    self.move_end();
678                }
679                true
680            }
681            _ => false,
682        }
683    }
684}