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