Merge pull request #2 from km-clay/readline_refactor

Readline refactor
This commit is contained in:
2025-06-09 02:34:17 -04:00
committed by GitHub
13 changed files with 3897 additions and 2820 deletions

View File

@@ -13,13 +13,15 @@ debug = true
bitflags = "2.8.0"
clap = { version = "4.5.38", features = ["derive"] }
glob = "0.3.2"
insta = "1.42.2"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
pretty_assertions = "1.4.1"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] }
regex = "1.11.1"
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"
[dev-dependencies]
insta = "1.42.2"
pretty_assertions = "1.4.1"
[[bin]]
name = "fern"
path = "src/fern.rs"

View File

@@ -103,7 +103,7 @@ fn fern_interactive() {
.unwrap()
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
.unwrap();
let input = match prompt::read_line(edit_mode) {
let input = match prompt::readline(edit_mode) {
Ok(line) => {
readline_err_count = 0;
line

View File

@@ -22,7 +22,7 @@ fn get_prompt() -> ShResult<String> {
expand_prompt(&prompt)
}
pub fn read_line(edit_mode: FernEditMode) -> ShResult<String> {
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
let prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),

View File

@@ -1,6 +1,6 @@
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::prelude::*;
use super::vicmd::Direction; // surprisingly useful
@@ -206,6 +206,10 @@ impl History {
&self.entries
}
pub fn masked_entries(&self) -> &[HistEntry] {
&self.search_mask
}
pub fn push_empty_entry(&mut self) {
}
@@ -245,6 +249,7 @@ impl History {
}
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
flog!(DEBUG,constraint);
let SearchConstraint { kind, term } = constraint;
match kind {
SearchKind::Prefix => {
@@ -273,7 +278,7 @@ impl History {
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) {
let entry = self.hint_entry()?;
let prefix = self.cursor_entry()?.command();
Some(entry.command().strip_prefix(prefix)?.to_string())
Some(entry.command().to_string())
} else {
None
}

View File

@@ -3,7 +3,7 @@ use unicode_segmentation::UnicodeSegmentation;
// Credit to Rustyline for the design ideas in this module
// https://github.com/kkawakam/rustyline
#[derive(Clone,Debug)]
#[derive(Clone,PartialEq,Eq,Debug)]
pub struct KeyEvent(pub KeyCode, pub ModKeys);
@@ -92,7 +92,7 @@ impl KeyEvent {
}
}
#[derive(Clone,Debug)]
#[derive(Clone,PartialEq,Eq,Debug)]
pub enum KeyCode {
UnknownEscSeq,
Backspace,

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +1,186 @@
use std::time::Duration;
use history::{History, SearchConstraint, SearchKind};
use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{strip_ansi_codes_and_escapes, LineBuf, SelectionAnchor, SelectionMode};
use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use term::Terminal;
use unicode_width::UnicodeWidthStr;
use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use linebuf::{LineBuf, SelectAnchor, SelectMode};
use nix::libc::STDOUT_FILENO;
use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter};
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
use crate::prelude::*;
pub mod keys;
pub mod term;
pub mod linebuf;
pub mod layout;
pub mod keys;
pub mod vicmd;
pub mod mode;
pub mod register;
pub mod vimode;
pub mod history;
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nmagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\nconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
/*
* Known issues:
* If the line buffer scrolls past the terminal height, shit gets fucked
* the cursor sometimes spazzes out during redraw, but ends up in the right place
*/
/// Unified interface for different line editing methods
pub trait Readline {
fn readline(&mut self) -> ShResult<String>;
}
pub struct FernVi {
term: Terminal,
line: LineBuf,
history: History,
prompt: String,
mode: Box<dyn ViMode>,
last_action: Option<CmdReplay>,
last_movement: Option<MotionCmd>,
pub reader: Box<dyn KeyReader>,
pub writer: Box<dyn LineWriter>,
pub prompt: String,
pub mode: Box<dyn ViMode>,
pub old_layout: Option<Layout>,
pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf,
pub history: History
}
impl Readline for FernVi {
fn readline(&mut self) -> ShResult<String> {
/* a monument to the insanity of debugging this shit
self.term.writeln("This is a line!");
self.term.writeln("This is a line!");
self.term.writeln("This is a line!");
let prompt_thing = "prompt thing -> ";
self.term.write(prompt_thing);
let line = "And another!";
let mut iters: usize = 0;
let mut newlines_written = 0;
loop {
iters += 1;
for i in 0..iters {
self.term.writeln(line);
}
std::thread::sleep(Duration::from_secs(1));
self.clear_lines(iters,prompt_thing.len() + 1);
}
panic!()
*/
self.print_buf(false)?;
loop {
let key = self.term.read_key();
let raw_mode_guard = raw_mode(); // Restores termios state on drop
loop {
raw_mode_guard.disable_for(|| self.print_line())?;
let Some(key) = self.reader.read_key() else {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard);
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"))
};
flog!(DEBUG, key);
if let KeyEvent(KeyCode::Char('V'), ModKeys::CTRL) = key {
self.handle_verbatim()?;
continue
}
if self.should_accept_hint(&key) {
self.line.accept_hint();
self.history.update_pending_cmd(self.line.as_str());
self.print_buf(true)?;
self.editor.accept_hint();
self.history.update_pending_cmd(self.editor.as_str());
self.print_line()?;
continue
}
let Some(cmd) = self.mode.handle_key(key) else {
let Some(mut cmd) = self.mode.handle_key(key) else {
continue
};
cmd.alter_line_motion_if_no_verb();
if self.should_grab_history(&cmd) {
flog!(DEBUG, "scrolling");
self.scroll_history(cmd);
self.print_buf(true)?;
self.print_line()?;
continue
}
if cmd.should_submit() {
self.term.unposition_cursor()?;
self.term.write("\n");
let command = std::mem::take(&mut self.line).pack_line();
if !command.is_empty() {
// We're just going to trim the command
// reduces clutter in the case of two history commands whose only difference is insignificant whitespace
self.history.update_pending_cmd(&command);
self.history.save()?;
}
return Ok(command);
}
let line = self.line.to_string();
self.exec_cmd(cmd.clone())?;
let new_line = self.line.as_str();
let has_changes = line != new_line;
flog!(DEBUG, has_changes);
if has_changes {
self.history.update_pending_cmd(self.line.as_str());
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard);
return Ok(self.editor.take_buf())
}
self.print_buf(true)?;
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() {
std::mem::drop(raw_mode_guard);
sh_quit(0);
} else {
self.editor.buffer.clear();
continue
}
}
flog!(DEBUG,cmd);
let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?;
let after = self.editor.as_str();
if before != after {
self.history.update_pending_cmd(self.editor.as_str());
}
let hint = self.history.get_hint();
self.editor.set_hint(hint);
}
}
}
impl FernVi {
pub fn new(prompt: Option<String>) -> ShResult<Self> {
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
let line = LineBuf::new();//.with_initial(LOREM_IPSUM);
let term = Terminal::new();
let history = History::new()?;
Ok(Self {
term,
line,
history,
prompt,
reader: Box::new(TermReader::new()),
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
mode: Box::new(ViInsert::new()),
last_action: None,
last_movement: None,
old_layout: None,
repeat_action: None,
repeat_motion: None,
editor: LineBuf::new().with_initial("this buffer has (some delimited) text", 0),
history: History::new()?
})
}
pub fn get_layout(&mut self) -> Layout {
let line = self.editor.to_string();
flog!(DEBUG,line);
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols,_) = get_win_size(STDIN_FILENO);
Layout::from_parts(
/*tab_stop:*/ 8,
cols,
&self.prompt,
to_cursor,
&line
)
}
pub fn scroll_history(&mut self, cmd: ViCmd) {
flog!(DEBUG,"scrolling");
/*
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
self.history.constrain_entries(constraint);
}
*/
let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1;
flog!(DEBUG,count,motion);
flog!(DEBUG,self.history.masked_entries());
let entry = match motion {
Motion::LineUpCharwise => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
return
};
flog!(DEBUG,"found entry");
flog!(DEBUG,hist_entry.command());
hist_entry
}
Motion::LineDownCharwise => {
let Some(hist_entry) = self.history.scroll(*count as isize) else {
return
};
flog!(DEBUG,"found entry");
flog!(DEBUG,hist_entry.command());
hist_entry
}
_ => unreachable!()
};
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
let mut buf = LineBuf::new().with_initial(entry.command(),0);
let line_end = buf.end_of_line();
if let Some(dest) = self.mode.hist_scroll_start_pos() {
match dest {
To::Start => {
/* Already at 0 */
}
To::End => {
// History entries cannot be empty
// So this subtraction is safe (maybe)
buf.cursor.add(line_end);
}
}
} else {
let target = (col).min(line_end);
buf.cursor.add(target);
}
self.editor = buf
}
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.line.at_end_of_buffer() && self.line.has_hint() {
flog!(DEBUG,self.editor.cursor_at_max());
flog!(DEBUG,self.editor.cursor);
if self.editor.cursor_at_max() && self.editor.has_hint() {
match self.mode.report_mode() {
ModeReport::Replace |
ModeReport::Insert => {
@@ -164,211 +209,97 @@ impl FernVi {
false
}
}
/// Ctrl+V handler
pub fn handle_verbatim(&mut self) -> ShResult<()> {
let mut buf = [0u8; 8];
let mut collected = Vec::new();
loop {
let n = self.term.read_byte(&mut buf[..1]);
if n == 0 {
continue;
}
collected.push(buf[0]);
// If it starts with ESC, treat as escape sequence
if collected[0] == 0x1b {
loop {
let n = self.term.peek_byte(&mut buf[..1]);
if n == 0 {
break
}
collected.push(buf[0]);
// Ends a CSI sequence
if (0x40..=0x7e).contains(&buf[0]) {
break;
}
}
let Ok(seq) = std::str::from_utf8(&collected) else {
return Ok(())
};
let cmd = ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))),
motion: None,
raw_seq: seq.to_string(),
};
self.line.exec_cmd(cmd)?;
}
// Optional: handle other edge cases, e.g., raw control codes
if collected[0] < 0x20 || collected[0] == 0x7F {
let ctrl_seq = std::str::from_utf8(&collected).unwrap();
let cmd = ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::Insert(ctrl_seq.to_string()))),
motion: None,
raw_seq: ctrl_seq.to_string(),
};
self.line.exec_cmd(cmd)?;
break;
}
// Try to parse as UTF-8 if it's a valid Unicode sequence
if let Ok(s) = std::str::from_utf8(&collected) {
if s.chars().count() == 1 {
let ch = s.chars().next().unwrap();
// You got a literal Unicode char
eprintln!("Got char: {:?}", ch);
break;
}
}
}
Ok(())
}
pub fn scroll_history(&mut self, cmd: ViCmd) {
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
let constraint = SearchConstraint::new(SearchKind::Prefix, self.line.to_string());
self.history.constrain_entries(constraint);
}
let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1;
flog!(DEBUG,count,motion);
let entry = match motion {
Motion::LineUp => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
return
};
flog!(DEBUG,"found entry");
flog!(DEBUG,hist_entry.command());
hist_entry
}
Motion::LineDown => {
let Some(hist_entry) = self.history.scroll(*count as isize) else {
return
};
flog!(DEBUG,"found entry");
flog!(DEBUG,hist_entry.command());
hist_entry
}
_ => unreachable!()
};
let col = self.line.saved_col().unwrap_or(self.line.cursor_column());
let mut buf = LineBuf::new().with_initial(entry.command());
let line_end = buf.end_of_line();
if let Some(dest) = self.mode.hist_scroll_start_pos() {
match dest {
To::Start => {
/* Already at 0 */
}
To::End => {
// History entries cannot be empty
// So this subtraction is safe (maybe)
buf.cursor_fwd_to(line_end + 1);
}
}
} else {
let target = (col + 1).min(line_end + 1);
buf.cursor_fwd_to(target);
}
self.line = buf
}
pub fn should_grab_history(&self, cmd: &ViCmd) -> bool {
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
cmd.verb().is_none() &&
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) &&
self.line.start_of_line() == 0
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) &&
self.editor.start_of_line() == 0
) ||
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) &&
self.line.end_of_line() == self.line.byte_len()
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) &&
self.editor.end_of_line() == self.editor.cursor_max() &&
!self.history.cursor_entry().is_some_and(|ent| ent.is_new())
)
}
pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> {
let (height,width) = self.term.get_dimensions()?;
if refresh {
self.term.unwrite()?;
pub fn print_line(&mut self) -> ShResult<()> {
let new_layout = self.get_layout();
if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?;
}
let hint = self.history.get_hint();
self.line.set_hint(hint);
let offset = self.calculate_prompt_offset();
self.line.set_first_line_offset(offset);
self.line.update_term_dims((height,width));
let mut line_buf = self.prompt.clone();
line_buf.push_str(&self.line.to_string());
self.writer.redraw(
&self.prompt,
&self.editor,
&new_layout
)?;
self.term.recorded_write(&line_buf, offset)?;
self.term.position_cursor(self.line.cursor_display_coords(width))?;
self.writer.flush_write(&self.mode.cursor_style())?;
self.term.write(&self.mode.cursor_style());
self.old_layout = Some(new_layout);
Ok(())
}
pub fn calculate_prompt_offset(&self) -> usize {
if self.prompt.ends_with('\n') {
return 0
}
strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() + 1 // 1 indexed
}
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut selecting = false;
let mut is_insert_mode = false;
if cmd.is_mode_transition() {
let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
Verb::Change |
Verb::InsertModeLineBreak(_) |
Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::NormalMode => {
Box::new(ViNormal::new())
}
Verb::ReplaceMode => {
Box::new(ViReplace::new().with_count(count as u16))
}
Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual {
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
}
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
std::mem::swap(&mut mode, &mut self.mode);
self.line.set_cursor_clamp(self.mode.clamp_cursor());
self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo());
self.term.write(&mode.cursor_style());
return self.line.exec_cmd(cmd)
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
return self.editor.exec_cmd(cmd)
}
Verb::VisualMode => {
selecting = true;
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
Box::new(ViVisual::new())
}
_ => unreachable!()
};
flog!(DEBUG, self.mode.report_mode());
flog!(DEBUG, mode.report_mode());
std::mem::swap(&mut mode, &mut self.mode);
flog!(DEBUG, self.mode.report_mode());
self.line.set_cursor_clamp(self.mode.clamp_cursor());
self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo());
self.term.write(&mode.cursor_style());
if mode.is_repeatable() {
self.last_action = mode.as_replay();
self.repeat_action = mode.as_replay();
}
self.line.exec_cmd(cmd)?;
self.editor.exec_cmd(cmd)?;
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting {
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
} else {
self.line.stop_selecting();
self.editor.stop_selecting();
}
if is_insert_mode {
self.editor.mark_insert_mode_start_pos();
} else {
self.editor.clear_insert_mode_start_pos();
}
return Ok(())
} else if cmd.is_cmd_repeat() {
let Some(replay) = self.last_action.clone() else {
let Some(replay) = self.repeat_action.clone() else {
return Ok(())
};
let ViCmd { verb, .. } = cmd;
@@ -381,7 +312,7 @@ impl FernVi {
for _ in 0..repeat {
let cmds = cmds.clone();
for cmd in cmds {
self.line.exec_cmd(cmd)?
self.editor.exec_cmd(cmd)?
}
}
}
@@ -399,7 +330,7 @@ impl FernVi {
return Ok(()) // it has to have a verb to be repeatable, something weird happened
}
}
self.line.exec_cmd(cmd)?;
self.editor.exec_cmd(cmd)?;
}
_ => unreachable!("motions should be handled in the other branch")
}
@@ -407,19 +338,20 @@ impl FernVi {
} else if cmd.is_motion_repeat() {
match cmd.motion.as_ref().unwrap() {
MotionCmd(count,Motion::RepeatMotion) => {
let Some(motion) = self.last_movement.clone() else {
let Some(motion) = self.repeat_motion.clone() else {
return Ok(())
};
let repeat_cmd = ViCmd {
register: RegisterName::default(),
verb: None,
motion: Some(motion),
raw_seq: format!("{count};")
raw_seq: format!("{count};"),
flags: CmdFlags::empty()
};
return self.line.exec_cmd(repeat_cmd);
return self.editor.exec_cmd(repeat_cmd);
}
MotionCmd(count,Motion::RepeatMotionRev) => {
let Some(motion) = self.last_movement.clone() else {
let Some(motion) = self.repeat_motion.clone() else {
return Ok(())
};
let mut new_motion = motion.invert_char_motion();
@@ -428,9 +360,10 @@ impl FernVi {
register: RegisterName::default(),
verb: None,
motion: Some(new_motion),
raw_seq: format!("{count},")
raw_seq: format!("{count},"),
flags: CmdFlags::empty()
};
return self.line.exec_cmd(repeat_cmd);
return self.editor.exec_cmd(repeat_cmd);
}
_ => unreachable!()
}
@@ -440,23 +373,24 @@ impl FernVi {
if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to assign it here
// in order to be able to repeat it
let range = self.line.selected_range().unwrap();
cmd.motion = Some(MotionCmd(1,Motion::Range(range.start, range.end)))
let range = self.editor.select_range().unwrap();
cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1)))
}
self.last_action = Some(CmdReplay::Single(cmd.clone()));
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
}
if cmd.is_char_search() {
self.last_movement = cmd.motion.clone()
self.repeat_motion = cmd.motion.clone()
}
self.line.exec_cmd(cmd.clone())?;
self.editor.exec_cmd(cmd.clone())?;
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
self.line.stop_selecting();
self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
std::mem::swap(&mut mode, &mut self.mode);
}
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,9 @@
use bitflags::bitflags;
use super::register::{append_register, read_register, write_register};
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor
#[derive(Clone,Copy,Debug)]
pub struct RegisterName {
name: Option<char>,
@@ -52,12 +56,22 @@ impl Default for RegisterName {
}
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CmdFlags: u32 {
const VISUAL = 1<<0;
const VISUAL_LINE = 1<<1;
const VISUAL_BLOCK = 1<<2;
}
}
#[derive(Clone,Default,Debug)]
pub struct ViCmd {
pub register: RegisterName,
pub verb: Option<VerbCmd>,
pub motion: Option<MotionCmd>,
pub raw_seq: String,
pub flags: CmdFlags,
}
impl ViCmd {
@@ -82,6 +96,15 @@ impl ViCmd {
pub fn motion_count(&self) -> usize {
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
}
pub fn normalize_counts(&mut self) {
let Some(verb) = self.verb.as_mut() else { return };
let Some(motion) = self.motion.as_mut() else { return };
let VerbCmd(v_count, _) = verb;
let MotionCmd(m_count, _) = motion;
let product = *v_count * *m_count;
verb.0 = 1;
motion.0 = product;
}
pub fn is_repeatable(&self) -> bool {
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
}
@@ -95,13 +118,36 @@ impl ViCmd {
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
}
pub fn should_submit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine))
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
}
pub fn is_undo_op(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
}
pub fn is_inplace_edit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) &&
self.motion.is_none()
}
pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown))
self.motion.as_ref().is_some_and(|m| {
matches!(m.1,
Motion::LineUp |
Motion::LineDown |
Motion::LineUpCharwise |
Motion::LineDownCharwise
)
})
}
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
pub fn alter_line_motion_if_no_verb(&mut self) {
if self.is_line_motion() && self.verb.is_none() {
if let Some(motion) = self.motion.as_mut() {
match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!()
}
}
}
}
pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| {
@@ -140,12 +186,13 @@ impl MotionCmd {
#[non_exhaustive]
pub enum Verb {
Delete,
DeleteChar(Anchor),
Change,
Yank,
ReplaceChar(char),
Substitute,
ToggleCase,
Rot13, // lol
ReplaceChar(char), // char to replace with, number of chars to replace
ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace
ToggleCaseInplace(u16), // Number of chars to toggle
ToggleCaseRange,
ToLower,
ToUpper,
Complete,
@@ -166,47 +213,31 @@ pub enum Verb {
JoinLines,
InsertChar(char),
Insert(String),
Breakline(Anchor),
Indent,
Dedent,
Equalize,
AcceptLine,
Rot13, // lol
Builder(VerbBuilder),
AcceptLineOrNewline,
EndOfFile
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum VerbBuilder {
}
impl Verb {
pub fn needs_motion(&self) -> bool {
matches!(self,
Self::Indent |
Self::Dedent |
Self::Delete |
Self::Change |
Self::Yank
)
}
pub fn is_repeatable(&self) -> bool {
matches!(self,
Self::Delete |
Self::DeleteChar(_) |
Self::Change |
Self::ReplaceChar(_) |
Self::Substitute |
Self::ReplaceCharInplace(_,_) |
Self::ToLower |
Self::ToUpper |
Self::ToggleCase |
Self::ToggleCaseRange |
Self::ToggleCaseInplace(_) |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Breakline(_) |
Self::Indent |
Self::Dedent |
Self::Equalize
@@ -215,11 +246,11 @@ impl Verb {
pub fn is_edit(&self) -> bool {
matches!(self,
Self::Delete |
Self::DeleteChar(_) |
Self::Change |
Self::ReplaceChar(_) |
Self::Substitute |
Self::ToggleCase |
Self::ReplaceCharInplace(_,_) |
Self::ToggleCaseRange |
Self::ToggleCaseInplace(_) |
Self::ToLower |
Self::ToUpper |
Self::RepeatLast |
@@ -229,7 +260,6 @@ impl Verb {
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Breakline(_) |
Self::Rot13 |
Self::EndOfFile
)
@@ -238,7 +268,8 @@ impl Verb {
matches!(self,
Self::Change |
Self::InsertChar(_) |
Self::ReplaceChar(_)
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_)
)
}
}
@@ -251,15 +282,20 @@ pub enum Motion {
BeginningOfFirstWord,
BeginningOfLine,
EndOfLine,
BackwardWord(To, Word),
ForwardWord(To, Word),
WordMotion(To,Word,Direction),
CharSearch(Direction,Dest,char),
BackwardChar,
ForwardChar,
BackwardCharForced, // These two variants can cross line boundaries
ForwardCharForced,
LineUp,
LineUpCharwise,
ScreenLineUp,
ScreenLineUpCharwise,
LineDown,
LineDownCharwise,
ScreenLineDown,
ScreenLineDownCharwise,
BeginningOfScreenLine,
FirstGraphicalOnScreenLine,
HalfOfScreen,
@@ -267,23 +303,65 @@ pub enum Motion {
WholeBuffer,
BeginningOfBuffer,
EndOfBuffer,
ToColumn(usize),
ToColumn,
ToDelimMatch,
ToBrace(Direction),
ToBracket(Direction),
ToParen(Direction),
Range(usize,usize),
Builder(MotionBuilder),
RepeatMotion,
RepeatMotionRev,
Null
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum MotionBuilder {
CharSearch(Option<Direction>,Option<Dest>,Option<char>),
TextObj(Option<TextObj>,Option<Bound>)
#[derive(Clone,Copy,PartialEq,Eq,Debug)]
pub enum MotionBehavior {
Exclusive,
Inclusive,
Linewise
}
impl Motion {
pub fn needs_verb(&self) -> bool {
matches!(self, Self::TextObj(_, _))
pub fn behavior(&self) -> MotionBehavior {
if self.is_linewise() {
MotionBehavior::Linewise
} else if self.is_exclusive() {
MotionBehavior::Exclusive
} else {
MotionBehavior::Inclusive
}
}
pub fn is_exclusive(&self) -> bool {
matches!(&self,
Self::BeginningOfLine |
Self::BeginningOfFirstWord |
Self::BeginningOfScreenLine |
Self::FirstGraphicalOnScreenLine |
Self::LineDownCharwise |
Self::LineUpCharwise |
Self::ScreenLineUpCharwise |
Self::ScreenLineDownCharwise |
Self::ToColumn |
Self::TextObj(TextObj::Sentence(_),_) |
Self::TextObj(TextObj::Paragraph(_),_) |
Self::CharSearch(Direction::Backward, _, _) |
Self::WordMotion(To::Start,_,_) |
Self::ToBrace(_) |
Self::ToBracket(_) |
Self::ToParen(_) |
Self::ScreenLineDown |
Self::ScreenLineUp |
Self::Range(_, _)
)
}
pub fn is_linewise(&self) -> bool {
matches!(self,
Self::WholeLine |
Self::LineUp |
Self::LineDown |
Self::ScreenLineDown |
Self::ScreenLineUp
)
}
}
@@ -297,14 +375,11 @@ pub enum TextObj {
/// `iw`, `aw` — inner word, around word
Word(Word),
/// for stuff like 'dd'
Line,
/// `is`, `as` — inner sentence, around sentence
Sentence,
Sentence(Direction),
/// `ip`, `ap` — inner paragraph, around paragraph
Paragraph,
Paragraph(Direction),
/// `i"`, `a"` — inner/around double quotes
DoubleQuote,

View File

@@ -2,9 +2,11 @@ use std::iter::Peekable;
use std::str::Chars;
use nix::NixPath;
use unicode_segmentation::UnicodeSegmentation;
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word};
use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use super::linebuf::CharClass;
use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word};
use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -51,6 +53,17 @@ pub trait ViMode {
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;
fn report_mode(&self) -> ModeReport;
fn cmds_from_raw(&mut self, raw: &str) -> Vec<ViCmd> {
let mut cmds = vec![];
for ch in raw.graphemes(true) {
let key = E::new(ch, M::NONE);
let Some(cmd) = self.handle_key(key) else {
continue
};
cmds.push(cmd)
}
cmds
}
}
#[derive(Default,Debug)]
@@ -69,7 +82,8 @@ impl ViInsert {
self
}
pub fn register_and_return(&mut self) -> Option<ViCmd> {
let cmd = self.take_cmd();
let mut cmd = self.take_cmd();
cmd.normalize_counts();
self.register_cmd(&cmd);
Some(cmd)
}
@@ -99,20 +113,14 @@ impl ViMode for ViInsert {
self.register_and_return()
}
E(K::Char('W'), M::CTRL) => {
if self.ctrl_w_is_undo() {
self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo));
self.cmds.clear();
Some(self.take_cmd())
} else {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal)));
self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
self.register_and_return()
}
}
E(K::Char('H'), M::CTRL) |
E(K::Backspace, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar));
self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardCharForced));
self.register_and_return()
}
@@ -136,6 +144,7 @@ impl ViMode for ViInsert {
}
}
fn is_repeatable(&self) -> bool {
true
}
@@ -180,19 +189,11 @@ impl ViReplace {
self
}
pub fn register_and_return(&mut self) -> Option<ViCmd> {
let cmd = self.take_cmd();
let mut cmd = self.take_cmd();
cmd.normalize_counts();
self.register_cmd(&cmd);
Some(cmd)
}
pub fn ctrl_w_is_undo(&self) -> bool {
let insert_count = self.cmds.iter().filter(|cmd| {
matches!(cmd.verb(),Some(VerbCmd(1, Verb::ReplaceChar(_))))
}).count();
let backspace_count = self.cmds.iter().filter(|cmd| {
matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete)))
}).count();
insert_count > backspace_count
}
pub fn register_cmd(&mut self, cmd: &ViCmd) {
self.cmds.push(cmd.clone())
}
@@ -210,15 +211,10 @@ impl ViMode for ViReplace {
self.register_and_return()
}
E(K::Char('W'), M::CTRL) => {
if self.ctrl_w_is_undo() {
self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo));
self.cmds.clear();
Some(self.take_cmd())
} else {
self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal)));
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
self.register_and_return()
}
}
E(K::Char('H'), M::CTRL) |
E(K::Backspace, M::NONE) => {
self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar));
@@ -272,6 +268,7 @@ impl ViMode for ViReplace {
#[derive(Default,Debug)]
pub struct ViNormal {
pending_seq: String,
pending_flags: CmdFlags,
}
impl ViNormal {
@@ -284,6 +281,10 @@ impl ViNormal {
pub fn take_cmd(&mut self) -> String {
std::mem::take(&mut self.pending_seq)
}
pub fn flags(&self) -> CmdFlags {
self.pending_flags
}
#[allow(clippy::unnecessary_unwrap)]
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
if verb.is_none() {
match motion {
@@ -294,8 +295,7 @@ impl ViNormal {
}
if verb.is_some() && motion.is_none() {
match verb.unwrap() {
Verb::Put(_) |
Verb::DeleteChar(_) => CmdState::Complete,
Verb::Put(_) => CmdState::Complete,
_ => CmdState::Pending
}
} else {
@@ -329,6 +329,12 @@ impl ViNormal {
self.pending_seq.push(ch);
let mut chars = self.pending_seq.chars().peekable();
/*
* Parse the register
*
* Registers can be any letter a-z or A-Z.
* While uncommon, it is possible to give a count to a register name.
*/
let register = 'reg_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone);
@@ -350,6 +356,17 @@ impl ViNormal {
RegisterName::new(Some(reg_name), count)
};
/*
* We will now parse the verb
* If we hit an invalid sequence, we will call 'return self.quit_parse()'
* self.quit_parse() will clear the pending command and return None
*
* If we hit an incomplete sequence, we will simply return None.
* returning None leaves the pending sequence where it is
*
* Note that we do use a label here for the block and 'return' values from this scope
* using "break 'verb_parse <value>"
*/
let verb = 'verb_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
@@ -367,10 +384,26 @@ impl ViNormal {
register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags(),
}
)
}
'~' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
}
'u' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
}
'U' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
}
'?' => {
chars_clone.next();
chars = chars_clone;
@@ -389,16 +422,53 @@ impl ViNormal {
verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
'x' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::After)));
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
'X' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::Before)));
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
's' => {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags()
},
)
}
'S' => {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
'p' => {
chars = chars_clone;
@@ -421,9 +491,10 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
raw_seq: self.take_cmd()
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,count as u16))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -433,7 +504,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::ReplaceMode)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -441,9 +513,10 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
raw_seq: self.take_cmd()
verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -453,7 +526,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Undo)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -463,7 +537,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::VisualMode)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -473,7 +548,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::VisualModeLine)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -483,7 +559,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -493,7 +570,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -503,7 +581,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -513,7 +592,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -523,7 +603,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -533,7 +614,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -543,7 +625,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -565,7 +648,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -575,7 +659,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -585,7 +670,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -604,19 +690,30 @@ impl ViNormal {
let Some(ch) = chars_clone.next() else {
break 'motion_parse None
};
// Double inputs like 'dd' and 'cc', and some special cases
match (ch, &verb) {
// Double inputs
('?', Some(VerbCmd(_,Verb::Rot13))) |
('d', Some(VerbCmd(_,Verb::Delete))) |
('c', Some(VerbCmd(_,Verb::Change))) |
('y', Some(VerbCmd(_,Verb::Yank))) |
('=', Some(VerbCmd(_,Verb::Equalize))) |
('u', Some(VerbCmd(_,Verb::ToLower))) |
('U', Some(VerbCmd(_,Verb::ToUpper))) |
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
('>', Some(VerbCmd(_,Verb::Indent))) |
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
_ => {}
('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W'
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
}
_ => { /* Nothing weird, so let's continue */ }
}
match ch {
'g' => {
if let Some(ch) = chars_clone.peek() {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None
};
match ch {
'g' => {
chars_clone.next();
@@ -625,11 +722,11 @@ impl ViNormal {
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward)));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward)));
}
'k' => {
chars = chars_clone;
@@ -653,10 +750,30 @@ impl ViNormal {
}
_ => return self.quit_parse()
}
} else {
}
'v' => {
// We got 'v' after a verb
// Instead of normal operations, we will calculate the span based on how visual mode would see it
if self.flags().intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
// We can't have more than one of these
return self.quit_parse();
}
self.pending_flags |= CmdFlags::VISUAL;
break 'motion_parse None
}
'V' => {
// We got 'V' after a verb
// Instead of normal operations, we will calculate the span based on how visual line mode would see it
if self.flags().intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
// We can't have more than one of these
// I know vim can technically do this, but it doesn't really make sense to allow it
// since even in vim only the first one given is used
return self.quit_parse();
}
self.pending_flags |= CmdFlags::VISUAL;
break 'motion_parse None
}
// TODO: figure out how to include 'Ctrl+V' here, might need a refactor
'G' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
@@ -699,7 +816,7 @@ impl ViNormal {
}
'|' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
}
'^' => {
chars = chars_clone;
@@ -731,27 +848,27 @@ impl ViNormal {
}
'w' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Forward)));
}
'W' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward)));
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward)));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
}
'b' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
}
'B' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward)));
}
ch if ch == 'i' || ch == 'a' => {
let bound = match ch {
@@ -767,6 +884,7 @@ impl ViNormal {
'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket,
@@ -795,7 +913,8 @@ impl ViNormal {
register,
verb,
motion,
raw_seq: std::mem::take(&mut self.pending_seq)
raw_seq: std::mem::take(&mut self.pending_seq),
flags: self.flags()
}
)
}
@@ -812,7 +931,7 @@ impl ViNormal {
impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
let mut cmd = match key {
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => {
Some(ViCmd {
@@ -820,6 +939,7 @@ impl ViMode for ViNormal {
verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(),
flags: self.flags()
})
}
E(K::Char('R'), M::CTRL) => {
@@ -830,7 +950,8 @@ impl ViMode for ViNormal {
register: RegisterName::default(),
verb: Some(VerbCmd(count,Verb::Redo)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -846,7 +967,12 @@ impl ViMode for ViNormal {
None
}
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {
@@ -894,6 +1020,8 @@ impl ViVisual {
pub fn take_cmd(&mut self) -> String {
std::mem::take(&mut self.pending_seq)
}
#[allow(clippy::unnecessary_unwrap)]
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
if verb.is_none() {
match motion {
@@ -902,10 +1030,9 @@ impl ViVisual {
None => return CmdState::Pending
}
}
if verb.is_some() && motion.is_none() {
if motion.is_none() && verb.is_some() {
match verb.unwrap() {
Verb::Put(_) |
Verb::DeleteChar(_) => CmdState::Complete,
Verb::Put(_) => CmdState::Complete,
_ => CmdState::Pending
}
} else {
@@ -977,7 +1104,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -987,7 +1115,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::Rot13)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1004,6 +1133,7 @@ impl ViVisual {
verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1018,6 +1148,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1027,7 +1158,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1037,7 +1169,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1049,6 +1182,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1059,6 +1193,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Indent)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1069,6 +1204,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Dedent)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1079,6 +1215,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Equalize)),
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1094,7 +1231,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1102,9 +1240,10 @@ impl ViVisual {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1114,7 +1253,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::ToLower)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1124,7 +1264,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::ToUpper)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1135,7 +1276,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1145,7 +1287,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1155,7 +1298,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1165,7 +1309,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1190,7 +1335,8 @@ impl ViVisual {
register,
verb: Some(verb),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
})
}
@@ -1220,18 +1366,22 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer))
}
'e' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward)));
}
'E' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward)));
}
'k' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
}
'j' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
}
@@ -1246,28 +1396,28 @@ impl ViVisual {
break 'motion_parse None
};
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into())))
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch)))
}
'F' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None
};
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into())))
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch)))
}
't' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None
};
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into())))
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch)))
}
'T' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None
};
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into())))
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch)))
}
';' => {
chars = chars_clone;
@@ -1279,7 +1429,7 @@ impl ViVisual {
}
'|' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
}
'0' => {
chars = chars_clone;
@@ -1307,27 +1457,27 @@ impl ViVisual {
}
'w' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Forward)));
}
'W' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward)));
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward)));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
}
'b' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
}
'B' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big)));
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward)));
}
ch if ch == 'i' || ch == 'a' => {
let bound = match ch {
@@ -1343,6 +1493,7 @@ impl ViVisual {
'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket,
@@ -1366,15 +1517,15 @@ impl ViVisual {
match self.validate_combination(verb_ref, motion_ref) {
CmdState::Complete => {
let cmd = Some(
Some(
ViCmd {
register,
verb,
motion,
raw_seq: std::mem::take(&mut self.pending_seq)
raw_seq: std::mem::take(&mut self.pending_seq),
flags: CmdFlags::empty()
}
);
cmd
)
}
CmdState::Pending => {
None
@@ -1389,7 +1540,7 @@ impl ViVisual {
impl ViMode for ViVisual {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
let mut cmd = match key {
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => {
Some(ViCmd {
@@ -1397,6 +1548,7 @@ impl ViMode for ViVisual {
verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(),
flags: CmdFlags::empty()
})
}
E(K::Char('R'), M::CTRL) => {
@@ -1407,7 +1559,8 @@ impl ViMode for ViVisual {
register: RegisterName::default(),
verb: Some(VerbCmd(count,Verb::Redo)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1417,7 +1570,8 @@ impl ViMode for ViVisual {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)),
motion: Some(MotionCmd(1, Motion::Null)),
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
})
}
_ => {
@@ -1428,7 +1582,12 @@ impl ViMode for ViVisual {
None
}
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {
@@ -1473,11 +1632,17 @@ pub fn common_cmds(key: E) -> Option<ViCmd> {
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)),
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)),
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)),
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLine)),
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLineOrNewline)),
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)),
E(K::Delete, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::After))),
E(K::Delete, M::NONE) => {
pending_cmd.set_verb(VerbCmd(1,Verb::Delete));
pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar));
}
E(K::Backspace, M::NONE) |
E(K::Char('H'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::Before))),
E(K::Char('H'), M::CTRL) => {
pending_cmd.set_verb(VerbCmd(1,Verb::Delete));
pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar));
}
_ => return None
}
Some(pending_cmd)

View File

@@ -26,6 +26,7 @@ pub mod error;
pub mod getopt;
pub mod script;
pub mod highlight;
pub mod readline;
/// Unsafe to use outside of tests
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>

654
src/tests/readline.rs Normal file
View File

@@ -0,0 +1,654 @@
use std::collections::VecDeque;
use crate::{libsh::term::{Style, Styled}, prompt::readline::{history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{raw_mode, KeyReader, LineWriter}, vimode::{ViInsert, ViMode, ViNormal}, FernVi, Readline}};
use pretty_assertions::assert_eq;
use super::super::*;
#[derive(Default,Debug)]
struct TestReader {
pub bytes: VecDeque<u8>
}
impl TestReader {
pub fn new() -> Self {
Self::default()
}
pub fn with_initial(mut self, bytes: &[u8]) -> Self {
let bytes = bytes.iter();
self.bytes.extend(bytes);
self
}
pub fn parse_esc_seq_from_bytes(&mut self) -> Option<KeyEvent> {
let mut seq = vec![0x1b];
let b1 = self.bytes.pop_front()?;
seq.push(b1);
match b1 {
b'[' => {
let b2 = self.bytes.pop_front()?;
seq.push(b2);
match b2 {
b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())),
b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())),
b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())),
b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())),
b'1'..=b'9' => {
let mut digits = vec![b2];
while let Some(&b) = self.bytes.front() {
seq.push(b);
self.bytes.pop_front();
if b == b'~' || b == b';' {
break;
} else if b.is_ascii_digit() {
digits.push(b);
} else {
break;
}
}
let key = match digits.as_slice() {
[b'1'] => KeyCode::Home,
[b'3'] => KeyCode::Delete,
[b'4'] => KeyCode::End,
[b'5'] => KeyCode::PageUp,
[b'6'] => KeyCode::PageDown,
[b'7'] => KeyCode::Home, // xterm alternate
[b'8'] => KeyCode::End, // xterm alternate
[b'1', b'5'] => KeyCode::F(5),
[b'1', b'7'] => KeyCode::F(6),
[b'1', b'8'] => KeyCode::F(7),
[b'1', b'9'] => KeyCode::F(8),
[b'2', b'0'] => KeyCode::F(9),
[b'2', b'1'] => KeyCode::F(10),
[b'2', b'3'] => KeyCode::F(11),
[b'2', b'4'] => KeyCode::F(12),
_ => KeyCode::Esc,
};
Some(KeyEvent(key, ModKeys::empty()))
}
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
b'O' => {
let b2 = self.bytes.pop_front()?;
seq.push(b2);
let key = match b2 {
b'P' => KeyCode::F(1),
b'Q' => KeyCode::F(2),
b'R' => KeyCode::F(3),
b'S' => KeyCode::F(4),
_ => KeyCode::Esc,
};
Some(KeyEvent(key, ModKeys::empty()))
}
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
}
impl KeyReader for TestReader {
fn read_key(&mut self) -> Option<KeyEvent> {
use core::str;
let mut collected = Vec::with_capacity(4);
loop {
let byte = self.bytes.pop_front()?;
collected.push(byte);
// If it's an escape sequence, delegate
if collected[0] == 0x1b && collected.len() == 1 {
if let Some(&_next @ (b'[' | b'0')) = self.bytes.front() {
println!("found escape seq");
let seq = self.parse_esc_seq_from_bytes();
println!("{seq:?}");
return seq
}
}
// Try parse as valid UTF-8
if let Ok(s) = str::from_utf8(&collected) {
return Some(KeyEvent::new(s, ModKeys::empty()));
}
if collected.len() >= 4 {
break;
}
}
None
}
}
pub struct TestWriter {
}
impl TestWriter {
pub fn new() -> Self {
Self {}
}
}
impl LineWriter for TestWriter {
fn clear_rows(&mut self, _layout: &prompt::readline::term::Layout) -> libsh::error::ShResult<()> {
Ok(())
}
fn redraw(
&mut self,
_prompt: &str,
_line: &LineBuf,
_new_layout: &prompt::readline::term::Layout,
) -> libsh::error::ShResult<()> {
Ok(())
}
fn flush_write(&mut self, _buf: &str) -> libsh::error::ShResult<()> {
Ok(())
}
}
impl FernVi {
pub fn new_test(prompt: Option<String>,input: &str, initial: &str) -> Self {
Self {
reader: Box::new(TestReader::new().with_initial(input.as_bytes())),
writer: Box::new(TestWriter::new()),
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
mode: Box::new(ViInsert::new()),
old_layout: None,
repeat_action: None,
repeat_motion: None,
history: History::new().unwrap(),
editor: LineBuf::new().with_initial(initial, 0)
}
}
}
fn fernvi_test(input: &str, initial: &str) -> String {
let mut fernvi = FernVi::new_test(None,input,initial);
let raw_mode = raw_mode();
let line = fernvi.readline().unwrap();
std::mem::drop(raw_mode);
line
}
fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String,usize) {
let cmd = ViNormal::new()
.cmds_from_raw(cmd)
.pop()
.unwrap();
let mut buf = LineBuf::new().with_initial(buf, cursor);
buf.exec_cmd(cmd).unwrap();
(buf.as_str().to_string(),buf.cursor.get())
}
#[test]
fn vimode_insert_cmds() {
let raw = "abcdefghijklmnopqrstuvwxyz1234567890-=[];'<>/\\x1b";
let mut mode = ViInsert::new();
let cmds = mode.cmds_from_raw(raw);
insta::assert_debug_snapshot!(cmds)
}
#[test]
fn vimode_normal_cmds() {
let raw = "d2wg?5b2P5x";
let mut mode = ViNormal::new();
let cmds = mode.cmds_from_raw(raw);
insta::assert_debug_snapshot!(cmds)
}
#[test]
fn linebuf_empty_linebuf() {
let mut buf = LineBuf::new();
assert_eq!(buf.as_str(), "");
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_indices(), &[]);
assert!(buf.slice(0..0).is_none());
}
#[test]
fn linebuf_ascii_content() {
let mut buf = LineBuf::new().with_initial("hello", 0);
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]);
assert_eq!(buf.grapheme_at(0), Some("h"));
assert_eq!(buf.grapheme_at(4), Some("o"));
assert_eq!(buf.slice(1..4), Some("ell"));
assert_eq!(buf.slice_to(2), Some("he"));
assert_eq!(buf.slice_from(2), Some("llo"));
}
#[test]
fn linebuf_unicode_graphemes() {
let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0);
buf.update_graphemes_lazy();
let indices = buf.grapheme_indices();
assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker
assert_eq!(buf.grapheme_at(0), Some("a"));
assert_eq!(buf.grapheme_at(1), Some("🇺🇸"));
assert_eq!(buf.grapheme_at(2), Some("")); // b + combining accent
assert_eq!(buf.grapheme_at(3), Some("c"));
assert_eq!(buf.grapheme_at(4), None); // out of bounds
assert_eq!(buf.slice(0..2), Some("a🇺🇸"));
assert_eq!(buf.slice(1..3), Some("🇺🇸b́"));
assert_eq!(buf.slice(2..4), Some("b́c"));
}
#[test]
fn linebuf_slice_to_from_cursor() {
let mut buf = LineBuf::new().with_initial("abçd", 2);
buf.update_graphemes_lazy();
assert_eq!(buf.slice_to_cursor(), Some("ab"));
assert_eq!(buf.slice_from_cursor(), Some("çd"));
}
#[test]
fn linebuf_out_of_bounds_slices() {
let mut buf = LineBuf::new().with_initial("test", 0);
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_at(5), None); // out of bounds
assert_eq!(buf.slice(2..5), None); // end out of bounds
assert_eq!(buf.slice(4..4), None); // valid but empty
}
#[test]
fn linebuf_this_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.this_line();
assert_eq!(buf.slice(start..end), Some("This is the third line\n"))
}
#[test]
fn linebuf_prev_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the second line\n"))
}
#[test]
fn linebuf_prev_line_first_line_is_empty() {
let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 36);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the first line\n"))
}
#[test]
fn linebuf_next_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
}
#[test]
fn linebuf_next_line_last_line_is_empty() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the fourth line\n"))
}
#[test]
fn linebuf_next_line_several_trailing_newlines() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 81);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
fn linebuf_next_line_only_newlines() {
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(start, 8);
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
fn linebuf_prev_line_only_newlines() {
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("\n"));
assert_eq!(start, 6);
}
#[test]
fn linebuf_cursor_motion() {
let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
buf.update_graphemes_lazy();
let total = buf.grapheme_indices.as_ref().unwrap().len();
for i in 0..total {
buf.cursor.set(i);
let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string();
let expected_from = if i + 1 < total {
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string()
} else {
// last grapheme, ends at buffer end
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string()
};
let expected_at = {
let start = buf.grapheme_indices_owned()[i];
let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len());
buf.buffer.get(start..end).map(|slice| slice.to_string())
};
assert_eq!(
buf.slice_to_cursor(),
Some(expected_to.as_str()),
"Failed at cursor position {i}: slice_to_cursor"
);
assert_eq!(
buf.slice_from_cursor(),
Some(expected_from.as_str()),
"Failed at cursor position {i}: slice_from_cursor"
);
assert_eq!(
buf.grapheme_at(i).map(|slice| slice.to_string()),
expected_at,
"Failed at cursor position {i}: grapheme_at"
);
}
}
#[test]
fn editor_delete_word() {
assert_eq!(normal_cmd(
"dw",
"The quick brown fox jumps over the lazy dog",
16),
("The quick brown jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_backwards() {
assert_eq!(normal_cmd(
"2db",
"The quick brown fox jumps over the lazy dog",
16),
("The fox jumps over the lazy dog".into(), 4)
);
}
#[test]
fn editor_rot13_five_words_backwards() {
assert_eq!(normal_cmd(
"g?5b",
"The quick brown fox jumps over the lazy dog",
31),
("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4)
);
}
#[test]
fn editor_delete_word_on_whitespace() {
assert_eq!(normal_cmd(
"dw",
"The quick brown fox",
10), //on the whitespace between "quick" and "brown"
("The quick brown fox".into(), 10)
);
}
#[test]
fn editor_delete_5_words() {
assert_eq!(normal_cmd(
"5dw",
"The quick brown fox jumps over the lazy dog",
16,),
("The quick brown dog".into(), 16)
);
}
#[test]
fn editor_delete_end_includes_last() {
assert_eq!(normal_cmd(
"de",
"The quick brown fox::::jumps over the lazy dog",
16),
("The quick brown ::::jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_end_unicode_word() {
assert_eq!(normal_cmd(
"de",
"naïve café world",
0),
(" café world".into(), 0)
);
}
#[test]
fn editor_inplace_edit_cursor_position() {
assert_eq!(normal_cmd(
"5~",
"foobar",
0),
("FOOBAr".into(), 4)
);
assert_eq!(normal_cmd(
"5rg",
"foobar",
0),
("gggggr".into(), 4)
);
}
#[test]
fn editor_insert_mode_not_clamped() {
assert_eq!(normal_cmd(
"a",
"foobar",
5),
("foobar".into(), 6)
)
}
#[test]
fn editor_overshooting_motions() {
assert_eq!(normal_cmd(
"5dw",
"foo bar",
0),
("".into(), 0)
);
assert_eq!(normal_cmd(
"3db",
"foo bar",
0),
("foo bar".into(), 0)
);
assert_eq!(normal_cmd(
"3dj",
"foo bar",
0),
("foo bar".into(), 0)
);
assert_eq!(normal_cmd(
"3dk",
"foo bar",
0),
("foo bar".into(), 0)
);
}
#[test]
fn editor_textobj_quoted() {
assert_eq!(normal_cmd(
"di\"",
"this buffer has \"some \\\"quoted\" text",
0),
("this buffer has \"\" text".into(), 17)
);
assert_eq!(normal_cmd(
"da\"",
"this buffer has \"some \\\"quoted\" text",
0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di'",
"this buffer has 'some \\'quoted' text",
0),
("this buffer has '' text".into(), 17)
);
assert_eq!(normal_cmd(
"da'",
"this buffer has 'some \\'quoted' text",
0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di`",
"this buffer has `some \\`quoted` text",
0),
("this buffer has `` text".into(), 17)
);
assert_eq!(normal_cmd(
"da`",
"this buffer has `some \\`quoted` text",
0),
("this buffer has text".into(), 16)
);
}
#[test]
fn editor_textobj_delimited() {
assert_eq!(normal_cmd(
"di)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0),
("this buffer has () text".into(), 17)
);
assert_eq!(normal_cmd(
"da)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0),
("this buffer has [] text".into(), 17)
);
assert_eq!(normal_cmd(
"da]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0),
("this buffer has {} text".into(), 17)
);
assert_eq!(normal_cmd(
"da}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0),
("this buffer has <> text".into(), 17)
);
assert_eq!(normal_cmd(
"da>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0),
("this buffer has text".into(), 16)
);
}
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
#[test]
fn editor_delete_line_up() {
assert_eq!(normal_cmd(
"dk",
LOREM_IPSUM,
237),
("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 240,)
)
}
#[test]
fn fernvi_test_simple() {
assert_eq!(fernvi_test(
"foo bar\x1bbdw\r",
""),
"foo "
)
}
#[test]
fn fernvi_test_mode_change() {
assert_eq!(fernvi_test(
"foo bar biz buzz\x1bbbb2cwbiz buzz bar\r",
""),
"foo biz buzz bar buzz"
)
}
#[test]
fn fernvi_test_lorem_ipsum_1() {
assert_eq!(fernvi_test(
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
}
#[test]
fn fernvi_test_lorem_ipsum_undo() {
assert_eq!(fernvi_test(
"\x1bwwwwwwwwainserting some characters now...\x1bu\r",
LOREM_IPSUM),
LOREM_IPSUM
)
}
#[test]
fn fernvi_test_lorem_ipsum_ctrl_w() {
assert_eq!(fernvi_test(
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
}