Files
Marcos Paulo 4ea2e9fd5a
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: outliner list moves, notescraft:// URI resolve, periodic notes (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 05:54:18 -03:00

165 lines
5.8 KiB
Rust

//! Outliner: moving list items with their children, and shifting subtrees.
use std::fmt::Write as _;
use std::time::{Duration, Instant};
use notescraft_editor::keys::{Edit, Indent};
use notescraft_editor::{Direction, indent_subtree, move_item};
/// The text with the edit applied.
fn applied(text: &str, edit: &Edit) -> String {
let mut out = String::from(text);
out.replace_range(edit.range.clone(), &edit.insert);
out
}
#[test]
fn outliner_moves_an_item_down_and_up() {
let text = "- a\n- b\n- c\n";
let edit = move_item(text, 4..4, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- a\n- c\n- b\n");
let edit = move_item(text, 8..8, Direction::Up, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- a\n- c\n- b\n");
}
#[test]
fn outliner_moves_the_children_with_their_item() {
let text = "- a\n - a1\n- b\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- b\n- a\n - a1\n");
let text = "- a\n - a1\n - a2\n - a3\n- b\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- b\n- a\n - a1\n - a2\n - a3\n");
}
#[test]
fn outliner_moves_across_a_task_item() {
let text = "- a\n- [ ] task\n- c\n";
let at = text.find("- c").unwrap();
let edit = move_item(text, at..at, Direction::Up, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- a\n- c\n- [ ] task\n");
}
#[test]
fn outliner_renumbers_an_ordered_list() {
let text = "1. a\n2. b\n3. c\n";
let at = text.find("3. c").unwrap();
let edit = move_item(text, at..at, Direction::Up, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "1. a\n2. c\n3. b\n");
}
#[test]
fn outliner_leaves_the_first_and_the_last_item_alone() {
let text = "- a\n- b\n";
assert!(move_item(text, 0..0, Direction::Up, Indent::default()).is_none());
assert!(move_item(text, 4..4, Direction::Down, Indent::default()).is_none());
assert!(move_item(text, 0..0, Direction::Down, Indent::default()).is_some());
assert!(move_item(text, 4..4, Direction::Up, Indent::default()).is_some());
}
#[test]
fn outliner_moves_a_fenced_code_block_with_its_item() {
let text = "- a\n ```\n code\n ```\n- b\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- b\n- a\n ```\n code\n ```\n");
}
#[test]
fn outliner_keeps_crlf_line_endings() {
let text = "- a\r\n- b\r\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- b\r\n- a\r\n");
}
#[test]
fn outliner_moves_items_indented_with_tabs_and_with_spaces() {
let text = "- a\n\t- b\n- c\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- c\n- a\n\t- b\n");
let text = "- a\n - b\n- c\n";
let edit = move_item(text, 0..0, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- c\n- a\n - b\n");
}
#[test]
fn outliner_keeps_the_cursor_on_the_moved_marker() {
let text = "- a\n- b\n- c\n";
let edit = move_item(text, 4..4, Direction::Down, Indent::default()).unwrap();
assert_eq!(edit.sel, 8..8);
assert_eq!(applied(text, &edit).get(8..9), Some("-"));
let text = "1. a\n2. b\n3. c\n";
let at = text.find("3. c").unwrap();
let edit = move_item(text, at..at, Direction::Up, Indent::default()).unwrap();
assert_eq!(edit.sel, 5..5);
assert_eq!(applied(text, &edit).get(5..6), Some("2"));
}
#[test]
fn outliner_moves_the_first_item_of_a_selection() {
let text = "- a\n- b\n- c\n";
let edit = move_item(text, 0..8, Direction::Down, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- b\n- a\n- c\n");
assert_eq!(edit.sel, 4..4);
}
#[test]
fn outliner_moves_a_long_list_quickly() {
let mut text = String::new();
for index in 0..2000 {
writeln!(text, "- item {index}").unwrap();
}
let start = Instant::now();
let edit = move_item(&text, 0..0, Direction::Down, Indent::default());
let elapsed = start.elapsed();
assert!(edit.is_some());
assert!(elapsed < Duration::from_millis(5), "moving an item took {elapsed:?}");
}
#[test]
fn outliner_indents_an_item_with_its_children() {
let text = "- a\n - b\n - c\n- d\n";
let edit = indent_subtree(text, 0..0, false, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), " - a\n - b\n - c\n- d\n");
assert_eq!(edit.sel, 4..4, "the cursor stays on its character");
}
#[test]
fn outliner_outdents_an_item_with_its_children() {
let text = " - a\n - b\n- c\n";
let at = text.find("- a").unwrap();
let edit = indent_subtree(text, at..at, true, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), "- a\n - b\n- c\n");
assert_eq!(edit.sel, 0..0);
}
#[test]
fn outliner_refuses_to_outdent_below_the_surface_of_the_list() {
let text = "- a\n- b\n";
assert!(indent_subtree(text, 0..0, true, Indent::default()).is_none());
}
#[test]
fn outliner_indents_with_the_published_indent_unit() {
let text = "- a\n";
let indent = Indent { tabs: true, width: 4 };
let edit = indent_subtree(text, 0..0, false, indent).unwrap();
assert_eq!(applied(text, &edit), "\t- a\n");
}
#[test]
fn outliner_shifts_every_item_a_selection_touches() {
let text = "- a\n- b\n";
let edit = indent_subtree(text, 0..8, false, Indent::default()).unwrap();
assert_eq!(applied(text, &edit), " - a\n - b\n");
}
#[test]
fn outliner_ignores_text_outside_a_list_item() {
let text = "plain words\n";
assert!(indent_subtree(text, 0..0, false, Indent::default()).is_none());
assert!(move_item(text, 0..0, Direction::Down, Indent::default()).is_none());
}