1use std::str::FromStr;
4
5use nvim_oxi::api::Buffer;
6use rootcause::prelude::ResultExt as _;
7use rootcause::report;
8
9#[derive(Debug)]
11#[cfg_attr(test, derive(Eq, PartialEq))]
12pub struct MruBuffer {
13 pub id: i32,
15 pub is_unlisted: bool,
17 pub name: String,
19 pub kind: BufferKind,
21}
22
23impl MruBuffer {
24 pub const fn is_term(&self) -> bool {
25 match self.kind {
26 BufferKind::Term => true,
27 BufferKind::GrugFar | BufferKind::Path | BufferKind::NoName => false,
28 }
29 }
30}
31
32impl From<&MruBuffer> for Buffer {
33 fn from(value: &MruBuffer) -> Self {
34 Self::from(value.id)
35 }
36}
37
38#[derive(Debug)]
40#[cfg_attr(test, derive(Eq, PartialEq))]
41pub enum BufferKind {
42 Term,
44 GrugFar,
46 Path,
48 NoName,
50}
51
52impl<T: AsRef<str>> From<T> for BufferKind {
53 fn from(value: T) -> Self {
54 let str = value.as_ref();
55 if str.starts_with("term://") {
56 Self::Term
57 } else if str.starts_with("Grug FAR") {
58 Self::GrugFar
59 } else if str.starts_with("[No Name]") {
60 Self::NoName
61 } else {
62 Self::Path
63 }
64 }
65}
66
67impl FromStr for MruBuffer {
74 type Err = rootcause::Report;
75
76 fn from_str(mru_buffer_line: &str) -> Result<Self, Self::Err> {
77 let mru_buffer_line = mru_buffer_line.trim();
78
79 let is_unlisted_idx = mru_buffer_line
80 .char_indices()
81 .find_map(|(idx, c)| if c.is_numeric() { None } else { Some(idx) })
82 .ok_or_else(|| report!("error finding buffer id end"))
83 .attach_with(|| format!("mru_buffer_line={mru_buffer_line:?}"))?;
84
85 let id: i32 = {
86 let id = mru_buffer_line
87 .get(..is_unlisted_idx)
88 .ok_or_else(|| report!("error extracting buffer id"))
89 .attach_with(|| format!("mru_buffer_line={mru_buffer_line:?}"))?;
90 id.parse()
91 .context("error parsing buffer id")
92 .attach_with(|| format!("id={id:?} mru_buffer_line={mru_buffer_line:?}"))?
93 };
94
95 let is_unlisted = mru_buffer_line
96 .get(is_unlisted_idx..=is_unlisted_idx)
97 .ok_or_else(|| report!("error extracting is_unlisted by idx"))
98 .attach_with(|| format!("idx={is_unlisted_idx} mru_buffer_line={mru_buffer_line:?}"))?
99 == "u";
100
101 let name_idx = mru_buffer_line
105 .get(is_unlisted_idx..)
106 .and_then(|s| s.find('"').map(|i| is_unlisted_idx.saturating_add(i).saturating_add(1)))
107 .ok_or_else(|| report!("error finding opening quote"))
108 .attach_with(|| format!("mru_buffer_line={mru_buffer_line:?}"))?;
109
110 let rest = mru_buffer_line
111 .get(name_idx..)
112 .ok_or_else(|| report!("error extracting name part by idx"))
113 .attach_with(|| format!("idx={name_idx} mru_buffer_line={mru_buffer_line:?}"))?;
114
115 let (name, _) = rest
116 .split_once('"')
117 .ok_or_else(|| report!("error extracting name"))
118 .attach_with(|| format!("rest={rest:?} mru_buffer_line={mru_buffer_line:?}"))?;
119
120 Ok(Self {
121 id,
122 is_unlisted,
123 name: name.to_string(),
124 kind: BufferKind::from(name),
125 })
126 }
127}
128
129pub fn get() -> Option<Vec<MruBuffer>> {
135 let Ok(mru_buffers_output) = nvim_oxi::api::call_function::<_, String>("execute", ("ls t",))
136 .inspect_err(|err| crate::notify::error(format!("error getting mru buffers | error={err:?}")))
137 else {
138 return None;
139 };
140
141 parse_mru_buffers_output(&mru_buffers_output)
142 .inspect_err(|err| {
143 crate::notify::error(format!(
144 "error parsing mru buffers output | mru_buffers_output={mru_buffers_output:?} error={err:?}"
145 ));
146 })
147 .ok()
148}
149
150fn parse_mru_buffers_output(mru_buffers_output: &str) -> rootcause::Result<Vec<MruBuffer>> {
155 if mru_buffers_output.is_empty() {
156 return Ok(vec![]);
157 }
158 let mut out = vec![];
159 for mru_buffer_line in mru_buffers_output.lines() {
160 if mru_buffer_line.is_empty() {
161 continue;
162 }
163 out.push(MruBuffer::from_str(mru_buffer_line)?);
164 }
165 Ok(out)
166}
167
168#[cfg(test)]
169mod tests {
170 use rstest::rstest;
171
172 use super::*;
173
174 #[rstest]
177 #[case(
178 "1u%a \"file.txt\"",
179 MruBuffer {
180 id: 1,
181 is_unlisted: true,
182 name: "file.txt".to_string(),
183 kind: BufferKind::Path,
184 }
185 )]
186 #[case(
187 "2 %a \"another.txt\"",
188 MruBuffer {
189 id: 2,
190 is_unlisted: false,
191 name: "another.txt".to_string(),
192 kind: BufferKind::Path,
193 }
194 )]
195 #[case(
196 "3 %a \"[No Name]\"",
197 MruBuffer {
198 id: 3,
199 is_unlisted: false,
200 name: "[No Name]".to_string(),
201 kind: BufferKind::NoName,
202 }
203 )]
204 #[case(
205 "4u a \"term://bash\"",
206 MruBuffer {
207 id: 4,
208 is_unlisted: true,
209 name: "term://bash".to_string(),
210 kind: BufferKind::Term,
211 }
212 )]
213 #[case(
214 "5 %a \"Grug FAR results\"",
215 MruBuffer {
216 id: 5,
217 is_unlisted: false,
218 name: "Grug FAR results".to_string(),
219 kind: BufferKind::GrugFar,
220 }
221 )]
222 #[case(
223 " 6 %a \"trimmed.txt\" ",
224 MruBuffer {
225 id: 6,
226 is_unlisted: false,
227 name: "trimmed.txt".to_string(),
228 kind: BufferKind::Path,
229 }
230 )]
231 #[case(
232 "10 #h \"multi_digit.txt\"",
233 MruBuffer {
234 id: 10,
235 is_unlisted: false,
236 name: "multi_digit.txt".to_string(),
237 kind: BufferKind::Path,
238 }
239 )]
240 #[case(
241 "7u aR \"term://~//12345:/bin/zsh\"",
242 MruBuffer {
243 id: 7,
244 is_unlisted: true,
245 name: "term://~//12345:/bin/zsh".to_string(),
246 kind: BufferKind::Term,
247 }
248 )]
249 fn from_str_when_valid_input_returns_mru_buffer(#[case] input: &str, #[case] expected: MruBuffer) {
250 let result = MruBuffer::from_str(input);
251 assert2::assert!(let Ok(mru_buffer) = result);
252 pretty_assertions::assert_eq!(mru_buffer, expected);
253 }
254
255 #[rstest]
256 #[case("", "error finding buffer id end")]
257 #[case(" %a \"file.txt\"", "error parsing buffer id")]
258 #[case("au %a \"file.txt\"", "error parsing buffer id")]
259 #[case("1u%a \"file.txt", "error extracting name")]
260 #[case("1u%a file.txt", "error finding opening quote")]
261 fn from_str_when_invalid_input_returns_error(#[case] input: &str, #[case] expected_err_substr: &str) {
262 let result = MruBuffer::from_str(input);
263 assert2::assert!(let Err(err) = result);
264 assert!(err.to_string().contains(expected_err_substr));
265 }
266}