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

Feature: vi visual mode #800

Merged
merged 10 commits into from
Jul 6, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,13 @@ Reedline has now all the basic features to become the primary line editor for [n
- Undo support.
- Clipboard integration
- Line completeness validation for seamless entry of multiline command sequences.
- Visual selection

### Areas for future improvements

- [ ] Support for Unicode beyond simple left-to-right scripts
- [ ] Easier keybinding configuration
- [ ] Support for more advanced vi commands
- [ ] Visual selection
- [ ] Smooth experience if completion or prompt content takes long to compute
- [ ] Support for a concurrent output stream from background tasks to be displayed, while the input prompt is active. ("Full duplex" mode)

Expand Down
5 changes: 3 additions & 2 deletions src/edit_mode/vi/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ impl Command {
Self::SubstituteCharWithInsert => vec![ReedlineOption::Edit(EditCommand::CutChar)],
Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)],
Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)],
// Mark a command as incomplete whenever a motion is required to finish the command
Self::Delete | Self::Change | Self::Incomplete => vec![ReedlineOption::Incomplete],
// Whenever a motion is required to finish the command we must be in visual mode
Self::Delete | Self::Change => vec![ReedlineOption::Edit(EditCommand::CutSelection)],
Self::Incomplete => vec![ReedlineOption::Incomplete],
Command::RepeatLastAction => match &vi_state.previous {
Some(event) => vec![ReedlineOption::Event(event.clone())],
None => vec![],
Expand Down
18 changes: 12 additions & 6 deletions src/edit_mode/vi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{
enum ViMode {
Normal,
Insert,
Visual,
}

/// This parses incoming input `Event`s like a Vi-Style editor
Expand Down Expand Up @@ -62,7 +63,12 @@ impl EditMode for Vi {
Event::Key(KeyEvent {
code, modifiers, ..
}) => match (self.mode, modifiers, code) {
(ViMode::Normal, modifier, KeyCode::Char(c)) => {
(ViMode::Normal, KeyModifiers::NONE, KeyCode::Char('v')) => {
self.cache.clear();
self.mode = ViMode::Visual;
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
}
(ViMode::Normal | ViMode::Visual, modifier, KeyCode::Char(c)) => {
let c = c.to_ascii_lowercase();

if let Some(event) = self
Expand All @@ -82,9 +88,9 @@ impl EditMode for Vi {
if !res.is_valid() {
self.cache.clear();
ReedlineEvent::None
} else if res.is_complete() {
if res.enters_insert_mode() {
self.mode = ViMode::Insert;
} else if res.is_complete(self.mode) {
if let Some(mode) = res.changes_mode() {
self.mode = mode;
}

let event = res.to_reedline_event(self);
Expand Down Expand Up @@ -143,7 +149,7 @@ impl EditMode for Vi {
self.mode = ViMode::Insert;
ReedlineEvent::Enter
}
(ViMode::Normal, _, _) => self
(ViMode::Normal | ViMode::Visual, _, _) => self
.normal_keybindings
.find_binding(modifiers, code)
.unwrap_or(ReedlineEvent::None),
Expand All @@ -165,7 +171,7 @@ impl EditMode for Vi {

fn edit_mode(&self) -> PromptEditMode {
match self.mode {
ViMode::Normal => PromptEditMode::Vi(PromptViMode::Normal),
ViMode::Normal | ViMode::Visual => PromptEditMode::Vi(PromptViMode::Normal),
ViMode::Insert => PromptEditMode::Vi(PromptViMode::Insert),
}
}
Expand Down
53 changes: 31 additions & 22 deletions src/edit_mode/vi/motion.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::iter::Peekable;

use crate::{EditCommand, ReedlineEvent, Vi};
use crate::{edit_mode::vi::ViMode, EditCommand, ReedlineEvent, Vi};

use super::parser::{ParseResult, ReedlineOption};

Expand Down Expand Up @@ -142,89 +142,98 @@ pub enum Motion {

impl Motion {
pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec<ReedlineOption> {
let select_mode = vi_state.mode == ViMode::Visual;
match self {
Motion::Left => vec![ReedlineOption::Event(ReedlineEvent::UntilFound(vec![
ReedlineEvent::MenuLeft,
ReedlineEvent::Left,
ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
select: select_mode,
}]),
]))],
Motion::Right => vec![ReedlineOption::Event(ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::MenuRight,
ReedlineEvent::Right,
ReedlineEvent::Edit(vec![EditCommand::MoveRight {
select: select_mode,
}]),
]))],
Motion::Up => vec![ReedlineOption::Event(ReedlineEvent::UntilFound(vec![
ReedlineEvent::MenuUp,
ReedlineEvent::Up,
// todo: add EditCommand::MoveLineUp
]))],
Motion::Down => vec![ReedlineOption::Event(ReedlineEvent::UntilFound(vec![
ReedlineEvent::MenuDown,
ReedlineEvent::Down,
// todo: add EditCommand::MoveLineDown
]))],
Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart {
select: false,
select: select_mode,
})],
Motion::NextBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart {
select: false,
select: select_mode,
})],
Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd {
select: false,
select: select_mode,
})],
Motion::NextBigWordEnd => {
vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightEnd {
select: false,
select: select_mode,
})]
}
Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft {
select: false,
select: select_mode,
})],
Motion::PreviousBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft {
select: false,
select: select_mode,
})],
Motion::Line => vec![], // Placeholder as unusable standalone motion
Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart {
select: false,
select: select_mode,
})],
Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd {
select: false,
select: select_mode,
})],
Motion::RightUntil(ch) => {
vi_state.last_char_search = Some(ViCharSearch::ToRight(*ch));
vec![ReedlineOption::Edit(EditCommand::MoveRightUntil {
c: *ch,
select: false,
select: select_mode,
})]
}
Motion::RightBefore(ch) => {
vi_state.last_char_search = Some(ViCharSearch::TillRight(*ch));
vec![ReedlineOption::Edit(EditCommand::MoveRightBefore {
c: *ch,
select: false,
select: select_mode,
})]
}
Motion::LeftUntil(ch) => {
vi_state.last_char_search = Some(ViCharSearch::ToLeft(*ch));
vec![ReedlineOption::Edit(EditCommand::MoveLeftUntil {
c: *ch,
select: false,
select: select_mode,
})]
}
Motion::LeftBefore(ch) => {
vi_state.last_char_search = Some(ViCharSearch::TillLeft(*ch));
vec![ReedlineOption::Edit(EditCommand::MoveLeftBefore {
c: *ch,
select: false,
select: select_mode,
})]
}
Motion::ReplayCharSearch => {
if let Some(char_search) = vi_state.last_char_search.as_ref() {
vec![ReedlineOption::Edit(char_search.to_move())]
vec![ReedlineOption::Edit(char_search.to_move(select_mode))]
} else {
vec![]
}
}
Motion::ReverseCharSearch => {
if let Some(char_search) = vi_state.last_char_search.as_ref() {
vec![ReedlineOption::Edit(char_search.reverse().to_move())]
vec![ReedlineOption::Edit(
char_search.reverse().to_move(select_mode),
)]
} else {
vec![]
}
Expand Down Expand Up @@ -257,23 +266,23 @@ impl ViCharSearch {
}
}

pub fn to_move(&self) -> EditCommand {
pub fn to_move(&self, select_mode: bool) -> EditCommand {
match self {
ViCharSearch::ToRight(c) => EditCommand::MoveRightUntil {
c: *c,
select: false,
select: select_mode,
},
ViCharSearch::ToLeft(c) => EditCommand::MoveLeftUntil {
c: *c,
select: false,
select: select_mode,
},
ViCharSearch::TillRight(c) => EditCommand::MoveRightBefore {
c: *c,
select: false,
select: select_mode,
},
ViCharSearch::TillLeft(c) => EditCommand::MoveLeftBefore {
c: *c,
select: false,
select: select_mode,
},
}
}
Expand Down
Loading
Loading