Files
nuwiki/crates/nuwiki-lsp/src/nav.rs
T

150 lines
5.5 KiB
Rust
Raw Normal View History

//! 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,
}
}