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}