Skip to main content

php_lsp/editing/
folding.rs

1use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
2use tower_lsp_server::ls_types::{FoldingRange, FoldingRangeKind};
3
4use crate::document::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::Switch(s) => {
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            for case in s.body.cases.iter() {
155                fold_stmts(&case.body, sv, out);
156            }
157        }
158        StmtKind::Block(stmts) => {
159            let start_line = sv.line_of(stmt.span.start);
160            let end_line = sv.line_of(stmt.span.end);
161            push(out, start_line, end_line, None);
162            fold_stmts(&stmts.stmts, sv, out);
163        }
164        StmtKind::Namespace(ns) => {
165            let start_line = sv.line_of(stmt.span.start);
166            let end_line = sv.line_of(stmt.span.end);
167            push(out, start_line, end_line, None);
168            if let NamespaceBody::Braced(inner) = &ns.body {
169                fold_stmts(&inner.stmts, sv, out);
170            }
171        }
172        _ => {}
173    }
174}
175
176/// Fold consecutive top-level `use` statements into a single range.
177fn fold_use_groups(stmts: &[Stmt<'_, '_>], sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
178    let mut group_start: Option<u32> = None;
179    let mut group_end: u32 = 0;
180    for stmt in stmts {
181        if matches!(stmt.kind, StmtKind::Use(_)) {
182            let line = sv.line_of(stmt.span.start);
183            if group_start.is_none() {
184                group_start = Some(line);
185            }
186            group_end = sv.line_of(stmt.span.end);
187        } else {
188            if let Some(start) = group_start.take() {
189                push(out, start, group_end, Some(FoldingRangeKind::Imports));
190            }
191        }
192    }
193    if let Some(start) = group_start {
194        push(out, start, group_end, Some(FoldingRangeKind::Imports));
195    }
196}
197
198/// Fold `/* ... */` and `/** ... */` multi-line block comments.
199fn fold_comments(sv: SourceView<'_>, out: &mut Vec<FoldingRange>) {
200    let bytes = sv.source().as_bytes();
201    let len = bytes.len();
202    let mut i = 0;
203    while i + 1 < len {
204        if bytes[i] == b'/' && bytes[i + 1] == b'*' {
205            let start_line = line_at(sv, i);
206            // find closing */
207            let mut j = i + 2;
208            while j + 1 < len {
209                if bytes[j] == b'*' && bytes[j + 1] == b'/' {
210                    let end_line = line_at(sv, j + 1);
211                    push(out, start_line, end_line, Some(FoldingRangeKind::Comment));
212                    i = j + 2;
213                    break;
214                }
215                j += 1;
216            }
217            if j + 1 >= len {
218                break;
219            }
220        } else {
221            i += 1;
222        }
223    }
224}
225
226/// Fold `// #region` … `// #endregion` pairs.
227fn fold_regions(source: &str, out: &mut Vec<FoldingRange>) {
228    let mut stack: Vec<u32> = Vec::new();
229    for (line_no, line) in source.lines().enumerate() {
230        let trimmed = line.trim();
231        if trimmed.starts_with("// #region") || trimmed.starts_with("//region") {
232            stack.push(line_no as u32);
233        } else if (trimmed.starts_with("// #endregion") || trimmed.starts_with("//endregion"))
234            && let Some(start) = stack.pop()
235        {
236            push(out, start, line_no as u32, Some(FoldingRangeKind::Region));
237        }
238    }
239}
240
241fn line_at(sv: SourceView<'_>, byte_offset: usize) -> u32 {
242    sv.line_of(byte_offset as u32)
243}
244
245fn push(
246    out: &mut Vec<FoldingRange>,
247    start_line: u32,
248    end_line: u32,
249    kind: Option<FoldingRangeKind>,
250) {
251    if end_line > start_line {
252        out.push(FoldingRange {
253            start_line,
254            start_character: None,
255            end_line,
256            end_character: None,
257            kind,
258            collapsed_text: None,
259        });
260    }
261}