finally found a good way to select line spans

This commit is contained in:
2025-06-07 03:50:35 -04:00
parent 4472478703
commit 3cfc49d638
5 changed files with 382 additions and 212 deletions

View File

@@ -92,9 +92,9 @@ pub enum MotionKind {
Inclusive((usize,usize)), // Range, inclusive
Exclusive((usize,usize)), // Range, exclusive
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position
InclusiveWithTarget((usize,usize),usize),
ExclusiveWithTarget((usize,usize),usize),
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position on the line
InclusiveWithTargetCol((usize,usize),usize),
ExclusiveWithTargetCol((usize,usize),usize),
Null
}
@@ -198,6 +198,9 @@ impl ClampedUsize {
self.max
}
}
/// Increment the ClampedUsize value
///
/// Returns false if the attempted increment is rejected by the clamp
pub fn inc(&mut self) -> bool {
let max = self.upper_bound();
if self.value == max {
@@ -206,6 +209,9 @@ impl ClampedUsize {
self.add(1);
true
}
/// Decrement the ClampedUsize value
///
/// Returns false if the attempted decrement would cause underflow
pub fn dec(&mut self) -> bool {
if self.value == 0 {
return false;
@@ -389,6 +395,7 @@ impl LineBuf {
let end = self.grapheme_indices()[end];
self.buffer.drain(start..end).collect()
};
flog!(DEBUG,drained);
self.update_graphemes();
drained
}
@@ -428,154 +435,97 @@ impl LineBuf {
self.last_selection = self.select_range.take();
}
}
pub fn rfind_newlines(&mut self, n: usize) -> (usize,bool) {
self.rfind_newlines_from(self.cursor.get(), n)
pub fn total_lines(&mut self) -> usize {
self.buffer
.graphemes(true)
.filter(|g| *g == "\n")
.count()
}
pub fn find_newlines(&mut self, n: usize) -> (usize,bool) {
self.find_newlines_from(self.cursor.get(), n)
pub fn cursor_line_number(&mut self) -> usize {
self.slice_to_cursor()
.map(|slice| {
slice.graphemes(true)
.filter(|g| *g == "\n")
.count()
}).unwrap_or(0)
}
pub fn find_newlines_in_direction(&mut self, start_pos: usize, n: usize, dir: Direction) -> (usize, bool) {
if n == 0 {
return (start_pos,true)
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize,usize)> {
let line_no = self.cursor_line_number() + n;
if line_no > self.total_lines() {
return None
}
let mut indices_iter = self.directional_indices_iter_from(start_pos, dir);
let default = match dir {
Direction::Backward => 0,
Direction::Forward => self.cursor.max
};
let mut result;
let mut count = 0;
// Special case: newline at start_pos
if self.grapheme_at(start_pos) == Some("\n") {
count += 1;
indices_iter.next();
if n == 1 {
return (start_pos,true);
}
}
while let Some(i) = indices_iter.find(|i| self.grapheme_at(*i) == Some("\n")) {
result = i;
count += 1;
if count == n {
return (result, true);
}
}
(default, false)
}
pub fn rfind_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
self.find_newlines_in_direction(start_pos, n, Direction::Backward)
}
pub fn find_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
self.find_newlines_in_direction(start_pos, n, Direction::Forward)
}
pub fn find_index_for(&self, byte_pos: usize) -> Option<usize> {
self.grapheme_indices()
.binary_search(&byte_pos)
.ok()
}
pub fn start_of_cursor_line(&mut self) -> usize {
let (mut pos,_) = self.rfind_newlines(1);
if self.grapheme_at(pos) == Some("\n") || pos != 0 {
pos += 1; // Don't include the newline itself
}
pos
}
pub fn end_of_cursor_line(&mut self) -> usize {
self.find_newlines(1).0
}
pub fn this_line(&mut self) -> (usize,usize) {
(
self.start_of_cursor_line(),
self.end_of_cursor_line()
)
Some(self.line_bounds(line_no))
}
pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> {
if self.start_of_cursor_line() == 0 {
let cursor_line_no = self.cursor_line_number();
if cursor_line_no == 0 {
return None
}
let (start,_) = self.select_lines_up(n);
let slice = self.slice_from_cursor()?;
let end = slice.find('\n').unwrap_or(self.cursor.max);
Some((start,end))
}
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize, usize)> {
if self.end_of_cursor_line() == self.cursor.max {
let line_no = cursor_line_no.saturating_sub(n);
if line_no > self.total_lines() {
return None
}
Some(self.line_bounds(line_no))
}
pub fn this_line(&mut self) -> (usize,usize) {
let line_no = self.cursor_line_number();
self.line_bounds(line_no)
}
pub fn start_of_line(&mut self) -> usize {
self.this_line().0
}
pub fn end_of_line(&mut self) -> usize {
self.this_line().1
}
pub fn select_lines_up(&mut self, n: usize) -> Option<(usize,usize)> {
if self.start_of_line() == 0 {
return None
}
let target_line = self.cursor_line_number().saturating_sub(n);
let end = self.end_of_line();
let (start,_) = self.line_bounds(target_line);
let (_,end) = self.select_lines_down(n);
let end_clamped = ClampedUsize::new(end, self.cursor.max, /*exclusive:*/ true);
let slice = self.slice_to(end_clamped.get())?;
let start = slice.rfind('\n').unwrap_or(0);
Some((start,end))
}
/// Include the leading newline, if any
pub fn prev_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
let (mut start,end) = self.nth_prev_line(1)?;
start = start.saturating_sub(1);
pub fn select_lines_down(&mut self, n: usize) -> Option<(usize,usize)> {
if self.end_of_line() == self.cursor.max {
return None
}
let target_line = self.cursor_line_number() + n;
let start = self.start_of_line();
let (_,end) = self.line_bounds(target_line);
Some((start,end))
}
/// Include the trailing newline, if any
pub fn prev_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
let (start,mut end) = self.nth_prev_line(1)?;
end = (end + 1).min(self.cursor.max);
Some((start,end))
}
/// Include the leading newline, if any
pub fn next_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
let (mut start,end) = self.nth_next_line(1)?;
start = start.saturating_sub(1);
Some((start,end))
}
/// Include the trailing newline, if any
pub fn next_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
let (start,mut end) = self.nth_next_line(1)?;
end = (end + 1).min(self.cursor.max);
Some((start,end))
}
pub fn select_lines_up(&mut self, n: usize) -> (usize,usize) {
let (mut start,end) = self.this_line();
if start == 0 {
return (start,end)
pub fn line_bounds(&mut self, n: usize) -> (usize,usize) {
if n > self.total_lines() {
panic!("Attempted to find line {n} when there are only {} lines",self.total_lines())
}
let mut grapheme_index = 0;
let mut start = 0;
// Fine the start of the line
for _ in 0..n {
let slice = self.slice_to(start - 1).unwrap();
if let Some(prev_nl) = slice.rfind('\n') {
start = self.find_index_for(prev_nl).unwrap();
} else {
start = 0;
break
}
}
(start,end)
}
pub fn select_lines_down(&mut self, n: usize) -> (usize,usize) {
let (start,mut end) = self.this_line();
if end == self.cursor.max {
return (start,end)
}
for _ in 0..=n {
let next_ln_start = end + 1;
if next_ln_start >= self.cursor.max {
end = self.cursor.max;
break
}
let slice = self.slice_from(next_ln_start).unwrap();
if let Some(next_nl) = slice.find('\n') {
end = self.find_index_for(next_nl).unwrap();
} else {
end = self.cursor.max;
break
for (_, g) in self.buffer.grapheme_indices(true).skip(grapheme_index) {
grapheme_index += 1;
if g == "\n" {
start = grapheme_index;
break;
}
}
}
(start,end)
let mut end = start;
// Find the end of the line
for (_, g) in self.buffer.grapheme_indices(true).skip(start) {
end += 1;
if g == "\n" {
break;
}
}
(start, end)
}
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
@@ -589,6 +539,8 @@ impl LineBuf {
return
};
edit.new.push_str(&diff.new);
edit.old.push_str(&diff.old);
@@ -830,6 +782,13 @@ impl LineBuf {
fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
let mut col = 0;
for (grapheme_index, g) in line.graphemes(true).enumerate() {
if g == "\n" {
if self.cursor.exclusive {
return grapheme_index.saturating_sub(1)
} else {
return grapheme_index;
}
}
let w = g.width();
if col + w > target_col {
return grapheme_index;
@@ -840,7 +799,7 @@ impl LineBuf {
line.graphemes(true).count()
}
pub fn cursor_col(&mut self) -> usize {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let end = self.cursor.get();
let Some(slice) = self.slice_inclusive(start..=end) else {
return start
@@ -903,6 +862,24 @@ impl LineBuf {
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
self.find_from(self.cursor.get(), op)
}
pub fn replace_at_cursor(&mut self, new: &str) {
self.replace_at(self.cursor.get(), new);
}
pub fn replace_at(&mut self, pos: usize, new: &str) {
let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else {
self.buffer.push_str(new);
return
};
if &gr == "\n" {
// Do not replace the newline, push it forward instead
let byte_pos = self.index_byte_pos(pos);
self.buffer.insert_str(byte_pos, new);
return
}
let start = self.index_byte_pos(pos);
let end = start + gr.len();
self.buffer.replace_range(start..end, new);
}
pub fn eval_motion(&mut self, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone();
if self.has_hint() {
@@ -912,9 +889,33 @@ impl LineBuf {
let eval = match motion {
MotionCmd(count,Motion::WholeLine) => {
let start = self.start_of_cursor_line();
let end = self.find_newlines(count).0;
MotionKind::Inclusive((start,end))
let Some((start,end)) = (if count == 1 {
Some(self.this_line())
} else {
self.select_lines_down(count)
}) else {
return MotionKind::Null
};
let target_col = if let Some(col) = self.saved_col {
col
} else {
let col = self.cursor_col();
self.saved_col = Some(col);
col
};
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
return MotionKind::Null
};
flog!(DEBUG,target_col);
flog!(DEBUG,target_col);
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_pos);
if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") {
target_pos = target_pos.saturating_sub(1); // Don't land on the newline
}
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
}
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
let pos = self.dispatch_word_motion(count, to, word, dir);
@@ -938,7 +939,7 @@ impl LineBuf {
}
MotionCmd(count,Motion::TextObj(text_obj, bound)) => todo!(),
MotionCmd(count,Motion::EndOfLastWord) => {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let mut newline_count = 0;
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
let mut last_graphical = None;
@@ -960,7 +961,7 @@ impl LineBuf {
MotionKind::On(last)
}
MotionCmd(_,Motion::BeginningOfFirstWord) => {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
let mut first_graphical = None;
while let Some(idx) = indices.next() {
@@ -978,9 +979,16 @@ impl LineBuf {
};
MotionKind::On(first)
}
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()),
MotionCmd(count,Motion::EndOfLine) => {
let pos = self.find_newlines(count).0;
let pos = if count == 1 {
self.end_of_line()
} else if let Some((_,end)) = self.select_lines_down(count) {
end
} else {
self.end_of_line()
};
MotionKind::On(pos)
}
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => {
@@ -1008,8 +1016,29 @@ impl LineBuf {
}
MotionKind::Onto(pos.get())
}
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
MotionCmd(count,motion @ (Motion::ForwardChar | Motion::BackwardChar)) => {
let mut target = self.cursor;
target.exclusive = false;
for _ in 0..count {
match motion {
Motion::BackwardChar => target.sub(1),
Motion::ForwardChar => {
if self.cursor.exclusive && self.grapheme_at(target.ret_add(1)) == Some("\n") {
flog!(DEBUG, "returning null");
return MotionKind::Null
}
target.add(1);
continue
}
_ => unreachable!()
}
if self.grapheme_at(target.get()) == Some("\n") {
flog!(DEBUG, "returning null outside of match");
return MotionKind::Null
}
}
MotionKind::On(target.get())
}
MotionCmd(count,Motion::LineDown) |
MotionCmd(count,Motion::LineUp) => {
let Some((start,end)) = (match motion.1 {
@@ -1017,12 +1046,11 @@ impl LineBuf {
Motion::LineDown => self.nth_next_line(1),
_ => unreachable!()
}) else {
flog!(WARN, "failed to find target line");
return MotionKind::Null
};
flog!(DEBUG, self.slice(start..end));
let target_col = if let Some(col) = self.saved_col {
let mut target_col = if let Some(col) = self.saved_col {
col
} else {
let col = self.cursor_col();
@@ -1033,15 +1061,21 @@ impl LineBuf {
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
return MotionKind::Null
};
let target_pos = start + self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_col);
flog!(DEBUG,target_col);
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_pos);
if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") {
target_pos = target_pos.saturating_sub(1); // Don't land on the newline
}
let (start,end) = match motion.1 {
Motion::LineUp => (start,self.end_of_cursor_line()),
Motion::LineDown => (self.start_of_cursor_line(),end),
Motion::LineUp => (start,self.end_of_line()),
Motion::LineDown => (self.start_of_line(),end),
_ => unreachable!()
};
MotionKind::InclusiveWithTarget((start,end),target_pos)
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
}
MotionCmd(count,Motion::LineDownCharwise) |
MotionCmd(count,Motion::LineUpCharwise) => {
@@ -1149,9 +1183,13 @@ impl LineBuf {
std::cmp::Ordering::Equal => { /* Do nothing */ }
}
}
MotionKind::InclusiveWithTarget((_,_),start) |
MotionKind::ExclusiveWithTargetCol((_,_),col) |
MotionKind::InclusiveWithTargetCol((_,_),col) => {
let (start,end) = self.this_line();
let end = end.min(col);
self.cursor.set(start + end)
}
MotionKind::Inclusive((start,_)) |
MotionKind::ExclusiveWithTarget((_,_),start) |
MotionKind::Exclusive((start,_)) => {
self.cursor.set(start)
}
@@ -1183,14 +1221,14 @@ impl LineBuf {
};
ordered(self.cursor.get(), pos)
}
MotionKind::InclusiveWithTarget((start,end),_) |
MotionKind::Inclusive((start,end)) => {
MotionKind::InclusiveWithTargetCol((start,end),_) |
MotionKind::Inclusive((start,end)) => ordered(*start, *end),
MotionKind::ExclusiveWithTargetCol((start,end),_) |
MotionKind::Exclusive((start,end)) => {
let (start, mut end) = ordered(*start, *end);
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
end = end.saturating_sub(1);
(start,end)
}
MotionKind::ExclusiveWithTarget((start,end),_) |
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
MotionKind::Null => return None
};
Some(range)
@@ -1212,8 +1250,12 @@ impl LineBuf {
};
register.write_to_register(register_text);
match motion {
MotionKind::ExclusiveWithTarget((_,_),pos) |
MotionKind::InclusiveWithTarget((_,_),pos) => self.cursor.set(pos),
MotionKind::ExclusiveWithTargetCol((_,_),pos) |
MotionKind::InclusiveWithTargetCol((_,_),pos) => {
let (start,end) = self.this_line();
self.cursor.set(start);
self.cursor.add(end.min(pos));
}
_ => self.cursor.set(start),
}
}
@@ -1227,8 +1269,55 @@ impl LineBuf {
self.buffer.replace_range(start..end, &rot13);
self.cursor.set(start);
}
Verb::ReplaceChar(_) => todo!(),
Verb::ToggleCase => todo!(),
Verb::ReplaceChar(ch) => {
let mut buf = [0u8;4];
let new = ch.encode_utf8(&mut buf);
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(())
}
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);
}
Verb::ToggleCaseRange => {
let Some((start,end)) = self.range_from_motion(&motion) else {
return Ok(())
};
for i in start..end {
let Some(gr) = self.grapheme_at(i) else {
continue
};
if gr.len() > 1 || gr.is_empty() {
continue
}
let ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() {
continue
}
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(i,new);
}
}
Verb::ToLower => todo!(),
Verb::ToUpper => todo!(),
Verb::Complete => todo!(),
@@ -1252,14 +1341,8 @@ impl LineBuf {
Verb::Indent => todo!(),
Verb::Dedent => todo!(),
Verb::Equalize => todo!(),
Verb::AcceptLineOrNewline => todo!(),
Verb::EndOfFile => {
if self.buffer.is_empty() {
}
}
Verb::InsertModeLineBreak(anchor) => {
let end = self.end_of_cursor_line();
let end = self.end_of_line();
self.insert_at(end,'\n');
self.cursor.set(end);
match anchor {
@@ -1268,16 +1351,15 @@ impl LineBuf {
}
}
Verb::ReplaceMode |
Verb::EndOfFile |
Verb::InsertMode |
Verb::NormalMode |
Verb::VisualMode |
Verb::ReplaceMode |
Verb::VisualModeLine |
Verb::VisualModeBlock |
Verb::VisualModeSelectLast => {
/* Already handled */
self.apply_motion(motion);
}
Verb::AcceptLineOrNewline |
Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
}
Ok(())
}
@@ -1286,6 +1368,7 @@ impl LineBuf {
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
let is_line_motion = cmd.is_line_motion();
let is_undo_op = cmd.is_undo_op();
let is_inplace_edit = cmd.is_inplace_edit();
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
// Merge character inserts into one edit
@@ -1297,13 +1380,13 @@ impl LineBuf {
let ViCmd { register, verb, motion, raw_seq: _ } = cmd;
let verb_count = verb.as_ref().map(|v| v.0);
let verb_count = verb.as_ref().map(|v| v.0).unwrap_or(1);
let motion_count = motion.as_ref().map(|m| m.0);
let before = self.buffer.clone();
let cursor_pos = self.cursor.get();
for _ in 0..verb_count.unwrap_or(1) {
for i in 0..verb_count {
/*
* Let's evaluate the motion now
* If motion is None, we will try to use self.select_range
@@ -1320,6 +1403,19 @@ impl LineBuf {
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);
}

View File

@@ -94,7 +94,7 @@ impl FernVi {
pub fn get_layout(&mut self) -> Layout {
let line = self.editor.as_str().to_string();
let to_cursor = self.editor.slice_to_cursor().unwrap();
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line)
}

View File

@@ -102,6 +102,10 @@ impl ViCmd {
pub fn is_undo_op(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
}
pub fn is_inplace_edit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceChar(_) | Verb::ToggleCaseSingle)) &&
self.motion.is_none()
}
pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| {
matches!(m.1,
@@ -165,7 +169,8 @@ pub enum Verb {
Yank,
Rot13, // lol
ReplaceChar(char),
ToggleCase,
ToggleCaseSingle,
ToggleCaseRange,
ToLower,
ToUpper,
Complete,
@@ -203,7 +208,8 @@ impl Verb {
Self::ReplaceChar(_) |
Self::ToLower |
Self::ToUpper |
Self::ToggleCase |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
@@ -221,7 +227,8 @@ impl Verb {
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ToggleCase |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::ToLower |
Self::ToUpper |
Self::RepeatLast |

View File

@@ -383,6 +383,21 @@ impl ViNormal {
}
)
}
'~' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
}
'u' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
}
'U' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
}
'?' => {
chars_clone.next();
chars = chars_clone;
@@ -465,8 +480,8 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
verb: Some(VerbCmd(count, Verb::ReplaceChar(ch))),
motion: None,
raw_seq: self.take_cmd()
}
)
@@ -485,8 +500,8 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
verb: Some(VerbCmd(count, Verb::ToggleCaseSingle)),
motion: None,
raw_seq: self.take_cmd()
}
)
@@ -654,6 +669,9 @@ impl ViNormal {
('c', Some(VerbCmd(_,Verb::Change))) |
('y', Some(VerbCmd(_,Verb::Yank))) |
('=', Some(VerbCmd(_,Verb::Equalize))) |
('u', Some(VerbCmd(_,Verb::ToLower))) |
('U', Some(VerbCmd(_,Verb::ToUpper))) |
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
('>', Some(VerbCmd(_,Verb::Indent))) |
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
_ => {}
@@ -1145,7 +1163,7 @@ impl ViVisual {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None,
raw_seq: self.take_cmd()
}