-
Notifications
You must be signed in to change notification settings - Fork 232
feat(vi): support deleting chars backward with X #1067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
77214ff
3da9506
68616b2
b472620
7d673eb
d0a6606
f6efb8d
76dbae5
9e7de5d
7c45794
951ca0d
bf46c0c
bce8cb4
2cbc43a
a9fc508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,6 +134,7 @@ impl Editor { | |
| EditCommand::Backspace => self.backspace(), | ||
| EditCommand::Delete => self.delete(), | ||
| EditCommand::CutChar => self.cut_char(), | ||
| EditCommand::CutCharLeft => self.cut_char_left(), | ||
| EditCommand::BackspaceWord => self.line_buffer.delete_word_left(), | ||
| EditCommand::DeleteWord => self.line_buffer.delete_word_right(), | ||
| EditCommand::Clear => self.line_buffer.clear(), | ||
|
|
@@ -184,7 +185,9 @@ impl Editor { | |
| self.move_left_until_char(*c, true, true, *select) | ||
| } | ||
| EditCommand::SelectAll => self.select_all(), | ||
| EditCommand::CutSelection => self.cut_selection_to_cut_buffer(), | ||
| EditCommand::CutSelection { granularity } => { | ||
| self.cut_selection_to_cut_buffer(*granularity) | ||
| } | ||
| EditCommand::CopySelection => self.copy_selection_to_cut_buffer(), | ||
| EditCommand::Paste => self.paste_cut_buffer(), | ||
| EditCommand::CopyFromStart => self.copy_from_start(), | ||
|
|
@@ -677,7 +680,7 @@ impl Editor { | |
|
|
||
| fn cut_char(&mut self) { | ||
| if self.line_buffer.selection_anchor().is_some() { | ||
| self.cut_selection_to_cut_buffer(); | ||
| self.cut_selection_to_cut_buffer(Granularity::CharWise); | ||
| } else { | ||
| let insertion_offset = self.line_buffer.insertion_point(); | ||
| let next_char = self.line_buffer.grapheme_right_index(); | ||
|
|
@@ -824,9 +827,10 @@ impl Editor { | |
| } | ||
| } | ||
|
|
||
| fn cut_selection_to_cut_buffer(&mut self) { | ||
| fn cut_selection_to_cut_buffer(&mut self, granularity: Granularity) { | ||
| if let Some((start, end)) = self.get_selection() { | ||
| self.cut_range(start..end); | ||
| let sel = Cursor::new(start, end); | ||
| self.operate(sel, OperatorVerb::Cut, granularity); | ||
| self.clear_selection(); | ||
| } | ||
| } | ||
|
|
@@ -891,6 +895,25 @@ impl Editor { | |
| } | ||
| } | ||
|
|
||
|
|
||
| /// Cut the grapheme left of the caret into the cut buffer (vi `X`). | ||
| /// | ||
| /// Intended for vi normal mode only, where there is no selection, since | ||
| /// visual `X` is dispatched as a linewise `CutSelection`. Bound directly | ||
| /// elsewhere, a selection could exist, so clear it first: `X` always means | ||
| /// "delete the grapheme before the caret," and clearing avoids leaving a | ||
| /// stale anchor pointing into the mutated buffer. The cut is clamped to | ||
| /// the current line so `X` never crosses a terminator (unconditional, | ||
| /// unlike the `cross_line_cursor`-gated clamp in `resolve_head`). | ||
| fn cut_char_left(&mut self) { | ||
| self.clear_selection(); | ||
| let cur_pos = self.line_buffer.insertion_point(); | ||
| let left_index = self.line_buffer.grapheme_left_index(); | ||
| if left_index < cur_pos && left_index >= self.line_buffer.current_line_range().start { | ||
| self.cut_range(left_index..cur_pos); | ||
| } | ||
| } | ||
|
|
||
| fn delete(&mut self) { | ||
| if self.line_buffer.selection_anchor().is_some() { | ||
| self.delete_selection(); | ||
|
|
@@ -1500,7 +1523,9 @@ mod test { | |
| } | ||
| // head on 'l' (byte 2); Vi-normal selection is inclusive → covers [0,3) | ||
| assert_eq!(editor.get_selection(), Some((0, 3))); | ||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
| assert_eq!(editor.get_buffer(), "lo"); | ||
| assert_eq!(editor.cut_buffer.get().0, "hel"); | ||
| } | ||
|
|
@@ -1517,7 +1542,9 @@ mod test { | |
| } | ||
| // head on 'é' (byte 3); inclusive end extends over both bytes of é → 5 | ||
| assert_eq!(editor.get_selection(), Some((0, 5))); | ||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
| assert_eq!(editor.get_buffer(), "x"); | ||
| assert_eq!(editor.cut_buffer.get().0, "café"); | ||
| } | ||
|
|
@@ -2045,6 +2072,98 @@ mod test { | |
| assert_eq!(editor.get_buffer(), "This is a \r\n test"); | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a positive test for cut_char_left?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also If you want a defensive test for cut char left with a selection present.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized you commented. I'll have a look at the comments and follow-up soon!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great thanks. Now that the release is out we can try to merge it soon and then bump nushells reedline so we can test it.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| #[test] | ||
| fn test_cut_char_left_cuts_char_and_puts_in_buffer() { | ||
| let mut editor = editor_with("hello"); | ||
| editor.line_buffer.set_insertion_point(3); | ||
| editor.run_edit_command(&EditCommand::CutCharLeft); | ||
| assert_eq!(editor.get_buffer(), "helo"); | ||
| assert_eq!(editor.cut_buffer.get().0, "l"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_char_left_at_beginning_of_line() { | ||
| let starting_line = "This is a single line test"; | ||
| let mut editor = editor_with(starting_line); | ||
| editor.line_buffer.set_insertion_point(0); | ||
| editor.run_edit_command(&EditCommand::CutCharLeft); | ||
| assert_eq!(editor.get_buffer(), starting_line); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_char_left_at_beginning_of_2nd_line() { | ||
| let starting_line = "This is a \r\nmulti-line test"; | ||
| let mut editor = editor_with(starting_line); | ||
| editor.line_buffer.set_insertion_point(12); | ||
| editor.run_edit_command(&EditCommand::CutCharLeft); | ||
| assert_eq!(editor.get_buffer(), starting_line); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_char_left_clears_stale_selection() { | ||
| let mut editor = editor_with("hello"); | ||
| editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal)); | ||
| editor.line_buffer.set_insertion_point(2); | ||
| editor.update_selection_anchor(true); | ||
| editor.run_edit_command(&EditCommand::MoveRight { select: true }); | ||
| // stale selection present | ||
| editor.run_edit_command(&EditCommand::CutCharLeft); | ||
| assert_eq!(editor.get_buffer(), "helo"); | ||
| assert_eq!(editor.cut_buffer.get().0, "l"); | ||
| assert!(editor.get_selection().is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_selection_linewise_single_line() { | ||
| let mut editor = editor_with("hello world"); | ||
| editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal)); | ||
| editor.line_buffer.set_insertion_point(2); | ||
| editor.update_selection_anchor(true); | ||
| // Select "llo" (positions 2..5 inclusive in normal mode) | ||
| for _ in 0..2 { | ||
| editor.run_edit_command(&EditCommand::MoveRight { select: true }); | ||
| } | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::LineWise, | ||
| }); | ||
| // X in visual mode should cut the entire line | ||
| assert_eq!(editor.get_buffer(), ""); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_selection_linewise_multi_line() { | ||
| let mut editor = editor_with("first\nsecond\nthird"); | ||
| editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal)); | ||
| // Place cursor in "second", select a portion | ||
| editor.line_buffer.set_insertion_point(8); // 's' of "second" | ||
| editor.update_selection_anchor(true); | ||
| for _ in 0..2 { | ||
| editor.run_edit_command(&EditCommand::MoveRight { select: true }); | ||
| } | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::LineWise, | ||
| }); | ||
| // X in visual mode should cut the entire line(s) covered by the selection | ||
| assert_eq!(editor.get_buffer(), "first\nthird"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cut_selection_linewise_spanning_two_lines() { | ||
| let mut editor = editor_with("first\nsecond\nthird"); | ||
| editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal)); | ||
| // Select from end of "first" to beginning of "second" | ||
| editor.line_buffer.set_insertion_point(3); // in "first" | ||
| editor.update_selection_anchor(true); | ||
| for _ in 0..6 { | ||
| editor.run_edit_command(&EditCommand::MoveRight { select: true }); | ||
| } | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::LineWise, | ||
| }); | ||
| // Should cut both "first\n" and "second\n" | ||
| assert_eq!(editor.get_buffer(), "third"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_undo_delete_with_newline() { | ||
| let mut editor = editor_with("This \n is a test"); | ||
|
|
@@ -2151,7 +2270,9 @@ mod test { | |
| // Should select "his " (from position 1 to 5, inclusive of char at position 4) | ||
| assert_eq!(editor.get_selection(), Some((1, 5))); | ||
|
|
||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
|
|
||
| // After cutting, should have "Tis some test content" (removed "his ") | ||
| assert_eq!(editor.get_buffer(), "Tis some test content"); | ||
|
|
@@ -2178,7 +2299,9 @@ mod test { | |
| assert_eq!(editor.get_selection(), Some((0, 5))); // should include character at position 4 | ||
|
|
||
| // Now simulate pressing 'c' - this should cut the selection | ||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
|
|
||
| // Should have " world" left (removed "hello") | ||
| assert_eq!(editor.get_buffer(), " world"); | ||
|
|
@@ -2204,7 +2327,9 @@ mod test { | |
| assert_eq!(editor.get_selection(), Some((0, 5))); // inclusive selection | ||
|
|
||
| // Now simulate pressing 'c' - this should cut the selection | ||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
|
|
||
| // Should have " world" left (removed "hello" including the 'o') | ||
| assert_eq!(editor.get_buffer(), " world"); | ||
|
|
@@ -2435,7 +2560,9 @@ mod test { | |
| assert_eq!(editor.line_buffer().selection_anchor(), Some(0)); | ||
| assert_eq!(editor.get_selection(), Some((0, 5))); // inclusive selection | ||
|
|
||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
|
|
||
| assert_eq!(editor.get_buffer(), " world"); | ||
| assert_eq!(editor.insertion_point(), 0); | ||
|
|
@@ -2479,7 +2606,9 @@ mod test { | |
|
|
||
| // Simulate vi 'c' command: mode switches to insert then cuts selection | ||
| editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert)); | ||
| editor.run_edit_command(&EditCommand::CutSelection); | ||
| editor.run_edit_command(&EditCommand::CutSelection { | ||
| granularity: Granularity::CharWise, | ||
| }); | ||
|
|
||
| assert_eq!(editor.get_buffer(), " world"); | ||
| assert_eq!(editor.insertion_point(), 0); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.