| //! Readline for Rust |
| //! |
| //! This implementation is based on [Antirez's |
| //! Linenoise](https://github.com/antirez/linenoise) |
| //! |
| //! # Example |
| //! |
| //! Usage |
| //! |
| //! ``` |
| //! let mut rl = rustyline::Editor::new(); |
| //! let readline = rl.readline(">> "); |
| //! match readline { |
| //! Ok(line) => println!("Line: {:?}",line), |
| //! Err(_) => println!("No input"), |
| //! } |
| //! ``` |
| #![feature(io)] |
| #![feature(unicode)] |
| #![allow(unknown_lints)] |
| |
| #[cfg(windows)] |
| extern crate kernel32; |
| extern crate libc; |
| #[macro_use] |
| extern crate log; |
| #[cfg(unix)] |
| extern crate nix; |
| extern crate std_unicode; |
| extern crate unicode_segmentation; |
| extern crate unicode_width; |
| #[cfg(windows)] |
| extern crate winapi; |
| |
| pub mod completion; |
| mod consts; |
| pub mod error; |
| pub mod hint; |
| pub mod history; |
| mod keymap; |
| mod kill_ring; |
| pub mod line_buffer; |
| pub mod config; |
| mod undo; |
| |
| mod tty; |
| |
| use std::cell::RefCell; |
| use std::collections::HashMap; |
| use std::fmt; |
| use std::io::{self, Write}; |
| use std::path::Path; |
| use std::result; |
| use std::rc::Rc; |
| use unicode_segmentation::UnicodeSegmentation; |
| use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; |
| |
| use tty::{Position, RawMode, RawReader, Renderer, Term, Terminal}; |
| |
| use completion::{longest_common_prefix, Completer}; |
| use hint::Hinter; |
| use history::{Direction, History}; |
| use line_buffer::{LineBuffer, WordAction, MAX_LINE}; |
| pub use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; |
| use keymap::EditState; |
| use kill_ring::{KillRing, Mode}; |
| pub use config::{CompletionType, Config, EditMode, HistoryDuplicates}; |
| use undo::Changeset; |
| pub use consts::KeyPress; |
| |
| /// The error type for I/O and Linux Syscalls (Errno) |
| pub type Result<T> = result::Result<T, error::ReadlineError>; |
| |
| /// Represent the state during line editing. |
| /// Implement rendering. |
| struct State<'out, 'prompt> { |
| out: &'out mut Renderer, |
| prompt: &'prompt str, // Prompt to display |
| prompt_size: Position, // Prompt Unicode/visible width and height |
| line: LineBuffer, // Edited line buffer |
| cursor: Position, /* Cursor position (relative to the start of the prompt |
| * for `row`) */ |
| old_rows: usize, // Number of rows used so far (from start of prompt to end of input) |
| history_index: usize, // The history index we are currently editing |
| saved_line_for_history: LineBuffer, // Current edited line before history browsing |
| byte_buffer: [u8; 4], |
| edit_state: EditState, |
| changes: Rc<RefCell<Changeset>>, |
| hinter: Option<Rc<RefCell<Hinter>>>, |
| } |
| |
| impl<'out, 'prompt> State<'out, 'prompt> { |
| fn new( |
| out: &'out mut Renderer, |
| config: &Config, |
| prompt: &'prompt str, |
| history_index: usize, |
| custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>, |
| hinter: Option<Rc<RefCell<Hinter>>>, |
| ) -> State<'out, 'prompt> { |
| let capacity = MAX_LINE; |
| let prompt_size = out.calculate_position(prompt, Position::default()); |
| State { |
| out: out, |
| prompt: prompt, |
| prompt_size: prompt_size, |
| line: LineBuffer::with_capacity(capacity), |
| cursor: prompt_size, |
| old_rows: prompt_size.row, |
| history_index: history_index, |
| saved_line_for_history: LineBuffer::with_capacity(capacity), |
| byte_buffer: [0; 4], |
| edit_state: EditState::new(config, custom_bindings), |
| changes: Rc::new(RefCell::new(Changeset::new())), |
| hinter: hinter, |
| } |
| } |
| |
| fn next_cmd<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { |
| loop { |
| let rc = self.edit_state.next_cmd(rdr); |
| if rc.is_err() && self.out.sigwinch() { |
| self.out.update_size(); |
| try!(self.refresh_line()); |
| continue; |
| } |
| return rc; |
| } |
| } |
| |
| fn backup(&mut self) { |
| self.saved_line_for_history |
| .update(self.line.as_str(), self.line.pos()); |
| } |
| fn restore(&mut self) { |
| self.line.update( |
| self.saved_line_for_history.as_str(), |
| self.saved_line_for_history.pos(), |
| ); |
| } |
| |
| fn move_cursor(&mut self) -> Result<()> { |
| // calculate the desired position of the cursor |
| let cursor = self.out |
| .calculate_position(&self.line[..self.line.pos()], self.prompt_size); |
| if self.cursor == cursor { |
| return Ok(()); |
| } |
| try!(self.out.move_cursor(self.cursor, cursor)); |
| self.cursor = cursor; |
| Ok(()) |
| } |
| |
| /// Rewrite the currently edited line accordingly to the buffer content, |
| /// cursor position, and number of columns of the terminal. |
| fn refresh_line(&mut self) -> Result<()> { |
| let prompt_size = self.prompt_size; |
| let hint = self.hint(); |
| self.refresh(self.prompt, prompt_size, hint) |
| } |
| |
| fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { |
| let prompt_size = self.out.calculate_position(prompt, Position::default()); |
| let hint = self.hint(); |
| self.refresh(prompt, prompt_size, hint) |
| } |
| |
| fn refresh(&mut self, prompt: &str, prompt_size: Position, hint: Option<String>) -> Result<()> { |
| let (cursor, end_pos) = try!(self.out.refresh_line( |
| prompt, |
| prompt_size, |
| &self.line, |
| hint, |
| self.cursor.row, |
| self.old_rows, |
| )); |
| |
| self.cursor = cursor; |
| self.old_rows = end_pos.row; |
| Ok(()) |
| } |
| |
| fn hint(&self) -> Option<String> { |
| if let Some(ref hinter) = self.hinter { |
| hinter |
| .borrow_mut() |
| .hint(self.line.as_str(), self.line.pos()) |
| } else { |
| None |
| } |
| } |
| } |
| |
| impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.debug_struct("State") |
| .field("prompt", &self.prompt) |
| .field("prompt_size", &self.prompt_size) |
| .field("buf", &self.line) |
| .field("cursor", &self.cursor) |
| .field("cols", &self.out.get_columns()) |
| .field("old_rows", &self.old_rows) |
| .field("history_index", &self.history_index) |
| .field("saved_line_for_history", &self.saved_line_for_history) |
| .finish() |
| } |
| } |
| |
| /// Insert the character `ch` at cursor current position. |
| fn edit_insert(s: &mut State, ch: char, n: RepeatCount) -> Result<()> { |
| if let Some(push) = s.line.insert(ch, n) { |
| if push { |
| let prompt_size = s.prompt_size; |
| let hint = s.hint(); |
| if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.out.get_columns() && |
| hint.is_none() |
| { |
| // Avoid a full update of the line in the trivial case. |
| let cursor = s.out |
| .calculate_position(&s.line[..s.line.pos()], s.prompt_size); |
| s.cursor = cursor; |
| let bits = ch.encode_utf8(&mut s.byte_buffer); |
| let bits = bits.as_bytes(); |
| s.out.write_and_flush(bits) |
| } else { |
| s.refresh(s.prompt, prompt_size, hint) |
| } |
| } else { |
| s.refresh_line() |
| } |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Replace a single (or n) character(s) under the cursor (Vi mode) |
| fn edit_replace_char(s: &mut State, ch: char, n: RepeatCount) -> Result<()> { |
| s.changes.borrow_mut().begin(); |
| let succeed = if let Some(chars) = s.line.delete(n) { |
| let count = chars.graphemes(true).count(); |
| s.line.insert(ch, count); |
| s.line.move_backward(1); |
| true |
| } else { |
| false |
| }; |
| s.changes.borrow_mut().end(); |
| if succeed { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Overwrite the character under the cursor (Vi mode) |
| fn edit_overwrite_char(s: &mut State, ch: char) -> Result<()> { |
| if let Some(end) = s.line.next_pos(1) { |
| { |
| let text = ch.encode_utf8(&mut s.byte_buffer); |
| let start = s.line.pos(); |
| s.line.replace(start..end, text); |
| } |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // Yank/paste `text` at current position. |
| fn edit_yank(s: &mut State, text: &str, anchor: Anchor, n: RepeatCount) -> Result<()> { |
| if let Anchor::After = anchor { |
| s.line.move_forward(1); |
| } |
| if s.line.yank(text, n).is_some() { |
| if !s.edit_state.is_emacs_mode() { |
| s.line.move_backward(1); |
| } |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // Delete previously yanked text and yank/paste `text` at current position. |
| fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> { |
| s.changes.borrow_mut().begin(); |
| s.line.yank_pop(yank_size, text); |
| let result = edit_yank(s, text, Anchor::Before, 1); |
| s.changes.borrow_mut().end(); |
| result |
| } |
| |
| /// Move cursor on the left. |
| fn edit_move_backward(s: &mut State, n: RepeatCount) -> Result<()> { |
| if s.line.move_backward(n) { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor on the right. |
| fn edit_move_forward(s: &mut State, n: RepeatCount) -> Result<()> { |
| if s.line.move_forward(n) { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor to the start of the line. |
| fn edit_move_home(s: &mut State) -> Result<()> { |
| if s.line.move_home() { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor to the end of the line. |
| fn edit_move_end(s: &mut State) -> Result<()> { |
| if s.line.move_end() { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Delete the character at the right of the cursor without altering the cursor |
| /// position. Basically this is what happens with the "Delete" keyboard key. |
| fn edit_delete(s: &mut State, n: RepeatCount) -> Result<()> { |
| if s.line.delete(n).is_some() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Backspace implementation. |
| fn edit_backspace(s: &mut State, n: RepeatCount) -> Result<()> { |
| if s.line.backspace(n) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Kill the text from point to the end of the line. |
| fn edit_kill_line(s: &mut State) -> Result<()> { |
| if s.line.kill_line() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Kill backward from point to the beginning of the line. |
| fn edit_discard_line(s: &mut State) -> Result<()> { |
| if s.line.discard_line() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Exchange the char before cursor with the character at cursor. |
| fn edit_transpose_chars(s: &mut State) -> Result<()> { |
| s.changes.borrow_mut().begin(); |
| let succeed = s.line.transpose_chars(); |
| s.changes.borrow_mut().end(); |
| if succeed { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<()> { |
| if s.line.move_to_prev_word(word_def, n) { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Delete the previous word, maintaining the cursor at the start of the |
| /// current word. |
| fn edit_delete_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<()> { |
| if s.line.delete_prev_word(word_def, n) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: RepeatCount) -> Result<()> { |
| if s.line.move_to_next_word(at, word_def, n) { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_move_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<()> { |
| if s.line.move_to(cs, n) { |
| s.move_cursor() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Kill from the cursor to the end of the current word, or, if between words, |
| /// to the end of the next word. |
| fn edit_delete_word(s: &mut State, at: At, word_def: Word, n: RepeatCount) -> Result<()> { |
| if s.line.delete_word(at, word_def, n) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_delete_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<()> { |
| if s.line.delete_to(cs, n) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_word(s: &mut State, a: WordAction) -> Result<()> { |
| s.changes.borrow_mut().begin(); |
| let succeed = s.line.edit_word(a); |
| s.changes.borrow_mut().end(); |
| if succeed { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_transpose_words(s: &mut State, n: RepeatCount) -> Result<()> { |
| s.changes.borrow_mut().begin(); |
| let succeed = s.line.transpose_words(n); |
| s.changes.borrow_mut().end(); |
| if succeed { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Substitute the currently edited line with the next or previous history |
| /// entry. |
| fn edit_history_next(s: &mut State, history: &History, prev: bool) -> Result<()> { |
| if history.is_empty() { |
| return Ok(()); |
| } |
| if s.history_index == history.len() { |
| if prev { |
| // Save the current edited line before overwriting it |
| s.backup(); |
| } else { |
| return Ok(()); |
| } |
| } else if s.history_index == 0 && prev { |
| return Ok(()); |
| } |
| if prev { |
| s.history_index -= 1; |
| } else { |
| s.history_index += 1; |
| } |
| if s.history_index < history.len() { |
| let buf = history.get(s.history_index).unwrap(); |
| s.changes.borrow_mut().begin(); |
| s.line.update(buf, buf.len()); |
| s.changes.borrow_mut().end(); |
| } else { |
| // Restore current edited line |
| s.restore(); |
| } |
| s.refresh_line() |
| } |
| |
| // Non-incremental, anchored search |
| fn edit_history_search(s: &mut State, history: &History, dir: Direction) -> Result<()> { |
| if history.is_empty() { |
| return s.out.beep(); |
| } |
| if s.history_index == history.len() && dir == Direction::Forward { |
| return s.out.beep(); |
| } else if s.history_index == 0 && dir == Direction::Reverse { |
| return s.out.beep(); |
| } |
| if dir == Direction::Reverse { |
| s.history_index -= 1; |
| } else { |
| s.history_index += 1; |
| } |
| if let Some(history_index) = |
| history.starts_with(&s.line.as_str()[..s.line.pos()], s.history_index, dir) |
| { |
| s.history_index = history_index; |
| let buf = history.get(history_index).unwrap(); |
| s.changes.borrow_mut().begin(); |
| s.line.update(buf, buf.len()); |
| s.changes.borrow_mut().end(); |
| s.refresh_line() |
| } else { |
| s.out.beep() |
| } |
| } |
| |
| /// Substitute the currently edited line with the first/last history entry. |
| fn edit_history(s: &mut State, history: &History, first: bool) -> Result<()> { |
| if history.is_empty() { |
| return Ok(()); |
| } |
| if s.history_index == history.len() { |
| if first { |
| // Save the current edited line before overwriting it |
| s.backup(); |
| } else { |
| return Ok(()); |
| } |
| } else if s.history_index == 0 && first { |
| return Ok(()); |
| } |
| if first { |
| s.history_index = 0; |
| let buf = history.get(s.history_index).unwrap(); |
| s.changes.borrow_mut().begin(); |
| s.line.update(buf, buf.len()); |
| s.changes.borrow_mut().end(); |
| } else { |
| s.history_index = history.len(); |
| // Restore current edited line |
| s.restore(); |
| } |
| s.refresh_line() |
| } |
| |
| /// Completes the line/word |
| fn complete_line<R: RawReader, C: Completer + ?Sized>( |
| rdr: &mut R, |
| s: &mut State, |
| completer: &mut C, |
| config: &Config, |
| ) -> Result<Option<Cmd>> { |
| // get a list of completions |
| let (start, candidates) = try!(completer.complete(&s.line, s.line.pos())); |
| // if no completions, we are done |
| if candidates.is_empty() { |
| try!(s.out.beep()); |
| Ok(None) |
| } else if CompletionType::Circular == config.completion_type() { |
| let mark = s.changes.borrow_mut().begin(); |
| // Save the current edited line before overwriting it |
| let backup = s.line.as_str().to_owned(); |
| let backup_pos = s.line.pos(); |
| let mut cmd; |
| let mut i = 0; |
| loop { |
| // Show completion or original buffer |
| if i < candidates.len() { |
| completer.update(&mut s.line, start, &candidates[i]); |
| try!(s.refresh_line()); |
| } else { |
| // Restore current edited line |
| s.line.update(&backup, backup_pos); |
| try!(s.refresh_line()); |
| } |
| |
| cmd = try!(s.next_cmd(rdr)); |
| match cmd { |
| Cmd::Complete => { |
| i = (i + 1) % (candidates.len() + 1); // Circular |
| if i == candidates.len() { |
| try!(s.out.beep()); |
| } |
| } |
| Cmd::Abort => { |
| // Re-show original buffer |
| if i < candidates.len() { |
| s.line.update(&backup, backup_pos); |
| try!(s.refresh_line()); |
| } |
| s.changes.borrow_mut().truncate(mark); |
| return Ok(None); |
| } |
| _ => { |
| s.changes.borrow_mut().end(); |
| break; |
| } |
| } |
| } |
| Ok(Some(cmd)) |
| } else if CompletionType::List == config.completion_type() { |
| // beep if ambiguous |
| if candidates.len() > 1 { |
| try!(s.out.beep()); |
| } |
| if let Some(lcp) = longest_common_prefix(&candidates) { |
| // if we can extend the item, extend it and return to main loop |
| if lcp.len() > s.line.pos() - start { |
| completer.update(&mut s.line, start, lcp); |
| try!(s.refresh_line()); |
| return Ok(None); |
| } |
| } |
| // we can't complete any further, wait for second tab |
| let mut cmd = try!(s.next_cmd(rdr)); |
| // if any character other than tab, pass it to the main loop |
| if cmd != Cmd::Complete { |
| return Ok(Some(cmd)); |
| } |
| // move cursor to EOL to avoid overwriting the command line |
| let save_pos = s.line.pos(); |
| try!(edit_move_end(s)); |
| s.line.set_pos(save_pos); |
| // we got a second tab, maybe show list of possible completions |
| let show_completions = if candidates.len() > config.completion_prompt_limit() { |
| let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len()); |
| try!(s.out.write_and_flush(msg.as_bytes())); |
| s.old_rows += 1; |
| while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && |
| cmd != Cmd::SelfInsert(1, 'n') && |
| cmd != Cmd::SelfInsert(1, 'N') && |
| cmd != Cmd::Kill(Movement::BackwardChar(1)) |
| { |
| cmd = try!(s.next_cmd(rdr)); |
| } |
| match cmd { |
| Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true, |
| _ => false, |
| } |
| } else { |
| true |
| }; |
| if show_completions { |
| page_completions(rdr, s, &candidates) |
| } else { |
| try!(s.refresh_line()); |
| Ok(None) |
| } |
| } else { |
| Ok(None) |
| } |
| } |
| |
| fn page_completions<R: RawReader>( |
| rdr: &mut R, |
| s: &mut State, |
| candidates: &[String], |
| ) -> Result<Option<Cmd>> { |
| use std::cmp; |
| |
| let min_col_pad = 2; |
| let cols = s.out.get_columns(); |
| let max_width = cmp::min( |
| cols, |
| candidates |
| .into_iter() |
| .map(|s| s.as_str().width()) |
| .max() |
| .unwrap() + min_col_pad, |
| ); |
| let num_cols = cols / max_width; |
| |
| let mut pause_row = s.out.get_rows() - 1; |
| let num_rows = (candidates.len() + num_cols - 1) / num_cols; |
| let mut ab = String::new(); |
| for row in 0..num_rows { |
| if row == pause_row { |
| try!(s.out.write_and_flush(b"\n--More--")); |
| let mut cmd = Cmd::Noop; |
| while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && |
| cmd != Cmd::SelfInsert(1, 'n') && |
| cmd != Cmd::SelfInsert(1, 'N') && |
| cmd != Cmd::SelfInsert(1, 'q') && |
| cmd != Cmd::SelfInsert(1, 'Q') && |
| cmd != Cmd::SelfInsert(1, ' ') && |
| cmd != Cmd::Kill(Movement::BackwardChar(1)) && |
| cmd != Cmd::AcceptLine |
| { |
| cmd = try!(s.next_cmd(rdr)); |
| } |
| match cmd { |
| Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => { |
| pause_row += s.out.get_rows() - 1; |
| } |
| Cmd::AcceptLine => { |
| pause_row += 1; |
| } |
| _ => break, |
| } |
| try!(s.out.write_and_flush(b"\n")); |
| } else { |
| try!(s.out.write_and_flush(b"\n")); |
| } |
| ab.clear(); |
| for col in 0..num_cols { |
| let i = (col * num_rows) + row; |
| if i < candidates.len() { |
| let candidate = &candidates[i]; |
| ab.push_str(candidate); |
| let width = candidate.as_str().width(); |
| if ((col + 1) * num_rows) + row < candidates.len() { |
| for _ in width..max_width { |
| ab.push(' '); |
| } |
| } |
| } |
| } |
| try!(s.out.write_and_flush(ab.as_bytes())); |
| } |
| try!(s.out.write_and_flush(b"\n")); |
| try!(s.refresh_line()); |
| Ok(None) |
| } |
| |
| /// Incremental search |
| fn reverse_incremental_search<R: RawReader>( |
| rdr: &mut R, |
| s: &mut State, |
| history: &History, |
| ) -> Result<Option<Cmd>> { |
| if history.is_empty() { |
| return Ok(None); |
| } |
| let mark = s.changes.borrow_mut().begin(); |
| // Save the current edited line (and cursor position) before overwriting it |
| let backup = s.line.as_str().to_owned(); |
| let backup_pos = s.line.pos(); |
| |
| let mut search_buf = String::new(); |
| let mut history_idx = history.len() - 1; |
| let mut direction = Direction::Reverse; |
| let mut success = true; |
| |
| let mut cmd; |
| // Display the reverse-i-search prompt and process chars |
| loop { |
| let prompt = if success { |
| format!("(reverse-i-search)`{}': ", search_buf) |
| } else { |
| format!("(failed reverse-i-search)`{}': ", search_buf) |
| }; |
| try!(s.refresh_prompt_and_line(&prompt)); |
| |
| cmd = try!(s.next_cmd(rdr)); |
| if let Cmd::SelfInsert(_, c) = cmd { |
| search_buf.push(c); |
| } else { |
| match cmd { |
| Cmd::Kill(Movement::BackwardChar(_)) => { |
| search_buf.pop(); |
| continue; |
| } |
| Cmd::ReverseSearchHistory => { |
| direction = Direction::Reverse; |
| if history_idx > 0 { |
| history_idx -= 1; |
| } else { |
| success = false; |
| continue; |
| } |
| } |
| Cmd::ForwardSearchHistory => { |
| direction = Direction::Forward; |
| if history_idx < history.len() - 1 { |
| history_idx += 1; |
| } else { |
| success = false; |
| continue; |
| } |
| } |
| Cmd::Abort => { |
| // Restore current edited line (before search) |
| s.line.update(&backup, backup_pos); |
| try!(s.refresh_line()); |
| s.changes.borrow_mut().truncate(mark); |
| return Ok(None); |
| } |
| _ => break, |
| } |
| } |
| success = match history.search(&search_buf, history_idx, direction) { |
| Some(idx) => { |
| history_idx = idx; |
| let entry = history.get(idx).unwrap(); |
| let pos = entry.find(&search_buf).unwrap(); |
| s.line.update(entry, pos); |
| true |
| } |
| _ => false, |
| }; |
| } |
| s.changes.borrow_mut().end(); |
| Ok(Some(cmd)) |
| } |
| |
| /// Handles reading and editting the readline buffer. |
| /// It will also handle special inputs in an appropriate fashion |
| /// (e.g., C-c will exit readline) |
| #[allow(let_unit_value)] |
| fn readline_edit( |
| prompt: &str, |
| initial: Option<(&str, &str)>, |
| editor: &mut Editor, |
| original_mode: tty::Mode, |
| ) -> Result<String> { |
| let completer = editor.completer.as_ref(); |
| let hinter = editor.hinter.as_ref().cloned(); |
| |
| let mut stdout = editor.term.create_writer(); |
| |
| editor.reset_kill_ring(); |
| let mut s = State::new( |
| &mut stdout, |
| &editor.config, |
| prompt, |
| editor.history.len(), |
| editor.custom_bindings.clone(), |
| hinter, |
| ); |
| |
| s.line.set_delete_listener(editor.kill_ring.clone()); |
| s.line.set_change_listener(s.changes.clone()); |
| |
| if let Some((left, right)) = initial { |
| s.line |
| .update((left.to_owned() + right).as_ref(), left.len()); |
| } |
| |
| try!(s.refresh_line()); |
| |
| let mut rdr = try!(editor.term.create_reader(&editor.config)); |
| |
| loop { |
| let rc = s.next_cmd(&mut rdr); |
| let mut cmd = try!(rc); |
| |
| if cmd.should_reset_kill_ring() { |
| editor.reset_kill_ring(); |
| } |
| |
| // autocomplete |
| if cmd == Cmd::Complete && completer.is_some() { |
| use std::ops::DerefMut; |
| let next = try!(complete_line( |
| &mut rdr, |
| &mut s, |
| completer.unwrap().borrow_mut().deref_mut(), |
| &editor.config, |
| )); |
| if next.is_some() { |
| cmd = next.unwrap(); |
| } else { |
| continue; |
| } |
| } |
| |
| if let Cmd::SelfInsert(n, c) = cmd { |
| try!(edit_insert(&mut s, c, n)); |
| continue; |
| } else if let Cmd::Insert(n, text) = cmd { |
| try!(edit_yank(&mut s, &text, Anchor::Before, n)); |
| continue; |
| } |
| |
| if cmd == Cmd::ReverseSearchHistory { |
| // Search history backward |
| let next = try!(reverse_incremental_search( |
| &mut rdr, |
| &mut s, |
| &editor.history, |
| )); |
| if next.is_some() { |
| cmd = next.unwrap(); |
| } else { |
| continue; |
| } |
| } |
| |
| match cmd { |
| Cmd::Move(Movement::BeginningOfLine) => { |
| // Move to the beginning of line. |
| try!(edit_move_home(&mut s)) |
| } |
| Cmd::Move(Movement::ViFirstPrint) => { |
| try!(edit_move_home(&mut s)); |
| try!(edit_move_to_next_word(&mut s, At::Start, Word::Big, 1)) |
| } |
| Cmd::Move(Movement::BackwardChar(n)) => { |
| // Move back a character. |
| try!(edit_move_backward(&mut s, n)) |
| } |
| Cmd::Kill(Movement::ForwardChar(n)) => { |
| // Delete (forward) one character at point. |
| try!(edit_delete(&mut s, n)) |
| } |
| Cmd::Replace(n, c) => { |
| try!(edit_replace_char(&mut s, c, n)); |
| } |
| Cmd::Overwrite(c) => { |
| try!(edit_overwrite_char(&mut s, c)); |
| } |
| Cmd::EndOfFile => if !s.edit_state.is_emacs_mode() && !s.line.is_empty() { |
| try!(edit_move_end(&mut s)); |
| break; |
| } else if s.line.is_empty() { |
| return Err(error::ReadlineError::Eof); |
| } else { |
| try!(edit_delete(&mut s, 1)) |
| }, |
| Cmd::Move(Movement::EndOfLine) => { |
| // Move to the end of line. |
| try!(edit_move_end(&mut s)) |
| } |
| Cmd::Move(Movement::ForwardChar(n)) => { |
| // Move forward a character. |
| try!(edit_move_forward(&mut s, n)) |
| } |
| Cmd::Kill(Movement::BackwardChar(n)) => { |
| // Delete one character backward. |
| try!(edit_backspace(&mut s, n)) |
| } |
| Cmd::Kill(Movement::EndOfLine) => { |
| // Kill the text from point to the end of the line. |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_kill_line(&mut s)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| Cmd::Kill(Movement::WholeLine) => { |
| try!(edit_move_home(&mut s)); |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_kill_line(&mut s)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| Cmd::ClearScreen => { |
| // Clear the screen leaving the current line at the top of the screen. |
| try!(s.out.clear_screen()); |
| try!(s.refresh_line()) |
| } |
| Cmd::NextHistory => { |
| // Fetch the next command from the history list. |
| try!(edit_history_next(&mut s, &editor.history, false)) |
| } |
| Cmd::PreviousHistory => { |
| // Fetch the previous command from the history list. |
| try!(edit_history_next(&mut s, &editor.history, true)) |
| } |
| Cmd::HistorySearchBackward => try!(edit_history_search( |
| &mut s, |
| &editor.history, |
| Direction::Reverse, |
| )), |
| Cmd::HistorySearchForward => try!(edit_history_search( |
| &mut s, |
| &editor.history, |
| Direction::Forward, |
| )), |
| Cmd::TransposeChars => { |
| // Exchange the char before cursor with the character at cursor. |
| try!(edit_transpose_chars(&mut s)) |
| } |
| Cmd::Kill(Movement::BeginningOfLine) => { |
| // Kill backward from point to the beginning of the line. |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_discard_line(&mut s)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| #[cfg(unix)] |
| Cmd::QuotedInsert => { |
| // Quoted insert |
| let c = try!(rdr.next_char()); |
| try!(edit_insert(&mut s, c, 1)) // FIXME |
| } |
| Cmd::Yank(n, anchor) => { |
| // retrieve (yank) last item killed |
| if let Some(text) = editor.kill_ring.borrow_mut().yank() { |
| try!(edit_yank(&mut s, text, anchor, n)) |
| } |
| } |
| Cmd::ViYankTo(mvt) => if let Some(text) = s.line.copy(mvt) { |
| editor.kill_ring.borrow_mut().kill(&text, Mode::Append) |
| }, |
| // TODO CTRL-_ // undo |
| Cmd::AcceptLine => { |
| // Accept the line regardless of where the cursor is. |
| try!(edit_move_end(&mut s)); |
| if s.hinter.is_some() { |
| // Force a refresh without hints to leave the previous |
| // line as the user typed it after a newline. |
| s.hinter = None; |
| try!(s.refresh_line()); |
| } |
| break; |
| } |
| Cmd::Kill(Movement::BackwardWord(n, word_def)) => { |
| // kill one word backward (until start of word) |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_delete_prev_word(&mut s, word_def, n)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| Cmd::BeginningOfHistory => { |
| // move to first entry in history |
| try!(edit_history(&mut s, &editor.history, true)) |
| } |
| Cmd::EndOfHistory => { |
| // move to last entry in history |
| try!(edit_history(&mut s, &editor.history, false)) |
| } |
| Cmd::Move(Movement::BackwardWord(n, word_def)) => { |
| // move backwards one word |
| try!(edit_move_to_prev_word(&mut s, word_def, n)) |
| } |
| Cmd::CapitalizeWord => { |
| // capitalize word after point |
| try!(edit_word(&mut s, WordAction::CAPITALIZE)) |
| } |
| Cmd::Kill(Movement::ForwardWord(n, at, word_def)) => { |
| // kill one word forward (until start/end of word) |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_delete_word(&mut s, at, word_def, n)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { |
| // move forwards one word |
| try!(edit_move_to_next_word(&mut s, at, word_def, n)) |
| } |
| Cmd::DowncaseWord => { |
| // lowercase word after point |
| try!(edit_word(&mut s, WordAction::LOWERCASE)) |
| } |
| Cmd::TransposeWords(n) => { |
| // transpose words |
| try!(edit_transpose_words(&mut s, n)) |
| } |
| Cmd::UpcaseWord => { |
| // uppercase word after point |
| try!(edit_word(&mut s, WordAction::UPPERCASE)) |
| } |
| Cmd::YankPop => { |
| // yank-pop |
| if let Some((yank_size, text)) = editor.kill_ring.borrow_mut().yank_pop() { |
| try!(edit_yank_pop(&mut s, yank_size, text)) |
| } |
| } |
| Cmd::Move(Movement::ViCharSearch(n, cs)) => try!(edit_move_to(&mut s, cs, n)), |
| Cmd::Kill(Movement::ViCharSearch(n, cs)) => { |
| editor.kill_ring.borrow_mut().start_killing(); |
| try!(edit_delete_to(&mut s, cs, n)); |
| editor.kill_ring.borrow_mut().stop_killing(); |
| } |
| Cmd::Undo => { |
| s.line.remove_change_listener(); |
| if s.changes.borrow_mut().undo(&mut s.line) { |
| try!(s.refresh_line()); |
| } |
| s.line.set_change_listener(s.changes.clone()); |
| } |
| Cmd::Interrupt => { |
| return Err(error::ReadlineError::Interrupted); |
| } |
| #[cfg(unix)] |
| Cmd::Suspend => { |
| try!(original_mode.disable_raw_mode()); |
| try!(tty::suspend()); |
| try!(editor.term.enable_raw_mode()); // TODO original_mode may have changed |
| try!(s.refresh_line()); |
| continue; |
| } |
| Cmd::Noop => {} |
| _ => { |
| // Ignore the character typed. |
| } |
| } |
| } |
| Ok(s.line.into_string()) |
| } |
| |
| struct Guard(tty::Mode); |
| |
| #[allow(unused_must_use)] |
| impl Drop for Guard { |
| fn drop(&mut self) { |
| let Guard(mode) = *self; |
| mode.disable_raw_mode(); |
| } |
| } |
| |
| /// Readline method that will enable RAW mode, call the `readline_edit()` |
| /// method and disable raw mode |
| fn readline_raw( |
| prompt: &str, |
| initial: Option<(&str, &str)>, |
| editor: &mut Editor, |
| ) -> Result<String> { |
| let original_mode = try!(editor.term.enable_raw_mode()); |
| let guard = Guard(original_mode); |
| let user_input = readline_edit(prompt, initial, editor, original_mode); |
| if editor.config.auto_add_history() { |
| if let Ok(ref line) = user_input { |
| editor.add_history_entry(line.as_ref()); |
| } |
| } |
| drop(guard); // try!(disable_raw_mode(original_mode)); |
| println!(""); |
| user_input |
| } |
| |
| fn readline_direct() -> Result<String> { |
| let mut line = String::new(); |
| if try!(io::stdin().read_line(&mut line)) > 0 { |
| Ok(line) |
| } else { |
| Err(error::ReadlineError::Eof) |
| } |
| } |
| |
| /// Line editor |
| pub struct Editor { |
| term: Terminal, |
| history: History, |
| completer: Option<Rc<RefCell<Completer>>>, |
| kill_ring: Rc<RefCell<KillRing>>, |
| config: Config, |
| custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>, |
| hinter: Option<Rc<RefCell<Hinter>>>, |
| } |
| |
| impl Editor { |
| /// Create an editor with the default configuration |
| pub fn new() -> Editor { |
| Self::with_config(Config::default()) |
| } |
| |
| /// Create an editor with a specific configuration. |
| pub fn with_config(config: Config) -> Editor { |
| let term = Terminal::new(); |
| Editor { |
| term: term, |
| history: History::with_config(config), |
| completer: None, |
| kill_ring: Rc::new(RefCell::new(KillRing::new(60))), |
| config: config, |
| custom_bindings: Rc::new(RefCell::new(HashMap::new())), |
| hinter: None, |
| } |
| } |
| |
| /// This method will read a line from STDIN and will display a `prompt`. |
| /// |
| /// It uses terminal-style interaction if `stdin` is connected to a |
| /// terminal. |
| /// Otherwise (e.g., if `stdin` is a pipe or the terminal is not supported), |
| /// it uses file-style interaction. |
| pub fn readline(&mut self, prompt: &str) -> Result<String> { |
| self.readline_with(prompt, None) |
| } |
| /// This function behaves in the exact same manner as `readline`, except |
| /// that it pre-populates the input area. |
| /// |
| /// The text that resides in the input area is given as a 2-tuple. |
| /// The string on the left of the tuple what will appear to the left of the |
| /// cursor |
| /// and the string on the right is what will appear to the right of the |
| /// cursor. |
| pub fn readline_with_initial(&mut self, prompt: &str, initial: (&str, &str)) -> Result<String> { |
| self.readline_with(prompt, Some(initial)) |
| } |
| |
| fn readline_with(&mut self, prompt: &str, initial: Option<(&str, &str)>) -> Result<String> { |
| if self.term.is_unsupported() { |
| debug!(target: "rustyline", "unsupported terminal"); |
| // Write prompt and flush it to stdout |
| let mut stdout = io::stdout(); |
| try!(stdout.write_all(prompt.as_bytes())); |
| try!(stdout.flush()); |
| |
| readline_direct() |
| } else if !self.term.is_stdin_tty() { |
| debug!(target: "rustyline", "stdin is not a tty"); |
| // Not a tty: read from file / pipe. |
| readline_direct() |
| } else { |
| readline_raw(prompt, initial, self) |
| } |
| } |
| |
| /// Load the history from the specified file. |
| pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> { |
| self.history.load(path) |
| } |
| /// Save the history in the specified file. |
| pub fn save_history<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> { |
| self.history.save(path) |
| } |
| /// Add a new entry in the history. |
| pub fn add_history_entry<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool { |
| self.history.add(line) |
| } |
| /// Clear history. |
| pub fn clear_history(&mut self) { |
| self.history.clear() |
| } |
| /// Return a mutable reference to the history object. |
| pub fn get_history(&mut self) -> &mut History { |
| &mut self.history |
| } |
| /// Return an immutable reference to the history object. |
| pub fn get_history_const(&self) -> &History { |
| &self.history |
| } |
| |
| /// Register a callback function to be called for tab-completion. |
| pub fn set_completer(&mut self, completer: Option<Rc<RefCell<Completer>>>) { |
| self.completer = completer; |
| } |
| |
| /// Register a hints function to be called to show hints to the uer at the |
| /// right of the prompt. |
| pub fn set_hinter(&mut self, hinter: Option<Rc<RefCell<Hinter>>>) { |
| self.hinter = hinter; |
| } |
| |
| /// Bind a sequence to a command. |
| pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> { |
| self.custom_bindings.borrow_mut().insert(key_seq, cmd) |
| } |
| /// Remove a binding for the given sequence. |
| pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> { |
| self.custom_bindings.borrow_mut().remove(&key_seq) |
| } |
| |
| /// ``` |
| /// let mut rl = rustyline::Editor::new(); |
| /// for readline in rl.iter("> ") { |
| /// match readline { |
| /// Ok(line) => { |
| /// println!("Line: {}", line); |
| /// }, |
| /// Err(err) => { |
| /// println!("Error: {:?}", err); |
| /// break |
| /// } |
| /// } |
| /// } |
| /// ``` |
| pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter { |
| Iter { |
| editor: self, |
| prompt: prompt, |
| } |
| } |
| |
| fn reset_kill_ring(&self) { |
| self.kill_ring.borrow_mut().reset(); |
| } |
| } |
| |
| impl fmt::Debug for Editor { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.debug_struct("Editor") |
| .field("term", &self.term) |
| .field("config", &self.config) |
| .finish() |
| } |
| } |
| |
| /// Edited lines iterator |
| pub struct Iter<'a> { |
| editor: &'a mut Editor, |
| prompt: &'a str, |
| } |
| |
| impl<'a> Iterator for Iter<'a> { |
| type Item = Result<String>; |
| |
| fn next(&mut self) -> Option<Result<String>> { |
| let readline = self.editor.readline(self.prompt); |
| match readline { |
| Ok(l) => Some(Ok(l)), |
| Err(error::ReadlineError::Eof) => None, |
| e @ Err(_) => Some(e), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use std::cell::RefCell; |
| use std::collections::HashMap; |
| use std::rc::Rc; |
| use line_buffer::LineBuffer; |
| use history::History; |
| use completion::Completer; |
| use config::Config; |
| use consts::KeyPress; |
| use keymap::{Cmd, EditState}; |
| use super::{Editor, Position, Result, State}; |
| use tty::Renderer; |
| use undo::Changeset; |
| |
| fn init_state<'out>(out: &'out mut Renderer, line: &str, pos: usize) -> State<'out, 'static> { |
| let config = Config::default(); |
| State { |
| out: out, |
| prompt: "", |
| prompt_size: Position::default(), |
| line: LineBuffer::init(line, pos, None), |
| cursor: Position::default(), |
| old_rows: 0, |
| history_index: 0, |
| saved_line_for_history: LineBuffer::with_capacity(100), |
| byte_buffer: [0; 4], |
| edit_state: EditState::new(&config, Rc::new(RefCell::new(HashMap::new()))), |
| changes: Rc::new(RefCell::new(Changeset::new())), |
| hinter: None, |
| } |
| } |
| |
| fn init_editor(keys: &[KeyPress]) -> Editor { |
| let mut editor = Editor::new(); |
| editor.term.keys.extend(keys.iter().cloned()); |
| editor |
| } |
| |
| #[test] |
| fn edit_history_next() { |
| let mut out = ::std::io::sink(); |
| let line = "current edited line"; |
| let mut s = init_state(&mut out, line, 6); |
| let mut history = History::new(); |
| history.add("line0"); |
| history.add("line1"); |
| s.history_index = history.len(); |
| |
| for _ in 0..2 { |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| assert_eq!(line, s.line.as_str()); |
| } |
| |
| super::edit_history_next(&mut s, &history, true).unwrap(); |
| assert_eq!(line, s.saved_line_for_history.as_str()); |
| assert_eq!(1, s.history_index); |
| assert_eq!("line1", s.line.as_str()); |
| |
| for _ in 0..2 { |
| super::edit_history_next(&mut s, &history, true).unwrap(); |
| assert_eq!(line, s.saved_line_for_history.as_str()); |
| assert_eq!(0, s.history_index); |
| assert_eq!("line0", s.line.as_str()); |
| } |
| |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| assert_eq!(line, s.saved_line_for_history.as_str()); |
| assert_eq!(1, s.history_index); |
| assert_eq!("line1", s.line.as_str()); |
| |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| // assert_eq!(line, s.saved_line_for_history); |
| assert_eq!(2, s.history_index); |
| assert_eq!(line, s.line.as_str()); |
| } |
| |
| struct SimpleCompleter; |
| impl Completer for SimpleCompleter { |
| fn complete(&mut self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { |
| Ok((0, vec![line.to_owned() + "t"])) |
| } |
| } |
| |
| #[test] |
| fn complete_line() { |
| let mut out = ::std::io::sink(); |
| let mut s = init_state(&mut out, "rus", 3); |
| let keys = &[KeyPress::Enter]; |
| let mut rdr = keys.iter(); |
| let mut completer = SimpleCompleter; |
| let cmd = |
| super::complete_line(&mut rdr, &mut s, &mut completer, &Config::default()).unwrap(); |
| assert_eq!(Some(Cmd::AcceptLine), cmd); |
| assert_eq!("rust", s.line.as_str()); |
| assert_eq!(4, s.line.pos()); |
| } |
| |
| fn assert_line(keys: &[KeyPress], expected_line: &str) { |
| let mut editor = init_editor(keys); |
| let actual_line = editor.readline(&">>").unwrap(); |
| assert_eq!(expected_line, actual_line); |
| } |
| |
| #[test] |
| fn delete_key() { |
| assert_line( |
| &[KeyPress::Char('a'), KeyPress::Delete, KeyPress::Enter], |
| "a", |
| ); |
| assert_line( |
| &[ |
| KeyPress::Char('a'), |
| KeyPress::Left, |
| KeyPress::Delete, |
| KeyPress::Enter, |
| ], |
| "", |
| ); |
| } |
| |
| #[test] |
| fn down_key() { |
| assert_line(&[KeyPress::Down, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn end_key() { |
| assert_line(&[KeyPress::End, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn home_key() { |
| assert_line(&[KeyPress::Home, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn left_key() { |
| assert_line(&[KeyPress::Left, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn meta_backspace_key() { |
| assert_line(&[KeyPress::Meta('\x08'), KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn page_down_key() { |
| assert_line(&[KeyPress::PageDown, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn page_up_key() { |
| assert_line(&[KeyPress::PageUp, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn right_key() { |
| assert_line(&[KeyPress::Right, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn up_key() { |
| assert_line(&[KeyPress::Up, KeyPress::Enter], ""); |
| } |
| |
| #[test] |
| fn unknown_esc_key() { |
| assert_line(&[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); |
| } |
| } |