Skip to content

Commit 853cde5

Browse files
deephbzwho
andauthored
Improve Visual Mode Selection and Command Consistency (#867)
* fix Vi mode change: restore normal mode after cut operations in visual mode * fix Cut/Change/Delete under visual-mode: Align Vim standards * fix ESC behavior under visual mode: clear selection when pressing ESC * fix ESC behavior under visual mode: follow-up: Implement ReedlineEvent::ResetSelection * fix Vi mode change: follow-up: Change/Delete + Incomplete motion could yield mode change from visual to insert/normal * fix: Cut/Change/Delete under visual-mode (follow-up): selection range considers UTF8 --------- Co-authored-by: WHOWHOWHOWHOWHOWHOWHOWHOWHOWHO <who@users.noreply.github.com>
1 parent abb5c08 commit 853cde5

File tree

7 files changed

+93
-13
lines changed

7 files changed

+93
-13
lines changed

src/core_editor/editor.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,10 +564,19 @@ impl Editor {
564564
/// The range is guaranteed to be ascending.
565565
pub fn get_selection(&self) -> Option<(usize, usize)> {
566566
self.selection_anchor.map(|selection_anchor| {
567+
let buffer_len = self.line_buffer.len();
567568
if self.insertion_point() > selection_anchor {
568-
(selection_anchor, self.insertion_point())
569+
(
570+
selection_anchor,
571+
self.line_buffer.grapheme_right_index().min(buffer_len),
572+
)
569573
} else {
570-
(self.insertion_point(), selection_anchor)
574+
(
575+
self.insertion_point(),
576+
self.line_buffer
577+
.grapheme_right_index_from_pos(selection_anchor)
578+
.min(buffer_len),
579+
)
571580
}
572581
})
573582
}
@@ -648,6 +657,10 @@ impl Editor {
648657
self.delete_selection();
649658
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut());
650659
}
660+
661+
pub(crate) fn reset_selection(&mut self) {
662+
self.selection_anchor = None;
663+
}
651664
}
652665

653666
fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) {

src/core_editor/line_buffer.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ impl LineBuffer {
168168
.unwrap_or(0)
169169
}
170170

171+
/// Cursor position *behind* the next unicode grapheme to the right from the given position
172+
pub fn grapheme_right_index_from_pos(&self, pos: usize) -> usize {
173+
self.lines[pos..]
174+
.grapheme_indices(true)
175+
.nth(1)
176+
.map(|(i, _)| pos + i)
177+
.unwrap_or_else(|| self.lines.len())
178+
}
179+
171180
/// Cursor position *behind* the next word to the right
172181
pub fn word_right_index(&self) -> usize {
173182
self.lines[self.insertion_point..]
@@ -1597,4 +1606,26 @@ mod test {
15971606

15981607
assert_eq!(index, expected);
15991608
}
1609+
1610+
#[rstest]
1611+
#[case("abc", 0, 1)] // Basic ASCII
1612+
#[case("abc", 1, 2)] // From middle position
1613+
#[case("abc", 2, 3)] // From last char
1614+
#[case("abc", 3, 3)] // From end of string
1615+
#[case("🦀rust", 0, 4)] // Unicode emoji
1616+
#[case("🦀rust", 4, 5)] // After emoji
1617+
#[case("é́", 0, 4)] // Combining characters
1618+
fn test_grapheme_right_index_from_pos(
1619+
#[case] input: &str,
1620+
#[case] position: usize,
1621+
#[case] expected: usize,
1622+
) {
1623+
let mut line = LineBuffer::new();
1624+
line.insert_str(input);
1625+
assert_eq!(
1626+
line.grapheme_right_index_from_pos(position),
1627+
expected,
1628+
"input: {input:?}, pos: {position}"
1629+
);
1630+
}
16001631
}

src/edit_mode/vi/command.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption};
1+
use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption, ViMode};
22
use crate::{EditCommand, ReedlineEvent, Vi};
33
use std::iter::Peekable;
44

@@ -166,11 +166,23 @@ impl Command {
166166
select: false,
167167
})],
168168
Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)],
169-
Self::DeleteChar => vec![ReedlineOption::Edit(EditCommand::CutChar)],
169+
Self::DeleteChar => {
170+
if vi_state.mode == ViMode::Visual {
171+
vec![ReedlineOption::Edit(EditCommand::CutSelection)]
172+
} else {
173+
vec![ReedlineOption::Edit(EditCommand::CutChar)]
174+
}
175+
}
170176
Self::ReplaceChar(c) => {
171177
vec![ReedlineOption::Edit(EditCommand::ReplaceChar(*c))]
172178
}
173-
Self::SubstituteCharWithInsert => vec![ReedlineOption::Edit(EditCommand::CutChar)],
179+
Self::SubstituteCharWithInsert => {
180+
if vi_state.mode == ViMode::Visual {
181+
vec![ReedlineOption::Edit(EditCommand::CutSelection)]
182+
} else {
183+
vec![ReedlineOption::Edit(EditCommand::CutChar)]
184+
}
185+
}
174186
Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)],
175187
Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)],
176188
// Whenever a motion is required to finish the command we must be in visual mode

src/edit_mode/vi/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ impl EditMode for Vi {
8989
self.cache.clear();
9090
ReedlineEvent::None
9191
} else if res.is_complete(self.mode) {
92-
if let Some(mode) = res.changes_mode() {
92+
let event = res.to_reedline_event(self);
93+
if let Some(mode) = res.changes_mode(self.mode) {
9394
self.mode = mode;
9495
}
95-
96-
let event = res.to_reedline_event(self);
9796
self.cache.clear();
9897
event
9998
} else {
@@ -143,7 +142,11 @@ impl EditMode for Vi {
143142
(_, KeyModifiers::NONE, KeyCode::Esc) => {
144143
self.cache.clear();
145144
self.mode = ViMode::Normal;
146-
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
145+
ReedlineEvent::Multiple(vec![
146+
ReedlineEvent::ResetSelection,
147+
ReedlineEvent::Esc,
148+
ReedlineEvent::Repaint,
149+
])
147150
}
148151
(_, KeyModifiers::NONE, KeyCode::Enter) => {
149152
self.mode = ViMode::Insert;
@@ -192,7 +195,11 @@ mod test {
192195

193196
assert_eq!(
194197
result,
195-
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
198+
ReedlineEvent::Multiple(vec![
199+
ReedlineEvent::ResetSelection,
200+
ReedlineEvent::Esc,
201+
ReedlineEvent::Repaint
202+
])
196203
);
197204
assert!(matches!(vi.mode, ViMode::Normal));
198205
}

src/edit_mode/vi/parser.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl ParsedViSequence {
9898
}
9999
}
100100

101-
pub fn changes_mode(&self) -> Option<ViMode> {
101+
pub fn changes_mode(&self, mode: ViMode) -> Option<ViMode> {
102102
match (&self.command, &self.motion) {
103103
(Some(Command::EnterViInsert), ParseResult::Incomplete)
104104
| (Some(Command::EnterViAppend), ParseResult::Incomplete)
@@ -109,12 +109,17 @@ impl ParsedViSequence {
109109
| (Some(Command::SubstituteCharWithInsert), ParseResult::Incomplete)
110110
| (Some(Command::HistorySearch), ParseResult::Incomplete)
111111
| (Some(Command::Change), ParseResult::Valid(_)) => Some(ViMode::Insert),
112-
(Some(Command::ChangeInside(char)), ParseResult::Incomplete)
112+
(Some(Command::Change), ParseResult::Incomplete) if mode == ViMode::Visual => {
113+
Some(ViMode::Insert)
114+
}
115+
(Some(Command::Delete), ParseResult::Incomplete) if mode == ViMode::Visual => {
116+
Some(ViMode::Normal)
117+
}
118+
(Some(Command::ChangeInside(char)), ParseResult::Valid(_))
113119
if is_valid_change_inside_left(char) || is_valid_change_inside_right(char) =>
114120
{
115121
Some(ViMode::Insert)
116122
}
117-
(Some(Command::Delete), ParseResult::Incomplete) => Some(ViMode::Normal),
118123
_ => None,
119124
}
120125
}

src/engine.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,10 @@ impl Reedline {
909909
self.input_mode = InputMode::Regular;
910910
Ok(EventStatus::Handled)
911911
}
912+
ReedlineEvent::ResetSelection => {
913+
self.editor.reset_selection();
914+
Ok(EventStatus::Handled)
915+
}
912916
// TODO: Check if events should be handled
913917
ReedlineEvent::Right
914918
| ReedlineEvent::Left
@@ -1197,6 +1201,10 @@ impl Reedline {
11971201
Ok(EventStatus::Handled)
11981202
}
11991203
ReedlineEvent::OpenEditor => self.open_editor().map(|_| EventStatus::Handled),
1204+
ReedlineEvent::ResetSelection => {
1205+
self.editor.reset_selection();
1206+
Ok(EventStatus::Handled)
1207+
}
12001208
ReedlineEvent::Resize(width, height) => {
12011209
self.painter.handle_resize(width, height);
12021210
Ok(EventStatus::Handled)

src/enums.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,9 @@ pub enum ReedlineEvent {
644644

645645
/// Open text editor
646646
OpenEditor,
647+
648+
/// Reset the current text selection
649+
ResetSelection,
647650
}
648651

649652
impl Display for ReedlineEvent {
@@ -687,6 +690,7 @@ impl Display for ReedlineEvent {
687690
ReedlineEvent::MenuPagePrevious => write!(f, "MenuPagePrevious"),
688691
ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"),
689692
ReedlineEvent::OpenEditor => write!(f, "OpenEditor"),
693+
ReedlineEvent::ResetSelection => write!(f, "ResetSelection"),
690694
}
691695
}
692696
}

0 commit comments

Comments
 (0)