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}