use std::panic::AssertUnwindSafe;
use std::path::Path;
use lsp_types::{FoldingRange, FoldingRangeKind};
use crate::incremental::Analysis;
use crate::parser::parse;
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
use crate::text::{LineIndex, PositionEncoding};
pub fn compute_folding_ranges(text: &str) -> Vec<FoldingRange> {
let root = parse(text).cst;
folds_for_tree(&root, text)
}
pub(crate) fn folding_ranges_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
) -> Vec<FoldingRange> {
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
let root = snapshot.parsed_tree(file);
Some(folds_for_tree(&root, text))
}));
match cached {
Ok(Some(folds)) => folds,
Ok(None) | Err(_) => compute_folding_ranges(text),
}
}
fn folds_for_tree(root: &SyntaxNode, text: &str) -> Vec<FoldingRange> {
let ctx = Ctx {
text,
line_index: LineIndex::new(text),
};
let mut out = Vec::new();
for node in root.descendants() {
match node.kind() {
SyntaxKind::MODULE_DEF
| SyntaxKind::FUNCTION_DEF
| SyntaxKind::MACRO_DEF
| SyntaxKind::STRUCT_DEF
| SyntaxKind::ABSTRACT_DEF
| SyntaxKind::PRIMITIVE_DEF
| SyntaxKind::IF_EXPR
| SyntaxKind::ELSEIF_CLAUSE
| SyntaxKind::ELSE_CLAUSE
| SyntaxKind::WHILE_EXPR
| SyntaxKind::FOR_EXPR
| SyntaxKind::LET_EXPR
| SyntaxKind::BEGIN_EXPR
| SyntaxKind::QUOTE_EXPR
| SyntaxKind::TRY_EXPR
| SyntaxKind::CATCH_CLAUSE
| SyntaxKind::FINALLY_CLAUSE
| SyntaxKind::DO_EXPR => ctx.push_fold(&mut out, node.text_range(), None),
SyntaxKind::USING_STMT | SyntaxKind::IMPORT_STMT => {
ctx.push_fold(&mut out, node.text_range(), Some(FoldingRangeKind::Imports));
}
_ => {}
}
collect_import_groups(&node, &ctx, &mut out);
}
collect_comment_folds(root, &ctx, &mut out);
out
}
struct Ctx<'a> {
text: &'a str,
line_index: LineIndex<'a>,
}
impl Ctx<'_> {
fn line_of(&self, offset: usize) -> u32 {
self.line_index
.byte_to_position(offset, PositionEncoding::Utf8)
.line
}
fn end_line(&self, range: rowan::TextRange) -> u32 {
let pos = self
.line_index
.byte_to_position(range.end().into(), PositionEncoding::Utf8);
if pos.character == 0 {
pos.line.saturating_sub(1)
} else {
pos.line
}
}
fn push_fold(
&self,
out: &mut Vec<FoldingRange>,
range: rowan::TextRange,
kind: Option<FoldingRangeKind>,
) {
let start_line = self.line_of(range.start().into());
let end_line = self.end_line(range);
if end_line > start_line {
out.push(FoldingRange {
start_line,
end_line,
kind,
..Default::default()
});
}
}
fn leads_its_line(&self, token: &SyntaxToken) -> bool {
let start = usize::from(token.text_range().start());
let character = self
.line_index
.byte_to_position(start, PositionEncoding::Utf8)
.character as usize;
let line_start = start - character;
self.text[line_start..start]
.chars()
.all(char::is_whitespace)
}
}
fn collect_import_groups(node: &SyntaxNode, ctx: &Ctx<'_>, out: &mut Vec<FoldingRange>) {
let mut run: Option<(u32, u32, usize)> = None;
for child in node.children() {
if matches!(
child.kind(),
SyntaxKind::USING_STMT | SyntaxKind::IMPORT_STMT
) {
let range = child.text_range();
let start = ctx.line_of(range.start().into());
let end = ctx.end_line(range);
match &mut run {
Some((_, run_end, count)) if start == *run_end + 1 => {
*run_end = end;
*count += 1;
}
_ => {
flush_import_run(run.take(), out);
run = Some((start, end, 1));
}
}
} else {
flush_import_run(run.take(), out);
}
}
flush_import_run(run, out);
}
fn flush_import_run(run: Option<(u32, u32, usize)>, out: &mut Vec<FoldingRange>) {
if let Some((start_line, end_line, count)) = run
&& count >= 2
&& end_line > start_line
{
out.push(FoldingRange {
start_line,
end_line,
kind: Some(FoldingRangeKind::Imports),
..Default::default()
});
}
}
fn collect_comment_folds(root: &SyntaxNode, ctx: &Ctx<'_>, out: &mut Vec<FoldingRange>) {
let mut run: Option<(u32, u32)> = None;
for token in root
.descendants_with_tokens()
.filter_map(|el| el.into_token())
{
match token.kind() {
SyntaxKind::BLOCK_COMMENT => {
flush_comment_run(run.take(), out);
ctx.push_fold(out, token.text_range(), Some(FoldingRangeKind::Comment));
}
SyntaxKind::COMMENT if ctx.leads_its_line(&token) => {
let line = ctx.line_of(token.text_range().start().into());
match &mut run {
Some((_, last)) if line == *last + 1 => *last = line,
Some(_) => {
flush_comment_run(run.take(), out);
run = Some((line, line));
}
None => run = Some((line, line)),
}
}
_ => {}
}
}
flush_comment_run(run, out);
}
fn flush_comment_run(run: Option<(u32, u32)>, out: &mut Vec<FoldingRange>) {
if let Some((start_line, end_line)) = run
&& end_line > start_line
{
out.push(FoldingRange {
start_line,
end_line,
kind: Some(FoldingRangeKind::Comment),
..Default::default()
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::IncrementalDatabase;
#[test]
fn folds_via_db_match_compute_and_fall_back() {
let path = Path::new("/work/a.jl");
let buffer = "function f(x)\n x\nend\n";
let expected = compute_folding_ranges(buffer);
assert_eq!(expected.len(), 1, "fixture must yield a fold");
let mut db = IncrementalDatabase::default();
db.upsert_file(path, buffer.to_string());
assert_eq!(
folding_ranges_via_db(&db.snapshot(), path, buffer),
expected,
"cached-tree folds must match the re-parse path"
);
let mut stale = IncrementalDatabase::default();
stale.upsert_file(path, "y = 1\n".to_string());
assert_eq!(
folding_ranges_via_db(&stale.snapshot(), path, buffer),
expected,
"version skew must fall back to the buffer text"
);
let empty = IncrementalDatabase::default();
assert_eq!(
folding_ranges_via_db(&empty.snapshot(), path, buffer),
expected,
"untracked path must fall back to the buffer text"
);
}
}