1use serde_json::{json, Value};
10
11use crate::ast::{BasicEventType, EtlDocument, Node};
12use crate::spanned::{
13 parse_document_with_spans, ElementKind, IndexedElement, PathKey, PathPart, Span, SpanIndex,
14 SpanKey,
15};
16
17const SYMBOL_NAMESPACE: u32 = 3;
19const SYMBOL_OBJECT: u32 = 19;
20const SYMBOL_FIELD: u32 = 8;
21const SYMBOL_EVENT: u32 = 24;
22const SYMBOL_METHOD: u32 = 6;
23
24const COMPLETION_KEYWORD: u32 = 14;
25const COMPLETION_FIELD: u32 = 5;
26const COMPLETION_REFERENCE: u32 = 18;
27
28fn char_to_byte(content: &str, offset: u32) -> usize {
31 content
32 .char_indices()
33 .nth(offset as usize)
34 .map(|(i, _)| i)
35 .unwrap_or(content.len())
36}
37
38fn range(span: &Span) -> Value {
39 json!({
40 "start": { "line": span.line, "character": span.column },
41 "end": { "line": span.end_line, "character": span.end_column }
42 })
43}
44
45fn location(el: &IndexedElement) -> Value {
46 let span = el.key_span.unwrap_or(el.span);
47 json!({ "range": range(&span) })
48}
49
50pub fn document_symbols(content: &str) -> Result<Value, String> {
55 let (doc, index) = parse_document_with_spans(content)?;
56 let mut symbols: Vec<Value> = Vec::new();
57
58 if let Some(section_el) = index.resolve(&SpanKey::Section("event_trees")) {
59 let mut children = Vec::new();
60 for (tree_name, tree) in &doc.event_trees {
61 let mut tree_children = Vec::new();
62 if let Some(ie) = index.resolve(&SpanKey::InitiatingEvent {
63 tree: tree_name.clone(),
64 field: "id",
65 }) {
66 tree_children.push(symbol("initiatingEvent", None, SYMBOL_EVENT, ie, vec![]));
67 }
68 let mut node_children = Vec::new();
69 for (node_id, node) in &tree.nodes {
70 let Some(el) = index.resolve(&SpanKey::Node {
71 tree: tree_name.clone(),
72 id: node_id.clone(),
73 }) else {
74 continue;
75 };
76 let detail = match node {
77 Node::Barrier(b) => Some(format!("barrier · {} branch(es)", b.branches.len())),
78 Node::Operation(op) => Some(format!("operation · handler {}", op.handler)),
79 Node::Consequence(c) => {
80 let op = match c.consequence_operation {
81 crate::ast::ConsequenceOperation::Send => "send",
82 crate::ast::ConsequenceOperation::Terminate => "terminate",
83 };
84 Some(format!("consequence · {}", op))
85 }
86 };
87 node_children.push(symbol(node_id, detail, SYMBOL_OBJECT, el, vec![]));
88 }
89 let nodes_el = index
90 .resolve(&SpanKey::NodeField {
91 tree: tree_name.clone(),
92 id: String::new(),
93 field: "branches",
94 })
95 .or_else(|| {
96 index.resolve(&SpanKey::Tree {
97 tree: tree_name.clone(),
98 })
99 });
100 if !node_children.is_empty() {
101 let group_el = nodes_el.unwrap_or(section_el);
102 tree_children.push(symbol("nodes", None, SYMBOL_FIELD, group_el, node_children));
103 }
104 let tree_el = index
105 .resolve(&SpanKey::Tree {
106 tree: tree_name.clone(),
107 })
108 .unwrap_or(section_el);
109 children.push(symbol(
110 tree_name,
111 None,
112 SYMBOL_NAMESPACE,
113 tree_el,
114 tree_children,
115 ));
116 }
117 symbols.push(symbol(
118 "eventTrees",
119 None,
120 SYMBOL_NAMESPACE,
121 section_el,
122 children,
123 ));
124 }
125
126 if let Some(section_el) = index.resolve(&SpanKey::Section("fault_trees")) {
127 let mut children = Vec::new();
128 if let Some(ftrees) = &doc.fault_trees {
129 for (ft_name, ft) in ftrees {
130 let mut ft_children = Vec::new();
131 if let Some(te) = index.resolve(&SpanKey::TopEvent {
132 tree: ft_name.clone(),
133 field: "id",
134 }) {
135 ft_children.push(symbol("topEvent", None, SYMBOL_EVENT, te, vec![]));
136 }
137 let mut gate_children = Vec::new();
138 if let Some(gates) = &ft.gates {
139 for (gate_id, gate) in gates {
140 let Some(el) = index.resolve(&SpanKey::Gate {
141 tree: ft_name.clone(),
142 id: gate_id.clone(),
143 }) else {
144 continue;
145 };
146 let detail =
147 format!("{:?} gate · {} input(s)", gate.gate_type, gate.inputs.len());
148 gate_children.push(symbol(
149 gate_id,
150 Some(detail),
151 SYMBOL_METHOD,
152 el,
153 vec![],
154 ));
155 }
156 }
157 if !gate_children.is_empty() {
158 let group_el = index
159 .resolve(&SpanKey::Gate {
160 tree: ft_name.clone(),
161 id: String::new(),
162 })
163 .or_else(|| {
164 index.resolve(&SpanKey::FaultTree {
165 tree: ft_name.clone(),
166 })
167 });
168 let group_el = group_el.unwrap_or(section_el);
169 ft_children.push(symbol("gates", None, SYMBOL_FIELD, group_el, gate_children));
170 }
171 let mut be_children = Vec::new();
172 for (be_id, be) in &ft.basic_events {
173 let Some(el) = index.resolve(&SpanKey::BasicEvent {
174 tree: ft_name.clone(),
175 id: be_id.clone(),
176 }) else {
177 continue;
178 };
179 let detail = match be.event_type {
180 Some(BasicEventType::House) => "house event".to_string(),
181 Some(BasicEventType::Undeveloped) => "undeveloped event".to_string(),
182 Some(BasicEventType::Conditional) => "conditional event".to_string(),
183 _ => "basic event".to_string(),
184 };
185 be_children.push(symbol(be_id, Some(detail), SYMBOL_EVENT, el, vec![]));
186 }
187 if !be_children.is_empty() {
188 let group_el = index
189 .resolve(&SpanKey::BasicEvent {
190 tree: ft_name.clone(),
191 id: String::new(),
192 })
193 .or_else(|| {
194 index.resolve(&SpanKey::FaultTree {
195 tree: ft_name.clone(),
196 })
197 });
198 let group_el = group_el.unwrap_or(section_el);
199 ft_children.push(symbol(
200 "basicEvents",
201 None,
202 SYMBOL_FIELD,
203 group_el,
204 be_children,
205 ));
206 }
207 let ft_el = index
208 .resolve(&SpanKey::FaultTree {
209 tree: ft_name.clone(),
210 })
211 .unwrap_or(section_el);
212 children.push(symbol(ft_name, None, SYMBOL_NAMESPACE, ft_el, ft_children));
213 }
214 }
215 symbols.push(symbol(
216 "faultTrees",
217 None,
218 SYMBOL_NAMESPACE,
219 section_el,
220 children,
221 ));
222 }
223
224 Ok(json!({
225 "symbols": symbols
226 }))
227}
228
229fn symbol(
230 name: &str,
231 detail: Option<String>,
232 kind: u32,
233 el: &IndexedElement,
234 children: Vec<Value>,
235) -> Value {
236 let mut obj = serde_json::Map::new();
237 obj.insert("name".to_string(), Value::String(name.to_string()));
238 if let Some(d) = detail {
239 obj.insert("detail".to_string(), Value::String(d));
240 }
241 obj.insert("kind".to_string(), Value::from(kind));
242 obj.insert("range".to_string(), range(&el.span));
243 let selection = el.key_span.unwrap_or(el.span);
244 obj.insert("selectionRange".to_string(), range(&selection));
245 if !children.is_empty() {
246 obj.insert("children".to_string(), Value::Array(children));
247 }
248 Value::Object(obj)
249}
250
251pub fn hover(content: &str, offset: u32) -> Result<Value, String> {
256 let (doc, index) = parse_document_with_spans(content)?;
257 let Some(el) = index.find_deepest(offset) else {
258 return Ok(json!(null));
259 };
260 let text = hover_text(&doc, &index, el);
261 let span = el.key_span.unwrap_or(el.span);
262 Ok(json!({
263 "contents": { "kind": "markdown", "value": text },
264 "range": range(&span)
265 }))
266}
267
268fn hover_text(doc: &EtlDocument, index: &SpanIndex, el: &IndexedElement) -> String {
269 let tree = el.tree.as_deref().unwrap_or("");
270 match el.kind {
271 ElementKind::Reference => {
272 let field = el.field.as_deref().unwrap_or("");
273 if matches!(field, "message" | "emits" | "channel") {
274 format!("**AsyncAPI reference** `{}`\n\nField: `{}`", el.name, field)
275 } else {
276 let mut text = format!("**References** `{}`", el.name);
277 if let Some(def) = index.definition(tree, &el.name) {
278 let span = def.key_span.unwrap_or(def.span);
279 text.push_str(&format!("\n\nDefined at line {}", span.line + 1));
280 }
281 text
282 }
283 }
284 ElementKind::Definition => {
285 if let Some((detail, description)) = definition_detail(doc, el) {
286 let mut text = format!("**{}**", el.name);
287 if !detail.is_empty() {
288 text.push_str(&format!(" — {}", detail));
289 }
290 if let Some(d) = description {
291 if !d.is_empty() {
292 text.push_str(&format!("\n\n{}", d));
293 }
294 }
295 text
296 } else {
297 format!("**{}**", el.name)
298 }
299 }
300 ElementKind::Field => {
301 let field = el.field.as_deref().unwrap_or("");
302 if !el.name.is_empty() {
303 format!("`{}` = `{}`", field, el.name)
304 } else {
305 format!("`{}`", field)
306 }
307 }
308 ElementKind::Section => format!("**Section** `{}`", el.name),
309 }
310}
311
312fn definition_detail(doc: &EtlDocument, el: &IndexedElement) -> Option<(String, Option<String>)> {
314 let tree = el.tree.as_deref().unwrap_or("");
315 if let Some(node) = doc
316 .event_trees
317 .get(tree)
318 .and_then(|t| t.nodes.get(&el.name))
319 {
320 match node {
321 Node::Barrier(b) => Some((
322 format!("barrier · {} branch(es)", b.branches.len()),
323 b.description.clone(),
324 )),
325 Node::Operation(op) => Some((
326 format!("operation · handler `{}`", op.handler),
327 op.description.clone(),
328 )),
329 Node::Consequence(c) => {
330 let op = match c.consequence_operation {
331 crate::ast::ConsequenceOperation::Send => "send",
332 crate::ast::ConsequenceOperation::Terminate => "terminate",
333 };
334 Some((format!("consequence · `{}`", op), c.description.clone()))
335 }
336 }
337 } else if doc.event_trees.contains_key(tree) && el.field.is_none() {
338 Some((
339 "event tree".to_string(),
340 doc.event_trees[tree].description.clone(),
341 ))
342 } else if let Some(ft) = doc.fault_trees.as_ref().and_then(|f| f.get(tree)) {
343 if let Some(gate) = ft.gates.as_ref().and_then(|g| g.get(&el.name)) {
344 Some((
345 format!("{:?} gate", gate.gate_type),
346 gate.description.clone(),
347 ))
348 } else if let Some(be) = ft.basic_events.get(&el.name) {
349 let detail = if be.failure_rate.is_some() {
350 "basic event (failure rate model)".to_string()
351 } else {
352 "basic event".to_string()
353 };
354 Some((detail, Some(be.description.clone())))
355 } else if el.name == tree {
356 Some(("fault tree".to_string(), ft.description.clone()))
357 } else {
358 None
359 }
360 } else if el.name == tree {
361 Some((
362 "event tree".to_string(),
363 doc.event_trees
364 .get(tree)
365 .and_then(|t| t.description.clone()),
366 ))
367 } else {
368 None
369 }
370}
371
372pub fn goto_definition(content: &str, offset: u32) -> Result<Value, String> {
377 let (_doc, index) = parse_document_with_spans(content)?;
378 let Some(el) = index.find_deepest(offset) else {
379 return Ok(json!(null));
380 };
381 match el.kind {
382 ElementKind::Reference => {
383 let field = el.field.as_deref().unwrap_or("");
384 let tree = el.tree.as_deref().unwrap_or("");
385 if matches!(field, "message" | "emits" | "channel") {
386 return Ok(json!(null)); }
388 if matches!(
390 field,
391 "on_failure_probability_source" | "probability_source"
392 ) {
393 let ft_id = el
394 .name
395 .trim_start_matches("#/faultTrees/")
396 .split('/')
397 .next()
398 .unwrap_or_default();
399 if let Some(def) = index.definition(ft_id, ft_id) {
400 return Ok(location(def));
401 }
402 return Ok(json!(null));
403 }
404 if let Some(def) = index.definition(tree, &el.name) {
405 return Ok(location(def));
406 }
407 Ok(json!(null))
408 }
409 ElementKind::Definition => Ok(location(el)),
410 _ => Ok(json!(null)),
411 }
412}
413
414pub fn find_references(content: &str, offset: u32) -> Result<Value, String> {
415 let (_doc, index) = parse_document_with_spans(content)?;
416 let Some(el) = index.find_deepest(offset) else {
417 return Ok(json!([]));
418 };
419 let (tree, id) = match el.kind {
420 ElementKind::Reference | ElementKind::Definition => (
421 el.tree.as_deref().unwrap_or("").to_string(),
422 el.name.clone(),
423 ),
424 _ => return Ok(json!([])),
425 };
426 let locations: Vec<Value> = index
427 .by_identity(&tree, &id)
428 .iter()
429 .map(|r| location(r))
430 .collect();
431 Ok(Value::Array(locations))
432}
433
434pub fn complete(content: &str, offset: u32) -> Result<Value, String> {
439 let (doc, index) = parse_document_with_spans(content)?;
440 let items = completion_items(&doc, &index, content, offset);
441 Ok(json!({ "isIncomplete": false, "items": items }))
442}
443
444fn completion_items(
445 doc: &EtlDocument,
446 index: &SpanIndex,
447 content: &str,
448 offset: u32,
449) -> Vec<Value> {
450 let byte = char_to_byte(content, offset);
451 let line_start = content[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
452 let line = &content[line_start..byte];
453
454 let is_value_position = line.contains(':');
455 let field = if is_value_position {
456 line.split(':').next().unwrap_or("").trim().to_string()
457 } else {
458 String::new()
459 };
460 let prefix = if is_value_position {
461 line.split(':').nth(1).unwrap_or("").trim()
462 } else {
463 line.trim()
464 };
465
466 let map_path = enclosing_map_path(index, offset);
467 let mut items: Vec<Value> = Vec::new();
468
469 if is_value_position {
470 for (label, kind, detail) in value_completions(doc, &map_path, &field) {
471 if label.starts_with(prefix) {
472 items.push(completion_item(&label, kind, detail));
473 }
474 }
475 } else if let Some(path) = map_path {
476 for (label, kind) in key_completions(doc, &path) {
477 if label.starts_with(prefix) {
478 items.push(completion_item(&label, kind, None));
479 }
480 }
481 }
482
483 items
484}
485
486fn enclosing_map_path(index: &SpanIndex, offset: u32) -> Option<PathKey> {
487 let el = index.find_deepest(offset)?;
488 match el.kind {
489 ElementKind::Definition => Some(el.path.clone()),
490 _ => {
491 let mut p = el.path.clone();
492 p.pop();
493 Some(p)
494 }
495 }
496}
497
498fn completion_item(label: &str, kind: u32, detail: Option<String>) -> Value {
499 json!({ "label": label, "kind": kind, "detail": detail })
500}
501
502fn key_completions(doc: &EtlDocument, path: &PathKey) -> Vec<(String, u32)> {
504 let last = path.last().map(|p| match p {
505 PathPart::Key(k) => k.as_str(),
506 PathPart::Index(_) => "",
507 });
508
509 let field = |s: &'static str| (s.to_string(), COMPLETION_FIELD);
510 let node = |s: &'static str| (s.to_string(), COMPLETION_KEYWORD);
511
512 match last {
513 None => vec![
514 node("etdl"),
515 node("info"),
516 node("asyncapi_imports"),
517 node("components"),
518 node("eventTrees"),
519 node("faultTrees"),
520 ],
521 Some("info") => vec![
522 field("title"),
523 field("version"),
524 field("domain"),
525 field("description"),
526 ],
527 Some("asyncapi_imports") => vec![],
528 Some("event_trees") => vec![],
529 Some("fault_trees") => vec![],
530 Some("initiating_event") => vec![field("id"), field("message"), field("next")],
531 Some("top_event") => vec![
532 field("id"),
533 field("description"),
534 field("message"),
535 field("rootCause"),
536 ],
537 Some("nodes") => vec![],
538 Some("gates") => vec![],
539 Some("basic_events") => vec![],
540 Some("transfers") => vec![],
541 Some("branches") => vec![
542 field("outcome"),
543 field("condition"),
544 field("probability"),
545 field("probabilityOfSuccess"),
546 field("probabilityOfFailure"),
547 field("probabilitySource"),
548 field("next"),
549 field("description"),
550 ],
551 Some("components") => vec![
552 node("barriers"),
553 node("operations"),
554 node("gates"),
555 node("basicEvents"),
556 ],
557 _ => {
558 if let Some((section, tree, sub)) = tree_sub_context(path) {
560 match sub {
561 TreeSub::Node(id) => node_field_keys(doc, section, &tree, &id),
562 TreeSub::Gate => vec![
563 node("type"),
564 field("inputs"),
565 field("k"),
566 field("inhibitCondition"),
567 field("description"),
568 ],
569 TreeSub::BasicEvent => vec![
570 field("description"),
571 field("probability"),
572 field("failureRate"),
573 field("missionTime"),
574 field("undeveloped"),
575 field("eventType"),
576 field("message"),
577 ],
578 TreeSub::None => {
579 if section == "event_trees" {
580 vec![node("initiatingEvent"), node("nodes"), field("description")]
581 } else {
582 vec![
583 node("topEvent"),
584 node("gates"),
585 node("basicEvents"),
586 node("transfers"),
587 field("description"),
588 ]
589 }
590 }
591 }
592 } else {
593 vec![]
594 }
595 }
596 }
597 .into_iter()
598 .collect()
599}
600
601enum TreeSub {
602 None,
603 Node(String),
604 Gate,
605 BasicEvent,
606}
607
608fn tree_sub_context(path: &PathKey) -> Option<(&str, String, TreeSub)> {
610 let section_idx = path
611 .iter()
612 .position(|p| matches!(p, PathPart::Key(k) if k == "event_trees" || k == "fault_trees"))?;
613 let PathPart::Key(section) = &path[section_idx] else {
614 return None;
615 };
616 let PathPart::Key(tree) = &path[section_idx + 1] else {
617 return None;
618 };
619
620 let after = &path[section_idx + 2..];
621 match after {
622 [] => Some((section, tree.clone(), TreeSub::None)),
623 [PathPart::Key(k)] => match k.as_str() {
624 "nodes" => Some((section, tree.clone(), TreeSub::Node(String::new()))),
625 "gates" => Some((section, tree.clone(), TreeSub::Gate)),
626 "basic_events" => Some((section, tree.clone(), TreeSub::BasicEvent)),
627 _ => Some((section, tree.clone(), TreeSub::None)),
628 },
629 [PathPart::Key(k), PathPart::Key(id)] => match k.as_str() {
630 "nodes" => Some((section, tree.clone(), TreeSub::Node(id.clone()))),
631 "gates" => Some((section, tree.clone(), TreeSub::Gate)),
632 "basic_events" => Some((section, tree.clone(), TreeSub::BasicEvent)),
633 _ => Some((section, tree.clone(), TreeSub::None)),
634 },
635 _ => Some((section, tree.clone(), TreeSub::None)),
636 }
637}
638
639fn node_field_keys(doc: &EtlDocument, section: &str, tree: &str, id: &str) -> Vec<(String, u32)> {
640 let field = |s: &'static str| (s.to_string(), COMPLETION_FIELD);
641 let kw = |s: &'static str| (s.to_string(), COMPLETION_KEYWORD);
642 let tree_map = if section == "event_trees" {
643 doc.event_trees.get(tree)
644 } else {
645 None
646 };
647 if let Some(node) = tree_map.and_then(|t| t.nodes.get(id)) {
648 match node {
649 Node::Barrier(_) => vec![kw("type"), kw("branches"), field("description")],
650 Node::Operation(_) => vec![
651 kw("type"),
652 kw("action"),
653 field("handler"),
654 field("emits"),
655 field("next"),
656 field("onFailure"),
657 field("onFailureProbabilitySource"),
658 kw("retryPolicy"),
659 field("timeoutMs"),
660 field("description"),
661 ],
662 Node::Consequence(_) => vec![
663 kw("type"),
664 kw("operation"),
665 field("channel"),
666 field("message"),
667 field("description"),
668 ],
669 }
670 } else if id.is_empty() {
671 vec![kw("type")]
673 } else {
674 vec![]
675 }
676}
677
678fn value_completions(
680 doc: &EtlDocument,
681 map_path: &Option<PathKey>,
682 field: &str,
683) -> Vec<(String, u32, Option<String>)> {
684 let mut out = Vec::new();
685 let Some(path) = map_path else { return out };
686
687 let tree = tree_from_path(path);
688 let section = tree.as_ref().map(|(s, _)| *s).unwrap_or("");
689
690 let mut push = |label: String, detail: Option<String>| {
691 out.push((label, COMPLETION_REFERENCE, detail));
692 };
693
694 match field {
695 "type" => {
696 if section == "fault_trees" {
697 for t in [
698 "AND",
699 "OR",
700 "NOT",
701 "XOR",
702 "VOTING",
703 "INHIBIT",
704 "PRIORITY_AND",
705 ] {
706 push(t.to_string(), Some("gate type".to_string()));
707 }
708 } else {
709 for t in ["barrier", "operation", "consequence"] {
710 push(t.to_string(), Some("node type".to_string()));
711 }
712 }
713 }
714 "operation" => {
715 for t in ["send", "terminate"] {
716 push(t.to_string(), Some("consequence operation".to_string()));
717 }
718 }
719 "action" => push("execute".to_string(), Some("operation action".to_string())),
720 "eventType" => {
721 for t in ["basic", "house", "undeveloped", "conditional"] {
722 push(t.to_string(), Some("basic event type".to_string()));
723 }
724 }
725 "backoffStrategy" => {
726 for t in ["fixed", "exponential"] {
727 push(t.to_string(), Some("backoff strategy".to_string()));
728 }
729 }
730 "condition" => push(
731 "default".to_string(),
732 Some("default branch condition".to_string()),
733 ),
734 "next" | "onFailure" => {
735 if let Some((_, tree_name)) = tree {
736 if let Some(t) = doc.event_trees.get(&tree_name) {
737 for nid in t.nodes.keys() {
738 push(nid.clone(), Some("node".to_string()));
739 }
740 }
741 }
742 }
743 "inputs" | "rootCause" => {
744 if let Some((_, tree_name)) = tree {
745 if let Some(ft) = doc.fault_trees.as_ref().and_then(|f| f.get(&tree_name)) {
746 if let Some(gates) = &ft.gates {
747 for gid in gates.keys() {
748 push(gid.clone(), Some("gate".to_string()));
749 }
750 }
751 for be_id in ft.basic_events.keys() {
752 push(be_id.clone(), Some("basic event".to_string()));
753 }
754 }
755 }
756 }
757 "message" | "emits" | "channel" => {
758 for alias in doc.asyncapi_imports.keys() {
759 push(
760 format!("{}#/", alias),
761 Some(format!("import alias `{}`", alias)),
762 );
763 }
764 }
765 "onFailureProbabilitySource" | "probabilitySource" => {
766 if let Some(ftrees) = &doc.fault_trees {
767 for ft_id in ftrees.keys() {
768 push(
769 format!("#/faultTrees/{}/topEvent", ft_id),
770 Some("fault tree top event".to_string()),
771 );
772 }
773 }
774 }
775 "target" => {
776 if let Some(ftrees) = &doc.fault_trees {
777 for ft_id in ftrees.keys() {
778 push(
779 format!("#/faultTrees/{}/topEvent", ft_id),
780 Some("fault tree top event".to_string()),
781 );
782 }
783 }
784 }
785 _ => {}
786 }
787 out
788}
789
790fn tree_from_path(path: &PathKey) -> Option<(&str, String)> {
791 for (i, p) in path.iter().enumerate() {
792 if let PathPart::Key(k) = p {
793 if k == "event_trees" || k == "fault_trees" {
794 if let Some(PathPart::Key(t)) = path.get(i + 1) {
795 return Some((k.as_str(), t.clone()));
796 }
797 }
798 }
799 }
800 None
801}
802
803pub fn format(content: &str) -> Result<Value, String> {
808 use saphyr::{LoadableYamlNode, Yaml, YamlEmitter};
809
810 let docs = Yaml::load_from_str(content).map_err(|e| e.to_string())?;
811 let doc = docs.first().ok_or("empty ETDL document")?;
812 let mut out = String::new();
813 let mut emitter = YamlEmitter::new(&mut out);
814 emitter.dump(doc).map_err(|e| e.to_string())?;
815 Ok(json!({ "text": out }))
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821
822 const FIXTURE: &str = include_str!("../tests/fixtures/order-fulfillment.etdl");
823
824 fn char_offset(content: &str, line: usize, col: usize) -> u32 {
825 let line_start = content
826 .lines()
827 .take(line)
828 .map(|l| l.len() + 1)
829 .sum::<usize>();
830 (line_start + col) as u32
831 }
832
833 #[test]
834 fn goto_definition_follows_references() {
835 let offset = char_offset(FIXTURE, 39, 15);
837 let loc = goto_definition(FIXTURE, offset).unwrap();
838 assert!(!loc.is_null(), "expected a definition location");
839 let range = &loc["range"];
840 assert_eq!(
841 range["start"]["line"], 48,
842 "target is the node definition line"
843 );
844 assert_eq!(range["start"]["character"], 6);
845 }
846
847 #[test]
848 fn goto_definition_null_for_external_ref() {
849 let offset = char_offset(FIXTURE, 16, 12);
851 let loc = goto_definition(FIXTURE, offset).unwrap();
852 assert!(loc.is_null(), "asyncapi refs have no local definition");
853 }
854
855 #[test]
856 fn find_references_returns_all() {
857 let offset = char_offset(FIXTURE, 49, 7);
859 let refs = find_references(FIXTURE, offset).unwrap();
860 let array = refs.as_array().expect("array of locations");
861 assert!(
862 array.len() >= 2,
863 "definition plus at least one reference: {:?}",
864 array
865 );
866 }
867
868 #[test]
869 fn hover_renders_markdown() {
870 let offset = char_offset(FIXTURE, 39, 15);
871 let hover = hover(FIXTURE, offset).unwrap();
872 let value = hover["contents"]["value"].as_str().unwrap();
873 assert!(value.contains("FulfillmentConsequence"));
874 assert!(hover.get("range").is_some());
875 }
876
877 #[test]
878 fn document_symbols_has_trees_and_nodes() {
879 let symbols = document_symbols(FIXTURE).unwrap();
880 let event = &symbols["symbols"][0];
881 assert_eq!(event["name"], "eventTrees");
882 let tree = &event["children"][0];
883 assert_eq!(tree["name"], "OrderFulfillment");
884 let names: Vec<&str> = tree["children"][1]["children"]
885 .as_array()
886 .unwrap()
887 .iter()
888 .map(|s| s["name"].as_str().unwrap())
889 .collect();
890 assert!(names.contains(&"InventoryCheckBarrier"));
891 }
892
893 #[test]
894 fn complete_suggests_node_fields() {
895 let offset = char_offset(FIXTURE, 21, 30);
897 let result = complete(FIXTURE, offset).unwrap();
898 let items = result["items"].as_array().unwrap();
899 let labels: Vec<&str> = items.iter().map(|i| i["label"].as_str().unwrap()).collect();
900 assert!(labels.contains(&"branches"));
901 assert!(labels.contains(&"type"));
902 }
903
904 #[test]
905 fn complete_suggests_next_values() {
906 let line = " next: ";
908 let byte_start = FIXTURE.lines().take(39).map(|l| l.len() + 1).sum::<usize>();
909 let offset = (byte_start + line.find(':').unwrap() + 1) as u32;
910 let result = complete(FIXTURE, offset).unwrap();
911 let items = result["items"].as_array().unwrap();
912 let labels: Vec<&str> = items.iter().map(|i| i["label"].as_str().unwrap()).collect();
913 assert!(labels.contains(&"FulfillmentConsequence"));
914 assert!(labels.contains(&"PaymentFailedConsequence"));
915 }
916
917 #[test]
918 fn format_round_trips() {
919 let result = format(FIXTURE).unwrap();
920 let text = result["text"].as_str().unwrap();
921 assert!(text.contains("eventTrees:"));
922 assert!(text.contains("OrderFulfillment:"));
923 }
924}