nvrim/diagnostics/filters/
buffer.rs

1//! Filter diagnostics based on the buffer path or type.
2//!
3//! Skips diagnostics entirely for buffers whose absolute path matches the configured blacklist entries
4//! (e.g. cargo registry), or whose type matches the configured blacklisted buffer types to prevent
5//! unwanted noise.
6
7use crate::diagnostics::filters::BufferWithPath;
8
9/// Defines filtering logic for buffers based on path and type criteria.
10///
11/// Implementations specify which buffer paths and types should be excluded from
12/// diagnostic processing to reduce noise from build artifacts and non-source files.
13pub trait BufferFilter {
14    /// Buffer path substrings for which diagnostics are skipped entirely.
15    ///
16    /// Buffers with paths containing these substrings are excluded from diagnostic processing
17    /// to avoid noise from build artifacts and dependencies (e.g. Cargo registry).
18    fn blacklisted_buf_paths(&self) -> &[&str];
19
20    /// Buffer types for which diagnostics are skipped entirely.
21    ///
22    /// Buffers with these `buftype` values are excluded from diagnostic processing
23    /// to avoid noise from non-source files (e.g. fzf-lua results, grug-far search buffers).
24    fn blacklisted_buf_types(&self) -> &[&str];
25
26    /// Checks if diagnostics should be skipped for the given buffer.
27    ///
28    /// # Errors
29    /// - Propagates [`nvim_oxi::api::Error`] from buffer type retrieval.
30    fn skip_diagnostic(&self, buffer_with_path: &BufferWithPath) -> nvim_oxi::Result<bool> {
31        if self
32            .blacklisted_buf_paths()
33            .iter()
34            .any(|bp| buffer_with_path.path.contains(bp))
35        {
36            return Ok(true);
37        }
38        let Some(buf_type) = buffer_with_path.buffer.get_buf_type() else {
39            return Ok(false);
40        };
41        Ok(self.blacklisted_buf_types().contains(&buf_type.as_str()))
42    }
43}
44
45pub struct BufferFilterImpl;
46
47impl BufferFilter for BufferFilterImpl {
48    fn blacklisted_buf_paths(&self) -> &[&str] {
49        &[".cargo"]
50    }
51
52    fn blacklisted_buf_types(&self) -> &[&str] {
53        &["nofile", "grug-far"]
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use rstest::rstest;
60    use ytil_noxi::buffer::mock::MockBuffer;
61
62    use super::*;
63
64    #[rstest]
65    #[case::path_contains_blacklisted_substring(
66        &[".cargo"],
67        &[],
68        "/home/user/.cargo/registry/src/index.crates.io/crate.tar.gz",
69        "",
70        true
71    )]
72    #[case::path_not_blacklisted_and_buf_type_not_blacklisted(
73        &[".cargo"],
74        &["nofile"],
75        "/home/user/src/main.rs",
76        "",
77        false
78    )]
79    #[case::path_not_blacklisted_but_buf_type_is_blacklisted(
80        &[".cargo"],
81        &["nofile"],
82        "/home/user/src/main.rs",
83        "nofile",
84        true
85    )]
86    #[case::multiple_blacklisted_paths_and_types(
87        &[".cargo", "target"],
88        &["nofile", "grug-far"],
89        "/home/user/target/debug/main",
90        "",
91        true
92    )]
93    #[case::no_blacklists_configured(
94        &[],
95        &[],
96        "/home/user/src/main.rs",
97        "normal",
98        false
99    )]
100    #[case::path_exactly_matches_blacklisted_substring(
101        &[".cargo"],
102        &[],
103        ".cargo",
104        "",
105        true
106    )]
107    #[case::path_contains_multiple_occurrences_of_blacklisted_substring(
108        &["target"],
109        &[],
110        "/target/debug/target/release/target",
111        "",
112        true
113    )]
114    #[case::empty_path(
115        &[".cargo"],
116        &["nofile"],
117        "",
118        "",
119        false
120    )]
121    #[case::unicode_path_containing_blacklisted_substring(
122        &[".cargo"],
123        &[],
124        "/home/user/📁/.cargo/registry/🚀.tar.gz",
125        "",
126        true
127    )]
128    #[case::both_path_and_buffer_type_are_blacklisted(
129        &[".cargo"],
130        &["nofile"],
131        "/home/user/.cargo/main.rs",
132        "nofile",
133        true
134    )]
135    fn skip_diagnostic_works_as_expected(
136        #[case] blacklisted_paths: &[&str],
137        #[case] blacklisted_types: &[&str],
138        #[case] buffer_path: &str,
139        #[case] buffer_type: &str,
140        #[case] expected: bool,
141    ) {
142        let filter = TestBufferFilter::new(blacklisted_paths, blacklisted_types);
143        let buffer_with_path = create_buffer_with_path(buffer_path, buffer_type);
144
145        assert2::let_assert!(Ok(result) = filter.skip_diagnostic(&buffer_with_path));
146        pretty_assertions::assert_eq!(result, expected);
147    }
148
149    /// Test implementation of [`BufferFilter`] with configurable blacklists.
150    struct TestBufferFilter<'a> {
151        blacklisted_paths: &'a [&'a str],
152        blacklisted_types: &'a [&'a str],
153    }
154
155    impl<'a> TestBufferFilter<'a> {
156        fn new(blacklisted_paths: &'a [&'a str], blacklisted_types: &'a [&'a str]) -> Self {
157            Self {
158                blacklisted_paths,
159                blacklisted_types,
160            }
161        }
162    }
163
164    impl BufferFilter for TestBufferFilter<'_> {
165        fn blacklisted_buf_paths(&self) -> &[&str] {
166            self.blacklisted_paths
167        }
168
169        fn blacklisted_buf_types(&self) -> &[&str] {
170            self.blacklisted_types
171        }
172    }
173
174    fn create_buffer_with_path(path: &str, buf_type: &str) -> BufferWithPath {
175        BufferWithPath {
176            buffer: Box::new(MockBuffer::with_buf_type(vec![], buf_type)),
177            path: path.to_string(),
178        }
179    }
180}