1use std::str::FromStr;
4
5use color_eyre::eyre::Context as _;
6use color_eyre::eyre::eyre;
7use nvim_oxi::api::Buffer;
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 = color_eyre::eyre::Error;
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(|| eyre!("error finding buffer id end | mru_buffer_line={mru_buffer_line:?}"))?;
83
84 let id: i32 = {
85 let id = mru_buffer_line
86 .get(..is_unlisted_idx)
87 .ok_or_else(|| eyre!("error extracting buffer id | mru_buffer_line={mru_buffer_line:?}"))?;
88 id.parse()
89 .wrap_err_with(|| format!("error parsing buffer id | id={id:?} mru_buffer_line={mru_buffer_line:?}"))?
90 };
91
92 let is_unlisted = mru_buffer_line.get(is_unlisted_idx..=is_unlisted_idx).ok_or_else(|| {
93 eyre!("error extracting is_unlisted by idx | idx={is_unlisted_idx} mru_buffer_line={mru_buffer_line:?}")
94 })? == "u";
95
96 let name_idx = is_unlisted_idx.saturating_add(7);
98
99 let rest = mru_buffer_line.get(name_idx..).ok_or_else(|| {
100 eyre!("error extracting name part by idx | idx={name_idx} mru_buffer_line={mru_buffer_line:?}")
101 })?;
102
103 let (name, _) = rest
104 .split_once('"')
105 .ok_or_else(|| eyre!("error extracting name | rest={rest:?} mru_buffer_line={mru_buffer_line:?}"))?;
106
107 Ok(Self {
108 id,
109 is_unlisted,
110 name: name.to_string(),
111 kind: BufferKind::from(name),
112 })
113 }
114}
115
116pub fn get() -> Option<Vec<MruBuffer>> {
122 let Ok(mru_buffers_output) = nvim_oxi::api::call_function::<_, String>("execute", ("ls t",))
123 .inspect_err(|err| crate::notify::error(format!("error getting mru buffers | error={err:?}")))
124 else {
125 return None;
126 };
127
128 parse_mru_buffers_output(&mru_buffers_output)
129 .inspect_err(|err| {
130 crate::notify::error(format!(
131 "error parsing mru buffers output | mru_buffers_output={mru_buffers_output:?} error={err:?}"
132 ));
133 })
134 .ok()
135}
136
137fn parse_mru_buffers_output(mru_buffers_output: &str) -> color_eyre::Result<Vec<MruBuffer>> {
142 if mru_buffers_output.is_empty() {
143 return Ok(vec![]);
144 }
145 let mut out = vec![];
146 for mru_buffer_line in mru_buffers_output.lines() {
147 if mru_buffer_line.is_empty() {
148 continue;
149 }
150 out.push(MruBuffer::from_str(mru_buffer_line)?);
151 }
152 Ok(out)
153}
154
155#[cfg(test)]
156mod tests {
157 use rstest::rstest;
158
159 use super::*;
160
161 #[rstest]
162 #[case(
163 "1u %a \"file.txt\"",
164 MruBuffer {
165 id: 1,
166 is_unlisted: true,
167 name: "ile.txt".to_string(),
168 kind: BufferKind::Path,
169 }
170 )]
171 #[case(
172 "2 %a \"another.txt\"",
173 MruBuffer {
174 id: 2,
175 is_unlisted: false,
176 name: "nother.txt".to_string(),
177 kind: BufferKind::Path,
178 }
179 )]
180 #[case(
181 "3 %a \"[No Name]\"",
182 MruBuffer {
183 id: 3,
184 is_unlisted: false,
185 name: "No Name]".to_string(),
186 kind: BufferKind::Path,
187 }
188 )]
189 #[case(
190 "4 %a \"term://bash\"",
191 MruBuffer {
192 id: 4,
193 is_unlisted: false,
194 name: "erm://bash".to_string(),
195 kind: BufferKind::Path,
196 }
197 )]
198 #[case(
199 "5 %a \"Grug FAR results\"",
200 MruBuffer {
201 id: 5,
202 is_unlisted: false,
203 name: "rug FAR results".to_string(),
204 kind: BufferKind::Path,
205 }
206 )]
207 #[case(
208 " 6 %a \"trimmed.txt\" ",
209 MruBuffer {
210 id: 6,
211 is_unlisted: false,
212 name: "rimmed.txt".to_string(),
213 kind: BufferKind::Path,
214 }
215 )]
216 fn from_str_when_valid_input_returns_mru_buffer(#[case] input: &str, #[case] expected: MruBuffer) {
217 let result = MruBuffer::from_str(input);
218 assert2::let_assert!(Ok(mru_buffer) = result);
219 pretty_assertions::assert_eq!(mru_buffer, expected);
220 }
221
222 #[rstest]
223 #[case("", "error finding buffer id end")]
224 #[case(" %a \"file.txt\"", "error parsing buffer id")]
225 #[case("au %a \"file.txt\"", "error parsing buffer id")]
226 #[case("1u %a \"file.txt", "error extracting name")]
227 #[case("1u %a file.txt", "error extracting name")]
228 fn from_str_when_invalid_input_returns_error(#[case] input: &str, #[case] expected_err_substr: &str) {
229 let result = MruBuffer::from_str(input);
230 assert2::let_assert!(Err(err) = result);
231 assert!(err.to_string().contains(expected_err_substr));
232 }
233}