use crossterm::event::KeyModifiers;
use edamame::config::Theme;
use edamame::document::Buffer;
use edamame::editor::link::LinkTarget;
use edamame::editor::{edit_ops, footnote_edit, mouse_ops, EditorState, Mode};
use edamame::input::MouseAction;
const VP: usize = 40;
const VW: usize = 80;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn state(text: &str) -> EditorState {
EditorState::new(Buffer::from_str(text), theme())
}
#[test]
fn footnote_edit_api_is_reachable_and_correct() {
let src = "A[^2] B[^1]\n\n[^2]: two\n[^1]: one\n";
assert_eq!(footnote_edit::next_footnote_number(src), 3);
assert!(footnote_edit::renumber_footnotes(src).is_some());
assert!(footnote_edit::renumber_footnotes("A[^1] B[^2]\n\n[^1]: x\n[^2]: y\n").is_none());
}
#[test]
fn renumber_wrapper_reorders_buffer_by_first_reference() {
let mut st = state("A[^2] B[^1]\n\n[^2]: two\n[^1]: one\n");
st.mode = Mode::Rendered;
assert!(edit_ops::renumber_footnotes(&mut st, VP, VW));
assert_eq!(st.contents(), "A[^1] B[^2]\n\n[^1]: two\n[^2]: one\n");
}
#[test]
fn insert_footnote_wrapper_auto_numbers() {
let mut st = state("Claim.\n\n[^1]: prior note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = st.buffer.rope().byte_to_char("Claim.".len());
edit_ops::insert_footnote_at_cursor(&mut st, VP, VW);
assert!(
st.contents().starts_with("Claim.[^2]"),
"got: {:?}",
st.contents()
);
}
#[test]
fn delete_footnote_wrapper_off_a_footnote_returns_false() {
let mut st = state("plain text\n");
st.mode = Mode::Rendered;
st.cursor.offset = 3;
assert!(!edit_ops::delete_footnote_at_cursor(&mut st, VP, VW));
}
#[test]
fn mouse_click_on_reference_stages_footnote_follow() {
let mut st = state("Body[^1] more.\n\n[^1]: the note.\n");
assert_eq!(st.mode, Mode::Preview);
let action = MouseAction::Click {
col: 4,
row: 0,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::Footnote("1".into())),
"click on the marker should stage a footnote follow"
);
}
#[test]
fn hover_over_reference_marker_is_clickable() {
let st = state("Body[^1] more.\n\n[^1]: the note.\n");
assert!(
mouse_ops::hit_test_clickable(&st, 4, 0, VW, &[]),
"the footnote marker should be clickable"
);
assert!(
!mouse_ops::hit_test_clickable(&st, 1, 0, VW, &[]),
"plain text should not be clickable"
);
}
#[test]
fn click_inside_a_fused_marker_follows_the_label_clicked() {
let src = "First line.\n\nBody[^1][^2] more.\n\n[^1]: one.\n\n[^2]: two.\n";
for (col, label) in [(5u16, "1"), (7u16, "2")] {
let mut st = state(src);
st.cursor.offset = 0;
let row = row_containing(&st, "Body");
let action = MouseAction::Click {
col,
row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::Footnote(label.into())),
"click at col {col} should follow footnote {label}"
);
}
}
#[test]
fn hover_over_definition_back_link_is_clickable() {
let st = state("Body[^1] more.\n\n[^1]: the note.\n");
let def_row = st
.parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains("the note.")
})
.expect("definition line") as u16;
assert!(
mouse_ops::hit_test_clickable(&st, 0, def_row, VW, &[]),
"the definition leader should be clickable"
);
let def_line: String = st.parsed.lines[def_row as usize]
.spans
.iter()
.map(|s| s.content.as_ref())
.collect();
let glyph_col = (def_line.chars().count() - 1) as u16;
assert!(
def_line.ends_with('↩'),
"definition should end with the back-link glyph: {def_line:?}"
);
assert!(
mouse_ops::hit_test_clickable(&st, glyph_col, def_row, VW, &[]),
"the trailing back-link glyph should be clickable"
);
}
fn row_containing(st: &EditorState, needle: &str) -> u16 {
st.parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains(needle)
})
.expect("line present") as u16
}
#[test]
fn rendered_mode_plain_click_on_reference_follows() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "Body"); let action = MouseAction::Click {
col: 4,
row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::Footnote("1".into())),
"plain click on the marker should follow in Rendered mode"
);
}
#[test]
fn rendered_mode_plain_click_on_back_link_follows() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "the note."); let action = MouseAction::Click {
col: 0,
row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::FootnoteBack("1".into())),
"plain click on the leader should follow in Rendered mode"
);
}
#[test]
fn rendered_mode_plain_click_on_trailing_glyph_follows() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "the note.");
let def_line: String = st.parsed.lines[row as usize]
.spans
.iter()
.map(|s| s.content.as_ref())
.collect();
assert!(def_line.ends_with('↩'), "got: {def_line:?}");
let glyph_col = (def_line.chars().count() - 1) as u16;
let action = MouseAction::Click {
col: glyph_col,
row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::FootnoteBack("1".into())),
"plain click on the trailing ↩ glyph should follow"
);
}
#[test]
fn rendered_mode_click_on_space_before_glyph_follows() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "the note.");
let width = st.parsed.lines[row as usize]
.spans
.iter()
.map(|s| s.content.chars().count())
.sum::<usize>();
let space_col = (width - 2) as u16; let action = MouseAction::Click {
col: space_col,
row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow,
Some(LinkTarget::FootnoteBack("1".into())),
"the space before the glyph should follow"
);
}
#[test]
fn rendered_mode_click_past_glyph_places_cursor_not_follow() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "the note.");
let width = st.parsed.lines[row as usize]
.spans
.iter()
.map(|s| s.content.chars().count())
.sum::<usize>();
let action = MouseAction::Click {
col: (width + 5) as u16, row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(
st.pending_link_follow, None,
"a click past the glyph should not follow the back-link"
);
}
#[test]
fn rendered_mode_plain_click_on_text_places_cursor_not_follow() {
let mut st = state("First line.\n\nBody[^1] more.\n\n[^1]: the note.\n");
st.mode = Mode::Rendered;
st.cursor.offset = 0;
let row = row_containing(&st, "Body");
let action = MouseAction::Click {
col: 1, row,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(st.pending_link_follow, None);
}
#[test]
fn mouse_click_on_plain_text_stages_nothing() {
let mut st = state("Body[^1] more.\n\n[^1]: the note.\n");
let action = MouseAction::Click {
col: 1, row: 0,
modifiers: KeyModifiers::NONE,
};
let mut drag = None;
mouse_ops::apply(&mut st, action, &mut drag, &[], VP, VW);
assert_eq!(st.pending_link_follow, None);
}
fn back_link_cols(src: &str, width: u16, row: u16) -> (Vec<u16>, Vec<u16>) {
let fresh = || {
let mut st = state(src);
st.mode = Mode::Rendered;
st.set_viewport_width(usize::from(width));
st.cursor.offset = 0;
st
};
let mut follows = Vec::new();
let mut hovers = Vec::new();
for col in 0..width {
if mouse_ops::hit_test_clickable_non_link(&fresh(), col, row, usize::from(width), &[]) {
hovers.push(col);
}
let mut st = fresh();
let action = MouseAction::Click {
col,
row,
modifiers: KeyModifiers::NONE,
};
mouse_ops::apply(&mut st, action, &mut None, &[], VP, usize::from(width));
if st.pending_link_follow == Some(LinkTarget::FootnoteBack("1".into())) {
follows.push(col);
}
}
(follows, hovers)
}
#[test]
fn the_back_link_after_wide_text_is_where_it_paints() {
let src = "First line.\n\nBody[^1] more.\n\n[^1]: 日本語の注釈です。\n";
let (follows, hovers) = back_link_cols(src, 80, 4);
let past_leader = |cols: Vec<u16>| cols.into_iter().filter(|&c| c >= 6).collect::<Vec<_>>();
assert_eq!(past_leader(follows), [24, 25]);
assert_eq!(past_leader(hovers), [24, 25]);
}
#[test]
fn the_back_link_on_a_wrapped_definitions_last_row_is_clickable() {
let src =
"First line.\n\nBody[^1] more.\n\n[^1]: alpha bravo charlie delta echo foxtrot golf\n";
assert_eq!(back_link_cols(src, 26, 5), (vec![], vec![]));
assert_eq!(back_link_cols(src, 26, 6), (vec![10, 11], vec![10, 11]));
}
#[test]
fn a_footnote_marker_after_wide_text_hovers() {
let mut st = state("日本語日本語日本語日本語[^1] tail\n\n[^1]: n\n");
st.mode = Mode::Rendered;
st.cursor.offset = st.buffer.rope().len_chars();
for col in 24..=26 {
assert!(
mouse_ops::hit_test_clickable_non_link(&st, col, 0, VW, &[]),
"col {col}"
);
}
assert!(!mouse_ops::hit_test_clickable_non_link(&st, 28, 0, VW, &[]));
}