use kode_core::{Editor, Position, Selection, Transaction};
use crate::parse::MarkdownTree;
pub struct MarkdownEditor {
editor: Editor,
tree: MarkdownTree,
}
impl MarkdownEditor {
pub fn new(text: &str) -> Self {
Self {
editor: Editor::new(text),
tree: MarkdownTree::new(text),
}
}
pub fn empty() -> Self {
Self::new("")
}
pub fn editor(&self) -> &Editor {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut Editor {
&mut self.editor
}
pub fn buffer(&self) -> &kode_core::Buffer {
self.editor.buffer()
}
pub fn version(&self) -> u64 {
self.editor.version()
}
pub fn tree(&self) -> &MarkdownTree {
&self.tree
}
pub fn sync_tree(&mut self) {
self.tree.set_source(&self.editor.text());
}
pub fn insert(&mut self, text: &str) {
self.editor.insert(text);
self.sync_tree();
}
pub fn backspace(&mut self) {
self.editor.backspace();
self.sync_tree();
}
pub fn delete_forward(&mut self) {
self.editor.delete_forward();
self.sync_tree();
}
pub fn insert_newline(&mut self) {
self.editor.insert_newline();
self.sync_tree();
}
pub fn undo(&mut self) {
self.editor.undo();
self.sync_tree();
}
pub fn redo(&mut self) {
self.editor.redo();
self.sync_tree();
}
pub fn delete_selection(&mut self) {
self.editor.delete_selection();
self.sync_tree();
}
pub fn apply_transaction(&mut self, tx: Transaction) {
self.editor.apply_transaction(tx);
self.sync_tree();
}
pub fn indent(&mut self) {
self.editor.indent();
self.sync_tree();
}
pub fn outdent(&mut self) {
self.editor.outdent();
self.sync_tree();
}
pub fn duplicate_lines(&mut self) {
self.editor.duplicate_lines();
self.sync_tree();
}
pub fn delete_word_back(&mut self) {
self.editor.delete_word_back();
self.sync_tree();
}
pub fn delete_word_forward(&mut self) {
self.editor.delete_word_forward();
self.sync_tree();
}
pub fn text(&self) -> String {
self.editor.text()
}
pub fn cursor(&self) -> Position {
self.editor.cursor()
}
pub fn selection(&self) -> Selection {
self.editor.selection()
}
pub fn selected_text(&self) -> String {
self.editor.selected_text()
}
pub fn set_cursor(&mut self, pos: Position) {
self.editor.set_cursor(pos);
}
pub fn set_selection(&mut self, anchor: Position, head: Position) {
self.editor.set_selection(anchor, head);
}
pub fn select_all(&mut self) {
self.editor.select_all();
}
pub fn move_left(&mut self) {
self.editor.move_left();
}
pub fn move_right(&mut self) {
self.editor.move_right();
}
pub fn move_up(&mut self) {
self.editor.move_up();
}
pub fn move_down(&mut self) {
self.editor.move_down();
}
pub fn move_word_left(&mut self) {
self.editor.move_word_left();
}
pub fn move_word_right(&mut self) {
self.editor.move_word_right();
}
pub fn move_to_line_start(&mut self) {
self.editor.move_to_line_start();
}
pub fn move_to_line_end(&mut self) {
self.editor.move_to_line_end();
}
pub fn move_to_start(&mut self) {
self.editor.move_to_start();
}
pub fn move_to_end(&mut self) {
self.editor.move_to_end();
}
pub fn extend_selection_left(&mut self) {
self.editor.extend_selection_left();
}
pub fn extend_selection_right(&mut self) {
self.editor.extend_selection_right();
}
pub fn extend_selection_up(&mut self) {
self.editor.extend_selection_up();
}
pub fn extend_selection_down(&mut self) {
self.editor.extend_selection_down();
}
pub fn extend_selection(&mut self, head: Position) {
self.editor.extend_selection(head);
}
pub fn extend_selection_word_left(&mut self) {
self.editor.extend_selection_word_left();
}
pub fn extend_selection_word_right(&mut self) {
self.editor.extend_selection_word_right();
}
pub fn extend_selection_to_line_start(&mut self) {
self.editor.extend_selection_to_line_start();
}
pub fn extend_selection_to_line_end(&mut self) {
self.editor.extend_selection_to_line_end();
}
pub fn extend_selection_to_start(&mut self) {
self.editor.extend_selection_to_start();
}
pub fn extend_selection_to_end(&mut self) {
self.editor.extend_selection_to_end();
}
pub fn select_word(&mut self) {
self.editor.select_word();
}
pub fn select_line(&mut self) {
self.editor.select_line();
}
pub fn page_up(&mut self, page_lines: usize) {
self.editor.page_up(page_lines);
}
pub fn page_down(&mut self, page_lines: usize) {
self.editor.page_down(page_lines);
}
pub fn is_dirty(&self) -> bool {
self.editor.is_dirty()
}
pub fn mark_clean(&mut self) {
self.editor.mark_clean();
}
pub fn can_undo(&self) -> bool {
self.editor.can_undo()
}
pub fn can_redo(&self) -> bool {
self.editor.can_redo()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{InputRules, MarkdownCommands, NodeKind};
#[test]
fn insert_text_syncs_editor_and_tree() {
let mut md = MarkdownEditor::new("");
md.insert("# Hello");
assert_eq!(md.editor().text(), md.tree().source());
assert_eq!(md.editor().text(), "# Hello");
}
#[test]
fn undo_reverts_both_editor_and_tree() {
let mut md = MarkdownEditor::new("");
md.insert("# Hello");
assert_eq!(md.text(), "# Hello");
assert_eq!(md.tree().source(), "# Hello");
md.undo();
assert_eq!(md.editor().text(), "");
assert_eq!(md.tree().source(), "");
assert_eq!(md.editor().text(), md.tree().source());
}
#[test]
fn walk_blocks_returns_correct_blocks_after_edits() {
let mut md = MarkdownEditor::new("# Title");
md.set_cursor(Position::new(0, 7));
md.insert_newline();
md.insert_newline();
md.insert("Some paragraph text.");
let mut blocks = Vec::new();
md.tree().walk_blocks(|info| blocks.push(info.kind));
assert!(
blocks.contains(&NodeKind::Heading { level: 1 }),
"Expected heading block, got: {:?}",
blocks
);
assert!(
blocks.contains(&NodeKind::Paragraph),
"Expected paragraph block, got: {:?}",
blocks
);
}
#[test]
fn toggle_bold_via_editor_mut_and_sync_tree() {
let mut md = MarkdownEditor::new("hello world");
md.set_selection(Position::new(0, 6), Position::new(0, 11));
MarkdownCommands::toggle_bold(md.editor_mut());
md.sync_tree();
assert_eq!(md.editor().text(), "hello **world**");
assert_eq!(md.tree().source(), "hello **world**");
assert_eq!(md.editor().text(), md.tree().source());
}
#[test]
fn input_rules_handle_enter_via_editor_mut_and_sync_tree() {
let mut md = MarkdownEditor::new("- item 1");
md.set_cursor(Position::new(0, 8));
let handled = InputRules::handle_enter(md.editor_mut());
md.sync_tree();
assert!(handled);
assert_eq!(md.editor().text(), "- item 1\n- ");
assert_eq!(md.tree().source(), "- item 1\n- ");
assert_eq!(md.editor().text(), md.tree().source());
}
#[test]
fn backspace_syncs_tree() {
let mut md = MarkdownEditor::new("abc");
md.set_cursor(Position::new(0, 3));
md.backspace();
assert_eq!(md.editor().text(), "ab");
assert_eq!(md.tree().source(), "ab");
}
#[test]
fn delete_forward_syncs_tree() {
let mut md = MarkdownEditor::new("abc");
md.set_cursor(Position::new(0, 0));
md.delete_forward();
assert_eq!(md.editor().text(), "bc");
assert_eq!(md.tree().source(), "bc");
}
#[test]
fn redo_syncs_tree() {
let mut md = MarkdownEditor::new("");
md.insert("# Test");
md.undo();
assert_eq!(md.tree().source(), "");
md.redo();
assert_eq!(md.editor().text(), "# Test");
assert_eq!(md.tree().source(), "# Test");
}
#[test]
fn delete_selection_syncs_tree() {
let mut md = MarkdownEditor::new("hello world");
md.set_selection(Position::new(0, 5), Position::new(0, 11));
md.delete_selection();
assert_eq!(md.editor().text(), "hello");
assert_eq!(md.tree().source(), "hello");
}
#[test]
fn indent_outdent_sync_tree() {
let mut md = MarkdownEditor::new("line1\nline2");
md.set_selection(Position::new(0, 0), Position::new(1, 5));
md.indent();
assert_eq!(md.editor().text(), md.tree().source());
assert_eq!(md.editor().text(), " line1\n line2");
md.set_selection(Position::new(0, 0), Position::new(1, 7));
md.outdent();
assert_eq!(md.editor().text(), "line1\nline2");
assert_eq!(md.tree().source(), "line1\nline2");
}
#[test]
fn duplicate_lines_syncs_tree() {
let mut md = MarkdownEditor::new("line1\nline2");
md.set_cursor(Position::new(0, 0));
md.duplicate_lines();
assert_eq!(md.editor().text(), md.tree().source());
assert_eq!(md.editor().text(), "line1\nline1\nline2");
}
#[test]
fn apply_transaction_syncs_tree() {
let mut md = MarkdownEditor::new("Hello\nWorld");
let tx = Transaction::new(vec![
kode_core::EditStep::replace(0, "Hello".to_string(), "> Hello".to_string()),
kode_core::EditStep::replace(8, "World".to_string(), "> World".to_string()),
]);
md.apply_transaction(tx);
assert_eq!(md.editor().text(), "> Hello\n> World");
assert_eq!(md.tree().source(), "> Hello\n> World");
}
#[test]
fn delete_word_back_syncs_tree() {
let mut md = MarkdownEditor::new("hello world");
md.set_cursor(Position::new(0, 11));
md.delete_word_back();
assert_eq!(md.editor().text(), "hello ");
assert_eq!(md.tree().source(), "hello ");
}
#[test]
fn delete_word_forward_syncs_tree() {
let mut md = MarkdownEditor::new("hello world");
md.set_cursor(Position::new(0, 0));
md.delete_word_forward();
assert_eq!(md.editor().text(), "world");
assert_eq!(md.tree().source(), "world");
}
#[test]
fn read_only_methods_work() {
let md = MarkdownEditor::new("# Hello");
assert_eq!(md.text(), "# Hello");
assert_eq!(md.cursor(), Position::zero());
assert!(md.selection().is_cursor());
assert_eq!(md.selected_text(), "");
assert!(!md.is_dirty());
assert!(!md.can_undo());
assert!(!md.can_redo());
}
#[test]
fn movement_methods_work() {
let mut md = MarkdownEditor::new("hello world\nsecond line");
md.move_right();
assert_eq!(md.cursor(), Position::new(0, 1));
md.move_to_line_end();
assert_eq!(md.cursor(), Position::new(0, 11));
md.move_down();
assert_eq!(md.cursor(), Position::new(1, 11));
md.move_to_line_start();
assert_eq!(md.cursor(), Position::new(1, 0));
md.move_left();
assert_eq!(md.cursor(), Position::new(0, 11));
md.move_to_start();
assert_eq!(md.cursor(), Position::zero());
md.move_to_end();
assert_eq!(md.cursor(), Position::new(1, 11));
md.move_to_start();
md.move_up();
assert_eq!(md.cursor(), Position::zero());
}
}