Skip to main content

freya_code_editor/
editor_data.rs

1use std::{
2    borrow::Cow,
3    fmt::Display,
4    ops::{
5        Mul,
6        Range,
7    },
8    time::Duration,
9};
10
11use freya_components::scrollviews::ScrollController;
12use freya_core::{
13    elements::paragraph::ParagraphHolderInner,
14    prelude::*,
15};
16use freya_edit::*;
17use ropey::Rope;
18use torin::geometry::Size2D;
19use tree_sitter::InputEdit;
20
21use crate::{
22    editor_theme::EditorSyntaxTheme,
23    languages::EditorLanguage,
24    metrics::EditorMetrics,
25    syntax::InputEditExt,
26};
27
28pub struct CodeEditorData {
29    pub(crate) history: EditorHistory,
30    pub rope: Rope,
31    pub(crate) selection: TextSelection,
32    pub(crate) last_saved_history_change: usize,
33    pub(crate) metrics: EditorMetrics,
34    pub(crate) dragging: TextDragging,
35    pub(crate) pending_edit: Option<InputEdit>,
36    pub language: Option<EditorLanguage>,
37    pub viewport: Size2D,
38    theme: EditorSyntaxTheme,
39}
40
41impl CodeEditorData {
42    /// Creates the editor data for the given [`Rope`] and language.
43    pub fn new(rope: Rope, language: impl Into<Option<EditorLanguage>>) -> Self {
44        let mut data = Self {
45            rope,
46            selection: TextSelection::new_cursor(0),
47            history: EditorHistory::new(Duration::from_secs(1)),
48            last_saved_history_change: 0,
49            metrics: EditorMetrics::new(),
50            dragging: TextDragging::default(),
51            pending_edit: None,
52            language: language.into(),
53            viewport: Size2D::default(),
54            theme: EditorSyntaxTheme::default(),
55        };
56        data.configure_highlighter();
57        data
58    }
59
60    /// Scrolls the given controller just enough to make the cursor visible.
61    ///
62    /// Returns whether the scroll position changed.
63    pub fn scroll_to_cursor(
64        &self,
65        mut scroll_controller: ScrollController,
66        line_height: f32,
67    ) -> bool {
68        if self.viewport.width <= 0.
69            || self.viewport.height <= 0.
70            || line_height <= 0.
71            || self.metrics.char_width <= 0.
72        {
73            return false;
74        }
75
76        let (scroll_x, scroll_y) = scroll_controller.into();
77        let scrolled_x = -scroll_x as f32;
78        let scrolled_y = -scroll_y as f32;
79        let cursor_left = self.cursor_col() as f32 * self.metrics.char_width;
80        let cursor_right = cursor_left + self.metrics.char_width;
81        let cursor_top = self.cursor_row() as f32 * line_height;
82        let cursor_bottom = cursor_top + line_height;
83
84        let horizontal_target = if cursor_left < scrolled_x {
85            Some(cursor_left)
86        } else if cursor_right > scrolled_x + self.viewport.width {
87            Some(cursor_right - self.viewport.width)
88        } else {
89            None
90        };
91
92        let vertical_target = if cursor_top < scrolled_y {
93            Some(cursor_top)
94        } else if cursor_bottom > scrolled_y + self.viewport.height {
95            Some(cursor_bottom - self.viewport.height)
96        } else {
97            None
98        };
99
100        let mut changed = false;
101
102        if let Some(target) = horizontal_target {
103            changed |= scroll_controller.scroll_to_x(-target as i32);
104        }
105
106        if let Some(target) = vertical_target {
107            changed |= scroll_controller.scroll_to_y(-target as i32);
108        }
109
110        changed
111    }
112
113    /// Reconfigures the highlighter with the current language and theme.
114    fn configure_highlighter(&mut self) {
115        self.metrics
116            .highlighter
117            .set_language(self.language.as_ref(), &self.theme);
118    }
119
120    /// Sets the language used for syntax highlighting, or disables it with `None`.
121    pub fn set_language(&mut self, language: impl Into<Option<EditorLanguage>>) {
122        self.language = language.into();
123        self.configure_highlighter();
124    }
125
126    pub fn is_edited(&self) -> bool {
127        self.history.current_change() != self.last_saved_history_change
128    }
129
130    pub fn mark_as_saved(&mut self) {
131        self.last_saved_history_change = self.history.current_change();
132    }
133
134    pub fn parse(&mut self) {
135        let edit = self.pending_edit.take();
136        self.metrics.run_parser(&self.rope, edit, &self.theme);
137    }
138
139    pub fn measure(&mut self, font_size: f32, font_family: &str) {
140        self.metrics
141            .measure_longest_line(font_size, font_family, &self.rope);
142    }
143
144    pub fn set_theme(&mut self, theme: EditorSyntaxTheme) {
145        self.theme = theme;
146        self.configure_highlighter();
147    }
148
149    pub fn process(
150        &mut self,
151        font_size: f32,
152        font_family: &str,
153        edit_event: EditableEvent,
154    ) -> bool {
155        let mut processed = false;
156        match edit_event {
157            EditableEvent::Down {
158                location,
159                editor_line,
160                holder,
161            } => {
162                let holder = holder.0.borrow();
163                let ParagraphHolderInner {
164                    paragraph,
165                    scale_factor,
166                    ..
167                } = holder.as_ref().unwrap();
168
169                let current_selection = self.selection().clone();
170
171                if self.dragging.shift || self.dragging.clicked {
172                    self.selection_mut().set_as_range();
173                } else {
174                    self.clear_selection();
175                }
176
177                if &current_selection != self.selection() {
178                    processed = true;
179                }
180
181                self.dragging.clicked = true;
182
183                let char_position = paragraph.get_glyph_position_at_coordinate(
184                    location.mul(*scale_factor).to_i32().to_tuple(),
185                );
186                let press_selection =
187                    self.measure_selection(char_position.position as usize, editor_line);
188
189                let new_selection = match EventsCombos::<()>::pressed(location) {
190                    PressEventType::Quadruple => {
191                        TextSelection::new_range((0, self.rope.len_utf16_cu()))
192                    }
193                    PressEventType::Triple => {
194                        let line = self.char_to_line(press_selection.pos());
195                        let line_char = self.line_to_char(line);
196                        let line_len = self.line(line).unwrap().utf16_len();
197                        TextSelection::new_range((line_char, line_char + line_len))
198                    }
199                    PressEventType::Double => {
200                        let range = self.find_word_boundaries(press_selection.pos());
201                        TextSelection::new_range(range)
202                    }
203                    PressEventType::Single => press_selection,
204                };
205
206                if *self.selection() != new_selection {
207                    *self.selection_mut() = new_selection;
208                    processed = true;
209                }
210            }
211            EditableEvent::Move {
212                location,
213                editor_line,
214                holder,
215            } => {
216                if self.dragging.clicked {
217                    let paragraph = holder.0.borrow();
218                    let ParagraphHolderInner {
219                        paragraph,
220                        scale_factor,
221                        ..
222                    } = paragraph.as_ref().unwrap();
223
224                    let dist_position = location.mul(*scale_factor);
225
226                    // Calculate the end of the highlighting
227                    let dist_char = paragraph
228                        .get_glyph_position_at_coordinate(dist_position.to_i32().to_tuple());
229                    let to = dist_char.position as usize;
230
231                    if self.get_selection().is_none() {
232                        self.selection_mut().set_as_range();
233                        processed = true;
234                    }
235
236                    let current_selection = self.selection().clone();
237
238                    let new_selection = self.measure_selection(to, editor_line);
239
240                    // Update the cursor if it has changed
241                    if current_selection != new_selection {
242                        *self.selection_mut() = new_selection;
243                        processed = true;
244                    }
245                }
246            }
247            EditableEvent::Release => {
248                self.dragging.clicked = false;
249            }
250            EditableEvent::KeyDown {
251                key,
252                modifiers,
253                editor_line,
254                holder,
255            } => {
256                match key {
257                    // Handle dragging
258                    Key::Named(NamedKey::Shift) => {
259                        self.dragging.shift = true;
260                    }
261                    // Handle editing
262                    _ => {
263                        let event = self.process_key(
264                            key,
265                            &modifiers,
266                            editor_line,
267                            holder,
268                            true,
269                            true,
270                            true,
271                            true,
272                        );
273                        if event.contains(TextEvent::TEXT_CHANGED) {
274                            self.parse();
275                            self.measure(font_size, font_family);
276                            self.dragging = TextDragging::default();
277                        }
278                        if !event.is_empty() {
279                            processed = true;
280                        }
281                    }
282                }
283            }
284            EditableEvent::KeyUp { key, .. } => {
285                if *key == Key::Named(NamedKey::Shift) {
286                    self.dragging.shift = false;
287                }
288            }
289        };
290        processed
291    }
292}
293
294impl Display for CodeEditorData {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        f.write_str(&self.rope.to_string())
297    }
298}
299
300impl TextEditor for CodeEditorData {
301    type LinesIterator<'a>
302        = LinesIterator<'a>
303    where
304        Self: 'a;
305
306    fn lines(&self) -> Self::LinesIterator<'_> {
307        unimplemented!("Unused.")
308    }
309
310    fn text(&self) -> Cow<'_, str> {
311        self.rope.slice(..).into()
312    }
313
314    fn insert_char(&mut self, ch: char, idx: usize) -> usize {
315        let idx_utf8 = self.utf16_cu_to_char(idx);
316        let selection = self.selection.clone();
317
318        // Capture byte offset and position before mutation for InputEdit.
319        let start_byte = self.rope.char_to_byte(idx_utf8);
320        let start_line = self.rope.char_to_line(idx_utf8);
321        let start_line_byte = self.rope.line_to_byte(start_line);
322        let start_col = start_byte - start_line_byte;
323
324        let len_before_insert = self.rope.len_utf16_cu();
325        self.rope.insert_char(idx_utf8, ch);
326        let len_after_insert = self.rope.len_utf16_cu();
327
328        let inserted_text_len = len_after_insert - len_before_insert;
329
330        // Compute new end position after insertion.
331        let new_end_char = idx_utf8 + 1; // one char inserted
332        let new_end_byte = self.rope.char_to_byte(new_end_char);
333        let new_end_line = self.rope.char_to_line(new_end_char);
334        let new_end_line_byte = self.rope.line_to_byte(new_end_line);
335        let new_end_col = new_end_byte - new_end_line_byte;
336
337        self.pending_edit = Some(InputEdit::new_edit(
338            start_byte,
339            start_byte,
340            new_end_byte,
341            (start_line, start_col),
342            (start_line, start_col),
343            (new_end_line, new_end_col),
344        ));
345
346        self.history.push_change(HistoryChange::InsertChar {
347            idx,
348            ch,
349            len: inserted_text_len,
350            selection,
351        });
352
353        inserted_text_len
354    }
355
356    fn insert(&mut self, text: &str, idx: usize) -> usize {
357        let idx_utf8 = self.utf16_cu_to_char(idx);
358        let selection = self.selection.clone();
359
360        // Capture byte offset and position before mutation for InputEdit.
361        let start_byte = self.rope.char_to_byte(idx_utf8);
362        let start_line = self.rope.char_to_line(idx_utf8);
363        let start_line_byte = self.rope.line_to_byte(start_line);
364        let start_col = start_byte - start_line_byte;
365
366        let len_before_insert = self.rope.len_utf16_cu();
367        self.rope.insert(idx_utf8, text);
368        let len_after_insert = self.rope.len_utf16_cu();
369
370        let inserted_text_len = len_after_insert - len_before_insert;
371
372        // Compute new end position after insertion.
373        let inserted_chars = text.chars().count();
374        let new_end_char = idx_utf8 + inserted_chars;
375        let new_end_byte = self.rope.char_to_byte(new_end_char);
376        let new_end_line = self.rope.char_to_line(new_end_char);
377        let new_end_line_byte = self.rope.line_to_byte(new_end_line);
378        let new_end_col = new_end_byte - new_end_line_byte;
379
380        self.pending_edit = Some(InputEdit::new_edit(
381            start_byte,
382            start_byte,
383            new_end_byte,
384            (start_line, start_col),
385            (start_line, start_col),
386            (new_end_line, new_end_col),
387        ));
388
389        self.history.push_change(HistoryChange::InsertText {
390            idx,
391            text: text.to_owned(),
392            len: inserted_text_len,
393            selection,
394        });
395
396        inserted_text_len
397    }
398
399    fn remove(&mut self, range_utf16: Range<usize>) -> usize {
400        let range =
401            self.utf16_cu_to_char(range_utf16.start)..self.utf16_cu_to_char(range_utf16.end);
402        let text = self.rope.slice(range.clone()).to_string();
403        let selection = self.selection.clone();
404
405        // Capture byte offsets and positions before mutation for InputEdit.
406        let start_byte = self.rope.char_to_byte(range.start);
407        let old_end_byte = self.rope.char_to_byte(range.end);
408        let start_line = self.rope.char_to_line(range.start);
409        let start_line_byte = self.rope.line_to_byte(start_line);
410        let start_col = start_byte - start_line_byte;
411        let old_end_line = self.rope.char_to_line(range.end);
412        let old_end_line_byte = self.rope.line_to_byte(old_end_line);
413        let old_end_col = old_end_byte - old_end_line_byte;
414
415        let len_before_remove = self.rope.len_utf16_cu();
416        self.rope.remove(range);
417        let len_after_remove = self.rope.len_utf16_cu();
418
419        let removed_text_len = len_before_remove - len_after_remove;
420
421        // After removal, new_end == start (the removed range collapses to a point).
422        self.pending_edit = Some(InputEdit::new_edit(
423            start_byte,
424            old_end_byte,
425            start_byte,
426            (start_line, start_col),
427            (old_end_line, old_end_col),
428            (start_line, start_col),
429        ));
430
431        self.history.push_change(HistoryChange::Remove {
432            idx: range_utf16.end - removed_text_len,
433            text,
434            len: removed_text_len,
435            selection,
436        });
437
438        removed_text_len
439    }
440
441    fn char_to_line(&self, char_idx: usize) -> usize {
442        self.rope.char_to_line(char_idx)
443    }
444
445    fn line_to_char(&self, line_idx: usize) -> usize {
446        self.rope.line_to_char(line_idx)
447    }
448
449    fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize {
450        self.rope.utf16_cu_to_char(utf16_cu_idx)
451    }
452
453    fn char_to_utf16_cu(&self, idx: usize) -> usize {
454        self.rope.char_to_utf16_cu(idx)
455    }
456
457    fn line(&self, line_idx: usize) -> Option<Line<'_>> {
458        let line = self.rope.get_line(line_idx);
459
460        line.map(|line| Line {
461            text: Cow::Owned(line.to_string()),
462            utf16_len: line.len_utf16_cu(),
463        })
464    }
465
466    fn len_lines(&self) -> usize {
467        self.rope.len_lines()
468    }
469
470    fn len_chars(&self) -> usize {
471        self.rope.len_chars()
472    }
473
474    fn len_utf16_cu(&self) -> usize {
475        self.rope.len_utf16_cu()
476    }
477
478    fn has_any_selection(&self) -> bool {
479        self.selection.is_range()
480    }
481
482    fn get_selection(&self) -> Option<(usize, usize)> {
483        match self.selection {
484            TextSelection::Cursor(_) => None,
485            TextSelection::Range { from, to } => Some((from, to)),
486        }
487    }
488
489    fn set(&mut self, text: &str) {
490        self.rope.remove(0..);
491        self.rope.insert(0, text);
492    }
493
494    fn clear_selection(&mut self) {
495        let end = self.selection().end();
496        self.selection_mut().set_as_cursor();
497        self.selection_mut().move_to(end);
498    }
499
500    fn set_selection(&mut self, (from, to): (usize, usize)) {
501        self.selection = TextSelection::Range { from, to };
502    }
503
504    fn get_selected_text(&self) -> Option<String> {
505        let (start, end) = self.get_selection_range()?;
506
507        Some(self.rope.get_slice(start..end)?.to_string())
508    }
509
510    fn get_selection_range(&self) -> Option<(usize, usize)> {
511        let (start, end) = match self.selection {
512            TextSelection::Cursor(_) => return None,
513            TextSelection::Range { from, to } => (from, to),
514        };
515
516        // Use left-to-right selection
517        let (start, end) = if start < end {
518            (start, end)
519        } else {
520            (end, start)
521        };
522
523        Some((start, end))
524    }
525
526    fn undo(&mut self) -> Option<TextSelection> {
527        // Undo can make arbitrary changes, therefore invalidate the tree for a full re-parse.
528        self.pending_edit = None;
529        self.metrics.highlighter.invalidate_tree();
530        self.history.undo(&mut self.rope)
531    }
532
533    fn redo(&mut self) -> Option<TextSelection> {
534        // Redo can make arbitrary changes, therefore invalidate the tree for a full re-parse.
535        self.pending_edit = None;
536        self.metrics.highlighter.invalidate_tree();
537        self.history.redo(&mut self.rope)
538    }
539
540    fn editor_history(&self) -> &EditorHistory {
541        &self.history
542    }
543
544    fn editor_history_mut(&mut self) -> &mut EditorHistory {
545        &mut self.history
546    }
547
548    fn selection(&self) -> &TextSelection {
549        &self.selection
550    }
551
552    fn selection_mut(&mut self) -> &mut TextSelection {
553        &mut self.selection
554    }
555
556    fn get_indentation(&self) -> u8 {
557        4
558    }
559}