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 nvim_oxi::Object;
7use nvim_oxi::conversion::FromObject;
8use nvim_oxi::lua::Poppable;
9use nvim_oxi::lua::ffi::State;
10use nvim_oxi::serde::Deserializer;
11use serde::Deserialize;
12
13/// Formats a diagnostic into a human-readable string.
14#[allow(clippy::needless_pass_by_value)]
15pub fn format(diagnostic: Diagnostic) -> Option<String> {
16    let Some(msg) = get_msg(&diagnostic).map(|s| s.trim_end_matches('.').to_string()) else {
17        ytil_noxi::notify::error(format!("error missing diagnostic message | diagnostic={diagnostic:#?}"));
18        return None;
19    };
20
21    let Some(src) = get_src(&diagnostic).map(str::to_string) else {
22        ytil_noxi::notify::error(format!("error missing diagnostic source | diagnostic={diagnostic:#?}"));
23        return None;
24    };
25
26    let src_and_code = get_code(&diagnostic).map_or_else(|| src.clone(), |c| format!("{src}: {c}"));
27
28    Some(format!("▶ {msg} [{src_and_code}]"))
29}
30
31/// Extracts LSP diagnostic message from [`LspData::rendered`] or directly from the supplied [`Diagnostic`].
32fn get_msg(diag: &Diagnostic) -> Option<&str> {
33    diag.user_data
34        .as_ref()
35        .and_then(|user_data| {
36            user_data
37                .lsp
38                .as_ref()
39                .and_then(|lsp| {
40                    lsp.data
41                        .as_ref()
42                        .and_then(|lsp_data| lsp_data.rendered.as_deref())
43                        .or(lsp.message.as_deref())
44                })
45                .or(diag.message.as_deref())
46        })
47        .or(diag.message.as_deref())
48}
49
50/// Extracts the "source" from [`Diagnostic::user_data`] or [`Diagnostic::source`].
51fn get_src(diag: &Diagnostic) -> Option<&str> {
52    diag.user_data
53        .as_ref()
54        .and_then(|user_data| user_data.lsp.as_ref().and_then(|lsp| lsp.source.as_deref()))
55        .or(diag.source.as_deref())
56}
57
58/// Extracts the "code" from [`Diagnostic::user_data`] or [`Diagnostic::code`].
59fn get_code(diag: &Diagnostic) -> Option<&str> {
60    diag.user_data
61        .as_ref()
62        .and_then(|user_data| user_data.lsp.as_ref().and_then(|lsp| lsp.code.as_deref()))
63        .or(diag.code.as_deref())
64}
65
66/// Represents a diagnostic from Nvim.
67#[derive(Debug, Deserialize)]
68pub struct Diagnostic {
69    /// The diagnostic code.
70    code: Option<String>,
71    /// The diagnostic message.
72    message: Option<String>,
73    /// The source of the diagnostic.
74    source: Option<String>,
75    /// Additional user data.
76    user_data: Option<UserData>,
77}
78
79/// Implementation of [`FromObject`] for [`Diagnostic`].
80impl FromObject for Diagnostic {
81    fn from_object(obj: Object) -> Result<Self, nvim_oxi::conversion::Error> {
82        Self::deserialize(Deserializer::new(obj)).map_err(Into::into)
83    }
84}
85
86/// Implementation of [`Poppable`] for [`Diagnostic`].
87impl Poppable for Diagnostic {
88    unsafe fn pop(lstate: *mut State) -> Result<Self, nvim_oxi::lua::Error> {
89        unsafe {
90            let obj = Object::pop(lstate)?;
91            Self::from_object(obj).map_err(nvim_oxi::lua::Error::pop_error_from_err::<Self, _>)
92        }
93    }
94}
95
96/// User data associated with a diagnostic.
97#[derive(Debug, Deserialize)]
98pub struct UserData {
99    /// LSP-specific diagnostic payload injected by Nvim.
100    lsp: Option<Lsp>,
101}
102
103/// LSP data within user data.
104#[derive(Debug, Deserialize)]
105pub struct Lsp {
106    /// The diagnostic code.
107    code: Option<String>,
108    /// Additional LSP data.
109    data: Option<LspData>,
110    /// The diagnostic message.
111    message: Option<String>,
112    /// The source of the diagnostic.
113    source: Option<String>,
114}
115
116/// Additional LSP data.
117#[derive(Debug, Deserialize)]
118pub struct LspData {
119    rendered: Option<String>,
120}