nvrim/diagnostics/
filters.rs1use nvim_oxi::Dictionary;
7use nvim_oxi::api::Buffer;
8use rootcause::bail;
9use ytil_noxi::buffer::BufferExt;
10use ytil_noxi::dict::DictionaryExt;
11
12use crate::diagnostics::filters::lsps::harper_ls::HarperLsFilter;
13use crate::diagnostics::filters::lsps::typos_lsp::TyposLspFilter;
14use crate::diagnostics::filters::related_info::RelatedInfoFilter;
15
16pub mod buffer;
17pub mod lsps;
18pub mod related_info;
19
20pub struct BufferWithPath {
22 buffer: Box<dyn BufferExt>,
24 path: String,
26}
27
28impl BufferWithPath {
29 pub fn path(&self) -> &str {
30 &self.path
31 }
32}
33
34impl TryFrom<Buffer> for BufferWithPath {
35 type Error = rootcause::Report;
36
37 fn try_from(value: Buffer) -> Result<Self, Self::Error> {
38 let path = value.get_name().map(|s| s.to_string_lossy().into_owned())?;
39 Ok(Self {
40 path,
41 buffer: Box::new(value),
42 })
43 }
44}
45
46pub trait DiagnosticsFilter {
48 fn skip_diagnostic(&self, buf: &BufferWithPath, lsp_diag: &Dictionary) -> rootcause::Result<bool>;
54}
55
56pub struct DiagnosticsFilters(Vec<Box<dyn DiagnosticsFilter>>);
58
59impl DiagnosticsFilters {
60 pub fn all(lsp_diags: &[Dictionary]) -> rootcause::Result<Self> {
65 let mut filters = TyposLspFilter::filters();
66 filters.extend(HarperLsFilter::filters());
67 filters.push(Box::new(RelatedInfoFilter::new(lsp_diags)?));
68 Ok(Self(filters))
69 }
70}
71
72impl DiagnosticsFilter for DiagnosticsFilters {
74 fn skip_diagnostic(&self, buf: &BufferWithPath, lsp_diag: &Dictionary) -> rootcause::Result<bool> {
79 for filter in &self.0 {
82 if filter.skip_diagnostic(buf, lsp_diag)? {
83 return Ok(true);
84 }
85 }
86 Ok(false)
87 }
88}
89
90#[derive(Debug)]
92#[cfg_attr(test, derive(Eq, PartialEq))]
93struct DiagnosticLocation {
94 lnum: usize,
96 col: usize,
98 end_col: usize,
100 end_lnum: usize,
102}
103
104impl DiagnosticLocation {
105 pub const fn start(&self) -> (usize, usize) {
107 (self.lnum, self.col)
108 }
109
110 pub const fn end(&self) -> (usize, usize) {
112 (self.end_lnum, self.end_col)
113 }
114}
115
116impl TryFrom<&Dictionary> for DiagnosticLocation {
117 type Error = rootcause::Report;
118
119 fn try_from(value: &Dictionary) -> Result<Self, Self::Error> {
126 let lnum = value
127 .get_t::<nvim_oxi::Integer>("lnum")
128 .and_then(|n| usize::try_from(n).map_err(From::from))?;
129 let col = value
130 .get_t::<nvim_oxi::Integer>("col")
131 .and_then(|n| usize::try_from(n).map_err(From::from))?;
132 let end_col = value
133 .get_t::<nvim_oxi::Integer>("end_col")
134 .and_then(|n| usize::try_from(n).map_err(From::from))?;
135 let end_lnum = value
136 .get_t::<nvim_oxi::Integer>("end_lnum")
137 .and_then(|n| usize::try_from(n).map_err(From::from))?;
138
139 if lnum > end_lnum {
140 bail!("inconsistent line boundaries lnum {lnum} > end_lnum {end_lnum}");
141 }
142 if lnum == end_lnum && col > end_col {
143 bail!("inconsistent col boundaries col {col} > end_col {end_col} on same line");
144 }
145
146 Ok(Self {
147 lnum,
148 col,
149 end_col,
150 end_lnum,
151 })
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use test_that::prelude::*;
158
159 use super::*;
160
161 #[test]
162 fn test_try_from_valid_dictionary_succeeds() {
163 let dict = create_diag(0, 1, 2, 3);
164 let loc_result = DiagnosticLocation::try_from(&dict);
165 assert_that!(loc_result, ok(anything()));
166 let loc = loc_result.expect("valid diagnostic location should parse");
167 assert_that!(loc.lnum, eq(0));
168 assert_that!(loc.col, eq(1));
169 assert_that!(loc.end_lnum, eq(2));
170 assert_that!(loc.end_col, eq(3));
171 }
172
173 #[test]
174 fn test_try_from_missing_lnum_key_fails() {
175 let dict = ytil_noxi::dict! { col: 1_i64, end_col: 3_i64, end_lnum: 2_i64 };
176 assert_that!(
177 (DiagnosticLocation::try_from(&dict)).map(|_| ()),
178 err(displays_as(contains_substring("missing dict value")))
179 );
180 }
181
182 #[test]
183 fn test_try_from_wrong_type_for_lnum_fails() {
184 let dict = ytil_noxi::dict! { lnum: "not_an_int", col: 1_i64, end_col: 3_i64, end_lnum: 2_i64 };
185 assert_that!(
186 DiagnosticLocation::try_from(&dict).map_err(|err| err.to_string()),
187 err(all!(
188 contains_substring(r#"value "not_an_int" of key "lnum""#),
189 contains_substring("is String but Integer was expected"),
190 ))
191 );
192 }
193
194 #[test]
195 fn test_try_from_negative_lnum_fails() {
196 let dict = create_diag(-1, 1, 2, 3);
197 assert_that!(
198 (DiagnosticLocation::try_from(&dict)).map(|_| ()),
199 err(displays_as(contains_substring("out of range")))
200 );
201 }
202
203 #[test]
204 fn test_try_from_lnum_greater_than_end_lnum_fails() {
205 let dict = create_diag(2, 1, 0, 3);
206 assert_that!(
207 (DiagnosticLocation::try_from(&dict)).map(|_| ()),
208 err(displays_as(all!(
209 contains_substring("inconsistent line boundaries"),
210 contains_substring("lnum 2 > end_lnum 0")
211 )))
212 );
213 }
214
215 #[test]
216 fn test_try_from_col_greater_than_end_col_fails() {
217 let dict = create_diag(0, 3, 0, 1);
218 assert_that!(
219 (DiagnosticLocation::try_from(&dict)).map(|_| ()),
220 err(displays_as(all!(
221 contains_substring("inconsistent col boundaries"),
222 contains_substring("col 3 > end_col 1 on same line")
223 )))
224 );
225 }
226
227 #[test]
228 fn test_try_from_equal_boundaries_succeeds() {
229 let dict = create_diag(1, 2, 1, 2);
230 assert_that!(
231 DiagnosticLocation::try_from(&dict),
232 ok(eq(DiagnosticLocation {
233 lnum: 1,
234 col: 2,
235 end_lnum: 1,
236 end_col: 2
237 }))
238 );
239 }
240
241 fn create_diag(lnum: i64, col: i64, end_lnum: i64, end_col: i64) -> Dictionary {
242 ytil_noxi::dict! { col: col, end_col: end_col, lnum: lnum, end_lnum: end_lnum }
243 }
244}