use super::snapshot::EditorMode;
use super::text_coords::byte_col_to_char_col;
#[derive(Debug, Clone, PartialEq)]
pub enum DecodedState {
Command { cmdline: String },
Content {
mode: EditorMode,
lines: Vec<String>,
cursor: (usize, usize),
visual_selection: Option<((usize, usize), (usize, usize))>,
},
}
pub fn decode(value: &nvim_rs::Value) -> Option<DecodedState> {
let arr = value.as_array()?;
let mode_str = arr.first().and_then(|v| v.as_str())?;
let mode = EditorMode::from_nvim_str(mode_str);
if mode == EditorMode::Command {
let cmdtype = arr.get(1).and_then(|v| v.as_str()).unwrap_or("");
let cmdline = arr.get(2).and_then(|v| v.as_str()).unwrap_or("");
return Some(DecodedState::Command {
cmdline: format!("{cmdtype}{cmdline}"),
});
}
let lines: Vec<String> = arr
.get(1)
.and_then(|v| v.as_array())
.map(|ls| {
ls.iter()
.filter_map(|l| l.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let lines = if lines.is_empty() {
vec![String::new()]
} else {
lines
};
let cursor = arr
.get(2)
.and_then(|v| v.as_array())
.and_then(|c| {
let row = c.first()?.as_u64()? as usize;
let byte_col = c.get(1)?.as_u64()? as usize;
let row0 = row.saturating_sub(1).min(lines.len() - 1);
let char_col = byte_col_to_char_col(&lines[row0], byte_col);
Some((row0, char_col))
})
.unwrap_or((0, 0));
let visual_selection = if matches!(mode, EditorMode::Visual | EditorMode::VisualLine) {
arr.get(3)
.and_then(|v| v.as_array())
.and_then(|p| {
let lnum = p.get(1)?.as_u64()? as usize;
let vcol_byte = p.get(2)?.as_u64()? as usize;
if lnum == 0 {
return None;
}
let row0 = lnum.saturating_sub(1).min(lines.len() - 1);
let char_col = byte_col_to_char_col(&lines[row0], vcol_byte.saturating_sub(1));
Some((row0, char_col))
})
.map(|anchor| {
let (mut start, mut end) = if anchor <= cursor {
(anchor, cursor)
} else {
(cursor, anchor)
};
if mode == EditorMode::VisualLine {
start.1 = 0;
end.1 = usize::MAX;
}
(start, end)
})
} else {
None
};
Some(DecodedState::Content {
mode,
lines,
cursor,
visual_selection,
})
}
#[cfg(test)]
mod tests {
use super::*;
use nvim_rs::Value;
fn s(text: &str) -> Value {
Value::from(text)
}
fn u(n: u64) -> Value {
Value::from(n)
}
fn arr(items: Vec<Value>) -> Value {
Value::Array(items)
}
#[test]
fn decode_non_array_is_none() {
assert_eq!(decode(&s("nope")), None);
}
#[test]
fn decode_missing_mode_is_none() {
assert_eq!(decode(&arr(vec![])), None);
}
#[test]
fn decode_normal_cursor_ascii() {
let v = arr(vec![
s("n"),
arr(vec![s("hello"), s("world")]),
arr(vec![u(2), u(3)]),
arr(vec![u(0), u(0), u(0), u(0)]),
]);
let d = decode(&v).unwrap();
assert_eq!(
d,
DecodedState::Content {
mode: EditorMode::Normal,
lines: vec!["hello".into(), "world".into()],
cursor: (1, 3), visual_selection: None,
}
);
}
#[test]
fn decode_cursor_multibyte_converts_byte_to_char() {
let v = arr(vec![
s("n"),
arr(vec![s("wørld")]),
arr(vec![u(1), u(4)]),
arr(vec![u(0), u(0), u(0), u(0)]),
]);
match decode(&v).unwrap() {
DecodedState::Content { cursor, .. } => assert_eq!(cursor, (0, 3)),
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_empty_buffer_normalises_to_single_blank_line() {
let v = arr(vec![
s("n"),
arr(vec![]),
arr(vec![u(1), u(0)]),
arr(vec![u(0), u(0), u(0), u(0)]),
]);
match decode(&v).unwrap() {
DecodedState::Content { lines, cursor, .. } => {
assert_eq!(lines, vec![String::new()]);
assert_eq!(cursor, (0, 0));
}
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_cursor_row_out_of_bounds_clamps_to_last_line() {
let v = arr(vec![
s("n"),
arr(vec![s("wørld")]),
arr(vec![u(5), u(4)]), arr(vec![u(0), u(0), u(0), u(0)]),
]);
match decode(&v).unwrap() {
DecodedState::Content { cursor, .. } => {
assert_eq!(cursor, (0, 3));
}
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_unknown_mode_is_other() {
let v = arr(vec![
s("t"), arr(vec![s("x")]),
arr(vec![u(1), u(0)]),
arr(vec![u(0), u(0), u(0), u(0)]),
]);
match decode(&v).unwrap() {
DecodedState::Content { mode, .. } => {
assert_eq!(mode, EditorMode::Other("t".into()))
}
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_command_mode_concatenates_type_and_line() {
let v = arr(vec![s("c"), s(":"), s("set nu")]);
assert_eq!(
decode(&v).unwrap(),
DecodedState::Command {
cmdline: ":set nu".into()
}
);
}
#[test]
fn decode_command_search_prefix() {
let v = arr(vec![s("c"), s("/"), s("pattern")]);
assert_eq!(
decode(&v).unwrap(),
DecodedState::Command {
cmdline: "/pattern".into()
}
);
}
#[test]
fn decode_visual_anchor_before_cursor() {
let v = arr(vec![
s("v"),
arr(vec![s("abcdef")]),
arr(vec![u(1), u(3)]), arr(vec![u(0), u(1), u(1), u(0)]), ]);
match decode(&v).unwrap() {
DecodedState::Content {
visual_selection, ..
} => assert_eq!(visual_selection, Some(((0, 0), (0, 3)))),
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_visual_anchor_after_cursor_orders_start_end() {
let v = arr(vec![
s("v"),
arr(vec![s("abcdef")]),
arr(vec![u(1), u(1)]), arr(vec![u(0), u(1), u(5), u(0)]), ]);
match decode(&v).unwrap() {
DecodedState::Content {
visual_selection, ..
} => assert_eq!(visual_selection, Some(((0, 1), (0, 4)))),
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_visual_line_spans_full_columns() {
let v = arr(vec![
s("V"),
arr(vec![s("abc"), s("defgh")]),
arr(vec![u(2), u(2)]), arr(vec![u(0), u(1), u(1), u(0)]), ]);
match decode(&v).unwrap() {
DecodedState::Content {
mode,
visual_selection,
..
} => {
assert_eq!(mode, EditorMode::VisualLine);
assert_eq!(visual_selection, Some(((0, 0), (1, usize::MAX))));
}
other => panic!("expected Content, got {other:?}"),
}
}
#[test]
fn decode_visual_with_zero_lnum_anchor_is_none() {
let v = arr(vec![
s("v"),
arr(vec![s("abc")]),
arr(vec![u(1), u(0)]),
arr(vec![u(0), u(0), u(0), u(0)]), ]);
match decode(&v).unwrap() {
DecodedState::Content {
visual_selection, ..
} => assert_eq!(visual_selection, None),
other => panic!("expected Content, got {other:?}"),
}
}
}