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