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_width::UnicodeWidthStr;
use super::{term::Layout, vicmd::{Anchor, Dest, Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}};
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}, prelude::*};
use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, ViCmd, Word};
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};
#[derive(Default,PartialEq,Eq,Debug,Clone,Copy)]
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)]
pub enum SelectAnchor {
#[default]
@@ -93,6 +89,28 @@ pub enum SelectMode {
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)]
pub enum MotionKind {
To(usize), // Absolute position, exclusive
@@ -323,7 +341,12 @@ impl LineBuf {
pub fn accept_hint(&mut self) {
let Some(hint) = self.hint.take() else { return };
let old = self.buffer.clone();
let cursor_pos = self.cursor.get();
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());
}
pub fn set_cursor_clamp(&mut self, yn: bool) {
@@ -339,6 +362,12 @@ impl LineBuf {
.copied()
.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
#[track_caller]
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
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 {
To::Start => {
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: {
// 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);
@@ -662,7 +973,7 @@ impl LineBuf {
To::End => {
match 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.
///
/// 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 {
Direction::Backward => 0,
Direction::Forward => self.grapheme_indices().len()
@@ -691,7 +1002,12 @@ impl LineBuf {
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
if on_boundary {
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
@@ -702,9 +1018,12 @@ impl LineBuf {
// Find the next 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
};
if include_last_char {
return ws_pos
}
}
// Return the next visible grapheme position
@@ -716,7 +1035,11 @@ impl LineBuf {
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));
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 {
@@ -739,9 +1062,9 @@ impl LineBuf {
return default
};
// 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
}
}
}
// We are now certainly on a whitespace character. Advance until a non-whitespace character.
@@ -939,6 +1262,11 @@ impl LineBuf {
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
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) {
self.replace_at(self.cursor.get(), new);
}
@@ -966,7 +1294,7 @@ impl LineBuf {
let end = start + gr.len();
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();
if self.has_hint() {
let hint = self.hint.clone().unwrap();
@@ -1004,7 +1332,12 @@ impl LineBuf {
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
}
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);
// End-based operations must include the last character
// But the cursor must also stop just before it when moving
@@ -1023,7 +1356,17 @@ impl LineBuf {
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) => {
let start = self.start_of_line();
let mut newline_count = 0;
@@ -1204,11 +1547,21 @@ impl LineBuf {
MotionCmd(_count,Motion::BeginningOfBuffer) => MotionKind::On(0),
MotionCmd(_count,Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()),
MotionCmd(_count,Motion::ToColumn) => todo!(),
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::Range(start, end)) => todo!(),
MotionCmd(count,Motion::Range(start, end)) => {
let mut final_end = end;
if self.cursor.exclusive {
final_end += 1;
}
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::RepeatMotionRev) => todo!(),
MotionCmd(count,Motion::Null) => MotionKind::Null
@@ -1268,6 +1621,35 @@ impl LineBuf {
self.move_cursor(motion);
}
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) {
match motion {
@@ -1382,24 +1764,45 @@ impl LineBuf {
self.replace_at_cursor(new);
self.apply_motion(motion);
}
Verb::ToggleCaseSingle => {
let Some(gr) = self.grapheme_at_cursor() else {
return Ok(())
};
if gr.len() > 1 || gr.is_empty() {
return Ok(())
Verb::ReplaceCharInplace(ch,count) => {
for i in 0..count {
let mut buf = [0u8;4];
let new = ch.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 ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() {
return Ok(())
}
Verb::ToggleCaseInplace(count) => {
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 => {
let Some((start,end)) = self.range_from_motion(&motion) else {
@@ -1476,7 +1879,9 @@ impl LineBuf {
Verb::Redo |
Verb::Undo => {
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),
// Undo = pop from undo stack, push to redo stack
Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack),
_ => unreachable!()
};
@@ -1495,8 +1900,30 @@ impl LineBuf {
self.update_graphemes();
}
Verb::RepeatLast => todo!(),
Verb::Put(anchor) => todo!(),
Verb::SwapVisualAnchor => todo!(),
Verb::Put(anchor) => {
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 => {
let start = self.start_of_line();
let Some((_,mut end)) = self.nth_next_line(1) else {
@@ -1529,7 +1956,6 @@ impl LineBuf {
let graphemes = string.graphemes(true).count();
self.cursor.add(graphemes);
}
Verb::Breakline(anchor) => todo!(),
Verb::Indent => {
let Some((start,end)) = self.range_from_motion(&motion) else {
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 motion_count = motion.as_ref().map(|m| m.0);
let verb_cmd_ref = verb.as_ref();
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 cursor_pos = self.cursor.get();
for i in 0..verb_count {
/*
* Let's evaluate the motion now
* If motion is None, we will try to use self.select_range
* If self.select_range is None, we will use MotionKind::Null
*/
let motion_eval = motion
/*
* 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 self.select_range is None, we will use MotionKind::Null
*/
let motion_eval = if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
let motion = motion
.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({
self.select_range
.map(MotionKind::Inclusive)
.unwrap_or(MotionKind::Null)
});
})
};
if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion_eval, register)?;
if is_inplace_edit && i != verb_count.saturating_sub(1) {
/*
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);
}
if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion_eval, register)?;
} else {
self.apply_motion(motion_eval);
}
/* Done executing, do some cleanup */
let after = self.buffer.clone();
if clear_redos {
self.redo_stack.clear();
@@ -1701,14 +2138,32 @@ impl LineBuf {
}
impl Display for LineBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let buf = self.buffer.clone();
write!(f,"{buf}")?;
if let Some(hint) = self.hint() {
let hint_styled = hint.styled(Style::BrightBlack);
write!(f,"{hint_styled}")?;
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut full_buf = self.buffer.clone();
if let Some((start,end)) = self.select_range.clone() {
let mode = self.select_mode.unwrap();
let start_byte = self.read_idx_byte_pos(start);
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 nix::libc::STDOUT_FILENO;
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 crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
@@ -107,7 +107,7 @@ impl FernVi {
old_layout: None,
repeat_action: None,
repeat_motion: None,
editor: LineBuf::new(),
editor: LineBuf::new().with_initial("this buffer has (some delimited) text", 0),
history: History::new()?
})
}
@@ -280,12 +280,12 @@ impl FernVi {
std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if mode.is_repeatable() {
self.repeat_action = mode.as_replay();
}
self.editor.exec_cmd(cmd)?;
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
@@ -345,7 +345,8 @@ impl FernVi {
register: RegisterName::default(),
verb: None,
motion: Some(motion),
raw_seq: format!("{count};")
raw_seq: format!("{count};"),
flags: CmdFlags::empty()
};
return self.editor.exec_cmd(repeat_cmd);
}
@@ -359,7 +360,8 @@ impl FernVi {
register: RegisterName::default(),
verb: None,
motion: Some(new_motion),
raw_seq: format!("{count},")
raw_seq: format!("{count},"),
flags: CmdFlags::empty()
};
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};
//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)]
pub struct ViCmd {
pub register: RegisterName,
pub verb: Option<VerbCmd>,
pub motion: Option<MotionCmd>,
pub raw_seq: String,
pub flags: CmdFlags,
}
impl ViCmd {
@@ -84,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())
}
@@ -103,7 +124,7 @@ impl ViCmd {
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::ReplaceChar(_) | Verb::ToggleCaseSingle)) &&
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 {
@@ -168,8 +189,9 @@ pub enum Verb {
Change,
Yank,
Rot13, // lol
ReplaceChar(char),
ToggleCaseSingle,
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,
@@ -191,7 +213,6 @@ pub enum Verb {
JoinLines,
InsertChar(char),
Insert(String),
Breakline(Anchor),
Indent,
Dedent,
Equalize,
@@ -206,17 +227,17 @@ impl Verb {
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToLower |
Self::ToUpper |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::ToggleCaseInplace(_) |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Breakline(_) |
Self::Indent |
Self::Dedent |
Self::Equalize
@@ -227,8 +248,9 @@ impl Verb {
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::ToggleCaseInplace(_) |
Self::ToLower |
Self::ToUpper |
Self::RepeatLast |
@@ -238,7 +260,6 @@ impl Verb {
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Breakline(_) |
Self::Rot13 |
Self::EndOfFile
)
@@ -247,7 +268,8 @@ impl Verb {
matches!(self,
Self::Change |
Self::InsertChar(_) |
Self::ReplaceChar(_)
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_)
)
}
}
@@ -320,10 +342,8 @@ impl Motion {
Self::ScreenLineUpCharwise |
Self::ScreenLineDownCharwise |
Self::ToColumn |
Self::TextObj(TextObj::ForwardSentence,_) |
Self::TextObj(TextObj::BackwardSentence,_) |
Self::TextObj(TextObj::ForwardParagraph,_) |
Self::TextObj(TextObj::BackwardParagraph,_) |
Self::TextObj(TextObj::Sentence(_),_) |
Self::TextObj(TextObj::Paragraph(_),_) |
Self::CharSearch(Direction::Backward, _, _) |
Self::WordMotion(To::Start,_,_) |
Self::ToBrace(_) |
@@ -355,16 +375,11 @@ pub enum TextObj {
/// `iw`, `aw` — inner word, around word
Word(Word),
/// for stuff like 'dd'
Line,
/// `is`, `as` — inner sentence, around sentence
ForwardSentence,
BackwardSentence,
Sentence(Direction),
/// `ip`, `ap` — inner paragraph, around paragraph
ForwardParagraph,
BackwardParagraph,
Paragraph(Direction),
/// `i"`, `a"` — inner/around double quotes
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::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::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -82,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)
}
@@ -188,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())
}
@@ -218,14 +211,9 @@ 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::WordMotion(To::Start, Word::Normal, Direction::Backward)));
self.register_and_return()
}
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) => {
@@ -280,6 +268,7 @@ impl ViMode for ViReplace {
#[derive(Default,Debug)]
pub struct ViNormal {
pending_seq: String,
pending_flags: CmdFlags,
}
impl ViNormal {
@@ -292,6 +281,9 @@ 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() {
@@ -337,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);
@@ -345,7 +343,7 @@ impl ViNormal {
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
};
match reg_name {
@@ -358,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);
@@ -375,7 +384,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
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)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -421,7 +432,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
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,
verb: Some(VerbCmd(count, Verb::Delete)),
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,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd()
}
raw_seq: self.take_cmd(),
flags: self.flags()
},
)
}
'S' => {
@@ -451,7 +465,8 @@ impl ViNormal {
register,
verb: Some(VerbCmd(count, Verb::Change)),
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(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ReplaceChar(ch))),
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,count as u16))),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -488,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()
}
)
}
@@ -496,9 +513,10 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ToggleCaseSingle)),
verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: self.flags()
}
)
}
@@ -508,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()
}
)
}
@@ -518,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()
}
)
}
@@ -528,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()
}
)
}
@@ -538,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()
}
)
}
@@ -548,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()
}
)
}
@@ -558,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()
}
)
}
@@ -568,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()
}
)
}
@@ -578,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()
}
)
}
@@ -588,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()
}
)
}
@@ -598,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()
}
)
}
@@ -620,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()
}
)
}
@@ -630,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()
}
)
}
@@ -640,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()
}
)
}
@@ -672,15 +703,6 @@ impl ViNormal {
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
('>', Some(VerbCmd(_,Verb::Indent))) |
('<', 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))) => {
// Same with 'W'
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
@@ -689,47 +711,69 @@ impl ViNormal {
}
match ch {
'g' => {
if let Some(ch) = chars_clone.peek() {
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 {
let Some(ch) = chars_clone.peek() else {
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' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
@@ -840,6 +884,7 @@ impl ViNormal {
'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket,
@@ -868,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()
}
)
}
@@ -885,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 {
@@ -893,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) => {
@@ -903,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()
}
)
}
@@ -919,7 +967,12 @@ impl ViMode for ViNormal {
None
}
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {
@@ -1051,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()
}
)
}
@@ -1061,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()
}
)
}
@@ -1078,6 +1133,7 @@ impl ViVisual {
verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1092,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()
}
)
}
@@ -1101,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()
}
)
}
@@ -1111,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()
}
)
}
@@ -1123,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()
}
)
}
@@ -1133,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()
}
)
}
@@ -1143,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()
}
)
}
@@ -1153,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()
}
)
}
@@ -1168,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()
}
)
}
@@ -1178,7 +1242,8 @@ impl ViVisual {
register,
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None,
raw_seq: self.take_cmd()
raw_seq: self.take_cmd(),
flags: CmdFlags::empty()
}
)
}
@@ -1188,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()
}
)
}
@@ -1198,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()
}
)
}
@@ -1209,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()
}
)
}
@@ -1219,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()
}
)
}
@@ -1229,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()
}
)
}
@@ -1239,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()
}
)
}
@@ -1264,7 +1335,8 @@ impl ViVisual {
register,
verb: Some(verb),
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))
}
'e' => {
chars_clone.next();
chars = chars_clone;
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::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));
}
@@ -1417,6 +1493,7 @@ impl ViVisual {
'W' => TextObj::Word(Word::Big),
'"' => TextObj::DoubleQuote,
'\'' => TextObj::SingleQuote,
'`' => TextObj::BacktickQuote,
'(' | ')' | 'b' => TextObj::Paren,
'{' | '}' | 'B' => TextObj::Brace,
'[' | ']' => TextObj::Bracket,
@@ -1445,7 +1522,8 @@ impl ViVisual {
register,
verb,
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 {
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 {
@@ -1470,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) => {
@@ -1480,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()
}
)
}
@@ -1490,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()
})
}
_ => {
@@ -1501,7 +1582,12 @@ impl ViMode for ViVisual {
None
}
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {