Skip to content
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

Improve Visual Mode Selection and Command Consistency #867

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
17 changes: 15 additions & 2 deletions src/core_editor/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,10 +564,19 @@ impl Editor {
/// The range is guaranteed to be ascending.
pub fn get_selection(&self) -> Option<(usize, usize)> {
self.selection_anchor.map(|selection_anchor| {
let buffer_len = self.line_buffer.len();
if self.insertion_point() > selection_anchor {
(selection_anchor, self.insertion_point())
(
selection_anchor,
self.line_buffer.grapheme_right_index().min(buffer_len),
)
} else {
(self.insertion_point(), selection_anchor)
(
self.insertion_point(),
self.line_buffer
.grapheme_right_index_from_pos(selection_anchor)
.min(buffer_len),
)
}
})
}
Expand Down Expand Up @@ -648,6 +657,10 @@ impl Editor {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut());
}

pub(crate) fn reset_selection(&mut self) {
self.selection_anchor = None;
}
}

fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) {
Expand Down
31 changes: 31 additions & 0 deletions src/core_editor/line_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ impl LineBuffer {
.unwrap_or(0)
}

/// Cursor position *behind* the next unicode grapheme to the right from the given position
pub fn grapheme_right_index_from_pos(&self, pos: usize) -> usize {
self.lines[pos..]
.grapheme_indices(true)
.nth(1)
.map(|(i, _)| pos + i)
.unwrap_or_else(|| self.lines.len())
}

/// Cursor position *behind* the next word to the right
pub fn word_right_index(&self) -> usize {
self.lines[self.insertion_point..]
Expand Down Expand Up @@ -1597,4 +1606,26 @@ mod test {

assert_eq!(index, expected);
}

#[rstest]
#[case("abc", 0, 1)] // Basic ASCII
#[case("abc", 1, 2)] // From middle position
#[case("abc", 2, 3)] // From last char
#[case("abc", 3, 3)] // From end of string
#[case("🦀rust", 0, 4)] // Unicode emoji
#[case("🦀rust", 4, 5)] // After emoji
#[case("é́", 0, 4)] // Combining characters
fn test_grapheme_right_index_from_pos(
#[case] input: &str,
#[case] position: usize,
#[case] expected: usize,
) {
let mut line = LineBuffer::new();
line.insert_str(input);
assert_eq!(
line.grapheme_right_index_from_pos(position),
expected,
"input: {input:?}, pos: {position}"
);
}
}
18 changes: 15 additions & 3 deletions src/edit_mode/vi/command.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption};
use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption, ViMode};
use crate::{EditCommand, ReedlineEvent, Vi};
use std::iter::Peekable;

Expand Down Expand Up @@ -166,11 +166,23 @@ impl Command {
select: false,
})],
Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)],
Self::DeleteChar => vec![ReedlineOption::Edit(EditCommand::CutChar)],
Self::DeleteChar => {
if vi_state.mode == ViMode::Visual {
vec![ReedlineOption::Edit(EditCommand::CutSelection)]
} else {
vec![ReedlineOption::Edit(EditCommand::CutChar)]
}
}
Self::ReplaceChar(c) => {
vec![ReedlineOption::Edit(EditCommand::ReplaceChar(*c))]
}
Self::SubstituteCharWithInsert => vec![ReedlineOption::Edit(EditCommand::CutChar)],
Self::SubstituteCharWithInsert => {
if vi_state.mode == ViMode::Visual {
vec![ReedlineOption::Edit(EditCommand::CutSelection)]
} else {
vec![ReedlineOption::Edit(EditCommand::CutChar)]
}
}
Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)],
Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)],
// Whenever a motion is required to finish the command we must be in visual mode
Expand Down
17 changes: 12 additions & 5 deletions src/edit_mode/vi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,10 @@ impl EditMode for Vi {
self.cache.clear();
ReedlineEvent::None
} else if res.is_complete(self.mode) {
if let Some(mode) = res.changes_mode() {
let event = res.to_reedline_event(self);
if let Some(mode) = res.changes_mode(self.mode) {
self.mode = mode;
}

let event = res.to_reedline_event(self);
self.cache.clear();
event
} else {
Expand Down Expand Up @@ -143,7 +142,11 @@ impl EditMode for Vi {
(_, KeyModifiers::NONE, KeyCode::Esc) => {
self.cache.clear();
self.mode = ViMode::Normal;
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
ReedlineEvent::Multiple(vec![
ReedlineEvent::ResetSelection,
ReedlineEvent::Esc,
ReedlineEvent::Repaint,
])
}
(_, KeyModifiers::NONE, KeyCode::Enter) => {
self.mode = ViMode::Insert;
Expand Down Expand Up @@ -192,7 +195,11 @@ mod test {

assert_eq!(
result,
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
ReedlineEvent::Multiple(vec![
ReedlineEvent::ResetSelection,
ReedlineEvent::Esc,
ReedlineEvent::Repaint
])
);
assert!(matches!(vi.mode, ViMode::Normal));
}
Expand Down
11 changes: 8 additions & 3 deletions src/edit_mode/vi/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl ParsedViSequence {
}
}

pub fn changes_mode(&self) -> Option<ViMode> {
pub fn changes_mode(&self, mode: ViMode) -> Option<ViMode> {
match (&self.command, &self.motion) {
(Some(Command::EnterViInsert), ParseResult::Incomplete)
| (Some(Command::EnterViAppend), ParseResult::Incomplete)
Expand All @@ -109,12 +109,17 @@ impl ParsedViSequence {
| (Some(Command::SubstituteCharWithInsert), ParseResult::Incomplete)
| (Some(Command::HistorySearch), ParseResult::Incomplete)
| (Some(Command::Change), ParseResult::Valid(_)) => Some(ViMode::Insert),
(Some(Command::ChangeInside(char)), ParseResult::Incomplete)
(Some(Command::Change), ParseResult::Incomplete) if mode == ViMode::Visual => {
Some(ViMode::Insert)
}
(Some(Command::Delete), ParseResult::Incomplete) if mode == ViMode::Visual => {
Some(ViMode::Normal)
}
(Some(Command::ChangeInside(char)), ParseResult::Valid(_))
if is_valid_change_inside_left(char) || is_valid_change_inside_right(char) =>
{
Some(ViMode::Insert)
}
(Some(Command::Delete), ParseResult::Incomplete) => Some(ViMode::Normal),
_ => None,
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@ impl Reedline {
self.input_mode = InputMode::Regular;
Ok(EventStatus::Handled)
}
ReedlineEvent::ResetSelection => {
self.editor.reset_selection();
Ok(EventStatus::Handled)
}
// TODO: Check if events should be handled
ReedlineEvent::Right
| ReedlineEvent::Left
Expand Down Expand Up @@ -1197,6 +1201,10 @@ impl Reedline {
Ok(EventStatus::Handled)
}
ReedlineEvent::OpenEditor => self.open_editor().map(|_| EventStatus::Handled),
ReedlineEvent::ResetSelection => {
self.editor.reset_selection();
Ok(EventStatus::Handled)
}
ReedlineEvent::Resize(width, height) => {
self.painter.handle_resize(width, height);
Ok(EventStatus::Handled)
Expand Down
4 changes: 4 additions & 0 deletions src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,9 @@ pub enum ReedlineEvent {

/// Open text editor
OpenEditor,

/// Reset the current text selection
ResetSelection,
}

impl Display for ReedlineEvent {
Expand Down Expand Up @@ -687,6 +690,7 @@ impl Display for ReedlineEvent {
ReedlineEvent::MenuPagePrevious => write!(f, "MenuPagePrevious"),
ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"),
ReedlineEvent::OpenEditor => write!(f, "OpenEditor"),
ReedlineEvent::ResetSelection => write!(f, "ResetSelection"),
}
}
}
Expand Down
Loading