Skip to main content

bynk_lsp/
sequence_request.rs

1//! #846: `bynk/sequenceModel` — the sequence-diagram custom LSP request.
2//!
3//! The first custom (non-standard) request in this server: no `workspace/*/
4//! refresh` nudge exists for it (there is no generic "refresh a custom
5//! method" in the LSP spec or in `tower_lsp::Client`), and none is needed —
6//! Tier 1 is on-demand: the client re-issues the request each time the
7//! command/lens fires, rather than the server pushing updates.
8//!
9//! Two responsibilities live here, out of `lib.rs`: locating the `Handler`
10//! AST node enclosing a cursor position (by re-parsing the committed
11//! snapshot, the same convention `identifier_at` uses in `lib.rs`), and the
12//! wire shape sent to the client (a plain serde mirror of
13//! [`bynk_ide::sequence::SequenceModel`], `Span` lowered to LSP `Range` —
14//! same convention as `SerKey` in `lib.rs`).
15
16use bynk_ide::sequence::{
17    self, AltKind, HandlerOwner, MessageKind, ParticipantKind, SequenceModel,
18};
19use bynk_syntax::ast::{CommonsItem, Handler, SourceUnit};
20
21/// Locate the `Handler` enclosing `offset` in `text` and build its sequence
22/// model. `info` is the owning unit's cross-context/agent table — `None`
23/// degrades classification to capabilities only (still correct; just unable
24/// to recognise agent/cross-context lifelines), which happens for a unit
25/// `sequence_info` has no entry for (a commons file, or one this round never
26/// reached because the pipeline bailed before it).
27pub fn sequence_model_at(
28    text: &str,
29    offset: usize,
30    info: Option<&bynk_ide::ContextSequenceInfo>,
31) -> Option<SequenceModel> {
32    let tokens = bynk_syntax::lexer::tokenize(text).ok()?;
33    let (unit, _errs) = bynk_syntax::parser::parse_unit_with_recovery(&tokens, text);
34    let items: &[CommonsItem] = match unit.as_ref()? {
35        SourceUnit::Context(c) => &c.items,
36        SourceUnit::Adapter(a) => &a.items,
37        SourceUnit::Commons(_) | SourceUnit::Suite(_) => return None,
38    };
39    for item in items {
40        match item {
41            CommonsItem::Service(s) => {
42                if let Some(h) = handler_at(&s.handlers, offset) {
43                    return Some(sequence::sequence_model(
44                        h,
45                        HandlerOwner::Service(&s.name.name),
46                        // v0.155: a handler with no `given`/`by` of its own
47                        // inherits the service-level default. See `sequence_model`.
48                        &s.default_given,
49                        s.default_by.as_ref(),
50                        info,
51                    ));
52                }
53            }
54            CommonsItem::Agent(a) => {
55                if let Some(h) = handler_at(&a.handlers, offset) {
56                    return Some(sequence::sequence_model(
57                        h,
58                        HandlerOwner::Agent(&a.name.name),
59                        // Agents have no service-level `given` default and no
60                        // principal (`by`).
61                        &[],
62                        None,
63                        info,
64                    ));
65                }
66            }
67            _ => {}
68        }
69    }
70    None
71}
72
73fn handler_at(handlers: &[Handler], offset: usize) -> Option<&Handler> {
74    handlers
75        .iter()
76        .find(|h| h.span.start <= offset && offset < h.span.end)
77}
78
79/// Every `on <kind>` handler declaration in `text`, for the per-handler
80/// "Show Sequence" CodeLens. **Not** `index_queries::code_lenses` — that
81/// walks `SymbolKind::Handler` sites, which only agent handlers get
82/// (`bynk-check/src/index.rs`: "Service handlers have no per-handler name...
83/// so only agent dispatch is covered"); reusing it as-is would silently drop
84/// the lens for every service (non-agent) handler, which is most of them.
85/// A direct AST walk covers both uniformly.
86pub fn handler_lens_sites(text: &str) -> Vec<bynk_syntax::span::Span> {
87    let Ok(tokens) = bynk_syntax::lexer::tokenize(text) else {
88        return Vec::new();
89    };
90    let (unit, _errs) = bynk_syntax::parser::parse_unit_with_recovery(&tokens, text);
91    let Some(unit) = unit else {
92        return Vec::new();
93    };
94    let items: &[CommonsItem] = match &unit {
95        SourceUnit::Context(c) => &c.items,
96        SourceUnit::Adapter(a) => &a.items,
97        SourceUnit::Commons(_) | SourceUnit::Suite(_) => return Vec::new(),
98    };
99    let mut sites = Vec::new();
100    for item in items {
101        match item {
102            CommonsItem::Service(s) => sites.extend(s.handlers.iter().map(|h| h.span)),
103            CommonsItem::Agent(a) => sites.extend(a.handlers.iter().map(|h| h.span)),
104            _ => {}
105        }
106    }
107    sites
108}
109
110/// The `bynk/sequenceModel` request payload — the same two-field
111/// text-document + cursor-position shape every other cursor-anchored request
112/// in this server uses (`HoverParams`, `SignatureHelpParams`, …).
113///
114/// #847: `rename_all = "camelCase"` added — the client sends the LSP wire names
115/// `textDocument`/`position`, so the params must deserialize from camelCase.
116/// Without it the request failed with a missing-field error the moment a real
117/// client called it (the #846 in-crate tests drive `sequence_model_at` directly
118/// and never deserialize these params, and the live VS Code path was not run —
119/// so the mismatch shipped latent).
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct SequenceModelParams {
123    pub text_document: tower_lsp::lsp_types::TextDocumentIdentifier,
124    pub position: tower_lsp::lsp_types::Position,
125}
126
127// -- Wire shape: a plain serde mirror of `bynk_ide::sequence::SequenceModel`,
128// -- `Span` lowered to `Range` (converted against the committed snapshot text
129// -- the caller already has — this module stays position-map-agnostic).
130
131#[derive(Debug, Clone, serde::Serialize)]
132pub struct WireSequenceModel {
133    pub participants: Vec<WireParticipant>,
134    pub messages: Vec<WireMessage>,
135    pub blocks: Vec<WireAltBlock>,
136}
137
138#[derive(Debug, Clone, serde::Serialize)]
139pub struct WireParticipant {
140    pub id: u32,
141    pub kind: &'static str,
142    pub name: String,
143    pub range: Option<tower_lsp::lsp_types::Range>,
144}
145
146#[derive(Debug, Clone, serde::Serialize)]
147pub struct WireMessage {
148    pub from: u32,
149    pub to: u32,
150    pub kind: &'static str,
151    pub label: String,
152    pub range: tower_lsp::lsp_types::Range,
153    pub block: Option<u32>,
154}
155
156#[derive(Debug, Clone, serde::Serialize)]
157pub struct WireAltBlock {
158    pub id: u32,
159    pub kind: &'static str,
160    pub branches: Vec<WireBranch>,
161    pub range: tower_lsp::lsp_types::Range,
162    pub parent: Option<u32>,
163    #[serde(rename = "parentBranch")]
164    pub parent_branch: Option<u32>,
165}
166
167#[derive(Debug, Clone, serde::Serialize)]
168pub struct WireBranch {
169    pub label: String,
170    #[serde(rename = "messageIds")]
171    pub message_ids: Vec<usize>,
172    /// The branch's rendered outcome (`Ok(view)`) — see
173    /// [`bynk_ide::sequence::Branch::reply`]. `null` on the wire when absent.
174    pub reply: Option<String>,
175}
176
177fn participant_kind_str(k: ParticipantKind) -> &'static str {
178    match k {
179        ParticipantKind::Entry => "Entry",
180        ParticipantKind::Capability => "Capability",
181        ParticipantKind::Context => "Context",
182        ParticipantKind::Agent => "Agent",
183        ParticipantKind::Actor => "Actor",
184    }
185}
186
187fn message_kind_str(k: MessageKind) -> &'static str {
188    match k {
189        MessageKind::Call => "Call",
190        MessageKind::Return => "Return",
191        MessageKind::Send => "Send",
192    }
193}
194
195fn alt_kind_str(k: AltKind) -> &'static str {
196    match k {
197        AltKind::If => "If",
198        AltKind::Match => "Match",
199        AltKind::Collapsed => "Collapsed",
200    }
201}
202
203pub fn to_wire(model: &SequenceModel, text: &str) -> WireSequenceModel {
204    WireSequenceModel {
205        participants: model
206            .participants
207            .iter()
208            .map(|p| WireParticipant {
209                id: p.id,
210                kind: participant_kind_str(p.kind),
211                name: p.name.clone(),
212                range: p.span.map(|s| crate::position::span_to_range(text, s)),
213            })
214            .collect(),
215        messages: model
216            .messages
217            .iter()
218            .map(|m| WireMessage {
219                from: m.from,
220                to: m.to,
221                kind: message_kind_str(m.kind),
222                label: m.label.clone(),
223                range: crate::position::span_to_range(text, m.span),
224                block: m.block,
225            })
226            .collect(),
227        blocks: model
228            .blocks
229            .iter()
230            .map(|b| WireAltBlock {
231                id: b.id,
232                kind: alt_kind_str(b.kind),
233                branches: b
234                    .branches
235                    .iter()
236                    .map(|br| WireBranch {
237                        label: br.label.clone(),
238                        message_ids: br.message_ids.clone(),
239                        reply: br.reply.clone(),
240                    })
241                    .collect(),
242                range: crate::position::span_to_range(text, b.span),
243                parent: b.parent,
244                parent_branch: b.parent_branch,
245            })
246            .collect(),
247    }
248}