89 lines
3.3 KiB
Rust
89 lines
3.3 KiB
Rust
|
|
//! `textDocument/foldingRange` provider.
|
||
|
|
//!
|
||
|
|
//! Strategy: one fold per heading (line → line before the next heading
|
||
|
|
//! of same-or-higher level) plus one fold per top-level list block.
|
||
|
|
//! Folding is deliberately conservative — we never produce nested folds
|
||
|
|
//! beyond what the AST already groups, since vimwiki users mostly want
|
||
|
|
//! "collapse this section" behaviour rather than fine-grained code-fold
|
||
|
|
//! semantics.
|
||
|
|
|
||
|
|
use nuwiki_core::ast::{BlockNode, DocumentNode};
|
||
|
|
use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
|
||
|
|
|
||
|
|
/// Compute folding ranges for the whole document. `total_lines` is the
|
||
|
|
/// number of lines in the source (`text.lines().count() as u32`) — used
|
||
|
|
/// to extend the last heading's fold to end-of-document.
|
||
|
|
pub fn folding_ranges(ast: &DocumentNode, total_lines: u32) -> Vec<FoldingRange> {
|
||
|
|
let mut out = Vec::new();
|
||
|
|
let last_line = total_lines.saturating_sub(1);
|
||
|
|
|
||
|
|
// Collect heading spans + levels in source order. We walk the
|
||
|
|
// top-level children only — nested headings inside blockquotes are
|
||
|
|
// rare and folding them adds complexity for little gain.
|
||
|
|
let mut headings: Vec<(u32, u8)> = Vec::new();
|
||
|
|
for block in &ast.children {
|
||
|
|
if let BlockNode::Heading(h) = block {
|
||
|
|
headings.push((h.span.start.line, h.level));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// For each heading, end-line is one before the next heading whose
|
||
|
|
// level is <= current level (or last_line otherwise).
|
||
|
|
for (i, &(line, level)) in headings.iter().enumerate() {
|
||
|
|
let end = headings[i + 1..]
|
||
|
|
.iter()
|
||
|
|
.find(|(_, l)| *l <= level)
|
||
|
|
.map(|(next_line, _)| next_line.saturating_sub(1))
|
||
|
|
.unwrap_or(last_line);
|
||
|
|
if end > line {
|
||
|
|
out.push(FoldingRange {
|
||
|
|
start_line: line,
|
||
|
|
end_line: end,
|
||
|
|
start_character: None,
|
||
|
|
end_character: None,
|
||
|
|
kind: Some(FoldingRangeKind::Region),
|
||
|
|
collapsed_text: None,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fold each top-level list. Sublists fold implicitly because their
|
||
|
|
// parent item's source span already covers them.
|
||
|
|
for block in &ast.children {
|
||
|
|
if let BlockNode::List(l) = block {
|
||
|
|
let start = l.span.start.line;
|
||
|
|
let end = l.span.end.line;
|
||
|
|
if end > start {
|
||
|
|
out.push(FoldingRange {
|
||
|
|
start_line: start,
|
||
|
|
end_line: end,
|
||
|
|
start_character: None,
|
||
|
|
end_character: None,
|
||
|
|
kind: Some(FoldingRangeKind::Region),
|
||
|
|
collapsed_text: None,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Count lines in `text` (always ≥ 1 for non-empty input).
|
||
|
|
pub fn line_count(text: &str) -> u32 {
|
||
|
|
if text.is_empty() {
|
||
|
|
0
|
||
|
|
} else {
|
||
|
|
let mut n = text.bytes().filter(|b| *b == b'\n').count() as u32;
|
||
|
|
if !text.ends_with('\n') {
|
||
|
|
n += 1;
|
||
|
|
} else {
|
||
|
|
// trailing newline still bounds a virtual line; LSP positions
|
||
|
|
// index 0-based, so the file ends at line `n` (the empty one
|
||
|
|
// after the final newline). We treat "lines" as the count
|
||
|
|
// *including* that trailing empty line for fold-end clamping.
|
||
|
|
n += 1;
|
||
|
|
}
|
||
|
|
n
|
||
|
|
}
|
||
|
|
}
|