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, Hover, LabelCtx, RenderOut, WireNode,
16};
17
18/// The hook-event interface a Lex extension implements.
19///
20/// Every method has a default implementation that returns the identity
21/// (`Ok(None)`, `Ok(Vec::new())`, `()`), so an extension only needs to
22/// override the methods it cares about. An empty `impl LexHandler for Foo {}`
23/// is a no-op handler that compiles and runs.
24pub trait LexHandler: Send + Sync {
25    /// Informational notification fired during the analyse phase. No response
26    /// is expected. Use this for handlers that maintain external state
27    /// (caches, indices, link graphs).
28    fn on_label(&self, _ctx: &LabelCtx) {}
29
30    /// Returns diagnostics for a labelled node. Fires during analyse, after
31    /// resolve.
32    fn on_validate(&self, _ctx: &LabelCtx) -> Result<Vec<Diagnostic>, HandlerError> {
33        Ok(Vec::new())
34    }
35
36    /// Returns an AST replacement subtree, which the host splices into the
37    /// parent in place of the labelled node. Fires during the resolve phase,
38    /// before analyse. `Ok(None)` leaves the original node in place.
39    fn on_resolve(&self, _ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
40        Ok(None)
41    }
42
43    /// Returns the labelled node's representation in a target format. Fires
44    /// during `lexd convert` or library-driven rendering. `Ok(None)` falls
45    /// back to default rendering of the underlying node.
46    fn on_render(&self, _ctx: &LabelCtx, _fmt: Format) -> Result<Option<RenderOut>, HandlerError> {
47        Ok(None)
48    }
49
50    /// Returns hover content for a labelled node. Fires in response to
51    /// `textDocument/hover` LSP requests.
52    fn on_hover(&self, _ctx: &LabelCtx) -> Result<Option<Hover>, HandlerError> {
53        Ok(None)
54    }
55
56    /// Returns completion items for a position inside a labelled node's
57    /// params or body. Fires in response to `textDocument/completion`.
58    fn on_completion(&self, _ctx: &LabelCtx) -> Result<Vec<Completion>, HandlerError> {
59        Ok(Vec::new())
60    }
61
62    /// Returns code actions for a labelled node. Fires in response to
63    /// `textDocument/codeAction`.
64    fn on_code_action(&self, _ctx: &LabelCtx) -> Result<Vec<CodeAction>, HandlerError> {
65        Ok(Vec::new())
66    }
67}
68
69/// Errors a [`LexHandler`] method can surface.
70///
71/// A handler that hits an internal failure returns `Err(HandlerError::...)`;
72/// the host folds the error into a synthetic diagnostic at the labelled
73/// node's range and continues processing other labels. Subprocess transports
74/// map these variants onto JSON-RPC error responses with the standard
75/// reserved code ranges (`-32000..=-32099` for handler-defined; `-32601` for
76/// unsupported method/format).
77#[derive(Debug, Clone, PartialEq)]
78pub enum HandlerError {
79    /// Handler hit an internal error (panic, library failure, unexpected
80    /// state). Maps to JSON-RPC `-32603`.
81    Internal { message: String },
82    /// Handler does not support the requested operation — for example,
83    /// `on_render` was called with a format the handler does not produce.
84    /// Maps to JSON-RPC `-32601`.
85    Unsupported { detail: String },
86    /// Handler-defined error. `code` should fall in the
87    /// `-32000..=-32099` range reserved for handler use. Maps to
88    /// JSON-RPC `error` with the supplied code, message, and optional data.
89    Custom {
90        code: i32,
91        message: String,
92        data: Option<serde_json::Value>,
93    },
94}
95
96impl HandlerError {
97    /// Convenience constructor for the common case of an internal error.
98    pub fn internal(message: impl Into<String>) -> Self {
99        Self::Internal {
100            message: message.into(),
101        }
102    }
103
104    /// Convenience constructor for an unsupported operation.
105    pub fn unsupported(detail: impl Into<String>) -> Self {
106        Self::Unsupported {
107            detail: detail.into(),
108        }
109    }
110}
111
112impl std::fmt::Display for HandlerError {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            HandlerError::Internal { message } => {
116                write!(f, "handler internal error: {message}")
117            }
118            HandlerError::Unsupported { detail } => {
119                write!(f, "handler does not support: {detail}")
120            }
121            HandlerError::Custom { code, message, .. } => {
122                write!(f, "handler error {code}: {message}")
123            }
124        }
125    }
126}
127
128impl std::error::Error for HandlerError {}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::wire::{LabelCtx, NodeRef, Position, Range};
134
135    /// A no-op handler should compile with no method overrides — the
136    /// ergonomics check called out in PR 1's success criteria.
137    struct NoOp;
138    impl LexHandler for NoOp {}
139
140    fn ctx() -> LabelCtx {
141        LabelCtx {
142            label: "test.label".into(),
143            params: serde_json::json!({}),
144            body: crate::wire::AnnotationBody::None,
145            node: NodeRef {
146                kind: "annotation".into(),
147                range: Range {
148                    start: Position(0, 0),
149                    end: Position(0, 0),
150                },
151                origin: None,
152            },
153        }
154    }
155
156    #[test]
157    fn noop_handler_returns_defaults() {
158        let h = NoOp;
159        let c = ctx();
160        h.on_label(&c);
161        assert!(h.on_validate(&c).unwrap().is_empty());
162        assert!(h.on_resolve(&c).unwrap().is_none());
163        assert!(h.on_render(&c, Format::Html).unwrap().is_none());
164        assert!(h.on_hover(&c).unwrap().is_none());
165        assert!(h.on_completion(&c).unwrap().is_empty());
166        assert!(h.on_code_action(&c).unwrap().is_empty());
167    }
168
169    #[test]
170    fn handler_error_display() {
171        assert_eq!(
172            HandlerError::internal("boom").to_string(),
173            "handler internal error: boom"
174        );
175        assert_eq!(
176            HandlerError::unsupported("png").to_string(),
177            "handler does not support: png"
178        );
179        assert_eq!(
180            HandlerError::Custom {
181                code: -32001,
182                message: "custom".into(),
183                data: None,
184            }
185            .to_string(),
186            "handler error -32001: custom"
187        );
188    }
189}