Skip to main content

etdl_parser/
semantic.rs

1//! LSP-style semantic endpoints over an ETDL document (completions, hover,
2//! go-to-definition, find-references, document symbols, formatting).
3//!
4//! These are structural services built on the typed AST
5//! ([`crate::parse_document`]) and the position index
6//! ([`crate::spanned::SpanIndex`]). Line/column numbers are 0-based; offsets are
7//! character offsets, matching LSP conventions.
8
9use 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
17// LSP `SymbolKind` / `CompletionItemKind` constants.
18const 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
28/// Convert a character offset to a byte offset (clamped). `content[..byte]`
29/// contains exactly `offset` characters.
30fn 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
50// ---------------------------------------------------------------------------
51// document_symbols
52// ---------------------------------------------------------------------------
53
54pub 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
251// ---------------------------------------------------------------------------
252// hover
253// ---------------------------------------------------------------------------
254
255pub 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
312/// Best-effort human description for a definition element.
313fn 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
372// ---------------------------------------------------------------------------
373// goto_definition / find_references
374// ---------------------------------------------------------------------------
375
376pub 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)); // AsyncAPI refs resolve in another document
387            }
388            // References that point at a fault tree via an internal pointer.
389            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
434// ---------------------------------------------------------------------------
435// complete
436// ---------------------------------------------------------------------------
437
438pub 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
502/// Allowed keys for a map at the given path, with LSP completion kinds.
503fn 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            // Path-based node/gate/basic-event field maps.
559            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
608/// Interpret a path as `section > tree > sub (node/gate/basic-event id)`.
609fn 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        // Just entered a new node; suggest `type` first.
672        vec![kw("type")]
673    } else {
674        vec![]
675    }
676}
677
678/// Value completions for a field within a given map path.
679fn 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
803// ---------------------------------------------------------------------------
804// format
805// ---------------------------------------------------------------------------
806
807pub 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        // "next: FulfillmentConsequence" is 0-based line 39, value at column 15.
836        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        // "message" line 16, value column 12.
850        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        // Offset on the FulfillmentConsequence definition name (0-based line 49, col 7).
858        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        // Offset after the node name line (0-based line 21, col 30) — key position.
896        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        // Value position for `next:` inside ProcessPaymentOperation (line 39).
907        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}