1use std::collections::HashSet;
12
13use bynk_syntax::ast::*;
14use bynk_syntax::lexer::{TokenKind, tokenize};
15use bynk_syntax::parser::parse_unit_with_recovery;
16use bynk_syntax::span::Span;
17use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind, Position, Range, SelectionRange};
18
19use crate::position::{offset_to_position, position_to_offset, span_to_range};
20
21fn collect(source: &str) -> Vec<(Span, bool)> {
26 let Ok(tokens) = tokenize(source) else {
27 return Vec::new();
28 };
29 let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, source) else {
30 return Vec::new();
31 };
32 let mut out = Vec::new();
33 walk_unit(&unit, &mut out);
34 out
35}
36
37fn walk_unit(unit: &SourceUnit, out: &mut Vec<(Span, bool)>) {
38 match unit {
39 SourceUnit::Commons(c) => {
40 out.push((c.span, true));
41 c.items.iter().for_each(|i| walk_item(i, out));
42 }
43 SourceUnit::Context(c) => {
44 out.push((c.span, true));
45 c.items.iter().for_each(|i| walk_item(i, out));
46 }
47 SourceUnit::Adapter(a) => {
48 out.push((a.span, true));
49 a.items.iter().for_each(|i| walk_item(i, out));
50 }
51 SourceUnit::Suite(t) => {
52 out.push((t.span, true));
53 for case in &t.cases {
54 out.push((case.span, true));
55 walk_block(&case.body, out);
56 }
57 }
58 SourceUnit::Integration(i) => {
59 out.push((i.span, true));
60 for case in &i.cases {
61 out.push((case.span, true));
62 }
63 }
64 }
65}
66
67fn walk_item(item: &CommonsItem, out: &mut Vec<(Span, bool)>) {
68 match item {
69 CommonsItem::Type(t) => {
70 out.push((t.span, true));
71 match &t.body {
72 TypeBody::Record(r) => out.push((r.span, true)),
73 TypeBody::Sum(s) => out.push((s.span, true)),
74 TypeBody::Opaque { .. } | TypeBody::Refined { .. } => {}
75 }
76 }
77 CommonsItem::Fn(f) => {
78 out.push((f.span, true));
79 walk_block(&f.body, out);
80 }
81 CommonsItem::Capability(c) => out.push((c.span, true)),
82 CommonsItem::Provider(p) => {
83 out.push((p.span, true));
84 for op in &p.ops {
85 out.push((op.span, true));
86 walk_block(&op.body, out);
87 }
88 }
89 CommonsItem::Service(s) => {
90 out.push((s.span, true));
91 for h in &s.handlers {
92 out.push((h.span, true));
93 walk_block(&h.body, out);
94 }
95 }
96 CommonsItem::Agent(a) => {
97 out.push((a.span, true));
98 for h in &a.handlers {
99 out.push((h.span, true));
100 walk_block(&h.body, out);
101 }
102 }
103 CommonsItem::Actor(a) => {
104 out.push((a.span, true));
105 }
106 }
107}
108
109fn walk_block(b: &Block, out: &mut Vec<(Span, bool)>) {
110 out.push((b.span, true));
111 for s in &b.statements {
112 out.push((s.span(), false));
113 match s {
114 Statement::Let(l) | Statement::EffectLet(l) => walk_expr(&l.value, out),
115 Statement::Expect(a) => walk_expr(&a.value, out),
116 Statement::Send(s) => walk_expr(&s.value, out),
117 Statement::Assign(a) => walk_expr(&a.value, out),
118 }
119 }
120 walk_expr(&b.tail, out);
121}
122
123fn walk_expr(e: &Expr, out: &mut Vec<(Span, bool)>) {
124 let foldable = matches!(
125 e.kind,
126 ExprKind::Block(_)
127 | ExprKind::If { .. }
128 | ExprKind::Match { .. }
129 | ExprKind::RecordConstruction { .. }
130 | ExprKind::RecordSpread { .. }
131 | ExprKind::ListLit(_)
132 | ExprKind::Lambda(_)
133 );
134 out.push((e.span, foldable));
135 match &e.kind {
136 ExprKind::Block(b) => {
137 for s in &b.statements {
138 out.push((s.span(), false));
139 match s {
140 Statement::Let(l) | Statement::EffectLet(l) => walk_expr(&l.value, out),
141 Statement::Expect(a) => walk_expr(&a.value, out),
142 Statement::Send(s) => walk_expr(&s.value, out),
143 Statement::Assign(a) => walk_expr(&a.value, out),
144 }
145 }
146 walk_expr(&b.tail, out);
147 }
148 ExprKind::If {
149 cond,
150 then_block,
151 else_block,
152 } => {
153 walk_expr(cond, out);
154 walk_block(then_block, out);
155 walk_block(else_block, out);
156 }
157 ExprKind::Match { discriminant, arms } => {
158 walk_expr(discriminant, out);
159 for arm in arms {
160 out.push((arm.span, true));
161 match &arm.body {
162 MatchBody::Expr(ex) => walk_expr(ex, out),
163 MatchBody::Block(bl) => walk_block(bl, out),
164 }
165 }
166 }
167 ExprKind::RecordConstruction { fields, .. } => {
168 for f in fields {
169 if let Some(v) = &f.value {
170 walk_expr(v, out);
171 }
172 }
173 }
174 ExprKind::RecordSpread {
175 base, overrides, ..
176 } => {
177 walk_expr(base, out);
178 for f in overrides {
179 if let Some(v) = &f.value {
180 walk_expr(v, out);
181 }
182 }
183 }
184 ExprKind::ListLit(elems) => elems.iter().for_each(|el| walk_expr(el, out)),
185 ExprKind::Lambda(l) => walk_expr(&l.body, out),
186 ExprKind::BinOp(_, a, b) => {
187 walk_expr(a, out);
188 walk_expr(b, out);
189 }
190 ExprKind::UnaryOp(_, x)
191 | ExprKind::Paren(x)
192 | ExprKind::Ok(x)
193 | ExprKind::Err(x)
194 | ExprKind::Question(x)
195 | ExprKind::Some(x)
196 | ExprKind::EffectPure(x)
197 | ExprKind::Expect(x) => walk_expr(x, out),
198 ExprKind::Call { args, .. } | ExprKind::ConstructorCall { args, .. } => {
199 args.iter().for_each(|a| walk_expr(a, out))
200 }
201 ExprKind::MethodCall { receiver, args, .. } => {
202 walk_expr(receiver, out);
203 args.iter().for_each(|a| walk_expr(a, out));
204 }
205 ExprKind::FieldAccess { receiver, .. } => walk_expr(receiver, out),
206 ExprKind::Is { value, .. } => walk_expr(value, out),
207 ExprKind::Val { args, .. } => args.iter().for_each(|a| walk_expr(a, out)),
208 ExprKind::InterpStr(parts) => parts.iter().for_each(|part| {
210 if let InterpPart::Hole(hole) = part {
211 walk_expr(hole, out);
212 }
213 }),
214 ExprKind::IntLit(_)
216 | ExprKind::FloatLit { .. }
217 | ExprKind::DurationLit { .. }
218 | ExprKind::StrLit(_)
219 | ExprKind::BoolLit(_)
220 | ExprKind::Ident(_)
221 | ExprKind::None
222 | ExprKind::UnitLit => {}
223 }
224}
225
226pub fn folding_ranges(source: &str) -> Vec<FoldingRange> {
231 let mut out = Vec::new();
232 let mut seen: HashSet<(u32, u32)> = HashSet::new();
233 for (span, foldable) in collect(source) {
234 if !foldable {
235 continue;
236 }
237 let start = offset_to_position(source, span.start).line;
238 let end = offset_to_position(source, span.end).line;
239 if end > start && seen.insert((start, end)) {
240 out.push(fold(start, end, None));
241 }
242 }
243 out.extend(comment_folds(source));
244 out
245}
246
247fn comment_folds(source: &str) -> Vec<FoldingRange> {
251 let Ok(tokens) = tokenize(source) else {
252 return Vec::new();
253 };
254 let comments: Vec<Span> = tokens
255 .iter()
256 .filter(|t| t.kind == TokenKind::Comment)
257 .map(|t| t.span)
258 .collect();
259 let mut out = Vec::new();
260 let mut i = 0;
261 while i < comments.len() {
262 let start = offset_to_position(source, comments[i].start).line;
263 let mut end = offset_to_position(source, comments[i].end).line;
264 let mut j = i;
265 while j + 1 < comments.len() {
266 let next = offset_to_position(source, comments[j + 1].start).line;
267 if next == end + 1 {
268 j += 1;
269 end = offset_to_position(source, comments[j].end).line;
270 } else {
271 break;
272 }
273 }
274 if end > start {
275 out.push(fold(start, end, Some(FoldingRangeKind::Comment)));
276 }
277 i = j + 1;
278 }
279 out
280}
281
282fn fold(start_line: u32, end_line: u32, kind: Option<FoldingRangeKind>) -> FoldingRange {
283 FoldingRange {
284 start_line,
285 start_character: None,
286 end_line,
287 end_character: None,
288 kind,
289 collapsed_text: None,
290 }
291}
292
293pub fn selection_ranges(source: &str, positions: &[Position]) -> Vec<SelectionRange> {
298 let nodes = collect(source);
299 positions
300 .iter()
301 .map(|pos| selection_at(source, &nodes, *pos))
302 .collect()
303}
304
305fn selection_at(source: &str, nodes: &[(Span, bool)], pos: Position) -> SelectionRange {
306 let empty = SelectionRange {
307 range: Range::new(pos, pos),
308 parent: None,
309 };
310 let Some(offset) = position_to_offset(source, pos) else {
311 return empty;
312 };
313 let mut spans: Vec<Span> = nodes
315 .iter()
316 .map(|(s, _)| *s)
317 .filter(|s| s.start <= offset && offset <= s.end)
318 .collect();
319 spans.sort_by_key(|s| (s.start, s.end));
320 spans.dedup();
321 spans.sort_by_key(|s| s.end - s.start);
322 let mut chain: Option<Box<SelectionRange>> = None;
324 for span in spans.into_iter().rev() {
325 chain = Some(Box::new(SelectionRange {
326 range: span_to_range(source, span),
327 parent: chain,
328 }));
329 }
330 chain.map(|b| *b).unwrap_or(empty)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 const SRC: &str = concat!(
338 "context shop\n",
339 "\n",
340 "-- a comment\n",
341 "-- second line\n",
342 "\n",
343 "type Money = {\n",
344 " cents: Int,\n",
345 " currency: String,\n",
346 "}\n",
347 "\n",
348 "fn total(m: Money) -> Int {\n",
349 " if m.cents > 0 {\n",
350 " m.cents\n",
351 " } else {\n",
352 " 0\n",
353 " }\n",
354 "}\n",
355 );
356
357 fn line_of(needle: &str) -> u32 {
359 let off = SRC.find(needle).expect("substring present");
360 offset_to_position(SRC, off).line
361 }
362
363 #[test]
364 fn folds_structural_constructs_and_comment_runs_omitting_single_lines() {
365 let folds = folding_ranges(SRC);
366
367 assert!(folds.iter().all(|f| f.end_line > f.start_line));
369
370 let comment = folds
372 .iter()
373 .find(|f| f.kind == Some(FoldingRangeKind::Comment))
374 .expect("comment run folds");
375 assert_eq!((comment.start_line, comment.end_line), (2, 3));
376
377 assert!(
379 folds
380 .iter()
381 .any(|f| f.start_line == line_of("type Money") && f.end_line == line_of("}\n\nfn")),
382 "record body folds"
383 );
384
385 assert!(
388 folds
389 .iter()
390 .any(|f| f.start_line == line_of("if m.cents") && f.kind.is_none()),
391 "if folds"
392 );
393 let tail_line = line_of(" m.cents"); assert!(
395 !folds.iter().any(|f| f.start_line == tail_line),
396 "single-line tail expression is not folded"
397 );
398 }
399
400 #[test]
401 fn selection_chain_widens_from_the_cursor_to_the_file() {
402 let off = SRC.find(" m.cents").unwrap() + 6; let pos = offset_to_position(SRC, off);
405 let ranges = selection_ranges(SRC, &[pos]);
406 assert_eq!(ranges.len(), 1);
407
408 let mut levels = 0;
410 let mut cur = Some(&ranges[0]);
411 let mut prev: Option<&Range> = None;
412 let mut outermost = ranges[0].range;
413 while let Some(node) = cur {
414 if let Some(p) = prev {
415 assert!(node.range.start <= p.start && node.range.end >= p.end);
417 assert!(node.range != *p, "ranges strictly widen");
418 }
419 outermost = node.range;
420 prev = Some(&node.range);
421 levels += 1;
422 cur = node.parent.as_deref();
423 }
424 assert!(levels >= 4, "cursor → … → context is several levels");
425 assert_eq!(outermost.start.line, 0);
427 }
428
429 #[test]
430 fn partial_parse_still_folds_what_parsed() {
431 let src = "context shop\n\ntype Money = {\n cents: Int,\n}\n\nfn broken(";
434 let folds = folding_ranges(src);
435 assert!(
436 folds.iter().any(|f| f.start_line == 2),
437 "the type still folds"
438 );
439 let sel = selection_ranges(src, &[Position::new(3, 4)]);
441 assert_eq!(sel.len(), 1);
442 }
443}