Skip to main content

lex_extension/wire/
payload.rs

1//! Hook response payloads — diagnostics, render output, hover, completions,
2//! code actions.
3
4use serde::{Deserialize, Serialize};
5
6use super::ast::WireNode;
7use super::range::Range;
8
9/// A diagnostic returned by `on_validate`.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct Diagnostic {
12    pub severity: DiagnosticSeverity,
13    pub message: String,
14    pub range: Range,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub code: Option<String>,
17    #[serde(default, skip_serializing_if = "Vec::is_empty")]
18    pub related: Vec<RelatedDiagnostic>,
19}
20
21/// A diagnostic linked to another location (e.g., the definition the
22/// diagnostic is about).
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct RelatedDiagnostic {
25    pub message: String,
26    pub range: Range,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub uri: Option<String>,
29}
30
31/// Diagnostic severity.
32///
33/// Forward compatibility: unknown wire values deserialise as
34/// [`DiagnosticSeverity::Info`]. Adding new variants is non-breaking
35/// (`#[non_exhaustive]`).
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
37#[serde(rename_all = "snake_case")]
38#[non_exhaustive]
39pub enum DiagnosticSeverity {
40    Error,
41    Warning,
42    Info,
43    Hint,
44}
45
46impl<'de> Deserialize<'de> for DiagnosticSeverity {
47    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
48    where
49        D: serde::Deserializer<'de>,
50    {
51        let s = String::deserialize(deserializer)?;
52        Ok(match s.as_str() {
53            "error" => Self::Error,
54            "warning" => Self::Warning,
55            "info" => Self::Info,
56            "hint" => Self::Hint,
57            _ => Self::Info,
58        })
59    }
60}
61
62/// The result of `on_render`. Either a target-format string snippet or a
63/// wire AST in a tree-shaped target's vocabulary.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[serde(tag = "kind", rename_all = "snake_case")]
66pub enum RenderOut {
67    /// String-shaped target (HTML, LaTeX, Markdown).
68    String { string: String },
69    /// Tree-shaped target (intermediate AST, namespace-defined format).
70    WireAst { ast: WireNode },
71}
72
73/// Hover content returned by `on_hover`.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Hover {
76    pub contents: String,
77    pub format: HoverFormat,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub range: Option<Range>,
80}
81
82/// Hover content format.
83///
84/// Forward compatibility: unknown wire values deserialise as
85/// [`HoverFormat::Plaintext`]. Adding new variants is non-breaking.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
87#[serde(rename_all = "snake_case")]
88#[non_exhaustive]
89pub enum HoverFormat {
90    Plaintext,
91    Markdown,
92}
93
94impl<'de> Deserialize<'de> for HoverFormat {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: serde::Deserializer<'de>,
98    {
99        let s = String::deserialize(deserializer)?;
100        Ok(match s.as_str() {
101            "plaintext" => Self::Plaintext,
102            "markdown" => Self::Markdown,
103            _ => Self::Plaintext,
104        })
105    }
106}
107
108/// One completion item returned by `on_completion`.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct Completion {
111    pub label: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub detail: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub doc: Option<String>,
116    pub insert: String,
117    pub kind: CompletionKind,
118}
119
120/// Completion item kind.
121///
122/// Forward compatibility: unknown wire values deserialise as
123/// [`CompletionKind::Value`]. Adding new variants is non-breaking.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
125#[serde(rename_all = "snake_case")]
126#[non_exhaustive]
127pub enum CompletionKind {
128    Value,
129    Param,
130    Namespace,
131    Snippet,
132}
133
134impl<'de> Deserialize<'de> for CompletionKind {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: serde::Deserializer<'de>,
138    {
139        let s = String::deserialize(deserializer)?;
140        Ok(match s.as_str() {
141            "value" => Self::Value,
142            "param" => Self::Param,
143            "namespace" => Self::Namespace,
144            "snippet" => Self::Snippet,
145            _ => Self::Value,
146        })
147    }
148}
149
150/// One code action returned by `on_code_action`.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct CodeAction {
153    pub title: String,
154    pub kind: CodeActionKind,
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub edits: Vec<TextEdit>,
157}
158
159/// Code-action kind.
160///
161/// Forward compatibility: unknown wire values deserialise as
162/// [`CodeActionKind::Refactor`]. Adding new variants is non-breaking.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
164#[serde(rename_all = "snake_case")]
165#[non_exhaustive]
166pub enum CodeActionKind {
167    Quickfix,
168    Refactor,
169    Source,
170}
171
172impl<'de> Deserialize<'de> for CodeActionKind {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: serde::Deserializer<'de>,
176    {
177        let s = String::deserialize(deserializer)?;
178        Ok(match s.as_str() {
179            "quickfix" => Self::Quickfix,
180            "refactor" => Self::Refactor,
181            "source" => Self::Source,
182            _ => Self::Refactor,
183        })
184    }
185}
186
187/// A textual edit applied as part of a code action.
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct TextEdit {
190    pub range: Range,
191    pub new_text: String,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub uri: Option<String>,
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::wire::range::Position;
200
201    fn r(s_l: u32, s_c: u32, e_l: u32, e_c: u32) -> Range {
202        Range::new(Position::new(s_l, s_c), Position::new(e_l, e_c))
203    }
204
205    #[test]
206    fn diagnostic_round_trips() {
207        let d = Diagnostic {
208            severity: DiagnosticSeverity::Error,
209            message: "oops".into(),
210            range: r(0, 0, 0, 5),
211            code: Some("E001".into()),
212            related: vec![],
213        };
214        let s = serde_json::to_string(&d).unwrap();
215        let back: Diagnostic = serde_json::from_str(&s).unwrap();
216        assert_eq!(back, d);
217    }
218
219    #[test]
220    fn render_out_string_round_trips() {
221        let r0 = RenderOut::String {
222            string: "<p>hi</p>".into(),
223        };
224        let s = serde_json::to_string(&r0).unwrap();
225        assert!(s.contains(r#""kind":"string""#));
226        let back: RenderOut = serde_json::from_str(&s).unwrap();
227        assert_eq!(back, r0);
228    }
229
230    #[test]
231    fn render_out_wire_ast_round_trips() {
232        let r0 = RenderOut::WireAst {
233            ast: WireNode::Paragraph {
234                range: r(0, 0, 0, 5),
235                origin: None,
236                inlines: vec![],
237            },
238        };
239        let s = serde_json::to_string(&r0).unwrap();
240        assert!(s.contains(r#""kind":"wire_ast""#));
241        let back: RenderOut = serde_json::from_str(&s).unwrap();
242        assert_eq!(back, r0);
243    }
244
245    #[test]
246    fn hover_round_trips() {
247        let h = Hover {
248            contents: "**bold**".into(),
249            format: HoverFormat::Markdown,
250            range: Some(r(0, 0, 0, 5)),
251        };
252        let s = serde_json::to_string(&h).unwrap();
253        let back: Hover = serde_json::from_str(&s).unwrap();
254        assert_eq!(back, h);
255    }
256
257    #[test]
258    fn completion_round_trips() {
259        let c = Completion {
260            label: "foo".into(),
261            detail: Some("Foo the bar".into()),
262            doc: None,
263            insert: "foo".into(),
264            kind: CompletionKind::Param,
265        };
266        let s = serde_json::to_string(&c).unwrap();
267        let back: Completion = serde_json::from_str(&s).unwrap();
268        assert_eq!(back, c);
269    }
270
271    #[test]
272    fn code_action_round_trips() {
273        let a = CodeAction {
274            title: "Add missing footnote".into(),
275            kind: CodeActionKind::Quickfix,
276            edits: vec![TextEdit {
277                range: r(10, 0, 10, 0),
278                new_text: "[^1]: ...\n".into(),
279                uri: None,
280            }],
281        };
282        let s = serde_json::to_string(&a).unwrap();
283        let back: CodeAction = serde_json::from_str(&s).unwrap();
284        assert_eq!(back, a);
285    }
286
287    #[test]
288    fn unknown_severity_falls_back_to_info() {
289        let s: DiagnosticSeverity = serde_json::from_str(r#""trace""#).unwrap();
290        assert_eq!(s, DiagnosticSeverity::Info);
291    }
292
293    #[test]
294    fn unknown_completion_kind_falls_back_to_value() {
295        let k: CompletionKind = serde_json::from_str(r#""template""#).unwrap();
296        assert_eq!(k, CompletionKind::Value);
297    }
298
299    #[test]
300    fn unknown_code_action_kind_falls_back_to_refactor() {
301        let k: CodeActionKind = serde_json::from_str(r#""rename""#).unwrap();
302        assert_eq!(k, CodeActionKind::Refactor);
303    }
304
305    #[test]
306    fn unknown_hover_format_falls_back_to_plaintext() {
307        let f: HoverFormat = serde_json::from_str(r#""asciidoc""#).unwrap();
308        assert_eq!(f, HoverFormat::Plaintext);
309    }
310}