150 lines
5.5 KiB
Rust
150 lines
5.5 KiB
Rust
|
|
//! 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,
|
||
|
|
}
|
||
|
|
}
|