use std::borrow::Cow;
use std::num::NonZeroU64;
pub struct EditorSnapshot<'a> {
pub lines: Cow<'a, [String]>,
pub cursor: (usize, usize),
pub content_revision: NonZeroU64,
}
impl<'a> EditorSnapshot<'a> {
pub fn borrowed(
lines: &'a [String],
cursor: (usize, usize),
content_revision: NonZeroU64,
) -> Self {
Self {
lines: Cow::Borrowed(lines),
cursor,
content_revision,
}
}
pub fn owned(
lines: Vec<String>,
cursor: (usize, usize),
content_revision: NonZeroU64,
) -> EditorSnapshot<'static> {
EditorSnapshot {
lines: Cow::Owned(lines),
cursor,
content_revision,
}
}
pub fn cursor_in_bounds(&self) -> bool {
self.cursor.0 < self.lines.len()
}
pub fn cursor_row_clamped(&self) -> usize {
if self.lines.is_empty() {
0
} else {
self.cursor.0.min(self.lines.len() - 1)
}
}
pub fn cursor_line(&self) -> &str {
self.lines
.get(self.cursor_row_clamped())
.map(String::as_str)
.unwrap_or("")
}
pub fn cursor_byte_offset(&self) -> usize {
let row = self.cursor.0;
let mut byte = 0;
for line in self.lines.iter().take(row) {
byte += line.len() + 1; }
let Some(line) = self.lines.get(row) else {
return byte;
};
byte + line
.char_indices()
.nth(self.cursor.1)
.map(|(b, _)| b)
.unwrap_or(line.len())
}
}
#[derive(Debug, Clone)]
pub struct NvimSnapshot {
pub lines: Vec<String>,
pub cursor: (usize, usize),
pub mode: EditorMode,
pub cmdline: Option<String>,
pub dirty: bool,
pub content_gen: u64,
pub visual_selection: Option<((usize, usize), (usize, usize))>,
}
impl Default for NvimSnapshot {
fn default() -> Self {
Self {
lines: vec![String::new()],
cursor: (0, 0),
mode: EditorMode::Normal,
cmdline: None,
dirty: false,
content_gen: 0,
visual_selection: None,
}
}
}
impl NvimSnapshot {
pub fn footer_label(&self) -> String {
if self.mode == EditorMode::Command
&& let Some(cmd) = &self.cmdline
{
return format!("{}\u{2590}", cmd); }
self.mode.label().to_string()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EditorMode {
Normal,
Insert,
Replace,
Visual,
VisualLine,
Command,
Other(String),
}
impl EditorMode {
pub fn label(&self) -> &str {
match self {
EditorMode::Normal => "NORMAL",
EditorMode::Insert => "INSERT",
EditorMode::Replace => "REPLACE",
EditorMode::Visual => "VISUAL",
EditorMode::VisualLine => "V-LINE",
EditorMode::Command => "COMMAND",
EditorMode::Other(_) => "OTHER",
}
}
pub fn from_nvim_str(s: &str) -> Self {
match s {
"n" | "no" | "nov" | "noV" | "no\x16" => EditorMode::Normal,
"i" => EditorMode::Insert,
"R" => EditorMode::Replace,
"v" => EditorMode::Visual,
"V" => EditorMode::VisualLine,
"c" => EditorMode::Command,
other => EditorMode::Other(other.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rev(n: u64) -> NonZeroU64 {
NonZeroU64::new(n).unwrap()
}
#[test]
fn snapshot_borrowed_passes_cursor_through() {
let lines = vec!["a".to_string(), "b".to_string()];
let snap = EditorSnapshot::borrowed(&lines, (1, 0), rev(5));
assert_eq!(snap.cursor, (1, 0));
assert!(snap.cursor_in_bounds());
assert_eq!(snap.cursor_line(), "b");
}
#[test]
fn snapshot_helpers_on_empty_buffer() {
let snap: EditorSnapshot<'_> = EditorSnapshot::owned(Vec::new(), (0, 0), rev(1));
assert!(!snap.cursor_in_bounds());
assert_eq!(snap.cursor_row_clamped(), 0);
assert_eq!(snap.cursor_line(), "");
}
#[test]
fn snapshot_cursor_byte_offset_across_rows() {
let lines = vec!["hello".to_string(), "wørld".to_string()];
let snap = EditorSnapshot::borrowed(&lines, (1, 2), rev(1));
assert_eq!(snap.cursor_byte_offset(), 9);
}
#[test]
fn snapshot_clamps_stale_cursor_row() {
let lines = vec!["only".to_string()];
let snap = EditorSnapshot::borrowed(&lines, (5, 2), rev(1));
assert_eq!(snap.cursor_row_clamped(), 0);
assert_eq!(snap.cursor_line(), "only");
}
#[test]
fn default_snapshot_is_not_dirty() {
let snap = NvimSnapshot::default();
assert!(!snap.dirty);
}
#[test]
fn mode_label_normal() {
assert_eq!(EditorMode::Normal.label(), "NORMAL");
}
#[test]
fn mode_label_insert() {
assert_eq!(EditorMode::Insert.label(), "INSERT");
}
#[test]
fn mode_label_visual() {
assert_eq!(EditorMode::Visual.label(), "VISUAL");
}
#[test]
fn mode_label_visual_line() {
assert_eq!(EditorMode::VisualLine.label(), "V-LINE");
}
#[test]
fn mode_label_command() {
assert_eq!(EditorMode::Command.label(), "COMMAND");
}
#[test]
fn mode_from_str_normal() {
assert!(matches!(EditorMode::from_nvim_str("n"), EditorMode::Normal));
}
#[test]
fn mode_from_str_insert() {
assert!(matches!(EditorMode::from_nvim_str("i"), EditorMode::Insert));
}
#[test]
fn mode_from_str_visual() {
assert!(matches!(EditorMode::from_nvim_str("v"), EditorMode::Visual));
}
#[test]
fn mode_from_str_visual_line() {
assert!(matches!(
EditorMode::from_nvim_str("V"),
EditorMode::VisualLine
));
}
#[test]
fn mode_from_str_command() {
assert!(matches!(
EditorMode::from_nvim_str("c"),
EditorMode::Command
));
}
#[test]
fn mode_from_str_replace() {
assert!(matches!(
EditorMode::from_nvim_str("R"),
EditorMode::Replace
));
}
#[test]
fn mode_from_str_unknown() {
let m = EditorMode::from_nvim_str("t"); assert!(matches!(m, EditorMode::Other(_)));
if let EditorMode::Other(s) = m {
assert_eq!(s, "t");
}
}
#[test]
fn footer_label_normal_mode() {
let snap = NvimSnapshot {
mode: EditorMode::Normal,
cmdline: None,
..Default::default()
};
assert_eq!(snap.footer_label(), "NORMAL");
}
#[test]
fn footer_label_command_mode_with_cmdline() {
let snap = NvimSnapshot {
mode: EditorMode::Command,
cmdline: Some(":set nu".to_string()),
..Default::default()
};
assert_eq!(snap.footer_label(), ":set nu\u{2590}");
}
#[test]
fn footer_label_command_mode_no_cmdline() {
let snap = NvimSnapshot {
mode: EditorMode::Command,
cmdline: None,
..Default::default()
};
assert_eq!(snap.footer_label(), "COMMAND");
}
}