Skip to content

Commit eedb81a

Browse files
committed
fix(vi-mode): fix ^ motion
Previously the `^` motion was behaving like the `0` motion which is to move the cursor to the start of the line. But in vim, the `^` motion is different from `0` motion. In vim `^` moved the cursor to the first non-blank character of the line. (See `:h ^`) Updated test case for `d^` and `c^` command since it has different behaviour now.
1 parent 9abb8ce commit eedb81a

File tree

6 files changed

+83
-3
lines changed

6 files changed

+83
-3
lines changed

src/core_editor/editor.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use super::{edit_stack::EditStack, Clipboard, ClipboardMode, LineBuffer};
33
use crate::core_editor::get_system_clipboard;
44
use crate::enums::{EditType, TextObject, TextObjectScope, TextObjectType, UndoBehavior};
55
use crate::{core_editor::get_local_clipboard, EditCommand};
6+
use std::cmp::{max, min};
67
use std::ops::{DerefMut, Range};
78

89
/// Stateful editor executing changes to the underlying [`LineBuffer`]
@@ -50,6 +51,9 @@ impl Editor {
5051
match command {
5152
EditCommand::MoveToStart { select } => self.move_to_start(*select),
5253
EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select),
54+
EditCommand::MoveToLineNonBlankStart { select } => {
55+
self.move_to_line_non_blank_start(*select)
56+
}
5357
EditCommand::MoveToEnd { select } => self.move_to_end(*select),
5458
EditCommand::MoveToLineEnd { select } => self.move_to_line_end(*select),
5559
EditCommand::MoveToPosition { position, select } => {
@@ -82,6 +86,7 @@ impl Editor {
8286
EditCommand::CutCurrentLine => self.cut_current_line(),
8387
EditCommand::CutFromStart => self.cut_from_start(),
8488
EditCommand::CutFromLineStart => self.cut_from_line_start(),
89+
EditCommand::CutFromLineNonBlankStart => self.cut_from_line_non_blank_start(),
8590
EditCommand::CutToEnd => self.cut_from_end(),
8691
EditCommand::CutToLineEnd => self.cut_to_line_end(),
8792
EditCommand::KillLine => self.kill_line(),
@@ -123,6 +128,7 @@ impl Editor {
123128
EditCommand::Paste => self.paste_cut_buffer(),
124129
EditCommand::CopyFromStart => self.copy_from_start(),
125130
EditCommand::CopyFromLineStart => self.copy_from_line_start(),
131+
EditCommand::CopyFromLineNonBlankStart => self.copy_from_line_non_blank_start(),
126132
EditCommand::CopyToEnd => self.copy_from_end(),
127133
EditCommand::CopyToLineEnd => self.copy_to_line_end(),
128134
EditCommand::CopyWordLeft => self.copy_word_left(),
@@ -291,6 +297,11 @@ impl Editor {
291297
self.line_buffer.move_to_line_start();
292298
}
293299

300+
pub(crate) fn move_to_line_non_blank_start(&mut self, select: bool) {
301+
self.update_selection_anchor(select);
302+
self.line_buffer.move_to_line_non_blank_start();
303+
}
304+
294305
pub(crate) fn move_to_line_end(&mut self, select: bool) {
295306
self.update_selection_anchor(select);
296307
self.line_buffer.move_to_line_end();
@@ -351,6 +362,14 @@ impl Editor {
351362
}
352363
}
353364

365+
fn cut_from_line_non_blank_start(&mut self) {
366+
let offset_a = self.line_buffer.insertion_point();
367+
self.line_buffer.move_to_line_non_blank_start();
368+
let offset_b = self.line_buffer.insertion_point();
369+
let deletion_range = min(offset_a, offset_b)..max(offset_a, offset_b);
370+
self.cut_range(deletion_range);
371+
}
372+
354373
fn cut_from_end(&mut self) {
355374
let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
356375
if !cut_slice.is_empty() {
@@ -852,6 +871,15 @@ impl Editor {
852871
self.copy_range(copy_range);
853872
}
854873

874+
pub(crate) fn copy_from_line_non_blank_start(&mut self) {
875+
let offset_a = self.line_buffer.insertion_point();
876+
self.line_buffer.move_to_line_non_blank_start();
877+
let offset_b = self.line_buffer.insertion_point();
878+
self.line_buffer.set_insertion_point(offset_a);
879+
let copy_range = min(offset_a, offset_b)..max(offset_a, offset_b);
880+
self.copy_range(copy_range);
881+
}
882+
855883
pub(crate) fn copy_from_end(&mut self) {
856884
let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len();
857885
self.copy_range(copy_range);

src/core_editor/line_buffer.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ impl LineBuffer {
113113
// str is guaranteed to be utf8, thus \n is safe to assume 1 byte long
114114
}
115115

116+
/// Move the cursor before the first non whitespace character of the line
117+
pub fn move_to_line_non_blank_start(&mut self) {
118+
let line_start = self.lines[..self.insertion_point]
119+
.rfind('\n')
120+
.map_or(0, |offset| offset + 1);
121+
// str is guaranteed to be utf8, thus \n is safe to assume 1 byte long
122+
123+
self.insertion_point = self.lines[line_start..]
124+
.find(|c: char| !c.is_whitespace() || c == '\n')
125+
.map(|offset| line_start + offset)
126+
.unwrap_or(self.lines.len());
127+
}
128+
116129
/// Move cursor position to the end of the line
117130
///
118131
/// Insertion will append to the line.

src/edit_mode/vi/command.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ impl Command {
344344
Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))])
345345
}
346346
Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]),
347+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
348+
EditCommand::CutFromLineNonBlankStart,
349+
)]),
347350
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
348351
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
349352
Motion::Up => None,
@@ -411,6 +414,9 @@ impl Command {
411414
Motion::Start => {
412415
Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)])
413416
}
417+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
418+
EditCommand::CutFromLineNonBlankStart,
419+
)]),
414420
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
415421
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
416422
Motion::Up => None,
@@ -473,6 +479,9 @@ impl Command {
473479
Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))])
474480
}
475481
Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]),
482+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
483+
EditCommand::CopyFromLineNonBlankStart,
484+
)]),
476485
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]),
477486
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]),
478487
Motion::Up => None,

src/edit_mode/vi/motion.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ where
5252
let _ = input.next();
5353
ParseResult::Valid(Motion::NextBigWordEnd)
5454
}
55-
Some('0' | '^') => {
55+
Some('0') => {
5656
let _ = input.next();
5757
ParseResult::Valid(Motion::Start)
5858
}
59+
Some('^') => {
60+
let _ = input.next();
61+
ParseResult::Valid(Motion::NonBlankStart)
62+
}
5963
Some('$') => {
6064
let _ = input.next();
6165
ParseResult::Valid(Motion::End)
@@ -146,6 +150,7 @@ pub enum Motion {
146150
PreviousBigWord,
147151
Line,
148152
Start,
153+
NonBlankStart,
149154
End,
150155
FirstLine,
151156
LastLine,
@@ -208,6 +213,11 @@ impl Motion {
208213
Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart {
209214
select: select_mode,
210215
})],
216+
Motion::NonBlankStart => {
217+
vec![ReedlineOption::Edit(EditCommand::MoveToLineNonBlankStart {
218+
select: select_mode,
219+
})]
220+
}
211221
Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd {
212222
select: select_mode,
213223
})],

src/edit_mode/vi/parser.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ mod tests {
541541
// #[case(&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))]
542542
#[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight])]))]
543543
#[case(&['d', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))]
544-
#[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))]
544+
#[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart])]))]
545545
#[case(&['d', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]))]
546546
#[case(&['d', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))]
547547
#[case(&['d', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')])]))]
@@ -559,7 +559,7 @@ mod tests {
559559
]))]
560560
#[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))]
561561
#[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
562-
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
562+
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), ReedlineEvent::Repaint]))]
563563
#[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))]
564564
#[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))]
565565
#[case(&['c', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), ReedlineEvent::Repaint]))]

src/enums.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ pub enum EditCommand {
7272
select: bool,
7373
},
7474

75+
/// Move to the start of the current line skipping any whitespace
76+
MoveToLineNonBlankStart {
77+
/// Select the text between the current cursor position and destination
78+
select: bool,
79+
},
80+
7581
/// Move to the end of the buffer
7682
MoveToEnd {
7783
/// Select the text between the current cursor position and destination
@@ -197,6 +203,9 @@ pub enum EditCommand {
197203
/// Cut from the start of the current line to the insertion point
198204
CutFromLineStart,
199205

206+
/// Cut from the first non whitespace character of the current line to the insertion point
207+
CutFromLineNonBlankStart,
208+
200209
/// Cut from the insertion point to the end of the buffer
201210
CutToEnd,
202211

@@ -317,6 +326,9 @@ pub enum EditCommand {
317326
/// Copy from the start of the current line to the insertion point
318327
CopyFromLineStart,
319328

329+
/// Copy from the first non whitespace character of the current line to the insertion point
330+
CopyFromLineNonBlankStart,
331+
320332
/// Copy from the insertion point to the end of the buffer
321333
CopyToEnd,
322334

@@ -423,6 +435,9 @@ impl Display for EditCommand {
423435
EditCommand::MoveToLineStart { .. } => {
424436
write!(f, "MoveToLineStart Optional[select: <bool>]")
425437
}
438+
EditCommand::MoveToLineNonBlankStart { .. } => {
439+
write!(f, "MoveToLineNonBlankStart Optional[select: <bool>]")
440+
}
426441
EditCommand::MoveToEnd { .. } => write!(f, "MoveToEnd Optional[select: <bool>]"),
427442
EditCommand::MoveToLineEnd { .. } => {
428443
write!(f, "MoveToLineEnd Optional[select: <bool>]")
@@ -473,6 +488,7 @@ impl Display for EditCommand {
473488
EditCommand::CutCurrentLine => write!(f, "CutCurrentLine"),
474489
EditCommand::CutFromStart => write!(f, "CutFromStart"),
475490
EditCommand::CutFromLineStart => write!(f, "CutFromLineStart"),
491+
EditCommand::CutFromLineNonBlankStart => write!(f, "CutFromLineNonBlankStart"),
476492
EditCommand::CutToEnd => write!(f, "CutToEnd"),
477493
EditCommand::CutToLineEnd => write!(f, "CutToLineEnd"),
478494
EditCommand::KillLine => write!(f, "KillLine"),
@@ -504,6 +520,7 @@ impl Display for EditCommand {
504520
EditCommand::Paste => write!(f, "Paste"),
505521
EditCommand::CopyFromStart => write!(f, "CopyFromStart"),
506522
EditCommand::CopyFromLineStart => write!(f, "CopyFromLineStart"),
523+
EditCommand::CopyFromLineNonBlankStart => write!(f, "CopyFromLineNonBlankStart"),
507524
EditCommand::CopyToEnd => write!(f, "CopyToEnd"),
508525
EditCommand::CopyToLineEnd => write!(f, "CopyToLineEnd"),
509526
EditCommand::CopyCurrentLine => write!(f, "CopyCurrentLine"),
@@ -546,6 +563,7 @@ impl EditCommand {
546563
| EditCommand::MoveToEnd { select, .. }
547564
| EditCommand::MoveToLineStart { select, .. }
548565
| EditCommand::MoveToLineEnd { select, .. }
566+
| EditCommand::MoveToLineNonBlankStart { select, .. }
549567
| EditCommand::MoveToPosition { select, .. }
550568
| EditCommand::MoveLeft { select, .. }
551569
| EditCommand::MoveRight { select, .. }
@@ -582,6 +600,7 @@ impl EditCommand {
582600
| EditCommand::CutCurrentLine
583601
| EditCommand::CutFromStart
584602
| EditCommand::CutFromLineStart
603+
| EditCommand::CutFromLineNonBlankStart
585604
| EditCommand::CutToLineEnd
586605
| EditCommand::KillLine
587606
| EditCommand::CutToEnd
@@ -622,6 +641,7 @@ impl EditCommand {
622641
EditCommand::CopyTextObject { .. } => EditType::NoOp,
623642
EditCommand::CopyFromStart
624643
| EditCommand::CopyFromLineStart
644+
| EditCommand::CopyFromLineNonBlankStart
625645
| EditCommand::CopyToEnd
626646
| EditCommand::CopyToLineEnd
627647
| EditCommand::CopyCurrentLine

0 commit comments

Comments
 (0)