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    fn on_resolve(&self, _ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
41        Ok(None)
42    }
43
44    /// Returns the labelled node's representation in a target format. Fires
45    /// during `lexd convert` or library-driven rendering. `Ok(None)` falls
46    /// back to default rendering of the underlying node.
47    fn on_render(&self, _ctx: &LabelCtx, _fmt: Format) -> Result<Option<RenderOut>, HandlerError> {
48        Ok(None)
49    }
50
51    /// Returns hover content for a labelled node. Fires in response to
52    /// `textDocument/hover` LSP requests.
53    fn on_hover(&self, _ctx: &LabelCtx) -> Result<Option<Hover>, HandlerError> {
54        Ok(None)
55    }
56
57    /// Returns completion items for a position inside a labelled node's
58    /// params or body. Fires in response to `textDocument/completion`.
59    fn on_completion(&self, _ctx: &LabelCtx) -> Result<Vec<Completion>, HandlerError> {
60        Ok(Vec::new())
61    }
62
63    /// Returns code actions for a labelled node. Fires in response to
64    /// `textDocument/codeAction`.
65    fn on_code_action(&self, _ctx: &LabelCtx) -> Result<Vec<CodeAction>, HandlerError> {
66        Ok(Vec::new())
67    }
68
69    /// Returns the Lex-source representation of a typed AST subtree
70    /// owned by this handler's namespace — the inverse of
71    /// [`on_resolve`](Self::on_resolve), and the reverse-direction
72    /// sibling of [`on_render`](Self::on_render) for the Lex target
73    /// format.
74    ///
75    /// Phase 4a of #570 ships this trait method, the `FormatCtx` /
76    /// `LexAnnotationOut` wire types, and the
77    /// [`Registry::dispatch_format`](`lex_extension_host::registry::Registry::dispatch_format`)
78    /// entry point. Phase 4b implements `on_format` in the built-in
79    /// `lex.tabular.*` / `lex.media.*` handlers. Production call
80    /// sites in `to_lex.rs` and `lexd format` get wired in a Phase 4b
81    /// follow-up — until that lands, the hook is invocable through
82    /// the registry (tests + library embedders use it) but no
83    /// built-in pass dispatches through it yet, so a handler
84    /// implementing `on_format` will be exercised by direct
85    /// `Registry::dispatch_format` callers only.
86    ///
87    /// `Ok(None)` lets the host fall back to its built-in formatter
88    /// for the underlying node kind — there is no separate
89    /// "not handled" error code. See `comms/specs/proposals/lex-extension-wire.lex`
90    /// §4.8 for the full wire contract.
91    fn on_format(&self, _ctx: &FormatCtx) -> Result<Option<LexAnnotationOut>, HandlerError> {
92        Ok(None)
93    }
94}
95
96/// Errors a [`LexHandler`] method can surface.
97///
98/// A handler that hits an internal failure returns `Err(HandlerError::...)`;
99/// the host folds the error into a synthetic diagnostic at the labelled
100/// node's range and continues processing other labels. Subprocess transports
101/// map these variants onto JSON-RPC error responses with the standard
102/// reserved code ranges (`-32000..=-32099` for handler-defined; `-32601` for
103/// unsupported method/format).
104#[derive(Debug, Clone, PartialEq)]
105pub enum HandlerError {
106    /// Handler hit an internal error (panic, library failure, unexpected
107    /// state). Maps to JSON-RPC `-32603`.
108    Internal { message: String },
109    /// Handler does not support the requested operation — for example,
110    /// `on_render` was called with a format the handler does not produce.
111    /// Maps to JSON-RPC `-32601`.
112    Unsupported { detail: String },
113    /// Handler-defined error. `code` should fall in the
114    /// `-32000..=-32099` range reserved for handler use. Maps to
115    /// JSON-RPC `error` with the supplied code, message, and optional data.
116    Custom {
117        code: i32,
118        message: String,
119        data: Option<serde_json::Value>,
120    },
121}
122
123impl HandlerError {
124    /// Convenience constructor for the common case of an internal error.
125    pub fn internal(message: impl Into<String>) -> Self {
126        Self::Internal {
127            message: message.into(),
128        }
129    }
130
131    /// Convenience constructor for an unsupported operation.
132    pub fn unsupported(detail: impl Into<String>) -> Self {
133        Self::Unsupported {
134            detail: detail.into(),
135        }
136    }
137}
138
139impl std::fmt::Display for HandlerError {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            HandlerError::Internal { message } => {
143                write!(f, "handler internal error: {message}")
144            }
145            HandlerError::Unsupported { detail } => {
146                write!(f, "handler does not support: {detail}")
147            }
148            HandlerError::Custom { code, message, .. } => {
149                write!(f, "handler error {code}: {message}")
150            }
151        }
152    }
153}
154
155impl std::error::Error for HandlerError {}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::wire::{LabelCtx, NodeRef, Position, Range};
161
162    /// A no-op handler should compile with no method overrides — the
163    /// ergonomics check called out in PR 1's success criteria.
164    struct NoOp;
165    impl LexHandler for NoOp {}
166
167    fn ctx() -> LabelCtx {
168        LabelCtx {
169            label: "test.label".into(),
170            params: serde_json::json!({}),
171            body: crate::wire::AnnotationBody::None,
172            node: NodeRef {
173                kind: "annotation".into(),
174                range: Range {
175                    start: Position(0, 0),
176                    end: Position(0, 0),
177                },
178                origin: None,
179            },
180        }
181    }
182
183    #[test]
184    fn noop_handler_returns_defaults() {
185        let h = NoOp;
186        let c = ctx();
187        h.on_label(&c);
188        assert!(h.on_validate(&c).unwrap().is_empty());
189        assert!(h.on_resolve(&c).unwrap().is_none());
190        assert!(h.on_render(&c, Format::Html).unwrap().is_none());
191        assert!(h.on_hover(&c).unwrap().is_none());
192        assert!(h.on_completion(&c).unwrap().is_empty());
193        assert!(h.on_code_action(&c).unwrap().is_empty());
194        // on_format added in #570 Phase 4a — same Ok(None) default.
195        let format_ctx = crate::wire::FormatCtx {
196            label: "test.label".into(),
197            params: vec![],
198            node: WireNode::Paragraph {
199                range: Range {
200                    start: Position(0, 0),
201                    end: Position(0, 0),
202                },
203                origin: None,
204                inlines: vec![],
205            },
206            format_options: None,
207        };
208        assert!(h.on_format(&format_ctx).unwrap().is_none());
209    }
210
211    #[test]
212    fn handler_error_display() {
213        assert_eq!(
214            HandlerError::internal("boom").to_string(),
215            "handler internal error: boom"
216        );
217        assert_eq!(
218            HandlerError::unsupported("png").to_string(),
219            "handler does not support: png"
220        );
221        assert_eq!(
222            HandlerError::Custom {
223                code: -32001,
224                message: "custom".into(),
225                data: None,
226            }
227            .to_string(),
228            "handler error -32001: custom"
229        );
230    }
231}