use super::helpers::*;
use tower_lsp_server::ls_types::*;
#[tokio::test]
async fn test_hover_on_included_footnote() {
let temp_dir = tempfile::TempDir::new().unwrap();
let child_path = temp_dir.path().join("_child.qmd");
let parent_path = temp_dir.path().join("parent.qmd");
std::fs::write(&child_path, "[^1]: Included footnote content.\n").unwrap();
std::fs::write(&parent_path, "{{< include _child.qmd >}}\nRef[^1].\n").unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(temp_dir.path()).expect("root uri");
let parent_uri = Uri::from_file_path(&parent_path).expect("parent uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
parent_uri.as_str(),
&std::fs::read_to_string(&parent_path).unwrap(),
"quarto",
)
.await;
let hover = server.hover(parent_uri.as_str(), 1, 4).await;
let Some(h) = hover else {
panic!("Expected hover content");
};
if let HoverContents::Markup(markup) = h.contents {
assert!(markup.value.contains("Included footnote content"));
} else {
panic!("Expected markup hover content");
}
}
#[tokio::test]
async fn test_hover_included_updates_after_watcher_change() {
let temp_dir = tempfile::TempDir::new().unwrap();
let child_path = temp_dir.path().join("_child.qmd");
let parent_path = temp_dir.path().join("parent.qmd");
std::fs::write(&child_path, "[^1]: Included footnote content.\n").unwrap();
std::fs::write(&parent_path, "{{< include _child.qmd >}}\nRef[^1].\n").unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(temp_dir.path()).expect("root uri");
let parent_uri = Uri::from_file_path(&parent_path).expect("parent uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
parent_uri.as_str(),
&std::fs::read_to_string(&parent_path).unwrap(),
"quarto",
)
.await;
assert!(
server.hover(parent_uri.as_str(), 1, 4).await.is_some(),
"Sanity check: should resolve hover before edit"
);
std::fs::write(&child_path, "[^2]: Included footnote content.\n").unwrap();
server
.did_change_watched_files(vec![FileEvent {
uri: Uri::from_file_path(&child_path).expect("child uri"),
typ: FileChangeType::CHANGED,
}])
.await;
let hover = server.hover(parent_uri.as_str(), 1, 4).await;
assert!(
hover.is_none(),
"After watcher update, hover should no longer resolve"
);
}
#[tokio::test]
async fn test_hover_on_footnote_reference() {
let server = TestLspServer::new();
let content = r#"Text with footnote[^1] here.
[^1]: This is the footnote content with details.
"#;
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
0, 20, )
.await;
assert!(hover.is_some(), "Should have hover info for footnote");
if let Some(h) = hover {
if let HoverContents::Markup(markup) = h.contents {
assert_eq!(markup.kind, MarkupKind::Markdown);
assert!(
markup.value.contains("footnote content"),
"Should show footnote content"
);
} else {
panic!("Expected markup hover content");
}
}
}
#[tokio::test]
async fn test_hover_on_plain_text() {
let server = TestLspServer::new();
let content = "Just plain text without footnotes.";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server.hover("file:///test.md", 0, 10).await;
assert!(hover.is_none(), "Should not have hover for plain text");
}
#[tokio::test]
async fn test_hover_on_footnote_with_formatting() {
let server = TestLspServer::new();
let content = r#"Reference[^note] in text.
[^note]: Footnote with *emphasis* and `code`.
"#;
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
0, 10, )
.await;
assert!(hover.is_some(), "Should have hover for formatted footnote");
if let Some(h) = hover
&& let HoverContents::Markup(markup) = h.contents
{
let content = markup.value;
assert!(content.contains("*emphasis*"));
assert!(content.contains("`code`"));
}
}
#[tokio::test]
async fn test_hover_on_citation_preview() {
let temp_dir = tempfile::TempDir::new().unwrap();
let root = temp_dir.path();
let bib_path = root.join("refs.bib");
let doc_path = root.join("doc.qmd");
std::fs::write(
&bib_path,
"@article{citekey,\n author = {Doe, Jane},\n year = {2020},\n title = {Sample Title},\n journal = {Journal Name},\n volume = {12},\n number = {3},\n pages = {45-67}\n}\n",
)
.unwrap();
std::fs::write(
&doc_path,
"---\nbibliography: refs.bib\n---\n\nSee [@citekey].\n",
)
.unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(root).expect("root uri");
let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
doc_uri.as_str(),
&std::fs::read_to_string(&doc_path).unwrap(),
"quarto",
)
.await;
let result = server.hover(doc_uri.as_str(), 4, 7).await;
let Some(Hover { contents, .. }) = result else {
panic!("Expected hover content");
};
let content = match contents {
HoverContents::Markup(markup) => markup.value,
HoverContents::Scalar(scalar) => match scalar {
MarkedString::String(text) => text,
MarkedString::LanguageString(lang) => lang.value,
},
HoverContents::Array(array) => array
.iter()
.map(|item| match item {
MarkedString::String(text) => text.clone(),
MarkedString::LanguageString(lang) => lang.value.clone(),
})
.collect::<Vec<_>>()
.join("\n"),
};
assert!(content.contains("Doe"));
assert!(content.contains("2020"));
assert!(content.contains("Sample Title"));
assert!(content.contains("Journal Name"));
}
#[tokio::test]
async fn test_hover_on_undefined_footnote() {
let server = TestLspServer::new();
let content = "Text with undefined[^missing] footnote.";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
0,
25, )
.await;
assert!(
hover.is_none(),
"Should not have hover for undefined footnote"
);
}
#[tokio::test]
async fn test_hover_returns_none_inside_yaml_frontmatter() {
let temp_dir = tempfile::TempDir::new().unwrap();
let root = temp_dir.path();
let bib_path = root.join("refs.bib");
let doc_path = root.join("doc.qmd");
std::fs::write(&bib_path, "@article{known,\n title = {Known}\n}\n").unwrap();
std::fs::write(
&doc_path,
"---\ntitle: \"@known\"\nbibliography: refs.bib\n---\n\nSee [@known].\n",
)
.unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(root).expect("root uri");
let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
doc_uri.as_str(),
&std::fs::read_to_string(&doc_path).unwrap(),
"quarto",
)
.await;
let hover = server.hover(doc_uri.as_str(), 1, 10).await;
assert!(
hover.is_none(),
"Expected no hover when cursor is inside YAML frontmatter"
);
}
#[tokio::test]
async fn test_hover_on_heading_reference_shows_section_preview() {
let server = TestLspServer::new();
let content = "# Intro {#intro}\n\nFirst paragraph in intro section.\n\n## Next\n\nTail.\n\nSee [go](#intro).\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
8, 10, )
.await;
let Some(h) = hover else {
panic!("Expected hover content for heading reference");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Section"));
assert!(content.contains("Intro"));
assert!(content.contains("First paragraph in intro section."));
}
#[tokio::test]
async fn test_hover_on_heading_declaration_returns_none() {
let server = TestLspServer::new();
let content = "# Intro {#intro}\n\nBody.\n\nSee [go](#intro).\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server.hover("file:///test.md", 0, 3).await;
assert!(
hover.is_none(),
"Heading declaration should not produce section preview hover"
);
}
#[tokio::test]
async fn test_hover_on_heading_reference_with_empty_section_shows_title_only() {
let server = TestLspServer::new();
let content = "# Intro {#intro}\n\n## Next\n\nSee [go](#intro).\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
4, 10,
)
.await;
let Some(h) = hover else {
panic!("Expected hover content for heading reference");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Section"));
assert!(content.contains("Intro"));
assert!(!content.contains("..."));
}
#[tokio::test]
async fn test_hover_on_heading_reference_crops_preview() {
let server = TestLspServer::new();
let long_body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(8);
let content = format!(
"# Intro {{#intro}}\n\n{}\n\n## Next\n\nSee [go](#intro).\n",
long_body
);
server
.open_document("file:///test.md", &content, "markdown")
.await;
let hover = server.hover("file:///test.md", 6, 10).await;
let Some(h) = hover else {
panic!("Expected hover content for heading reference");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(
content.ends_with("..."),
"Expected cropped preview to end with ellipsis"
);
}
#[tokio::test]
async fn test_hover_on_reference_link_definition_to_heading_shows_section_preview() {
let server = TestLspServer::new();
let content = "# Intro {#bar}\n\nSection body here.\n\nSee [foo][myref].\n\n[myref]: #bar\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server
.hover(
"file:///test.md",
4, 11, )
.await;
let Some(h) = hover else {
panic!("Expected hover content for reference-style heading link");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Section"));
assert!(content.contains("Intro"));
assert!(content.contains("Section body here."));
}
#[tokio::test]
async fn test_hover_on_reference_link_definition_to_non_heading_returns_none() {
let server = TestLspServer::new();
let content = "# Intro {#bar}\n\nSection body here.\n\nSee [foo][myref].\n\n[myref]: https://example.com\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server.hover("file:///test.md", 4, 11).await;
assert!(
hover.is_none(),
"Non-heading reference definitions should not produce section preview hover"
);
}
#[tokio::test]
async fn test_hover_on_equation_reference_shows_equation_preview() {
let server = TestLspServer::new();
let content = "$$\na = b + c\n$$ {#eq-foo}\n\n@eq-foo\n";
server
.open_document("file:///test.qmd", content, "quarto")
.await;
let hover = server.hover("file:///test.qmd", 4, 4).await;
let Some(h) = hover else {
panic!("Expected hover content for equation reference");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Equation"));
assert!(content.contains("eq-foo"));
assert!(content.contains("```tex"));
assert!(content.contains("a = b + c"));
}
#[tokio::test]
async fn test_hover_on_equation_reference_crops_preview_lines() {
let server = TestLspServer::new();
let content =
"$$\nline1\nline2\nline3\nline4\nline5\nline6\nline7\n$$ {#eq-long}\n\n@eq-long\n";
server
.open_document("file:///test.qmd", content, "quarto")
.await;
let hover = server.hover("file:///test.qmd", 10, 4).await;
let Some(h) = hover else {
panic!("Expected hover content for equation reference");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("line6"));
assert!(!content.contains("line7"));
assert!(content.contains("\n...\n```"));
}
#[tokio::test]
async fn test_hover_on_direct_local_markdown_link_shows_linked_doc_preview() {
let temp_dir = tempfile::TempDir::new().unwrap();
let doc_path = temp_dir.path().join("doc.qmd");
let linked_path = temp_dir.path().join("linked.md");
std::fs::write(
&linked_path,
"# Linked title\n\nLinked paragraph preview text.\n",
)
.unwrap();
std::fs::write(&doc_path, "See [linked](./linked.md).\n").unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(temp_dir.path()).expect("root uri");
let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
doc_uri.as_str(),
&std::fs::read_to_string(&doc_path).unwrap(),
"quarto",
)
.await;
let hover = server.hover(doc_uri.as_str(), 0, 17).await;
let Some(h) = hover else {
panic!("Expected hover content for direct local markdown link");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Linked document"));
assert!(content.contains("linked.md"));
assert!(content.contains("Linked title"));
assert!(content.contains("Linked paragraph preview text."));
}
#[tokio::test]
async fn test_hover_on_reference_definition_local_markdown_link_shows_linked_doc_preview() {
let temp_dir = tempfile::TempDir::new().unwrap();
let doc_path = temp_dir.path().join("doc.qmd");
let linked_path = temp_dir.path().join("linked.qmd");
std::fs::write(
&linked_path,
"# Ref linked title\n\nRef linked paragraph preview text.\n",
)
.unwrap();
std::fs::write(&doc_path, "See [linked][ref].\n\n[ref]: ./linked.qmd\n").unwrap();
let server = TestLspServer::new();
let root_uri = Uri::from_file_path(temp_dir.path()).expect("root uri");
let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
server.initialize(root_uri.as_str()).await;
server
.open_document(
doc_uri.as_str(),
&std::fs::read_to_string(&doc_path).unwrap(),
"quarto",
)
.await;
let hover = server.hover(doc_uri.as_str(), 0, 14).await;
let Some(h) = hover else {
panic!("Expected hover content for reference-definition local markdown link");
};
let content = match h.contents {
HoverContents::Markup(markup) => markup.value,
_ => panic!("Expected markdown hover content"),
};
assert!(content.contains("Linked document"));
assert!(content.contains("linked.qmd"));
assert!(content.contains("Ref linked title"));
assert!(content.contains("Ref linked paragraph preview text."));
}
#[tokio::test]
async fn test_hover_on_missing_local_markdown_link_returns_none() {
let server = TestLspServer::new();
let content = "See [missing](./does-not-exist.md).\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server.hover("file:///test.md", 0, 20).await;
assert!(
hover.is_none(),
"Missing local linked document should not produce hover preview"
);
}
#[tokio::test]
async fn test_hover_on_external_url_link_returns_none_for_linked_doc_preview() {
let server = TestLspServer::new();
let content = "See [site](https://example.com).\n";
server
.open_document("file:///test.md", content, "markdown")
.await;
let hover = server.hover("file:///test.md", 0, 13).await;
assert!(
hover.is_none(),
"External URL links should not produce linked-document previews"
);
}