1use super::snapshot::EditorMode;
15use super::text_coords::byte_col_to_char_col;
16
17#[derive(Debug, Clone, PartialEq)]
23pub enum DecodedState {
24 Command { cmdline: String },
27 Content {
29 mode: EditorMode,
30 lines: Vec<String>,
33 cursor: (usize, usize),
36 visual_selection: Option<((usize, usize), (usize, usize))>,
40 },
41}
42
43pub fn decode(value: &nvim_rs::Value) -> Option<DecodedState> {
47 let arr = value.as_array()?;
48 let mode_str = arr.first().and_then(|v| v.as_str())?;
49 let mode = EditorMode::from_nvim_str(mode_str);
50
51 if mode == EditorMode::Command {
52 let cmdtype = arr.get(1).and_then(|v| v.as_str()).unwrap_or("");
54 let cmdline = arr.get(2).and_then(|v| v.as_str()).unwrap_or("");
55 return Some(DecodedState::Command {
56 cmdline: format!("{cmdtype}{cmdline}"),
57 });
58 }
59
60 let lines: Vec<String> = arr
65 .get(1)
66 .and_then(|v| v.as_array())
67 .map(|ls| {
68 ls.iter()
69 .filter_map(|l| l.as_str().map(|s| s.to_string()))
70 .collect()
71 })
72 .unwrap_or_default();
73 let lines = if lines.is_empty() {
74 vec![String::new()]
75 } else {
76 lines
77 };
78
79 let cursor = arr
83 .get(2)
84 .and_then(|v| v.as_array())
85 .and_then(|c| {
86 let row = c.first()?.as_u64()? as usize;
87 let byte_col = c.get(1)?.as_u64()? as usize;
88 let row0 = row.saturating_sub(1).min(lines.len() - 1);
92 let char_col = byte_col_to_char_col(&lines[row0], byte_col);
93 Some((row0, char_col))
94 })
95 .unwrap_or((0, 0));
96
97 let visual_selection = if matches!(mode, EditorMode::Visual | EditorMode::VisualLine) {
100 arr.get(3)
101 .and_then(|v| v.as_array())
102 .and_then(|p| {
103 let lnum = p.get(1)?.as_u64()? as usize;
104 let vcol_byte = p.get(2)?.as_u64()? as usize;
105 if lnum == 0 {
106 return None;
107 }
108 let row0 = lnum.saturating_sub(1).min(lines.len() - 1);
109 let char_col = byte_col_to_char_col(&lines[row0], vcol_byte.saturating_sub(1));
110 Some((row0, char_col))
111 })
112 .map(|anchor| {
113 let (mut start, mut end) = if anchor <= cursor {
114 (anchor, cursor)
115 } else {
116 (cursor, anchor)
117 };
118 if mode == EditorMode::VisualLine {
119 start.1 = 0;
120 end.1 = usize::MAX;
121 }
122 (start, end)
123 })
124 } else {
125 None
126 };
127
128 Some(DecodedState::Content {
129 mode,
130 lines,
131 cursor,
132 visual_selection,
133 })
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use nvim_rs::Value;
140
141 fn s(text: &str) -> Value {
142 Value::from(text)
143 }
144 fn u(n: u64) -> Value {
145 Value::from(n)
146 }
147 fn arr(items: Vec<Value>) -> Value {
148 Value::Array(items)
149 }
150
151 #[test]
156 fn decode_non_array_is_none() {
157 assert_eq!(decode(&s("nope")), None);
158 }
159
160 #[test]
161 fn decode_missing_mode_is_none() {
162 assert_eq!(decode(&arr(vec![])), None);
163 }
164
165 #[test]
168 fn decode_normal_cursor_ascii() {
169 let v = arr(vec![
171 s("n"),
172 arr(vec![s("hello"), s("world")]),
173 arr(vec![u(2), u(3)]),
174 arr(vec![u(0), u(0), u(0), u(0)]),
175 ]);
176 let d = decode(&v).unwrap();
177 assert_eq!(
178 d,
179 DecodedState::Content {
180 mode: EditorMode::Normal,
181 lines: vec!["hello".into(), "world".into()],
182 cursor: (1, 3), visual_selection: None,
184 }
185 );
186 }
187
188 #[test]
189 fn decode_cursor_multibyte_converts_byte_to_char() {
190 let v = arr(vec![
192 s("n"),
193 arr(vec![s("wørld")]),
194 arr(vec![u(1), u(4)]),
195 arr(vec![u(0), u(0), u(0), u(0)]),
196 ]);
197 match decode(&v).unwrap() {
198 DecodedState::Content { cursor, .. } => assert_eq!(cursor, (0, 3)),
199 other => panic!("expected Content, got {other:?}"),
200 }
201 }
202
203 #[test]
204 fn decode_empty_buffer_normalises_to_single_blank_line() {
205 let v = arr(vec![
206 s("n"),
207 arr(vec![]),
208 arr(vec![u(1), u(0)]),
209 arr(vec![u(0), u(0), u(0), u(0)]),
210 ]);
211 match decode(&v).unwrap() {
212 DecodedState::Content { lines, cursor, .. } => {
213 assert_eq!(lines, vec![String::new()]);
214 assert_eq!(cursor, (0, 0));
215 }
216 other => panic!("expected Content, got {other:?}"),
217 }
218 }
219
220 #[test]
221 fn decode_cursor_row_out_of_bounds_clamps_to_last_line() {
222 let v = arr(vec![
225 s("n"),
226 arr(vec![s("wørld")]),
227 arr(vec![u(5), u(4)]), arr(vec![u(0), u(0), u(0), u(0)]),
229 ]);
230 match decode(&v).unwrap() {
231 DecodedState::Content { cursor, .. } => {
232 assert_eq!(cursor, (0, 3));
234 }
235 other => panic!("expected Content, got {other:?}"),
236 }
237 }
238
239 #[test]
240 fn decode_unknown_mode_is_other() {
241 let v = arr(vec![
242 s("t"), arr(vec![s("x")]),
244 arr(vec![u(1), u(0)]),
245 arr(vec![u(0), u(0), u(0), u(0)]),
246 ]);
247 match decode(&v).unwrap() {
248 DecodedState::Content { mode, .. } => {
249 assert_eq!(mode, EditorMode::Other("t".into()))
250 }
251 other => panic!("expected Content, got {other:?}"),
252 }
253 }
254
255 #[test]
258 fn decode_command_mode_concatenates_type_and_line() {
259 let v = arr(vec![s("c"), s(":"), s("set nu")]);
261 assert_eq!(
262 decode(&v).unwrap(),
263 DecodedState::Command {
264 cmdline: ":set nu".into()
265 }
266 );
267 }
268
269 #[test]
270 fn decode_command_search_prefix() {
271 let v = arr(vec![s("c"), s("/"), s("pattern")]);
272 assert_eq!(
273 decode(&v).unwrap(),
274 DecodedState::Command {
275 cmdline: "/pattern".into()
276 }
277 );
278 }
279
280 #[test]
283 fn decode_visual_anchor_before_cursor() {
284 let v = arr(vec![
287 s("v"),
288 arr(vec![s("abcdef")]),
289 arr(vec![u(1), u(3)]), arr(vec![u(0), u(1), u(1), u(0)]), ]);
292 match decode(&v).unwrap() {
293 DecodedState::Content {
294 visual_selection, ..
295 } => assert_eq!(visual_selection, Some(((0, 0), (0, 3)))),
296 other => panic!("expected Content, got {other:?}"),
297 }
298 }
299
300 #[test]
301 fn decode_visual_anchor_after_cursor_orders_start_end() {
302 let v = arr(vec![
304 s("v"),
305 arr(vec![s("abcdef")]),
306 arr(vec![u(1), u(1)]), arr(vec![u(0), u(1), u(5), u(0)]), ]);
309 match decode(&v).unwrap() {
310 DecodedState::Content {
311 visual_selection, ..
312 } => assert_eq!(visual_selection, Some(((0, 1), (0, 4)))),
313 other => panic!("expected Content, got {other:?}"),
314 }
315 }
316
317 #[test]
318 fn decode_visual_line_spans_full_columns() {
319 let v = arr(vec![
320 s("V"),
321 arr(vec![s("abc"), s("defgh")]),
322 arr(vec![u(2), u(2)]), arr(vec![u(0), u(1), u(1), u(0)]), ]);
325 match decode(&v).unwrap() {
326 DecodedState::Content {
327 mode,
328 visual_selection,
329 ..
330 } => {
331 assert_eq!(mode, EditorMode::VisualLine);
332 assert_eq!(visual_selection, Some(((0, 0), (1, usize::MAX))));
334 }
335 other => panic!("expected Content, got {other:?}"),
336 }
337 }
338
339 #[test]
340 fn decode_visual_with_zero_lnum_anchor_is_none() {
341 let v = arr(vec![
342 s("v"),
343 arr(vec![s("abc")]),
344 arr(vec![u(1), u(0)]),
345 arr(vec![u(0), u(0), u(0), u(0)]), ]);
347 match decode(&v).unwrap() {
348 DecodedState::Content {
349 visual_selection, ..
350 } => assert_eq!(visual_selection, None),
351 other => panic!("expected Content, got {other:?}"),
352 }
353 }
354}