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 as _;
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 super::*;
58
59    #[test]
60    fn get_diag_msg_or_skip_when_buf_path_not_matched_returns_skip() {
61        let filter = TestFilter {
62            source: "Test",
63            path_substring: Some("src/"),
64        };
65        let diag = dict! {
66            source: "Test",
67            message: "some message",
68        };
69        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("tests/main.rs", &diag));
70        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Skip);
71    }
72
73    #[test]
74    fn get_diag_msg_or_skip_when_buf_path_matched_but_source_none_returns_skip() {
75        let filter = TestFilter {
76            source: "Test",
77            path_substring: Some("src/"),
78        };
79        let diag = dict! {
80            message: "some message",
81        };
82        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("src/main.rs", &diag));
83        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Skip);
84    }
85
86    #[test]
87    fn get_diag_msg_or_skip_when_buf_path_matched_but_source_mismatch_returns_skip() {
88        let filter = TestFilter {
89            source: "Test",
90            path_substring: Some("src/"),
91        };
92        let diag = dict! {
93            source: "Other",
94            message: "some message",
95        };
96        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("src/main.rs", &diag));
97        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Skip);
98    }
99
100    #[test]
101    fn get_diag_msg_or_skip_when_buf_path_and_source_matches_returns_msg() {
102        let filter = TestFilter {
103            source: "Test",
104            path_substring: Some("src/"),
105        };
106        let diag = dict! {
107            source: "Test",
108            message: "some message",
109        };
110        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("src/main.rs", &diag));
111        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Msg("some message".to_string()));
112    }
113
114    #[test]
115    fn get_diag_msg_or_skip_when_no_buf_path_and_source_matches_returns_msg() {
116        let filter = TestFilter {
117            source: "Test",
118            path_substring: None,
119        };
120        let diag = dict! {
121            source: "Test",
122            message: "another message",
123        };
124        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("any/path.rs", &diag));
125        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Msg("another message".to_string()));
126    }
127
128    #[test]
129    fn get_diag_msg_or_skip_when_source_contains_filter_source_returns_msg() {
130        let filter = TestFilter {
131            source: "Test",
132            path_substring: None,
133        };
134        let diag = dict! {
135            source: "TestLSP",
136            message: "some message",
137        };
138        assert2::assert!(let Ok(result) = filter.get_diag_msg_or_skip("any/path.rs", &diag));
139        pretty_assertions::assert_eq!(result, GetDiagMsgOutput::Msg("some message".to_string()));
140    }
141
142    struct TestFilter {
143        source: &'static str,
144        path_substring: Option<&'static str>,
145    }
146
147    impl LspFilter for TestFilter {
148        fn path_substring(&self) -> Option<&str> {
149            self.path_substring
150        }
151
152        fn source(&self) -> &str {
153            self.source
154        }
155    }
156}