Skip to main content

lex_extension/
handler.rs

1//! The [`LexHandler`] trait — the protocol's source of truth.
2//!
3//! Native handlers (built-ins, in-process Rust embedders) `impl` this trait
4//! directly. Subprocess and WASM transports are delivered as generic adapters
5//! that `impl` the same trait by serialising calls to JSON-RPC or component
6//! imports respectively.
7//!
8//! Methods that produce non-trivial output return
9//! `Result<Option<T>, HandlerError>`. The `Result` distinguishes "I hit an
10//! error you should surface as a diagnostic" from "I succeeded but have
11//! nothing to contribute"; the inner `Option`/`Vec` covers the latter.
12//! [`LexHandler::on_label`] returns `()` because it is a notification.
13
14use crate::wire::{
15    CodeAction, Completion, Diagnostic, Format, FormatCtx, Hover, LabelCtx, LexAnnotationOut,
16    RenderOut, WireNode,
17};
18
19/// The hook-event interface a Lex extension implements.
20///
21/// Every method has a default implementation that returns the identity
22/// (`Ok(None)`, `Ok(Vec::new())`, `()`), so an extension only needs to
23/// override the methods it cares about. An empty `impl LexHandler for Foo {}`
24/// is a no-op handler that compiles and runs.
25pub trait LexHandler: Send + Sync {
26    /// Informational notification fired during the analyse phase. No response
27    /// is expected. Use this for handlers that maintain external state
28    /// (caches, indices, link graphs).
29    fn on_label(&self, _ctx: &LabelCtx) {}
30
31    /// Returns diagnostics for a labelled node. Fires during analyse, after
32    /// resolve.
33    fn on_validate(&self, _ctx: &LabelCtx) -> Result<Vec<Diagnostic>, HandlerError> {
34        Ok(Vec::new())
35    }
36
37    /// Returns an AST replacement subtree, which the host splices into the
38    /// parent in place of the labelled node. Fires during the resolve phase,
39    /// before analyse. `Ok(None)` leaves the original node in place.
40    ///
41    /// `on_resolve` is the AST-substitution lifecycle: the canonical example
42    /// is `lex.include`, which splices the resolved file's content into the
43    /// host document. Verbatim labels that hydrate into typed IR nodes
44    /// (`lex.tabular.table`, `lex.media.*`) belong on
45    /// [`on_ir_build`](Self::on_ir_build) instead — that hook is the
46    /// IR-construction lifecycle and is invoked during `from_lex` IR build.
47    fn on_resolve(&self, _ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
48        Ok(None)
49    }
50
51    /// Returns a typed wire node consumed by the host while building its
52    /// in-memory IR from the parsed source. Fires during IR construction
53    /// (`from_lex`), strictly after parsing and strictly before render.
54    /// `Ok(None)` falls back to the host's generic verbatim/annotation IR.
55    ///
56    /// This is the lifecycle hook for **content-typing** labels — the
57    /// canonical examples are `lex.tabular.table` (verbatim body → typed
58    /// `WireNode::Table`) and `lex.media.{image,video,audio}` (params →
59    /// typed `WireNode::Image|Video|Audio`). Pair an `on_ir_build` hook
60    /// with an [`on_render`](Self::on_render) hook on the same schema to
61    /// give one label both an IR shape and per-format serialization
62    /// behaviour through the unified registry surface (#615).
63    ///
64    /// IR-build hooks do **not** receive the host's lex-core AST: they
65    /// see only the parsed verbatim payload (label + params + body) via
66    /// [`LabelCtx`]. Coupling content-typing to the IR phase rather than
67    /// to parsing keeps a buggy or slow handler from corrupting the
68    /// parser, and gives extension authors a single registration point
69    /// for both lifecycle phases.
70    fn on_ir_build(&self, _ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
71        Ok(None)
72    }
73
74    /// Returns the labelled node's representation in a target format. Fires
75    /// during `lexd convert` or library-driven rendering. `Ok(None)` falls
76    /// back to default rendering of the underlying node.
77    fn on_render(&self, _ctx: &LabelCtx, _fmt: Format) -> Result<Option<RenderOut>, HandlerError> {
78        Ok(None)
79    }
80
81    /// Returns hover content for a labelled node. Fires in response to
82    /// `textDocument/hover` LSP requests.
83    fn on_hover(&self, _ctx: &LabelCtx) -> Result<Option<Hover>, HandlerError> {
84        Ok(None)
85    }
86
87    /// Returns completion items for a position inside a labelled node's
88    /// params or body. Fires in response to `textDocument/completion`.
89    fn on_completion(&self, _ctx: &LabelCtx) -> Result<Vec<Completion>, HandlerError> {
90        Ok(Vec::new())
91    }
92
93    /// Returns code actions for a labelled node. Fires in response to
94    /// `textDocument/codeAction`.
95    fn on_code_action(&self, _ctx: &LabelCtx) -> Result<Vec<CodeAction>, HandlerError> {
96        Ok(Vec::new())
97    }
98
99    /// Returns the Lex-source representation of a typed AST subtree
100    /// owned by this handler's namespace — the inverse of
101    /// [`on_resolve`](Self::on_resolve), and the reverse-direction
102    /// sibling of [`on_render`](Self::on_render) for the Lex target
103    /// format.
104    ///
105    /// Phase 4a of #570 ships this trait method, the `FormatCtx` /
106    /// `LexAnnotationOut` wire types, and the
107    /// [`Registry::dispatch_format`](`lex_extension_host::registry::Registry::dispatch_format`)
108    /// entry point. Phase 4b implements `on_format` in the built-in
109    /// `lex.tabular.*` / `lex.media.*` handlers. Production call
110    /// sites in `to_lex.rs` and `lexd format` get wired in a Phase 4b
111    /// follow-up — until that lands, the hook is invocable through
112    /// the registry (tests + library embedders use it) but no
113    /// built-in pass dispatches through it yet, so a handler
114    /// implementing `on_format` will be exercised by direct
115    /// `Registry::dispatch_format` callers only.
116    ///
117    /// `Ok(None)` lets the host fall back to its built-in formatter
118    /// for the underlying node kind — there is no separate
119    /// "not handled" error code. See `comms/specs/proposals/lex-extension-wire.lex`
120    /// §4.8 for the full wire contract.
121    fn on_format(&self, _ctx: &FormatCtx) -> Result<Option<LexAnnotationOut>, HandlerError> {
122        Ok(None)
123    }
124}
125
126/// Errors a [`LexHandler`] method can surface.
127///
128/// A handler that hits an internal failure returns `Err(HandlerError::...)`;
129/// the host folds the error into a synthetic diagnostic at the labelled
130/// node's range and continues processing other labels. Subprocess transports
131/// map these variants onto JSON-RPC error responses with the standard
132/// reserved code ranges (`-32000..=-32099` for handler-defined; `-32601` for
133/// unsupported method/format).
134#[derive(Debug, Clone, PartialEq)]
135pub enum HandlerError {
136    /// Handler hit an internal error (panic, library failure, unexpected
137    /// state). Maps to JSON-RPC `-32603`.
138    Internal { message: String },
139    /// Handler does not support the requested operation — for example,
140    /// `on_render` was called with a format the handler does not produce.
141    /// Maps to JSON-RPC `-32601`.
142    Unsupported { detail: String },
143    /// Handler-defined error. `code` should fall in the
144    /// `-32000..=-32099` range reserved for handler use. Maps to
145    /// JSON-RPC `error` with the supplied code, message, and optional data.
146    Custom {
147        code: i32,
148        message: String,
149        data: Option<serde_json::Value>,
150    },
151}
152
153impl HandlerError {
154    /// Convenience constructor for the common case of an internal error.
155    pub fn internal(message: impl Into<String>) -> Self {
156        Self::Internal {
157            message: message.into(),
158        }
159    }
160
161    /// Convenience constructor for an unsupported operation.
162    pub fn unsupported(detail: impl Into<String>) -> Self {
163        Self::Unsupported {
164            detail: detail.into(),
165        }
166    }
167}
168
169impl std::fmt::Display for HandlerError {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self {
172            HandlerError::Internal { message } => {
173                write!(f, "handler internal error: {message}")
174            }
175            HandlerError::Unsupported { detail } => {
176                write!(f, "handler does not support: {detail}")
177            }
178            HandlerError::Custom { code, message, .. } => {
179                write!(f, "handler error {code}: {message}")
180            }
181        }
182    }
183}
184
185impl std::error::Error for HandlerError {}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::wire::{LabelCtx, NodeRef, Position, Range};
191
192    /// A no-op handler should compile with no method overrides — the
193    /// ergonomics check called out in PR 1's success criteria.
194    struct NoOp;
195    impl LexHandler for NoOp {}
196
197    fn ctx() -> LabelCtx {
198        LabelCtx {
199            label: "test.label".into(),
200            params: serde_json::json!({}),
201            body: crate::wire::AnnotationBody::None,
202            node: NodeRef {
203                kind: "annotation".into(),
204                range: Range {
205                    start: Position(0, 0),
206                    end: Position(0, 0),
207                },
208                origin: None,
209            },
210        }
211    }
212
213    #[test]
214    fn noop_handler_returns_defaults() {
215        let h = NoOp;
216        let c = ctx();
217        h.on_label(&c);
218        assert!(h.on_validate(&c).unwrap().is_empty());
219        assert!(h.on_resolve(&c).unwrap().is_none());
220        assert!(h.on_ir_build(&c).unwrap().is_none());
221        assert!(h.on_render(&c, Format::Html).unwrap().is_none());
222        assert!(h.on_hover(&c).unwrap().is_none());
223        assert!(h.on_completion(&c).unwrap().is_empty());
224        assert!(h.on_code_action(&c).unwrap().is_empty());
225        // on_format added in #570 Phase 4a — same Ok(None) default.
226        let format_ctx = crate::wire::FormatCtx {
227            label: "test.label".into(),
228            params: vec![],
229            node: WireNode::Paragraph {
230                range: Range {
231                    start: Position(0, 0),
232                    end: Position(0, 0),
233                },
234                origin: None,
235                inlines: vec![],
236            },
237            format_options: None,
238        };
239        assert!(h.on_format(&format_ctx).unwrap().is_none());
240    }
241
242    #[test]
243    fn handler_error_display() {
244        assert_eq!(
245            HandlerError::internal("boom").to_string(),
246            "handler internal error: boom"
247        );
248        assert_eq!(
249            HandlerError::unsupported("png").to_string(),
250            "handler does not support: png"
251        );
252        assert_eq!(
253            HandlerError::Custom {
254                code: -32001,
255                message: "custom".into(),
256                data: None,
257            }
258            .to_string(),
259            "handler error -32001: custom"
260        );
261    }
262}