implemented history for the line editor

This commit is contained in:
2025-05-28 20:24:09 -04:00
parent f67543c111
commit 8cacbfdbdd
8 changed files with 731 additions and 147 deletions

View File

@@ -316,6 +316,7 @@ pub enum ShErrKind {
ParseErr, ParseErr,
InternalErr, InternalErr,
ExecFail, ExecFail,
HistoryReadErr,
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno, Errno,
@@ -332,22 +333,23 @@ pub enum ShErrKind {
impl Display for ShErrKind { impl Display for ShErrKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = match self { let output = match self {
ShErrKind::IoErr => "I/O Error", Self::IoErr => "I/O Error",
ShErrKind::SyntaxErr => "Syntax Error", Self::SyntaxErr => "Syntax Error",
ShErrKind::ParseErr => "Parse Error", Self::ParseErr => "Parse Error",
ShErrKind::InternalErr => "Internal Error", Self::InternalErr => "Internal Error",
ShErrKind::ExecFail => "Execution Failed", Self::HistoryReadErr => "History Parse Error",
ShErrKind::ResourceLimitExceeded => "Resource Limit Exceeded", Self::ExecFail => "Execution Failed",
ShErrKind::BadPermission => "Bad Permissions", Self::ResourceLimitExceeded => "Resource Limit Exceeded",
ShErrKind::Errno => "ERRNO", Self::BadPermission => "Bad Permissions",
ShErrKind::FileNotFound(file) => &format!("File not found: {file}"), Self::Errno => "ERRNO",
ShErrKind::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), Self::FileNotFound(file) => &format!("File not found: {file}"),
ShErrKind::CleanExit(_) => "", Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"),
ShErrKind::FuncReturn(_) => "", Self::CleanExit(_) => "",
ShErrKind::LoopContinue(_) => "", Self::FuncReturn(_) => "",
ShErrKind::LoopBreak(_) => "", Self::LoopContinue(_) => "",
ShErrKind::ReadlineErr => "Line Read Error", Self::LoopBreak(_) => "",
ShErrKind::Null => "", Self::ReadlineErr => "Line Read Error",
Self::Null => "",
}; };
write!(f,"{output}") write!(f,"{output}")
} }

View File

@@ -14,18 +14,18 @@ fn get_prompt() -> ShResult<String> {
// //
// username@hostname // username@hostname
// short/path/to/pwd/ // short/path/to/pwd/
// $ // $ _
let default = "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; let default = "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
return Ok(format!("{}",expand_prompt(default)?)) return expand_prompt(default)
}; };
Ok(format!("\n{}",expand_prompt(&prompt)?)) expand_prompt(&prompt)
} }
pub fn read_line(edit_mode: FernEditMode) -> ShResult<String> { pub fn read_line(edit_mode: FernEditMode) -> ShResult<String> {
let prompt = get_prompt()?; let prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = match edit_mode { let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))), FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
FernEditMode::Emacs => todo!() FernEditMode::Emacs => todo!()
}; };
reader.readline() reader.readline()

View File

@@ -0,0 +1,258 @@
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(Debug)]
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
}
}
impl FromStr for HistEntry {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::<u64>() else { return err };
let Ok(id) = id.parse::<u32>() 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<HistEntry>);
impl FromStr for HistEntries {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::<HistEntry>()?;
entries.push(entry);
cur_line.clear();
}
Ok(Self(entries))
}
}
fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
if !path.exists() {
fs::File::create(path)?;
}
let raw = fs::read_to_string(path)?;
Ok(raw.parse::<HistEntries>()?.0)
}
pub struct History {
path: PathBuf,
entries: Vec<HistEntry>,
cursor: usize,
search_direction: Direction,
ignore_dups: bool,
max_size: Option<u32>,
}
impl History {
pub fn new() -> ShResult<Self> {
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
let home = env::var("HOME").unwrap();
format!("{home}/.fern_history")
}));
let entries = read_hist_file(&path)?;
let cursor = entries.len();
let mut new = Self {
path,
entries,
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 push_empty_entry(&mut self) {
let id = self.get_new_id();
let timestamp = SystemTime::now();
let command = "".into();
self.entries.push(HistEntry { id, timestamp, command, new: true })
}
pub fn update_pending_cmd(&mut self, command: &str) {
flog!(DEBUG, "updating command");
let Some(ent) = self.last_mut() else {
return
};
ent.command = command.to_string()
}
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<u32>) {
self.max_size = size
}
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
let new_idx = self.cursor
.saturating_add_signed(offset)
.clamp(0, self.entries.len());
let ent = self.entries.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 entries = self.entries.iter_mut().filter(|ent| ent.new);
let mut data = String::new();
for ent in entries {
ent.new = false;
write!(data, "{ent}").unwrap();
}
file.write_all(data.as_bytes())?;
Ok(())
}
}

View File

@@ -138,6 +138,10 @@ impl Edit {
pub fn stop_merge(&mut self) { pub fn stop_merge(&mut self) {
self.merging = false self.merging = false
} }
pub fn is_empty(&self) -> bool {
self.new.is_empty() &&
self.old.is_empty()
}
} }
#[derive(Default,Debug)] #[derive(Default,Debug)]
@@ -151,11 +155,12 @@ pub struct LineBuf {
move_cursor_on_undo: bool, move_cursor_on_undo: bool,
undo_stack: Vec<Edit>, undo_stack: Vec<Edit>,
redo_stack: Vec<Edit>, redo_stack: Vec<Edit>,
tab_stop: usize
} }
impl LineBuf { impl LineBuf {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self { tab_stop: 8, ..Default::default() }
} }
pub fn with_initial(mut self, initial: &str) -> Self { pub fn with_initial(mut self, initial: &str) -> Self {
self.buffer = initial.to_string(); self.buffer = initial.to_string();
@@ -167,6 +172,9 @@ impl LineBuf {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.buffer &self.buffer
} }
pub fn saved_col(&self) -> Option<usize> {
self.saved_col
}
pub fn update_term_dims(&mut self, dims: (usize,usize)) { pub fn update_term_dims(&mut self, dims: (usize,usize)) {
self.term_dims = dims self.term_dims = dims
} }
@@ -181,6 +189,9 @@ impl LineBuf {
pub fn byte_len(&self) -> usize { pub fn byte_len(&self) -> usize {
self.buffer.len() self.buffer.len()
} }
pub fn undos(&self) -> usize {
self.undo_stack.len()
}
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.buffer.is_empty() self.buffer.is_empty()
} }
@@ -195,11 +206,21 @@ impl LineBuf {
self.cursor_back(1); self.cursor_back(1);
} }
} }
pub fn clamp_range(&self, range: Range<usize>) -> Range<usize> {
let (mut start,mut end) = (range.start,range.end);
start = start.max(0);
end = end.min(self.byte_len());
start..end
}
pub fn grapheme_len(&self) -> usize { pub fn grapheme_len(&self) -> usize {
self.buffer.grapheme_indices(true).count() self.buffer.grapheme_indices(true).count()
} }
pub fn slice_from_cursor(&self) -> &str { pub fn slice_from_cursor(&self) -> &str {
&self.buffer[self.cursor..] if let Some(slice) = &self.buffer.get(self.cursor..) {
slice
} else {
""
}
} }
pub fn slice_to_cursor(&self) -> &str { pub fn slice_to_cursor(&self) -> &str {
if let Some(slice) = self.buffer.get(..self.cursor) { if let Some(slice) = self.buffer.get(..self.cursor) {
@@ -291,6 +312,11 @@ impl LineBuf {
.map(|(i, _)| i) .map(|(i, _)| i)
} }
} }
pub fn sync_cursor(&mut self) {
if !self.buffer.is_char_boundary(self.cursor) {
self.cursor = self.prev_pos(1).unwrap_or(0)
}
}
pub fn cursor_back(&mut self, dist: usize) -> bool { pub fn cursor_back(&mut self, dist: usize) -> bool {
let Some(pos) = self.prev_pos(dist) else { let Some(pos) = self.prev_pos(dist) else {
return false return false
@@ -298,6 +324,35 @@ impl LineBuf {
self.cursor = pos; self.cursor = pos;
true true
} }
/// Constrain the cursor to the current line
pub fn cursor_back_confined(&mut self, dist: usize) -> bool {
for _ in 0..dist {
let Some(pos) = self.prev_pos(1) else {
return false
};
if let Some("\n") = self.grapheme_at(pos) {
return false
}
if !self.cursor_back(1) {
return false
}
}
true
}
pub fn cursor_fwd_confined(&mut self, dist: usize) -> bool {
for _ in 0..dist {
let Some(pos) = self.next_pos(1) else {
return false
};
if let Some("\n") = self.grapheme_at(pos) {
return false
}
if !self.cursor_fwd(1) {
return false
}
}
true
}
/// Up to but not including 'dist' /// Up to but not including 'dist'
pub fn cursor_back_to(&mut self, dist: usize) -> bool { pub fn cursor_back_to(&mut self, dist: usize) -> bool {
let dist = dist.saturating_sub(1); let dist = dist.saturating_sub(1);
@@ -322,77 +377,89 @@ impl LineBuf {
self.cursor = pos; self.cursor = pos;
true true
} }
pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize {
let mut lines = 0;
let mut col = offset.max(1);
for ch in self.buffer.chars() {
match ch {
'\n' => {
lines += 1;
col = 1;
}
_ => {
col += 1;
if col > term_width {
lines += 1;
col = 1
}
}
}
}
lines
}
pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize {
let mut lines = 0;
let mut col = offset.max(1);
for ch in self.slice_to_cursor().chars() {
match ch {
'\n' => {
lines += 1;
col = 1;
}
_ => {
col += 1;
if col > term_width {
lines += 1;
col = 1
}
}
}
}
lines
}
pub fn display_coords(&self, term_width: usize) -> (usize,usize) {
let chars = self.slice_to_cursor().chars();
fn compute_display_positions<'a>(
text: impl Iterator<Item = &'a str>,
start_col: usize,
tab_stop: usize,
term_width: usize,
) -> (usize, usize) {
let mut lines = 0; let mut lines = 0;
let mut col = 0; let mut col = start_col;
for ch in chars {
match ch { for grapheme in text {
'\n' => { match grapheme {
"\n" => {
lines += 1; lines += 1;
col = 1; col = 1;
} }
_ => { "\t" => {
let spaces_to_next_tab = tab_stop - (col % tab_stop);
if col + spaces_to_next_tab > term_width {
lines += 1;
col = 1;
} else {
col += spaces_to_next_tab;
}
// Don't ask why this is here
// I don't know either
// All I know is that it only finds the correct cursor position
// if i add one to the column here, for literally no reason
// Thank you linux terminal :)
col += 1; col += 1;
}
_ => {
col += grapheme.width();
if col > term_width { if col > term_width {
lines += 1; lines += 1;
col = 1 col = 1;
} }
} }
} }
} }
(lines, col) (lines, col)
} }
pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize {
let (lines, _) = Self::compute_display_positions(
self.buffer.graphemes(true),
offset.max(1),
self.tab_stop,
term_width,
);
lines
}
pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize {
let (lines, _) = Self::compute_display_positions(
self.slice_to_cursor().graphemes(true),
offset.max(1),
self.tab_stop,
term_width,
);
lines
}
pub fn display_coords(&self, term_width: usize) -> (usize, usize) {
Self::compute_display_positions(
self.slice_to_cursor().graphemes(true),
0,
self.tab_stop,
term_width,
)
}
pub fn cursor_display_coords(&self, term_width: usize) -> (usize, usize) { pub fn cursor_display_coords(&self, term_width: usize) -> (usize, usize) {
let (d_line, mut d_col) = self.display_coords(term_width); let (d_line, mut d_col) = self.display_coords(term_width);
let line = self.count_display_lines(self.first_line_offset, term_width) - d_line; let total_lines = self.count_display_lines(self.first_line_offset, term_width);
let logical_line = total_lines - d_line;
if line == self.count_lines() { if logical_line == self.count_lines() {
d_col += self.first_line_offset; d_col += self.first_line_offset;
} }
(line,d_col) (logical_line, d_col)
} }
pub fn insert(&mut self, ch: char) { pub fn insert(&mut self, ch: char) {
if self.buffer.is_empty() { if self.buffer.is_empty() {
@@ -471,6 +538,20 @@ impl LineBuf {
self.end_of_line() self.end_of_line()
) )
} }
pub fn prev_line(&self, offset: usize) -> (usize,usize) {
let (start,_) = self.select_lines_up(offset);
let end = self.slice_from_cursor().find('\n').unwrap_or(self.byte_len());
(start,end)
}
pub fn next_line(&self, offset: usize) -> Option<(usize,usize)> {
if self.this_line().1 == self.byte_len() {
return None
}
let (_,mut end) = self.select_lines_down(offset);
end = end.min(self.byte_len().saturating_sub(1));
let start = self.slice_to(end + 1).rfind('\n').unwrap_or(0);
Some((start,end))
}
pub fn count_lines(&self) -> usize { pub fn count_lines(&self) -> usize {
self.buffer self.buffer
.chars() .chars()
@@ -522,8 +603,9 @@ impl LineBuf {
} }
for _ in 0..n { for _ in 0..n {
if let Some(prev_newline) = self.slice_to(start - 1).rfind('\n') { let slice = self.slice_to(start - 1);
start = prev_newline + 1; if let Some(prev_newline) = slice.rfind('\n') {
start = prev_newline;
} else { } else {
start = 0; start = 0;
break break
@@ -548,9 +630,14 @@ impl LineBuf {
return (start,end) return (start,end)
} }
for _ in 0..n { for _ in 0..=n {
if let Some(next_newline) = self.slice_from(end).find('\n') { let next_ln_start = end + 1;
end = next_newline if next_ln_start >= self.byte_len() {
end = self.byte_len();
break
}
if let Some(next_newline) = self.slice_from(next_ln_start).find('\n') {
end += next_newline;
} else { } else {
end = self.byte_len(); end = self.byte_len();
break break
@@ -626,6 +713,9 @@ impl LineBuf {
Direction::Forward => { Direction::Forward => {
match to { match to {
To::Start => { To::Start => {
if self.on_whitespace() {
return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
}
if self.on_start_of_word(word) { if self.on_start_of_word(word) {
pos += 1; pos += 1;
if pos >= self.byte_len() { if pos >= self.byte_len() {
@@ -637,6 +727,9 @@ impl LineBuf {
Some(word_start) Some(word_start)
} }
To::End => { To::End => {
if self.on_whitespace() {
pos = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
}
match self.on_end_of_word(word) { match self.on_end_of_word(word) {
true => { true => {
pos += 1; pos += 1;
@@ -662,6 +755,9 @@ impl LineBuf {
Direction::Backward => { Direction::Backward => {
match to { match to {
To::Start => { To::Start => {
if self.on_whitespace() {
pos = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
}
match self.on_start_of_word(word) { match self.on_start_of_word(word) {
true => { true => {
pos = pos.checked_sub(1)?; pos = pos.checked_sub(1)?;
@@ -680,6 +776,9 @@ impl LineBuf {
} }
} }
To::End => { To::End => {
if self.on_whitespace() {
return self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
}
if self.on_end_of_word(word) { if self.on_end_of_word(word) {
pos = pos.checked_sub(1)?; pos = pos.checked_sub(1)?;
} }
@@ -696,6 +795,9 @@ impl LineBuf {
Direction::Forward => { Direction::Forward => {
match to { match to {
To::Start => { To::Start => {
if self.on_whitespace() {
return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
}
if self.on_start_of_word(word) { if self.on_start_of_word(word) {
pos += 1; pos += 1;
if pos >= self.byte_len() { if pos >= self.byte_len() {
@@ -712,6 +814,9 @@ impl LineBuf {
} }
} }
To::End => { To::End => {
if self.on_whitespace() {
pos = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
}
match self.on_end_of_word(word) { match self.on_end_of_word(word) {
true => { true => {
pos += 1; pos += 1;
@@ -752,6 +857,9 @@ impl LineBuf {
Direction::Backward => { Direction::Backward => {
match to { match to {
To::Start => { To::Start => {
if self.on_whitespace() {
pos = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
}
match self.on_start_of_word(word) { match self.on_start_of_word(word) {
true => { true => {
pos = pos.checked_sub(1)?; pos = pos.checked_sub(1)?;
@@ -772,6 +880,9 @@ impl LineBuf {
} }
} }
To::End => { To::End => {
if self.on_whitespace() {
return self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
}
if self.on_end_of_word(word) { if self.on_end_of_word(word) {
pos = pos.checked_sub(1)?; pos = pos.checked_sub(1)?;
} }
@@ -829,10 +940,7 @@ impl LineBuf {
} }
pub fn eval_motion(&mut self, motion: Motion) -> MotionKind { pub fn eval_motion(&mut self, motion: Motion) -> MotionKind {
match motion { match motion {
Motion::WholeLine => { Motion::WholeLine => MotionKind::Line(0),
let (start,end) = self.this_line();
MotionKind::range(start..=end)
}
Motion::TextObj(text_obj, bound) => todo!(), Motion::TextObj(text_obj, bound) => todo!(),
Motion::BeginningOfFirstWord => { Motion::BeginningOfFirstWord => {
let (start,_) = self.this_line(); let (start,_) = self.this_line();
@@ -962,7 +1070,7 @@ impl LineBuf {
.map(|(_, byte_idx, _)| *byte_idx) .map(|(_, byte_idx, _)| *byte_idx)
} }
pub fn get_range_from_motion(&self, verb: &Verb, motion: &MotionKind) -> Option<Range<usize>> { pub fn get_range_from_motion(&self, verb: &Verb, motion: &MotionKind) -> Option<Range<usize>> {
match motion { let range = match motion {
MotionKind::Forward(n) => { MotionKind::Forward(n) => {
let pos = self.next_pos(*n)?; let pos = self.next_pos(*n)?;
let range = self.cursor..pos; let range = self.cursor..pos;
@@ -983,18 +1091,37 @@ impl LineBuf {
Some(range.0..range.1) Some(range.0..range.1)
} }
MotionKind::Line(n) => { MotionKind::Line(n) => {
let (start,end) = match n.cmp(&0) { match n.cmp(&0) {
Ordering::Less => self.select_lines_up(n.unsigned_abs()), Ordering::Less => {
Ordering::Equal => self.this_line(), let (start,end) = self.select_lines_up(n.unsigned_abs());
Ordering::Greater => self.select_lines_down(*n as usize) let mut range = match verb {
}; Verb::Delete => mk_range_inclusive(start,end),
let range = match verb { _ => mk_range(start,end),
Verb::Change => mk_range(start,end),
Verb::Delete => mk_range(start,(end + 1).min(self.byte_len())),
_ => unreachable!()
}; };
range = self.clamp_range(range);
Some(range) Some(range)
} }
Ordering::Equal => {
let (start,end) = self.this_line();
let mut range = match verb {
Verb::Delete => mk_range_inclusive(start,end),
_ => mk_range(start,end),
};
range = self.clamp_range(range);
Some(range)
}
Ordering::Greater => {
let (start, mut end) = self.select_lines_down(*n as usize);
end = (end + 1).min(self.byte_len() - 1);
let mut range = match verb {
Verb::Delete => mk_range_inclusive(start,end),
_ => mk_range(start,end),
};
range = self.clamp_range(range);
Some(range)
}
}
}
MotionKind::ToLine(n) => { MotionKind::ToLine(n) => {
let (start,end) = self.select_lines_to(*n); let (start,end) = self.select_lines_to(*n);
let range = match verb { let range = match verb {
@@ -1009,7 +1136,35 @@ impl LineBuf {
let pos = self.calculate_display_offset(*n)?; let pos = self.calculate_display_offset(*n)?;
Some(mk_range(pos, self.cursor)) Some(mk_range(pos, self.cursor))
} }
};
range.map(|rng| self.clamp_range(rng))
} }
pub fn indent_lines(&mut self, range: Range<usize>) {
let (start,end) = (range.start,range.end);
self.buffer.insert(start, '\t');
let graphemes = self.buffer[start + 1..end].grapheme_indices(true);
let mut tab_insert_indices = vec![];
let mut next_is_tab_pos = false;
for (i,g) in graphemes {
if g == "\n" {
next_is_tab_pos = true;
} else if next_is_tab_pos {
tab_insert_indices.push(start + i + 1);
next_is_tab_pos = false;
}
}
for i in tab_insert_indices {
if i < self.byte_len() {
self.buffer.insert(i, '\t');
}
}
}
pub fn dedent_lines(&mut self, range: Range<usize>) {
todo!()
} }
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
match verb { match verb {
@@ -1018,9 +1173,21 @@ impl LineBuf {
let Some(range) = self.get_range_from_motion(&verb, &motion) else { let Some(range) = self.get_range_from_motion(&verb, &motion) else {
return Ok(()) return Ok(())
}; };
let restore_col = matches!(motion, MotionKind::Line(_)) && matches!(verb, Verb::Delete);
if restore_col {
self.saved_col = Some(self.cursor_column())
}
let deleted = self.buffer.drain(range.clone()); let deleted = self.buffer.drain(range.clone());
register.write_to_register(deleted.collect()); register.write_to_register(deleted.collect());
self.cursor = range.start; self.cursor = range.start;
if restore_col {
let saved = self.saved_col.unwrap();
let line_start = self.this_line().0;
self.cursor = line_start + saved;
}
} }
Verb::DeleteChar(anchor) => { Verb::DeleteChar(anchor) => {
match anchor { match anchor {
@@ -1078,7 +1245,6 @@ impl LineBuf {
let Some(undo) = self.undo_stack.pop() else { let Some(undo) = self.undo_stack.pop() else {
return Ok(()) return Ok(())
}; };
flog!(DEBUG, undo);
let Edit { pos, cursor_pos, old, new, .. } = undo; let Edit { pos, cursor_pos, old, new, .. } = undo;
let range = pos..pos + new.len(); let range = pos..pos + new.len();
self.buffer.replace_range(range, &old); self.buffer.replace_range(range, &old);
@@ -1093,7 +1259,6 @@ impl LineBuf {
let Some(redo) = self.redo_stack.pop() else { let Some(redo) = self.redo_stack.pop() else {
return Ok(()) return Ok(())
}; };
flog!(DEBUG, redo);
let Edit { pos, cursor_pos, old, new, .. } = redo; let Edit { pos, cursor_pos, old, new, .. } = redo;
let range = pos..pos + new.len(); let range = pos..pos + new.len();
self.buffer.replace_range(range, &old); self.buffer.replace_range(range, &old);
@@ -1139,10 +1304,28 @@ impl LineBuf {
} }
} }
} }
Verb::JoinLines => todo!(), Verb::JoinLines => {
let (start,end) = self.this_line();
let Some((nstart,nend)) = self.next_line(1) else {
return Ok(())
};
let line = &self.buffer[start..end];
let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace
flog!(DEBUG,next_line);
let replace_newline_with_space = !line.ends_with([' ', '\t']);
self.cursor = end;
if replace_newline_with_space {
self.buffer.replace_range(end..end+1, " ");
self.buffer.replace_range(end+1..nend, next_line);
} else {
self.buffer.replace_range(end..end+1, "");
self.buffer.replace_range(end..nend, next_line);
}
}
Verb::InsertChar(ch) => { Verb::InsertChar(ch) => {
self.insert(ch); self.insert(ch);
self.apply_motion(motion); self.apply_motion(/*forced*/ true, motion);
} }
Verb::Insert(str) => { Verb::Insert(str) => {
for ch in str.chars() { for ch in str.chars() {
@@ -1151,8 +1334,18 @@ impl LineBuf {
} }
} }
Verb::Breakline(anchor) => todo!(), Verb::Breakline(anchor) => todo!(),
Verb::Indent => todo!(), Verb::Indent => {
Verb::Dedent => todo!(), let Some(range) = self.get_range_from_motion(&verb, &motion) else {
return Ok(())
};
self.indent_lines(range)
}
Verb::Dedent => {
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
return Ok(())
};
self.dedent_lines(range)
}
Verb::Equalize => todo!(), // I fear this one Verb::Equalize => todo!(), // I fear this one
Verb::Builder(verb_builder) => todo!(), Verb::Builder(verb_builder) => todo!(),
Verb::EndOfFile => { Verb::EndOfFile => {
@@ -1170,25 +1363,33 @@ impl LineBuf {
Verb::NormalMode | Verb::NormalMode |
Verb::VisualMode => { Verb::VisualMode => {
/* Already handled */ /* Already handled */
self.apply_motion(motion); self.apply_motion(/*forced*/ true,motion);
} }
} }
Ok(()) Ok(())
} }
pub fn apply_motion(&mut self, motion: MotionKind) { pub fn apply_motion(&mut self, forced: bool, motion: MotionKind) {
match motion { match motion {
MotionKind::Forward(n) => { MotionKind::Forward(n) => {
for _ in 0..n { for _ in 0..n {
if forced {
if !self.cursor_fwd(1) { if !self.cursor_fwd(1) {
break break
} }
} else if !self.cursor_fwd_confined(1) {
break
}
} }
} }
MotionKind::Backward(n) => { MotionKind::Backward(n) => {
for _ in 0..n { for _ in 0..n {
if forced {
if !self.cursor_back(1) { if !self.cursor_back(1) {
break break
} }
} else if !self.cursor_back_confined(1) {
break
}
} }
} }
MotionKind::To(n) => { MotionKind::To(n) => {
@@ -1206,28 +1407,22 @@ impl LineBuf {
} }
MotionKind::Line(n) => { MotionKind::Line(n) => {
match n.cmp(&0) { match n.cmp(&0) {
Ordering::Equal => { Ordering::Equal => (),
let (start,_) = self.this_line();
if start == 0 {
return
}
self.cursor = start;
}
Ordering::Less => { Ordering::Less => {
let (start,_) = self.select_lines_up(n.unsigned_abs()); for _ in 0..n.unsigned_abs() {
if start == 0 { let Some(pos) = self.find_prev_line_pos() else {
return return
};
self.cursor = pos;
} }
self.cursor = start;
} }
Ordering::Greater => { Ordering::Greater => {
let (_,end) = self.select_lines_down(n.unsigned_abs()); for _ in 0..n.unsigned_abs() {
if end == self.byte_len() { let Some(pos) = self.find_next_line_pos() else {
return return
};
self.cursor = pos;
} }
self.cursor = end.saturating_sub(1);
let (start,_) = self.this_line();
self.cursor = start;
} }
} }
} }
@@ -1252,6 +1447,9 @@ impl LineBuf {
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) { pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
if self.edit_is_merging() { if self.edit_is_merging() {
let diff = Edit::diff(&old, &new, curs_pos); let diff = Edit::diff(&old, &new, curs_pos);
if diff.is_empty() {
return
}
let Some(mut edit) = self.undo_stack.pop() else { let Some(mut edit) = self.undo_stack.pop() else {
self.undo_stack.push(diff); self.undo_stack.push(diff);
return return
@@ -1263,9 +1461,11 @@ impl LineBuf {
self.undo_stack.push(edit); self.undo_stack.push(edit);
} else { } else {
let diff = Edit::diff(&old, &new, curs_pos); let diff = Edit::diff(&old, &new, curs_pos);
if !diff.is_empty() {
self.undo_stack.push(diff); self.undo_stack.push(diff);
} }
} }
}
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert()); let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
@@ -1278,10 +1478,6 @@ impl LineBuf {
edit.stop_merge(); edit.stop_merge();
} }
} }
if clear_redos {
flog!(DEBUG, "clearing redos");
flog!(DEBUG,cmd);
}
let ViCmd { register, verb, motion, .. } = cmd; let ViCmd { register, verb, motion, .. } = cmd;
@@ -1301,7 +1497,7 @@ impl LineBuf {
if let Some(verb) = verb.clone() { if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion, register)?; self.exec_verb(verb.1, motion, register)?;
} else { } else {
self.apply_motion(motion); self.apply_motion(/*forced*/ false,motion);
} }
} }
} }
@@ -1328,6 +1524,7 @@ impl LineBuf {
if self.clamp_cursor { if self.clamp_cursor {
self.clamp_cursor(); self.clamp_cursor();
} }
self.sync_cursor();
Ok(()) Ok(())
} }
} }
@@ -1368,6 +1565,11 @@ pub fn is_grapheme_boundary(s: &str, pos: usize) -> bool {
s.is_char_boundary(pos) && s.grapheme_indices(true).any(|(i,_)| i == pos) s.is_char_boundary(pos) && s.grapheme_indices(true).any(|(i,_)| i == pos)
} }
fn mk_range_inclusive(a: usize, b: usize) -> Range<usize> {
let b = b + 1;
std::cmp::min(a, b)..std::cmp::max(a, b)
}
fn mk_range(a: usize, b: usize) -> Range<usize> { fn mk_range(a: usize, b: usize) -> Range<usize> {
std::cmp::min(a, b)..std::cmp::max(a, b) std::cmp::min(a, b)..std::cmp::max(a, b)
} }

View File

@@ -1,11 +1,12 @@
use std::time::Duration; use std::time::Duration;
use history::History;
use keys::{KeyCode, KeyEvent, ModKeys}; use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{strip_ansi_codes_and_escapes, LineBuf}; use linebuf::{strip_ansi_codes_and_escapes, LineBuf};
use mode::{CmdReplay, ViInsert, ViMode, ViNormal, ViReplace}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal, ViReplace};
use term::Terminal; use term::Terminal;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use vicmd::{Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
use crate::prelude::*; use crate::prelude::*;
@@ -16,6 +17,9 @@ pub mod linebuf;
pub mod vicmd; pub mod vicmd;
pub mod mode; pub mod mode;
pub mod register; pub mod register;
pub mod history;
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nmagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\nconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
/// Unified interface for different line editing methods /// Unified interface for different line editing methods
pub trait Readline { pub trait Readline {
@@ -25,6 +29,7 @@ pub trait Readline {
pub struct FernVi { pub struct FernVi {
term: Terminal, term: Terminal,
line: LineBuf, line: LineBuf,
history: History,
prompt: String, prompt: String,
mode: Box<dyn ViMode>, mode: Box<dyn ViMode>,
last_action: Option<CmdReplay>, last_action: Option<CmdReplay>,
@@ -54,41 +59,68 @@ impl Readline for FernVi {
*/ */
self.print_buf(false)?; self.print_buf(false)?;
loop { loop {
let key = self.term.read_key(); let key = self.term.read_key();
if let KeyEvent(KeyCode::Char('V'), ModKeys::CTRL) = key { if let KeyEvent(KeyCode::Char('V'), ModKeys::CTRL) = key {
self.handle_verbatim(); self.handle_verbatim()?;
continue continue
} }
let Some(cmd) = self.mode.handle_key(key) else { let Some(cmd) = self.mode.handle_key(key) else {
continue continue
}; };
if cmd.should_submit() { if self.should_grab_history(&cmd) {
self.term.write("\n"); flog!(DEBUG, "scrolling");
return Ok(self.line.to_string()); self.scroll_history(cmd);
self.print_buf(true)?;
continue
} }
if cmd.should_submit() {
self.term.write("\n");
let command = self.line.to_string();
if !command.is_empty() {
// We're just going to trim the command
// reduces clutter in the case of two history commands whose only difference is insignificant whitespace
self.history.push(command.trim().to_string());
self.history.save()?;
}
return Ok(command);
}
let line = self.line.to_string();
self.exec_cmd(cmd.clone())?; self.exec_cmd(cmd.clone())?;
let new_line = self.line.as_str();
let has_changes = line != new_line;
flog!(DEBUG, has_changes);
if cmd.verb().is_some_and(|v| v.1.is_edit()) && has_changes {
self.history.update_pending_cmd(self.line.as_str());
}
self.print_buf(true)?; self.print_buf(true)?;
} }
} }
} }
impl FernVi { impl FernVi {
pub fn new(prompt: Option<String>) -> Self { pub fn new(prompt: Option<String>) -> ShResult<Self> {
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold)); let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n"); let line = LineBuf::new().with_initial(LOREM_IPSUM);
let term = Terminal::new(); let term = Terminal::new();
Self { let history = History::new()?;
Ok(Self {
term, term,
line, line,
history,
prompt, prompt,
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
last_action: None, last_action: None,
last_movement: None, last_movement: None,
})
} }
} /// Ctrl+V handler
pub fn handle_verbatim(&mut self) -> ShResult<()> { pub fn handle_verbatim(&mut self) -> ShResult<()> {
let mut buf = [0u8; 8]; let mut buf = [0u8; 8];
let mut collected = Vec::new(); let mut collected = Vec::new();
@@ -151,6 +183,62 @@ impl FernVi {
} }
Ok(()) Ok(())
} }
pub fn scroll_history(&mut self, cmd: ViCmd) {
let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1;
flog!(DEBUG,count,motion);
let entry = match motion {
Motion::LineUp => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
return
};
flog!(DEBUG,"found entry");
flog!(DEBUG,hist_entry.command());
hist_entry
}
Motion::LineDown => {
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.line.saved_col().unwrap_or(self.line.cursor_column());
let mut buf = LineBuf::new().with_initial(entry.command());
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_fwd_to(line_end + 1);
}
}
} else {
let target = (col + 1).min(line_end + 1);
buf.cursor_fwd_to(target);
}
self.line = buf
}
pub fn should_grab_history(&self, cmd: &ViCmd) -> bool {
cmd.verb().is_none() &&
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) &&
self.line.start_of_line() == 0
) ||
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) &&
self.line.end_of_line() == self.line.byte_len()
)
}
pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> { pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> {
let (height,width) = self.term.get_dimensions()?; let (height,width) = self.term.get_dimensions()?;
if refresh { if refresh {
@@ -228,7 +316,7 @@ impl FernVi {
v_mut.0 = count v_mut.0 = count
} }
if let Some(m_mut) = cmd.motion.as_mut() { if let Some(m_mut) = cmd.motion.as_mut() {
m_mut.0 = 0 m_mut.0 = 1
} }
} else { } else {
return Ok(()) // it has to have a verb to be repeatable, something weird happened return Ok(()) // it has to have a verb to be repeatable, something weird happened

View File

@@ -40,6 +40,7 @@ pub trait ViMode {
fn pending_seq(&self) -> Option<String>; fn pending_seq(&self) -> Option<String>;
fn move_cursor_on_undo(&self) -> bool; fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool; fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;
} }
#[derive(Default,Debug)] #[derive(Default,Debug)]
@@ -145,6 +146,9 @@ impl ViMode for ViInsert {
fn clamp_cursor(&self) -> bool { fn clamp_cursor(&self) -> bool {
false false
} }
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
} }
#[derive(Default,Debug)] #[derive(Default,Debug)]
@@ -245,6 +249,9 @@ impl ViMode for ViReplace {
fn clamp_cursor(&self) -> bool { fn clamp_cursor(&self) -> bool {
true true
} }
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
} }
#[derive(Default,Debug)] #[derive(Default,Debug)]
pub struct ViNormal { pub struct ViNormal {
@@ -295,7 +302,9 @@ impl ViNormal {
} }
} }
/// End the parse and clear the pending sequence /// End the parse and clear the pending sequence
#[track_caller]
pub fn quit_parse(&mut self) -> Option<ViCmd> { pub fn quit_parse(&mut self) -> Option<ViCmd> {
flog!(DEBUG, std::panic::Location::caller());
flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq); flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq);
self.clear_cmd(); self.clear_cmd();
None None
@@ -359,6 +368,14 @@ impl ViNormal {
chars = chars_clone; chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
} }
'>' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Indent));
}
'<' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Dedent));
}
'r' => { 'r' => {
let ch = chars_clone.next()?; let ch = chars_clone.next()?;
return Some( return Some(
@@ -460,6 +477,16 @@ impl ViNormal {
} }
) )
} }
'J' => {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None,
raw_seq: self.take_cmd()
}
)
}
'y' => { 'y' => {
chars = chars_clone; chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Yank)) break 'verb_parse Some(VerbCmd(count, Verb::Yank))
@@ -521,6 +548,7 @@ impl ViNormal {
('d', Some(VerbCmd(_,Verb::Delete))) | ('d', Some(VerbCmd(_,Verb::Delete))) |
('c', Some(VerbCmd(_,Verb::Change))) | ('c', Some(VerbCmd(_,Verb::Change))) |
('y', Some(VerbCmd(_,Verb::Yank))) | ('y', Some(VerbCmd(_,Verb::Yank))) |
('=', Some(VerbCmd(_,Verb::Equalize))) |
('>', Some(VerbCmd(_,Verb::Indent))) | ('>', Some(VerbCmd(_,Verb::Indent))) |
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
_ => {} _ => {}
@@ -763,6 +791,9 @@ impl ViMode for ViNormal {
fn clamp_cursor(&self) -> bool { fn clamp_cursor(&self) -> bool {
true true
} }
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
} }
pub fn common_cmds(key: E) -> Option<ViCmd> { pub fn common_cmds(key: E) -> Option<ViCmd> {

View File

@@ -6,6 +6,7 @@ use std::mem::zeroed;
use std::io; use std::io;
use crate::libsh::error::ShResult; use crate::libsh::error::ShResult;
use crate::prelude::*;
use super::keys::{KeyCode, KeyEvent, ModKeys}; use super::keys::{KeyCode, KeyEvent, ModKeys};
@@ -195,6 +196,7 @@ impl Terminal {
} }
pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> { pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> {
flog!(DEBUG,lines);
self.cursor_records.lines = lines; self.cursor_records.lines = lines;
self.cursor_records.cols = col; self.cursor_records.cols = col;
self.cursor_records.offset = self.cursor_pos().1; self.cursor_records.offset = self.cursor_pos().1;
@@ -250,7 +252,7 @@ impl Terminal {
let tab_size = 8; let tab_size = 8;
let next_tab = tab_size - (self.write_records.cols % tab_size); let next_tab = tab_size - (self.write_records.cols % tab_size);
self.write_records.cols += next_tab; self.write_records.cols += next_tab;
if self.write_records.cols >= width { if self.write_records.cols > width {
self.write_records.lines += 1; self.write_records.lines += 1;
self.write_records.cols = 0; self.write_records.cols = 0;
} }

View File

@@ -223,6 +223,7 @@ impl Verb {
} }
pub fn is_char_insert(&self) -> bool { pub fn is_char_insert(&self) -> bool {
matches!(self, matches!(self,
Self::Change |
Self::InsertChar(_) | Self::InsertChar(_) |
Self::ReplaceChar(_) Self::ReplaceChar(_)
) )