pub mod completion;
pub mod config;
mod edit;
pub mod error;
pub mod highlight;
pub mod hint;
pub mod history;
mod keymap;
mod keys;
mod kill_ring;
mod layout;
pub mod line_buffer;
mod tty;
mod undo;
pub mod validate;
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Write};
use std::path::Path;
use std::result;
use std::sync::{Arc, Mutex, RwLock};
use log::debug;
use unicode_width::UnicodeWidthStr;
use crate::tty::{RawMode, Renderer, Term, Terminal};
use crate::completion::{longest_common_prefix, Candidate, Completer};
pub use crate::config::{
ColorMode, CompletionType, Config, EditMode, HistoryDuplicates, OutputStreamType,
};
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};
use crate::keymap::{InputState, Refresher};
pub use crate::keys::KeyPress;
use crate::kill_ring::{KillRing, Mode};
use crate::line_buffer::WordAction;
use crate::validate::Validator;
pub type Result<T> = result::Result<T, error::ReadlineError>;
fn complete_line<H: Helper>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
input_state: &mut InputState,
config: &Config,
) -> Result<Option<Cmd>> {
#[cfg(all(unix, feature = "with-fuzzy"))]
use skim::{Skim, SkimOptionsBuilder};
let completer = s.helper.unwrap();
let (start, candidates) = completer.complete(&s.line, s.line.pos(), &s.ctx)?;
if candidates.is_empty() {
s.out.beep()?;
Ok(None)
} else if CompletionType::Circular == config.completion_type() {
let mark = s.changes.borrow_mut().begin();
let backup = s.line.as_str().to_owned();
let backup_pos = s.line.pos();
let mut cmd;
let mut i = 0;
loop {
if i < candidates.len() {
let candidate = candidates[i].replacement();
completer.update(&mut s.line, start, candidate);
s.refresh_line()?;
} else {
s.line.update(&backup, backup_pos);
s.refresh_line()?;
}
cmd = s.next_cmd(input_state, rdr, true)?;
match cmd {
Cmd::Complete => {
i = (i + 1) % (candidates.len() + 1);
if i == candidates.len() {
s.out.beep()?;
}
}
Cmd::CompleteBackward => {
if i == 0 {
i = candidates.len();
s.out.beep()?;
} else {
i = (i - 1) % (candidates.len() + 1);
}
}
Cmd::Abort => {
if i < candidates.len() {
s.line.update(&backup, backup_pos);
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() {
if let Some(lcp) = longest_common_prefix(&candidates) {
if lcp.len() > s.line.pos() - start {
completer.update(&mut s.line, start, lcp);
s.refresh_line()?;
}
}
if candidates.len() > 1 {
s.out.beep()?;
} else {
return Ok(None);
}
let mut cmd = s.next_cmd(input_state, rdr, true)?;
if cmd != Cmd::Complete {
return Ok(Some(cmd));
}
let save_pos = s.line.pos();
s.edit_move_end()?;
s.line.set_pos(save_pos);
let show_completions = if candidates.len() > config.completion_prompt_limit() {
let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len());
s.out.write_and_flush(msg.as_bytes())?;
s.layout.end.row += 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 = s.next_cmd(input_state, rdr, false)?;
}
match cmd {
Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true,
_ => false,
}
} else {
true
};
if show_completions {
page_completions(rdr, s, input_state, &candidates)
} else {
s.refresh_line()?;
Ok(None)
}
} else {
#[cfg(all(unix, feature = "with-fuzzy"))]
{
if CompletionType::Fuzzy == config.completion_type() {
let input = candidates
.iter()
.map(|c| c.display())
.collect::<Vec<_>>()
.join("\n");
let options = SkimOptionsBuilder::default()
.height(Some("20%"))
.prompt(Some("? "))
.reverse(true)
.build()
.unwrap();
let selected_items =
Skim::run_with(&options, Some(Box::new(std::io::Cursor::new(input))))
.map(|out| out.selected_items)
.unwrap_or_else(Vec::new);
if let Some(item) = selected_items.first() {
if let Some(candidate) = candidates.get(item.get_index()) {
completer.update(&mut s.line, start, candidate.replacement());
}
}
s.refresh_line()?;
}
};
Ok(None)
}
}
fn complete_hint_line<H: Helper>(s: &mut State<'_, '_, H>) -> Result<()> {
let hint = match s.hint.as_ref() {
Some(hint) => hint,
None => return Ok(()),
};
s.line.move_end();
if s.line.yank(hint, 1).is_none() {
s.out.beep()?;
}
s.refresh_line_with_msg(None)?;
Ok(())
}
fn page_completions<C: Candidate, H: Helper>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
input_state: &mut InputState,
candidates: &[C],
) -> Result<Option<Cmd>> {
use std::cmp;
let min_col_pad = 2;
let cols = s.out.get_columns();
let max_width = cmp::min(
cols,
candidates
.iter()
.map(|s| s.display().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 {
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 != Cmd::AcceptOrInsertLine
{
cmd = s.next_cmd(input_state, rdr, false)?;
}
match cmd {
Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => {
pause_row += s.out.get_rows() - 1;
}
Cmd::AcceptLine | Cmd::AcceptOrInsertLine => {
pause_row += 1;
}
_ => break,
}
s.out.write_and_flush(b"\n")?;
} else {
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].display();
let width = candidate.width();
if let Some(highlighter) = s.highlighter() {
ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List));
} else {
ab.push_str(candidate);
}
if ((col + 1) * num_rows) + row < candidates.len() {
for _ in width..max_width {
ab.push(' ');
}
}
}
}
s.out.write_and_flush(ab.as_bytes())?;
}
s.out.write_and_flush(b"\n")?;
s.refresh_line()?;
Ok(None)
}
fn reverse_incremental_search<H: Helper>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
input_state: &mut InputState,
history: &History,
) -> Result<Option<Cmd>> {
if history.is_empty() {
return Ok(None);
}
let mark = s.changes.borrow_mut().begin();
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;
loop {
let prompt = if success {
format!("(reverse-i-search)`{}': ", search_buf)
} else {
format!("(failed reverse-i-search)`{}': ", search_buf)
};
s.refresh_prompt_and_line(&prompt)?;
cmd = s.next_cmd(input_state, rdr, true)?;
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 => {
s.line.update(&backup, backup_pos);
s.refresh_line()?;
s.changes.borrow_mut().truncate(mark);
return Ok(None);
}
Cmd::Move(_) => {
s.refresh_line()?;
break;
}
_ => 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))
}
fn readline_edit<H: Helper>(
prompt: &str,
initial: Option<(&str, &str)>,
editor: &mut Editor<H>,
original_mode: &tty::Mode,
) -> Result<String> {
let helper = editor.helper.as_ref();
let mut stdout = editor.term.create_writer();
editor.reset_kill_ring();
let ctx = Context::new(&editor.history);
let mut s = State::new(&mut stdout, prompt, helper, ctx);
let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings));
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());
}
let mut rdr = editor.term.create_reader(&editor.config)?;
if editor.term.is_output_tty() {
s.move_cursor_at_leftmost(&mut rdr)?;
}
s.refresh_line()?;
loop {
let rc = s.next_cmd(&mut input_state, &mut rdr, false);
let mut cmd = rc?;
if cmd.should_reset_kill_ring() {
editor.reset_kill_ring();
}
if cmd == Cmd::Complete && s.helper.is_some() {
let next = complete_line(&mut rdr, &mut s, &mut input_state, &editor.config)?;
if let Some(next) = next {
cmd = next;
} else {
continue;
}
}
if Cmd::CompleteHint == cmd {
complete_hint_line(&mut s)?;
continue;
}
if let Cmd::SelfInsert(n, c) = cmd {
s.edit_insert(c, n)?;
continue;
} else if let Cmd::Insert(n, text) = cmd {
s.edit_yank(&input_state, &text, Anchor::Before, n)?;
continue;
}
if cmd == Cmd::ReverseSearchHistory {
let next =
reverse_incremental_search(&mut rdr, &mut s, &mut input_state, &editor.history)?;
if let Some(next) = next {
cmd = next;
} else {
continue;
}
}
match cmd {
Cmd::Move(Movement::BeginningOfLine) => {
s.edit_move_home()?
}
Cmd::Move(Movement::ViFirstPrint) => {
s.edit_move_home()?;
s.edit_move_to_next_word(At::Start, Word::Big, 1)?
}
Cmd::Move(Movement::BackwardChar(n)) => {
s.edit_move_backward(n)?
}
Cmd::ReplaceChar(n, c) => s.edit_replace_char(c, n)?,
Cmd::Replace(mvt, text) => {
s.edit_kill(&mvt)?;
if let Some(text) = text {
s.edit_insert_text(&text)?
}
}
Cmd::Overwrite(c) => {
s.edit_overwrite_char(c)?;
}
Cmd::EndOfFile => {
if !input_state.is_emacs_mode() && !s.line.is_empty() {
s.edit_move_end()?;
break;
} else if s.line.is_empty() {
return Err(error::ReadlineError::Eof);
} else {
s.edit_delete(1)?
}
}
Cmd::Move(Movement::EndOfLine) => {
s.edit_move_end()?
}
Cmd::Move(Movement::ForwardChar(n)) => {
s.edit_move_forward(n)?
}
Cmd::ClearScreen => {
s.clear_screen()?;
s.refresh_line()?
}
Cmd::NextHistory => {
s.edit_history_next(false)?
}
Cmd::PreviousHistory => {
s.edit_history_next(true)?
}
Cmd::LineUpOrPreviousHistory => {
if !s.edit_move_line_up(1)? {
s.edit_history_next(true)?
}
}
Cmd::LineDownOrNextHistory => {
if !s.edit_move_line_down(1)? {
s.edit_history_next(false)?
}
}
Cmd::HistorySearchBackward => s.edit_history_search(Direction::Reverse)?,
Cmd::HistorySearchForward => s.edit_history_search(Direction::Forward)?,
Cmd::TransposeChars => {
s.edit_transpose_chars()?
}
#[cfg(unix)]
Cmd::QuotedInsert => {
use tty::RawReader;
let c = rdr.next_char()?;
s.edit_insert(c, 1)?
}
Cmd::Yank(n, anchor) => {
let mut kill_ring = editor.kill_ring.lock().unwrap();
if let Some(text) = kill_ring.yank() {
s.edit_yank(&input_state, text, anchor, n)?
}
}
Cmd::ViYankTo(ref mvt) => {
if let Some(text) = s.line.copy(mvt) {
let mut kill_ring = editor.kill_ring.lock().unwrap();
kill_ring.kill(&text, Mode::Append)
}
}
Cmd::AcceptLine | Cmd::AcceptOrInsertLine => {
#[cfg(test)]
{
editor.term.cursor = s.layout.cursor.col;
}
if s.has_hint() || !s.is_default_prompt() {
s.refresh_line_with_msg(None)?;
}
if s.validate()? && (cmd == Cmd::AcceptLine || s.line.is_end_of_input()) {
break;
} else {
s.edit_insert('\n', 1)?;
}
continue;
}
Cmd::BeginningOfHistory => {
s.edit_history(true)?
}
Cmd::EndOfHistory => {
s.edit_history(false)?
}
Cmd::Move(Movement::BackwardWord(n, word_def)) => {
s.edit_move_to_prev_word(word_def, n)?
}
Cmd::CapitalizeWord => {
s.edit_word(WordAction::CAPITALIZE)?
}
Cmd::Kill(ref mvt) => {
s.edit_kill(mvt)?;
}
Cmd::Move(Movement::ForwardWord(n, at, word_def)) => {
s.edit_move_to_next_word(at, word_def, n)?
}
Cmd::Move(Movement::LineUp(n)) => {
s.edit_move_line_up(n)?;
}
Cmd::Move(Movement::LineDown(n)) => {
s.edit_move_line_down(n)?;
}
Cmd::Move(Movement::BeginningOfBuffer) => {
s.edit_move_buffer_start()?
}
Cmd::Move(Movement::EndOfBuffer) => {
s.edit_move_buffer_end()?
}
Cmd::DowncaseWord => {
s.edit_word(WordAction::LOWERCASE)?
}
Cmd::TransposeWords(n) => {
s.edit_transpose_words(n)?
}
Cmd::UpcaseWord => {
s.edit_word(WordAction::UPPERCASE)?
}
Cmd::YankPop => {
let mut kill_ring = editor.kill_ring.lock().unwrap();
if let Some((yank_size, text)) = kill_ring.yank_pop() {
s.edit_yank_pop(yank_size, text)?
}
}
Cmd::Move(Movement::ViCharSearch(n, cs)) => s.edit_move_to(cs, n)?,
Cmd::Undo(n) => {
if s.changes.borrow_mut().undo(&mut s.line, n) {
s.refresh_line()?;
}
}
Cmd::Interrupt => {
return Err(error::ReadlineError::Interrupted);
}
#[cfg(unix)]
Cmd::Suspend => {
original_mode.disable_raw_mode()?;
tty::suspend()?;
editor.term.enable_raw_mode()?;
s.refresh_line()?;
continue;
}
_ => {
}
}
}
if cfg!(windows) {
let _ = original_mode;
}
Ok(s.line.into_string())
}
struct Guard<'m>(&'m tty::Mode);
#[allow(unused_must_use)]
impl Drop for Guard<'_> {
fn drop(&mut self) {
let Guard(mode) = *self;
mode.disable_raw_mode();
}
}
fn readline_raw<H: Helper>(
prompt: &str,
initial: Option<(&str, &str)>,
editor: &mut Editor<H>,
) -> Result<String> {
let original_mode = 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_str());
}
}
drop(guard);
match editor.config.output_stream() {
OutputStreamType::Stdout => writeln!(io::stdout())?,
OutputStreamType::Stderr => writeln!(io::stderr())?,
};
user_input
}
fn readline_direct() -> Result<String> {
let mut line = String::new();
if io::stdin().read_line(&mut line)? > 0 {
Ok(line)
} else {
Err(error::ReadlineError::Eof)
}
}
pub trait Helper
where
Self: Completer + Hinter + Highlighter + Validator,
{
}
impl Helper for () {}
impl<'h, H: ?Sized + Helper> Helper for &'h H {}
pub struct Context<'h> {
history: &'h History,
history_index: usize,
}
impl<'h> Context<'h> {
pub fn new(history: &'h History) -> Self {
Context {
history,
history_index: history.len(),
}
}
pub fn history(&self) -> &History {
&self.history
}
pub fn history_index(&self) -> usize {
self.history_index
}
}
pub struct Editor<H: Helper> {
term: Terminal,
history: History,
helper: Option<H>,
kill_ring: Arc<Mutex<KillRing>>,
config: Config,
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
}
#[allow(clippy::new_without_default)]
impl<H: Helper> Editor<H> {
pub fn new() -> Self {
Self::with_config(Config::default())
}
pub fn with_config(config: Config) -> Self {
let term = Terminal::new(
config.color_mode(),
config.output_stream(),
config.tab_stop(),
config.bell_style(),
);
Self {
term,
history: History::with_config(config),
helper: None,
kill_ring: Arc::new(Mutex::new(KillRing::new(60))),
config,
custom_bindings: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn readline(&mut self, prompt: &str) -> Result<String> {
self.readline_with(prompt, None)
}
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");
let mut stdout = io::stdout();
stdout.write_all(prompt.as_bytes())?;
stdout.flush()?;
readline_direct()
} else if self.term.is_stdin_tty() {
readline_raw(prompt, initial, self)
} else {
debug!(target: "rustyline", "stdin is not a tty");
readline_direct()
}
}
pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
self.history.load(path)
}
pub fn save_history<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> {
self.history.save(path)
}
pub fn add_history_entry<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool {
self.history.add(line)
}
pub fn clear_history(&mut self) {
self.history.clear()
}
pub fn history_mut(&mut self) -> &mut History {
&mut self.history
}
pub fn history(&self) -> &History {
&self.history
}
pub fn set_helper(&mut self, helper: Option<H>) {
self.helper = helper;
}
pub fn helper_mut(&mut self) -> Option<&mut H> {
self.helper.as_mut()
}
pub fn helper(&self) -> Option<&H> {
self.helper.as_ref()
}
pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.insert(key_seq, cmd)
} else {
None
}
}
pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.remove(&key_seq)
} else {
None
}
}
pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter<'_, H> {
Iter {
editor: self,
prompt,
}
}
fn reset_kill_ring(&self) {
let mut kill_ring = self.kill_ring.lock().unwrap();
kill_ring.reset();
}
pub fn dimensions(&mut self) -> Option<(usize, usize)> {
if self.term.is_output_tty() {
let out = self.term.create_writer();
Some((out.get_columns(), out.get_rows()))
} else {
None
}
}
}
impl<H: Helper> config::Configurer for Editor<H> {
fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
fn set_max_history_size(&mut self, max_size: usize) {
self.config_mut().set_max_history_size(max_size);
self.history.set_max_len(max_size);
}
fn set_history_ignore_dups(&mut self, yes: bool) {
self.config_mut().set_history_ignore_dups(yes);
self.history.ignore_dups = yes;
}
fn set_history_ignore_space(&mut self, yes: bool) {
self.config_mut().set_history_ignore_space(yes);
self.history.ignore_space = yes;
}
fn set_color_mode(&mut self, color_mode: ColorMode) {
self.config_mut().set_color_mode(color_mode);
self.term.color_mode = color_mode;
}
}
impl<H: Helper> fmt::Debug for Editor<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Editor")
.field("term", &self.term)
.field("config", &self.config)
.finish()
}
}
pub struct Iter<'a, H: Helper> {
editor: &'a mut Editor<H>,
prompt: &'a str,
}
impl<'a, H: Helper> Iterator for Iter<'a, H> {
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)]
#[macro_use]
extern crate assert_matches;
#[cfg(test)]
mod test;
#[cfg(doctest)]
doc_comment::doctest!("../README.md");