#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "custom-bindings")]
mod binding;
mod command;
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::fmt;
use std::io::{self, BufRead, Write};
use std::path::Path;
use std::result;
use std::sync::{Arc, Mutex};
use log::debug;
use unicode_width::UnicodeWidthStr;
use crate::tty::{RawMode, RawReader, Renderer, Term, Terminal};
#[cfg(feature = "custom-bindings")]
pub use crate::binding::{ConditionalEventHandler, Event, EventContext, EventHandler};
use crate::completion::{longest_common_prefix, Candidate, Completer};
pub use crate::config::{Behavior, ColorMode, CompletionType, Config, EditMode, HistoryDuplicates};
use crate::edit::State;
use crate::error::ReadlineError;
use crate::highlight::Highlighter;
use crate::hint::Hinter;
use crate::history::{History, SearchDirection};
pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word};
use crate::keymap::{Bindings, InputState, Refresher};
pub use crate::keys::{KeyCode, KeyEvent, Modifiers};
use crate::kill_ring::KillRing;
pub use crate::tty::ExternalPrinter;
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::prelude::{
unbounded, Skim, SkimItem, SkimItemReceiver, SkimItemSender, 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);
} else {
s.line.update(&backup, backup_pos);
}
s.refresh_line()?;
cmd = s.next_cmd(input_state, rdr, true, 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, 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_str())?;
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, true)?;
}
matches!(cmd, Cmd::SelfInsert(1, 'y' | 'Y'))
} 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"))]
{
use std::borrow::Cow;
if CompletionType::Fuzzy == config.completion_type() {
struct Candidate {
index: usize,
text: String,
}
impl SkimItem for Candidate {
fn text(&self) -> Cow<str> {
Cow::Borrowed(&self.text)
}
}
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
candidates
.iter()
.enumerate()
.map(|(i, c)| Candidate {
index: i,
text: c.display().to_owned(),
})
.for_each(|c| {
let _ = tx_item.send(Arc::new(c));
});
drop(tx_item); let options = SkimOptionsBuilder::default()
.height(Some("20%"))
.prompt(Some("? "))
.reverse(true)
.build()
.unwrap();
let selected_items = Skim::run_with(&options, Some(rx_item))
.map(|out| out.selected_items)
.unwrap_or_else(Vec::new);
if let Some(item) = selected_items.first() {
let item: &Candidate = (*item).as_any() .downcast_ref::<Candidate>() .expect("something wrong with downcast");
if let Some(candidate) = candidates.get(item.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 let Some(text) = hint.completion() {
if s.line.yank(text, 1).is_none() {
s.out.beep()?;
}
} else {
s.out.beep()?;
}
s.refresh_line()
}
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("\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::Newline
&& !matches!(cmd, Cmd::AcceptOrInsertLine { .. })
{
cmd = s.next_cmd(input_state, rdr, false, true)?;
}
match cmd {
Cmd::SelfInsert(1, 'y' | 'Y' | ' ') => {
pause_row += s.out.get_rows() - 1;
}
Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } => {
pause_row += 1;
}
_ => break,
}
}
s.out.write_and_flush("\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_str())?;
}
s.out.write_and_flush("\n")?;
s.layout.end.row = 0; s.layout.cursor.row = 0;
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 = SearchDirection::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, 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 = SearchDirection::Reverse;
if history_idx > 0 {
history_idx -= 1;
} else {
success = false;
continue;
}
}
Cmd::ForwardSearchHistory => {
direction = SearchDirection::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(sr) => {
history_idx = sr.idx;
s.line.update(sr.entry, sr.pos);
true
}
_ => false,
};
}
s.changes.borrow_mut().end();
Ok(Some(cmd))
}
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 apply_backspace_direct(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut grapheme_sizes: Vec<u8> = Vec::with_capacity(input.len());
for g in unicode_segmentation::UnicodeSegmentation::graphemes(input, true) {
if g == "\u{0008}" {
if let Some(n) = grapheme_sizes.pop() {
out.truncate(out.len() - n as usize);
}
} else {
out.push_str(g);
grapheme_sizes.push(g.len() as u8);
}
}
out
}
fn readline_direct(
mut reader: impl BufRead,
mut writer: impl Write,
validator: &Option<impl Validator>,
) -> Result<String> {
let mut input = String::new();
loop {
if reader.read_line(&mut input)? == 0 {
return Err(error::ReadlineError::Eof);
}
let trailing_n = input.ends_with('\n');
let trailing_r;
if trailing_n {
input.pop();
trailing_r = input.ends_with('\r');
if trailing_r {
input.pop();
}
} else {
trailing_r = false;
}
input = apply_backspace_direct(&input);
match validator.as_ref() {
None => return Ok(input),
Some(v) => {
let mut ctx = input.as_str();
let mut ctx = validate::ValidationContext::new(&mut ctx);
match v.validate(&mut ctx)? {
validate::ValidationResult::Valid(msg) => {
if let Some(msg) = msg {
writer.write_all(msg.as_bytes())?;
}
return Ok(input);
}
validate::ValidationResult::Invalid(Some(msg)) => {
writer.write_all(msg.as_bytes())?;
}
validate::ValidationResult::Incomplete => {
if trailing_r {
input.push('\r');
}
if trailing_n {
input.push('\n');
}
}
_ => {}
}
}
}
}
}
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> {
#[must_use]
pub fn new(history: &'h History) -> Self {
Context {
history,
history_index: history.len(),
}
}
#[must_use]
pub fn history(&self) -> &History {
self.history
}
#[must_use]
pub fn history_index(&self) -> usize {
self.history_index
}
}
#[must_use]
pub struct Editor<H: Helper> {
term: Terminal,
history: History,
helper: Option<H>,
kill_ring: Arc<Mutex<KillRing>>,
config: Config,
custom_bindings: Bindings,
}
#[allow(clippy::new_without_default)]
impl<H: Helper> Editor<H> {
pub fn new() -> Result<Self> {
Self::with_config(Config::default())
}
pub fn with_config(config: Config) -> Result<Self> {
let term = Terminal::new(
config.color_mode(),
config.behavior(),
config.tab_stop(),
config.bell_style(),
config.enable_bracketed_paste(),
)?;
Ok(Self {
term,
history: History::with_config(config),
helper: None,
kill_ring: Arc::new(Mutex::new(KillRing::new(60))),
config,
custom_bindings: Bindings::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(io::stdin().lock(), io::stderr(), &self.helper)
} else if self.term.is_input_tty() {
let (original_mode, term_key_map) = self.term.enable_raw_mode()?;
let guard = Guard(&original_mode);
let user_input = self.readline_edit(prompt, initial, &original_mode, term_key_map);
if self.config.auto_add_history() {
if let Ok(ref line) = user_input {
self.add_history_entry(line.as_str());
}
}
drop(guard); self.term.writeln()?;
user_input
} else {
debug!(target: "rustyline", "stdin is not a tty");
readline_direct(io::stdin().lock(), io::stderr(), &self.helper)
}
}
fn readline_edit(
&mut self,
prompt: &str,
initial: Option<(&str, &str)>,
original_mode: &tty::Mode,
term_key_map: tty::KeyMap,
) -> Result<String> {
let mut stdout = self.term.create_writer();
self.reset_kill_ring(); let ctx = Context::new(&self.history);
let mut s = State::new(&mut stdout, prompt, self.helper.as_ref(), ctx);
let mut input_state = InputState::new(&self.config, &self.custom_bindings);
s.line.set_delete_listener(self.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 = self.term.create_reader(&self.config, term_key_map);
if self.term.is_output_tty() && self.config.check_cursor_position() {
if let Err(e) = s.move_cursor_at_leftmost(&mut rdr) {
if let ReadlineError::WindowResized = e {
s.out.update_size();
} else {
return Err(e);
}
}
}
s.refresh_line()?;
loop {
let mut cmd = s.next_cmd(&mut input_state, &mut rdr, false, false)?;
if cmd.should_reset_kill_ring() {
self.reset_kill_ring();
}
if cmd == Cmd::Complete && s.helper.is_some() {
let next = complete_line(&mut rdr, &mut s, &mut input_state, &self.config)?;
if let Some(next) = next {
cmd = next;
} else {
continue;
}
}
if cmd == Cmd::ReverseSearchHistory {
let next =
reverse_incremental_search(&mut rdr, &mut s, &mut input_state, &self.history)?;
if let Some(next) = next {
cmd = next;
} else {
continue;
}
}
#[cfg(unix)]
if cmd == Cmd::Suspend {
original_mode.disable_raw_mode()?;
tty::suspend()?;
let _ = self.term.enable_raw_mode()?; s.out.update_size(); s.refresh_line()?;
continue;
}
#[cfg(unix)]
if cmd == Cmd::QuotedInsert {
let c = rdr.next_char()?;
s.edit_insert(c, 1)?;
continue;
}
#[cfg(windows)]
if cmd == Cmd::PasteFromClipboard {
let clipboard = rdr.read_pasted_text()?;
s.edit_yank(&input_state, &clipboard[..], Anchor::Before, 1)?;
}
#[cfg(test)]
if matches!(
cmd,
Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. }
) {
self.term.cursor = s.layout.cursor.col;
}
match command::execute(cmd, &mut s, &input_state, &self.kill_ring, &self.config)? {
command::Status::Proceed => continue,
command::Status::Submit => break,
}
}
s.edit_move_buffer_end()?;
if cfg!(windows) {
let _ = original_mode; }
Ok(s.line.into_string())
}
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>(&mut self, path: &P) -> Result<()> {
self.history.save(path)
}
pub fn append_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
self.history.append(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()
}
#[cfg(feature = "custom-bindings")]
#[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))]
pub fn bind_sequence<E: Into<Event>, R: Into<EventHandler>>(
&mut self,
key_seq: E,
handler: R,
) -> Option<EventHandler> {
self.custom_bindings
.insert(Event::normalize(key_seq.into()), handler.into())
}
#[cfg(feature = "custom-bindings")]
#[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))]
pub fn unbind_sequence<E: Into<Event>>(&mut self, key_seq: E) -> Option<EventHandler> {
self.custom_bindings
.remove(&Event::normalize(key_seq.into()))
}
pub fn iter<'a>(&'a mut self, prompt: &'a str) -> impl Iterator<Item = Result<String>> + 'a {
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
}
}
pub fn create_external_printer(&mut self) -> Result<<Terminal as Term>::ExternalPrinter> {
self.term.create_external_printer()
}
}
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()
}
}
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");