Skip to main content

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 test_that::prelude::*;
61    use ytil_noxi::buffer::mock::MockBuffer;
62
63    use super::*;
64
65    #[rstest]
66    #[case::path_contains_blacklisted_substring(
67        &[".cargo"],
68        &[],
69        "/home/user/.cargo/registry/src/index.crates.io/crate.tar.gz",
70        "",
71        true
72    )]
73    #[case::path_not_blacklisted_and_buf_type_not_blacklisted(
74        &[".cargo"],
75        &["nofile"],
76        "/home/user/src/main.rs",
77        "",
78        false
79    )]
80    #[case::path_not_blacklisted_but_buf_type_is_blacklisted(
81        &[".cargo"],
82        &["nofile"],
83        "/home/user/src/main.rs",
84        "nofile",
85        true
86    )]
87    #[case::multiple_blacklisted_paths_and_types(
88        &[".cargo", "target"],
89        &["nofile", "grug-far"],
90        "/home/user/target/debug/main",
91        "",
92        true
93    )]
94    #[case::no_blacklists_configured(
95        &[],
96        &[],
97        "/home/user/src/main.rs",
98        "normal",
99        false
100    )]
101    #[case::path_exactly_matches_blacklisted_substring(
102        &[".cargo"],
103        &[],
104        ".cargo",
105        "",
106        true
107    )]
108    #[case::path_contains_multiple_occurrences_of_blacklisted_substring(
109        &["target"],
110        &[],
111        "/target/debug/target/release/target",
112        "",
113        true
114    )]
115    #[case::empty_path(
116        &[".cargo"],
117        &["nofile"],
118        "",
119        "",
120        false
121    )]
122    #[case::unicode_path_containing_blacklisted_substring(
123        &[".cargo"],
124        &[],
125        "/home/user/📁/.cargo/registry/🚀.tar.gz",
126        "",
127        true
128    )]
129    #[case::both_path_and_buffer_type_are_blacklisted(
130        &[".cargo"],
131        &["nofile"],
132        "/home/user/.cargo/main.rs",
133        "nofile",
134        true
135    )]
136    fn skip_diagnostic_works_as_expected(
137        #[case] blacklisted_paths: &[&str],
138        #[case] blacklisted_types: &[&str],
139        #[case] buffer_path: &str,
140        #[case] buffer_type: &str,
141        #[case] expected: bool,
142    ) {
143        let filter = TestBufferFilter::new(blacklisted_paths, blacklisted_types);
144        let buffer_with_path = create_buffer_with_path(buffer_path, buffer_type);
145
146        assert_that!(filter.skip_diagnostic(&buffer_with_path), ok(eq(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}