Skip to main content

php_lsp/editing/
folding.rs

1use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
2use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
3
4use crate::ast::{ParsedDoc, SourceView};
5
6pub fn folding_ranges(_source: &str, doc: &ParsedDoc) -> Vec<FoldingRange> {
7    let sv = doc.view();
8    let mut ranges = Vec::new();
9    fold_stmts(&doc.program().stmts, sv, &mut ranges);
10    fold_use_groups(&doc.program().stmts, sv, &mut ranges);
11    fold_comments(sv, &mut ranges);
12    fold_regions(sv.source(), &mut ranges);
13    ranges
14}
15
16fn fold_stmts(stmts: &[Stmt<'_, '_>], sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
17    for stmt in stmts {
18        fold_stmt(stmt, sv, out);
19    }
20}
21
22/// Fold the contents of a block body without emitting a fold for the block itself.
23/// Used for control-flow statements (`if`, `while`, `for`, `foreach`, `do-while`)
24/// where the outer statement already covers the same span as the inner `Block`.
25fn fold_body(body: &Stmt<'_, '_>, sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
26    if let StmtKind::Block(stmts) = &body.kind {
27        fold_stmts(&stmts.stmts, sv, out);
28    }
29}
30
31fn fold_stmt(stmt: &Stmt<'_, '_>, sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
32    match &stmt.kind {
33        StmtKind::Function(f) => {
34            let start_line = sv.line_of(stmt.span.start);
35            let end_line = sv.line_of(stmt.span.end);
36            push(out, start_line, end_line, None);
37            fold_stmts(&f.body.stmts, sv, out);
38        }
39        StmtKind::Class(c) => {
40            let start_line = sv.line_of(stmt.span.start);
41            let end_line = sv.line_of(stmt.span.end);
42            push(out, start_line, end_line, None);
43            for member in c.body.members.iter() {
44                if let ClassMemberKind::Method(m) = &member.kind {
45                    let m_start = sv.line_of(member.span.start);
46                    // member.span.end is exclusive and includes the trailing newline;
47                    // subtract 1 so the end line is the line containing the closing `}`.
48                    let m_end = sv.line_of(member.span.end.saturating_sub(1));
49                    push(out, m_start, m_end, None);
50                    if let Some(body) = &m.body {
51                        fold_stmts(&body.stmts, sv, out);
52                    }
53                }
54            }
55        }
56        StmtKind::Interface(i) => {
57            let start_line = sv.line_of(stmt.span.start);
58            let end_line = sv.line_of(stmt.span.end);
59            push(out, start_line, end_line, None);
60            // Interface methods are abstract (no body) — nothing to fold per method.
61            for member in i.body.members.iter() {
62                if let ClassMemberKind::Method(m) = &member.kind
63                    && let Some(body) = &m.body
64                {
65                    let m_start = sv.line_of(member.span.start);
66                    let m_end = sv.line_of(member.span.end.saturating_sub(1));
67                    push(out, m_start, m_end, None);
68                    fold_stmts(&body.stmts, sv, out);
69                }
70            }
71        }
72        StmtKind::Trait(t) => {
73            let start_line = sv.line_of(stmt.span.start);
74            let end_line = sv.line_of(stmt.span.end);
75            push(out, start_line, end_line, None);
76            for member in t.body.members.iter() {
77                if let ClassMemberKind::Method(m) = &member.kind {
78                    let m_start = sv.line_of(member.span.start);
79                    let m_end = sv.line_of(member.span.end.saturating_sub(1));
80                    push(out, m_start, m_end, None);
81                    if let Some(body) = &m.body {
82                        fold_stmts(&body.stmts, sv, out);
83                    }
84                }
85            }
86        }
87        StmtKind::Enum(e) => {
88            let start_line = sv.line_of(stmt.span.start);
89            let end_line = sv.line_of(stmt.span.end);
90            push(out, start_line, end_line, None);
91            for member in e.body.members.iter() {
92                if let EnumMemberKind::Method(m) = &member.kind {
93                    let m_start = sv.line_of(member.span.start);
94                    let m_end = sv.line_of(member.span.end.saturating_sub(1));
95                    push(out, m_start, m_end, None);
96                    if let Some(body) = &m.body {
97                        fold_stmts(&body.stmts, sv, out);
98                    }
99                }
100            }
101        }
102        StmtKind::If(i) => {
103            let start_line = sv.line_of(stmt.span.start);
104            let end_line = sv.line_of(stmt.span.end);
105            push(out, start_line, end_line, None);
106            fold_body(i.then_branch, sv, out);
107            for ei in i.elseif_branches.iter() {
108                fold_body(&ei.body, sv, out);
109            }
110            if let Some(e) = &i.else_branch {
111                fold_body(e, sv, out);
112            }
113        }
114        StmtKind::While(w) => {
115            let start_line = sv.line_of(stmt.span.start);
116            let end_line = sv.line_of(stmt.span.end);
117            push(out, start_line, end_line, None);
118            fold_body(w.body, sv, out);
119        }
120        StmtKind::For(f) => {
121            let start_line = sv.line_of(stmt.span.start);
122            let end_line = sv.line_of(stmt.span.end);
123            push(out, start_line, end_line, None);
124            fold_body(f.body, sv, out);
125        }
126        StmtKind::Foreach(f) => {
127            let start_line = sv.line_of(stmt.span.start);
128            let end_line = sv.line_of(stmt.span.end);
129            push(out, start_line, end_line, None);
130            fold_body(f.body, sv, out);
131        }
132        StmtKind::DoWhile(d) => {
133            let start_line = sv.line_of(stmt.span.start);
134            let end_line = sv.line_of(stmt.span.end);
135            push(out, start_line, end_line, None);
136            fold_body(d.body, sv, out);
137        }
138        StmtKind::TryCatch(t) => {
139            let start_line = sv.line_of(stmt.span.start);
140            let end_line = sv.line_of(stmt.span.end);
141            push(out, start_line, end_line, None);
142            fold_stmts(&t.body.stmts, sv, out);
143            for catch in t.catches.iter() {
144                fold_stmts(&catch.body.stmts, sv, out);
145            }
146            if let Some(finally) = &t.finally {
147                fold_stmts(&finally.stmts, sv, out);
148            }
149        }
150        StmtKind::Block(stmts) => {
151            let start_line = sv.line_of(stmt.span.start);
152            let end_line = sv.line_of(stmt.span.end);
153            push(out, start_line, end_line, None);
154            fold_stmts(&stmts.stmts, sv, out);
155        }
156        StmtKind::Namespace(ns) => {
157            let start_line = sv.line_of(stmt.span.start);
158            let end_line = sv.line_of(stmt.span.end);
159            push(out, start_line, end_line, None);
160            if let NamespaceBody::Braced(inner) = &ns.body {
161                fold_stmts(&inner.stmts, sv, out);
162            }
163        }
164        _ => {}
165    }
166}
167
168/// Fold consecutive top-level `use` statements into a single range.
169fn fold_use_groups(stmts: &[Stmt<'_, '_>], sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
170    let mut group_start: Option<u32> = None;
171    let mut group_end: u32 = 0;
172    for stmt in stmts {
173        if matches!(stmt.kind, StmtKind::Use(_)) {
174            let line = sv.line_of(stmt.span.start);
175            if group_start.is_none() {
176                group_start = Some(line);
177            }
178            group_end = sv.line_of(stmt.span.end);
179        } else {
180            if let Some(start) = group_start.take() {
181                push(out, start, group_end, Some(FoldingRangeKind::Imports));
182            }
183        }
184    }
185    if let Some(start) = group_start {
186        push(out, start, group_end, Some(FoldingRangeKind::Imports));
187    }
188}
189
190/// Fold `/* ... */` and `/** ... */` multi-line block comments.
191fn fold_comments(sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
192    let bytes = sv.source().as_bytes();
193    let len = bytes.len();
194    let mut i = 0;
195    while i + 1 < len {
196        if bytes[i] == b'/' && bytes[i + 1] == b'*' {
197            let start_line = line_at(sv, i);
198            // find closing */
199            let mut j = i + 2;
200            while j + 1 < len {
201                if bytes[j] == b'*' && bytes[j + 1] == b'/' {
202                    let end_line = line_at(sv, j + 1);
203                    push(out, start_line, end_line, Some(FoldingRangeKind::Comment));
204                    i = j + 2;
205                    break;
206                }
207                j += 1;
208            }
209            if j + 1 >= len {
210                break;
211            }
212        } else {
213            i += 1;
214        }
215    }
216}
217
218/// Fold `// #region` … `// #endregion` pairs.
219fn fold_regions(source: &str, out: &mut Vec<FoldingRange>) {
220    let mut stack: Vec<u32> = Vec::new();
221    for (line_no, line) in source.lines().enumerate() {
222        let trimmed = line.trim();
223        if trimmed.starts_with("// #region") || trimmed.starts_with("//region") {
224            stack.push(line_no as u32);
225        } else if (trimmed.starts_with("// #endregion") || trimmed.starts_with("//endregion"))
226            && let Some(start) = stack.pop()
227        {
228            push(out, start, line_no as u32, Some(FoldingRangeKind::Region));
229        }
230    }
231}
232
233fn line_at(sv: SourceView<'_>, byte_offset: usize) -> u32 {
234    sv.line_of(byte_offset as u32)
235}
236
237fn push(
238    out: &mut Vec<FoldingRange>,
239    start_line: u32,
240    end_line: u32,
241    kind: Option<FoldingRangeKind>,
242) {
243    if end_line > start_line {
244        out.push(FoldingRange {
245            start_line,
246            start_character: None,
247            end_line,
248            end_character: None,
249            kind,
250            collapsed_text: None,
251        });
252    }
253}