From e80dfbd328b9bd9bf012bdf214cbaaeaef9f1496 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Wed, 25 Feb 2026 15:54:31 -0500 Subject: [PATCH] Implemented proper behavior for deleting/yanking lines into registers and putting them --- src/prompt/readline/linebuf.rs | 57 +++++++++--- src/prompt/readline/register.rs | 159 ++++++++++++++++++++++++-------- src/prompt/readline/vicmd.rs | 6 +- 3 files changed, 167 insertions(+), 55 deletions(-) diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index cf32a82..030c315 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -14,7 +14,7 @@ use crate::{ libsh::{ error::ShResult, term::{Style, Styled}, - }, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::write_register}, state::read_shopts + }, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -2441,7 +2441,7 @@ impl LineBuf { do_indent = read_shopts(|o| o.prompt.auto_indent); } - let register_text = if verb == Verb::Yank { + let text = if verb == Verb::Yank { self .slice(start..end) .map(|c| c.to_string()) @@ -2451,7 +2451,16 @@ impl LineBuf { self.update_graphemes(); drained }; - register.write_to_register(register_text); + let is_linewise = matches!( + motion, + MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..) + ); + let register_content = if is_linewise { + RegisterContent::Line(text) + } else { + RegisterContent::Span(text) + }; + register.write_to_register(register_content); self.cursor.set(start); if do_indent { self.calc_indent_level(); @@ -2630,22 +2639,46 @@ impl LineBuf { let Some(content) = register.read_from_register() else { return Ok(()); }; + if content.is_empty() { + return Ok(()); + } if let Some(range) = self.select_range { let register_text = self.drain_inclusive(range.0..=range.1); - write_register(None, register_text); // swap deleted text into register + write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register - self.insert_str_at(range.0, &content); - self.cursor.set(range.0 + content.chars().count()); + let text = content.as_str(); + self.insert_str_at(range.0, text); + self.cursor.set(range.0 + content.char_count()); self.select_range = None; self.update_graphemes(); return Ok(()); } - let insert_idx = match anchor { - Anchor::After => self.cursor.ret_add(1), - Anchor::Before => self.cursor.get(), - }; - self.insert_str_at(insert_idx, &content); - self.cursor.add(content.len().saturating_sub(1)); + match content { + RegisterContent::Span(ref text) => { + let insert_idx = match anchor { + Anchor::After => self.cursor.ret_add(1), + Anchor::Before => self.cursor.get(), + }; + self.insert_str_at(insert_idx, text); + self.cursor.add(text.len().saturating_sub(1)); + } + RegisterContent::Line(ref text) => { + let insert_idx = match anchor { + Anchor::After => self.end_of_line(), + Anchor::Before => self.start_of_line(), + }; + let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n"); + if needs_newline { + let full = format!("\n{}", text); + self.insert_str_at(insert_idx, &full); + self.cursor.set(insert_idx + 1); + } else { + self.insert_str_at(insert_idx, text); + self.cursor.set(insert_idx); + } + } + RegisterContent::Empty => {} + } } Verb::SwapVisualAnchor => { if let Some((start, end)) = self.select_range() diff --git a/src/prompt/readline/register.rs b/src/prompt/readline/register.rs index b1e0e0d..1fd8581 100644 --- a/src/prompt/readline/register.rs +++ b/src/prompt/readline/register.rs @@ -1,26 +1,84 @@ -use std::sync::Mutex; +use std::{fmt::Display, sync::Mutex}; pub static REGISTERS: Mutex = Mutex::new(Registers::new()); -pub fn read_register(ch: Option) -> Option { +pub fn read_register(ch: Option) -> Option { let lock = REGISTERS.lock().unwrap(); - lock.get_reg(ch).map(|r| r.buf().clone()) + lock.get_reg(ch).map(|r| r.content().clone()) } -pub fn write_register(ch: Option, buf: String) { +pub fn write_register(ch: Option, buf: RegisterContent) { let mut lock = REGISTERS.lock().unwrap(); if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } } -pub fn append_register(ch: Option, buf: String) { +pub fn append_register(ch: Option, buf: RegisterContent) { let mut lock = REGISTERS.lock().unwrap(); if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } } +#[derive(Default, Clone, Debug)] +pub enum RegisterContent { + Span(String), + Line(String), + #[default] + Empty, +} + +impl Display for RegisterContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Span(s) => write!(f, "{}", s), + Self::Line(s) => write!(f, "{}", s), + Self::Empty => write!(f, ""), + } + } +} + +impl RegisterContent { + pub fn clear(&mut self) { + match self { + Self::Span(s) => s.clear(), + Self::Line(s) => s.clear(), + Self::Empty => {} + } + } + pub fn len(&self) -> usize { + match self { + Self::Span(s) => s.len(), + Self::Line(s) => s.len(), + Self::Empty => 0, + } + } + pub fn is_empty(&self) -> bool { + match self { + Self::Span(s) => s.is_empty(), + Self::Line(s) => s.is_empty(), + Self::Empty => true, + } + } + pub fn is_line(&self) -> bool { + matches!(self, Self::Line(_)) + } + pub fn is_span(&self) -> bool { + matches!(self, Self::Span(_)) + } + pub fn as_str(&self) -> &str { + match self { + Self::Span(s) => s, + Self::Line(s) => s, + Self::Empty => "", + } + } + pub fn char_count(&self) -> usize { + self.as_str().chars().count() + } +} + #[derive(Default, Debug)] pub struct Registers { default: Register, @@ -55,33 +113,33 @@ pub struct Registers { impl Registers { pub const fn new() -> Self { Self { - default: Register(String::new()), - a: Register(String::new()), - b: Register(String::new()), - c: Register(String::new()), - d: Register(String::new()), - e: Register(String::new()), - f: Register(String::new()), - g: Register(String::new()), - h: Register(String::new()), - i: Register(String::new()), - j: Register(String::new()), - k: Register(String::new()), - l: Register(String::new()), - m: Register(String::new()), - n: Register(String::new()), - o: Register(String::new()), - p: Register(String::new()), - q: Register(String::new()), - r: Register(String::new()), - s: Register(String::new()), - t: Register(String::new()), - u: Register(String::new()), - v: Register(String::new()), - w: Register(String::new()), - x: Register(String::new()), - y: Register(String::new()), - z: Register(String::new()), + default: Register::new(), + a: Register::new(), + b: Register::new(), + c: Register::new(), + d: Register::new(), + e: Register::new(), + f: Register::new(), + g: Register::new(), + h: Register::new(), + i: Register::new(), + j: Register::new(), + k: Register::new(), + l: Register::new(), + m: Register::new(), + n: Register::new(), + o: Register::new(), + p: Register::new(), + q: Register::new(), + r: Register::new(), + s: Register::new(), + t: Register::new(), + u: Register::new(), + v: Register::new(), + w: Register::new(), + x: Register::new(), + y: Register::new(), + z: Register::new(), } } pub fn get_reg(&self, ch: Option) -> Option<&Register> { @@ -155,18 +213,39 @@ impl Registers { } #[derive(Clone, Default, Debug)] -pub struct Register(String); +pub struct Register { + content: RegisterContent, +} + impl Register { - pub fn buf(&self) -> &String { - &self.0 + pub const fn new() -> Self { + Self { + content: RegisterContent::Span(String::new()), + } } - pub fn write(&mut self, buf: String) { - self.0 = buf + pub fn content(&self) -> &RegisterContent { + &self.content } - pub fn append(&mut self, buf: String) { - self.0.push_str(&buf) + pub fn write(&mut self, buf: RegisterContent) { + self.content = buf + } + pub fn append(&mut self, buf: RegisterContent) { + match buf { + RegisterContent::Empty => {} + RegisterContent::Span(ref s) | RegisterContent::Line(ref s) => match &mut self.content { + RegisterContent::Empty => self.content = buf, + RegisterContent::Span(existing) => existing.push_str(s), + RegisterContent::Line(existing) => existing.push_str(s), + }, + } } pub fn clear(&mut self) { - self.0.clear() + self.content.clear() + } + pub fn is_line(&self) -> bool { + self.content.is_line() + } + pub fn is_span(&self) -> bool { + self.content.is_span() } } diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index 0ced843..7959b82 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -1,6 +1,6 @@ use bitflags::bitflags; -use super::register::{append_register, read_register, write_register}; +use super::register::{append_register, read_register, write_register, RegisterContent}; //TODO: write tests that take edit results and cursor positions from actual // neovim edits and test them against the behavior of this editor @@ -35,14 +35,14 @@ impl RegisterName { pub fn count(&self) -> usize { self.count } - pub fn write_to_register(&self, buf: String) { + pub fn write_to_register(&self, buf: RegisterContent) { if self.append { append_register(self.name, buf); } else { write_register(self.name, buf); } } - pub fn read_from_register(&self) -> Option { + pub fn read_from_register(&self) -> Option { read_register(self.name) } }