5036ec2dbb
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
187 lines
5.4 KiB
Rust
187 lines
5.4 KiB
Rust
//! Completion triggers for each form, negative cases and `apply`.
|
|
|
|
use notescraft_editor::completion::{Trigger, apply, trigger_at};
|
|
|
|
fn trigger_text(text: &str, cursor: usize) -> Trigger {
|
|
trigger_at(text, cursor).expect("expected a trigger")
|
|
}
|
|
|
|
#[test]
|
|
fn wikilink_open_query() {
|
|
let text = "see [[hello";
|
|
let t = trigger_text(text, text.len());
|
|
match t {
|
|
Trigger::Wikilink { query, replace } => {
|
|
assert_eq!(query, "hello");
|
|
assert_eq!(replace, text.find('[').unwrap() + 2..text.len());
|
|
}
|
|
other => panic!("wrong trigger {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wikilink_closed_is_negative() {
|
|
let text = "see [[hello]]";
|
|
assert!(trigger_at(text, text.len()).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn wikilink_newline_inside_is_negative() {
|
|
let text = "[[hello\nworld";
|
|
assert!(trigger_at(text, text.len()).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn heading_trigger() {
|
|
let text = "[[note#Head";
|
|
let t = trigger_text(text, text.len());
|
|
match t {
|
|
Trigger::Heading { note, query, replace } => {
|
|
assert_eq!(note, "note");
|
|
assert_eq!(query, "Head");
|
|
assert_eq!(replace, text.find('#').unwrap() + 1..text.len());
|
|
}
|
|
other => panic!("wrong trigger {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn block_trigger_query_with_caret() {
|
|
let text = "[[note#Head^b";
|
|
let t = trigger_text(text, text.len());
|
|
match t {
|
|
Trigger::Block { note, query, .. } => {
|
|
assert_eq!(note, "note");
|
|
assert_eq!(query, "Head^b");
|
|
}
|
|
other => panic!("wrong trigger {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tag_trigger_word() {
|
|
let text = "a #work/tag";
|
|
let t = trigger_text(text, text.len());
|
|
match t {
|
|
Trigger::Tag { query, replace } => {
|
|
assert_eq!(query, "work/tag");
|
|
assert_eq!(replace, text.find('#').unwrap() + 1..text.len());
|
|
}
|
|
other => panic!("wrong trigger {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tag_hash_midword_negative() {
|
|
let text = "x#not-a-tag";
|
|
assert!(trigger_at(text, text.len()).is_none());
|
|
let text = "_#not";
|
|
assert!(trigger_at(text, text.len()).is_none());
|
|
// `# heading` (a space between # and the word, cursor at end) is not a tag.
|
|
let text = "# heading";
|
|
let at = text.len();
|
|
assert!(trigger_at(text, at).is_none());
|
|
// `## double` likewise.
|
|
let text = "## title";
|
|
let at = text.len();
|
|
assert!(trigger_at(text, at).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn callout_trigger() {
|
|
let text = "> [!info";
|
|
let t = trigger_text(text, text.len());
|
|
match t {
|
|
Trigger::Callout { query, replace } => {
|
|
assert_eq!(query, "info");
|
|
assert_eq!(replace, text.find("[!").unwrap() + 2..text.len());
|
|
}
|
|
other => panic!("wrong trigger {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn callout_needs_quote_prefix() {
|
|
let text = "[!info";
|
|
assert!(trigger_at(text, text.len()).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn tag_inside_code_span_negative() {
|
|
let text = "`#maybe`";
|
|
let at = text.find('#').unwrap();
|
|
assert!(trigger_at(text, at).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn apply_appends_closing_brackets() {
|
|
let text = "[[hel";
|
|
let t = trigger_text(text, text.len());
|
|
let edit = apply(text, &t, text.len(), "hello.md").expect("apply works");
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
assert_eq!(out, "[[hello.md]]");
|
|
assert_eq!(edit.sel, edit.range.start + edit.insert.len()..edit.range.start + edit.insert.len());
|
|
}
|
|
|
|
#[test]
|
|
fn apply_does_not_double_close() {
|
|
let text = "[[hel]]";
|
|
// Trigger found at the moment before `]]` was typed: cursor between.
|
|
let cursor = text.find("]]").unwrap();
|
|
let t = trigger_text("[[hel", cursor);
|
|
let edit = apply(text, &t, cursor, "hello.md").expect("apply works");
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
assert_eq!(out, "[[hello.md]]");
|
|
}
|
|
|
|
#[test]
|
|
fn apply_tag_and_heading() {
|
|
let text = "see #my/tag";
|
|
let t = trigger_text(text, text.len());
|
|
let edit = apply(text, &t, text.len(), "my/full/tag").expect("apply works");
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
assert_eq!(out, "see #my/full/tag");
|
|
|
|
let text = "[[note#Hea";
|
|
let t = trigger_text(text, text.len());
|
|
let edit = apply(text, &t, text.len(), "Heading two").expect("apply works");
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
assert_eq!(out, "[[note#Heading two");
|
|
}
|
|
|
|
#[test]
|
|
fn apply_callout_inserts_bracket_and_space() {
|
|
let text = "> [!";
|
|
let t = trigger_text(text, text.len());
|
|
let edit = apply(text, &t, text.len(), "info").expect("apply works");
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
assert_eq!(out, "> [!info] ");
|
|
}
|
|
|
|
#[test]
|
|
fn triggers_suppressed_inside_code_fences() {
|
|
let text = "```\n#nota\n```";
|
|
let mid = text.find("#nota").unwrap();
|
|
assert!(trigger_at(text, mid).is_none());
|
|
|
|
let text = "```\n[[note";
|
|
let mid = text.find("[[").unwrap();
|
|
assert!(trigger_at(text, mid).is_none());
|
|
|
|
let text = "> no\n```\n> [!inf";
|
|
let mid = text.find("[!").unwrap();
|
|
assert!(trigger_at(text, mid).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn apply_rejects_bad_ranges() {
|
|
let t = Trigger::Tag { query: String::from("x"), replace: 99..100 };
|
|
let text = "abc";
|
|
assert!(apply(text, &t, 3, "y").is_none());
|
|
}
|