lex_analysis/
folding_ranges.rs

1use lex_core::lex::ast::{
2    Annotation, AstNode, ContentItem, Definition, Document, List, ListItem, Range, Session,
3    Verbatim,
4};
5use lsp_types::FoldingRangeKind;
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct LexFoldingRange {
9    pub start_line: u32,
10    pub start_character: Option<u32>,
11    pub end_line: u32,
12    pub end_character: Option<u32>,
13    pub kind: Option<FoldingRangeKind>,
14}
15
16pub fn folding_ranges(document: &Document) -> Vec<LexFoldingRange> {
17    let mut collector = FoldingCollector { ranges: Vec::new() };
18    collector.process_document(document);
19    collector.ranges
20}
21
22struct FoldingCollector {
23    ranges: Vec<LexFoldingRange>,
24}
25
26impl FoldingCollector {
27    fn process_document(&mut self, document: &Document) {
28        self.process_annotations(document.annotations());
29        self.process_session(&document.root, true);
30    }
31
32    fn process_session(&mut self, session: &Session, is_root: bool) {
33        if !is_root {
34            if let Some(body) = session.body_location() {
35                if session.range().start.line < body.end.line {
36                    self.push_fold(
37                        session.header_location(),
38                        session.range(),
39                        Some(FoldingRangeKind::Region),
40                    );
41                }
42            }
43        }
44        self.process_annotations(session.annotations());
45        for child in session.children.iter() {
46            self.process_content_item(child);
47        }
48    }
49
50    fn process_content_item(&mut self, item: &ContentItem) {
51        match item {
52            ContentItem::Paragraph(paragraph) => {
53                self.process_annotations(paragraph.annotations());
54            }
55            ContentItem::Session(session) => self.process_session(session, false),
56            ContentItem::List(list) => self.process_list(list),
57            ContentItem::ListItem(list_item) => self.process_list_item(list_item),
58            ContentItem::Definition(definition) => self.process_definition(definition),
59            ContentItem::Annotation(annotation) => self.process_annotation(annotation),
60            ContentItem::VerbatimBlock(verbatim) => self.process_verbatim(verbatim),
61            ContentItem::TextLine(_)
62            | ContentItem::VerbatimLine(_)
63            | ContentItem::BlankLineGroup(_) => {}
64        }
65    }
66
67    fn process_list(&mut self, list: &List) {
68        if list.range().start.line < list.range().end.line {
69            self.push_fold(
70                Some(list.range()),
71                list.range(),
72                Some(FoldingRangeKind::Region),
73            );
74        }
75        self.process_annotations(list.annotations());
76        for item in list.items.iter() {
77            if let ContentItem::ListItem(list_item) = item {
78                self.process_list_item(list_item);
79            }
80        }
81    }
82
83    fn process_list_item(&mut self, list_item: &ListItem) {
84        self.process_annotations(list_item.annotations());
85        if let Some(children_range) =
86            Range::bounding_box(list_item.children.iter().map(|child| child.range()))
87        {
88            if list_item.range().start.line < children_range.end.line {
89                self.push_fold(
90                    Some(list_item.range()),
91                    &children_range,
92                    Some(FoldingRangeKind::Region),
93                );
94            }
95        }
96        for child in list_item.children.iter() {
97            self.process_content_item(child);
98        }
99    }
100
101    fn process_definition(&mut self, definition: &Definition) {
102        if definition.range().start.line < definition.range().end.line {
103            let header = definition.header_location();
104            self.push_fold(header, definition.range(), Some(FoldingRangeKind::Region));
105        }
106        self.process_annotations(definition.annotations());
107        for child in definition.children.iter() {
108            self.process_content_item(child);
109        }
110    }
111
112    fn process_annotation(&mut self, annotation: &Annotation) {
113        if annotation.body_location().is_some() {
114            self.push_fold(
115                Some(annotation.header_location()),
116                annotation.range(),
117                Some(FoldingRangeKind::Comment),
118            );
119        }
120        for child in annotation.children.iter() {
121            self.process_content_item(child);
122        }
123    }
124
125    fn process_verbatim(&mut self, verbatim: &Verbatim) {
126        self.process_annotations(verbatim.annotations());
127        if let Some(subject_range) = &verbatim.subject.location {
128            if subject_range.start.line < verbatim.range().end.line {
129                self.push_fold(
130                    Some(subject_range),
131                    verbatim.range(),
132                    Some(FoldingRangeKind::Region),
133                );
134            }
135        }
136    }
137
138    fn process_annotations(&mut self, annotations: &[Annotation]) {
139        for annotation in annotations {
140            self.process_annotation(annotation);
141        }
142    }
143
144    fn push_fold(
145        &mut self,
146        start_range: Option<&Range>,
147        end_range: &Range,
148        kind: Option<FoldingRangeKind>,
149    ) {
150        let start = start_range.unwrap_or(end_range);
151        if start.start.line >= end_range.end.line {
152            return;
153        }
154        self.ranges.push(LexFoldingRange {
155            start_line: start.start.line as u32,
156            start_character: Some(start.start.column as u32),
157            end_line: end_range.end.line as u32,
158            end_character: Some(end_range.end.column as u32),
159            kind,
160        });
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::test_support::sample_document;
168
169    #[test]
170    fn creates_ranges_for_sessions_and_definitions() {
171        let document = sample_document();
172        let ranges = folding_ranges(&document);
173        assert!(ranges
174            .iter()
175            .any(|range| range.kind == Some(FoldingRangeKind::Region) && range.start_line == 2));
176        assert!(ranges
177            .iter()
178            .any(|range| range.kind == Some(FoldingRangeKind::Region) && range.start_line > 2));
179        // No comment folding ranges because :: callout :: is consumed by Verbatim
180        // assert!(ranges
181        //     .iter()
182        //     .any(|range| range.kind == Some(FoldingRangeKind::Comment)));
183    }
184}