diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 95f6c3c..ae9cc9c 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -25,7 +25,7 @@ fn get_prompt() -> ShResult { pub fn readline(edit_mode: FernEditMode) -> ShResult { let prompt = get_prompt()?; let mut reader: Box = match edit_mode { - FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))), + FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?), FernEditMode::Emacs => todo!() }; reader.readline() diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs new file mode 100644 index 0000000..99d768f --- /dev/null +++ b/src/prompt/readline/history.rs @@ -0,0 +1,350 @@ +use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}}; + +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::prelude::*; + +use super::vicmd::Direction; // surprisingly useful + +#[derive(Default,Clone,Copy,Debug)] +pub enum SearchKind { + Fuzzy, + #[default] + Prefix +} + +#[derive(Default,Clone,Debug)] +pub struct SearchConstraint { + kind: SearchKind, + term: String, +} + +impl SearchConstraint { + pub fn new(kind: SearchKind, term: String) -> Self { + Self { kind, term } + } +} + +#[derive(Debug,Clone)] +pub struct HistEntry { + id: u32, + timestamp: SystemTime, + command: String, + new: bool +} + +impl HistEntry { + pub fn id(&self) -> u32 { + self.id + } + pub fn timestamp(&self) -> &SystemTime { + &self.timestamp + } + pub fn command(&self) -> &str { + &self.command + } + fn with_escaped_newlines(&self) -> String { + let mut escaped = String::new(); + let mut chars = self.command.chars(); + while let Some(ch) = chars.next() { + match ch { + '\\' => { + escaped.push(ch); + if let Some(ch) = chars.next() { + escaped.push(ch) + } + } + '\n' => { + escaped.push_str("\\\n"); + } + _ => escaped.push(ch), + } + } + escaped + } + pub fn is_new(&self) -> bool { + self.new + } +} + +impl FromStr for HistEntry { + type Err = ShErr; + fn from_str(s: &str) -> Result { + let err = Err( + ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] } + ); + + //: 248972349;148;echo foo; echo bar + let Some(cleaned) = s.strip_prefix(": ") else { return err }; + //248972349;148;echo foo; echo bar + let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err }; + //("248972349","148;echo foo; echo bar") + let Some((id,command)) = id_and_command.split_once(';') else { return err }; + //("148","echo foo; echo bar") + let Ok(ts_seconds) = timestamp.parse::() else { return err }; + let Ok(id) = id.parse::() else { return err }; + let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds); + let command = command.to_string(); + Ok(Self { id, timestamp, command, new: false }) + } +} + +impl Display for HistEntry { + /// Similar to zsh's history format, but not entirely + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let command = self.with_escaped_newlines(); + let HistEntry { id, timestamp, command: _, new: _ } = self; + let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs(); + writeln!(f, ": {timestamp};{id};{command}") + } +} + +pub struct HistEntries(Vec); + + +impl FromStr for HistEntries { + type Err = ShErr; + fn from_str(s: &str) -> Result { + let mut entries = vec![]; + + let mut lines = s.lines().enumerate().peekable(); + let mut cur_line = String::new(); + + while let Some((i,line)) = lines.next() { + if !line.starts_with(": ") { + return Err( + ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } + ) + } + let mut chars = line.chars().peekable(); + let mut feeding_lines = true; + while feeding_lines { + feeding_lines = false; + while let Some(ch) = chars.next() { + match ch { + '\\' => { + if let Some(esc_ch) = chars.next() { + cur_line.push(esc_ch); + } else { + cur_line.push('\n'); + feeding_lines = true; + } + } + '\n' => { + break + } + _ => { + cur_line.push(ch); + } + } + } + if feeding_lines { + let Some((_,line)) = lines.next() else { + return Err( + ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } + ) + }; + chars = line.chars().peekable(); + } + } + let entry = cur_line.parse::()?; + entries.push(entry); + cur_line.clear(); + } + + + Ok(Self(entries)) + } +} + +fn read_hist_file(path: &Path) -> ShResult> { + if !path.exists() { + fs::File::create(path)?; + } + let raw = fs::read_to_string(path)?; + Ok(raw.parse::()?.0) +} + +pub struct History { + path: PathBuf, + entries: Vec, + search_mask: Vec, + cursor: usize, + search_direction: Direction, + ignore_dups: bool, + max_size: Option, +} + +impl History { + pub fn new() -> ShResult { + let path = PathBuf::from(env::var("FERNHIST").unwrap_or({ + let home = env::var("HOME").unwrap(); + format!("{home}/.fern_history") + })); + let mut entries = read_hist_file(&path)?; + { + let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0); + let timestamp = SystemTime::now(); + let command = "".into(); + entries.push(HistEntry { id, timestamp, command, new: true }) + } + let search_mask = entries.clone(); + let cursor = entries.len() - 1; + let mut new = Self { + path, + entries, + search_mask, + cursor, + search_direction: Direction::Backward, + ignore_dups: true, + max_size: None, + }; + new.push_empty_entry(); // Current pending command + Ok(new) + } + + pub fn entries(&self) -> &[HistEntry] { + &self.entries + } + + pub fn masked_entries(&self) -> &[HistEntry] { + &self.search_mask + } + + pub fn push_empty_entry(&mut self) { + } + + pub fn cursor_entry(&self) -> Option<&HistEntry> { + self.search_mask.get(self.cursor) + } + + pub fn update_pending_cmd(&mut self, command: &str) { + let Some(ent) = self.last_mut() else { + return + }; + let cmd = command.to_string(); + let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() }; + + + ent.command = cmd; + self.constrain_entries(constraint); + } + + pub fn last_mut(&mut self) -> Option<&mut HistEntry> { + self.entries.last_mut() + } + + pub fn get_new_id(&self) -> u32 { + let Some(ent) = self.entries.last() else { + return 0 + }; + ent.id + 1 + } + + pub fn ignore_dups(&mut self, yn: bool) { + self.ignore_dups = yn + } + + pub fn max_hist_size(&mut self, size: Option) { + self.max_size = size + } + + pub fn constrain_entries(&mut self, constraint: SearchConstraint) { + flog!(DEBUG,constraint); + let SearchConstraint { kind, term } = constraint; + match kind { + SearchKind::Prefix => { + if term.is_empty() { + self.search_mask = self.entries.clone(); + } else { + let filtered = self.entries + .clone() + .into_iter() + .filter(|ent| ent.command().starts_with(&term)); + + self.search_mask = filtered.collect(); + } + self.cursor = self.search_mask.len().saturating_sub(1); + } + SearchKind::Fuzzy => todo!(), + } + } + + pub fn hint_entry(&self) -> Option<&HistEntry> { + let second_to_last = self.search_mask.len().checked_sub(2)?; + self.search_mask.get(second_to_last) + } + + pub fn get_hint(&self) -> Option { + if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) { + let entry = self.hint_entry()?; + let prefix = self.cursor_entry()?.command(); + Some(entry.command().to_string()) + } else { + None + } + } + + pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { + let new_idx = self.cursor + .saturating_add_signed(offset) + .clamp(0, self.search_mask.len().saturating_sub(1)); + let ent = self.search_mask.get(new_idx)?; + + self.cursor = new_idx; + + Some(ent) + } + + pub fn push(&mut self, command: String) { + let timestamp = SystemTime::now(); + let id = self.get_new_id(); + if self.ignore_dups && self.is_dup(&command) { + return + } + self.entries.push(HistEntry { id, timestamp, command, new: true }); + } + + pub fn is_dup(&self, other: &str) -> bool { + let Some(ent) = self.entries.last() else { + return false + }; + let ent_cmd = &ent.command; + ent_cmd == other + } + + pub fn save(&mut self) -> ShResult<()> { + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.path)?; + + let last_file_entry = self.entries + .iter() + .filter(|ent| !ent.new) + .next_back() + .map(|ent| ent.command.clone()) + .unwrap_or_default(); + + let entries = self.entries + .iter_mut() + .filter(|ent| { + ent.new && + !ent.command.is_empty() && + if self.ignore_dups { + ent.command() != last_file_entry + } else { + true + } + }); + + let mut data = String::new(); + for ent in entries { + ent.new = false; + write!(data, "{ent}").unwrap(); + } + + file.write_all(data.as_bytes())?; + + Ok(()) + } +} diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 3b2579f..94acd5e 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -1,10 +1,10 @@ -use std::{ops::{Range, RangeBounds, RangeInclusive}, string::Drain}; +use std::{fmt::Display, ops::{Range, RangeBounds, RangeInclusive}, string::Drain}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use super::{term::Layout, vicmd::{Anchor, Dest, Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}}; -use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*}; +use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}, prelude::*}; #[derive(Default,PartialEq,Eq,Debug,Clone,Copy)] pub enum CharClass { @@ -300,12 +300,32 @@ impl LineBuf { self.cursor = ClampedUsize::new(cursor, self.grapheme_indices().len(), self.cursor.exclusive); self } + pub fn take_buf(&mut self) -> String { + std::mem::take(&mut self.buffer) + } pub fn has_hint(&self) -> bool { self.hint.is_some() } pub fn hint(&self) -> Option<&String> { self.hint.as_ref() } + pub fn set_hint(&mut self, hint: Option) { + if let Some(hint) = hint { + let hint = hint.strip_prefix(&self.buffer).unwrap(); // If this ever panics, I will eat my hat + if !hint.is_empty() { + self.hint = Some(hint.to_string()) + } + } else { + self.hint = None + } + flog!(DEBUG,self.hint) + } + pub fn accept_hint(&mut self) { + let Some(hint) = self.hint.take() else { return }; + + self.push_str(&hint); + self.cursor.add(hint.len()); + } pub fn set_cursor_clamp(&mut self, yn: bool) { self.cursor.exclusive = yn; } @@ -320,11 +340,13 @@ impl LineBuf { .unwrap_or(self.buffer.len()) } /// Update self.grapheme_indices with the indices of the current buffer + #[track_caller] pub fn update_graphemes(&mut self) { let indices: Vec<_> = self.buffer .grapheme_indices(true) .map(|(i,_)| i) .collect(); + flog!(DEBUG,std::panic::Location::caller()); self.cursor.set_max(indices.len()); self.grapheme_indices = Some(indices) } @@ -697,9 +719,12 @@ impl LineBuf { pos = *next_idx } - let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default }; + if is_other_class_not_ws(&cur_char, &next_char) { + return pos + } let on_whitespace = is_whitespace(&cur_char); // Advance until hitting whitespace or a different character class @@ -707,7 +732,7 @@ impl LineBuf { let other_class_pos = indices_iter.find( |i| { self.grapheme_at(*i) - .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) + .is_some_and(|c| is_other_class_or_is_ws(c, &next_char)) } ); let Some(other_class_pos) = other_class_pos else { @@ -844,6 +869,12 @@ impl LineBuf { // If we reach here, the target_col is past end of line line.graphemes(true).count() } + pub fn cursor_max(&self) -> usize { + self.cursor.max + } + pub fn cursor_at_max(&self) -> bool { + self.cursor.get() == self.cursor.upper_bound() + } pub fn cursor_col(&mut self) -> usize { let start = self.start_of_line(); let end = self.cursor.get(); @@ -1094,6 +1125,8 @@ impl LineBuf { } MotionKind::On(target.get()) } + MotionCmd(count, Motion::ForwardCharForced) => MotionKind::On(self.cursor.ret_add(count)), + MotionCmd(count, Motion::BackwardCharForced) => MotionKind::On(self.cursor.ret_sub(count)), MotionCmd(count,Motion::LineDown) | MotionCmd(count,Motion::LineUp) => { let Some((start,end)) = (match motion.1 { @@ -1192,10 +1225,20 @@ impl LineBuf { if self.has_hint() { let hint = self.hint.take().unwrap(); + let saved_buffer = self.buffer.clone(); // cringe self.push_str(&hint); self.move_cursor(motion); - if self.cursor.get() > last_grapheme_pos { + let has_consumed_hint = ( + self.cursor.exclusive && self.cursor.get() >= last_grapheme_pos + ) || ( + !self.cursor.exclusive && self.cursor.get() > last_grapheme_pos + ); + flog!(DEBUG,has_consumed_hint); + flog!(DEBUG,self.cursor.get()); + flog!(DEBUG,last_grapheme_pos); + + if has_consumed_hint { let buf_end = if self.cursor.exclusive { self.cursor.ret_add(1) } else { @@ -1212,14 +1255,19 @@ impl LineBuf { self.buffer = buffer.to_string(); } else { let old_buffer = self.slice_to(last_grapheme_pos + 1).unwrap().to_string(); - let old_hint = self.slice_from(last_grapheme_pos + 1).unwrap().to_string(); + let Some(old_hint) = self.slice_from(last_grapheme_pos + 1) else { + self.set_buffer(format!("{saved_buffer}{hint}")); + self.hint = None; + return + }; - self.hint = Some(old_hint); + self.hint = Some(old_hint.to_string()); self.set_buffer(old_buffer); } } else { self.move_cursor(motion); } + self.update_graphemes(); } pub fn move_cursor(&mut self, motion: MotionKind) { match motion { @@ -1302,7 +1350,10 @@ impl LineBuf { .map(|c| c.to_string()) .unwrap_or_default() } else { - self.drain(start, end) + let drained = self.drain(start, end); + self.update_graphemes(); + flog!(DEBUG,self.cursor); + drained }; register.write_to_register(register_text); match motion { @@ -1513,6 +1564,20 @@ impl LineBuf { Verb::Equalize => todo!(), Verb::InsertModeLineBreak(anchor) => { let (mut start,end) = self.this_line(); + if start == 0 && end == self.cursor.max { + match anchor { + Anchor::After => { + self.push('\n'); + self.cursor.set(self.cursor_max()); + return Ok(()) + } + Anchor::Before => { + self.insert_at(0, '\n'); + self.cursor.set(0); + return Ok(()) + } + } + } // We want the position of the newline, or start of buffer start = start.saturating_sub(1).min(self.cursor.max); match anchor { @@ -1605,15 +1670,17 @@ impl LineBuf { self.redo_stack.clear(); } - if before != after && !is_undo_op { - self.handle_edit(before, after, cursor_pos); + if before != after { + if !is_undo_op { + self.handle_edit(before, after, cursor_pos); + } /* * The buffer has been edited, * which invalidates the grapheme_indices vector * We set it to None now, so that self.update_graphemes_lazy() * will update it when it is needed again */ - self.grapheme_indices = None; + self.update_graphemes(); } if !is_line_motion { @@ -1633,6 +1700,18 @@ impl LineBuf { } } +impl Display for LineBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let buf = self.buffer.clone(); + write!(f,"{buf}")?; + if let Some(hint) = self.hint() { + let hint_styled = hint.styled(Style::BrightBlack); + write!(f,"{hint_styled}")?; + } + Ok(()) + } +} + /// Rotate alphabetic characters by 13 alphabetic positions pub fn rot13(input: &str) -> String { input.chars() diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 869edb5..c180de3 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,8 +1,9 @@ +use history::{History, SearchConstraint, SearchKind}; use keys::{KeyCode, KeyEvent, ModKeys}; use linebuf::{LineBuf, SelectAnchor, SelectMode}; use nix::libc::STDOUT_FILENO; use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter}; -use vicmd::{Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; +use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}}; @@ -15,6 +16,7 @@ pub mod keys; pub mod vicmd; pub mod register; pub mod vimode; +pub mod history; pub trait Readline { fn readline(&mut self) -> ShResult; @@ -28,7 +30,8 @@ pub struct FernVi { pub old_layout: Option, pub repeat_action: Option, pub repeat_motion: Option, - pub editor: LineBuf + pub editor: LineBuf, + pub history: History } impl Readline for FernVi { @@ -36,11 +39,8 @@ impl Readline for FernVi { let raw_mode_guard = raw_mode(); // Restores termios state on drop loop { - let new_layout = self.get_layout(); - if let Some(layout) = self.old_layout.as_ref() { - self.writer.clear_rows(layout)?; - } - raw_mode_guard.disable_for(|| self.print_line(new_layout))?; + raw_mode_guard.disable_for(|| self.print_line())?; + let Some(key) = self.reader.read_key() else { raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; std::mem::drop(raw_mode_guard); @@ -48,15 +48,28 @@ impl Readline for FernVi { }; flog!(DEBUG, key); + if self.should_accept_hint(&key) { + self.editor.accept_hint(); + self.history.update_pending_cmd(self.editor.as_str()); + self.print_line()?; + continue + } + let Some(mut cmd) = self.mode.handle_key(key) else { continue }; cmd.alter_line_motion_if_no_verb(); + if self.should_grab_history(&cmd) { + self.scroll_history(cmd); + self.print_line()?; + continue + } + if cmd.should_submit() { raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; std::mem::drop(raw_mode_guard); - return Ok(std::mem::take(&mut self.editor.buffer)) + return Ok(self.editor.take_buf()) } if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { @@ -70,21 +83,23 @@ impl Readline for FernVi { } flog!(DEBUG,cmd); + let before = self.editor.buffer.clone(); self.exec_cmd(cmd)?; + let after = self.editor.as_str(); + if before != after { + self.history.update_pending_cmd(self.editor.as_str()); + } + + let hint = self.history.get_hint(); + self.editor.set_hint(hint); } } } -impl Default for FernVi { - fn default() -> Self { - Self::new(None) - } -} - impl FernVi { - pub fn new(prompt: Option) -> Self { - Self { + pub fn new(prompt: Option) -> ShResult { + Ok(Self { reader: Box::new(TermReader::new()), writer: Box::new(TermWriter::new(STDOUT_FILENO)), prompt: prompt.unwrap_or("$ ".styled(Style::Green)), @@ -92,12 +107,14 @@ impl FernVi { old_layout: None, repeat_action: None, repeat_motion: None, - editor: LineBuf::new().with_initial("\nThe quick brown fox jumps over\n the lazy dogThe quick\nbrown fox jumps over the a", 1004) - } + editor: LineBuf::new(), + history: History::new()? + }) } pub fn get_layout(&mut self) -> Layout { - let line = self.editor.as_str().to_string(); + let line = self.editor.to_string(); + flog!(DEBUG,line); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let (cols,_) = get_win_size(STDIN_FILENO); Layout::from_parts( @@ -107,9 +124,110 @@ impl FernVi { to_cursor, &line ) + } + pub fn scroll_history(&mut self, cmd: ViCmd) { + flog!(DEBUG,"scrolling"); + /* + if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { + let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string()); + self.history.constrain_entries(constraint); + } + */ + let count = &cmd.motion().unwrap().0; + let motion = &cmd.motion().unwrap().1; + flog!(DEBUG,count,motion); + flog!(DEBUG,self.history.masked_entries()); + let entry = match motion { + Motion::LineUpCharwise => { + let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { + return + }; + flog!(DEBUG,"found entry"); + flog!(DEBUG,hist_entry.command()); + hist_entry + } + Motion::LineDownCharwise => { + let Some(hist_entry) = self.history.scroll(*count as isize) else { + return + }; + flog!(DEBUG,"found entry"); + flog!(DEBUG,hist_entry.command()); + hist_entry + } + _ => unreachable!() + }; + let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); + let mut buf = LineBuf::new().with_initial(entry.command(),0); + let line_end = buf.end_of_line(); + if let Some(dest) = self.mode.hist_scroll_start_pos() { + match dest { + To::Start => { + /* Already at 0 */ + } + To::End => { + // History entries cannot be empty + // So this subtraction is safe (maybe) + buf.cursor.add(line_end); + } + } + } else { + let target = (col).min(line_end); + buf.cursor.add(target); + } + + self.editor = buf + } + pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { + flog!(DEBUG,self.editor.cursor_at_max()); + flog!(DEBUG,self.editor.cursor); + if self.editor.cursor_at_max() && self.editor.has_hint() { + match self.mode.report_mode() { + ModeReport::Replace | + ModeReport::Insert => { + matches!( + event, + KeyEvent(KeyCode::Right, ModKeys::NONE) + ) + } + ModeReport::Visual | + ModeReport::Normal => { + matches!( + event, + KeyEvent(KeyCode::Right, ModKeys::NONE) + ) || + ( + self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() && + matches!( + event, + KeyEvent(KeyCode::Char('l'), ModKeys::NONE) + ) + ) + } + _ => unimplemented!() + } + } else { + false + } } - pub fn print_line(&mut self, new_layout: Layout) -> ShResult<()> { + pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool { + cmd.verb().is_none() && + ( + cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) && + self.editor.start_of_line() == 0 + ) || + ( + cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) && + self.editor.end_of_line() == self.editor.cursor_max() && + !self.history.cursor_entry().is_some_and(|ent| ent.is_new()) + ) + } + + pub fn print_line(&mut self) -> ShResult<()> { + let new_layout = self.get_layout(); + if let Some(layout) = self.old_layout.as_ref() { + self.writer.clear_rows(layout)?; + } self.writer.redraw( &self.prompt, diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 7f8c7a3..36f3468 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -669,7 +669,7 @@ impl LineWriter for TermWriter { let cursor = new_layout.cursor; self.buffer.push_str(prompt); - self.buffer.push_str(line.as_str()); + self.buffer.push_str(&line.to_string()); if end.col == 0 && end.row > 0 && !self.buffer.ends_with('\n') { // The line has wrapped. We need to use our own line break. diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index d5f782d..545fd46 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -264,6 +264,8 @@ pub enum Motion { CharSearch(Direction,Dest,char), BackwardChar, ForwardChar, + BackwardCharForced, // These two variants can cross line boundaries + ForwardCharForced, LineUp, LineUpCharwise, ScreenLineUp, diff --git a/src/prompt/readline/vimode.rs b/src/prompt/readline/vimode.rs index 143514a..22c0e08 100644 --- a/src/prompt/readline/vimode.rs +++ b/src/prompt/readline/vimode.rs @@ -119,7 +119,7 @@ impl ViMode for ViInsert { E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => { self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); + self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardCharForced)); self.register_and_return() }