diff --git a/src/expand.rs b/src/expand.rs index 66d524f..efeca46 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -13,6 +13,16 @@ use crate::parse::execute::exec_input; use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +const PARAMETERS: [char;7] = [ + '@', + '*', + '#', + '$', + '?', + '!', + '0' +]; + /// Variable substitution marker pub const VAR_SUB: char = '\u{fdd0}'; /// Double quote '"' marker @@ -170,6 +180,13 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { chars.next(); // safe to consume var_name.push(ch); } + ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { + chars.next(); + let parameter = format!("{ch}"); + let val = read_vars(|v| v.get_var(¶meter)); + flog!(DEBUG, val); + return Ok(val) + } ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => { let val = read_vars(|v| v.get_var(&var_name)); flog!(INFO,var_name); @@ -627,7 +644,13 @@ pub fn unescape_str(raw: &str) -> String { } } } - '$' => result.push(VAR_SUB), + '$' => { + result.push(VAR_SUB); + if chars.peek() == Some(&'$') { + chars.next(); + result.push('$'); + } + } _ => result.push(ch) } first_char = false; diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 4a1b1a2..0d66a38 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -198,13 +198,14 @@ impl Dispatcher { self.io_stack.append_to_frame(func.redirs); let func_name = argv.remove(0).span.as_str().to_string(); + let argv = prepare_argv(argv)?; if let Some(func) = read_logic(|l| l.get_func(&func_name)) { let snapshot = get_snapshots(); // Set up the inner scope write_vars(|v| { **v = VarTab::new(); v.clear_args(); - for arg in argv { + for (arg,_) in argv { v.bpush_arg(arg.to_string()); } }); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e95f54e..e25ea94 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -696,6 +696,8 @@ impl ParseStream { return Ok(None) } node_tks.push(self.next_tk().unwrap()); + + self.catch_separator(&mut node_tks); loop { @@ -707,6 +709,9 @@ impl ParseStream { node_tks.extend(node.tokens.clone()); body.push(node); } + dbg!(&self.tokens); + panic!("{body:#?}"); + self.catch_separator(&mut node_tks); if !self.next_tk_is_some() { self.panic_mode(&mut node_tks); return Err(parse_err_full( diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index 41984d4..0abef8e 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -1,11 +1,30 @@ 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::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; use crate::prelude::*; use super::vicmd::Direction; // surprisingly useful -#[derive(Debug)] +#[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, @@ -42,6 +61,9 @@ impl HistEntry { } escaped } + pub fn is_new(&self) -> bool { + self.new + } } impl FromStr for HistEntry { @@ -145,6 +167,7 @@ fn read_hist_file(path: &Path) -> ShResult> { pub struct History { path: PathBuf, entries: Vec, + search_mask: Vec, cursor: usize, search_direction: Direction, ignore_dups: bool, @@ -157,11 +180,19 @@ impl History { let home = env::var("HOME").unwrap(); format!("{home}/.fern_history") })); - let entries = read_hist_file(&path)?; - let cursor = entries.len(); + 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, @@ -176,19 +207,22 @@ impl History { } 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 cursor_entry(&self) -> Option<&HistEntry> { + self.search_mask.get(self.cursor) } pub fn update_pending_cmd(&mut self, command: &str) { - flog!(DEBUG, "updating command"); let Some(ent) = self.last_mut() else { return }; + let cmd = command.to_string(); + let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() }; - ent.command = command.to_string() + + ent.command = cmd; + self.constrain_entries(constraint); } pub fn last_mut(&mut self) -> Option<&mut HistEntry> { @@ -210,11 +244,46 @@ impl History { self.max_size = size } + pub fn constrain_entries(&mut self, constraint: SearchConstraint) { + 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().strip_prefix(prefix)?.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.entries.len()); - let ent = self.entries.get(new_idx)?; + .clamp(0, self.search_mask.len().saturating_sub(1)); + let ent = self.search_mask.get(new_idx)?; self.cursor = new_idx; @@ -244,7 +313,7 @@ impl History { .append(true) .open(&self.path)?; - let entries = self.entries.iter_mut().filter(|ent| ent.new); + let entries = self.entries.iter_mut().filter(|ent| ent.new && !ent.command.is_empty()); let mut data = String::new(); for ent in entries { ent.new = false; diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 3eb1406..d4ded81 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -3,7 +3,7 @@ use std::{cmp::Ordering, fmt::Display, ops::{Range, RangeBounds}}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::libsh::{error::ShResult, sys::sh_quit}; +use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}}; use crate::prelude::*; use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word}; @@ -147,6 +147,7 @@ impl Edit { #[derive(Default,Debug)] pub struct LineBuf { buffer: String, + hint: Option, cursor: usize, clamp_cursor: bool, first_line_offset: usize, @@ -166,6 +167,12 @@ impl LineBuf { self.buffer = initial.to_string(); self } + pub fn has_hint(&self) -> bool { + self.hint.is_some() + } + pub fn set_hint(&mut self, hint: Option) { + self.hint = hint + } pub fn set_first_line_offset(&mut self, offset: usize) { self.first_line_offset = offset } @@ -189,6 +196,13 @@ impl LineBuf { pub fn byte_len(&self) -> usize { self.buffer.len() } + pub fn at_end_of_buffer(&self) -> bool { + if self.clamp_cursor { + self.cursor == self.byte_len().saturating_sub(1) + } else { + self.cursor == self.byte_len() + } + } pub fn undos(&self) -> usize { self.undo_stack.len() } @@ -480,7 +494,11 @@ impl LineBuf { self.move_to(0) } pub fn move_buf_end(&mut self) -> bool { - self.move_to(self.byte_len()) + if self.clamp_cursor { + self.move_to(self.byte_len().saturating_sub(1)) + } else { + self.move_to(self.byte_len()) + } } pub fn move_home(&mut self) -> bool { let start = self.start_of_line(); @@ -490,6 +508,63 @@ impl LineBuf { let end = self.end_of_line(); self.move_to(end) } + pub fn accept_hint(&mut self) { + if let Some(hint) = self.hint.take() { + flog!(DEBUG, "accepting hint"); + let old_buf = self.buffer.clone(); + self.buffer.push_str(&hint); + let new_buf = self.buffer.clone(); + self.handle_edit(old_buf, new_buf, self.cursor); + self.move_buf_end(); + } + } + pub fn accept_hint_partial(&mut self, accept_to: usize) { + if let Some(hint) = self.hint.take() { + let accepted = &hint[..accept_to]; + let remainder = &hint[accept_to..]; + self.buffer.push_str(accepted); + self.hint = Some(remainder.to_string()); + } + } + /// If we have a hint, then motions are able to extend into it + /// and partially accept pieces of it, instead of the whole thing + pub fn apply_motion_with_hint(&mut self, motion: MotionKind) { + let buffer_end = self.byte_len().saturating_sub(1); + flog!(DEBUG,self.hint); + if let Some(hint) = self.hint.take() { + self.buffer.push_str(&hint); + flog!(DEBUG,motion); + self.apply_motion(/*forced*/ true, motion); + flog!(DEBUG, self.cursor); + flog!(DEBUG, self.grapheme_at_cursor()); + if self.cursor > buffer_end { + let remainder = if self.clamp_cursor { + self.slice_from((self.cursor + 1).min(self.byte_len())) + } else { + self.slice_from_cursor() + }; + flog!(DEBUG,remainder); + if !remainder.is_empty() { + self.hint = Some(remainder.to_string()); + } + let buffer = if self.clamp_cursor { + self.slice_to((self.cursor + 1).min(self.byte_len())) + } else { + self.slice_to_cursor() + }; + flog!(DEBUG,buffer); + self.buffer = buffer.to_string(); + flog!(DEBUG,self.hint); + } else { + let old_hint = self.slice_from(buffer_end + 1); + flog!(DEBUG,old_hint); + self.hint = Some(old_hint.to_string()); + let buffer = self.slice_to(buffer_end + 1); + flog!(DEBUG,buffer); + self.buffer = buffer.to_string(); + } + } + } pub fn find_prev_line_pos(&mut self) -> Option { if self.start_of_line() == 0 { return None @@ -799,10 +874,16 @@ impl LineBuf { return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) } if self.on_start_of_word(word) { + let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); pos += 1; if pos >= self.byte_len() { return None } + let next_char = self.grapheme_at(self.next_pos(1)?)?; + let next_char_class = CharClass::from(next_char); + if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace { + return Some(pos) + } } let cur_graph = self.grapheme_at(pos)?; let diff_class_pos = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph))?; @@ -814,15 +895,24 @@ impl LineBuf { } } To::End => { + flog!(DEBUG,self.buffer); if self.on_whitespace() { pos = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; } match self.on_end_of_word(word) { true => { + flog!(DEBUG, "on end of word"); + let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); pos += 1; if pos >= self.byte_len() { return None } + let next_char = self.grapheme_at(self.next_pos(1)?)?; + let next_char_class = CharClass::from(next_char); + if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace { + return Some(pos) + } + let cur_graph = self.grapheme_at(pos)?; match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) { Some(n) => { @@ -844,7 +934,9 @@ impl LineBuf { } } false => { + flog!(DEBUG, "not on end of word"); let cur_graph = self.grapheme_at(pos)?; + flog!(DEBUG,cur_graph); match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) { Some(n) => Some(n.saturating_sub(1)), // Land on char before other char class None => Some(self.byte_len()) // End of buffer @@ -863,6 +955,13 @@ impl LineBuf { match self.on_start_of_word(word) { true => { pos = pos.checked_sub(1)?; + let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); + let prev_char = self.grapheme_at(self.prev_pos(1)?)?; + let prev_char_class = CharClass::from(prev_char); + let is_diff_class = cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace; + if is_diff_class && self.is_start_of_word(Word::Normal, self.prev_pos(1)?) { + return Some(pos) + } let prev_word_end = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; let cur_graph = self.grapheme_at(prev_word_end)?; match self.rfind_from(prev_word_end, |c| is_other_class_or_ws(c, cur_graph)) { @@ -885,6 +984,12 @@ impl LineBuf { } if self.on_end_of_word(word) { pos = pos.checked_sub(1)?; + let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); + let prev_char = self.grapheme_at(self.prev_pos(1)?)?; + let prev_char_class = CharClass::from(prev_char); + if cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace { + return Some(pos) + } } let cur_graph = self.grapheme_at(pos)?; let diff_class_pos = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph))?; @@ -938,7 +1043,19 @@ impl LineBuf { } None } + pub fn eval_motion_with_hint(&mut self, motion: Motion) -> MotionKind { + let Some(hint) = self.hint.as_ref() else { + return MotionKind::Null + }; + let buffer = self.buffer.clone(); + self.buffer.push_str(hint); + let motion_eval = self.eval_motion(motion); + self.buffer = buffer; + motion_eval + } pub fn eval_motion(&mut self, motion: Motion) -> MotionKind { + flog!(DEBUG,self.buffer); + flog!(DEBUG,motion); match motion { Motion::WholeLine => MotionKind::Line(0), Motion::TextObj(text_obj, bound) => todo!(), @@ -1461,6 +1578,7 @@ impl LineBuf { self.undo_stack.push(edit); } else { let diff = Edit::diff(&old, &new, curs_pos); + flog!(DEBUG, diff); if !diff.is_empty() { self.undo_stack.push(diff); } @@ -1489,15 +1607,23 @@ impl LineBuf { for _ in 0..verb_count.unwrap_or(1) { for _ in 0..motion_count.unwrap_or(1) { - let motion = motion + let motion_eval = motion .clone() .map(|m| self.eval_motion(m.1)) .unwrap_or(MotionKind::Null); + flog!(DEBUG,self.hint); if let Some(verb) = verb.clone() { - self.exec_verb(verb.1, motion, register)?; + self.exec_verb(verb.1, motion_eval, register)?; + } else if self.has_hint() { + let motion_eval = motion + .clone() + .map(|m| self.eval_motion_with_hint(m.1)) + .unwrap_or(MotionKind::Null); + flog!(DEBUG, "applying motion with hint"); + self.apply_motion_with_hint(motion_eval); } else { - self.apply_motion(/*forced*/ false,motion); + self.apply_motion(/*forced*/ false,motion_eval); } } } @@ -1531,7 +1657,11 @@ impl LineBuf { impl Display for LineBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f,"{}",self.buffer) + let mut full_buf = self.buffer.clone(); + if let Some(hint) = self.hint.as_ref() { + full_buf.push_str(&hint.styled(Style::BrightBlack)); + } + write!(f,"{}",full_buf) } } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index a617a90..982aea2 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use history::History; +use history::{History, SearchConstraint, SearchKind}; use keys::{KeyCode, KeyEvent, ModKeys}; use linebuf::{strip_ansi_codes_and_escapes, LineBuf}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal, ViReplace}; @@ -65,6 +65,12 @@ impl Readline for FernVi { self.handle_verbatim()?; continue } + if self.should_accept_hint(&key) { + self.line.accept_hint(); + self.history.update_pending_cmd(self.line.as_str()); + self.print_buf(true)?; + continue + } let Some(cmd) = self.mode.handle_key(key) else { continue @@ -78,7 +84,9 @@ impl Readline for FernVi { } + if cmd.should_submit() { + self.term.unposition_cursor()?; self.term.write("\n"); let command = self.line.to_string(); if !command.is_empty() { @@ -95,7 +103,7 @@ impl Readline for FernVi { let has_changes = line != new_line; flog!(DEBUG, has_changes); - if cmd.verb().is_some_and(|v| v.1.is_edit()) && has_changes { + if has_changes { self.history.update_pending_cmd(self.line.as_str()); } @@ -120,6 +128,16 @@ impl FernVi { last_movement: None, }) } + pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { + if self.line.at_end_of_buffer() && self.line.has_hint() { + matches!( + event, + KeyEvent(KeyCode::Right, ModKeys::NONE) + ) + } else { + false + } + } /// Ctrl+V handler pub fn handle_verbatim(&mut self) -> ShResult<()> { let mut buf = [0u8; 8]; @@ -184,6 +202,10 @@ impl FernVi { Ok(()) } pub fn scroll_history(&mut self, cmd: ViCmd) { + if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { + let constraint = SearchConstraint::new(SearchKind::Prefix, self.line.to_string()); + self.history.constrain_entries(constraint); + } let count = &cmd.motion().unwrap().0; let motion = &cmd.motion().unwrap().1; flog!(DEBUG,count,motion); @@ -244,11 +266,14 @@ impl FernVi { if refresh { self.term.unwrite()?; } + let hint = self.history.get_hint(); + self.line.set_hint(hint); + let offset = self.calculate_prompt_offset(); self.line.set_first_line_offset(offset); self.line.update_term_dims((height,width)); let mut line_buf = self.prompt.clone(); - line_buf.push_str(self.line.as_str()); + line_buf.push_str(&self.line.to_string()); self.term.recorded_write(&line_buf, offset)?; self.term.position_cursor(self.line.cursor_display_coords(width))?;