Skip to main content

nvrim/diagnostics/
formatter.rs

1//! Diagnostic formatting helpers.
2//!
3//! Converts raw LSP diagnostics plus embedded `user_data` into concise messages with source / code.
4//! Missing required fields trigger user notifications and yield `None`.
5
6use serde::Deserialize;
7
8/// Formats a diagnostic into a human-readable string.
9#[expect(
10    clippy::needless_pass_by_value,
11    reason = "nvim function binding requires owned Lua-converted arguments"
12)]
13pub fn format(diagnostic: Diagnostic) -> Option<String> {
14    let Some(msg) = get_msg(&diagnostic).map(|s| s.trim_end_matches('.').to_string()) else {
15        ytil_noxi::notify::error(format!("error missing diagnostic message | diagnostic={diagnostic:#?}"));
16        return None;
17    };
18
19    let Some(src) = get_src(&diagnostic).map(str::to_string) else {
20        ytil_noxi::notify::error(format!("error missing diagnostic source | diagnostic={diagnostic:#?}"));
21        return None;
22    };
23
24    let src_and_code = get_code(&diagnostic).map_or_else(|| src.clone(), |c| format!("{src}: {c}"));
25
26    Some(format!("▶ {msg} [{src_and_code}]"))
27}
28
29/// Represents a diagnostic from Nvim.
30#[derive(Debug, Deserialize)]
31pub struct Diagnostic {
32    /// The diagnostic code.
33    code: Option<String>,
34    /// The diagnostic message.
35    message: Option<String>,
36    /// The source of the diagnostic.
37    source: Option<String>,
38    /// Additional user data.
39    user_data: Option<UserData>,
40}
41
42ytil_noxi::impl_nvim_deserializable!(Diagnostic);
43
44/// User data associated with a diagnostic.
45#[derive(Debug, Deserialize)]
46pub struct UserData {
47    /// LSP-specific diagnostic payload injected by Nvim.
48    lsp: Option<Lsp>,
49}
50
51/// LSP data within user data.
52#[derive(Debug, Deserialize)]
53pub struct Lsp {
54    /// The diagnostic code.
55    code: Option<String>,
56    /// Additional LSP data.
57    data: Option<LspData>,
58    /// The diagnostic message.
59    message: Option<String>,
60    /// The source of the diagnostic.
61    source: Option<String>,
62}
63
64/// Additional LSP data.
65#[derive(Debug, Deserialize)]
66pub struct LspData {
67    rendered: Option<String>,
68}
69
70/// Extracts LSP diagnostic message from [`LspData::rendered`] or directly from the supplied [`Diagnostic`].
71fn get_msg(diag: &Diagnostic) -> Option<&str> {
72    diag.user_data
73        .as_ref()
74        .and_then(|user_data| {
75            user_data
76                .lsp
77                .as_ref()
78                .and_then(|lsp| {
79                    lsp.data
80                        .as_ref()
81                        .and_then(|lsp_data| lsp_data.rendered.as_deref())
82                        .or(lsp.message.as_deref())
83                })
84                .or(diag.message.as_deref())
85        })
86        .or(diag.message.as_deref())
87}
88
89/// Extracts the "source" from [`Diagnostic::user_data`] or [`Diagnostic::source`].
90fn get_src(diag: &Diagnostic) -> Option<&str> {
91    diag.user_data
92        .as_ref()
93        .and_then(|user_data| user_data.lsp.as_ref().and_then(|lsp| lsp.source.as_deref()))
94        .or(diag.source.as_deref())
95}
96
97/// Extracts the "code" from [`Diagnostic::user_data`] or [`Diagnostic::code`].
98fn get_code(diag: &Diagnostic) -> Option<&str> {
99    diag.user_data
100        .as_ref()
101        .and_then(|user_data| user_data.lsp.as_ref().and_then(|lsp| lsp.code.as_deref()))
102        .or(diag.code.as_deref())
103}