Skip to main content

nvrim/diagnostics/filters/
lsps.rs

1//! Filter diagnostics based on LSP source and buffer path.
2//!
3//! Provides the [`LspFilter`] trait for filtering diagnostics by LSP source and buffer path,
4//! along with implementations for specific LSPs like Harper and Typos.
5
6use nvim_oxi::Dictionary;
7use ytil_noxi::dict::DictionaryExt;
8
9pub mod harper_ls;
10pub mod typos_lsp;
11
12/// Output of diagnostic message extraction or skip decision.
13#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
14pub enum GetDiagMsgOutput {
15    /// Diagnostic message extracted successfully.
16    Msg(String),
17    /// Skip this diagnostic.
18    Skip,
19}
20
21/// Common interface for LSP-specific diagnostic filters.
22///
23/// Provides utilities for path and source matching before message extraction.
24pub trait LspFilter {
25    /// Optional buffer path substring required for filtering.
26    ///
27    /// If present, filtering only applies to buffers containing this substring.
28    fn path_substring(&self) -> Option<&str>;
29
30    /// LSP source name for this filter.
31    fn source(&self) -> &str;
32
33    /// Extract diagnostic message or decide to skip.
34    ///
35    /// Checks path substring and source match, then extracts message if applicable.
36    ///
37    /// # Errors
38    /// - Missing or invalid "source" key.
39    /// - Missing or invalid "message" key.
40    fn get_diag_msg_or_skip(&self, buf_path: &str, lsp_diag: &Dictionary) -> rootcause::Result<GetDiagMsgOutput> {
41        if self
42            .path_substring()
43            .is_some_and(|path_substring| !buf_path.contains(path_substring))
44        {
45            return Ok(GetDiagMsgOutput::Skip);
46        }
47        let maybe_diag_source = lsp_diag.get_opt_t::<nvim_oxi::String>("source")?;
48        if maybe_diag_source.is_none_or(|diag_source| !diag_source.contains(self.source())) {
49            return Ok(GetDiagMsgOutput::Skip);
50        }
51        Ok(GetDiagMsgOutput::Msg(lsp_diag.get_t::<nvim_oxi::String>("message")?))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use test_that::prelude::*;
58
59    use super::*;
60
61    #[test]
62    fn test_get_diag_msg_or_skip_when_buf_path_not_matched_returns_skip() {
63        let filter = TestFilter {
64            source: "Test",
65            path_substring: Some("src/"),
66        };
67        let diag = dict! {
68            source: "Test",
69            message: "some message",
70        };
71        assert_that!(
72            filter.get_diag_msg_or_skip("tests/main.rs", &diag),
73            ok(eq(GetDiagMsgOutput::Skip))
74        );
75    }
76
77    #[test]
78    fn test_get_diag_msg_or_skip_when_buf_path_matched_but_source_none_returns_skip() {
79        let filter = TestFilter {
80            source: "Test",
81            path_substring: Some("src/"),
82        };
83        let diag = dict! {
84            message: "some message",
85        };
86        assert_that!(
87            filter.get_diag_msg_or_skip("src/main.rs", &diag),
88            ok(eq(GetDiagMsgOutput::Skip))
89        );
90    }
91
92    #[test]
93    fn test_get_diag_msg_or_skip_when_buf_path_matched_but_source_mismatch_returns_skip() {
94        let filter = TestFilter {
95            source: "Test",
96            path_substring: Some("src/"),
97        };
98        let diag = dict! {
99            source: "Other",
100            message: "some message",
101        };
102        assert_that!(
103            filter.get_diag_msg_or_skip("src/main.rs", &diag),
104            ok(eq(GetDiagMsgOutput::Skip))
105        );
106    }
107
108    #[test]
109    fn test_get_diag_msg_or_skip_when_buf_path_and_source_matches_returns_msg() {
110        let filter = TestFilter {
111            source: "Test",
112            path_substring: Some("src/"),
113        };
114        let diag = dict! {
115            source: "Test",
116            message: "some message",
117        };
118        assert_that!(
119            filter.get_diag_msg_or_skip("src/main.rs", &diag),
120            ok(eq(GetDiagMsgOutput::Msg("some message".to_string())))
121        );
122    }
123
124    #[test]
125    fn test_get_diag_msg_or_skip_when_no_buf_path_and_source_matches_returns_msg() {
126        let filter = TestFilter {
127            source: "Test",
128            path_substring: None,
129        };
130        let diag = dict! {
131            source: "Test",
132            message: "another message",
133        };
134        assert_that!(
135            filter.get_diag_msg_or_skip("any/path.rs", &diag),
136            ok(eq(GetDiagMsgOutput::Msg("another message".to_string())))
137        );
138    }
139
140    #[test]
141    fn test_get_diag_msg_or_skip_when_source_contains_filter_source_returns_msg() {
142        let filter = TestFilter {
143            source: "Test",
144            path_substring: None,
145        };
146        let diag = dict! {
147            source: "TestLSP",
148            message: "some message",
149        };
150        assert_that!(
151            filter.get_diag_msg_or_skip("any/path.rs", &diag),
152            ok(eq(GetDiagMsgOutput::Msg("some message".to_string())))
153        );
154    }
155
156    struct TestFilter {
157        source: &'static str,
158        path_substring: Option<&'static str>,
159    }
160
161    impl LspFilter for TestFilter {
162        fn path_substring(&self) -> Option<&str> {
163            self.path_substring
164        }
165
166        fn source(&self) -> &str {
167            self.source
168        }
169    }
170}