From bde962bbf3b8773b8b83f7455286b367e055af2a Mon Sep 17 00:00:00 2001 From: YizhePKU Date: Sat, 6 Jul 2024 04:47:44 +0800 Subject: [PATCH 1/4] Add PWD to the `Reedline` state (#796) * Add PWD as part of the state * Allows cwd to be set to None --- src/engine.rs | 24 ++++++++++++++++++++++++ src/hinter/cwd_aware.rs | 2 ++ src/hinter/default.rs | 1 + src/hinter/mod.rs | 1 + src/history/base.rs | 18 ++++-------------- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 6b323c1e..e4da6e6a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -142,6 +142,10 @@ pub struct Reedline { // Use ansi coloring or not use_ansi_coloring: bool, + // Current working directory as defined by the application. If set, it will + // override the actual working directory of the process. + cwd: Option, + // Engine Menus menus: Vec, @@ -224,6 +228,7 @@ impl Reedline { hide_hints: false, validator, use_ansi_coloring: true, + cwd: None, menus: Vec::new(), buffer_editor: None, cursor_shapes: None, @@ -360,6 +365,13 @@ impl Reedline { self } + /// Update current working directory. + #[must_use] + pub fn with_cwd(mut self, cwd: Option) -> Self { + self.cwd = cwd; + self + } + /// A builder that configures the highlighter for your instance of the Reedline engine /// # Example /// ```rust @@ -1557,6 +1569,12 @@ impl Reedline { .history .search(SearchQuery::last_with_prefix_and_cwd( parsed.prefix.unwrap().to_string(), + self.cwd.clone().unwrap_or_else(|| { + std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }), self.get_history_session_id(), )) .unwrap_or_else(|_| Vec::new()) @@ -1739,6 +1757,12 @@ impl Reedline { cursor_position_in_buffer, self.history.as_ref(), self.use_ansi_coloring, + &self.cwd.clone().unwrap_or_else(|| { + std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }), ) }) } else { diff --git a/src/hinter/cwd_aware.rs b/src/hinter/cwd_aware.rs index 67ab8697..63a70322 100644 --- a/src/hinter/cwd_aware.rs +++ b/src/hinter/cwd_aware.rs @@ -22,11 +22,13 @@ impl Hinter for CwdAwareHinter { #[allow(unused_variables)] pos: usize, history: &dyn History, use_ansi_coloring: bool, + cwd: &str, ) -> String { self.current_hint = if line.chars().count() >= self.min_chars { let with_cwd = history .search(SearchQuery::last_with_prefix_and_cwd( line.to_string(), + cwd.to_string(), history.session(), )) .or_else(|err| { diff --git a/src/hinter/default.rs b/src/hinter/default.rs index 08ae57e8..c9bea6ef 100644 --- a/src/hinter/default.rs +++ b/src/hinter/default.rs @@ -15,6 +15,7 @@ impl Hinter for DefaultHinter { #[allow(unused_variables)] pos: usize, history: &dyn History, use_ansi_coloring: bool, + _cwd: &str, ) -> String { self.current_hint = if line.chars().count() >= self.min_chars { history diff --git a/src/hinter/mod.rs b/src/hinter/mod.rs index cf6f4701..fcd29b67 100644 --- a/src/hinter/mod.rs +++ b/src/hinter/mod.rs @@ -40,6 +40,7 @@ pub trait Hinter: Send { pos: usize, history: &dyn History, use_ansi_coloring: bool, + cwd: &str, ) -> String; /// Return the current hint unformatted to perform the completion of the full hint diff --git a/src/history/base.rs b/src/history/base.rs index a7c56f6e..93f7f456 100644 --- a/src/history/base.rs +++ b/src/history/base.rs @@ -146,24 +146,14 @@ impl SearchQuery { )) } - /// Get the most recent entry starting with the `prefix` and `cwd` same as the current cwd + /// Get the most recent entry starting with the `prefix` and `cwd` pub fn last_with_prefix_and_cwd( prefix: String, + cwd: String, session: Option, ) -> SearchQuery { - let cwd = std::env::current_dir(); - if let Ok(cwd) = cwd { - SearchQuery::last_with_search(SearchFilter::from_text_search_cwd( - cwd.to_string_lossy().to_string(), - CommandLineSearch::Prefix(prefix), - session, - )) - } else { - SearchQuery::last_with_search(SearchFilter::from_text_search( - CommandLineSearch::Prefix(prefix), - session, - )) - } + let prefix = CommandLineSearch::Prefix(prefix); + SearchQuery::last_with_search(SearchFilter::from_text_search_cwd(cwd, prefix, session)) } /// Query to get all entries in the given [`SearchDirection`] From 295f16f367d79f1eb5dfb511ef26065c4e625573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20L=C3=A9cuyer?= <30299784+Jiogo18@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:23:31 +0200 Subject: [PATCH 2/4] Fix #793 using width() for column menu alignements with special characters (#794) --- src/menu/columnar_menu.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 6eac27ab..611b93e8 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -299,7 +299,7 @@ impl ColumnarMenu { use_ansi_coloring: bool, ) -> String { if use_ansi_coloring { - let match_len = self.working_details.shortest_base_string.len(); + let match_len = self.working_details.shortest_base_string.width(); // Split string so the match text can be styled let (match_str, remaining_str) = suggestion.value.split_at(match_len); @@ -408,7 +408,7 @@ impl ColumnarMenu { + self .default_details .col_padding - .saturating_sub(marker.len()), + .saturating_sub(marker.width()), ) } else { format!( @@ -417,7 +417,7 @@ impl ColumnarMenu { &suggestion.value, "", self.end_of_line(column), - empty = empty_space.saturating_sub(marker.len()), + empty = empty_space.saturating_sub(marker.width()), ) }; @@ -500,7 +500,7 @@ impl Menu for ColumnarMenu { self.working_details.shortest_base_string = base_ranges .iter() .map(|range| editor.get_buffer()[range.clone()].to_string()) - .min_by_key(|s| s.len()) + .min_by_key(|s| s.width()) .unwrap_or_default(); self.reset_position(); @@ -530,15 +530,15 @@ impl Menu for ColumnarMenu { self.working_details.col_width = painter.screen_width() as usize; self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { + if prev >= suggestion.value.width() { prev } else { - suggestion.value.len() + suggestion.value.width() } }); } else { let max_width = self.get_values().iter().fold(0, |acc, suggestion| { - let str_len = suggestion.value.len() + self.default_details.col_padding; + let str_len = suggestion.value.width() + self.default_details.col_padding; if str_len > acc { str_len } else { @@ -654,7 +654,7 @@ impl Menu for ColumnarMenu { // Correcting the enumerate index based on the number of skipped values let index = index + skip_values; let column = index as u16 % self.get_cols(); - let empty_space = self.get_width().saturating_sub(suggestion.value.len()); + let empty_space = self.get_width().saturating_sub(suggestion.value.width()); self.create_string(suggestion, index, column, empty_space, use_ansi_coloring) }) From 979b910a69b630f32020df0df01e1d539dcca087 Mon Sep 17 00:00:00 2001 From: Yash Thakur <45539777+ysthakur@users.noreply.github.com> Date: Sat, 6 Jul 2024 08:25:32 -0400 Subject: [PATCH 3/4] Make menus process events before updating working details (#799) * Make columnar menu update values after process events * Remove inaccurate comment from ide_menu Previously, it said that the working details were updated before processing events, which is not the case. The comment may have been copied from columnar_menu. * Make description_menu process events before updating values --- src/menu/columnar_menu.rs | 72 +++++++++++++------------- src/menu/description_menu.rs | 98 ++++++++++++++++++------------------ src/menu/ide_menu.rs | 1 - 3 files changed, 84 insertions(+), 87 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 611b93e8..03ca79ab 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -515,8 +515,42 @@ impl Menu for ColumnarMenu { painter: &Painter, ) { if let Some(event) = self.event.take() { - // The working value for the menu are updated first before executing any of the - // menu events + match event { + MenuEvent::Activate(updated) => { + self.active = true; + self.reset_position(); + + self.input = if self.settings.only_buffer_difference { + Some(editor.get_buffer().to_string()) + } else { + None + }; + + if !updated { + self.update_values(editor, completer); + } + } + MenuEvent::Deactivate => self.active = false, + MenuEvent::Edit(updated) => { + self.reset_position(); + + if !updated { + self.update_values(editor, completer); + } + } + MenuEvent::NextElement => self.move_next(), + MenuEvent::PreviousElement => self.move_previous(), + MenuEvent::MoveUp => self.move_up(), + MenuEvent::MoveDown => self.move_down(), + MenuEvent::MoveLeft => self.move_left(), + MenuEvent::MoveRight => self.move_right(), + MenuEvent::PreviousPage | MenuEvent::NextPage => { + // The columnar menu doest have the concept of pages, yet + } + } + + // The working value for the menu are updated only after executing the menu events, + // so they have the latest suggestions // // If there is at least one suggestion that contains a description, then the layout // is changed to one column to fit the description @@ -572,40 +606,6 @@ impl Menu for ColumnarMenu { self.working_details.columns = possible_cols; } } - - match event { - MenuEvent::Activate(updated) => { - self.active = true; - self.reset_position(); - - self.input = if self.settings.only_buffer_difference { - Some(editor.get_buffer().to_string()) - } else { - None - }; - - if !updated { - self.update_values(editor, completer); - } - } - MenuEvent::Deactivate => self.active = false, - MenuEvent::Edit(updated) => { - self.reset_position(); - - if !updated { - self.update_values(editor, completer); - } - } - MenuEvent::NextElement => self.move_next(), - MenuEvent::PreviousElement => self.move_previous(), - MenuEvent::MoveUp => self.move_up(), - MenuEvent::MoveDown => self.move_down(), - MenuEvent::MoveLeft => self.move_left(), - MenuEvent::MoveRight => self.move_right(), - MenuEvent::PreviousPage | MenuEvent::NextPage => { - // The columnar menu doest have the concept of pages, yet - } - } } } diff --git a/src/menu/description_menu.rs b/src/menu/description_menu.rs index 95197b7a..d9687206 100644 --- a/src/menu/description_menu.rs +++ b/src/menu/description_menu.rs @@ -462,56 +462,6 @@ impl Menu for DescriptionMenu { painter: &Painter, ) { if let Some(event) = self.event.take() { - // Updating all working parameters from the menu before executing any of the - // possible event - let max_width = self.get_values().iter().fold(0, |acc, suggestion| { - let str_len = suggestion.value.len() + self.default_details.col_padding; - if str_len > acc { - str_len - } else { - acc - } - }); - - // If no default width is found, then the total screen width is used to estimate - // the column width based on the default number of columns - let default_width = if let Some(col_width) = self.default_details.col_width { - col_width - } else { - let col_width = painter.screen_width() / self.default_details.columns; - col_width as usize - }; - - // Adjusting the working width of the column based the max line width found - // in the menu values - if max_width > default_width { - self.working_details.col_width = max_width; - } else { - self.working_details.col_width = default_width; - }; - - // The working columns is adjusted based on possible number of columns - // that could be fitted in the screen with the calculated column width - let possible_cols = painter.screen_width() / self.working_details.col_width as u16; - if possible_cols > self.default_details.columns { - self.working_details.columns = self.default_details.columns.max(1); - } else { - self.working_details.columns = possible_cols; - } - - // Updating the working rows to display the description - if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() { - self.working_details.description_rows = self.default_details.description_rows; - self.show_examples = true; - } else { - self.working_details.description_rows = painter - .remaining_lines() - .saturating_sub(self.default_details.selection_rows + 1) - as usize; - - self.show_examples = false; - } - match event { MenuEvent::Activate(_) => { self.reset_position(); @@ -578,6 +528,54 @@ impl Menu for DescriptionMenu { } MenuEvent::PreviousPage | MenuEvent::NextPage => {} } + + let max_width = self.get_values().iter().fold(0, |acc, suggestion| { + let str_len = suggestion.value.len() + self.default_details.col_padding; + if str_len > acc { + str_len + } else { + acc + } + }); + + // If no default width is found, then the total screen width is used to estimate + // the column width based on the default number of columns + let default_width = if let Some(col_width) = self.default_details.col_width { + col_width + } else { + let col_width = painter.screen_width() / self.default_details.columns; + col_width as usize + }; + + // Adjusting the working width of the column based the max line width found + // in the menu values + if max_width > default_width { + self.working_details.col_width = max_width; + } else { + self.working_details.col_width = default_width; + }; + + // The working columns is adjusted based on possible number of columns + // that could be fitted in the screen with the calculated column width + let possible_cols = painter.screen_width() / self.working_details.col_width as u16; + if possible_cols > self.default_details.columns { + self.working_details.columns = self.default_details.columns.max(1); + } else { + self.working_details.columns = possible_cols; + } + + // Updating the working rows to display the description + if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() { + self.working_details.description_rows = self.default_details.description_rows; + self.show_examples = true; + } else { + self.working_details.description_rows = painter + .remaining_lines() + .saturating_sub(self.default_details.selection_rows + 1) + as usize; + + self.show_examples = false; + } } } diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 5dc325f7..fac1feab 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -652,7 +652,6 @@ impl Menu for IdeMenu { painter: &Painter, ) { if let Some(event) = self.event.take() { - // The working value for the menu are updated first before executing any of the match event { MenuEvent::Activate(updated) => { self.active = true; From 480059a3f52cf919341cda88e8c544edd846bc73 Mon Sep 17 00:00:00 2001 From: Adam Schmalhofer Date: Sat, 6 Jul 2024 14:26:54 +0200 Subject: [PATCH 4/4] Feature: vi visual mode (#800) * 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 --- README.md | 2 +- src/edit_mode/vi/command.rs | 5 +- src/edit_mode/vi/mod.rs | 18 ++-- src/edit_mode/vi/motion.rs | 53 ++++++----- src/edit_mode/vi/parser.rs | 175 +++++++++++++++++++++++++++++------- 5 files changed, 190 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index b8110c4f..4b485e9b 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 145e3dd0..e7bf0426 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -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![], diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 4428c645..7c7601d1 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -19,6 +19,7 @@ use crate::{ enum ViMode { Normal, Insert, + Visual, } /// This parses incoming input `Event`s like a Vi-Style editor @@ -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 @@ -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); @@ -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), @@ -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), } } diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index 2095b100..a0e1ad3c 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -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}; @@ -142,89 +142,98 @@ pub enum Motion { impl Motion { pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec { + 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![] } @@ -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, }, } } diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index 777d7cba..d3d9289d 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -1,6 +1,6 @@ use super::command::{parse_command, Command}; use super::motion::{parse_motion, Motion}; -use crate::{EditCommand, ReedlineEvent, Vi}; +use crate::{edit_mode::vi::ViMode, EditCommand, ReedlineEvent, Vi}; use std::iter::Peekable; #[derive(Debug, Clone)] @@ -50,11 +50,16 @@ impl ParsedViSequence { !self.motion.is_invalid() } - pub fn is_complete(&self) -> bool { + pub fn is_complete(&self, mode: ViMode) -> bool { + assert!(mode == ViMode::Normal || mode == ViMode::Visual); match (&self.command, &self.motion) { (None, ParseResult::Valid(_)) => true, (Some(Command::Incomplete), _) => false, - (Some(cmd), ParseResult::Incomplete) if !cmd.requires_motion() => true, + (Some(cmd), ParseResult::Incomplete) + if !cmd.requires_motion() || mode == ViMode::Visual => + { + true + } (Some(_), ParseResult::Valid(_)) => true, (Some(cmd), ParseResult::Incomplete) if cmd.requires_motion() => false, _ => false, @@ -91,22 +96,20 @@ impl ParsedViSequence { } } - pub fn enters_insert_mode(&self) -> bool { - matches!( - (&self.command, &self.motion), + pub fn changes_mode(&self) -> Option { + match (&self.command, &self.motion) { (Some(Command::EnterViInsert), ParseResult::Incomplete) - | (Some(Command::EnterViAppend), ParseResult::Incomplete) - | (Some(Command::ChangeToLineEnd), ParseResult::Incomplete) - | (Some(Command::AppendToEnd), ParseResult::Incomplete) - | (Some(Command::PrependToStart), ParseResult::Incomplete) - | (Some(Command::RewriteCurrentLine), ParseResult::Incomplete) - | ( - Some(Command::SubstituteCharWithInsert), - ParseResult::Incomplete - ) - | (Some(Command::HistorySearch), ParseResult::Incomplete) - | (Some(Command::Change), ParseResult::Valid(_)) - ) + | (Some(Command::EnterViAppend), ParseResult::Incomplete) + | (Some(Command::ChangeToLineEnd), ParseResult::Incomplete) + | (Some(Command::AppendToEnd), ParseResult::Incomplete) + | (Some(Command::PrependToStart), ParseResult::Incomplete) + | (Some(Command::RewriteCurrentLine), ParseResult::Incomplete) + | (Some(Command::SubstituteCharWithInsert), ParseResult::Incomplete) + | (Some(Command::HistorySearch), ParseResult::Incomplete) + | (Some(Command::Change), ParseResult::Valid(_)) => Some(ViMode::Insert), + (Some(Command::Delete), ParseResult::Incomplete) => Some(ViMode::Normal), + _ => None, + } } pub fn to_reedline_event(&self, vi_state: &mut Vi) -> ReedlineEvent { @@ -188,6 +191,25 @@ mod tests { parse(&mut input.iter().peekable()) } + #[test] + fn test_delete_without_motion() { + let input = ['d']; + let output = vi_parse(&input); + + assert_eq!( + output, + ParsedViSequence { + multiplier: None, + command: Some(Command::Delete), + count: None, + motion: ParseResult::Incomplete, + } + ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(ViMode::Normal), false); + assert_eq!(output.is_complete(ViMode::Visual), true); + } + #[test] fn test_delete_word() { let input = ['d', 'w']; @@ -203,7 +225,29 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); + } + + #[test] + fn test_two_delete_without_motion() { + let input = ['2', 'd']; + let output = vi_parse(&input); + + assert_eq!( + output, + ParsedViSequence { + multiplier: Some(2), + command: Some(Command::Delete), + count: None, + motion: ParseResult::Incomplete, + } + ); + assert_eq!(output.is_valid(), true); + // in visual mode vim ignores the multiplier, + // so we can accept this as valid even there + assert_eq!(output.is_complete(ViMode::Normal), false); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -221,7 +265,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -239,7 +284,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -257,7 +303,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -275,7 +322,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -293,7 +341,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -329,7 +378,8 @@ mod tests { ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), false); + assert_eq!(output.is_complete(ViMode::Normal), false); + assert_eq!(output.is_complete(ViMode::Visual), false); } #[test] @@ -347,7 +397,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), false); + assert_eq!(output.is_complete(ViMode::Normal), false); + assert_eq!(output.is_complete(ViMode::Visual), false); } #[test] @@ -366,7 +417,8 @@ mod tests { ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -384,7 +436,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[test] @@ -402,7 +455,8 @@ mod tests { } ); assert_eq!(output.is_valid(), true); - assert_eq!(output.is_complete(), true); + assert_eq!(output.is_complete(ViMode::Normal), true); + assert_eq!(output.is_complete(ViMode::Visual), true); } #[rstest] @@ -425,16 +479,16 @@ mod tests { ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, ReedlineEvent::MenuRight, - ReedlineEvent::Right, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), ]),ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, ReedlineEvent::MenuRight, - ReedlineEvent::Right, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), ]) ]))] #[case(&['l'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, ReedlineEvent::MenuRight, - ReedlineEvent::Right, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:false}]), ])]))] #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart{select:false}])]))] #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd{select:false}])]))] @@ -463,4 +517,61 @@ mod tests { assert_eq!(output, expected); } + + #[rstest] + #[case(&['2', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::MenuUp, + ReedlineEvent::Up, + ]), ReedlineEvent::UntilFound(vec![ + ReedlineEvent::MenuUp, + ReedlineEvent::Up, + ])]))] + #[case(&['k'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::MenuUp, + ReedlineEvent::Up, + ])]))] + #[case(&['w'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:true}])]))] + #[case(&['W'], + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:true}])]))] + #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), + ]),ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), + ]) ]))] + #[case(&['l'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ + ReedlineEvent::HistoryHintComplete, + ReedlineEvent::MenuRight, + ReedlineEvent::Edit(vec![EditCommand::MoveRight{select:true}]), + ])]))] + #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart{select:true}])]))] + #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd{select:true}])]))] + #[case(&['i'], ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]))] + #[case(&['p'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter])]))] + #[case(&['2', 'p'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]), + ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter]) + ]))] + #[case(&['u'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Undo])]))] + #[case(&['2', 'u'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::Undo]), + ReedlineEvent::Edit(vec![EditCommand::Undo]) + ]))] + #[case(&['d'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::CutSelection])]))] + fn test_reedline_move_in_visual_mode(#[case] input: &[char], #[case] expected: ReedlineEvent) { + let mut vi = Vi { + mode: ViMode::Visual, + ..Default::default() + }; + let res = vi_parse(input); + let output = res.to_reedline_event(&mut vi); + + assert_eq!(output, expected); + } }