Skip to content

Commit

Permalink
Feature: vi visual mode (#800)
Browse files Browse the repository at this point in the history
* Add vi visual mode

as a proof of concept

* Fix h, l in vi visual mode

* Extend vi command parsing for vi visual mode

Commands requiring motion in normal mode, don't
in visual mode.

* Add delete command to vi visual mode

* Refractor: generalized enters_insert_mode()

to allow switching from vi visual mode to
vi normal mode instead of just to vi insert mode.

* Add switch from vi visual mode to normal mode

after deleting selection.

* Dokumentation: Visual selection implemented

* Cleanup: `cargo fmt --all`

* Made clippy clean

* Made `cargo fmt --call` clean
  • Loading branch information
adaschma authored Jul 6, 2024
1 parent 979b910 commit 480059a
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 63 deletions.
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

0 comments on commit 480059a

Please sign in to comment.