Skip to content

Commit 40d8da1

Browse files
authored
vi-mode: add gg/G motions and fix ^ motion behavior (#953)
* feat(vi-mode): add `gg` and `G` motion There is a bit of divergence in behaviour of `G` motion of reedline and vim. In vim, `G` takes the cursor to the first character of the last line. In reedline, it tales to the very last character of the buffer instead. Also when using these motion with a command, e.g. `c`, `d` and `y`, there are some inconsistency in handling the new line character. Added test case for `dgg`, `dG`, `cgg` and `cG` command. * 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. * make dgg and dG not leave empty line * use if expressions instead of converting bool to usize * rename leave_blank_line to Value in the Display impl for EditCommand Although I am not sure if this is a good idea
1 parent 4c16687 commit 40d8da1

File tree

6 files changed

+249
-3
lines changed

6 files changed

+249
-3
lines changed

src/core_editor/editor.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::core_editor::get_system_clipboard;
44
use crate::enums::{EditType, TextObject, TextObjectScope, TextObjectType, UndoBehavior};
55
use crate::prompt::{PromptEditMode, PromptViMode};
66
use crate::{core_editor::get_local_clipboard, EditCommand};
7+
use std::cmp::{max, min};
78
use std::ops::{DerefMut, Range};
89

910
/// Stateful editor executing changes to the underlying [`LineBuffer`]
@@ -55,6 +56,9 @@ impl Editor {
5556
match command {
5657
EditCommand::MoveToStart { select } => self.move_to_start(*select),
5758
EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select),
59+
EditCommand::MoveToLineNonBlankStart { select } => {
60+
self.move_to_line_non_blank_start(*select)
61+
}
5862
EditCommand::MoveToEnd { select } => self.move_to_end(*select),
5963
EditCommand::MoveToLineEnd { select } => self.move_to_line_end(*select),
6064
EditCommand::MoveToPosition { position, select } => {
@@ -86,8 +90,15 @@ impl Editor {
8690
EditCommand::ClearToLineEnd => self.line_buffer.clear_to_line_end(),
8791
EditCommand::CutCurrentLine => self.cut_current_line(),
8892
EditCommand::CutFromStart => self.cut_from_start(),
93+
EditCommand::CutFromStartLinewise { leave_blank_line } => {
94+
self.cut_from_start_linewise(*leave_blank_line)
95+
}
8996
EditCommand::CutFromLineStart => self.cut_from_line_start(),
97+
EditCommand::CutFromLineNonBlankStart => self.cut_from_line_non_blank_start(),
9098
EditCommand::CutToEnd => self.cut_from_end(),
99+
EditCommand::CutToEndLinewise { leave_blank_line } => {
100+
self.cut_from_end_linewise(*leave_blank_line)
101+
}
91102
EditCommand::CutToLineEnd => self.cut_to_line_end(),
92103
EditCommand::KillLine => self.kill_line(),
93104
EditCommand::CutWordLeft => self.cut_word_left(),
@@ -127,8 +138,11 @@ impl Editor {
127138
EditCommand::CopySelection => self.copy_selection_to_cut_buffer(),
128139
EditCommand::Paste => self.paste_cut_buffer(),
129140
EditCommand::CopyFromStart => self.copy_from_start(),
141+
EditCommand::CopyFromStartLinewise => self.copy_from_start_linewise(),
130142
EditCommand::CopyFromLineStart => self.copy_from_line_start(),
143+
EditCommand::CopyFromLineNonBlankStart => self.copy_from_line_non_blank_start(),
131144
EditCommand::CopyToEnd => self.copy_from_end(),
145+
EditCommand::CopyToEndLinewise => self.copy_from_end_linewise(),
132146
EditCommand::CopyToLineEnd => self.copy_to_line_end(),
133147
EditCommand::CopyWordLeft => self.copy_word_left(),
134148
EditCommand::CopyBigWordLeft => self.copy_big_word_left(),
@@ -308,6 +322,11 @@ impl Editor {
308322
self.line_buffer.move_to_line_start();
309323
}
310324

325+
pub(crate) fn move_to_line_non_blank_start(&mut self, select: bool) {
326+
self.update_selection_anchor(select);
327+
self.line_buffer.move_to_line_non_blank_start();
328+
}
329+
311330
pub(crate) fn move_to_line_end(&mut self, select: bool) {
312331
self.update_selection_anchor(select);
313332
self.line_buffer.move_to_line_end();
@@ -357,6 +376,29 @@ impl Editor {
357376
}
358377
}
359378

379+
fn cut_from_start_linewise(&mut self, leave_blank_line: bool) {
380+
let insertion_offset = self.line_buffer.insertion_point();
381+
let end_offset = self.line_buffer.get_buffer()[insertion_offset..]
382+
.find('\n')
383+
.map_or(self.line_buffer.len(), |offset| {
384+
// When leave_blank_line is true, we do **not** add 1 to the offset
385+
// So there will remain an empty line after the operation
386+
if leave_blank_line {
387+
insertion_offset + offset
388+
} else {
389+
insertion_offset + offset + 1
390+
}
391+
});
392+
if end_offset > 0 {
393+
self.cut_buffer.set(
394+
&self.line_buffer.get_buffer()[..end_offset],
395+
ClipboardMode::Lines,
396+
);
397+
self.line_buffer.clear_range(..end_offset);
398+
self.line_buffer.move_to_start();
399+
}
400+
}
401+
360402
fn cut_from_line_start(&mut self) {
361403
let previous_offset = self.line_buffer.insertion_point();
362404
self.line_buffer.move_to_line_start();
@@ -368,6 +410,14 @@ impl Editor {
368410
}
369411
}
370412

413+
fn cut_from_line_non_blank_start(&mut self) {
414+
let cursor_pos = self.line_buffer.insertion_point();
415+
self.line_buffer.move_to_line_non_blank_start();
416+
let other_pos = self.line_buffer.insertion_point();
417+
let deletion_range = min(cursor_pos, other_pos)..max(cursor_pos, other_pos);
418+
self.cut_range(deletion_range);
419+
}
420+
371421
fn cut_from_end(&mut self) {
372422
let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
373423
if !cut_slice.is_empty() {
@@ -376,6 +426,27 @@ impl Editor {
376426
}
377427
}
378428

429+
fn cut_from_end_linewise(&mut self, leave_blank_line: bool) {
430+
let start_offset = self.line_buffer.get_buffer()[..self.line_buffer.insertion_point()]
431+
.rfind('\n')
432+
.map_or(0, |offset| {
433+
// When leave_blank_line is true, we add 1 to the offset
434+
// So the \n character is not truncated
435+
if leave_blank_line {
436+
offset + 1
437+
} else {
438+
offset
439+
}
440+
});
441+
442+
let cut_slice = &self.line_buffer.get_buffer()[start_offset..];
443+
if !cut_slice.is_empty() {
444+
self.cut_buffer.set(cut_slice, ClipboardMode::Lines);
445+
self.line_buffer.set_insertion_point(start_offset);
446+
self.line_buffer.clear_to_end();
447+
}
448+
}
449+
379450
fn cut_to_line_end(&mut self) {
380451
let cut_slice = &self.line_buffer.get_buffer()
381452
[self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end()];
@@ -876,6 +947,20 @@ impl Editor {
876947
}
877948
}
878949

950+
pub(crate) fn copy_from_start_linewise(&mut self) {
951+
let insertion_point = self.line_buffer.insertion_point();
952+
let end_offset = self.line_buffer.get_buffer()[insertion_point..]
953+
.find('\n')
954+
.map_or(self.line_buffer.len(), |offset| insertion_point + offset);
955+
if end_offset > 0 {
956+
self.cut_buffer.set(
957+
&self.line_buffer.get_buffer()[..end_offset],
958+
ClipboardMode::Lines,
959+
);
960+
}
961+
self.line_buffer.move_to_start();
962+
}
963+
879964
pub(crate) fn copy_from_line_start(&mut self) {
880965
let previous_offset = self.line_buffer.insertion_point();
881966
let start_offset = {
@@ -889,11 +974,29 @@ impl Editor {
889974
self.copy_range(copy_range);
890975
}
891976

977+
pub(crate) fn copy_from_line_non_blank_start(&mut self) {
978+
let cursor_pos = self.line_buffer.insertion_point();
979+
self.line_buffer.move_to_line_non_blank_start();
980+
let other_pos = self.line_buffer.insertion_point();
981+
self.line_buffer.set_insertion_point(cursor_pos);
982+
let copy_range = min(cursor_pos, other_pos)..max(cursor_pos, other_pos);
983+
self.copy_range(copy_range);
984+
}
985+
892986
pub(crate) fn copy_from_end(&mut self) {
893987
let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len();
894988
self.copy_range(copy_range);
895989
}
896990

991+
pub(crate) fn copy_from_end_linewise(&mut self) {
992+
self.line_buffer.move_to_line_start();
993+
let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len();
994+
if copy_range.start < copy_range.end {
995+
let slice = &self.line_buffer.get_buffer()[copy_range];
996+
self.cut_buffer.set(slice, ClipboardMode::Lines);
997+
}
998+
}
999+
8971000
pub(crate) fn copy_to_line_end(&mut self) {
8981001
let copy_range =
8991002
self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end();

src/core_editor/line_buffer.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ 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_or(self.lines.len(), |offset| line_start + offset);
126+
}
127+
116128
/// Move cursor position to the end of the line
117129
///
118130
/// Insertion will append to the line.

src/edit_mode/vi/command.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,23 @@ 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,
350353
Motion::Down => None,
354+
Motion::FirstLine => Some(vec![ReedlineOption::Edit(
355+
EditCommand::CutFromStartLinewise {
356+
leave_blank_line: false,
357+
},
358+
)]),
359+
Motion::LastLine => {
360+
Some(vec![ReedlineOption::Edit(EditCommand::CutToEndLinewise {
361+
leave_blank_line: false,
362+
})])
363+
}
351364
Motion::ReplayCharSearch => vi_state
352365
.last_char_search
353366
.as_ref()
@@ -399,10 +412,23 @@ impl Command {
399412
Motion::Start => {
400413
Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)])
401414
}
415+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
416+
EditCommand::CutFromLineNonBlankStart,
417+
)]),
402418
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
403419
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
404420
Motion::Up => None,
405421
Motion::Down => None,
422+
Motion::FirstLine => Some(vec![ReedlineOption::Edit(
423+
EditCommand::CutFromStartLinewise {
424+
leave_blank_line: true,
425+
},
426+
)]),
427+
Motion::LastLine => {
428+
Some(vec![ReedlineOption::Edit(EditCommand::CutToEndLinewise {
429+
leave_blank_line: true,
430+
})])
431+
}
406432
Motion::ReplayCharSearch => vi_state
407433
.last_char_search
408434
.as_ref()
@@ -453,10 +479,19 @@ impl Command {
453479
Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))])
454480
}
455481
Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]),
482+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
483+
EditCommand::CopyFromLineNonBlankStart,
484+
)]),
456485
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]),
457486
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]),
458487
Motion::Up => None,
459488
Motion::Down => None,
489+
Motion::FirstLine => Some(vec![ReedlineOption::Edit(
490+
EditCommand::CopyFromStartLinewise,
491+
)]),
492+
Motion::LastLine => {
493+
Some(vec![ReedlineOption::Edit(EditCommand::CopyToEndLinewise)])
494+
}
460495
Motion::ReplayCharSearch => vi_state
461496
.last_char_search
462497
.as_ref()

src/edit_mode/vi/motion.rs

Lines changed: 34 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)
@@ -108,6 +112,21 @@ where
108112
let _ = input.next();
109113
ParseResult::Valid(Motion::ReverseCharSearch)
110114
}
115+
Some('g') => {
116+
let _ = input.next();
117+
match input.peek() {
118+
Some('g') => {
119+
input.next();
120+
ParseResult::Valid(Motion::FirstLine)
121+
}
122+
Some(_) => ParseResult::Invalid,
123+
None => ParseResult::Incomplete,
124+
}
125+
}
126+
Some('G') => {
127+
let _ = input.next();
128+
ParseResult::Valid(Motion::LastLine)
129+
}
111130
ch if ch == command_char.as_ref().as_ref() && command_char.is_some() => {
112131
let _ = input.next();
113132
ParseResult::Valid(Motion::Line)
@@ -131,7 +150,10 @@ pub enum Motion {
131150
PreviousBigWord,
132151
Line,
133152
Start,
153+
NonBlankStart,
134154
End,
155+
FirstLine,
156+
LastLine,
135157
RightUntil(char),
136158
RightBefore(char),
137159
LeftUntil(char),
@@ -191,9 +213,20 @@ impl Motion {
191213
Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart {
192214
select: select_mode,
193215
})],
216+
Motion::NonBlankStart => {
217+
vec![ReedlineOption::Edit(EditCommand::MoveToLineNonBlankStart {
218+
select: select_mode,
219+
})]
220+
}
194221
Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd {
195222
select: select_mode,
196223
})],
224+
Motion::FirstLine => vec![ReedlineOption::Edit(EditCommand::MoveToStart {
225+
select: select_mode,
226+
})],
227+
Motion::LastLine => vec![ReedlineOption::Edit(EditCommand::MoveToEnd {
228+
select: select_mode,
229+
})],
197230
Motion::RightUntil(ch) => {
198231
vi_state.last_char_search = Some(ViCharSearch::ToRight(*ch));
199232
vec![ReedlineOption::Edit(EditCommand::MoveRightUntil {

src/edit_mode/vi/parser.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,20 +541,33 @@ 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')])]))]
548548
#[case(&['d', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))]
549549
#[case(&['d', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')])]))]
550+
#[case(&['d', 'g', 'g'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { leave_blank_line: false }])]))]
551+
#[case(&['d', 'G'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { leave_blank_line: false }])]))]
550552
#[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))]
551553
#[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
552-
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
554+
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), ReedlineEvent::Repaint]))]
553555
#[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))]
554556
#[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))]
555557
#[case(&['c', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), ReedlineEvent::Repaint]))]
556558
#[case(&['c', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))]
557559
#[case(&['c', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')]), ReedlineEvent::Repaint]))]
560+
#[case(&['c', 'g', 'g'], ReedlineEvent::Multiple(vec![
561+
ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { leave_blank_line: true }]),
562+
ReedlineEvent::Repaint,
563+
]))]
564+
#[case(&['c', 'G'], ReedlineEvent::Multiple(vec![
565+
ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { leave_blank_line: true }]),
566+
ReedlineEvent::Repaint,
567+
]))]
568+
#[case(&['y', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyFromLineNonBlankStart])]))]
569+
#[case(&['y', 'g', 'g'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyFromStartLinewise])]))]
570+
#[case(&['y', 'G'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CopyToEndLinewise])]))]
558571
fn test_reedline_move(#[case] input: &[char], #[case] expected: ReedlineEvent) {
559572
let mut vi = Vi::default();
560573
let res = vi_parse(input);

0 commit comments

Comments
 (0)