//! Helpers shared by the Phase 8 navigation handlers (definition, //! references, hover, completion). //! //! Two responsibilities: //! //! 1. **Position conversion** — clients send LSP `Position`s in either //! UTF-8 or UTF-16 encoding; nuwiki AST spans are byte offsets. //! `lsp_to_byte_pos` translates the LSP position into the same shape //! so it can be compared against AST spans. //! 2. **Position lookup** — given a byte position, find the most specific //! `InlineNode` whose span covers it. Used to identify the link under //! the cursor. use nuwiki_core::ast::{BlockNode, DocumentNode, InlineNode, ListItemNode, Span}; use tower_lsp::lsp_types::Position as LspPosition; use crate::semantic_tokens::LineIndex; /// Translate an LSP `Position` (whose `character` is a UTF-16 code-unit /// offset by default, or a UTF-8 byte offset when negotiated) into our /// internal `(line, byte_col)` shape. pub fn lsp_to_byte_pos(p: LspPosition, text: &str, utf8: bool) -> (u32, u32) { if utf8 { return (p.line, p.character); } let idx = LineIndex::new(text); let line_str = idx.line_str(p.line, text); let byte_col = utf16_to_byte_col(line_str, p.character); (p.line, byte_col) } fn utf16_to_byte_col(line: &str, target_utf16: u32) -> u32 { let mut byte = 0u32; let mut units = 0u32; for ch in line.chars() { if units >= target_utf16 { break; } units += ch.len_utf16() as u32; byte += ch.len_utf8() as u32; } byte } /// Find the most specific inline node whose span covers the given byte /// position. Walks block structure first, then descends into inline /// containers (bold, italic, …) until no child claims the position. pub fn find_inline_at(doc: &DocumentNode, line: u32, byte_col: u32) -> Option<&InlineNode> { for block in &doc.children { if let Some(node) = find_in_block(block, line, byte_col) { return Some(node); } } None } fn find_in_block(block: &BlockNode, line: u32, col: u32) -> Option<&InlineNode> { match block { BlockNode::Heading(h) => find_in_inlines(&h.children, line, col), BlockNode::Paragraph(p) => find_in_inlines(&p.children, line, col), BlockNode::Blockquote(b) => b.children.iter().find_map(|c| find_in_block(c, line, col)), BlockNode::List(l) => l .items .iter() .find_map(|it| find_in_list_item(it, line, col)), BlockNode::DefinitionList(dl) => dl.items.iter().find_map(|it| { it.term .as_ref() .and_then(|t| find_in_inlines(t, line, col)) .or_else(|| { it.definitions .iter() .find_map(|d| find_in_inlines(d, line, col)) }) }), BlockNode::Table(t) => t.rows.iter().find_map(|r| { r.cells .iter() .find_map(|c| find_in_inlines(&c.children, line, col)) }), _ => None, } } fn find_in_list_item(item: &ListItemNode, line: u32, col: u32) -> Option<&InlineNode> { find_in_inlines(&item.children, line, col).or_else(|| { item.sublist.as_ref().and_then(|sub| { sub.items .iter() .find_map(|i| find_in_list_item(i, line, col)) }) }) } fn find_in_inlines(inlines: &[InlineNode], line: u32, col: u32) -> Option<&InlineNode> { for n in inlines { if !span_contains(span_of_inline(n), line, col) { continue; } // Descend into inline containers so we return the *innermost* hit. let deeper = match n { InlineNode::Bold(b) => find_in_inlines(&b.children, line, col), InlineNode::Italic(i) => find_in_inlines(&i.children, line, col), InlineNode::BoldItalic(bi) => find_in_inlines(&bi.children, line, col), InlineNode::Strikethrough(s) => find_in_inlines(&s.children, line, col), InlineNode::Superscript(s) => find_in_inlines(&s.children, line, col), InlineNode::Subscript(s) => find_in_inlines(&s.children, line, col), InlineNode::Color(c) => find_in_inlines(&c.children, line, col), InlineNode::WikiLink(w) => w .description .as_ref() .and_then(|d| find_in_inlines(d, line, col)), InlineNode::ExternalLink(e) => e .description .as_ref() .and_then(|d| find_in_inlines(d, line, col)), _ => None, }; return Some(deeper.unwrap_or(n)); } None } fn span_contains(s: Span, line: u32, col: u32) -> bool { let start = (s.start.line, s.start.column); let end = (s.end.line, s.end.column); let p = (line, col); p >= start && p < end } pub fn span_of_inline(node: &InlineNode) -> Span { match node { InlineNode::Text(n) => n.span, InlineNode::Bold(n) => n.span, InlineNode::Italic(n) => n.span, InlineNode::BoldItalic(n) => n.span, InlineNode::Strikethrough(n) => n.span, InlineNode::Code(n) => n.span, InlineNode::Superscript(n) => n.span, InlineNode::Subscript(n) => n.span, InlineNode::MathInline(n) => n.span, InlineNode::Keyword(n) => n.span, InlineNode::Color(n) => n.span, InlineNode::WikiLink(n) => n.span, InlineNode::ExternalLink(n) => n.span, InlineNode::Transclusion(n) => n.span, InlineNode::RawUrl(n) => n.span, } }