use crate::position::Range;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hover {
markdown: String,
pub range: Option<Range>,
}
impl Hover {
#[must_use]
pub fn new(markdown: impl Into<String>, range: Option<Range>) -> Self {
Self {
markdown: crate::lsp_helpers::replace_markdown_unsafe_chars_keep_newlines(
&markdown.into(),
),
range,
}
}
#[must_use]
pub fn markdown(&self) -> &str {
&self.markdown
}
pub fn push_markdown(&mut self, extra: &str) {
self.markdown
.push_str(&crate::lsp_helpers::replace_markdown_unsafe_chars_keep_newlines(extra));
}
pub fn rewrite_markdown(&mut self, f: impl FnOnce(&str) -> String) {
let rewritten = f(&self.markdown);
self.markdown = crate::lsp_helpers::replace_markdown_unsafe_chars_keep_newlines(&rewritten);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::position::Position;
#[test]
fn test_hover_new_sets_markdown_and_range() {
let range = Range::new(Position::new(0, 0), Position::new(0, 4));
let hover = Hover::new("# serde", Some(range));
assert_eq!(hover.markdown(), "# serde");
assert_eq!(hover.range, Some(range));
}
#[test]
fn test_hover_new_leaves_range_unset_when_none() {
let hover = Hover::new("# serde", None);
assert_eq!(hover.range, None);
}
#[test]
fn test_hover_new_sanitizes_bidi_override_in_markdown() {
let hover = Hover::new("# serde\u{202E}gnp.sj", None);
assert_eq!(hover.markdown(), "# serde gnp.sj");
}
#[test]
fn test_hover_new_preserves_structural_newlines() {
let hover = Hover::new("# serde\n\n**Current**: `1.0.0`", None);
assert_eq!(hover.markdown(), "# serde\n\n**Current**: `1.0.0`");
}
#[test]
fn test_hover_push_markdown_appends_sanitized_content() {
let mut hover = Hover::new("# express", None);
hover.push_markdown("\n\n**Catalog**: `default`\u{202E}gnp.sj");
assert_eq!(
hover.markdown(),
"# express\n\n**Catalog**: `default` gnp.sj"
);
}
#[test]
fn test_hover_rewrite_markdown_replaces_sanitized_content() {
let mut hover = Hover::new("**Current**: `main`", None);
hover.rewrite_markdown(|md| format!("{md}\u{202E}gnp.sj"));
assert_eq!(hover.markdown(), "**Current**: `main` gnp.sj");
}
#[test]
fn test_hover_rewrite_markdown_can_splice_mid_string() {
let mut hover = Hover::new("**Current**: `main`", None);
hover.rewrite_markdown(|md| md.replace("`main`", "`main` (resolved: `a1b2c3d`)"));
assert_eq!(
hover.markdown(),
"**Current**: `main` (resolved: `a1b2c3d`)"
);
}
#[test]
fn test_hover_new_sanitizes_full_unsafe_char_class_and_is_idempotent() {
for c in [
'\u{202E}', '\u{2066}', '\u{200B}', '\u{2060}', '\u{2028}', '\u{2029}', '\u{FEFF}', '\u{FFFA}', '\u{E0041}', '\t',
] {
let once = Hover::new(format!("a{c}b"), None);
assert_eq!(
once.markdown(),
"a b",
"{c:?} must be replaced with a space"
);
let twice = Hover::new(once.markdown(), None);
assert_eq!(
twice.markdown(),
once.markdown(),
"{c:?} must not change on re-application"
);
}
}
#[test]
fn test_hover_preserves_newlines_through_every_sanitizing_entry_point() {
let mut hover = Hover::new("# serde", None);
hover.push_markdown("\n\n**Current**: `1.0.0`");
assert_eq!(hover.markdown(), "# serde\n\n**Current**: `1.0.0`");
hover.rewrite_markdown(|md| format!("{md}\n\n**Latest**: `1.0.1`"));
assert_eq!(
hover.markdown(),
"# serde\n\n**Current**: `1.0.0`\n\n**Latest**: `1.0.1`"
);
}
}