editor-core 0.4.1

A headless editor engine focused on state management, Unicode-aware text measurement, and coordinate conversion.
Documentation
use std::time::{Duration, Instant};

use editor_core::{
    Command, CommandExecutor, CommentConfig, CursorCommand, EditCommand, Position, Selection,
    SelectionDirection,
};

fn selection(start: Position, end: Position) -> Selection {
    Selection {
        start,
        end,
        direction: SelectionDirection::Forward,
    }
}

#[test]
fn test_toggle_line_comment_single_line() {
    let mut ex = CommandExecutor::new("let x = 1;", 80);
    ex.execute(Command::Cursor(CursorCommand::MoveTo {
        line: 0,
        column: 0,
    }))
    .unwrap();

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "// let x = 1;");
    assert_eq!(ex.editor().cursor_position(), Position::new(0, 3));

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "let x = 1;");
    assert_eq!(ex.editor().cursor_position(), Position::new(0, 0));
}

#[test]
fn test_toggle_line_comment_multi_line_selection() {
    let mut ex = CommandExecutor::new("a\n  b\nc", 80);

    ex.execute(Command::Cursor(CursorCommand::SetSelection {
        start: Position::new(0, 0),
        end: Position::new(2, 0),
    }))
    .unwrap();

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "// a\n  // b\n// c");

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "a\n  b\nc");
}

#[test]
fn test_toggle_line_comment_unicode_multiline_with_empty_line() {
    let mut ex = CommandExecutor::new("\n\n\t🙂", 80);

    ex.execute(Command::Cursor(CursorCommand::SetSelection {
        start: Position::new(0, 0),
        end: Position::new(2, 0),
    }))
    .unwrap();

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "  // 你\n// \n\t// 🙂");

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "\n\n\t🙂");
}

#[test]
fn test_toggle_line_comment_cjk_emoji_tab_and_space_indents() {
    let original = "\t\n    🙂\n\t\tvalue";
    let mut ex = CommandExecutor::new(original, 80);

    ex.execute(Command::Cursor(CursorCommand::SetSelection {
        start: Position::new(0, 0),
        end: Position::new(2, 0),
    }))
    .unwrap();

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("#"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "\t  # 你\n    # 🙂\n\t\t# value");

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("#"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), original);
}

#[test]
fn test_toggle_line_comment_long_multiselection_has_no_quadratic_column_scan() {
    let indent = " ".repeat(2048);
    let line = format!("{indent}🙂 value");
    let original = vec![line; 256].join("\n");
    let mut ex = CommandExecutor::new(&original, 80);

    ex.execute(Command::Cursor(CursorCommand::SetSelections {
        selections: vec![
            selection(Position::new(0, 0), Position::new(127, 0)),
            selection(Position::new(128, 0), Position::new(255, 0)),
        ],
        primary_index: 0,
    }))
    .unwrap();

    let start = Instant::now();
    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();
    assert!(
        ex.editor()
            .get_text()
            .starts_with(&format!("{indent}// 🙂 value"))
    );

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::line("//"),
    }))
    .unwrap();
    let elapsed = start.elapsed();

    assert_eq!(ex.editor().get_text(), original);
    assert!(
        elapsed < Duration::from_secs(10),
        "long multi-selection toggle comment took {elapsed:?}"
    );
}

#[test]
fn test_toggle_block_comment_inline_single_line_selection() {
    let mut ex = CommandExecutor::new("abc", 80);

    ex.execute(Command::Cursor(CursorCommand::SetSelection {
        start: Position::new(0, 1),
        end: Position::new(0, 2),
    }))
    .unwrap();

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::block("/*", "*/"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "a/*b*/c");

    ex.execute(Command::Edit(EditCommand::ToggleComment {
        config: CommentConfig::block("/*", "*/"),
    }))
    .unwrap();

    assert_eq!(ex.editor().get_text(), "abc");
}