use crate::buffer::LineBuffer;
use crate::display::Renderer;
use crate::terminal::WinSize;
use std::os::unix::io::RawFd;
const RING_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct KillRing {
slots: Vec<Option<String>>,
index: usize,
}
impl Default for KillRing {
fn default() -> Self {
Self::new()
}
}
impl KillRing {
#[must_use]
pub fn new() -> Self {
KillRing {
slots: vec![None; RING_SIZE],
index: 0,
}
}
pub fn push(&mut self, text: String) {
if text.is_empty() {
return;
}
self.index = (self.index + 1) % RING_SIZE;
self.slots[self.index] = Some(text);
}
#[must_use]
pub fn yank(&self) -> Option<&str> {
self.slots[self.index].as_deref()
}
pub fn rotate(&mut self) {
for _ in 0..RING_SIZE {
self.index = (self.index + RING_SIZE - 1) % RING_SIZE;
if self.slots[self.index].is_some() {
return;
}
}
}
}
#[derive(Debug, Clone)]
pub struct CompletionCycle {
pub start: usize,
pub end: usize,
pub candidates: Vec<String>,
pub index: Option<usize>,
}
pub struct EditorState {
pub buffer: LineBuffer,
pub kill_ring: KillRing,
pub renderer: Renderer,
pub prompt: String,
pub continuation: String,
pub accumulated: String,
pub history_index: usize,
pub scratch: String,
pub last_yank: Option<(usize, usize)>,
pub ifd: RawFd,
pub ofd: RawFd,
pub paste_mode: bool,
pub paused: bool,
pub mask: bool,
pub escape_next: bool,
pub ctrl_x_pending: bool,
pub completion: Option<CompletionCycle>,
pub dirty: bool,
}
impl EditorState {
#[must_use]
pub fn new(
prompt: &str,
continuation: &str,
ifd: RawFd,
ofd: RawFd,
win_size: WinSize,
) -> Self {
EditorState {
buffer: LineBuffer::new(),
kill_ring: KillRing::new(),
renderer: Renderer::new(win_size),
prompt: prompt.to_string(),
continuation: continuation.to_string(),
accumulated: String::new(),
history_index: 0,
scratch: String::new(),
last_yank: None,
ifd,
ofd,
paste_mode: false,
paused: false,
mask: false,
escape_next: false,
ctrl_x_pending: false,
completion: None,
dirty: true,
}
}
#[must_use]
pub fn full_text(&self) -> String {
let mut text = self.accumulated.clone();
text.push_str(self.buffer.as_str());
text
}
pub fn begin_continuation(&mut self) {
self.accumulated.push_str(self.buffer.as_str());
self.accumulated.push('\n');
self.buffer.clear();
self.prompt = self.continuation.clone();
self.completion = None;
self.dirty = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_yanks_the_most_recent_kill() {
let mut ring = KillRing::new();
assert_eq!(ring.yank(), None);
ring.push("one".into());
assert_eq!(ring.yank(), Some("one"));
ring.push("two".into());
assert_eq!(ring.yank(), Some("two"));
}
#[test]
fn ring_ignores_empty_kills() {
let mut ring = KillRing::new();
ring.push(String::new());
assert_eq!(ring.yank(), None);
}
#[test]
fn rotate_walks_backwards_through_kills() {
let mut ring = KillRing::new();
for text in ["a", "b", "c"] {
ring.push(text.into());
}
assert_eq!(ring.yank(), Some("c"));
ring.rotate();
assert_eq!(ring.yank(), Some("b"));
ring.rotate();
assert_eq!(ring.yank(), Some("a"));
ring.rotate();
assert_eq!(ring.yank(), Some("c"));
}
#[test]
fn rotate_on_an_empty_ring_is_harmless() {
let mut ring = KillRing::new();
ring.rotate();
assert_eq!(ring.yank(), None);
}
#[test]
fn ring_evicts_after_eight_kills() {
let mut ring = KillRing::new();
for i in 0..10 {
ring.push(format!("kill{i}"));
}
assert_eq!(ring.yank(), Some("kill9"));
for _ in 0..RING_SIZE {
ring.rotate();
}
assert_eq!(ring.yank(), Some("kill9"));
}
#[test]
fn continuation_accumulates_lines() {
let mut state = EditorState::new("> ", "... ", 0, 1, WinSize::default());
state.buffer.insert("first");
state.begin_continuation();
assert_eq!(state.prompt, "... ");
assert_eq!(state.buffer.as_str(), "");
state.buffer.insert("second");
assert_eq!(state.full_text(), "first\nsecond");
assert!(state.dirty, "a new continuation line needs drawing");
}
}