use lsp_types::{FoldingRange, FoldingRangeKind};
use crate::semantic::{OutlineItem, OutlineSymbol, outline};
use crate::syntax::{SyntaxKind, SyntaxNode};
use crate::text::LineIndex;
pub(crate) fn folding_ranges(root: &SyntaxNode, idx: &LineIndex, text: &str) -> Vec<FoldingRange> {
let line_of = |offset: usize| idx.utf16_position(text, offset).0;
let mut ranges = Vec::new();
collect_section_folds(&outline(root), &line_of, &mut ranges);
for node in root
.descendants()
.filter(|n| n.kind() == SyntaxKind::ENVIRONMENT)
{
let begin = node
.children()
.find(|c| c.kind() == SyntaxKind::BEGIN)
.unwrap_or_else(|| node.clone());
emit(
&mut ranges,
line_of(begin.text_range().start().into()),
line_of(node.text_range().end().into()),
None,
);
}
let mut standalone: Vec<u32> = Vec::new();
let mut code_on_line = false;
for token in root
.descendants_with_tokens()
.filter_map(|el| el.into_token())
{
match token.kind() {
SyntaxKind::NEWLINE => code_on_line = false,
SyntaxKind::WHITESPACE => {}
SyntaxKind::COMMENT => {
if !code_on_line {
standalone.push(line_of(token.text_range().start().into()));
}
}
_ => code_on_line = true,
}
}
let mut i = 0;
while i < standalone.len() {
let mut j = i + 1;
while j < standalone.len() && standalone[j] == standalone[j - 1] + 1 {
j += 1;
}
if j - i >= 2 {
ranges.push(FoldingRange {
start_line: standalone[i],
end_line: standalone[j - 1],
kind: Some(FoldingRangeKind::Comment),
..Default::default()
});
}
i = j;
}
ranges
}
fn collect_section_folds(
items: &[OutlineItem],
line_of: &impl Fn(usize) -> u32,
ranges: &mut Vec<FoldingRange>,
) {
for item in items {
if item.kind == OutlineSymbol::Section {
let range = item.range;
if range.end() > range.start() {
emit(
ranges,
line_of(item.selection_range.start().into()),
line_of(usize::from(range.end()) - 1),
None,
);
}
}
collect_section_folds(&item.children, line_of, ranges);
}
}
fn emit(
ranges: &mut Vec<FoldingRange>,
start_line: u32,
end_line: u32,
kind: Option<FoldingRangeKind>,
) {
if end_line > start_line {
ranges.push(FoldingRange {
start_line,
end_line,
kind,
..Default::default()
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn folds(src: &str) -> Vec<FoldingRange> {
let root = SyntaxNode::new_root(parse(src).green);
let idx = LineIndex::new(src);
folding_ranges(&root, &idx, src)
}
fn triples(ranges: &[FoldingRange]) -> Vec<(u32, u32, Option<FoldingRangeKind>)> {
ranges
.iter()
.map(|r| (r.start_line, r.end_line, r.kind.clone()))
.collect()
}
#[test]
fn sibling_sections_fold_each_to_before_next() {
let src = "\\section{A}\ntext\n\\section{B}\nmore\n";
let t = triples(&folds(src));
assert!(t.contains(&(0, 1, None)), "section A folds 0..1, got {t:?}");
assert!(t.contains(&(2, 3, None)), "section B folds 2..3, got {t:?}");
}
#[test]
fn nested_subsection_folds_within_section() {
let src = "\\section{A}\n\\subsection{B}\nbody\n\\section{C}\nx\n";
let t = triples(&folds(src));
assert!(t.contains(&(0, 2, None)), "section A, got {t:?}");
assert!(t.contains(&(1, 2, None)), "subsection B, got {t:?}");
}
#[test]
fn single_line_section_does_not_fold() {
let src = "\\section{A}\n";
assert!(folds(src).is_empty());
}
#[test]
fn last_section_runs_to_eof() {
let src = "\\section{A}\nl1\nl2\n";
let t = triples(&folds(src));
assert!(t.contains(&(0, 2, None)), "got {t:?}");
}
#[test]
fn multiline_environment_folds() {
let src = "\\begin{itemize}\n\\item x\n\\end{itemize}\n";
let t = triples(&folds(src));
assert!(t.contains(&(0, 2, None)), "itemize folds 0..2, got {t:?}");
}
#[test]
fn single_line_environment_does_not_fold() {
let src = "\\begin{a}x\\end{a}\n";
assert!(folds(src).is_empty());
}
#[test]
fn comment_run_folds_as_comment() {
let src = "% a\n% b\n% c\ntext\n";
let t = triples(&folds(src));
assert_eq!(t, vec![(0, 2, Some(FoldingRangeKind::Comment))]);
}
#[test]
fn single_comment_does_not_fold() {
let src = "% lonely\ntext\n";
assert!(folds(src).is_empty());
}
#[test]
fn leading_comments_fold_separately_from_their_construct() {
let src = "% a\n% b\n\\begin{itemize}\n\\item x\n\\end{itemize}\n";
let t = triples(&folds(src));
assert!(
t.contains(&(0, 1, Some(FoldingRangeKind::Comment))),
"comment run, got {t:?}"
);
assert!(t.contains(&(2, 4, None)), "itemize from \\begin, got {t:?}");
let src = "% a\n% b\n\\section{A}\nbody\nmore\n";
let t = triples(&folds(src));
assert!(
t.contains(&(0, 1, Some(FoldingRangeKind::Comment))),
"comment run, got {t:?}"
);
assert!(t.contains(&(2, 4, None)), "section from heading, got {t:?}");
}
#[test]
fn trailing_comment_does_not_join_a_run() {
let src = "code % a\n% b\nmore\n";
assert!(folds(src).is_empty(), "got {:?}", triples(&folds(src)));
}
}