use std::cmp;
use std::fmt::{self, Write};
use std::io;
use strip_ansi_escapes::strip;
use termion::{self, clear, color, cursor};
use super::complete::Completer;
use crate::context::ColorClosure;
use crate::event::*;
use crate::util;
use crate::Buffer;
use crate::Context;
use itertools::Itertools;
#[derive(Clone, Copy, Debug)]
pub enum ViPromptMode {
Normal,
Insert,
}
#[derive(Debug)]
pub struct ViStatus {
pub mode: ViPromptMode,
normal: String,
insert: String,
}
impl ViStatus {
pub fn new<N, I>(mode: ViPromptMode, normal: N, insert: I) -> Self
where
N: Into<String>,
I: Into<String>,
{
Self {
mode,
normal: normal.into(),
insert: insert.into(),
}
}
pub fn as_str(&self) -> &str {
use ViPromptMode::*;
match self.mode {
Normal => &self.normal,
Insert => &self.insert,
}
}
}
impl Default for ViStatus {
fn default() -> Self {
ViStatus {
mode: ViPromptMode::Insert,
normal: String::from("[N] "),
insert: String::from("[I] "),
}
}
}
impl fmt::Display for ViStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ViPromptMode::*;
match self.mode {
Normal => write!(f, "{}", self.normal),
Insert => write!(f, "{}", self.insert),
}
}
}
pub struct Prompt {
pub prompt: String,
pub vi_status: Option<ViStatus>,
}
impl Prompt {
pub fn from<P: Into<String>>(prompt: P) -> Self {
Prompt {
prompt: prompt.into(),
vi_status: None,
}
}
pub fn prefix(&self) -> &str {
match &self.vi_status {
Some(status) => status.as_str(),
None => "",
}
}
}
impl fmt::Display for Prompt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(status) = &self.vi_status {
write!(f, "{}", status)?
}
write!(f, "{}", self.prompt)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorPosition {
InWord(usize),
OnWordLeftEdge(usize),
OnWordRightEdge(usize),
InSpace(Option<usize>, Option<usize>),
}
impl CursorPosition {
pub fn get(cursor: usize, words: &[(usize, usize)]) -> CursorPosition {
use CursorPosition::*;
if words.is_empty() {
return InSpace(None, None);
} else if cursor == words[0].0 {
return OnWordLeftEdge(0);
} else if cursor < words[0].0 {
return InSpace(None, Some(0));
}
for (i, &(start, end)) in words.iter().enumerate() {
if start == cursor {
return OnWordLeftEdge(i);
} else if end == cursor {
return OnWordRightEdge(i);
} else if start < cursor && cursor < end {
return InWord(i);
} else if cursor < start {
return InSpace(Some(i - 1), Some(i));
}
}
InSpace(Some(words.len() - 1), None)
}
}
pub struct Editor<'a, W: io::Write> {
prompt: Prompt,
out: W,
context: &'a mut Context,
closure: Option<ColorClosure>,
cursor: usize,
new_buf: Buffer,
hist_buf: Buffer,
hist_buf_valid: bool,
cur_history_loc: Option<usize>,
term_cursor_line: usize,
show_completions_hint: Option<(Vec<String>, Option<usize>)>,
show_autosuggestions: bool,
pub no_eol: bool,
reverse_search: bool,
forward_search: bool,
buffer_changed: bool,
history_subset_index: Vec<usize>,
history_subset_loc: Option<usize>,
autosuggestion: Option<Buffer>,
history_fresh: bool,
}
macro_rules! cur_buf_mut {
($s:expr) => {{
$s.buffer_changed = true;
match $s.cur_history_loc {
Some(i) => {
if !$s.hist_buf_valid {
$s.hist_buf.copy_buffer(&$s.context.history[i]);
$s.hist_buf_valid = true;
}
&mut $s.hist_buf
}
_ => &mut $s.new_buf,
}
}};
}
macro_rules! cur_buf {
($s:expr) => {
match $s.cur_history_loc {
Some(_) if $s.hist_buf_valid => &$s.hist_buf,
Some(i) => &$s.context.history[i],
_ => &$s.new_buf,
}
};
}
impl<'a, W: io::Write> Editor<'a, W> {
pub fn new(
out: W,
prompt: Prompt,
f: Option<ColorClosure>,
context: &'a mut Context,
) -> io::Result<Self> {
Editor::new_with_init_buffer(out, prompt, f, context, Buffer::new())
}
pub fn new_with_init_buffer<B: Into<Buffer>>(
mut out: W,
prompt: Prompt,
f: Option<ColorClosure>,
context: &'a mut Context,
buffer: B,
) -> io::Result<Self> {
out.write_all("⏎".as_bytes())?;
for _ in 0..(util::terminal_width().unwrap_or(80) - 1) {
out.write_all(b" ")?; }
out.write_all("\r \r".as_bytes())?; let Prompt {
mut prompt,
vi_status,
} = prompt;
out.write_all(prompt.split('\n').join("\r\n").as_bytes())?;
if let Some(index) = prompt.rfind('\n') {
prompt = prompt.split_at(index + 1).1.into()
}
let prompt = Prompt { prompt, vi_status };
let mut ed = Editor {
prompt,
cursor: 0,
out,
closure: f,
new_buf: buffer.into(),
hist_buf: Buffer::new(),
hist_buf_valid: false,
cur_history_loc: None,
context,
show_completions_hint: None,
show_autosuggestions: true,
term_cursor_line: 1,
no_eol: false,
reverse_search: false,
forward_search: false,
buffer_changed: false,
history_subset_index: vec![],
history_subset_loc: None,
autosuggestion: None,
history_fresh: false,
};
if !ed.new_buf.is_empty() {
ed.move_cursor_to_end_of_line()?;
}
ed.display()?;
Ok(ed)
}
fn is_search(&self) -> bool {
self.reverse_search || self.forward_search
}
fn clear_search(&mut self) {
self.reverse_search = false;
self.forward_search = false;
self.history_subset_loc = None;
self.history_subset_index.clear();
}
pub fn current_history_location(&self) -> Option<usize> {
self.cur_history_loc
}
pub fn get_words_and_cursor_position(&self) -> (Vec<(usize, usize)>, CursorPosition) {
let word_fn = &self.context.word_divider_fn;
let words = word_fn(cur_buf!(self));
let pos = CursorPosition::get(self.cursor, &words);
(words, pos)
}
pub fn set_prompt(&mut self, mut prompt: Prompt) {
if let Some(passed_status) = &mut prompt.vi_status {
if let Some(old_status) = &self.prompt.vi_status {
passed_status.mode = old_status.mode;
}
}
self.prompt = prompt;
}
pub fn context(&mut self) -> &mut Context {
self.context
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn handle_newline(&mut self) -> io::Result<bool> {
self.history_fresh = false;
if self.is_search() {
self.accept_autosuggestion()?;
}
self.clear_search();
if self.show_completions_hint.is_some() {
self.show_completions_hint = None;
return Ok(false);
}
let char_before_cursor = cur_buf!(self).char_before(self.cursor);
if char_before_cursor == Some('\\') {
self.insert_after_cursor('\n')?;
Ok(false)
} else {
self.cursor = cur_buf!(self).num_chars();
self._display(false)?;
self.out.write_all(b"\r\n")?;
self.show_completions_hint = None;
Ok(true)
}
}
fn search_history_loc(&self) -> Option<usize> {
self.history_subset_loc
.and_then(|i| self.history_subset_index.get(i).cloned())
}
fn freshen_history(&mut self) {
if self.context.history.share && !self.history_fresh {
let _ = self.context.history.load_history(false);
self.history_fresh = true;
}
}
fn refresh_search(&mut self, forward: bool) {
let search_history_loc = self.search_history_loc();
self.history_subset_index = self.context.history.search_index(&self.new_buf);
if !self.history_subset_index.is_empty() {
self.history_subset_loc = if forward {
Some(0)
} else {
Some(self.history_subset_index.len() - 1)
};
if let Some(target_loc) = search_history_loc {
for (i, history_loc) in self.history_subset_index.iter().enumerate() {
if target_loc <= *history_loc {
if forward || target_loc == *history_loc || i == 0 {
self.history_subset_loc = Some(i);
} else {
self.history_subset_loc = Some(i - 1);
}
break;
}
}
}
} else {
self.history_subset_loc = None;
}
self.reverse_search = !forward;
self.forward_search = forward;
self.cur_history_loc = None;
self.hist_buf_valid = false;
self.buffer_changed = false;
}
pub fn search(&mut self, forward: bool) -> io::Result<()> {
if !self.is_search() {
self.freshen_history();
self.refresh_search(forward);
} else if !self.history_subset_index.is_empty() {
self.history_subset_loc = if let Some(p) = self.history_subset_loc {
if forward {
if p < self.history_subset_index.len() - 1 {
Some(p + 1)
} else {
Some(0)
}
} else if p > 0 {
Some(p - 1)
} else {
Some(self.history_subset_index.len() - 1)
}
} else {
None
};
}
self.display()?;
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
self.out.flush()
}
pub fn undo(&mut self) -> io::Result<bool> {
let did = cur_buf_mut!(self).undo();
if did {
self.move_cursor_to_end_of_line()?;
} else {
self.display()?;
}
Ok(did)
}
pub fn redo(&mut self) -> io::Result<bool> {
let did = cur_buf_mut!(self).redo();
if did {
self.move_cursor_to_end_of_line()?;
} else {
self.display()?;
}
Ok(did)
}
pub fn revert(&mut self) -> io::Result<bool> {
let did = cur_buf_mut!(self).revert();
if did {
self.move_cursor_to_end_of_line()?;
} else {
self.display()?;
}
Ok(did)
}
fn print_completion_list(
completions: &[String],
highlighted: Option<usize>,
output_buf: &mut String,
) -> io::Result<usize> {
use std::cmp::max;
let (w, _) = termion::terminal_size()?;
let max_word_size = completions.iter().fold(1, |m, x| max(m, x.chars().count()));
let cols = max(1, w as usize / (max_word_size));
let col_width = 2 + w as usize / cols;
let cols = max(1, w as usize / col_width);
let lines = completions.len() / cols;
let mut i = 0;
for (index, com) in completions.iter().enumerate() {
if i == cols {
output_buf.push_str("\r\n");
i = 0;
} else if i > cols {
unreachable!()
}
if Some(index) == highlighted {
write!(
output_buf,
"{}{}",
color::Black.fg_str(),
color::White.bg_str()
);
}
write!(output_buf, "{:<1$}", com, col_width);
if Some(index) == highlighted {
write!(
output_buf,
"{}{}",
color::Reset.bg_str(),
color::Reset.fg_str()
);
}
i += 1;
}
Ok(lines)
}
pub fn skip_completions_hint(&mut self) {
self.show_completions_hint = None;
}
pub fn complete<T: Completer>(&mut self, handler: &mut T) -> io::Result<()> {
handler.on_event(Event::new(self, EventKind::BeforeComplete));
if let Some((completions, i_in)) = self.show_completions_hint.take() {
let i = i_in.map_or(0, |i| (i + 1) % completions.len());
match i_in {
Some(x) if cur_buf!(self) == &Buffer::from(&completions[x][..]) => {
cur_buf_mut!(self).truncate(0);
self.cursor = 0;
}
_ => self.delete_word_before_cursor(false)?,
}
self.insert_str_after_cursor(&completions[i])?;
self.show_completions_hint = Some((completions, Some(i)));
}
if self.show_completions_hint.is_some() {
self.display()?;
return Ok(());
}
let (word, completions) = {
let word_range = self.get_word_before_cursor(false);
let buf = cur_buf_mut!(self);
let word = match word_range {
Some((start, end)) => buf.range(start, end),
None => "".into(),
};
let mut completions = handler.completions(word.as_ref());
completions.sort();
completions.dedup();
(word, completions)
};
if completions.is_empty() {
self.show_completions_hint = None;
Ok(())
} else if completions.len() == 1 {
self.show_completions_hint = None;
self.delete_word_before_cursor(false)?;
self.insert_str_after_cursor(completions[0].as_ref())
} else {
let common_prefix = util::find_longest_common_prefix(
&completions
.iter()
.map(|x| x.chars().collect())
.collect::<Vec<Vec<char>>>()[..],
);
if let Some(p) = common_prefix {
let s = p.iter().cloned().collect::<String>();
if s.len() > word.len() && s.starts_with(&word[..]) {
self.delete_word_before_cursor(false)?;
return self.insert_str_after_cursor(s.as_ref());
}
}
self.show_completions_hint = Some((completions, None));
self.display()?;
Ok(())
}
}
fn get_word_before_cursor(&self, ignore_space_before_cursor: bool) -> Option<(usize, usize)> {
let (words, pos) = self.get_words_and_cursor_position();
match pos {
CursorPosition::InWord(i) => Some(words[i]),
CursorPosition::InSpace(Some(i), _) => {
if ignore_space_before_cursor {
Some(words[i])
} else {
None
}
}
CursorPosition::InSpace(None, _) => None,
CursorPosition::OnWordLeftEdge(i) => {
if ignore_space_before_cursor && i > 0 {
Some(words[i - 1])
} else {
None
}
}
CursorPosition::OnWordRightEdge(i) => Some(words[i]),
}
}
pub fn delete_word_before_cursor(
&mut self,
ignore_space_before_cursor: bool,
) -> io::Result<()> {
if let Some((start, _)) = self.get_word_before_cursor(ignore_space_before_cursor) {
let moved = cur_buf_mut!(self).remove(start, self.cursor);
self.cursor -= moved;
}
self.display()
}
pub fn clear(&mut self) -> io::Result<()> {
write!(
&mut self.context.buf,
"{}{}",
clear::All,
cursor::Goto(1, 1)
);
self.term_cursor_line = 1;
self.clear_search();
self.display()
}
pub fn move_up(&mut self) -> io::Result<()> {
if self.is_search() {
self.search(false)
} else {
self.hist_buf_valid = false;
self.freshen_history();
if self.new_buf.num_chars() > 0 {
match self.history_subset_loc {
Some(i) if i > 0 => {
self.history_subset_loc = Some(i - 1);
self.cur_history_loc = Some(self.history_subset_index[i - 1]);
}
None => {
self.history_subset_index =
self.context.history.get_history_subset(&self.new_buf);
if !self.history_subset_index.is_empty() {
self.history_subset_loc = Some(self.history_subset_index.len() - 1);
self.cur_history_loc = Some(
self.history_subset_index[self.history_subset_index.len() - 1],
);
}
}
_ => (),
}
} else {
match self.cur_history_loc {
Some(i) if i > 0 => self.cur_history_loc = Some(i - 1),
None if !self.context.history.is_empty() => {
self.cur_history_loc = Some(self.context.history.len() - 1)
}
_ => (),
}
}
self.move_cursor_to_end_of_line()
}
}
pub fn move_down(&mut self) -> io::Result<()> {
if self.is_search() {
self.search(true)
} else {
self.hist_buf_valid = false;
if self.new_buf.num_chars() > 0 {
if let Some(i) = self.history_subset_loc {
if i < self.history_subset_index.len() - 1 {
self.history_subset_loc = Some(i + 1);
self.cur_history_loc = Some(self.history_subset_index[i + 1]);
} else {
self.cur_history_loc = None;
self.history_subset_loc = None;
self.history_subset_index.clear();
self.history_fresh = false;
}
}
} else {
match self.cur_history_loc.take() {
Some(i) if i < self.context.history.len() - 1 => {
self.cur_history_loc = Some(i + 1)
}
_ => self.history_fresh = false,
}
}
self.move_cursor_to_end_of_line()
}
}
pub fn move_to_start_of_history(&mut self) -> io::Result<()> {
self.hist_buf_valid = false;
if self.context.history.is_empty() {
self.cur_history_loc = None;
self.display()
} else {
self.cur_history_loc = Some(0);
self.move_cursor_to_end_of_line()
}
}
pub fn move_to_end_of_history(&mut self) -> io::Result<()> {
self.hist_buf_valid = false;
if self.cur_history_loc.is_some() {
self.cur_history_loc = None;
self.move_cursor_to_end_of_line()
} else {
self.display()
}
}
pub fn insert_str_after_cursor(&mut self, s: &str) -> io::Result<()> {
self.insert_chars_after_cursor(&s.chars().collect::<Vec<char>>()[..])
}
pub fn insert_after_cursor(&mut self, c: char) -> io::Result<()> {
self.insert_chars_after_cursor(&[c])
}
pub fn insert_chars_after_cursor(&mut self, cs: &[char]) -> io::Result<()> {
{
let buf = cur_buf_mut!(self);
buf.insert(self.cursor, cs);
}
self.cursor += cs.len();
self.display()
}
pub fn delete_before_cursor(&mut self) -> io::Result<()> {
if self.cursor > 0 {
let buf = cur_buf_mut!(self);
buf.remove(self.cursor - 1, self.cursor);
self.cursor -= 1;
}
self.display()
}
pub fn delete_after_cursor(&mut self) -> io::Result<()> {
{
let buf = cur_buf_mut!(self);
if self.cursor < buf.num_chars() {
buf.remove(self.cursor, self.cursor + 1);
}
}
self.display()
}
pub fn delete_all_before_cursor(&mut self) -> io::Result<()> {
cur_buf_mut!(self).remove(0, self.cursor);
self.cursor = 0;
self.display()
}
pub fn delete_all_after_cursor(&mut self) -> io::Result<()> {
{
let buf = cur_buf_mut!(self);
buf.truncate(self.cursor);
}
self.display()
}
pub fn delete_until(&mut self, position: usize) -> io::Result<()> {
{
let buf = cur_buf_mut!(self);
buf.remove(
cmp::min(self.cursor, position),
cmp::max(self.cursor, position),
);
self.cursor = cmp::min(self.cursor, position);
}
self.display()
}
pub fn delete_until_inclusive(&mut self, position: usize) -> io::Result<()> {
{
let buf = cur_buf_mut!(self);
buf.remove(
cmp::min(self.cursor, position),
cmp::max(self.cursor + 1, position + 1),
);
self.cursor = cmp::min(self.cursor, position);
}
self.display()
}
pub fn move_cursor_left(&mut self, mut count: usize) -> io::Result<()> {
if count > self.cursor {
count = self.cursor;
}
self.cursor -= count;
self.display()
}
pub fn move_cursor_right(&mut self, mut count: usize) -> io::Result<()> {
{
let buf = cur_buf!(self);
if count > buf.num_chars() - self.cursor {
count = buf.num_chars() - self.cursor;
}
self.cursor += count;
}
self.display()
}
pub fn move_cursor_to(&mut self, pos: usize) -> io::Result<()> {
self.cursor = pos;
let buf_len = cur_buf!(self).num_chars();
if self.cursor > buf_len {
self.cursor = buf_len;
}
self.display()
}
pub fn move_cursor_to_start_of_line(&mut self) -> io::Result<()> {
self.cursor = 0;
self.display()
}
pub fn move_cursor_to_end_of_line(&mut self) -> io::Result<()> {
self.cursor = cur_buf!(self).num_chars();
self.display()
}
pub fn cursor_is_at_end_of_line(&self) -> bool {
let num_chars = cur_buf!(self).num_chars();
if self.no_eol {
self.cursor == num_chars - 1
} else {
self.cursor == num_chars
}
}
pub fn current_buffer(&self) -> &Buffer {
cur_buf!(self)
}
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
cur_buf_mut!(self)
}
pub fn accept_autosuggestion(&mut self) -> io::Result<()> {
if self.show_autosuggestions {
{
let autosuggestion = self.autosuggestion.clone();
let search = self.is_search();
let buf = self.current_buffer_mut();
match autosuggestion {
Some(ref x) if search => buf.copy_buffer(x),
Some(ref x) => buf.insert_from_buffer(x),
None => (),
}
}
}
self.clear_search();
self.move_cursor_to_end_of_line()
}
fn current_autosuggestion(&mut self) -> Option<Buffer> {
if self.hist_buf_valid {
return None;
}
let context_history = &self.context.history;
let autosuggestion = if self.is_search() {
self.search_history_loc().map(|i| &context_history[i])
} else if self.show_autosuggestions {
self.cur_history_loc
.map(|i| &context_history[i])
.or_else(|| {
context_history
.get_newest_match(Some(context_history.len()), &self.new_buf)
.map(|i| &context_history[i])
})
} else {
None
};
autosuggestion.cloned()
}
pub fn is_currently_showing_autosuggestion(&self) -> bool {
self.autosuggestion.is_some()
}
fn search_prompt(&mut self) -> (String, usize) {
if self.is_search() {
let (hplace, color) = if self.history_subset_index.is_empty() {
(0, color::Red.fg_str())
} else {
(
self.history_subset_loc.unwrap_or(0) + 1,
color::Green.fg_str(),
)
};
let prefix = self.prompt.prefix();
(
format!(
"{}(search)'{}{}{}` ({}/{}): ",
&prefix,
color,
self.current_buffer(),
color::Reset.fg_str(),
hplace,
self.history_subset_index.len()
),
strip(&prefix).unwrap().len() + 9,
)
} else {
(self.prompt.to_string(), 0)
}
}
fn _display(&mut self, show_autosuggest: bool) -> io::Result<()> {
fn calc_width(prompt_width: usize, buf_widths: &[usize], terminal_width: usize) -> usize {
let mut total = 0;
for line in buf_widths {
if total % terminal_width != 0 {
total = ((total / terminal_width) + 1) * terminal_width;
}
total += prompt_width + line;
}
total
}
let (prompt, rev_prompt_width) = self.search_prompt();
let terminal_width = util::terminal_width()?;
let prompt_width = util::last_prompt_line_width(&prompt);
let buf = cur_buf!(self);
let buf_width = buf.width();
let buf_num_chars = buf.num_chars();
if buf_num_chars < self.cursor {
self.cursor = buf_num_chars;
}
if self.no_eol && self.cursor != 0 && self.cursor == buf_num_chars {
self.cursor -= 1;
}
let buf_widths = match self.autosuggestion {
Some(ref suggestion) => suggestion.width(),
None => buf_width,
};
let buf_widths_to_cursor = match self.autosuggestion {
Some(ref suggestion) if self.cursor < suggestion.num_chars() => {
suggestion.range_width(0, self.cursor)
}
_ => buf.range_width(0, self.cursor),
};
let new_total_width = calc_width(prompt_width, &buf_widths, terminal_width);
let new_total_width_to_cursor = if self.is_search() {
calc_width(rev_prompt_width, &buf_widths_to_cursor, terminal_width)
} else {
calc_width(prompt_width, &buf_widths_to_cursor, terminal_width)
};
let new_num_lines = (new_total_width + terminal_width) / terminal_width;
self.context.buf.push_str("\x1B[?1000l\x1B[?1l");
if self.term_cursor_line > 1 {
write!(
&mut self.context.buf,
"{}",
cursor::Up(self.term_cursor_line as u16 - 1)
);
}
write!(&mut self.context.buf, "\r{}", clear::AfterCursor);
let mut completion_lines = 0;
if let Some((completions, i)) = self.show_completions_hint.as_ref() {
completion_lines =
1 + Self::print_completion_list(completions, *i, &mut self.context.buf)?;
self.context.buf.push_str("\r\n");
}
write!(&mut self.context.buf, "{}", prompt);
let lines = match self.autosuggestion {
Some(ref suggestion) if show_autosuggest => suggestion.lines(),
_ => buf.lines(),
};
let mut buf_num_remaining_bytes = buf.num_bytes();
let lines_len = lines.len();
for (i, line) in lines.into_iter().enumerate() {
if i > 0 {
write!(
&mut self.context.buf,
"{}",
cursor::Right(prompt_width as u16)
);
}
if buf_num_remaining_bytes == 0 {
self.context.buf.push_str(&line);
} else if line.len() > buf_num_remaining_bytes {
let start = &line[..buf_num_remaining_bytes];
let start = match self.closure {
Some(ref f) => f(start),
None => start.to_owned(),
};
if self.is_search() {
write!(&mut self.context.buf, "{}", color::Yellow.fg_str());
}
write!(&mut self.context.buf, "{}", start);
if !self.is_search() {
write!(&mut self.context.buf, "{}", color::Yellow.fg_str());
}
self.context.buf.push_str(&line[buf_num_remaining_bytes..]);
buf_num_remaining_bytes = 0;
} else {
buf_num_remaining_bytes -= line.len();
let written_line = match self.closure {
Some(ref f) => f(&line),
None => line,
};
if self.is_search() {
write!(&mut self.context.buf, "{}", color::Yellow.fg_str());
}
self.context.buf.push_str(&written_line);
}
if i + 1 < lines_len {
self.context.buf.push_str("\r\n");
}
}
if self.is_currently_showing_autosuggestion() || self.is_search() {
write!(&mut self.context.buf, "{}", color::Reset.fg_str());
}
if new_total_width % terminal_width == 0 {
self.context.buf.push_str("\r\n");
}
self.term_cursor_line = (new_total_width_to_cursor + terminal_width) / terminal_width;
let cursor_line_diff = new_num_lines as isize - self.term_cursor_line as isize;
if cursor_line_diff > 0 {
write!(
&mut self.context.buf,
"{}",
cursor::Up(cursor_line_diff as u16)
);
} else if cursor_line_diff < 0 {
unreachable!();
}
let cursor_col_diff = new_total_width as isize
- new_total_width_to_cursor as isize
- cursor_line_diff * terminal_width as isize;
if cursor_col_diff > 0 {
write!(
&mut self.context.buf,
"{}",
cursor::Left(cursor_col_diff as u16)
);
} else if cursor_col_diff < 0 {
write!(
&mut self.context.buf,
"{}",
cursor::Right((-cursor_col_diff) as u16)
);
}
self.term_cursor_line += completion_lines;
{
let out = &mut self.out;
out.write_all(self.context.buf.as_bytes());
self.context.buf.clear();
out.flush()
}
}
pub fn display(&mut self) -> io::Result<()> {
if self.is_search() && self.buffer_changed {
let forward = self.forward_search;
self.refresh_search(forward);
}
self.autosuggestion = self.current_autosuggestion();
self._display(true)
}
pub fn set_vi_mode(&mut self, mode: ViPromptMode) {
if let Some(status) = &mut self.prompt.vi_status {
status.mode = mode;
}
}
}
impl<'a, W: io::Write> From<Editor<'a, W>> for String {
fn from(ed: Editor<'a, W>) -> String {
match ed.cur_history_loc {
Some(i) => {
if ed.hist_buf_valid {
ed.hist_buf
} else {
ed.context.history[i].clone()
}
}
_ => ed.new_buf,
}
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use Context;
#[test]
fn delete_all_after_cursor_undo() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("delete all of this").unwrap();
ed.move_cursor_to_start_of_line().unwrap();
ed.delete_all_after_cursor().unwrap();
ed.undo().unwrap();
assert_eq!(String::from(ed), "delete all of this");
}
#[test]
fn move_cursor_left() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("let").unwrap();
assert_eq!(ed.cursor, 3);
ed.move_cursor_left(1).unwrap();
assert_eq!(ed.cursor, 2);
ed.insert_after_cursor('f').unwrap();
assert_eq!(ed.cursor, 3);
assert_eq!(String::from(ed), "left");
}
#[test]
fn cursor_movement() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("right").unwrap();
assert_eq!(ed.cursor, 5);
ed.move_cursor_left(2).unwrap();
ed.move_cursor_right(1).unwrap();
assert_eq!(ed.cursor, 4);
}
#[test]
fn delete_until_backwards() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("right").unwrap();
assert_eq!(ed.cursor, 5);
ed.delete_until(0).unwrap();
assert_eq!(ed.cursor, 0);
assert_eq!(String::from(ed), "");
}
#[test]
fn delete_until_forwards() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("right").unwrap();
ed.cursor = 0;
ed.delete_until(5).unwrap();
assert_eq!(ed.cursor, 0);
assert_eq!(String::from(ed), "");
}
#[test]
fn delete_until() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("right").unwrap();
ed.cursor = 4;
ed.delete_until(1).unwrap();
assert_eq!(ed.cursor, 1);
assert_eq!(String::from(ed), "rt");
}
#[test]
fn delete_until_inclusive() {
let mut context = Context::new();
let out = Vec::new();
let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap();
ed.insert_str_after_cursor("right").unwrap();
ed.cursor = 4;
ed.delete_until_inclusive(1).unwrap();
assert_eq!(ed.cursor, 1);
assert_eq!(String::from(ed), "r");
}
}