1use bynk_syntax::ast::*;
11use bynk_syntax::lexer::tokenize;
12use bynk_syntax::parser::parse_unit_with_recovery;
13use tower_lsp::lsp_types::{DocumentSymbol, Range, SymbolKind};
14
15use crate::position::span_to_range;
16
17pub fn outline(source: &str) -> Vec<DocumentSymbol> {
21 let Ok(tokens) = tokenize(source) else {
22 return Vec::new();
23 };
24 let (unit, _errs) = parse_unit_with_recovery(&tokens, source);
25 let Some(unit) = unit else {
26 return Vec::new();
27 };
28 match unit {
29 SourceUnit::Commons(c) => vec![commons_symbol(source, &c)],
30 SourceUnit::Context(c) => vec![context_symbol(source, &c)],
31 SourceUnit::Suite(t) => vec![test_symbol(source, &t)],
32 SourceUnit::Integration(i) => vec![integration_symbol(source, &i)],
33 SourceUnit::Adapter(a) => vec![adapter_symbol(source, &a)],
34 }
35}
36
37fn adapter_symbol(source: &str, a: &AdapterDecl) -> DocumentSymbol {
38 let children: Vec<DocumentSymbol> = a
39 .items
40 .iter()
41 .map(|item| item_symbol(source, item))
42 .collect();
43 make_symbol(
44 a.name.joined(),
45 detail_from_doc(&a.documentation),
46 SymbolKind::MODULE,
47 span_to_range(source, a.span),
48 span_to_range(source, a.name.span),
49 children,
50 )
51}
52
53fn integration_symbol(source: &str, i: &IntegrationDecl) -> DocumentSymbol {
54 let mut children: Vec<DocumentSymbol> = Vec::new();
55 for c in &i.cases {
56 children.push(make_symbol(
57 c.name.clone(),
58 None,
59 SymbolKind::FUNCTION,
60 span_to_range(source, c.span),
61 span_to_range(source, c.name_span),
62 Vec::new(),
63 ));
64 }
65 make_symbol(
66 format!("test integration \"{}\"", i.suite),
67 detail_from_doc(&i.documentation),
68 SymbolKind::MODULE,
69 span_to_range(source, i.span),
70 span_to_range(source, i.suite_span),
71 children,
72 )
73}
74
75fn test_symbol(source: &str, t: &SuiteDecl) -> DocumentSymbol {
76 let mut children: Vec<DocumentSymbol> = Vec::new();
77 for m in &t.mocks {
78 children.push(make_symbol(
79 format!("mocks {} = {}", m.target_name.name, m.impl_name.name),
80 None,
81 SymbolKind::INTERFACE,
82 span_to_range(source, m.span),
83 span_to_range(source, m.target_name.span),
84 Vec::new(),
85 ));
86 }
87 for c in &t.cases {
88 children.push(make_symbol(
89 c.name.clone(),
90 None,
91 SymbolKind::FUNCTION,
92 span_to_range(source, c.span),
93 span_to_range(source, c.name_span),
94 Vec::new(),
95 ));
96 }
97 make_symbol(
98 format!("test {}", t.target.joined()),
99 detail_from_doc(&t.documentation),
100 SymbolKind::MODULE,
101 span_to_range(source, t.span),
102 span_to_range(source, t.target.span),
103 children,
104 )
105}
106
107fn commons_symbol(source: &str, c: &Commons) -> DocumentSymbol {
108 let children: Vec<DocumentSymbol> = c
109 .items
110 .iter()
111 .map(|item| item_symbol(source, item))
112 .collect();
113 make_symbol(
114 c.name.joined(),
115 detail_from_doc(&c.documentation),
116 SymbolKind::MODULE,
117 span_to_range(source, c.span),
118 span_to_range(source, c.name.span),
119 children,
120 )
121}
122
123fn context_symbol(source: &str, c: &Context) -> DocumentSymbol {
124 let children: Vec<DocumentSymbol> = c
125 .items
126 .iter()
127 .map(|item| item_symbol(source, item))
128 .collect();
129 make_symbol(
130 c.name.joined(),
131 detail_from_doc(&c.documentation),
132 SymbolKind::MODULE,
133 span_to_range(source, c.span),
134 span_to_range(source, c.name.span),
135 children,
136 )
137}
138
139fn item_symbol(source: &str, item: &CommonsItem) -> DocumentSymbol {
140 match item {
141 CommonsItem::Type(t) => type_symbol(source, t),
142 CommonsItem::Fn(f) => fn_symbol(source, f),
143 CommonsItem::Capability(c) => capability_symbol(source, c),
144 CommonsItem::Provider(p) => provider_symbol(source, p),
145 CommonsItem::Service(s) => service_symbol(source, s),
146 CommonsItem::Agent(a) => agent_symbol(source, a),
147 CommonsItem::Actor(a) => actor_symbol(source, a),
148 }
149}
150
151fn actor_symbol(source: &str, a: &ActorDecl) -> DocumentSymbol {
152 make_symbol(
153 a.name.name.clone(),
154 detail_from_doc(&a.documentation),
155 SymbolKind::INTERFACE,
156 span_to_range(source, a.span),
157 span_to_range(source, a.name.span),
158 Vec::new(),
159 )
160}
161
162fn type_symbol(source: &str, t: &TypeDecl) -> DocumentSymbol {
163 let (kind, children) = match &t.body {
164 TypeBody::Record(r) => (SymbolKind::STRUCT, record_field_symbols(source, &r.fields)),
165 TypeBody::Sum(s) => (SymbolKind::ENUM, variant_symbols(source, &s.variants)),
166 TypeBody::Opaque { .. } => (SymbolKind::CLASS, Vec::new()),
167 TypeBody::Refined { .. } => (SymbolKind::TYPE_PARAMETER, Vec::new()),
168 };
169 make_symbol(
170 t.name.name.clone(),
171 detail_from_doc(&t.documentation),
172 kind,
173 span_to_range(source, t.span),
174 span_to_range(source, t.name.span),
175 children,
176 )
177}
178
179fn record_field_symbols(source: &str, fields: &[RecordField]) -> Vec<DocumentSymbol> {
180 fields
181 .iter()
182 .map(|f| {
183 make_symbol(
184 f.name.name.clone(),
185 None,
186 SymbolKind::FIELD,
187 span_to_range(source, f.span),
188 span_to_range(source, f.name.span),
189 Vec::new(),
190 )
191 })
192 .collect()
193}
194
195fn variant_symbols(source: &str, variants: &[Variant]) -> Vec<DocumentSymbol> {
196 variants
197 .iter()
198 .map(|v| {
199 make_symbol(
200 v.name.name.clone(),
201 None,
202 SymbolKind::ENUM_MEMBER,
203 span_to_range(source, v.span),
204 span_to_range(source, v.name.span),
205 Vec::new(),
206 )
207 })
208 .collect()
209}
210
211fn fn_symbol(source: &str, f: &FnDecl) -> DocumentSymbol {
212 let kind = match &f.name {
219 FnName::Free(_) => SymbolKind::FUNCTION,
220 FnName::Method { .. } => SymbolKind::METHOD,
221 };
222 make_symbol(
223 f.name.display(),
224 detail_from_doc(&f.documentation),
225 kind,
226 span_to_range(source, f.span),
227 span_to_range(source, f.name.ident().span),
228 Vec::new(),
229 )
230}
231
232fn capability_symbol(source: &str, c: &CapabilityDecl) -> DocumentSymbol {
233 let children = c
234 .ops
235 .iter()
236 .map(|op| {
237 make_symbol(
238 op.name.name.clone(),
239 detail_from_doc(&op.documentation),
240 SymbolKind::METHOD,
241 span_to_range(source, op.span),
242 span_to_range(source, op.name.span),
243 Vec::new(),
244 )
245 })
246 .collect();
247 make_symbol(
248 c.name.name.clone(),
249 detail_from_doc(&c.documentation),
250 SymbolKind::INTERFACE,
251 span_to_range(source, c.span),
252 span_to_range(source, c.name.span),
253 children,
254 )
255}
256
257fn provider_symbol(source: &str, p: &ProviderDecl) -> DocumentSymbol {
258 let children = p
259 .ops
260 .iter()
261 .map(|op| {
262 make_symbol(
263 op.name.name.clone(),
264 None,
265 SymbolKind::METHOD,
266 span_to_range(source, op.span),
267 span_to_range(source, op.name.span),
268 Vec::new(),
269 )
270 })
271 .collect();
272 let name = format!("{} = {}", p.capability.name, p.provider_name.name);
275 make_symbol(
276 name,
277 detail_from_doc(&p.documentation),
278 SymbolKind::OBJECT,
279 span_to_range(source, p.span),
280 span_to_range(source, p.provider_name.span),
281 children,
282 )
283}
284
285fn service_symbol(source: &str, s: &ServiceDecl) -> DocumentSymbol {
286 let children = s
287 .handlers
288 .iter()
289 .map(|h| handler_symbol(source, h))
290 .collect();
291 make_symbol(
292 s.name.name.clone(),
293 detail_from_doc(&s.documentation),
294 SymbolKind::CLASS,
295 span_to_range(source, s.span),
296 span_to_range(source, s.name.span),
297 children,
298 )
299}
300
301fn agent_symbol(source: &str, a: &AgentDecl) -> DocumentSymbol {
302 let mut children = Vec::new();
303 children.push(make_symbol(
305 a.key_name.name.clone(),
306 Some("key".into()),
307 SymbolKind::PROPERTY,
308 span_to_range(source, a.key_name.span),
309 span_to_range(source, a.key_name.span),
310 Vec::new(),
311 ));
312 for field in &a.store_fields {
314 children.push(make_symbol(
315 field.name.name.clone(),
316 Some("store".into()),
317 SymbolKind::PROPERTY,
318 span_to_range(source, field.span),
319 span_to_range(source, field.name.span),
320 Vec::new(),
321 ));
322 }
323 for h in &a.handlers {
325 children.push(handler_symbol(source, h));
326 }
327 make_symbol(
328 a.name.name.clone(),
329 detail_from_doc(&a.documentation),
330 SymbolKind::CLASS,
331 span_to_range(source, a.span),
332 span_to_range(source, a.name.span),
333 children,
334 )
335}
336
337fn handler_symbol(source: &str, h: &Handler) -> DocumentSymbol {
338 let name = match &h.method_name {
339 Some(m) => format!("call {}", m.name),
340 None => "call".to_string(),
341 };
342 let selection_span = h.method_name.as_ref().map(|m| m.span).unwrap_or(h.span);
343 make_symbol(
344 name,
345 detail_from_doc(&h.documentation),
346 SymbolKind::METHOD,
347 span_to_range(source, h.span),
348 span_to_range(source, selection_span),
349 Vec::new(),
350 )
351}
352
353fn detail_from_doc(doc: &Option<String>) -> Option<String> {
354 doc.as_ref().and_then(|d| {
355 let first = d
356 .lines()
357 .map(str::trim)
358 .find(|l| !l.is_empty())?
359 .to_string();
360 Some(first)
361 })
362}
363
364#[allow(deprecated)] fn make_symbol(
366 name: String,
367 detail: Option<String>,
368 kind: SymbolKind,
369 range: Range,
370 selection_range: Range,
371 children: Vec<DocumentSymbol>,
372) -> DocumentSymbol {
373 DocumentSymbol {
374 name,
375 detail,
376 kind,
377 tags: None,
378 deprecated: None,
379 range,
380 selection_range,
381 children: if children.is_empty() {
382 None
383 } else {
384 Some(children)
385 },
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 fn outline_of(src: &str) -> Vec<DocumentSymbol> {
394 outline(src)
395 }
396
397 #[test]
398 fn returns_empty_for_empty_input() {
399 assert!(outline_of("").is_empty());
400 }
401
402 #[test]
403 fn commons_with_types_and_fns_produces_module_with_children() {
404 let src = "commons demo.x {\n\
405 type Money = Int where NonNegative\n\
406 fn double(n: Int) -> Int { n + n }\n\
407 }";
408 let syms = outline_of(src);
409 assert_eq!(syms.len(), 1);
410 let module = &syms[0];
411 assert_eq!(module.kind, SymbolKind::MODULE);
412 assert_eq!(module.name, "demo.x");
413 let children = module.children.as_ref().expect("children");
414 assert_eq!(children.len(), 2);
415 assert_eq!(children[0].name, "Money");
416 assert_eq!(children[0].kind, SymbolKind::TYPE_PARAMETER);
417 assert_eq!(children[1].name, "double");
418 assert_eq!(children[1].kind, SymbolKind::FUNCTION);
419 }
420
421 #[test]
422 fn record_fields_nest_under_record_type() {
423 let src = "commons demo.x {\n\
424 type Pt = { x: Int, y: Int }\n\
425 }";
426 let syms = outline_of(src);
427 let module = &syms[0];
428 let children = module.children.as_ref().unwrap();
429 let pt = &children[0];
430 assert_eq!(pt.kind, SymbolKind::STRUCT);
431 let fields = pt.children.as_ref().expect("record fields");
432 assert_eq!(fields.len(), 2);
433 assert_eq!(fields[0].name, "x");
434 assert_eq!(fields[0].kind, SymbolKind::FIELD);
435 assert_eq!(fields[1].name, "y");
436 }
437
438 #[test]
439 fn sum_variants_nest_under_enum() {
440 let src = "commons demo.x {\n\
441 type Tag = enum { Foo, Bar, Baz }\n\
442 }";
443 let syms = outline_of(src);
444 let module = &syms[0];
445 let tag = &module.children.as_ref().unwrap()[0];
446 assert_eq!(tag.kind, SymbolKind::ENUM);
447 let variants = tag.children.as_ref().expect("variants");
448 assert_eq!(variants.len(), 3);
449 assert_eq!(variants[0].kind, SymbolKind::ENUM_MEMBER);
450 assert_eq!(variants[2].name, "Baz");
451 }
452
453 #[test]
454 fn opaque_type_uses_class_kind() {
455 let src = "commons demo.x {\n\
456 type Id = opaque Int where NonNegative\n\
457 }";
458 let syms = outline_of(src);
459 let id = &syms[0].children.as_ref().unwrap()[0];
460 assert_eq!(id.kind, SymbolKind::CLASS);
461 }
462
463 #[test]
464 fn context_with_service_and_agent_produces_hierarchical_tree() {
465 let src = "context demo.app {\n\
466 capability Clock { fn now() -> Int }\n\
467 service Api {\n\
468 on call(amount: Int) -> Int given Clock { amount }\n\
469 }\n\
470 agent Counter {\n\
471 key id: Int\n\
472 store value: Cell[Int]\n\
473 on call bump(amount: Int) -> Int { 0 }\n\
474 }\n\
475 }";
476 let syms = outline_of(src);
477 let module = &syms[0];
478 assert_eq!(module.name, "demo.app");
479 let children = module.children.as_ref().unwrap();
480 let kinds: Vec<SymbolKind> = children.iter().map(|c| c.kind).collect();
482 assert!(kinds.contains(&SymbolKind::INTERFACE));
483 assert!(kinds.contains(&SymbolKind::CLASS));
484 let service = children
485 .iter()
486 .find(|c| c.name == "Api")
487 .expect("Api service");
488 let service_children = service.children.as_ref().unwrap();
489 assert_eq!(service_children.len(), 1);
490 assert_eq!(service_children[0].kind, SymbolKind::METHOD);
491 let agent = children
492 .iter()
493 .find(|c| c.name == "Counter")
494 .expect("Counter agent");
495 let agent_children = agent.children.as_ref().unwrap();
496 assert_eq!(agent_children.len(), 3);
498 assert!(
499 agent_children
500 .iter()
501 .any(|c| c.kind == SymbolKind::PROPERTY && c.name == "value")
502 );
503 assert!(
504 agent_children
505 .iter()
506 .any(|c| c.kind == SymbolKind::METHOD && c.name == "call bump")
507 );
508 }
509
510 #[test]
511 fn adapter_unit_outlines_its_items() {
512 let src = "adapter tokens {\n\
513 binding \"./tokens.binding.ts\"\n\
514 exports capability { Jwt }\n\
515 capability Jwt {\n\
516 fn sign(secret: String) -> Effect[String]\n\
517 }\n\
518 provides Jwt = JoseJwt\n\
519 }";
520 let syms = outline_of(src);
521 assert_eq!(syms.len(), 1);
522 assert_eq!(syms[0].name, "tokens");
523 let children = syms[0].children.as_ref().unwrap();
524 assert!(children.iter().any(|c| c.name == "Jwt"));
526 assert!(children.iter().any(|c| c.name == "Jwt = JoseJwt"));
527 }
528
529 fn assert_selection_contained(sym: &DocumentSymbol, path: &str) {
533 let here = format!("{path}/{}", sym.name);
534 let outer = sym.range;
535 let inner = sym.selection_range;
536 let pos_le = |a: Position, b: Position| (a.line, a.character) <= (b.line, b.character);
537 assert!(
538 pos_le(outer.start, inner.start) && pos_le(inner.end, outer.end),
539 "selection_range not contained in range for {here}: range={outer:?} sel={inner:?}",
540 );
541 for c in sym.children.iter().flatten() {
542 assert_selection_contained(c, &here);
543 }
544 }
545
546 #[test]
551 fn fragment_context_with_all_items_dropped_keeps_valid_ranges() {
552 let src = "context demo.a\n\
553 \n\
554 agent Counter {\n\
555 key id: String\n\
556 store count: Cell[Int]\n\
557 on call bump() -> Effect[()] {\n\
558 let cur = count\n\
559 count := cur + 1\n\
560 ()\n\
561 }\n\
562 invariant bad:\n\
563 count >= 0\n\
564 }\n";
565 for sym in &outline(src) {
566 assert_selection_contained(sym, "");
567 }
568 }
569
570 use tower_lsp::lsp_types::Position;
571
572 #[test]
573 fn doc_block_first_line_appears_as_detail() {
574 let src = "commons demo.x {\n\
575 ---\n\
576 Short one-liner.\n\
577 Second line.\n\
578 ---\n\
579 type T = Int where Positive\n\
580 }";
581 let syms = outline_of(src);
582 let t = &syms[0].children.as_ref().unwrap()[0];
583 assert_eq!(t.detail.as_deref(), Some("Short one-liner."));
584 }
585}