use crate::syntax::{AstNode, FootnoteDefinition, FootnoteReference, InlineFootnote, SyntaxNode};
use tower_lsp_server::ls_types::{Range, TextEdit};
use super::super::conversions::offset_to_position;
pub fn find_footnote_reference_at_position(tree: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
find_ancestor_at_offset(tree, offset, FootnoteReference::cast)
.map(|reference| reference.syntax().clone())
}
pub fn find_inline_footnote_at_position(tree: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
find_ancestor_at_offset(tree, offset, InlineFootnote::cast)
.map(|inline| inline.syntax().clone())
}
pub fn can_convert_to_inline(reference_node: &SyntaxNode, tree: &SyntaxNode) -> bool {
let reference = match FootnoteReference::cast(reference_node.clone()) {
Some(r) => r,
None => return false,
};
let id = reference.id();
tree.descendants()
.find_map(|node| FootnoteDefinition::cast(node).filter(|def| def.id() == id))
.map(|def| def.is_simple())
.unwrap_or(false)
}
pub fn convert_to_inline(
reference_node: &SyntaxNode,
tree: &SyntaxNode,
text: &str,
) -> Vec<TextEdit> {
let reference = match FootnoteReference::cast(reference_node.clone()) {
Some(r) => r,
None => return vec![],
};
let id = reference.id();
let definition = match tree
.descendants()
.find_map(|node| FootnoteDefinition::cast(node).filter(|def| def.id() == id))
{
Some(def) => def,
None => return vec![],
};
if !definition.is_simple() {
return vec![];
}
let mut edits = Vec::new();
let content = definition.content().trim().to_string();
let ref_start = offset_to_position(text, reference_node.text_range().start().into());
let ref_end = offset_to_position(text, reference_node.text_range().end().into());
edits.push(TextEdit {
range: Range {
start: ref_start,
end: ref_end,
},
new_text: format!("^[{}]", content),
});
let def_node = definition.syntax();
let def_start: usize = def_node.text_range().start().into();
let def_end: usize = def_node.text_range().end().into();
let extended_end = if def_end < text.len() && text.as_bytes()[def_end] == b'\n' {
def_end + 1
} else {
def_end
};
edits.push(TextEdit {
range: Range {
start: offset_to_position(text, def_start),
end: offset_to_position(text, extended_end),
},
new_text: String::new(),
});
edits
}
pub fn generate_footnote_id(tree: &SyntaxNode) -> String {
let max_id = tree
.descendants()
.filter_map(FootnoteDefinition::cast)
.filter_map(|def| def.id().parse::<u32>().ok())
.max()
.unwrap_or(0);
(max_id + 1).to_string()
}
pub fn convert_to_reference(
inline_node: &SyntaxNode,
tree: &SyntaxNode,
text: &str,
) -> Vec<TextEdit> {
let inline = match InlineFootnote::cast(inline_node.clone()) {
Some(i) => i,
None => return vec![],
};
let content = inline.content();
let id = generate_footnote_id(tree);
let mut edits = Vec::new();
let inline_start = offset_to_position(text, inline_node.text_range().start().into());
let inline_end = offset_to_position(text, inline_node.text_range().end().into());
edits.push(TextEdit {
range: Range {
start: inline_start,
end: inline_end,
},
new_text: format!("[^{}]", id),
});
let insert_position = tree
.descendants()
.filter_map(FootnoteDefinition::cast)
.last()
.map(|def| {
let end: usize = def.syntax().text_range().end().into();
offset_to_position(text, end)
})
.unwrap_or_else(|| {
offset_to_position(text, text.len())
});
let prefix = if tree
.descendants()
.filter_map(FootnoteDefinition::cast)
.next()
.is_some()
{
"\n"
} else {
"\n\n"
};
edits.push(TextEdit {
range: Range {
start: insert_position,
end: insert_position,
},
new_text: format!("{}[^{}]: {}\n", prefix, id, content),
});
edits
}
fn find_ancestor_at_offset<T: AstNode<Language = crate::syntax::PanacheLanguage>>(
tree: &SyntaxNode,
offset: usize,
cast: fn(SyntaxNode) -> Option<T>,
) -> Option<T> {
let text_size = rowan::TextSize::from(offset as u32);
let token = tree.token_at_offset(text_size).right_biased()?;
token.parent_ancestors().find_map(cast)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn find_footnote_reference_at_cursor() {
let input = "Text [^1] more text.\n\n[^1]: Footnote content.";
let tree = parse(input, None);
let offset = input.find("[^1]").unwrap() + 2;
let node =
find_footnote_reference_at_position(&tree, offset).expect("Should find reference");
assert!(FootnoteReference::cast(node).is_some());
}
#[test]
fn find_inline_footnote_at_cursor() {
let input = "Text^[Inline note] more text.";
let tree = parse(input, None);
let offset = input.find("Inline").unwrap();
let node = find_inline_footnote_at_position(&tree, offset).expect("Should find inline");
assert!(InlineFootnote::cast(node).is_some());
}
#[test]
fn can_convert_simple_footnote() {
let input = "Text [^1] more.\n\n[^1]: Simple footnote.";
let tree = parse(input, None);
let ref_node = tree
.descendants()
.find_map(FootnoteReference::cast)
.map(|reference| reference.syntax().clone())
.unwrap();
assert!(can_convert_to_inline(&ref_node, &tree));
}
#[test]
fn cannot_convert_complex_footnote() {
let input = "Text [^1] more.\n\n[^1]: First para.\n\n Second para.";
let tree = parse(input, None);
let ref_node = tree
.descendants()
.find_map(FootnoteReference::cast)
.map(|reference| reference.syntax().clone())
.unwrap();
assert!(!can_convert_to_inline(&ref_node, &tree));
}
#[test]
fn test_convert_reference_to_inline() {
let input = "Text [^1] more.\n\n[^1]: Simple note.";
let tree = parse(input, None);
let ref_node = tree
.descendants()
.find_map(FootnoteReference::cast)
.map(|reference| reference.syntax().clone())
.unwrap();
let edits = convert_to_inline(&ref_node, &tree, input);
assert_eq!(edits.len(), 2);
assert!(edits[0].new_text.contains("^[Simple note."));
assert_eq!(edits[1].new_text, "");
}
#[test]
fn test_generate_footnote_id() {
let input = "[^1]: First.\n[^2]: Second.\n[^5]: Fifth.";
let tree = parse(input, None);
let id = generate_footnote_id(&tree);
assert_eq!(id, "6"); }
#[test]
fn test_generate_footnote_id_no_existing() {
let input = "Just text.";
let tree = parse(input, None);
let id = generate_footnote_id(&tree);
assert_eq!(id, "1"); }
#[test]
fn test_convert_inline_to_reference() {
let input = "Text^[Inline note] more.";
let tree = parse(input, None);
let inline_node = tree
.descendants()
.find_map(InlineFootnote::cast)
.map(|inline| inline.syntax().clone())
.unwrap();
let edits = convert_to_reference(&inline_node, &tree, input);
assert_eq!(edits.len(), 2);
assert!(edits[0].new_text.contains("[^1]"));
assert!(edits[1].new_text.contains("[^1]: Inline note"));
}
#[test]
fn test_convert_inline_to_reference_with_existing() {
let input = "Text^[New note] more.\n\n[^1]: Existing.";
let tree = parse(input, None);
let inline_node = tree
.descendants()
.find_map(InlineFootnote::cast)
.map(|inline| inline.syntax().clone())
.unwrap();
let edits = convert_to_reference(&inline_node, &tree, input);
assert_eq!(edits.len(), 2);
assert!(edits[0].new_text.contains("[^2]"));
assert!(edits[1].new_text.contains("[^2]: New note"));
}
}