Skip to main content

kode_markdown/
markdown_editor.rs

1use kode_core::{Editor, Position, Selection, Transaction};
2
3use crate::parse::MarkdownTree;
4
5/// Coordinated markdown editor that owns both the text `Editor` and the
6/// `MarkdownTree`. After every mutation that changes text content, the tree
7/// is automatically synced via `set_source()`. This is the single
8/// coordination point — callers should never need to manually sync.
9pub struct MarkdownEditor {
10    editor: Editor,
11    tree: MarkdownTree,
12}
13
14impl MarkdownEditor {
15    /// Create a new markdown editor with the given text.
16    pub fn new(text: &str) -> Self {
17        Self {
18            editor: Editor::new(text),
19            tree: MarkdownTree::new(text),
20        }
21    }
22
23    /// Create an empty markdown editor.
24    pub fn empty() -> Self {
25        Self::new("")
26    }
27
28    // ── Accessors ────────────────────────────────────────────────────────
29
30    /// Immutable access to the inner `Editor`.
31    pub fn editor(&self) -> &Editor {
32        &self.editor
33    }
34
35    /// Mutable access to the inner `Editor`.
36    ///
37    /// **Important**: If you mutate the editor text through this reference
38    /// (e.g., via `MarkdownCommands`, `InputRules`, or direct calls to
39    /// `insert()`, `backspace()`, `undo()`, `redo()`, etc.), you must call
40    /// `sync_tree()` afterward to keep the tree in sync.
41    pub fn editor_mut(&mut self) -> &mut Editor {
42        &mut self.editor
43    }
44
45    /// Immutable access to the inner `Buffer`.
46    pub fn buffer(&self) -> &kode_core::Buffer {
47        self.editor.buffer()
48    }
49
50    /// Document version, incremented on every edit.
51    pub fn version(&self) -> u64 {
52        self.editor.version()
53    }
54
55    /// Immutable access to the `MarkdownTree`.
56    pub fn tree(&self) -> &MarkdownTree {
57        &self.tree
58    }
59
60    /// Full reparse of the tree from the editor's current text.
61    /// Call this after using `editor_mut()` to make direct mutations.
62    pub fn sync_tree(&mut self) {
63        self.tree.set_source(&self.editor.text());
64    }
65
66    // ── Text-mutating wrappers (auto-sync tree) ─────────────────────────
67
68    /// Insert text at the cursor. If there's a selection, replace it.
69    pub fn insert(&mut self, text: &str) {
70        self.editor.insert(text);
71        self.sync_tree();
72    }
73
74    /// Delete the character before the cursor (backspace).
75    pub fn backspace(&mut self) {
76        self.editor.backspace();
77        self.sync_tree();
78    }
79
80    /// Delete the character after the cursor (forward delete).
81    pub fn delete_forward(&mut self) {
82        self.editor.delete_forward();
83        self.sync_tree();
84    }
85
86    /// Insert a newline at the cursor.
87    pub fn insert_newline(&mut self) {
88        self.editor.insert_newline();
89        self.sync_tree();
90    }
91
92    /// Undo the last transaction.
93    pub fn undo(&mut self) {
94        self.editor.undo();
95        self.sync_tree();
96    }
97
98    /// Redo the last undone transaction.
99    pub fn redo(&mut self) {
100        self.editor.redo();
101        self.sync_tree();
102    }
103
104    /// Delete the current selection. No-op if cursor only.
105    pub fn delete_selection(&mut self) {
106        self.editor.delete_selection();
107        self.sync_tree();
108    }
109
110    /// Apply a pre-built transaction atomically.
111    pub fn apply_transaction(&mut self, tx: Transaction) {
112        self.editor.apply_transaction(tx);
113        self.sync_tree();
114    }
115
116    /// Insert a tab (2 spaces) at the cursor, or indent all selected lines.
117    pub fn indent(&mut self) {
118        self.editor.indent();
119        self.sync_tree();
120    }
121
122    /// Remove one level of indentation from the current or selected lines.
123    pub fn outdent(&mut self) {
124        self.editor.outdent();
125        self.sync_tree();
126    }
127
128    /// Duplicate the current line or all selected lines.
129    pub fn duplicate_lines(&mut self) {
130        self.editor.duplicate_lines();
131        self.sync_tree();
132    }
133
134    /// Delete from cursor to start of previous word (Ctrl+Backspace).
135    pub fn delete_word_back(&mut self) {
136        self.editor.delete_word_back();
137        self.sync_tree();
138    }
139
140    /// Delete from cursor to end of next word (Ctrl+Delete).
141    pub fn delete_word_forward(&mut self) {
142        self.editor.delete_word_forward();
143        self.sync_tree();
144    }
145
146    // ── Read-only delegations (no sync needed) ──────────────────────────
147
148    /// Get the full text content.
149    pub fn text(&self) -> String {
150        self.editor.text()
151    }
152
153    /// Get the current cursor position.
154    pub fn cursor(&self) -> Position {
155        self.editor.cursor()
156    }
157
158    /// Get the current selection.
159    pub fn selection(&self) -> Selection {
160        self.editor.selection()
161    }
162
163    /// Get the selected text, or empty string if cursor only.
164    pub fn selected_text(&self) -> String {
165        self.editor.selected_text()
166    }
167
168    /// Set the cursor to a position, collapsing any selection.
169    pub fn set_cursor(&mut self, pos: Position) {
170        self.editor.set_cursor(pos);
171    }
172
173    /// Set a selection range.
174    pub fn set_selection(&mut self, anchor: Position, head: Position) {
175        self.editor.set_selection(anchor, head);
176    }
177
178    /// Select all text.
179    pub fn select_all(&mut self) {
180        self.editor.select_all();
181    }
182
183    // ── Cursor movement (no sync needed) ────────────────────────────────
184
185    /// Move cursor left by one character.
186    pub fn move_left(&mut self) {
187        self.editor.move_left();
188    }
189
190    /// Move cursor right by one character.
191    pub fn move_right(&mut self) {
192        self.editor.move_right();
193    }
194
195    /// Move cursor up by one line.
196    pub fn move_up(&mut self) {
197        self.editor.move_up();
198    }
199
200    /// Move cursor down by one line.
201    pub fn move_down(&mut self) {
202        self.editor.move_down();
203    }
204
205    /// Move cursor left to the start of the previous word.
206    pub fn move_word_left(&mut self) {
207        self.editor.move_word_left();
208    }
209
210    /// Move cursor right to the end of the next word.
211    pub fn move_word_right(&mut self) {
212        self.editor.move_word_right();
213    }
214
215    /// Move cursor to start of current line.
216    pub fn move_to_line_start(&mut self) {
217        self.editor.move_to_line_start();
218    }
219
220    /// Move cursor to end of current line.
221    pub fn move_to_line_end(&mut self) {
222        self.editor.move_to_line_end();
223    }
224
225    /// Move cursor to start of document.
226    pub fn move_to_start(&mut self) {
227        self.editor.move_to_start();
228    }
229
230    /// Move cursor to end of document.
231    pub fn move_to_end(&mut self) {
232        self.editor.move_to_end();
233    }
234
235    // ── Selection extension (no sync needed) ────────────────────────────
236
237    /// Extend selection one character left (Shift+Left).
238    pub fn extend_selection_left(&mut self) {
239        self.editor.extend_selection_left();
240    }
241
242    /// Extend selection one character right (Shift+Right).
243    pub fn extend_selection_right(&mut self) {
244        self.editor.extend_selection_right();
245    }
246
247    /// Extend selection up one line (Shift+Up).
248    pub fn extend_selection_up(&mut self) {
249        self.editor.extend_selection_up();
250    }
251
252    /// Extend selection down one line (Shift+Down).
253    pub fn extend_selection_down(&mut self) {
254        self.editor.extend_selection_down();
255    }
256
257    /// Extend selection to a specific position.
258    pub fn extend_selection(&mut self, head: Position) {
259        self.editor.extend_selection(head);
260    }
261
262    /// Extend selection to word boundary left (Ctrl+Shift+Left).
263    pub fn extend_selection_word_left(&mut self) {
264        self.editor.extend_selection_word_left();
265    }
266
267    /// Extend selection to word boundary right (Ctrl+Shift+Right).
268    pub fn extend_selection_word_right(&mut self) {
269        self.editor.extend_selection_word_right();
270    }
271
272    /// Extend selection to line start (Shift+Home).
273    pub fn extend_selection_to_line_start(&mut self) {
274        self.editor.extend_selection_to_line_start();
275    }
276
277    /// Extend selection to line end (Shift+End).
278    pub fn extend_selection_to_line_end(&mut self) {
279        self.editor.extend_selection_to_line_end();
280    }
281
282    /// Extend selection to document start (Ctrl+Shift+Home).
283    pub fn extend_selection_to_start(&mut self) {
284        self.editor.extend_selection_to_start();
285    }
286
287    /// Extend selection to document end (Ctrl+Shift+End).
288    pub fn extend_selection_to_end(&mut self) {
289        self.editor.extend_selection_to_end();
290    }
291
292    // ── Smart selection (no sync needed) ────────────────────────────────
293
294    /// Select the word at the cursor (double-click behavior).
295    pub fn select_word(&mut self) {
296        self.editor.select_word();
297    }
298
299    /// Select the entire current line (triple-click behavior).
300    pub fn select_line(&mut self) {
301        self.editor.select_line();
302    }
303
304    /// Move cursor up by N lines (PageUp).
305    pub fn page_up(&mut self, page_lines: usize) {
306        self.editor.page_up(page_lines);
307    }
308
309    /// Move cursor down by N lines (PageDown).
310    pub fn page_down(&mut self, page_lines: usize) {
311        self.editor.page_down(page_lines);
312    }
313
314    // ── Dirty / undo state (no sync needed) ─────────────────────────────
315
316    /// Check if the editor has unsaved changes.
317    pub fn is_dirty(&self) -> bool {
318        self.editor.is_dirty()
319    }
320
321    /// Mark the editor as clean (e.g., after saving).
322    pub fn mark_clean(&mut self) {
323        self.editor.mark_clean();
324    }
325
326    /// Check if undo is available.
327    pub fn can_undo(&self) -> bool {
328        self.editor.can_undo()
329    }
330
331    /// Check if redo is available.
332    pub fn can_redo(&self) -> bool {
333        self.editor.can_redo()
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::{InputRules, MarkdownCommands, NodeKind};
341
342    #[test]
343    fn insert_text_syncs_editor_and_tree() {
344        let mut md = MarkdownEditor::new("");
345        md.insert("# Hello");
346        assert_eq!(md.editor().text(), md.tree().source());
347        assert_eq!(md.editor().text(), "# Hello");
348    }
349
350    #[test]
351    fn undo_reverts_both_editor_and_tree() {
352        let mut md = MarkdownEditor::new("");
353        md.insert("# Hello");
354        assert_eq!(md.text(), "# Hello");
355        assert_eq!(md.tree().source(), "# Hello");
356
357        md.undo();
358        assert_eq!(md.editor().text(), "");
359        assert_eq!(md.tree().source(), "");
360        assert_eq!(md.editor().text(), md.tree().source());
361    }
362
363    #[test]
364    fn walk_blocks_returns_correct_blocks_after_edits() {
365        let mut md = MarkdownEditor::new("# Title");
366        // Add a paragraph
367        md.set_cursor(Position::new(0, 7));
368        md.insert_newline();
369        md.insert_newline();
370        md.insert("Some paragraph text.");
371
372        let mut blocks = Vec::new();
373        md.tree().walk_blocks(|info| blocks.push(info.kind));
374
375        assert!(
376            blocks.contains(&NodeKind::Heading { level: 1 }),
377            "Expected heading block, got: {:?}",
378            blocks
379        );
380        assert!(
381            blocks.contains(&NodeKind::Paragraph),
382            "Expected paragraph block, got: {:?}",
383            blocks
384        );
385    }
386
387    #[test]
388    fn toggle_bold_via_editor_mut_and_sync_tree() {
389        let mut md = MarkdownEditor::new("hello world");
390        // Select "world"
391        md.set_selection(Position::new(0, 6), Position::new(0, 11));
392        MarkdownCommands::toggle_bold(md.editor_mut());
393        md.sync_tree();
394
395        assert_eq!(md.editor().text(), "hello **world**");
396        assert_eq!(md.tree().source(), "hello **world**");
397        assert_eq!(md.editor().text(), md.tree().source());
398    }
399
400    #[test]
401    fn input_rules_handle_enter_via_editor_mut_and_sync_tree() {
402        let mut md = MarkdownEditor::new("- item 1");
403        md.set_cursor(Position::new(0, 8));
404        let handled = InputRules::handle_enter(md.editor_mut());
405        md.sync_tree();
406
407        assert!(handled);
408        assert_eq!(md.editor().text(), "- item 1\n- ");
409        assert_eq!(md.tree().source(), "- item 1\n- ");
410        assert_eq!(md.editor().text(), md.tree().source());
411    }
412
413    #[test]
414    fn backspace_syncs_tree() {
415        let mut md = MarkdownEditor::new("abc");
416        md.set_cursor(Position::new(0, 3));
417        md.backspace();
418        assert_eq!(md.editor().text(), "ab");
419        assert_eq!(md.tree().source(), "ab");
420    }
421
422    #[test]
423    fn delete_forward_syncs_tree() {
424        let mut md = MarkdownEditor::new("abc");
425        md.set_cursor(Position::new(0, 0));
426        md.delete_forward();
427        assert_eq!(md.editor().text(), "bc");
428        assert_eq!(md.tree().source(), "bc");
429    }
430
431    #[test]
432    fn redo_syncs_tree() {
433        let mut md = MarkdownEditor::new("");
434        md.insert("# Test");
435        md.undo();
436        assert_eq!(md.tree().source(), "");
437
438        md.redo();
439        assert_eq!(md.editor().text(), "# Test");
440        assert_eq!(md.tree().source(), "# Test");
441    }
442
443    #[test]
444    fn delete_selection_syncs_tree() {
445        let mut md = MarkdownEditor::new("hello world");
446        md.set_selection(Position::new(0, 5), Position::new(0, 11));
447        md.delete_selection();
448        assert_eq!(md.editor().text(), "hello");
449        assert_eq!(md.tree().source(), "hello");
450    }
451
452    #[test]
453    fn indent_outdent_sync_tree() {
454        let mut md = MarkdownEditor::new("line1\nline2");
455        md.set_selection(Position::new(0, 0), Position::new(1, 5));
456        md.indent();
457        assert_eq!(md.editor().text(), md.tree().source());
458        assert_eq!(md.editor().text(), "  line1\n  line2");
459
460        // Now outdent
461        md.set_selection(Position::new(0, 0), Position::new(1, 7));
462        md.outdent();
463        assert_eq!(md.editor().text(), "line1\nline2");
464        assert_eq!(md.tree().source(), "line1\nline2");
465    }
466
467    #[test]
468    fn duplicate_lines_syncs_tree() {
469        let mut md = MarkdownEditor::new("line1\nline2");
470        md.set_cursor(Position::new(0, 0));
471        md.duplicate_lines();
472        assert_eq!(md.editor().text(), md.tree().source());
473        assert_eq!(md.editor().text(), "line1\nline1\nline2");
474    }
475
476    #[test]
477    fn apply_transaction_syncs_tree() {
478        let mut md = MarkdownEditor::new("Hello\nWorld");
479        let tx = Transaction::new(vec![
480            kode_core::EditStep::replace(0, "Hello".to_string(), "> Hello".to_string()),
481            kode_core::EditStep::replace(8, "World".to_string(), "> World".to_string()),
482        ]);
483        md.apply_transaction(tx);
484        assert_eq!(md.editor().text(), "> Hello\n> World");
485        assert_eq!(md.tree().source(), "> Hello\n> World");
486    }
487
488    #[test]
489    fn delete_word_back_syncs_tree() {
490        let mut md = MarkdownEditor::new("hello world");
491        md.set_cursor(Position::new(0, 11));
492        md.delete_word_back();
493        assert_eq!(md.editor().text(), "hello ");
494        assert_eq!(md.tree().source(), "hello ");
495    }
496
497    #[test]
498    fn delete_word_forward_syncs_tree() {
499        let mut md = MarkdownEditor::new("hello world");
500        md.set_cursor(Position::new(0, 0));
501        md.delete_word_forward();
502        assert_eq!(md.editor().text(), "world");
503        assert_eq!(md.tree().source(), "world");
504    }
505
506    #[test]
507    fn read_only_methods_work() {
508        let md = MarkdownEditor::new("# Hello");
509        assert_eq!(md.text(), "# Hello");
510        assert_eq!(md.cursor(), Position::zero());
511        assert!(md.selection().is_cursor());
512        assert_eq!(md.selected_text(), "");
513        assert!(!md.is_dirty());
514        assert!(!md.can_undo());
515        assert!(!md.can_redo());
516    }
517
518    #[test]
519    fn movement_methods_work() {
520        let mut md = MarkdownEditor::new("hello world\nsecond line");
521        md.move_right();
522        assert_eq!(md.cursor(), Position::new(0, 1));
523        md.move_to_line_end();
524        assert_eq!(md.cursor(), Position::new(0, 11));
525        md.move_down();
526        assert_eq!(md.cursor(), Position::new(1, 11));
527        md.move_to_line_start();
528        assert_eq!(md.cursor(), Position::new(1, 0));
529        md.move_left();
530        // move_left from col 0 of line 1 wraps to end of line 0
531        // Actually, Editor::move_left goes to prev char offset, which is end of line 0 (the newline char)
532        // The offset of Position(1,0) is the char after '\n' on line 0. Going back one char
533        // lands on the '\n' itself, which is Position(0, 11).
534        assert_eq!(md.cursor(), Position::new(0, 11));
535        md.move_to_start();
536        assert_eq!(md.cursor(), Position::zero());
537        md.move_to_end();
538        assert_eq!(md.cursor(), Position::new(1, 11));
539        md.move_to_start();
540        md.move_up();
541        // Already at line 0, move_up goes to (0, 0)
542        assert_eq!(md.cursor(), Position::zero());
543    }
544}