diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 2a42a72..bd7334e 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -84,12 +84,6 @@ in default = true; description = "Whether to enable syntax highlighting in the shell"; }; - tabStop = lib.mkOption { - type = lib.types.int; - default = 4; - description = "The number of spaces to use for tab stop in the shell"; - }; - extraPostConfig = lib.mkOption { type = lib.types.str; default = ""; @@ -123,7 +117,6 @@ in "shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}" "shopt prompt.comp_limit=${toString cfg.settings.completionLimit}" "shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}" - "shopt prompt.tab_stop=${toString cfg.settings.tabStop}" ]) cfg.settings.extraPostConfig ]; diff --git a/src/parse/lex.rs b/src/parse/lex.rs index cc97c3a..19b350e 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -265,11 +265,12 @@ impl LexStream { } if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let span_start = self.cursor; self.cursor = pos; return Some(Err(ShErr::full( ShErrKind::ParseErr, "Invalid redirection", - Span::new(self.cursor..pos, self.source.clone()), + Span::new(span_start..pos, self.source.clone()), ))); } else { tk = self.get_token(self.cursor..pos, TkRule::Redir); diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index c7cc473..c7bdd9a 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -9,7 +9,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::linebuf::LineBuf}; use crate::prelude::*; use super::vicmd::Direction; // surprisingly useful @@ -207,7 +207,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec { pub struct History { path: PathBuf, - pub pending: Option<(String, usize)>, // command, cursor_pos + pub pending: Option, // command, cursor_pos entries: Vec, search_mask: Vec, no_matches: bool, @@ -272,7 +272,7 @@ impl History { pub fn update_pending_cmd(&mut self, buf: (&str, usize)) { let cursor_pos = if let Some(pending) = &self.pending { - pending.1 + pending.cursor.get() } else { buf.1 }; @@ -282,7 +282,10 @@ impl History { term: cmd.clone(), }; - self.pending = Some((cmd, cursor_pos)); + if let Some(pending) = &mut self.pending { + pending.set_buffer(cmd); + pending.cursor.set(cursor_pos); + } self.constrain_entries(constraint); } @@ -340,7 +343,7 @@ impl History { } pub fn get_hint(&self) -> Option { - if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) { + if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.buffer.is_empty()) { let entry = self.hint_entry()?; Some(entry.command().to_string()) } else { diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 86ac357..69e7717 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -14,8 +14,7 @@ use crate::{ libsh::{ error::ShResult, term::{Style, Styled}, - }, - prelude::*, prompt::readline::{markers, register::write_register}, + }, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::write_register}, state::read_shopts }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -327,6 +326,7 @@ pub struct LineBuf { pub insert_mode_start_pos: Option, pub saved_col: Option, + pub auto_indent_level: usize, pub undo_stack: Vec, pub redo_stack: Vec, @@ -409,7 +409,6 @@ 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(); self.cursor.set_max(indices.len()); @@ -1884,6 +1883,29 @@ impl LineBuf { let end = start + gr.len(); self.buffer.replace_range(start..end, new); } + pub fn calc_indent_level(&mut self) { + let input = Arc::new(self.buffer.clone()); + let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::>>() else { + log::error!("Failed to lex buffer for indent calculation"); + return; + }; + let mut level: usize = 0; + for tk in tokens { + if tk.flags.contains(TkFlags::KEYWORD) { + match tk.as_str() { + "then" | "do" => level += 1, + "done" | "fi" | "esac" => level = level.saturating_sub(1), + _ => { /* Continue */ } + } + } else if tk.class == TkRule::BraceGrpStart { + level += 1; + } else if tk.class == TkRule::BraceGrpEnd { + level = level.saturating_sub(1); + } + } + + self.auto_indent_level = level; + } pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { let buffer = self.buffer.clone(); if self.has_hint() { @@ -2669,12 +2691,13 @@ impl LineBuf { } } Verb::Dedent => { - let Some((start, end)) = self.range_from_motion(&motion) else { + let Some((start, mut end)) = self.range_from_motion(&motion) else { return Ok(()); }; if self.grapheme_at(start) == Some("\t") { self.remove(start); } + end = end.min(self.grapheme_indices().len().saturating_sub(1)); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); while let Some(idx) = range_indices.next() { let gr = self.grapheme_at(idx).unwrap(); @@ -2704,14 +2727,32 @@ impl LineBuf { Verb::Equalize => todo!(), Verb::InsertModeLineBreak(anchor) => { let (mut start, end) = self.this_line(); + let auto_indent = read_shopts(|o| o.prompt.auto_indent); if start == 0 && end == self.cursor.max { match anchor { Anchor::After => { self.push('\n'); + if auto_indent { + log::debug!("Calculating indent level for new line"); + self.calc_indent_level(); + log::debug!("Auto-indent level: {}", self.auto_indent_level); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + log::debug!("Pushing tab for auto-indent"); + self.push(tab); + } + } self.cursor.set(self.cursor_max()); return Ok(()); } Anchor::Before => { + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at(0, tab); + } + } self.insert_at(0, '\n'); self.cursor.set(0); return Ok(()); @@ -2724,11 +2765,28 @@ impl LineBuf { Anchor::After => { self.cursor.set(end); self.insert_at_cursor('\n'); + self.cursor.add(1); + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } } Anchor::Before => { self.cursor.set(start); self.insert_at_cursor('\n'); self.cursor.add(1); + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } } } } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 47f8d53..51f333c 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -249,7 +249,8 @@ impl FernVi { if cmd.should_submit() { self.editor.set_hint(None); - self.print_line()?; + self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end + self.print_line()?; // Redraw self.writer.flush_write("\n")?; let buf = self.editor.take_buf(); // Save command to history if auto_hist is enabled @@ -305,8 +306,7 @@ impl FernVi { pub fn get_layout(&mut self, line: &str) -> Layout { let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let (cols, _) = get_win_size(*TTY_FILENO); - let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16; - Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line) + Layout::from_parts(cols, &self.prompt, to_cursor, line) } pub fn scroll_history(&mut self, cmd: ViCmd) { /* @@ -324,19 +324,21 @@ impl FernVi { }; let entry = self.history.scroll(count); if let Some(entry) = entry { - let cursor_pos = self.editor.cursor.get(); - let pending = self.editor.take_buf(); + let editor = std::mem::take(&mut self.editor); self.editor.set_buffer(entry.command().to_string()); if self.history.pending.is_none() { - self.history.pending = Some((pending, cursor_pos)); + self.history.pending = Some(editor); } self.editor.set_hint(None); self.editor.move_cursor_to_end(); } else if let Some(pending) = self.history.pending.take() { - self.editor.set_buffer(pending.0); - self.editor.cursor.set(pending.1); - self.editor.set_hint(None); - } + self.editor = pending; + } else { + // If we are here it should mean we are on our pending command + // And the user tried to scroll history down + // Since there is no "future" history, we should just bell and do nothing + self.writer.send_bell().ok(); + } } pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { if self.editor.cursor_at_max() && self.editor.has_hint() { diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 118fc3b..76b8dac 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -107,6 +107,30 @@ fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> { Ok(()) } +/// Check if a string ends with a newline, ignoring any trailing ANSI escape sequences. +fn ends_with_newline(s: &str) -> bool { + let bytes = s.as_bytes(); + let mut i = bytes.len(); + while i > 0 { + // ANSI CSI sequences end with an alphabetic byte (e.g. \x1b[0m) + if bytes[i - 1].is_ascii_alphabetic() { + let term = i - 1; + let mut j = term; + // Walk back past parameter bytes (digits and ';') + while j > 0 && (bytes[j - 1].is_ascii_digit() || bytes[j - 1] == b';') { + j -= 1; + } + // Check for CSI introducer \x1b[ + if j >= 2 && bytes[j - 1] == b'[' && bytes[j - 2] == 0x1b { + i = j - 2; + continue; + } + } + break; + } + i > 0 && bytes[i - 1] == b'\n' +} + // Big credit to rustyline for this fn width(s: &str, esc_seq: &mut u8) -> u16 { let w_calc = width_calculator(); @@ -734,15 +758,14 @@ impl Layout { } } pub fn from_parts( - tab_stop: u16, term_width: u16, prompt: &str, to_cursor: &str, to_end: &str, ) -> Self { - let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 }); - let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end); - let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end); + let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 }); + let cursor = Self::calc_pos(term_width, to_cursor, prompt_end); + let end = Self::calc_pos(term_width, to_end, prompt_end); Layout { w_calc: width_calculator(), prompt_end, @@ -751,7 +774,8 @@ impl Layout { } } - pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos { + pub fn calc_pos(term_width: u16, s: &str, orig: Pos) -> Pos { + const TAB_STOP: u16 = 8; let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { @@ -760,7 +784,7 @@ impl Layout { pos.col = 0; } let c_width = if c == "\t" { - tab_stop - (pos.col % tab_stop) + TAB_STOP - (pos.col % TAB_STOP) } else { width(c, &mut esc_seq) }; @@ -790,7 +814,6 @@ pub struct TermWriter { t_cols: Col, // terminal width buffer: String, w_calc: Box, - tab_stop: u16, } impl TermWriter { @@ -802,7 +825,6 @@ impl TermWriter { t_cols, buffer: String::new(), w_calc, - tab_stop: 8, // TODO: add a way to configure this } } pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult { @@ -959,7 +981,7 @@ impl LineWriter for TermWriter { self.buffer.push_str(prompt); self.buffer.push_str(line); - if end.col == 0 && end.row > 0 && !self.buffer.ends_with('\n') { + if end.col == 0 && end.row > 0 && !ends_with_newline(&self.buffer) { // The line has wrapped. We need to use our own line break. self.buffer.push('\n'); } diff --git a/src/shopt.rs b/src/shopt.rs index 1d1fbbd..6f2de54 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -371,7 +371,7 @@ pub struct ShOptPrompt { pub edit_mode: FernEditMode, pub comp_limit: usize, pub highlight: bool, - pub tab_stop: usize, + pub auto_indent: bool } impl ShOptPrompt { @@ -413,15 +413,15 @@ impl ShOptPrompt { }; self.highlight = val; } - "tab_stop" => { - let Ok(val) = val.parse::() else { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - "shopt: expected a positive integer for tab_stop value", - )); - }; - self.tab_stop = val; - } + "auto_indent" => { + let Ok(val) = val.parse::() else { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for auto_indent value", + )); + }; + self.auto_indent = val; + } "custom" => { todo!() } @@ -440,7 +440,7 @@ impl ShOptPrompt { "edit_mode", "comp_limit", "highlight", - "tab_stop", + "auto_indent", "custom", ]), ), @@ -480,11 +480,11 @@ impl ShOptPrompt { output.push_str(&format!("{}", self.highlight)); Ok(Some(output)) } - "tab_stop" => { - let mut output = String::from("The number of spaces used by the tab character '\\t'\n"); - output.push_str(&format!("{}", self.tab_stop)); - Ok(Some(output)) - } + "auto_indent" => { + let mut output = String::from("Whether to automatically indent new lines in multiline commands\n"); + output.push_str(&format!("{}", self.auto_indent)); + Ok(Some(output)) + } _ => Err( ShErr::simple( ShErrKind::SyntaxErr, @@ -499,7 +499,7 @@ impl ShOptPrompt { "edit_mode", "comp_limit", "highlight", - "tab_stop", + "auto_indent", ]), ), ), @@ -515,7 +515,7 @@ impl Display for ShOptPrompt { output.push(format!("edit_mode = {}", self.edit_mode)); output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("highlight = {}", self.highlight)); - output.push(format!("tab_stop = {}", self.tab_stop)); + output.push(format!("auto_indent = {}", self.auto_indent)); let final_output = output.join("\n"); @@ -530,7 +530,7 @@ impl Default for ShOptPrompt { edit_mode: FernEditMode::Vi, comp_limit: 100, highlight: true, - tab_stop: 4, + auto_indent: true } } }