extern crate self as escriba_mode;
use escriba_core::{Mode, Operator};
use escriba_memori::{CaretMove, Chars, Offset, Ruler};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct PendingOp {
pub count: Option<u32>,
pub operator: Option<Operator>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "mode")]
pub enum ModalState {
Normal {
pending: PendingOp,
},
Insert,
Visual,
VisualLine,
Command {
#[serde(flatten)]
line: ExLine,
},
}
impl Default for ModalState {
fn default() -> Self {
Self::Normal {
pending: PendingOp::default(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(from = "ExLineWire")]
pub struct ExLine {
#[serde(rename = "minibuffer")]
text: String,
#[serde(default)]
caret: usize,
}
#[derive(Deserialize, schemars::JsonSchema)]
struct ExLineWire {
#[serde(default)]
minibuffer: String,
#[serde(default)]
caret: usize,
}
impl From<ExLineWire> for ExLine {
fn from(w: ExLineWire) -> Self {
let caret = w.caret.min(w.minibuffer.chars().count());
Self {
text: w.minibuffer,
caret,
}
}
}
impl ExLine {
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub const fn caret(&self) -> usize {
self.caret
}
#[must_use]
pub fn len_chars(&self) -> usize {
self.text.chars().count()
}
fn byte_of_caret(&self) -> usize {
Ruler::new(&self.text)
.to_bytes(Offset::<Chars>::new(self.caret))
.raw()
}
pub fn insert(&mut self, ch: char) {
let at = self.byte_of_caret();
self.text.insert(at, ch);
self.caret += 1;
}
pub fn push_str(&mut self, s: &str) {
self.text.push_str(s);
self.caret = self.len_chars();
}
pub fn move_caret(&mut self, to: CaretMove) {
self.caret = to.resolve(self.caret, self.len_chars());
}
pub fn delete(&mut self) {
let at = self.byte_of_caret();
if at < self.text.len() {
self.text.remove(at);
}
}
pub fn backspace(&mut self) -> Option<char> {
if self.caret == 0 {
return None;
}
let at = self.byte_of_caret();
let prev = self.text[..at]
.char_indices()
.next_back()
.map_or(0, |(i, _)| i);
let ch = self.text.remove(prev);
self.caret -= 1;
Some(ch)
}
pub fn clear(&mut self) {
self.text.clear();
self.caret = 0;
}
}
impl ModalState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn mode(&self) -> Mode {
match self {
Self::Normal { .. } => Mode::Normal,
Self::Insert => Mode::Insert,
Self::Visual => Mode::Visual,
Self::VisualLine => Mode::VisualLine,
Self::Command { .. } => Mode::Command,
}
}
pub fn enter_normal(&mut self) {
*self = Self::Normal {
pending: PendingOp::default(),
};
}
pub fn enter_insert(&mut self) {
*self = Self::Insert;
}
pub fn enter_visual(&mut self) {
*self = Self::Visual;
}
pub fn enter_visual_line(&mut self) {
*self = Self::VisualLine;
}
pub fn enter_command(&mut self) {
*self = Self::Command {
line: ExLine::default(),
};
}
pub fn escape(&mut self) {
self.enter_normal();
}
pub fn enter(&mut self, mode: Mode) {
match mode {
Mode::Normal => self.enter_normal(),
Mode::Insert => self.enter_insert(),
Mode::Visual => self.enter_visual(),
Mode::VisualLine => self.enter_visual_line(),
Mode::Command => self.enter_command(),
}
}
pub fn set_operator(&mut self, op: Operator) {
if let Self::Normal { pending } = self {
pending.operator = Some(op);
}
}
pub fn append_count(&mut self, digit: u32) {
if let Self::Normal { pending } = self {
let n = pending.count.unwrap_or(0);
pending.count = Some(n.saturating_mul(10).saturating_add(digit));
}
}
#[must_use]
pub const fn pending_count(&self) -> Option<u32> {
match self {
Self::Normal { pending } => pending.count,
_ => None,
}
}
#[must_use]
pub const fn pending_operator(&self) -> Option<Operator> {
match self {
Self::Normal { pending } => pending.operator,
_ => None,
}
}
#[must_use]
pub fn consume_count(&mut self) -> u32 {
match self {
Self::Normal { pending } => pending.count.take().unwrap_or(1).max(1),
_ => 1,
}
}
pub fn clear_count(&mut self) {
if let Self::Normal { pending } = self {
pending.count = None;
}
}
#[must_use]
pub fn consume_operator(&mut self) -> Option<Operator> {
match self {
Self::Normal { pending } => pending.operator.take(),
_ => None,
}
}
#[must_use]
pub fn minibuffer(&self) -> &str {
match self {
Self::Command { line } => line.text(),
_ => "",
}
}
pub fn push_minibuffer(&mut self, ch: char) {
if let Self::Command { line } = self {
line.insert(ch);
}
}
pub fn move_minibuffer_caret(&mut self, to: CaretMove) {
if let Self::Command { line } = self {
line.move_caret(to);
}
}
pub fn delete_minibuffer_at_caret(&mut self) {
if let Self::Command { line } = self {
line.delete();
}
}
#[must_use]
pub fn minibuffer_caret(&self) -> usize {
match self {
Self::Command { line } => line.caret(),
_ => 0,
}
}
pub fn pop_minibuffer(&mut self) -> Option<char> {
match self {
Self::Command { line } => line.backspace(),
_ => None,
}
}
pub fn push_minibuffer_str(&mut self, s: &str) {
if let Self::Command { line } = self {
line.push_str(s);
}
}
pub fn clear_minibuffer(&mut self) {
if let Self::Command { line } = self {
line.clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_clean_normal() {
let s = ModalState::new();
assert_eq!(s.mode(), Mode::Normal);
assert_eq!(s.pending_count(), None);
assert_eq!(s.pending_operator(), None);
}
#[test]
fn enter_transitions_set_mode() {
let mut s = ModalState::new();
s.enter_insert();
assert_eq!(s.mode(), Mode::Insert);
s.enter_visual();
assert_eq!(s.mode(), Mode::Visual);
s.enter_visual_line();
assert_eq!(s.mode(), Mode::VisualLine);
s.enter_command();
assert_eq!(s.mode(), Mode::Command);
s.escape();
assert_eq!(s.mode(), Mode::Normal);
}
#[test]
fn enter_by_mode_value_dispatches() {
for m in [
Mode::Normal,
Mode::Insert,
Mode::Visual,
Mode::VisualLine,
Mode::Command,
] {
let mut s = ModalState::new();
s.enter(m);
assert_eq!(s.mode(), m);
}
}
#[test]
fn leaving_normal_structurally_drops_pending() {
let mut s = ModalState::new();
s.set_operator(Operator::Delete);
s.append_count(5);
assert_eq!(s.pending_operator(), Some(Operator::Delete));
assert_eq!(s.pending_count(), Some(5));
s.enter_insert();
assert_eq!(s.pending_operator(), None);
assert_eq!(s.pending_count(), None);
}
#[test]
fn pending_ops_are_noops_outside_normal() {
let mut s = ModalState::new();
s.enter_insert();
s.set_operator(Operator::Yank);
s.append_count(9);
assert_eq!(s.pending_operator(), None);
assert_eq!(s.pending_count(), None);
assert_eq!(s.consume_count(), 1, "no count exists outside Normal");
assert_eq!(s.consume_operator(), None);
}
#[test]
fn minibuffer_is_noop_outside_command() {
let mut s = ModalState::new();
s.enter_insert();
s.push_minibuffer('w');
assert_eq!(s.minibuffer(), "", "insert mode has no minibuffer");
assert_eq!(s.pop_minibuffer(), None);
}
#[test]
fn entering_command_clears_prior_minibuffer() {
let mut s = ModalState::new();
s.enter_command();
s.push_minibuffer('q');
assert_eq!(s.minibuffer(), "q");
s.enter_command();
assert_eq!(s.minibuffer(), "");
}
#[test]
fn normal_resets_pending_state() {
let mut s = ModalState::new();
s.enter_insert();
s.set_operator(Operator::Delete);
s.append_count(5);
s.enter_normal();
assert!(s.pending_count().is_none());
assert!(s.pending_operator().is_none());
}
#[test]
fn count_accumulates() {
let mut s = ModalState::new();
s.append_count(5);
s.append_count(3);
assert_eq!(s.consume_count(), 53);
assert_eq!(s.consume_count(), 1); }
#[test]
fn operator_round_trip() {
let mut s = ModalState::new();
s.set_operator(Operator::Yank);
assert_eq!(s.consume_operator(), Some(Operator::Yank));
assert_eq!(s.consume_operator(), None);
}
#[test]
fn minibuffer_append_pop() {
let mut s = ModalState::new();
s.enter_command();
s.push_minibuffer('w');
assert_eq!(s.minibuffer(), "w");
assert_eq!(s.pop_minibuffer(), Some('w'));
}
#[test]
fn minibuffer_str_and_clear() {
let mut s = ModalState::new();
s.enter_command();
s.push_minibuffer_str("__quit__");
assert!(s.minibuffer().contains("__quit__"));
s.clear_minibuffer();
assert_eq!(s.minibuffer(), "");
}
#[test]
fn serde_round_trip_per_variant() {
for s in [
ModalState::new(),
{
let mut n = ModalState::new();
n.append_count(12);
n.set_operator(Operator::Delete);
n
},
ModalState::Insert,
ModalState::Visual,
ModalState::VisualLine,
{
let mut c = ModalState::new();
c.enter_command();
c.push_minibuffer('x');
c
},
] {
let json = serde_json::to_string(&s).unwrap();
let back: ModalState = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
}
}
#[cfg(test)]
mod ex_line_tests {
use super::*;
#[test]
fn clearing_the_ex_line_brings_the_caret_home() {
let mut s = ModalState::new();
s.enter_command();
for ch in "foo".chars() {
s.push_minibuffer(ch);
}
assert_eq!(s.minibuffer_caret(), 3);
s.clear_minibuffer();
assert_eq!(s.minibuffer(), "");
assert_eq!(s.minibuffer_caret(), 0, "the caret is half of the value");
s.push_minibuffer('x');
assert_eq!(s.minibuffer(), "x");
assert_eq!(
s.minibuffer_caret(),
1,
"and one char in means caret 1, not 4"
);
}
#[test]
fn the_caret_never_exceeds_the_line_it_indexes() {
let mut line = ExLine::default();
for ch in "héllo".chars() {
line.insert(ch);
}
line.move_caret(CaretMove::Start);
line.delete();
line.backspace();
line.move_caret(CaretMove::End);
line.push_str("!");
line.clear();
line.insert('a');
assert!(line.caret() <= line.len_chars());
assert_eq!(line.text(), "a");
}
#[test]
fn the_published_wire_shape_survives_the_extraction() {
let mut s = ModalState::new();
s.enter_command();
s.push_minibuffer('w');
s.push_minibuffer('q');
s.move_minibuffer_caret(CaretMove::Left);
let v: serde_json::Value = serde_json::to_value(&s).unwrap();
assert_eq!(v["mode"], "Command");
assert_eq!(v["minibuffer"], "wq", "NOT nested under a `line` key");
assert_eq!(v["caret"], 1);
assert_eq!(serde_json::from_value::<ModalState>(v).unwrap(), s);
}
#[test]
fn a_caret_past_the_end_is_clamped_at_the_parse_boundary() {
let s: ModalState =
serde_json::from_str(r#"{"mode":"Command","minibuffer":"ab","caret":99}"#).unwrap();
assert_eq!(s.minibuffer(), "ab");
assert_eq!(s.minibuffer_caret(), 2);
}
}