Skip to content

Commit 72bcd81

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 697d689 commit 72bcd81

File tree

6 files changed

+92
-3
lines changed

6 files changed

+92
-3
lines changed

src/core_editor/editor.rs

Lines changed: 37 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, UndoBehavior};
55
use crate::{core_editor::get_local_clipboard, EditCommand};
6+
use std::cmp::{max, min};
67
use std::ops::DerefMut;
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(),
@@ -287,6 +293,11 @@ impl Editor {
287293
self.line_buffer.move_to_line_start();
288294
}
289295

296+
pub(crate) fn move_to_line_non_blank_start(&mut self, select: bool) {
297+
self.update_selection_anchor(select);
298+
self.line_buffer.move_to_line_non_blank_start();
299+
}
300+
290301
pub(crate) fn move_to_line_end(&mut self, select: bool) {
291302
self.update_selection_anchor(select);
292303
self.line_buffer.move_to_line_end();
@@ -347,6 +358,20 @@ impl Editor {
347358
}
348359
}
349360

361+
fn cut_from_line_non_blank_start(&mut self) {
362+
let offset_a = self.line_buffer.insertion_point();
363+
self.line_buffer.move_to_line_non_blank_start();
364+
let offset_b = self.line_buffer.insertion_point();
365+
let deletion_range = min(offset_a, offset_b)..max(offset_a, offset_b);
366+
let cut_slice = &self.line_buffer.get_buffer()[deletion_range.clone()];
367+
if !cut_slice.is_empty() {
368+
self.cut_buffer.set(cut_slice, ClipboardMode::Normal);
369+
self.line_buffer.clear_range(deletion_range);
370+
self.line_buffer
371+
.set_insertion_point(min(offset_a, offset_b));
372+
}
373+
}
374+
350375
fn cut_from_end(&mut self) {
351376
let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
352377
if !cut_slice.is_empty() {
@@ -776,6 +801,18 @@ impl Editor {
776801
}
777802
}
778803

804+
pub(crate) fn copy_from_line_non_blank_start(&mut self) {
805+
let offset_a = self.line_buffer.insertion_point();
806+
self.line_buffer.move_to_line_non_blank_start();
807+
let offset_b = self.line_buffer.insertion_point();
808+
self.line_buffer.set_insertion_point(offset_a);
809+
let copy_range = min(offset_a, offset_b)..max(offset_a, offset_b);
810+
let copy_slice = &self.line_buffer.get_buffer()[copy_range];
811+
if !copy_slice.is_empty() {
812+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
813+
}
814+
}
815+
779816
pub(crate) fn copy_from_end(&mut self) {
780817
let copy_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
781818
if !copy_slice.is_empty() {

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
@@ -273,6 +273,9 @@ impl Command {
273273
Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))])
274274
}
275275
Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]),
276+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
277+
EditCommand::CutFromLineNonBlankStart,
278+
)]),
276279
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
277280
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
278281
Motion::Up => None,
@@ -340,6 +343,9 @@ impl Command {
340343
Motion::Start => {
341344
Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)])
342345
}
346+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
347+
EditCommand::CutFromLineNonBlankStart,
348+
)]),
343349
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
344350
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
345351
Motion::Up => None,
@@ -402,6 +408,9 @@ impl Command {
402408
Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))])
403409
}
404410
Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]),
411+
Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
412+
EditCommand::CopyFromLineNonBlankStart,
413+
)]),
405414
Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]),
406415
Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]),
407416
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
@@ -540,7 +540,7 @@ mod tests {
540540
// #[case(&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))]
541541
#[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight])]))]
542542
#[case(&['d', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))]
543-
#[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))]
543+
#[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart])]))]
544544
#[case(&['d', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]))]
545545
#[case(&['d', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))]
546546
#[case(&['d', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')])]))]
@@ -558,7 +558,7 @@ mod tests {
558558
]))]
559559
#[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))]
560560
#[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
561-
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))]
561+
#[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), ReedlineEvent::Repaint]))]
562562
#[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))]
563563
#[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))]
564564
#[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
@@ -32,6 +32,12 @@ pub enum EditCommand {
3232
select: bool,
3333
},
3434

35+
/// Move to the start of the current line skipping any whitespace
36+
MoveToLineNonBlankStart {
37+
/// Select the text between the current cursor position and destination
38+
select: bool,
39+
},
40+
3541
/// Move to the end of the buffer
3642
MoveToEnd {
3743
/// Select the text between the current cursor position and destination
@@ -157,6 +163,9 @@ pub enum EditCommand {
157163
/// Cut from the start of the current line to the insertion point
158164
CutFromLineStart,
159165

166+
/// Cut from the first non whitespace character of the current line to the insertion point
167+
CutFromLineNonBlankStart,
168+
160169
/// Cut from the insertion point to the end of the buffer
161170
CutToEnd,
162171

@@ -277,6 +286,9 @@ pub enum EditCommand {
277286
/// Copy from the start of the current line to the insertion point
278287
CopyFromLineStart,
279288

289+
/// Copy from the first non whitespace character of the current line to the insertion point
290+
CopyFromLineNonBlankStart,
291+
280292
/// Copy from the insertion point to the end of the buffer
281293
CopyToEnd,
282294

@@ -359,6 +371,9 @@ impl Display for EditCommand {
359371
EditCommand::MoveToLineStart { .. } => {
360372
write!(f, "MoveToLineStart Optional[select: <bool>]")
361373
}
374+
EditCommand::MoveToLineNonBlankStart { .. } => {
375+
write!(f, "MoveToLineNonBlankStart Optional[select: <bool>]")
376+
}
362377
EditCommand::MoveToEnd { .. } => write!(f, "MoveToEnd Optional[select: <bool>]"),
363378
EditCommand::MoveToLineEnd { .. } => {
364379
write!(f, "MoveToLineEnd Optional[select: <bool>]")
@@ -409,6 +424,7 @@ impl Display for EditCommand {
409424
EditCommand::CutCurrentLine => write!(f, "CutCurrentLine"),
410425
EditCommand::CutFromStart => write!(f, "CutFromStart"),
411426
EditCommand::CutFromLineStart => write!(f, "CutFromLineStart"),
427+
EditCommand::CutFromLineNonBlankStart => write!(f, "CutFromLineNonBlankStart"),
412428
EditCommand::CutToEnd => write!(f, "CutToEnd"),
413429
EditCommand::CutToLineEnd => write!(f, "CutToLineEnd"),
414430
EditCommand::KillLine => write!(f, "KillLine"),
@@ -440,6 +456,7 @@ impl Display for EditCommand {
440456
EditCommand::Paste => write!(f, "Paste"),
441457
EditCommand::CopyFromStart => write!(f, "CopyFromStart"),
442458
EditCommand::CopyFromLineStart => write!(f, "CopyFromLineStart"),
459+
EditCommand::CopyFromLineNonBlankStart => write!(f, "CopyFromLineNonBlankStart"),
443460
EditCommand::CopyToEnd => write!(f, "CopyToEnd"),
444461
EditCommand::CopyToLineEnd => write!(f, "CopyToLineEnd"),
445462
EditCommand::CopyCurrentLine => write!(f, "CopyCurrentLine"),
@@ -478,6 +495,7 @@ impl EditCommand {
478495
| EditCommand::MoveToEnd { select, .. }
479496
| EditCommand::MoveToLineStart { select, .. }
480497
| EditCommand::MoveToLineEnd { select, .. }
498+
| EditCommand::MoveToLineNonBlankStart { select, .. }
481499
| EditCommand::MoveToPosition { select, .. }
482500
| EditCommand::MoveLeft { select, .. }
483501
| EditCommand::MoveRight { select, .. }
@@ -514,6 +532,7 @@ impl EditCommand {
514532
| EditCommand::CutCurrentLine
515533
| EditCommand::CutFromStart
516534
| EditCommand::CutFromLineStart
535+
| EditCommand::CutFromLineNonBlankStart
517536
| EditCommand::CutToLineEnd
518537
| EditCommand::KillLine
519538
| EditCommand::CutToEnd
@@ -550,6 +569,7 @@ impl EditCommand {
550569
EditCommand::YankInside { .. } => EditType::EditText,
551570
EditCommand::CopyFromStart
552571
| EditCommand::CopyFromLineStart
572+
| EditCommand::CopyFromLineNonBlankStart
553573
| EditCommand::CopyToEnd
554574
| EditCommand::CopyToLineEnd
555575
| EditCommand::CopyCurrentLine

0 commit comments

Comments
 (0)