Newer
Older
//! This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise)
//! let mut rl = rustyline::Editor::<()>::new();
//! Ok(line) => println!("Line: {:?}",line),
//! Err(_) => println!("No input"),
//! }
extern crate nix;
#[cfg(windows)]
extern crate winapi;
#[cfg(windows)]
extern crate kernel32;
pub mod completion;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
use tty::{RawMode, RawReader, Terminal, Term};
use completion::{Completer, longest_common_prefix};
use history::{Direction, History};
use kill_ring::{Mode, KillRing};
pub use config::{CompletionType, Config, EditMode, HistoryDuplicates};
/// 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.
struct State<'out, 'prompt> {
out: &'out mut Write,
prompt: &'prompt str, // Prompt to display
prompt_size: Position, // Prompt Unicode width and height
cursor: Position, // Cursor position (relative to the start of the prompt for `row`)
cols: usize, // Number of columns in terminal
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
snapshot: LineBuffer, // Current edited line before history browsing/completion
#[derive(Copy, Clone, Debug, Default)]
struct Position {
col: usize,
row: usize,
prompt: &'prompt str,
history_index: usize)
let prompt_size = calculate_position(prompt, Position::default(), cols);
out: out,
prompt: prompt,
prompt_size: prompt_size,
line: LineBuffer::with_capacity(capacity),
cursor: prompt_size,
cols: cols,
old_rows: prompt_size.row,
history_index: history_index,
snapshot: LineBuffer::with_capacity(capacity),
edit_state: EditState::new(config),
}
}
fn next_cmd<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
loop {
let rc = self.edit_state.next_cmd(rdr, config);
if rc.is_err() && self.term.sigwinch() {
self.update_columns();
try!(self.refresh_line());
continue;
}
return rc;
/// 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;
self.refresh(self.prompt, prompt_size)
}
fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
let prompt_size = calculate_position(prompt, Position::default(), self.cols);
fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
// calculate the position of the end of the input line
let end_pos = calculate_position(&self.line, prompt_size, self.cols);
// calculate the desired position of the cursor
let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols);
let mut ab = String::new();
let cursor_row_movement = self.old_rows - self.cursor.row;
// move the cursor down as required
write!(ab, "\x1b[{}B", cursor_row_movement).unwrap();
}
// clear old rows
for _ in 0..self.old_rows {
ab.push_str("\r\x1b[0K\x1b[1A");
// clear the line
ab.push_str("\r\x1b[0K");
// we have to generate our own newline on line wrap
if end_pos.col == 0 && end_pos.row > 0 {
ab.push_str("\n");
// position the cursor
let cursor_row_movement = end_pos.row - cursor.row;
// move the cursor up as required
if cursor_row_movement > 0 {
write!(ab, "\x1b[{}A", cursor_row_movement).unwrap();
// position the cursor within the line
if cursor.col > 0 {
self.old_rows = end_pos.row;
write_and_flush(self.out, ab.as_bytes())
}
#[cfg(windows)]
fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
// calculate the position of the end of the input line
let end_pos = calculate_position(&self.line, prompt_size, self.cols);
// calculate the desired position of the cursor
let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols);
// position at the start of the prompt, clear to end of previous input
info.dwCursorPosition.X = 0;
info.dwCursorPosition.Y -= self.cursor.row as i16;
try!(self.term.set_console_cursor_position(info.dwCursorPosition));
try!(self.term
.fill_console_output_character((info.dwSize.X * (self.old_rows as i16 + 1)) as u32,
info.dwCursorPosition));
ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute)
// display the input line
ab.push_str(&self.line);
try!(write_and_flush(self.out, ab.as_bytes()));
// position the cursor
info.dwCursorPosition.X = cursor.col as i16;
info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16;
try!(self.term.set_console_cursor_position(info.dwCursorPosition));
}
fn update_columns(&mut self) {
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.cols)
.field("old_rows", &self.old_rows)
.field("history_index", &self.history_index)
.field("snapshot", &self.snapshot)
.finish()
fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> {
try!(w.write_all(buf));
try!(w.flush());
/// Beep, used for completion when there is nothing to complete or when all
/// the choices were already shown.
write_and_flush(&mut io::stderr(), b"\x07") // TODO bell-style
/// Calculate the number of columns and rows used to display `s` on a `cols` width terminal
/// starting at `orig`.
/// Control characters are treated as having zero width.
/// Characters with 2 column width are correctly handled (not splitted).
fn calculate_position(s: &str, orig: Position, cols: usize) -> Position {
let mut pos = orig;
let mut esc_seq = 0;
for c in s.chars() {
let cw = if esc_seq == 1 {
if c == '[' {
// CSI
esc_seq = 2;
// two-character sequence
esc_seq = 0;
}
None
} else if esc_seq == 2 {
if c == ';' || (c >= '0' && c <= '9') {
} else if c == 'm' {
// last
esc_seq = 0;
// not supported
esc_seq = 0;
}
None
} else if c == '\x1b' {
esc_seq = 1;
None
} else if c == '\n' {
pos.col = 0;
pos.row += 1;
None
} else {
};
if let Some(cw) = cw {
pos.col += cw;
if pos.col > cols {
pos.row += 1;
pos.col = cw;
if pos.col == cols {
pos.col = 0;
pos.row += 1;
}
pos
/// Insert the character `ch` at cursor current position.
fn edit_insert(s: &mut State, ch: char, n: usize) -> Result<()> {
if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.cols {
// Avoid a full update of the line in the trivial case.
let cursor = calculate_position(&s.line[..s.line.pos()], s.prompt_size, s.cols);
s.cursor = cursor;
let bits = ch.encode_utf8(&mut s.byte_buffer);
let bits = bits.as_bytes();
}
} else {
Ok(())
}
}
/// Replace a single (or n) character(s) under the cursor (Vi mode)
fn edit_replace_char(s: &mut State, ch: char, n: usize) -> Result<()> {
if let Some(chars) = s.line.delete(n) {
let count = chars.graphemes(true).count();
s.line.move_left(1);
s.refresh_line()
} else {
Ok(())
}
}
fn edit_yank(s: &mut State, text: &str, anchor: Anchor, n: usize) -> Result<()> {
// Delete previously yanked text and yank/paste `text` at current position.
fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> {
fn edit_move_left(s: &mut State, n: usize) -> Result<()> {
} else {
Ok(())
}
}
/// Move cursor on the right.
fn edit_move_right(s: &mut State, n: usize) -> Result<()> {
s.refresh_line()
} else {
Ok(())
}
}
/// Move cursor to the start of the line.
fn edit_move_home(s: &mut State) -> Result<()> {
} else {
Ok(())
}
}
/// Move cursor to the end of the line.
fn edit_move_end(s: &mut State) -> Result<()> {
if s.line.move_end() {
s.refresh_line()
} 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: usize) -> Result<()> {
if s.line.delete(n).is_some() {
} else {
Ok(())
}
}
/// Backspace implementation.
fn edit_backspace(s: &mut State, n: usize) -> Result<()> {
if s.line.backspace(n).is_some() {
} else {
Ok(())
}
}
/// Kill the text from point to the end of the line.
}
}
/// Kill backward from point to the beginning of the line.
}
}
/// Exchange the char before cursor with the character at cursor.
fn edit_transpose_chars(s: &mut State) -> Result<()> {
} else {
Ok(())
}
}
fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: usize) -> Result<()> {
s.refresh_line()
} 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: usize) -> Result<Option<String>> {
fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: usize) -> Result<()> {
fn edit_move_to(s: &mut State, cs: CharSearch, n: usize) -> Result<()> {
s.refresh_line()
} 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: usize) -> Result<Option<String>> {
try!(s.refresh_line());
Ok(Some(text))
} else {
Ok(None)
}
}
fn edit_delete_to(s: &mut State, cs: CharSearch, n: usize) -> Result<Option<String>> {
try!(s.refresh_line());
Ok(Some(text))
} else {
Ok(None)
}
}
fn edit_transpose_words(s: &mut State) -> Result<()> {
if s.line.transpose_words() {
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() {
} else if s.history_index == 0 && prev {
return Ok(());
}
if prev {
s.history_index -= 1;
if s.history_index < history.len() {
let buf = history.get(s.history_index).unwrap();
} else {
// Restore current edited line
s.snapshot();
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
}
s.refresh_line()
}
/// 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 to overwrite it
s.snapshot();
} 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.line.update(buf, buf.len());
} else {
s.history_index = history.len();
// Restore current edited line
s.snapshot();
}
fn complete_line<R: RawReader>(rdr: &mut R,
s: &mut State,
completer: &Completer,
config: &Config)
let (start, candidates) = try!(completer.complete(&s.line, s.line.pos()));
if candidates.is_empty() {
try!(beep());
Ok(None)
} else if CompletionType::Circular == config.completion_type() {
// Save the current edited line before to overwrite it
s.backup();
let mut i = 0;
loop {
// Show completion or original buffer
if i < candidates.len() {
completer.update(&mut s.line, start, &candidates[i]);
// Restore current edited line
s.snapshot();
cmd = try!(s.next_cmd(rdr, config));
match cmd {
Cmd::Complete => {
if i == candidates.len() {
try!(beep());
}
} else if CompletionType::List == config.completion_type() {
// beep if ambiguous
if candidates.len() > 1 {
try!(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 {
try!(s.refresh_line());
return Ok(None);
}
}
// if any character other than tab, pass it to the main loop
// 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 mut show_completions = true;
if candidates.len() > config.completion_prompt_limit() {
let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len());
try!(write_and_flush(s.out, 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::SelfInsert(1, 'y') |
Cmd::SelfInsert(1, 'Y') => true,
page_completions(rdr, s, config, &candidates)
fn page_completions<R: RawReader>(rdr: &mut R,
s: &mut State,
use std::cmp;
let min_col_pad = 2;
let max_width = cmp::min(s.cols,
candidates.into_iter()
.max()
.unwrap() + min_col_pad);
let num_cols = s.cols / max_width;
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!(write_and_flush(s.out, b"\n--More--"));
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::SelfInsert(1, 'y') |
Cmd::SelfInsert(1, 'Y') |
Cmd::SelfInsert(1, ' ') => {
} else {
try!(write_and_flush(s.out, 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);
if ((col + 1) * num_rows) + row < candidates.len() {
for _ in width..max_width {
ab.push(' ');
}
}
}
}
try!(write_and_flush(s.out, ab.as_bytes()));
}
try!(write_and_flush(s.out, b"\n"));
try!(s.refresh_line());
fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
s: &mut State,
if history.is_empty() {
return Ok(None);
}
// Save the current edited line (and cursor position) before to overwrite it
let mut search_buf = String::new();
let mut history_idx = history.len() - 1;
let mut direction = Direction::Reverse;
// 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));
direction = Direction::Reverse;
if history_idx > 0 {
history_idx -= 1;
} else {
success = false;
continue;
}
direction = Direction::Forward;
if history_idx < history.len() - 1 {
history_idx += 1;
} else {
success = false;
continue;
}
}
// Restore current edited line (before search)
s.snapshot();
success = match history.search(&search_buf, history_idx, direction) {
Some(idx) => {
history_idx = idx;
let entry = history.get(idx).unwrap();
/// Handles reading and editting the readline buffer.
/// It will also handle special inputs in an appropriate fashion
/// (e.g., C-c will exit readline)
fn readline_edit<C: Completer>(prompt: &str,
editor: &mut Editor<C>,
original_mode: tty::Mode)
-> Result<String> {
let completer = editor.completer.as_ref().map(|c| c as &Completer);
let mut stdout = editor.term.create_writer();
Main
committed
loop {
let rc = s.next_cmd(&mut rdr, &editor.config);
let mut cmd = try!(rc);
let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap(), &editor.config));
continue;
}
if cmd == Cmd::ReverseSearchHistory {
let next =
try!(reverse_incremental_search(&mut rdr, &mut s, &editor.history, &editor.config));
// Move to the beginning of line.
try!(edit_move_home(&mut s))
}
editor.kill_ring.kill(&text, Mode::Append)
Cmd::KillWholeLine => {
try!(edit_move_home(&mut s));
if let Some(text) = try!(edit_kill_line(&mut s)) {
editor.kill_ring.kill(&text, Mode::Append)
}
}
try!(edit_history_next(&mut s, &editor.history, false))
try!(edit_history_next(&mut s, &editor.history, true))
// Exchange the char before cursor with the character at cursor.
try!(edit_transpose_chars(&mut s))
}
editor.kill_ring.kill(&text, Mode::Prepend)
if let Some(text) = editor.kill_ring.yank() {
if let Some(text) = try!(edit_delete_prev_word(&mut s, word_def, n)) {
editor.kill_ring.kill(&text, Mode::Prepend)
// move to first entry in history
editor.kill_ring.reset();
try!(edit_history(&mut s, &editor.history, true))
editor.kill_ring.reset();
try!(edit_history(&mut s, &editor.history, false))