idet-core 0.4.0

Editing logic for text editors, without a frontend
Documentation
#![allow(clippy::unwrap_used, clippy::expect_used)]

use idet_core::{
    completion::{char_to_byte, collect_words, current_prefix, current_word, matches},
    edits::{changed_range, dedent_line, indent_line, insert, newline_indent},
    lineops::{LineMove, Segment, move_line, move_segment},
    wordops::{WordSwap, swap},
};

#[test]
fn move_line_swaps_and_keeps_column() {
    let text = "alpha\nbeta\ngamma";
    let (moved, cursor) = move_line(text, 8, LineMove::Up).unwrap();
    assert_eq!(moved, "beta\nalpha\ngamma");
    assert_eq!(cursor, 2);
}

#[test]
fn move_line_down_at_last_line_is_none() {
    let text = "one\ntwo";
    assert!(move_line(text, 5, LineMove::Down).is_none());
}

#[test]
fn move_line_up_at_first_line_is_none() {
    let text = "one\ntwo";
    assert!(move_line(text, 1, LineMove::Up).is_none());
}

#[test]
fn move_line_cursor_follows_its_line() {
    let text = "ab\ncdef";
    let (moved, cursor) = move_line(text, 1, LineMove::Down).unwrap();
    assert_eq!(moved, "cdef\nab");
    assert_eq!(cursor, 6);
}

#[test]
fn move_line_counts_multibyte_columns() {
    let text = "äö\nxy";
    let (moved, cursor) = move_line(text, 1, LineMove::Down).unwrap();
    assert_eq!(moved, "xy\näö");
    assert_eq!(cursor, 4);
}

#[test]
fn move_block_swaps_paragraphs_and_keeps_the_gap() {
    let text = "one\ntwo\n\n\nthree\nfour";
    let (moved, cursor) = move_segment(text, 10, Segment::Block, LineMove::Up).unwrap();
    assert_eq!(moved, "three\nfour\n\n\none\ntwo");
    assert_eq!(cursor, 0);
}

#[test]
fn move_block_down_carries_the_cursor_along() {
    let text = "one\ntwo\n\nthree";
    let (moved, cursor) = move_segment(text, 5, Segment::Block, LineMove::Down).unwrap();
    assert_eq!(moved, "three\n\none\ntwo");
    assert_eq!(cursor, 12);
}

#[test]
fn move_block_on_a_blank_line_is_none() {
    assert!(move_segment("one\n\ntwo", 4, Segment::Block, LineMove::Down).is_none());
}

#[test]
fn move_block_without_a_neighbour_is_none() {
    assert!(move_segment("one\ntwo", 0, Segment::Block, LineMove::Up).is_none());
    assert!(move_segment("one\n\n", 0, Segment::Block, LineMove::Down).is_none());
}

#[test]
fn move_section_takes_subsections_along() {
    let text = "# top\n\n## a\nbody a\n\n### deep\n\n## b\nbody b\n";
    let (moved, _) = move_segment(text, 8, Segment::Section, LineMove::Down).unwrap();
    assert_eq!(moved, "# top\n\n## b\nbody b\n\n## a\nbody a\n\n### deep\n");
}

#[test]
fn move_section_works_from_inside_the_body() {
    let text = "## a\nbody a\n## b\nbody b";
    let (moved, cursor) = move_segment(text, 7, Segment::Section, LineMove::Down).unwrap();
    assert_eq!(moved, "## b\nbody b\n## a\nbody a");
    assert_eq!(cursor, 19);
}

#[test]
fn move_section_stops_at_a_different_level() {
    let text = "# top\n## child\nbody\n# other";
    assert!(move_segment(text, 6, Segment::Section, LineMove::Down).is_none());
    assert!(move_segment(text, 6, Segment::Section, LineMove::Up).is_none());
}

#[test]
fn move_section_outside_any_heading_is_none() {
    assert!(move_segment("plain\ntext", 0, Segment::Section, LineMove::Down).is_none());
}

#[test]
fn heading_needs_a_space_after_the_hashes() {
    let text = "#nope\n# yes\nbody\n# also";
    let (moved, _) = move_segment(text, 8, Segment::Section, LineMove::Down).unwrap();
    assert_eq!(moved, "#nope\n# also\n# yes\nbody");
}

#[test]
fn collect_words_dedups_and_filters_short() {
    let words = collect_words("foo bar foo a bb baz_1");
    assert_eq!(
        words,
        vec![
            ("foo".to_owned(), 2),
            ("bar".to_owned(), 1),
            ("baz_1".to_owned(), 1),
        ]
    );
}

#[test]
fn current_prefix_reads_word_before_cursor() {
    assert_eq!(current_prefix("let val", 7), "val");
    assert_eq!(current_prefix("let val ", 8), "");
    assert_eq!(current_prefix("hä_ll", 5), "hä_ll");
}

#[test]
fn current_word_spans_both_directions() {
    assert_eq!(current_word("hello world", 3), (0, 5));
    assert_eq!(current_word("hello world", 6), (6, 11));
    assert_eq!(current_word("hello world", 5), (0, 5));
    assert_eq!(current_word("a  b", 2), (2, 2));
    assert_eq!(current_word("hä_ll", 5), (0, 5));
}

#[test]
fn matches_keeps_exact_word_beside_longer_ones() {
    let words = vec![
        ("value".to_owned(), 3),
        ("val".to_owned(), 1),
        ("other".to_owned(), 2),
    ];
    let found = matches(&words, "val");
    assert_eq!(
        found,
        vec![(&"value".to_owned(), 3), (&"val".to_owned(), 1)]
    );
}

#[test]
fn matches_is_empty_when_only_the_exact_word_fits() {
    let words = vec![("val".to_owned(), 1), ("other".to_owned(), 2)];
    assert!(matches(&words, "val").is_empty());
}

#[test]
fn char_to_byte_handles_multibyte() {
    assert_eq!(char_to_byte("äbc", 0), 0);
    assert_eq!(char_to_byte("äbc", 1), 2);
    assert_eq!(char_to_byte("äbc", 3), 4);
}

#[test]
fn swap_caret_word_with_next() {
    assert_eq!(
        swap("foo bar", (1, 1), WordSwap::Next).unwrap(),
        ("bar foo".to_owned(), (5, 5))
    );
}

#[test]
fn swap_caret_word_with_prev() {
    assert_eq!(
        swap("foo bar", (5, 5), WordSwap::Prev).unwrap(),
        ("bar foo".to_owned(), (1, 1))
    );
}

#[test]
fn swap_none_without_neighbour() {
    assert!(swap("foo", (1, 1), WordSwap::Next).is_none());
    assert!(swap("  ", (1, 1), WordSwap::Next).is_none());
}

#[test]
fn swap_caret_word_keeps_multibyte() {
    assert_eq!(
        swap("ä ö", (0, 0), WordSwap::Next).unwrap(),
        ("ö ä".to_owned(), (2, 2))
    );
}

#[test]
fn swap_selection_moves_whole_span() {
    assert_eq!(
        swap("don't next", (0, 5), WordSwap::Next).unwrap(),
        ("next don't".to_owned(), (5, 10))
    );
}

#[test]
fn swap_selection_prev_keeps_range() {
    assert_eq!(
        swap("one don't", (4, 9), WordSwap::Prev).unwrap(),
        ("don't one".to_owned(), (0, 5))
    );
}

#[test]
fn insert_places_after_insertion() {
    assert_eq!(insert("ab", 1, "XY"), ("aXYb".to_owned(), 3));
}

#[test]
fn newline_indent_copies_leading_whitespace() {
    assert_eq!(newline_indent("    x", 5), ("    x\n    ".to_owned(), 10));
}

#[test]
fn indent_line_prepends_a_tab_width_of_spaces() {
    assert_eq!(indent_line("ab\ncd", 4, 4), ("ab\n    cd".to_owned(), 8));
    assert_eq!(indent_line("ab\ncd", 4, 2), ("ab\n  cd".to_owned(), 6));
}

#[test]
fn changed_range_spans_restored_text() {
    assert_eq!(changed_range("ac", "abbc"), (1, 3));
}

#[test]
fn changed_range_is_empty_where_text_vanished() {
    assert_eq!(changed_range("abbc", "ac"), (1, 1));
}

#[test]
fn changed_range_counts_characters_not_bytes() {
    assert_eq!(changed_range("äc", "äöüc"), (1, 3));
}

#[test]
fn changed_range_handles_a_repeated_neighbour() {
    assert_eq!(changed_range("aa", "aaa"), (2, 3));
    assert_eq!(changed_range("", "abc"), (0, 3));
}

#[test]
fn dedent_line_removes_leading_spaces() {
    assert_eq!(dedent_line("      cd", 7, 4), ("  cd".to_owned(), 3));
    assert_eq!(dedent_line("cd", 1, 4), ("cd".to_owned(), 1));
    assert_eq!(dedent_line("      cd", 7, 2), ("    cd".to_owned(), 5));
}