implemented quote/delimiter text objects

This commit is contained in:
2025-06-09 02:29:34 -04:00
parent 2c14e4c202
commit ff0207a27f
5 changed files with 871 additions and 221 deletions

View File

@@ -1,10 +1,10 @@
use std::{fmt::Display, ops::{Range, RangeBounds, RangeInclusive}, string::Drain}; use std::{fmt::Display, ops::{Range, RangeInclusive}};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::{term::Layout, vicmd::{Anchor, Dest, Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}}; use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, ViCmd, Word};
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}, prelude::*}; use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};
#[derive(Default,PartialEq,Eq,Debug,Clone,Copy)] #[derive(Default,PartialEq,Eq,Debug,Clone,Copy)]
pub enum CharClass { pub enum CharClass {
@@ -75,10 +75,6 @@ fn is_other_class_or_is_ws(a: &str, b: &str) -> bool {
} }
} }
fn is_other_class_and_is_ws(a: &str, b: &str) -> bool {
is_other_class(a, b) && (is_whitespace(a) || is_whitespace(b))
}
#[derive(Default,Clone,Copy,PartialEq,Eq,Debug)] #[derive(Default,Clone,Copy,PartialEq,Eq,Debug)]
pub enum SelectAnchor { pub enum SelectAnchor {
#[default] #[default]
@@ -93,6 +89,28 @@ pub enum SelectMode {
Block(SelectAnchor), Block(SelectAnchor),
} }
impl SelectMode {
pub fn anchor(&self) -> &SelectAnchor {
match self {
SelectMode::Char(anchor) |
SelectMode::Line(anchor) |
SelectMode::Block(anchor) => anchor
}
}
pub fn invert_anchor(&mut self) {
match self {
SelectMode::Char(anchor) |
SelectMode::Line(anchor) |
SelectMode::Block(anchor) => {
*anchor = match anchor {
SelectAnchor::Start => SelectAnchor::End,
SelectAnchor::End => SelectAnchor::Start
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotionKind { pub enum MotionKind {
To(usize), // Absolute position, exclusive To(usize), // Absolute position, exclusive
@@ -323,7 +341,12 @@ impl LineBuf {
pub fn accept_hint(&mut self) { pub fn accept_hint(&mut self) {
let Some(hint) = self.hint.take() else { return }; let Some(hint) = self.hint.take() else { return };
let old = self.buffer.clone();
let cursor_pos = self.cursor.get();
self.push_str(&hint); self.push_str(&hint);
let new = self.as_str();
let edit = Edit::diff(&old, new, cursor_pos);
self.undo_stack.push(edit);
self.cursor.add(hint.len()); self.cursor.add(hint.len());
} }
pub fn set_cursor_clamp(&mut self, yn: bool) { pub fn set_cursor_clamp(&mut self, yn: bool) {
@@ -339,6 +362,12 @@ impl LineBuf {
.copied() .copied()
.unwrap_or(self.buffer.len()) .unwrap_or(self.buffer.len())
} }
pub fn read_idx_byte_pos(&self, index: usize) -> usize {
self.grapheme_indices()
.get(index)
.copied()
.unwrap_or(self.buffer.len())
}
/// Update self.grapheme_indices with the indices of the current buffer /// Update self.grapheme_indices with the indices of the current buffer
#[track_caller] #[track_caller]
pub fn update_graphemes(&mut self) { pub fn update_graphemes(&mut self) {
@@ -628,15 +657,297 @@ impl LineBuf {
} }
} }
} }
pub fn dispatch_text_obj(
&mut self,
count: usize,
text_obj: TextObj,
bound: Bound
) -> Option<(usize,usize)> {
match text_obj {
// Text groups
TextObj::Word(word) => self.text_obj_word(count, bound, word),
TextObj::Sentence(dir) => self.text_obj_sentence(count, dir, bound),
TextObj::Paragraph(dir) => self.text_obj_paragraph(count, dir, bound),
pub fn dispatch_word_motion(&mut self, count: usize, to: To, word: Word, dir: Direction) -> usize { // Quoted blocks
TextObj::DoubleQuote |
TextObj::SingleQuote |
TextObj::BacktickQuote => self.text_obj_quote(count, text_obj, bound),
// Delimited blocks
TextObj::Paren |
TextObj::Bracket |
TextObj::Brace |
TextObj::Angle => self.text_obj_delim(count, text_obj, bound),
// Other stuff
TextObj::Tag => todo!(),
TextObj::Custom(_) => todo!(),
}
}
pub fn text_obj_word(&mut self, count: usize, bound: Bound, word: Word) -> Option<(usize,usize)> {
todo!()
}
pub fn text_obj_sentence(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> {
todo!()
}
pub fn text_obj_paragraph(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> {
todo!()
}
pub fn text_obj_delim(&mut self, count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> {
let mut backward_indices = (0..self.cursor.get()).rev();
let (opener,closer) = match text_obj {
TextObj::Paren => ("(",")"),
TextObj::Bracket => ("[","]"),
TextObj::Brace => ("{","}"),
TextObj::Angle => ("<",">"),
_ => unreachable!()
};
let mut start_pos = None;
let mut closer_count: u32 = 0;
while let Some(idx) = backward_indices.next() {
let gr = self.grapheme_at(idx)?.to_string();
if gr != closer && gr != opener { continue }
let mut escaped = false;
while let Some(idx) = backward_indices.next() {
// Keep consuming indices as long as they refer to a backslash
let Some("\\") = self.grapheme_at(idx) else {
break
};
// On each backslash, flip this boolean
escaped = !escaped
}
// If there are an even number of backslashes (or none), we are not escaped
// Therefore, we have found the start position
if !escaped {
if gr == closer {
closer_count += 1;
} else if closer_count == 0 {
start_pos = Some(idx);
break
} else {
closer_count = closer_count.saturating_sub(1)
}
}
}
let (mut start, mut end) = if let Some(pos) = start_pos {
let start = pos;
let mut forward_indices = start+1..self.cursor.max;
let mut end = None;
let mut opener_count: u32 = 0;
while let Some(idx) = forward_indices.next() {
match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == opener => opener_count += 1,
gr if gr == closer => {
if opener_count == 0 {
end = Some(idx);
break
} else {
opener_count = opener_count.saturating_sub(1);
}
}
_ => { /* Continue */ }
}
}
(start,end?)
} else {
let mut forward_indices = self.cursor.get()..self.cursor.max;
let mut start = None;
let mut end = None;
let mut opener_count: u32 = 0;
while let Some(idx) = forward_indices.next() {
match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == opener => {
if opener_count == 0 {
start = Some(idx);
}
opener_count += 1;
}
gr if gr == closer => {
if opener_count == 1 {
end = Some(idx);
break
} else {
opener_count = opener_count.saturating_sub(1)
}
}
_ => { /* Continue */ }
}
}
(start?,end?)
};
match bound {
Bound::Inside => {
// Start includes the quote, so push it forward
start += 1;
}
Bound::Around => {
// End excludes the quote, so push it forward
end += 1;
// We also need to include any trailing whitespace
let end_of_line = self.end_of_line();
let remainder = end..end_of_line;
for idx in remainder {
let Some(gr) = self.grapheme_at(idx) else { break };
flog!(DEBUG, gr);
if is_whitespace(gr) {
end += 1;
} else {
break
}
}
}
}
Some((start,end))
}
pub fn text_obj_quote(&mut self, count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> {
let (start,end) = self.this_line(); // Only operates on the current line
// Get the grapheme indices backward from the cursor
let mut backward_indices = (start..self.cursor.get()).rev();
let target = match text_obj {
TextObj::DoubleQuote => "\"",
TextObj::SingleQuote => "'",
TextObj::BacktickQuote => "`",
_ => unreachable!()
};
let mut start_pos = None;
while let Some(idx) = backward_indices.next() {
match self.grapheme_at(idx)? {
gr if gr == target => {
// We are going backwards, so we need to handle escapes differently
// These things were not meant to be read backwards, so it's a little fucked up
let mut escaped = false;
while let Some(idx) = backward_indices.next() {
// Keep consuming indices as long as they refer to a backslash
let Some("\\") = self.grapheme_at(idx) else {
break
};
// On each backslash, flip this boolean
escaped = !escaped
}
// If there are an even number of backslashes, we are not escaped
// Therefore, we have found the start position
if !escaped {
start_pos = Some(idx);
break
}
}
_ => { /* Continue */ }
}
}
// Try to find a quote backwards
let (mut start, mut end) = if let Some(pos) = start_pos {
// Found one, only one more to go
let start = pos;
let mut forward_indices = start+1..end;
let mut end = None;
while let Some(idx) = forward_indices.next() {
match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == target => {
end = Some(idx);
break;
}
_ => { /* Continue */ }
}
}
let end = end?;
(start,end)
} else {
// Did not find one, have two find two of them forward now
let mut forward_indices = self.cursor.get()..end;
let mut start = None;
let mut end = None;
while let Some(idx) = forward_indices.next() {
match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == target => {
start = Some(idx);
break
}
_ => { /* Continue */ }
}
}
let start = start?;
while let Some(idx) = forward_indices.next() {
match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == target => {
end = Some(idx);
break;
}
_ => { /* Continue */ }
}
}
let end = end?;
(start,end)
};
match bound {
Bound::Inside => {
// Start includes the quote, so push it forward
start += 1;
}
Bound::Around => {
// End excludes the quote, so push it forward
end += 1;
// We also need to include any trailing whitespace
let end_of_line = self.end_of_line();
let remainder = end..end_of_line;
for idx in remainder {
let Some(gr) = self.grapheme_at(idx) else { break };
flog!(DEBUG, gr);
if is_whitespace(gr) {
end += 1;
} else {
break
}
}
}
}
Some((start, end))
}
pub fn dispatch_word_motion(
&mut self,
count: usize,
to: To,
word: Word,
dir: Direction,
include_last_char: bool
) -> usize {
// Not sorry for these method names btw // Not sorry for these method names btw
let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false); let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false);
for _ in 0..count { for i in 0..count {
// We alter 'include_last_char' to only be true on the last iteration
// Therefore, '5cw' will find the correct range for the first four and stop on the end of the fifth word
let include_last_char_and_is_last_word = include_last_char && i == count.saturating_sub(1);
pos.set(match to { pos.set(match to {
To::Start => { To::Start => {
match dir { match dir {
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir), Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, include_last_char_and_is_last_word),
Direction::Backward => 'backward: { Direction::Backward => 'backward: {
// We also need to handle insert mode's Ctrl+W behaviors here // We also need to handle insert mode's Ctrl+W behaviors here
let target = self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir); let target = self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir);
@@ -662,7 +973,7 @@ impl LineBuf {
To::End => { To::End => {
match dir { match dir {
Direction::Forward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir), Direction::Forward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir),
Direction::Backward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir), Direction::Backward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, false),
} }
} }
}); });
@@ -676,7 +987,7 @@ impl LineBuf {
/// are logically the same operation, if you use a reversed iterator for the backward motion. /// are logically the same operation, if you use a reversed iterator for the backward motion.
/// ///
/// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the longest method name I have ever written /// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the longest method name I have ever written
pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize { pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction, include_last_char: bool) -> usize {
let default = match dir { let default = match dir {
Direction::Backward => 0, Direction::Backward => 0,
Direction::Forward => self.grapheme_indices().len() Direction::Forward => self.grapheme_indices().len()
@@ -691,7 +1002,12 @@ impl LineBuf {
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace); let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
if on_boundary { if on_boundary {
let Some(idx) = indices_iter.next() else { return default }; let Some(idx) = indices_iter.next() else { return default };
pos = idx; // We have a 'cw' call, do not include the trailing whitespace
if include_last_char {
return idx;
} else {
pos = idx;
}
} }
// Check current grapheme // Check current grapheme
@@ -702,9 +1018,12 @@ impl LineBuf {
// Find the next whitespace // Find the next whitespace
if !on_whitespace { if !on_whitespace {
let Some(_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else {
return default return default
}; };
if include_last_char {
return ws_pos
}
} }
// Return the next visible grapheme position // Return the next visible grapheme position
@@ -716,7 +1035,11 @@ impl LineBuf {
let Some(next_idx) = indices_iter.peek() else { return default }; let Some(next_idx) = indices_iter.peek() else { return default };
let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
if on_boundary { if on_boundary {
pos = *next_idx if include_last_char {
return *next_idx
} else {
pos = *next_idx;
}
} }
let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
@@ -739,7 +1062,7 @@ impl LineBuf {
return default return default
}; };
// If we hit a different character class, we return here // If we hit a different character class, we return here
if self.grapheme_at(other_class_pos).is_some_and(|c| !is_whitespace(c)) { if self.grapheme_at(other_class_pos).is_some_and(|c| !is_whitespace(c)) || include_last_char {
return other_class_pos return other_class_pos
} }
} }
@@ -939,6 +1262,11 @@ impl LineBuf {
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize { pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
self.find_from(self.cursor.get(), op) self.find_from(self.cursor.get(), op)
} }
pub fn insert_str_at(&mut self, pos: usize, new: &str) {
let idx = self.index_byte_pos(pos);
self.buffer.insert_str(idx, new);
self.update_graphemes();
}
pub fn replace_at_cursor(&mut self, new: &str) { pub fn replace_at_cursor(&mut self, new: &str) {
self.replace_at(self.cursor.get(), new); self.replace_at(self.cursor.get(), new);
} }
@@ -966,7 +1294,7 @@ impl LineBuf {
let end = start + gr.len(); let end = start + gr.len();
self.buffer.replace_range(start..end, new); self.buffer.replace_range(start..end, new);
} }
pub fn eval_motion(&mut self, motion: MotionCmd) -> MotionKind { pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone(); let buffer = self.buffer.clone();
if self.has_hint() { if self.has_hint() {
let hint = self.hint.clone().unwrap(); let hint = self.hint.clone().unwrap();
@@ -1004,7 +1332,12 @@ impl LineBuf {
MotionKind::InclusiveWithTargetCol((start,end),target_pos) MotionKind::InclusiveWithTargetCol((start,end),target_pos)
} }
MotionCmd(count,Motion::WordMotion(to, word, dir)) => { MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
let pos = self.dispatch_word_motion(count, to, word, dir); // 'cw' is a weird case
// if you are on the word's left boundary, it will not delete whitespace after the end of the word
let include_last_char = verb == Some(&Verb::Change) &&
matches!(motion.1, Motion::WordMotion(To::Start, _, Direction::Forward));
let pos = self.dispatch_word_motion(count, to, word, dir, include_last_char);
let pos = ClampedUsize::new(pos,self.cursor.max,false); let pos = ClampedUsize::new(pos,self.cursor.max,false);
// End-based operations must include the last character // End-based operations must include the last character
// But the cursor must also stop just before it when moving // But the cursor must also stop just before it when moving
@@ -1023,7 +1356,17 @@ impl LineBuf {
MotionKind::On(pos.get()) MotionKind::On(pos.get())
} }
} }
MotionCmd(count,Motion::TextObj(text_obj, bound)) => todo!(), MotionCmd(count,Motion::TextObj(text_obj, bound)) => {
let Some((start,end)) = self.dispatch_text_obj(count, text_obj, bound) else {
return MotionKind::Null
};
MotionKind::Inclusive((start,end))
}
MotionCmd(count,Motion::ToDelimMatch) => todo!(),
MotionCmd(count,Motion::ToBrace(direction)) => todo!(),
MotionCmd(count,Motion::ToBracket(direction)) => todo!(),
MotionCmd(count,Motion::ToParen(direction)) => todo!(),
MotionCmd(count,Motion::EndOfLastWord) => { MotionCmd(count,Motion::EndOfLastWord) => {
let start = self.start_of_line(); let start = self.start_of_line();
let mut newline_count = 0; let mut newline_count = 0;
@@ -1204,11 +1547,21 @@ impl LineBuf {
MotionCmd(_count,Motion::BeginningOfBuffer) => MotionKind::On(0), MotionCmd(_count,Motion::BeginningOfBuffer) => MotionKind::On(0),
MotionCmd(_count,Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()), MotionCmd(_count,Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()),
MotionCmd(_count,Motion::ToColumn) => todo!(), MotionCmd(_count,Motion::ToColumn) => todo!(),
MotionCmd(count,Motion::ToDelimMatch) => todo!(), MotionCmd(count,Motion::Range(start, end)) => {
MotionCmd(count,Motion::ToBrace(direction)) => todo!(), let mut final_end = end;
MotionCmd(count,Motion::ToBracket(direction)) => todo!(), if self.cursor.exclusive {
MotionCmd(count,Motion::ToParen(direction)) => todo!(), final_end += 1;
MotionCmd(count,Motion::Range(start, end)) => todo!(), }
let delta = end - start;
let count = count.saturating_sub(1); // Becomes number of times to multiply the range
for _ in 0..count {
final_end += delta;
}
final_end = final_end.min(self.cursor.max);
MotionKind::Inclusive((start,final_end))
}
MotionCmd(count,Motion::RepeatMotion) => todo!(), MotionCmd(count,Motion::RepeatMotion) => todo!(),
MotionCmd(count,Motion::RepeatMotionRev) => todo!(), MotionCmd(count,Motion::RepeatMotionRev) => todo!(),
MotionCmd(count,Motion::Null) => MotionKind::Null MotionCmd(count,Motion::Null) => MotionKind::Null
@@ -1268,6 +1621,35 @@ impl LineBuf {
self.move_cursor(motion); self.move_cursor(motion);
} }
self.update_graphemes(); self.update_graphemes();
self.update_select_range();
}
pub fn update_select_range(&mut self) {
if let Some(mut mode) = self.select_mode {
let Some((mut start,mut end)) = self.select_range.clone() else {
return
};
match mode {
SelectMode::Char(anchor) => {
match anchor {
SelectAnchor::Start => {
start = self.cursor.get();
}
SelectAnchor::End => {
end = self.cursor.get();
}
}
}
SelectMode::Line(anchor) => todo!(),
SelectMode::Block(anchor) => todo!(),
}
if start >= end {
mode.invert_anchor();
std::mem::swap(&mut start, &mut end);
self.select_mode = Some(mode);
}
self.select_range = Some((start,end));
}
} }
pub fn move_cursor(&mut self, motion: MotionKind) { pub fn move_cursor(&mut self, motion: MotionKind) {
match motion { match motion {
@@ -1382,24 +1764,45 @@ impl LineBuf {
self.replace_at_cursor(new); self.replace_at_cursor(new);
self.apply_motion(motion); self.apply_motion(motion);
} }
Verb::ToggleCaseSingle => { Verb::ReplaceCharInplace(ch,count) => {
let Some(gr) = self.grapheme_at_cursor() else { for i in 0..count {
return Ok(()) let mut buf = [0u8;4];
}; let new = ch.encode_utf8(&mut buf);
if gr.len() > 1 || gr.is_empty() { self.replace_at_cursor(new);
return Ok(())
// try to increment the cursor until we are on the last iteration
// or until we hit the end of the buffer
if i != count.saturating_sub(1) && !self.cursor.inc() {
break
}
} }
let ch = gr.chars().next().unwrap(); }
if !ch.is_alphabetic() { Verb::ToggleCaseInplace(count) => {
return Ok(()) for i in 0..count {
let Some(gr) = self.grapheme_at_cursor() else {
return Ok(())
};
if gr.len() > 1 || gr.is_empty() {
return Ok(())
}
let ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() {
return Ok(())
}
let mut buf = [0u8;4];
let new = if ch.is_ascii_lowercase() {
ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else {
ch.to_ascii_lowercase().encode_utf8(&mut buf)
};
self.replace_at_cursor(new);
// try to increment the cursor until we are on the last iteration
// or until we hit the end of the buffer
if i != count.saturating_sub(1) && !self.cursor.inc() {
break
}
} }
let mut buf = [0u8;4];
let new = if ch.is_ascii_lowercase() {
ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else {
ch.to_ascii_lowercase().encode_utf8(&mut buf)
};
self.replace_at_cursor(new);
} }
Verb::ToggleCaseRange => { Verb::ToggleCaseRange => {
let Some((start,end)) = self.range_from_motion(&motion) else { let Some((start,end)) = self.range_from_motion(&motion) else {
@@ -1476,7 +1879,9 @@ impl LineBuf {
Verb::Redo | Verb::Redo |
Verb::Undo => { Verb::Undo => {
let (edit_provider,edit_receiver) = match verb { let (edit_provider,edit_receiver) = match verb {
// Redo = pop from redo stack, push to undo stack
Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack), Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack),
// Undo = pop from undo stack, push to redo stack
Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack), Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack),
_ => unreachable!() _ => unreachable!()
}; };
@@ -1495,8 +1900,30 @@ impl LineBuf {
self.update_graphemes(); self.update_graphemes();
} }
Verb::RepeatLast => todo!(), Verb::RepeatLast => todo!(),
Verb::Put(anchor) => todo!(), Verb::Put(anchor) => {
Verb::SwapVisualAnchor => todo!(), let Some(content) = register.read_from_register() else {
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));
}
Verb::SwapVisualAnchor => {
if let Some((start,end)) = self.select_range() {
if let Some(mut mode) = self.select_mode {
mode.invert_anchor();
let new_cursor_pos = match mode.anchor() {
SelectAnchor::Start => start,
SelectAnchor::End => end,
};
self.cursor.set(new_cursor_pos);
self.select_mode = Some(mode)
}
}
}
Verb::JoinLines => { Verb::JoinLines => {
let start = self.start_of_line(); let start = self.start_of_line();
let Some((_,mut end)) = self.nth_next_line(1) else { let Some((_,mut end)) = self.nth_next_line(1) else {
@@ -1529,7 +1956,6 @@ impl LineBuf {
let graphemes = string.graphemes(true).count(); let graphemes = string.graphemes(true).count();
self.cursor.add(graphemes); self.cursor.add(graphemes);
} }
Verb::Breakline(anchor) => todo!(),
Verb::Indent => { Verb::Indent => {
let Some((start,end)) = self.range_from_motion(&motion) else { let Some((start,end)) = self.range_from_motion(&motion) else {
return Ok(()) return Ok(())
@@ -1622,49 +2048,60 @@ impl LineBuf {
} }
} }
let ViCmd { register, verb, motion, raw_seq: _ } = cmd; let ViCmd { register, verb, motion, flags, raw_seq: _ } = cmd;
let verb_count = verb.as_ref().map(|v| v.0).unwrap_or(1); let verb_cmd_ref = verb.as_ref();
let motion_count = motion.as_ref().map(|m| m.0); let verb_ref = verb_cmd_ref.map(|v| v.1.clone());
let verb_count = verb_cmd_ref.map(|v| v.0).unwrap_or(1);
let before = self.buffer.clone(); let before = self.buffer.clone();
let cursor_pos = self.cursor.get(); let cursor_pos = self.cursor.get();
for i in 0..verb_count { /*
/* * Let's evaluate the motion now
* Let's evaluate the motion now * If we got some weird command like 'dvw' we will have to simulate a visual selection to get the range
* If motion is None, we will try to use self.select_range * If motion is None, we will try to use self.select_range
* If self.select_range is None, we will use MotionKind::Null * If self.select_range is None, we will use MotionKind::Null
*/ */
let motion_eval = motion let motion_eval = if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
let motion = motion
.clone() .clone()
.map(|m| self.eval_motion(m)) .map(|m| self.eval_motion(verb_ref.as_ref(), m))
.unwrap_or(MotionKind::Null);
let mode = match flags {
CmdFlags::VISUAL => SelectMode::Char(SelectAnchor::End),
CmdFlags::VISUAL_LINE => SelectMode::Line(SelectAnchor::End),
CmdFlags::VISUAL_BLOCK => SelectMode::Block(SelectAnchor::End),
_ => unreachable!()
};
// Start a selection
self.start_selecting(mode);
// Apply the cursor motion
self.apply_motion(motion);
// Use the selection range created by the motion
self.select_range
.map(MotionKind::Inclusive)
.unwrap_or(MotionKind::Null)
} else {
motion
.clone()
.map(|m| self.eval_motion(verb_ref.as_ref(), m))
.unwrap_or({ .unwrap_or({
self.select_range self.select_range
.map(MotionKind::Inclusive) .map(MotionKind::Inclusive)
.unwrap_or(MotionKind::Null) .unwrap_or(MotionKind::Null)
}); })
};
if let Some(verb) = verb.clone() { if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion_eval, register)?; self.exec_verb(verb.1, motion_eval, register)?;
} else {
if is_inplace_edit && i != verb_count.saturating_sub(1) { self.apply_motion(motion_eval);
/*
Used to calculate motions for stuff like '5~' or '8rg'
Those verbs don't have a motion, and always land on
the last character that they operate on.
Therefore, we increment the cursor until we hit verb_count - 1
or the end of the buffer
*/
if !self.cursor.inc() {
break
}
}
} else {
self.apply_motion(motion_eval);
}
} }
/* Done executing, do some cleanup */
let after = self.buffer.clone(); let after = self.buffer.clone();
if clear_redos { if clear_redos {
self.redo_stack.clear(); self.redo_stack.clear();
@@ -1701,14 +2138,32 @@ impl LineBuf {
} }
impl Display for LineBuf { impl Display for LineBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let buf = self.buffer.clone(); let mut full_buf = self.buffer.clone();
write!(f,"{buf}")?; if let Some((start,end)) = self.select_range.clone() {
if let Some(hint) = self.hint() { let mode = self.select_mode.unwrap();
let hint_styled = hint.styled(Style::BrightBlack); let start_byte = self.read_idx_byte_pos(start);
write!(f,"{hint_styled}")?; let end_byte = self.read_idx_byte_pos(end);
match mode.anchor() {
SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() {
inclusive = start_byte..=end_byte.saturating_sub(1);
}
let selected = full_buf[inclusive.clone()].styled(Style::BgWhite | Style::Black);
full_buf.replace_range(inclusive, &selected);
}
SelectAnchor::End => {
let selected = full_buf[start..end].styled(Style::BgWhite | Style::Black);
full_buf.replace_range(start_byte..end_byte, &selected);
}
}
} }
Ok(()) if let Some(hint) = self.hint.as_ref() {
full_buf.push_str(&hint.styled(Style::BrightBlack));
}
write!(f,"{}",full_buf)
} }
} }

View File

@@ -3,7 +3,7 @@ use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode}; use linebuf::{LineBuf, SelectAnchor, SelectMode};
use nix::libc::STDOUT_FILENO; use nix::libc::STDOUT_FILENO;
use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter}; use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter};
use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}}; use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
@@ -107,7 +107,7 @@ impl FernVi {
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new().with_initial("this buffer has (some delimited) text", 0),
history: History::new()? history: History::new()?
}) })
} }
@@ -280,12 +280,12 @@ impl FernVi {
std::mem::swap(&mut mode, &mut self.mode); std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if mode.is_repeatable() { if mode.is_repeatable() {
self.repeat_action = mode.as_replay(); self.repeat_action = mode.as_replay();
} }
self.editor.exec_cmd(cmd)?; self.editor.exec_cmd(cmd)?;
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting { if selecting {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
@@ -345,7 +345,8 @@ impl FernVi {
register: RegisterName::default(), register: RegisterName::default(),
verb: None, verb: None,
motion: Some(motion), motion: Some(motion),
raw_seq: format!("{count};") raw_seq: format!("{count};"),
flags: CmdFlags::empty()
}; };
return self.editor.exec_cmd(repeat_cmd); return self.editor.exec_cmd(repeat_cmd);
} }
@@ -359,7 +360,8 @@ impl FernVi {
register: RegisterName::default(), register: RegisterName::default(),
verb: None, verb: None,
motion: Some(new_motion), motion: Some(new_motion),
raw_seq: format!("{count},") raw_seq: format!("{count},"),
flags: CmdFlags::empty()
}; };
return self.editor.exec_cmd(repeat_cmd); return self.editor.exec_cmd(repeat_cmd);
} }

View File

@@ -1,3 +1,5 @@
use bitflags::bitflags;
use super::register::{append_register, read_register, write_register}; 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 //TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor
@@ -54,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)] #[derive(Clone,Default,Debug)]
pub struct ViCmd { pub struct ViCmd {
pub register: RegisterName, pub register: RegisterName,
pub verb: Option<VerbCmd>, pub verb: Option<VerbCmd>,
pub motion: Option<MotionCmd>, pub motion: Option<MotionCmd>,
pub raw_seq: String, pub raw_seq: String,
pub flags: CmdFlags,
} }
impl ViCmd { impl ViCmd {
@@ -84,6 +96,15 @@ impl ViCmd {
pub fn motion_count(&self) -> usize { pub fn motion_count(&self) -> usize {
self.motion.as_ref().map(|m| m.0).unwrap_or(1) 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 { pub fn is_repeatable(&self) -> bool {
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
} }
@@ -103,7 +124,7 @@ impl ViCmd {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
} }
pub fn is_inplace_edit(&self) -> bool { pub fn is_inplace_edit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceChar(_) | Verb::ToggleCaseSingle)) && self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) &&
self.motion.is_none() self.motion.is_none()
} }
pub fn is_line_motion(&self) -> bool { pub fn is_line_motion(&self) -> bool {
@@ -168,8 +189,9 @@ pub enum Verb {
Change, Change,
Yank, Yank,
Rot13, // lol Rot13, // lol
ReplaceChar(char), ReplaceChar(char), // char to replace with, number of chars to replace
ToggleCaseSingle, ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace
ToggleCaseInplace(u16), // Number of chars to toggle
ToggleCaseRange, ToggleCaseRange,
ToLower, ToLower,
ToUpper, ToUpper,
@@ -191,7 +213,6 @@ pub enum Verb {
JoinLines, JoinLines,
InsertChar(char), InsertChar(char),
Insert(String), Insert(String),
Breakline(Anchor),
Indent, Indent,
Dedent, Dedent,
Equalize, Equalize,
@@ -206,17 +227,17 @@ impl Verb {
Self::Delete | Self::Delete |
Self::Change | Self::Change |
Self::ReplaceChar(_) | Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToLower | Self::ToLower |
Self::ToUpper | Self::ToUpper |
Self::ToggleCaseRange | Self::ToggleCaseRange |
Self::ToggleCaseSingle | Self::ToggleCaseInplace(_) |
Self::Put(_) | Self::Put(_) |
Self::ReplaceMode | Self::ReplaceMode |
Self::InsertModeLineBreak(_) | Self::InsertModeLineBreak(_) |
Self::JoinLines | Self::JoinLines |
Self::InsertChar(_) | Self::InsertChar(_) |
Self::Insert(_) | Self::Insert(_) |
Self::Breakline(_) |
Self::Indent | Self::Indent |
Self::Dedent | Self::Dedent |
Self::Equalize Self::Equalize
@@ -227,8 +248,9 @@ impl Verb {
Self::Delete | Self::Delete |
Self::Change | Self::Change |
Self::ReplaceChar(_) | Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToggleCaseRange | Self::ToggleCaseRange |
Self::ToggleCaseSingle | Self::ToggleCaseInplace(_) |
Self::ToLower | Self::ToLower |
Self::ToUpper | Self::ToUpper |
Self::RepeatLast | Self::RepeatLast |
@@ -238,7 +260,6 @@ impl Verb {
Self::JoinLines | Self::JoinLines |
Self::InsertChar(_) | Self::InsertChar(_) |
Self::Insert(_) | Self::Insert(_) |
Self::Breakline(_) |
Self::Rot13 | Self::Rot13 |
Self::EndOfFile Self::EndOfFile
) )
@@ -247,7 +268,8 @@ impl Verb {
matches!(self, matches!(self,
Self::Change | Self::Change |
Self::InsertChar(_) | Self::InsertChar(_) |
Self::ReplaceChar(_) Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_)
) )
} }
} }
@@ -320,10 +342,8 @@ impl Motion {
Self::ScreenLineUpCharwise | Self::ScreenLineUpCharwise |
Self::ScreenLineDownCharwise | Self::ScreenLineDownCharwise |
Self::ToColumn | Self::ToColumn |
Self::TextObj(TextObj::ForwardSentence,_) | Self::TextObj(TextObj::Sentence(_),_) |
Self::TextObj(TextObj::BackwardSentence,_) | Self::TextObj(TextObj::Paragraph(_),_) |
Self::TextObj(TextObj::ForwardParagraph,_) |
Self::TextObj(TextObj::BackwardParagraph,_) |
Self::CharSearch(Direction::Backward, _, _) | Self::CharSearch(Direction::Backward, _, _) |
Self::WordMotion(To::Start,_,_) | Self::WordMotion(To::Start,_,_) |
Self::ToBrace(_) | Self::ToBrace(_) |
@@ -355,16 +375,11 @@ pub enum TextObj {
/// `iw`, `aw` — inner word, around word /// `iw`, `aw` — inner word, around word
Word(Word), Word(Word),
/// for stuff like 'dd'
Line,
/// `is`, `as` — inner sentence, around sentence /// `is`, `as` — inner sentence, around sentence
ForwardSentence, Sentence(Direction),
BackwardSentence,
/// `ip`, `ap` — inner paragraph, around paragraph /// `ip`, `ap` — inner paragraph, around paragraph
ForwardParagraph, Paragraph(Direction),
BackwardParagraph,
/// `i"`, `a"` — inner/around double quotes /// `i"`, `a"` — inner/around double quotes
DoubleQuote, DoubleQuote,

View File

@@ -6,7 +6,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use super::linebuf::CharClass; use super::linebuf::CharClass;
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word}; use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word};
use crate::prelude::*; use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -82,7 +82,8 @@ impl ViInsert {
self self
} }
pub fn register_and_return(&mut self) -> Option<ViCmd> { 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); self.register_cmd(&cmd);
Some(cmd) Some(cmd)
} }
@@ -188,19 +189,11 @@ impl ViReplace {
self self
} }
pub fn register_and_return(&mut self) -> Option<ViCmd> { 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); self.register_cmd(&cmd);
Some(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) { pub fn register_cmd(&mut self, cmd: &ViCmd) {
self.cmds.push(cmd.clone()) self.cmds.push(cmd.clone())
} }
@@ -218,14 +211,9 @@ impl ViMode for ViReplace {
self.register_and_return() self.register_and_return()
} }
E(K::Char('W'), M::CTRL) => { E(K::Char('W'), M::CTRL) => {
if self.ctrl_w_is_undo() { self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
self.cmds.clear(); self.register_and_return()
Some(self.take_cmd())
} else {
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::Char('H'), M::CTRL) |
E(K::Backspace, M::NONE) => { E(K::Backspace, M::NONE) => {
@@ -280,6 +268,7 @@ impl ViMode for ViReplace {
#[derive(Default,Debug)] #[derive(Default,Debug)]
pub struct ViNormal { pub struct ViNormal {
pending_seq: String, pending_seq: String,
pending_flags: CmdFlags,
} }
impl ViNormal { impl ViNormal {
@@ -292,6 +281,9 @@ impl ViNormal {
pub fn take_cmd(&mut self) -> String { pub fn take_cmd(&mut self) -> String {
std::mem::take(&mut self.pending_seq) std::mem::take(&mut self.pending_seq)
} }
pub fn flags(&self) -> CmdFlags {
self.pending_flags
}
#[allow(clippy::unnecessary_unwrap)] #[allow(clippy::unnecessary_unwrap)]
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
if verb.is_none() { if verb.is_none() {
@@ -337,6 +329,12 @@ impl ViNormal {
self.pending_seq.push(ch); self.pending_seq.push(ch);
let mut chars = self.pending_seq.chars().peekable(); 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 register = 'reg_parse: {
let mut chars_clone = chars.clone(); let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone); let count = self.parse_count(&mut chars_clone);
@@ -345,7 +343,7 @@ impl ViNormal {
break 'reg_parse RegisterName::default() break 'reg_parse RegisterName::default()
}; };
let Some(reg_name) = chars_clone.next() else { let Some(reg_name) = chars_clone.next() else {
return None // Pending register name return None // Pending register name
}; };
match reg_name { match reg_name {
@@ -358,6 +356,17 @@ impl ViNormal {
RegisterName::new(Some(reg_name), count) 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 verb = 'verb_parse: {
let mut chars_clone = chars.clone(); let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1); let count = self.parse_count(&mut chars_clone).unwrap_or(1);
@@ -375,7 +384,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags(),
} }
) )
} }
@@ -412,6 +422,7 @@ impl ViNormal {
verb: Some(VerbCmd(count, Verb::RepeatLast)), verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -421,7 +432,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Delete)), verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -431,7 +443,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Delete)), verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::BackwardChar)), motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -441,8 +454,9 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Change)), verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
} flags: self.flags()
},
) )
} }
'S' => { 'S' => {
@@ -451,7 +465,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Change)), verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -476,9 +491,10 @@ impl ViNormal {
return Some( return Some(
ViCmd { ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::ReplaceChar(ch))), verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,count as u16))),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -488,7 +504,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::ReplaceMode)), verb: Some(VerbCmd(count, Verb::ReplaceMode)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -496,9 +513,10 @@ impl ViNormal {
return Some( return Some(
ViCmd { ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::ToggleCaseSingle)), verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -508,7 +526,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Undo)), verb: Some(VerbCmd(count, Verb::Undo)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -518,7 +537,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::VisualMode)), verb: Some(VerbCmd(count, Verb::VisualMode)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -528,7 +548,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::VisualModeLine)), verb: Some(VerbCmd(count, Verb::VisualModeLine)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -538,7 +559,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))), verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -548,7 +570,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))), verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -558,7 +581,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -568,7 +592,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -578,7 +603,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -588,7 +614,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -598,7 +625,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::JoinLines)), verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -620,7 +648,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Yank)), verb: Some(VerbCmd(count, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -630,7 +659,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Delete)), verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -640,7 +670,8 @@ impl ViNormal {
register, register,
verb: Some(VerbCmd(count, Verb::Change)), verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -672,15 +703,6 @@ impl ViNormal {
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) | ('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
('>', Some(VerbCmd(_,Verb::Indent))) | ('>', Some(VerbCmd(_,Verb::Indent))) |
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
// Exception cases
('w', Some(VerbCmd(_, Verb::Change))) => {
// 'w' usually means 'to the start of the next word'
// e.g. 'dw' deleted to the start of the next word
// however, 'cw' only changes to the end of the current word
// 'caw' is used to change to the start of the next word
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward)));
}
('W', Some(VerbCmd(_, Verb::Change))) => { ('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W' // Same with 'W'
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
@@ -689,47 +711,69 @@ impl ViNormal {
} }
match ch { match ch {
'g' => { 'g' => {
if let Some(ch) = chars_clone.peek() { let Some(ch) = chars_clone.peek() else {
match ch {
'g' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer))
}
'e' => {
chars = chars_clone;
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::WordMotion(To::End, Word::Big, Direction::Backward)));
}
'k' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
}
'j' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
}
'_' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
}
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
}
_ => return self.quit_parse()
}
} else {
break 'motion_parse None break 'motion_parse None
};
match ch {
'g' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer))
}
'e' => {
chars = chars_clone;
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::WordMotion(To::End, Word::Big, Direction::Backward)));
}
'k' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
}
'j' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
}
'_' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
}
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
}
_ => return self.quit_parse()
} }
} }
'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' => { 'G' => {
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer)); break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
@@ -840,6 +884,7 @@ impl ViNormal {
'W' => TextObj::Word(Word::Big), 'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote, '"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote, '\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren, '(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace, '{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket, '[' | ']' => TextObj::Bracket,
@@ -868,7 +913,8 @@ impl ViNormal {
register, register,
verb, verb,
motion, motion,
raw_seq: std::mem::take(&mut self.pending_seq) raw_seq: std::mem::take(&mut self.pending_seq),
flags: self.flags()
} }
) )
} }
@@ -885,7 +931,7 @@ impl ViNormal {
impl ViMode for ViNormal { impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { 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::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => { E(K::Backspace, M::NONE) => {
Some(ViCmd { Some(ViCmd {
@@ -893,6 +939,7 @@ impl ViMode for ViNormal {
verb: None, verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)), motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(), raw_seq: "".into(),
flags: self.flags()
}) })
} }
E(K::Char('R'), M::CTRL) => { E(K::Char('R'), M::CTRL) => {
@@ -903,7 +950,8 @@ impl ViMode for ViNormal {
register: RegisterName::default(), register: RegisterName::default(),
verb: Some(VerbCmd(count,Verb::Redo)), verb: Some(VerbCmd(count,Verb::Redo)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: self.flags()
} }
) )
} }
@@ -919,7 +967,12 @@ impl ViMode for ViNormal {
None None
} }
} }
} };
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
} }
fn is_repeatable(&self) -> bool { fn is_repeatable(&self) -> bool {
@@ -1051,7 +1104,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1061,7 +1115,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::Rot13)), verb: Some(VerbCmd(1, Verb::Rot13)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1078,6 +1133,7 @@ impl ViVisual {
verb: Some(VerbCmd(count, Verb::RepeatLast)), verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1092,6 +1148,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1101,7 +1158,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::Yank)), verb: Some(VerbCmd(1, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1111,7 +1169,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1123,6 +1182,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Change)), verb: Some(VerbCmd(1, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1133,6 +1193,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Indent)), verb: Some(VerbCmd(1, Verb::Indent)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1143,6 +1204,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Dedent)), verb: Some(VerbCmd(1, Verb::Dedent)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1153,6 +1215,7 @@ impl ViVisual {
verb: Some(VerbCmd(1, Verb::Equalize)), verb: Some(VerbCmd(1, Verb::Equalize)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1168,7 +1231,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1178,7 +1242,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)), verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1188,7 +1253,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::ToLower)), verb: Some(VerbCmd(count, Verb::ToLower)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1198,7 +1264,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::ToUpper)), verb: Some(VerbCmd(count, Verb::ToUpper)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1209,7 +1276,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1219,7 +1287,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1229,7 +1298,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::InsertMode)), verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfLine)), motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1239,7 +1309,8 @@ impl ViVisual {
register, register,
verb: Some(VerbCmd(count, Verb::JoinLines)), verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1264,7 +1335,8 @@ impl ViVisual {
register, register,
verb: Some(verb), verb: Some(verb),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}) })
} }
@@ -1294,18 +1366,22 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer))
} }
'e' => { 'e' => {
chars_clone.next();
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward))); break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward)));
} }
'E' => { 'E' => {
chars_clone.next();
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))); break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward)));
} }
'k' => { 'k' => {
chars_clone.next();
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
} }
'j' => { 'j' => {
chars_clone.next();
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
} }
@@ -1417,6 +1493,7 @@ impl ViVisual {
'W' => TextObj::Word(Word::Big), 'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote, '"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote, '\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren, '(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace, '{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket, '[' | ']' => TextObj::Bracket,
@@ -1445,7 +1522,8 @@ impl ViVisual {
register, register,
verb, verb,
motion, motion,
raw_seq: std::mem::take(&mut self.pending_seq) raw_seq: std::mem::take(&mut self.pending_seq),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1462,7 +1540,7 @@ impl ViVisual {
impl ViMode for ViVisual { impl ViMode for ViVisual {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { 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::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => { E(K::Backspace, M::NONE) => {
Some(ViCmd { Some(ViCmd {
@@ -1470,6 +1548,7 @@ impl ViMode for ViVisual {
verb: None, verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)), motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(), raw_seq: "".into(),
flags: CmdFlags::empty()
}) })
} }
E(K::Char('R'), M::CTRL) => { E(K::Char('R'), M::CTRL) => {
@@ -1480,7 +1559,8 @@ impl ViMode for ViVisual {
register: RegisterName::default(), register: RegisterName::default(),
verb: Some(VerbCmd(count,Verb::Redo)), verb: Some(VerbCmd(count,Verb::Redo)),
motion: None, motion: None,
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
} }
) )
} }
@@ -1490,7 +1570,8 @@ impl ViMode for ViVisual {
register: Default::default(), register: Default::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)), verb: Some(VerbCmd(1, Verb::NormalMode)),
motion: Some(MotionCmd(1, Motion::Null)), motion: Some(MotionCmd(1, Motion::Null)),
raw_seq: self.take_cmd() raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}) })
} }
_ => { _ => {
@@ -1501,7 +1582,12 @@ impl ViMode for ViVisual {
None None
} }
} }
} };
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
} }
fn is_repeatable(&self) -> bool { fn is_repeatable(&self) -> bool {

View File

@@ -1,6 +1,6 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{libsh::term::{Style, Styled}, prompt::readline::{keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{raw_mode, KeyReader, LineWriter}, vimode::{ViInsert, ViMode, ViNormal}, FernVi, Readline}}; 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 pretty_assertions::assert_eq;
@@ -170,6 +170,7 @@ impl FernVi {
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
history: History::new().unwrap(),
editor: LineBuf::new().with_initial(initial, 0) editor: LineBuf::new().with_initial(initial, 0)
} }
} }
@@ -503,6 +504,97 @@ fn editor_overshooting_motions() {
); );
} }
#[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."; 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.";