From ff0207a27f360483c0d8a8e06334a7ec9c713ec6 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Mon, 9 Jun 2025 02:29:34 -0400 Subject: [PATCH] implemented quote/delimiter text objects --- src/prompt/readline/linebuf.rs | 617 ++++++++++++++++++++++++++++----- src/prompt/readline/mod.rs | 12 +- src/prompt/readline/vicmd.rs | 55 +-- src/prompt/readline/vimode.rs | 314 +++++++++++------ src/tests/readline.rs | 94 ++++- 5 files changed, 871 insertions(+), 221 deletions(-) diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 94acd5e..8417314 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -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 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) } } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index c180de3..3b84fd8 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -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); } diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index 545fd46..afd9f39 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -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, pub motion: Option, 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, diff --git a/src/prompt/readline/vimode.rs b/src/prompt/readline/vimode.rs index 22c0e08..ebc05f4 100644 --- a/src/prompt/readline/vimode.rs +++ b/src/prompt/readline/vimode.rs @@ -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 { - 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 { - 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 " + */ 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 { - 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 { - 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 { diff --git a/src/tests/readline.rs b/src/tests/readline.rs index 644fa80..0efc9f3 100644 --- a/src/tests/readline.rs +++ b/src/tests/readline.rs @@ -1,6 +1,6 @@ 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; @@ -170,6 +170,7 @@ impl FernVi { old_layout: None, repeat_action: None, repeat_motion: None, + history: History::new().unwrap(), 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 \\<\\>delimited> text", + 0), + ("this buffer has <> text".into(), 17) + ); + assert_eq!(normal_cmd( + "da>", + "this buffer has \\<\\>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.";