editor-core 0.4.1

A headless editor engine focused on state management, Unicode-aware text measurement, and coordinate conversion.
Documentation
use editor_core::{Command, CommandExecutor, EditCommand, EditorCore, LineIndex};

fn assert_editor_text(input: &str, expected_text: &str, expected_lines: &[&str]) {
    let core = EditorCore::new(input, 80);

    assert_eq!(core.get_text(), expected_text);
    assert_eq!(core.char_count(), expected_text.chars().count());
    assert_eq!(core.line_count(), expected_lines.len());
    assert_eq!(core.line_index().get_text(), expected_text);
    assert_eq!(
        core.line_index().char_count(),
        expected_text.chars().count()
    );
    assert_eq!(core.line_index().byte_count(), expected_text.len());

    for (line, expected) in expected_lines.iter().enumerate() {
        assert_eq!(
            core.line_index().get_line_text(line).as_deref(),
            Some(*expected)
        );
    }
}

#[test]
fn editor_text_buffer_preserves_document_shapes() {
    assert_editor_text("", "", &[""]);
    assert_editor_text("alpha\nbeta", "alpha\nbeta", &["alpha", "beta"]);
    assert_editor_text("alpha\nbeta\n", "alpha\nbeta\n", &["alpha", "beta", ""]);
    assert_editor_text("你好\n世界", "你好\n世界", &["你好", "世界"]);
    assert_editor_text("a👋\n🌍b", "a👋\n🌍b", &["a👋", "🌍b"]);
    assert_editor_text("a\r\nb\rc", "a\nb\nc", &["a", "b", "c"]);
}

#[test]
fn line_index_text_buffer_supports_char_ranges_and_conversions() {
    let mut index = LineIndex::from_text("a你好\n🌍b");

    assert_eq!(index.char_count(), 6);
    assert_eq!(index.byte_count(), "a你好\n🌍b".len());
    assert_eq!(index.get_range(1, 2), "你好");
    assert_eq!(index.get_range(4, 1), "🌍");
    assert_eq!(index.get_line_text(0).as_deref(), Some("a你好"));
    assert_eq!(index.get_line_text(1).as_deref(), Some("🌍b"));
    assert_eq!(index.position_to_char_offset(1, 1), 5);
    assert_eq!(index.char_offset_to_position(5), (1, 1));

    for char_offset in 0..=index.char_count() {
        let byte_offset = index.char_offset_to_byte_offset(char_offset);
        assert_eq!(index.byte_offset_to_char_offset(byte_offset), char_offset);
    }

    index.insert(1, "");
    assert_eq!(index.get_text(), "a中你好\n🌍b");
    assert_eq!(index.get_range(1, 3), "中你好");

    index.delete(2, 2);
    assert_eq!(index.get_text(), "a中\n🌍b");
    assert_eq!(index.get_line_text(0).as_deref(), Some("a中"));
}

#[test]
fn editor_text_range_reads_from_text_buffer() {
    let core = EditorCore::new("a你\n🙂b", 80);

    assert_eq!(core.text_range(1, 2), "\n");
    assert_eq!(core.text_range(3, 2), "🙂b");
    assert_eq!(core.text_range(99, 2), "");
    assert_eq!(core.line_index().get_text(), core.get_text());
}

#[test]
fn command_edits_keep_text_buffer_consistent() {
    let mut executor = CommandExecutor::new("", 80);

    executor
        .execute(Command::Edit(EditCommand::Insert {
            offset: 0,
            text: "one\r\ntwo🙂".to_string(),
        }))
        .unwrap();
    assert_eq!(executor.editor().get_text(), "one\ntwo🙂");

    executor
        .execute(Command::Edit(EditCommand::Replace {
            start: 3,
            length: 1,
            text: "\n".to_string(),
        }))
        .unwrap();
    assert_eq!(executor.editor().get_text(), "one\n中two🙂");

    executor
        .execute(Command::Edit(EditCommand::Delete {
            start: 0,
            length: 4,
        }))
        .unwrap();

    let editor = executor.editor();
    assert_eq!(editor.get_text(), "中two🙂");
    assert_eq!(editor.line_index().get_text(), editor.get_text());
    assert_eq!(editor.line_index().get_range(0, 1), "");
    assert_eq!(
        editor.line_index().get_line_text(0).as_deref(),
        Some("中two🙂")
    );
}