nvrim/diagnostics/
filters.rs1use color_eyre::eyre::bail;
7use nvim_oxi::Dictionary;
8use nvim_oxi::api::Buffer;
9use ytil_noxi::buffer::BufferExt;
10use ytil_noxi::dict::DictionaryExt as _;
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 = color_eyre::eyre::Error;
36
37 fn try_from(value: Buffer) -> Result<Self, Self::Error> {
38 let path = value.get_name().map(|s| s.to_string_lossy().to_string())?;
39 Ok(Self {
40 path,
41 buffer: Box::new(value),
42 })
43 }
44}
45
46#[derive(Debug)]
48#[cfg_attr(test, derive(Eq, PartialEq))]
49struct DiagnosticLocation {
50 lnum: usize,
52 col: usize,
54 end_col: usize,
56 end_lnum: usize,
58}
59
60impl DiagnosticLocation {
61 pub const fn start(&self) -> (usize, usize) {
63 (self.lnum, self.col)
64 }
65
66 pub const fn end(&self) -> (usize, usize) {
68 (self.end_lnum, self.end_col)
69 }
70}
71
72impl TryFrom<&Dictionary> for DiagnosticLocation {
73 type Error = color_eyre::eyre::Error;
74
75 fn try_from(value: &Dictionary) -> Result<Self, Self::Error> {
82 let lnum = value
83 .get_t::<nvim_oxi::Integer>("lnum")
84 .and_then(|n| usize::try_from(n).map_err(From::from))?;
85 let col = value
86 .get_t::<nvim_oxi::Integer>("col")
87 .and_then(|n| usize::try_from(n).map_err(From::from))?;
88 let end_col = value
89 .get_t::<nvim_oxi::Integer>("end_col")
90 .and_then(|n| usize::try_from(n).map_err(From::from))?;
91 let end_lnum = value
92 .get_t::<nvim_oxi::Integer>("end_lnum")
93 .and_then(|n| usize::try_from(n).map_err(From::from))?;
94
95 if lnum > end_lnum {
96 bail!("inconsistent line boundaries lnum {lnum} > end_lnum {end_lnum}");
97 }
98 if lnum == end_lnum && col > end_col {
99 bail!("inconsistent col boundaries col {col} > end_col {end_col} on same line");
100 }
101
102 Ok(Self {
103 lnum,
104 col,
105 end_col,
106 end_lnum,
107 })
108 }
109}
110
111pub trait DiagnosticsFilter {
113 fn skip_diagnostic(&self, buf: &BufferWithPath, lsp_diag: &Dictionary) -> color_eyre::Result<bool>;
119}
120
121pub struct DiagnosticsFilters(Vec<Box<dyn DiagnosticsFilter>>);
123
124impl DiagnosticsFilters {
125 pub fn all(lsp_diags: &[Dictionary]) -> color_eyre::Result<Self> {
130 let mut filters = TyposLspFilter::filters();
131 filters.extend(HarperLsFilter::filters());
132 filters.push(Box::new(RelatedInfoFilter::new(lsp_diags)?));
133 Ok(Self(filters))
134 }
135}
136
137impl DiagnosticsFilter for DiagnosticsFilters {
139 fn skip_diagnostic(&self, buf: &BufferWithPath, lsp_diag: &Dictionary) -> color_eyre::Result<bool> {
144 for filter in &self.0 {
147 if filter.skip_diagnostic(buf, lsp_diag)? {
148 return Ok(true);
149 }
150 }
151 Ok(false)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn try_from_valid_dictionary_succeeds() {
161 let dict = create_diag(0, 1, 2, 3);
162 assert2::let_assert!(Ok(loc) = DiagnosticLocation::try_from(&dict));
163 pretty_assertions::assert_eq!(loc.lnum, 0);
164 pretty_assertions::assert_eq!(loc.col, 1);
165 pretty_assertions::assert_eq!(loc.end_lnum, 2);
166 pretty_assertions::assert_eq!(loc.end_col, 3);
167 }
168
169 #[test]
170 fn try_from_missing_lnum_key_fails() {
171 let dict = ytil_noxi::dict! { col: 1_i64, end_col: 3_i64, end_lnum: 2_i64 };
172 assert2::let_assert!(Err(err) = DiagnosticLocation::try_from(&dict));
173 assert!(err.to_string().contains("missing dict value"));
174 }
175
176 #[test]
177 fn try_from_wrong_type_for_lnum_fails() {
178 let dict = ytil_noxi::dict! { lnum: "not_an_int", col: 1_i64, end_col: 3_i64, end_lnum: 2_i64 };
179 assert2::let_assert!(Err(err) = DiagnosticLocation::try_from(&dict));
180 assert!(err.to_string().contains(r#"value "not_an_int" of key "lnum""#));
181 assert!(err.to_string().contains("is String but Integer was expected"));
182 }
183
184 #[test]
185 fn try_from_negative_lnum_fails() {
186 let dict = create_diag(-1, 1, 2, 3);
187 assert2::let_assert!(Err(err) = DiagnosticLocation::try_from(&dict));
188 assert!(err.to_string().contains("out of range"));
189 }
190
191 #[test]
192 fn try_from_lnum_greater_than_end_lnum_fails() {
193 let dict = create_diag(2, 1, 0, 3);
194 assert2::let_assert!(Err(err) = DiagnosticLocation::try_from(&dict));
195 assert!(err.to_string().contains("inconsistent line boundaries"));
196 assert!(err.to_string().contains("lnum 2 > end_lnum 0"));
197 }
198
199 #[test]
200 fn try_from_col_greater_than_end_col_fails() {
201 let dict = create_diag(0, 3, 0, 1);
202 assert2::let_assert!(Err(err) = DiagnosticLocation::try_from(&dict));
203 assert!(err.to_string().contains("inconsistent col boundaries"));
204 assert!(err.to_string().contains("col 3 > end_col 1 on same line"));
205 }
206
207 #[test]
208 fn try_from_equal_boundaries_succeeds() {
209 let dict = create_diag(1, 2, 1, 2);
210 assert2::let_assert!(Ok(loc) = DiagnosticLocation::try_from(&dict));
211 pretty_assertions::assert_eq!(
212 loc,
213 DiagnosticLocation {
214 lnum: 1,
215 col: 2,
216 end_lnum: 1,
217 end_col: 2
218 }
219 );
220 }
221
222 fn create_diag(lnum: i64, col: i64, end_lnum: i64, end_col: i64) -> Dictionary {
223 ytil_noxi::dict! { col: col, end_col: end_col, lnum: lnum, end_lnum: end_lnum }
224 }
225}