Skip to main content

lex_analysis/
folding_ranges.rs

1use lex_core::lex::ast::{
2    Annotation, AstNode, ContentItem, Definition, Document, List, ListItem, Range, Session, Table,
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::Table(table) => self.process_table(table),
62            ContentItem::TextLine(_)
63            | ContentItem::VerbatimLine(_)
64            | ContentItem::BlankLineGroup(_) => {}
65        }
66    }
67
68    fn process_list(&mut self, list: &List) {
69        if list.range().start.line < list.range().end.line {
70            self.push_fold(
71                Some(list.range()),
72                list.range(),
73                Some(FoldingRangeKind::Region),
74            );
75        }
76        self.process_annotations(list.annotations());
77        for item in list.items.iter() {
78            if let ContentItem::ListItem(list_item) = item {
79                self.process_list_item(list_item);
80            }
81        }
82    }
83
84    fn process_list_item(&mut self, list_item: &ListItem) {
85        self.process_annotations(list_item.annotations());
86        if let Some(children_range) =
87            Range::bounding_box(list_item.children.iter().map(|child| child.range()))
88        {
89            if list_item.range().start.line < children_range.end.line {
90                self.push_fold(
91                    Some(list_item.range()),
92                    &children_range,
93                    Some(FoldingRangeKind::Region),
94                );
95            }
96        }
97        for child in list_item.children.iter() {
98            self.process_content_item(child);
99        }
100    }
101
102    fn process_definition(&mut self, definition: &Definition) {
103        if definition.range().start.line < definition.range().end.line {
104            let header = definition.header_location();
105            self.push_fold(header, definition.range(), Some(FoldingRangeKind::Region));
106        }
107        self.process_annotations(definition.annotations());
108        for child in definition.children.iter() {
109            self.process_content_item(child);
110        }
111    }
112
113    fn process_annotation(&mut self, annotation: &Annotation) {
114        if annotation.body_location().is_some() {
115            self.push_fold(
116                Some(annotation.header_location()),
117                annotation.range(),
118                Some(FoldingRangeKind::Comment),
119            );
120        }
121        for child in annotation.children.iter() {
122            self.process_content_item(child);
123        }
124    }
125
126    fn process_verbatim(&mut self, verbatim: &Verbatim) {
127        self.process_annotations(verbatim.annotations());
128        if let Some(subject_range) = &verbatim.subject.location {
129            if subject_range.start.line < verbatim.range().end.line {
130                self.push_fold(
131                    Some(subject_range),
132                    verbatim.range(),
133                    Some(FoldingRangeKind::Region),
134                );
135            }
136        }
137    }
138
139    fn process_table(&mut self, table: &Table) {
140        self.process_annotations(table.annotations());
141        // Process cell children with block content
142        for row in table.all_rows() {
143            for cell in &row.cells {
144                for child in cell.children.iter() {
145                    self.process_content_item(child);
146                }
147            }
148        }
149        if let Some(subject_range) = &table.subject.location {
150            if subject_range.start.line < table.range().end.line {
151                self.push_fold(
152                    Some(subject_range),
153                    table.range(),
154                    Some(FoldingRangeKind::Region),
155                );
156            }
157        }
158    }
159
160    fn process_annotations(&mut self, annotations: &[Annotation]) {
161        for annotation in annotations {
162            self.process_annotation(annotation);
163        }
164    }
165
166    fn push_fold(
167        &mut self,
168        start_range: Option<&Range>,
169        end_range: &Range,
170        kind: Option<FoldingRangeKind>,
171    ) {
172        let start = start_range.unwrap_or(end_range);
173        if start.start.line >= end_range.end.line {
174            return;
175        }
176        self.ranges.push(LexFoldingRange {
177            start_line: start.start.line as u32,
178            start_character: Some(start.start.column as u32),
179            end_line: end_range.end.line as u32,
180            end_character: Some(end_range.end.column as u32),
181            kind,
182        });
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::test_support::sample_document;
190
191    #[test]
192    fn creates_ranges_for_sessions_and_definitions() {
193        let document = sample_document();
194        let ranges = folding_ranges(&document);
195        assert!(ranges
196            .iter()
197            .any(|range| range.kind == Some(FoldingRangeKind::Region) && range.start_line == 2));
198        assert!(ranges
199            .iter()
200            .any(|range| range.kind == Some(FoldingRangeKind::Region) && range.start_line > 2));
201        // No comment folding ranges because :: callout :: is consumed by Verbatim
202        // assert!(ranges
203        //     .iter()
204        //     .any(|range| range.kind == Some(FoldingRangeKind::Comment)));
205    }
206}