Skip to main content

ratatui_code_editor/
editor.rs

1use crossterm::event::{
2    KeyEvent, KeyModifiers,
3    MouseEvent, MouseEventKind, MouseButton,
4};
5use ratatui::style::Color;
6use ratatui::style::Style;
7use ratatui::prelude::*;
8use std::time::Duration;
9use crate::click::{ClickKind, ClickTracker};
10use crate::code::Code;
11use crate::code::{EditKind, EditBatch};
12use crate::code::{RopeGraphemes, grapheme_width_and_chars_len, grapheme_width};
13use crate::selection::{Selection, SelectionSnap};
14use crate::actions::*;
15use crate::utils;
16use std::collections::HashMap;
17use std::cell::RefCell;
18use std::cmp::Ordering;
19use anyhow::{Result, anyhow};
20
21// keyword and ratatui style
22type Theme = HashMap<String, Style>;
23// start byte, end byte, style
24type Hightlight = (usize, usize, Style);
25// start offset, end offset
26type HightlightCache = HashMap<(usize, usize), Vec<Hightlight>>;
27
28/// Represents the text editor, which holds the code buffer, cursor, selection,
29/// theme, scroll offsets, highlight cache, clipboard, and user mark intervals.
30pub struct Editor {
31    /// Code buffer and editing/highlighting logic for the current language
32    pub(crate) code: Code,
33    /// Current cursor position as a character index in the document
34    pub(crate) cursor: usize,
35
36    /// Vertical scroll offset: index of the first visible line
37    pub(crate) offset_y: usize,
38
39    /// Horizontal scroll offset in characters (visual columns)
40    pub(crate) offset_x: usize,
41
42    /// Syntax theme: mapping of token name to ratatui Style
43    pub(crate) theme: Theme,
44
45    /// Current text selection, if any
46    pub(crate) selection: Option<Selection>,
47
48    /// Click tracker to detect single/double/triple clicks
49    pub(crate) clicks: ClickTracker,
50
51    /// Selection snapping mode (to word, to line, or none)
52    pub(crate) selection_snap: SelectionSnap,
53
54    /// Fallback clipboard storage when the system clipboard is unavailable
55    pub(crate) clipboard: Option<String>,
56
57    /// User marks for intervals: (start, end, color)
58    pub(crate) marks: Option<Vec<(usize, usize, Color)>>,
59
60    /// Syntax highlight cache by intervals to speed up rendering
61    pub(crate) highlights_cache: RefCell<HightlightCache>,
62}
63
64impl Editor {
65    pub fn new(lang: &str, text: &str, theme: Vec<(&str, &str)>) -> Result<Self> {
66        Self::new_with_highlights(lang, text, theme, None)
67    }
68
69    pub fn new_with_highlights(
70        lang: &str,
71        text: &str,
72        theme: Vec<(&str, &str)>,
73        custom_highlights: Option<HashMap<String, String>>,
74    ) -> Result<Self> {
75        let code = Code::new(text, lang, custom_highlights.clone())
76            .or_else(|_| Code::new(text, "text", custom_highlights))?;
77
78        let theme = Self::build_theme(&theme);
79        let highlights_cache = RefCell::new(HashMap::new());
80
81        Ok(Self {
82            code,
83            cursor: 0,
84            offset_y: 0,
85            offset_x: 0,
86            theme,
87            selection: None,
88            clicks: ClickTracker::new(Duration::from_millis(700)),
89            selection_snap: SelectionSnap::None,
90            clipboard: None,
91            marks: None,
92            highlights_cache,
93        })
94    }
95
96    pub fn input(
97        &mut self, key: KeyEvent, area: &Rect,
98    ) -> Result<()> {
99        use crossterm::event::KeyCode;
100
101        let shift = key.modifiers.contains(KeyModifiers::SHIFT);
102        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
103        let _alt = key.modifiers.contains(KeyModifiers::ALT);
104
105        match key.code {
106            KeyCode::Char('รท') => self.apply(ToggleComment { }),
107            KeyCode::Char('z') if ctrl => self.apply(Undo { }),
108            KeyCode::Char('y') if ctrl => self.apply(Redo { }),
109            KeyCode::Char('c') if ctrl => self.apply(Copy { }),
110            KeyCode::Char('v') if ctrl => self.apply(Paste { }),
111            KeyCode::Char('x') if ctrl => self.apply(Cut { }),
112            KeyCode::Char('k') if ctrl => self.apply(DeleteLine { }),
113            KeyCode::Char('d') if ctrl => self.apply(Duplicate { }),
114            KeyCode::Char('a') if ctrl => self.apply(SelectAll { }),
115            KeyCode::Left      => self.apply(MoveLeft { shift }),
116            KeyCode::Right     => self.apply(MoveRight { shift }),
117            KeyCode::Up        => self.apply(MoveUp { shift }),
118            KeyCode::Down      => self.apply(MoveDown { shift }),
119            KeyCode::Backspace => self.apply(Delete { }),
120            KeyCode::Enter     => self.apply(InsertNewline { }),
121            KeyCode::Char(c)   => self.apply(InsertText { text: c.to_string() }),
122            KeyCode::Tab       => self.apply(Indent { }),
123            KeyCode::BackTab   => self.apply(UnIndent { }),
124            _ => {}
125        }
126        self.focus(&area);
127        Ok(())
128    }
129    
130    pub fn focus(&mut self, area: &Rect) {
131        let width = area.width as usize;
132        let height = area.height as usize;
133        let total_lines = self.code.len_lines();
134        let max_line_number = total_lines.max(1);
135        let line_number_digits = max_line_number.to_string().len().max(5);
136        let line_number_width = (line_number_digits + 2) as usize;
137
138        let line = self.code.char_to_line(self.cursor);
139        let col = self.cursor - self.code.line_to_char(line);
140    
141        let visible_width = width.saturating_sub(line_number_width);
142        let visible_height = height;
143    
144        let step_size = 10;
145        if col < self.offset_x {
146            self.offset_x = col.saturating_sub(step_size);
147        } else if col >= self.offset_x + visible_width {
148            self.offset_x = col.saturating_sub(visible_width - step_size);
149        }
150    
151        if line < self.offset_y {
152            self.offset_y = line;
153        } else if line >= self.offset_y + visible_height {
154            self.offset_y = line.saturating_sub(visible_height - 1);
155        }
156    }
157
158    pub fn mouse(
159        &mut self, mouse: MouseEvent, area: &Rect,
160    ) -> Result<()> {
161
162        match mouse.kind {
163            MouseEventKind::ScrollUp => self.scroll_up(),
164            MouseEventKind::ScrollDown => self.scroll_down(area.height as usize),
165            MouseEventKind::Down(MouseButton::Left) => {
166                let pos = self.cursor_from_mouse(mouse.column, mouse.row, area);
167                if let Some(cursor) = pos {
168                    self.handle_mouse_down(cursor);
169                }
170            }
171            MouseEventKind::Drag(MouseButton::Left) => {
172                // Auto-scroll when dragging on the last or first visible row
173                if mouse.row == area.top() {
174                    self.scroll_up();
175                }
176                if mouse.row == area.bottom().saturating_sub(1) {
177                    self.scroll_down(area.height as usize);
178                }
179                let pos = self.cursor_from_mouse(mouse.column, mouse.row, area);
180                if let Some(cursor) = pos {
181                    self.handle_mouse_drag(cursor);
182                }
183            }
184            MouseEventKind::Up(MouseButton::Left) => {
185                self.selection_snap = SelectionSnap::None;
186            }
187            _ => {}
188        }
189        Ok(())
190    }
191
192    fn handle_mouse_down(&mut self, cursor: usize) {
193        let kind = self.clicks.register(cursor);
194        let (start, end, snap) = match kind {
195            ClickKind::Triple => {
196                let (line_start, line_end) = self.code.line_boundaries(cursor);
197                (line_start, line_end, SelectionSnap::Line { anchor: cursor })
198            }
199            ClickKind::Double => {
200                let (word_start, word_end) = self.code.word_boundaries(cursor);
201                (word_start, word_end, SelectionSnap::Word { anchor: cursor })
202            }
203            ClickKind::Single => (cursor, cursor, SelectionSnap::None),
204        };
205
206        self.selection = Some(Selection::from_anchor_and_cursor(start, end));
207        self.cursor = end;
208        self.selection_snap = snap;
209    }
210
211    fn handle_mouse_drag(&mut self, cursor: usize) {
212        match self.selection_snap {
213            SelectionSnap::Line { anchor } => {
214                let (anchor_start, anchor_end) = self.code.line_boundaries(anchor);
215                let (cur_start, cur_end) = self.code.line_boundaries(cursor);
216        
217                let (sel_start, sel_end, new_cursor) = match cursor.cmp(&anchor) {
218                    Ordering::Greater => (anchor_start, cur_end, cur_end),   // forward
219                    Ordering::Less => (cur_start, anchor_end, cur_start),    // backward
220                    Ordering::Equal => (anchor_start, anchor_end, anchor_end), 
221                };
222        
223                self.selection = Some(Selection::from_anchor_and_cursor(sel_start, sel_end));
224                self.cursor = new_cursor;
225            }
226            SelectionSnap::Word { anchor } => {
227                let (anchor_start, anchor_end) = self.code.word_boundaries(anchor);
228                let (cur_start, cur_end) = self.code.word_boundaries(cursor);
229        
230                let (sel_start, sel_end, new_cursor) = match cursor.cmp(&anchor) {
231                    Ordering::Greater => (anchor_start, cur_end, cur_end),   // forward
232                    Ordering::Less => (cur_start, anchor_end, cur_start),    // backward
233                    Ordering::Equal => (anchor_start, anchor_end, anchor_end),
234                };
235        
236                self.selection = Some(Selection::from_anchor_and_cursor(sel_start, sel_end));
237                self.cursor = new_cursor;
238            }
239            SelectionSnap::None => {
240                let anchor = self.selection_anchor();
241                self.selection = Some(Selection::from_anchor_and_cursor(anchor, cursor));
242                self.cursor = cursor;
243            }
244        }
245    }
246
247    fn cursor_from_mouse(
248        &self, mouse_x: u16, mouse_y: u16, area: &Rect
249    ) -> Option<usize> {
250        let total_lines = self.code.len_lines();
251        let max_line_number = total_lines.max(1);
252        let line_number_digits = max_line_number.to_string().len().max(5);
253        let line_number_width = (line_number_digits + 2) as u16;
254    
255        if mouse_y < area.top()
256            || mouse_y >= area.bottom()
257            || mouse_x < area.left() + line_number_width
258        {
259            return None;
260        }
261    
262        let clicked_row = (mouse_y - area.top()) as usize + self.offset_y;
263        if clicked_row >= self.code.len_lines() {
264            return None;
265        }
266    
267        let clicked_col = (mouse_x - area.left() - line_number_width) as usize;
268    
269        let line_start_char = self.code.line_to_char(clicked_row);
270        let line_len = self.code.line_len(clicked_row);
271    
272        let start_col = self.offset_x.min(line_len);
273        let end_col = line_len;
274    
275        let char_start = line_start_char + start_col;
276        let char_end = line_start_char + end_col;
277    
278        let mut current_col = 0;
279        let mut char_idx = start_col;        
280        let visible_chars = self.code.char_slice(char_start, char_end);
281        for g in RopeGraphemes::new(&visible_chars) {
282            let (g_width, g_chars) = grapheme_width_and_chars_len(g);        
283            if current_col + g_width > clicked_col { break; }
284            current_col += g_width;
285            char_idx += g_chars;
286        }
287    
288        let line = self.code.char_slice(line_start_char, line_start_char + line_len);
289        let visual_width: usize = RopeGraphemes::new(&line).map(grapheme_width).sum();
290    
291        if clicked_col + self.offset_x >= visual_width {
292            let mut end_idx = line.len_chars();
293            if end_idx > 0 && line.char(end_idx - 1) == '\n' {
294                end_idx -= 1;
295            }
296            char_idx = end_idx;
297        }
298    
299        Some(line_start_char + char_idx)
300    }
301
302    /// Clears any active selection.
303    pub fn clear_selection(&mut self) {
304        self.selection = None;
305    }
306
307    /// Extends or starts a selection from the current cursor to `new_cursor`.
308    pub fn extend_selection(&mut self, new_cursor: usize) {
309        // If there was already a selection, preserve the anchor (start point)
310        // otherwise, use the current cursor as the anchor.
311        let anchor = self.selection_anchor();
312        self.selection = Some(Selection::from_anchor_and_cursor(anchor, new_cursor));
313    }
314    
315    /// Returns the selection anchor position, or the cursor if no selection exists.
316    pub fn selection_anchor(&self) -> usize {
317        self.selection
318            .as_ref()
319            .map(|s| if self.cursor == s.start { s.end } else { s.start })
320            .unwrap_or(self.cursor)
321    }
322
323    pub fn apply<A: Action>(&mut self, mut action: A) {
324        action.apply(self);
325    }
326
327    pub fn set_content(&mut self, content: &str) {
328        self.code.tx();
329        self.code.set_state_before(self.cursor, self.selection);
330        self.code.remove(0, self.code.len());
331        self.code.insert(0, content);
332        self.code.set_state_after(self.cursor, self.selection);
333        self.code.commit();
334        self.reset_highlight_cache();
335    }
336
337    pub fn apply_batch(&mut self, batch: &EditBatch) {
338        self.code.tx();
339
340        if let Some(state) = &batch.state_before {
341            self.code.set_state_before(state.offset, state.selection);
342        }
343        if let Some(state) = &batch.state_after {
344            self.code.set_state_after(state.offset, state.selection);
345        }
346        
347        for edit in &batch.edits {
348            match &edit.kind {
349                EditKind::Insert { offset, text } => {
350                    self.code.insert(*offset, text);
351                }
352                EditKind::Remove { offset, text } => {
353                    self.code.remove(*offset, *offset + text.chars().count());
354                }
355            }
356        }
357        self.code.commit();
358        self.reset_highlight_cache();
359    }
360
361    pub fn set_cursor(&mut self, cursor: usize) {
362        self.cursor = cursor;
363        self.fit_cursor();
364    }
365
366    pub fn fit_cursor(&mut self) {
367        // make sure cursor is not out of bounds 
368        let len = self.code.len_chars();
369        self.cursor = self.cursor.min(len);
370
371        // make sure cursor is not out of bounds on the line
372        let (row, col) = self.code.point(self.cursor);
373        if col > self.code.line_len(row) {
374            self.cursor = self.code.line_to_char(row) + self.code.line_len(row);
375        }
376    }
377
378    pub fn scroll_up(&mut self) {
379        if self.offset_y > 0 {
380            self.offset_y -= 1;
381        }
382    }
383
384    pub fn scroll_down(&mut self, area_height: usize) {
385        let len_lines = self.code.len_lines();
386        if self.offset_y < len_lines.saturating_sub(area_height) {
387            self.offset_y += 1;
388        }
389    }
390
391    fn build_theme(theme: &Vec<(&str, &str)>) -> Theme {
392        theme.into_iter()
393            .map(|(name, hex)| {
394                let (r, g, b) = utils::rgb(hex);
395                (name.to_string(), Style::default().fg(Color::Rgb(r, g, b)))
396            })
397            .collect()
398    }
399
400    pub fn get_content(&self) -> String {
401        self.code.get_content()
402    }
403
404    pub fn get_content_slice(&self, start: usize, end: usize) -> String {
405        self.code.slice(start, end)
406    }
407
408    pub fn get_cursor(&self) -> usize {
409        self.cursor
410    }
411
412    pub fn set_clipboard(&mut self, text: &str) -> Result<()> {
413        arboard::Clipboard::new()
414            .and_then(|mut c| c.set_text(text.to_string()))
415            .unwrap_or_else(|_| self.clipboard = Some(text.to_string()));
416        Ok(())
417    }
418
419    pub fn get_clipboard(&self) -> Result<String> {
420        arboard::Clipboard::new()
421            .and_then(|mut c| c.get_text())
422            .ok()
423            .or_else(|| self.clipboard.clone())
424            .ok_or_else(|| anyhow!("cant get clipboard"))
425    }
426
427    pub fn set_marks(&mut self, marks: Vec<(usize, usize, &str)>) {
428        self.marks = Some(
429            marks.into_iter()
430                .map(|(start, end, color)| {
431                    let (r, g, b) = utils::rgb(color);
432                    (start, end, Color::Rgb(r, g, b))
433                })
434                .collect()
435        );
436    }
437
438    pub fn remove_marks(&mut self) {
439        self.marks = None;
440    }
441
442    pub fn has_marks(&self) -> bool {
443        self.marks.is_some()
444    }
445
446    pub fn get_marks(&self) -> Option<&Vec<(usize, usize, Color)>> {
447        self.marks.as_ref()
448    }
449
450    pub fn get_selection_text(&mut self) -> Option<String> {
451        if let Some(selection) = &self.selection && !selection.is_empty() {
452            let text = self.code.slice(selection.start, selection.end);
453            return Some(text);
454        }
455        None
456    }
457
458    pub fn get_selection(&mut self) -> Option<Selection> {
459       return self.selection;
460    }
461
462    pub fn set_selection(&mut self, selection: Option<Selection>) {
463        self.selection = selection;
464    }
465
466    pub fn set_offset_y(&mut self, offset_y: usize) {
467        self.offset_y = offset_y;
468    }
469
470    pub fn set_offset_x(&mut self, offset_x: usize) {
471        self.offset_x = offset_x;
472    }
473    
474    pub fn get_offset_y(&self) -> usize {
475        self.offset_y
476    }
477
478    pub fn get_offset_x(&self) -> usize {
479        self.offset_x
480    }
481
482    pub fn code_mut(&mut self) -> &mut Code {
483        &mut self.code
484    }
485
486    pub fn code_ref(&self) -> &Code {
487        &self.code
488    }
489
490    /// Set the change callback function for handling document changes
491    pub fn set_change_callback(
492        &mut self, callback: Box<dyn Fn(Vec<(usize, usize, usize, usize, String)>)>
493    ) {
494        self.code.set_change_callback(callback);
495    }
496
497    pub fn highlight_interval(
498        &self, start: usize, end: usize, theme: &Theme
499    ) -> Vec<(usize, usize, Style)> {
500        let mut cache = self.highlights_cache.borrow_mut();
501        let key = (start, end);
502        if let Some(v) = cache.get(&key) {
503            return v.clone();
504        }
505
506        let highlights = self.code.highlight_interval(start, end, theme);
507        cache.insert(key, highlights.clone());
508        highlights
509    }
510
511    pub fn reset_highlight_cache(&self) {
512        self.highlights_cache.borrow_mut().clear();
513    }
514    
515    /// calculates visible cursor position 
516    pub fn get_visible_cursor(
517        &self, area: &Rect
518    ) -> Option<(u16, u16)> {
519        let total_lines = self.code.len_lines();
520        let max_line_number = total_lines.max(1);
521        let line_number_digits = max_line_number.to_string().len().max(5);
522        let line_number_width = line_number_digits + 2;
523
524        let (cursor_line, cursor_char_col) = self.code.point(self.cursor);
525        
526        if cursor_line >= self.offset_y && cursor_line < self.offset_y + area.height as usize {
527            let line_start_char = self.code.line_to_char(cursor_line);
528            let line_len = self.code.line_len(cursor_line);
529        
530            let max_x = (area.width as usize).saturating_sub(line_number_width);
531            let start_col = self.offset_x;
532                
533            let cursor_visual_col: usize = {
534                let slice = self.code.char_slice(line_start_char, line_start_char + cursor_char_col.min(line_len));
535                RopeGraphemes::new(&slice).map(grapheme_width).sum()
536            };
537            
538            let offset_visual_col: usize = {
539                let slice = self.code.char_slice(line_start_char, line_start_char + start_col.min(line_len));
540                RopeGraphemes::new(&slice).map(grapheme_width).sum()
541            };
542        
543            let relative_visual_col = cursor_visual_col.saturating_sub(offset_visual_col);
544            let visible_x = relative_visual_col.min(max_x);
545        
546            let cursor_x = area.left() + (line_number_width + visible_x) as u16;
547            let cursor_y = area.top() + (cursor_line - self.offset_y) as u16;
548        
549            if cursor_x < area.right() && cursor_y < area.bottom() {
550                return Some((cursor_x, cursor_y));
551            }
552        }
553        
554        return None;
555    }
556}