nvrim/diagnostics/filters/
related_info.rs

1//! Filter for deduplicating diagnostics based on related information arrays.
2//!
3//! Extracts `user_data.lsp.relatedInformation` entries and skips root diagnostics whose rendered
4//! information is already represented, reducing noise (especially repeated hints).
5
6use color_eyre::eyre::Context;
7use nvim_oxi::Array;
8use nvim_oxi::Dictionary;
9use nvim_oxi::ObjectKind;
10use nvim_oxi::conversion::FromObject;
11use ytil_noxi::dict::DictionaryExt;
12
13use crate::diagnostics::filters::BufferWithPath;
14use crate::diagnostics::filters::DiagnosticsFilter;
15
16/// Filters out diagnostics already represented by other ones
17/// (e.g. HINTs pointing to a location already mentioned by other ERROR's rendered message)
18pub struct RelatedInfoFilter {
19    /// The set of already-seen related infos extracted from LSP diagnostics.
20    /// Used to skip duplicate root diagnostics that only repeat information.
21    rel_infos: Vec<RelatedInfo>,
22}
23
24impl RelatedInfoFilter {
25    /// Creates a new [`RelatedInfoFilter`] from LSP diagnostics.
26    ///
27    /// # Errors
28    /// - Extracting related information arrays fails (missing key or wrong type).
29    pub fn new(lsp_diags: &[Dictionary]) -> color_eyre::Result<Self> {
30        Ok(Self {
31            rel_infos: Self::get_related_infos(lsp_diags)?,
32        })
33    }
34
35    /// Get the [`RelatedInfo`] of an LSP diagnostic represented by a [`Dictionary`].
36    ///
37    /// # Errors
38    /// - Traversing diagnostics fails (unexpected value kinds or conversion errors).
39    fn get_related_infos(lsp_diags: &[Dictionary]) -> color_eyre::Result<Vec<RelatedInfo>> {
40        let mut out = vec![];
41        for lsp_diag in lsp_diags {
42            // Not all LSPs have "user_data.lsp.relatedInformation"; skip those missing it
43            let Some(lsp) = lsp_diag.get_dict(&["user_data", "lsp"])? else {
44                continue;
45            };
46            let rel_infos_key = "relatedInformation";
47            let Some(rel_infos) = lsp.get(rel_infos_key) else {
48                continue;
49            };
50
51            let rel_infos = Array::from_object(rel_infos.clone()).with_context(|| {
52                ytil_noxi::extract::unexpected_kind_error_msg(rel_infos, rel_infos_key, &lsp, ObjectKind::Array)
53            })?;
54            for rel_info in rel_infos {
55                let rel_info = Dictionary::try_from(rel_info)?;
56                out.push(RelatedInfo::from_related_info(&rel_info)?);
57            }
58        }
59        Ok(out)
60    }
61}
62
63impl DiagnosticsFilter for RelatedInfoFilter {
64    /// Returns true if the diagnostic is related information already covered.
65    ///
66    /// # Errors
67    /// - Building the candidate related info shape from the diagnostic fails.
68    fn skip_diagnostic(&self, _buf: &BufferWithPath, lsp_diag: &Dictionary) -> color_eyre::Result<bool> {
69        if self.rel_infos.is_empty() {
70            return Ok(false);
71        }
72        // All LSPs diagnostics should be deserializable into [`RelatedInfo`]
73        let rel_info = RelatedInfo::from_lsp_diagnostic(lsp_diag)?;
74        if self.rel_infos.contains(&rel_info) {
75            return Ok(true);
76        }
77        Ok(false)
78    }
79}
80
81/// Common shape of a root LSP diagnostic and the elements of its "`user_data.lsp.relatedInformation`".
82#[derive(Eq, PartialEq)]
83struct RelatedInfo {
84    /// The starting column number.
85    col: i64,
86    /// The ending column number.
87    end_col: i64,
88    /// The ending line number.
89    end_lnum: i64,
90    /// The starting line number.
91    lnum: i64,
92    /// The diagnostic message.
93    message: String,
94}
95
96impl RelatedInfo {
97    /// Create a [`RelatedInfo`] from a root LSP diagnostic.
98    ///
99    /// # Errors
100    /// - Required keys (`message`, `lnum`, `col`, `end_lnum`, `end_col`) are missing or of unexpected type.
101    fn from_lsp_diagnostic(lsp_diagnostic: &Dictionary) -> color_eyre::Result<Self> {
102        Ok(Self {
103            message: lsp_diagnostic.get_t::<nvim_oxi::String>("message")?,
104            lnum: lsp_diagnostic.get_t::<nvim_oxi::Integer>("lnum")?,
105            col: lsp_diagnostic.get_t::<nvim_oxi::Integer>("col")?,
106            end_lnum: lsp_diagnostic.get_t::<nvim_oxi::Integer>("end_lnum")?,
107            end_col: lsp_diagnostic.get_t::<nvim_oxi::Integer>("end_col")?,
108        })
109    }
110
111    /// Create a [`RelatedInfo`] from an element of an LSP diagnostic "`user_data.lsp.relatedInformation`" section.
112    ///
113    /// # Errors
114    /// - Required nested keys (range.start, range.end, message, line/character) are missing or wrong type.
115    fn from_related_info(rel_info: &Dictionary) -> color_eyre::Result<Self> {
116        let (start, end) = {
117            let range_query = ["location", "range"];
118            let range = rel_info.get_required_dict(&range_query)?;
119
120            let start_query = ["start"];
121            let end_query = ["end"];
122            (
123                range.get_required_dict(&start_query)?,
124                range.get_required_dict(&end_query)?,
125            )
126        };
127
128        Ok(Self {
129            message: rel_info.get_t::<nvim_oxi::String>("message")?,
130            lnum: start.get_t::<nvim_oxi::Integer>("line")?,
131            col: start.get_t::<nvim_oxi::Integer>("character")?,
132            end_lnum: end.get_t::<nvim_oxi::Integer>("line")?,
133            end_col: end.get_t::<nvim_oxi::Integer>("character")?,
134        })
135    }
136}