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

Vi mode indicators #369

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions examples/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use std::borrow::Cow::{self, Borrowed, Owned};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter, PromptState};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, InputMode, KeyPress};
use rustyline_derive::Helper;

#[derive(Helper)]
Expand All @@ -15,7 +15,6 @@ struct MyHelper {
highlighter: MatchingBracketHighlighter,
validator: MatchingBracketValidator,
hinter: HistoryHinter,
colored_prompt: String,
}

impl Completer for MyHelper {
Expand All @@ -41,10 +40,16 @@ impl Highlighter for MyHelper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
prompt_state: PromptState,
) -> Cow<'b, str> {
if default {
Borrowed(&self.colored_prompt)
if prompt_state.default {
let indicator = match prompt_state.mode {
Some(InputMode::Command) => ":",
Some(InputMode::Insert) => "+",
Some(InputMode::Replace) => "#",
_ => " ",
};
Owned(format!("\x1b[1;32m{}{}\x1b[0m", indicator, &prompt[1..]))
} else {
Borrowed(prompt)
}
Expand Down Expand Up @@ -83,14 +88,13 @@ fn main() -> rustyline::Result<()> {
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.edit_mode(EditMode::Vi)
.output_stream(OutputStreamType::Stdout)
.build();
let h = MyHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
colored_prompt: "".to_owned(),
validator: MatchingBracketValidator::new(),
};
let mut rl = Editor::with_config(config);
Expand All @@ -102,8 +106,7 @@ fn main() -> rustyline::Result<()> {
}
let mut count = 1;
loop {
let p = format!("{}> ", count);
rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
let p = format!(" {}> ", count);
let readline = rl.readline(&p);
match readline {
Ok(line) => {
Expand Down
20 changes: 19 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use std::default::Default;

/// User preferences
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be reverted.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 missed that.

pub struct Config {
/// Maximum number of entries in History.
max_history_size: usize, // history_max_entries
Expand Down Expand Up @@ -206,6 +206,24 @@ pub enum EditMode {
Vi,
}

/// Vi mode indicators printed before prompt.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViModeIndicators {
pub command: Option<String>,
pub insert: Option<String>,
pub replace: Option<String>,
}

impl Default for ViModeIndicators {
fn default() -> Self {
Self {
command: None,
insert: None,
replace: None,
}
}
}

/// Colorization mode
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
Expand Down
41 changes: 39 additions & 2 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ use std::rc::Rc;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;

use super::{Context, Helper, Result};
use super::{Context, EditMode, Helper, Result};
use crate::config::Config;
use crate::highlight::Highlighter;
use crate::history::Direction;
use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Invoke, Refresher};
use crate::keymap::{InputMode, InputState, Invoke, Refresher};
use crate::layout::{Layout, Position};
use crate::line_buffer::{LineBuffer, WordAction, MAX_LINE};
use crate::tty::{Renderer, Term, Terminal};
Expand All @@ -33,6 +34,7 @@ pub struct State<'out, 'prompt, H: Helper> {
pub ctx: Context<'out>, // Give access to history for `hinter`
pub hint: Option<String>, // last hint displayed
highlight_char: bool, // `true` if a char has been highlighted
input_mode: Option<InputMode>,
Copy link
Collaborator

@gwenn gwenn Apr 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may avoid to duplicate input_mode if we move InputState in State ?
I will give a try and do it in a dedicated PR.

}

enum Info<'m> {
Expand All @@ -47,6 +49,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
prompt: &'prompt str,
helper: Option<&'out H>,
ctx: Context<'out>,
config: &'prompt Config,
) -> State<'out, 'prompt, H> {
let prompt_size = out.calculate_position(prompt, Position::default());
State {
Expand All @@ -62,6 +65,10 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
ctx,
hint: None,
highlight_char: false,
input_mode: match &config.edit_mode() {
EditMode::Vi => Some(InputMode::Insert),
EditMode::Emacs => None,
},
}
}

Expand Down Expand Up @@ -164,6 +171,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
&self.layout,
&new_layout,
highlighter,
self.input_mode,
)?;
self.layout = new_layout;

Expand Down Expand Up @@ -229,6 +237,19 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
Ok(true)
}
}

//fn calculate_prompt_size(&mut self) -> Position {
// let prompt = self.prompt;
// self.calculate_custom_prompt_size(prompt)
//}

//fn calculate_custom_prompt_size(&mut self, prompt: &str) -> Position {
// let indicator_size = self.out.calculate_position(
// self.input_mode_indicator().unwrap_or(""),
// Position::default(),
// );
// self.out.calculate_position(prompt, indicator_size)
//}
}

impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> {
Expand Down Expand Up @@ -261,10 +282,25 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {

fn doing_insert(&mut self) {
self.changes.borrow_mut().begin();
if let Some(mode) = &mut self.input_mode {
*mode = InputMode::Insert;
self.refresh_line().unwrap();
}
}

fn doing_replace(&mut self) {
if let Some(mode) = &mut self.input_mode {
*mode = InputMode::Replace;
self.refresh_line().unwrap();
}
}

fn done_inserting(&mut self) {
self.changes.borrow_mut().end();
if let Some(mode) = &mut self.input_mode {
*mode = InputMode::Command;
self.refresh_line().unwrap();
}
}

fn last_insert(&self) -> Option<String> {
Expand Down Expand Up @@ -675,6 +711,7 @@ pub fn init_state<'out, H: Helper>(
ctx: Context::new(history),
hint: Some("hint".to_owned()),
highlight_char: false,
input_mode: None,
}
}

Expand Down
25 changes: 21 additions & 4 deletions src/highlight.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
//! Syntax highlighting

use crate::config::CompletionType;
use crate::keymap::InputMode;
use memchr::memchr;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::cell::Cell;

/// The current state of the prompt.
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct PromptState {
/// Is it the default prompt.
pub default: bool,
/// Current Vi mode.
pub mode: Option<InputMode>,
}

impl PromptState {
/// Construct a new prompt state.
pub fn new(default: bool, mode: Option<InputMode>) -> Self {
Self { default, mode }
}
}

/// Syntax highlighter with [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters).
/// Rustyline will try to handle escape sequence for ANSI color on windows
/// when not supported natively (windows <10).
Expand All @@ -26,9 +43,9 @@ pub trait Highlighter {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
prompt_state: PromptState,
) -> Cow<'b, str> {
let _ = default;
let _ = prompt_state;
Borrowed(prompt)
}
/// Takes the `hint` and
Expand Down Expand Up @@ -69,9 +86,9 @@ impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
prompt_state: PromptState,
) -> Cow<'b, str> {
(**self).highlight_prompt(prompt, default)
(**self).highlight_prompt(prompt, prompt_state)
}

fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Expand Down
2 changes: 1 addition & 1 deletion src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ mod tests {
#[test]
fn add() {
let config = Config::builder().history_ignore_space(true).build();
let mut history = History::with_config(config);
let mut history = History::with_config(config.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be reverted.

assert_eq!(config.max_history_size(), history.max_len);
assert!(history.add("line1"));
assert!(history.add("line2"));
Expand Down
16 changes: 13 additions & 3 deletions src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,9 @@ impl Movement {
}
}

#[derive(PartialEq)]
enum InputMode {
/// Vi input mode.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InputMode {
/// Vi Command/Alternate
Command,
/// Insert/Input mode
Expand Down Expand Up @@ -332,6 +333,8 @@ pub trait Refresher {
fn doing_insert(&mut self);
/// Vi only, switch to command mode.
fn done_inserting(&mut self);
/// Vi only, switch to replace mode.
fn doing_replace(&mut self);
/// Vi only, last text inserted.
fn last_insert(&self) -> Option<String>;
/// Returns `true` if the cursor is currently at the end of the line.
Expand Down Expand Up @@ -617,12 +620,16 @@ impl InputState {
KeyPress::Char('c') => {
self.input_mode = InputMode::Insert;
match self.vi_cmd_motion(rdr, wrt, key, n)? {
Some(mvt) => Cmd::Replace(mvt, None),
Some(mvt) => {
wrt.doing_insert();
Cmd::Replace(mvt, None)
}
None => Cmd::Unknown,
}
}
KeyPress::Char('C') => {
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Replace(Movement::EndOfLine, None)
}
KeyPress::Char('d') => match self.vi_cmd_motion(rdr, wrt, key, n)? {
Expand Down Expand Up @@ -675,16 +682,19 @@ impl InputState {
KeyPress::Char('R') => {
// vi-replace-mode (overwrite-mode)
self.input_mode = InputMode::Replace;
wrt.doing_replace();
Cmd::Replace(Movement::ForwardChar(0), None)
}
KeyPress::Char('s') => {
// vi-substitute-char:
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Replace(Movement::ForwardChar(n), None)
}
KeyPress::Char('S') => {
// vi-substitute-line:
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Replace(Movement::WholeLine, None)
}
KeyPress::Char('u') => Cmd::Undo(n),
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use crate::edit::State;
use crate::highlight::Highlighter;
use crate::hint::Hinter;
use crate::history::{Direction, History};
pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Refresher};
pub use crate::keys::KeyPress;
use crate::kill_ring::{KillRing, Mode};
Expand Down Expand Up @@ -424,7 +424,7 @@ fn readline_edit<H: Helper>(

editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc<Mutex<KillRing>>
let ctx = Context::new(&editor.history);
let mut s = State::new(&mut stdout, prompt, helper, ctx);
let mut s = State::new(&mut stdout, prompt, helper, ctx, &editor.config);
let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings));

s.line.set_delete_listener(editor.kill_ring.clone());
Expand Down Expand Up @@ -784,7 +784,7 @@ impl<H: Helper> Editor<H> {
);
Self {
term,
history: History::with_config(config),
history: History::with_config(config.clone()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be reverted.

helper: None,
kill_ring: Arc::new(Mutex::new(KillRing::new(60))),
config,
Expand Down
13 changes: 12 additions & 1 deletion src/tty/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! This module implements and describes common TTY methods & traits
use crate::config::{BellStyle, ColorMode, Config, OutputStreamType};
use crate::highlight::Highlighter;
use crate::keymap::InputMode;
use crate::keys::KeyPress;
use crate::layout::{Layout, Position};
use crate::line_buffer::LineBuffer;
Expand Down Expand Up @@ -39,6 +40,7 @@ pub trait Renderer {
old_layout: &Layout,
new_layout: &Layout,
highlighter: Option<&dyn Highlighter>,
vi_mode: Option<InputMode>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can factorize code by passing a PromptState instead of Option<InputMode>.

) -> Result<()>;

/// Compute layout for rendering prompt + line + some info (either hint,
Expand Down Expand Up @@ -118,8 +120,17 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R {
old_layout: &Layout,
new_layout: &Layout,
highlighter: Option<&dyn Highlighter>,
vi_mode: Option<InputMode>,
) -> Result<()> {
(**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter)
(**self).refresh_line(
prompt,
line,
hint,
old_layout,
new_layout,
highlighter,
vi_mode,
)
}

fn calculate_position(&self, s: &str, orig: Position) -> Position {
Expand Down
Loading