diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index e84abae..6da5d5f 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -3,7 +3,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, readline::complete::{BashCompSpec, CompContext, CompSpec}, state::{self, read_meta, write_meta}}; -pub const COMPGEN_OPTS: [OptSpec;7] = [ +pub const COMPGEN_OPTS: [OptSpec;8] = [ OptSpec { opt: Opt::Short('F'), takes_arg: true @@ -31,10 +31,14 @@ pub const COMPGEN_OPTS: [OptSpec;7] = [ OptSpec { opt: Opt::Short('v'), takes_arg: false + }, + OptSpec { + opt: Opt::Short('o'), + takes_arg: true } ]; -pub const COMP_OPTS: [OptSpec;10] = [ +pub const COMP_OPTS: [OptSpec;11] = [ OptSpec { opt: Opt::Short('F'), takes_arg: true @@ -74,6 +78,10 @@ pub const COMP_OPTS: [OptSpec;10] = [ OptSpec { opt: Opt::Short('v'), takes_arg: false + }, + OptSpec { + opt: Opt::Short('o'), + takes_arg: true } ]; @@ -88,6 +96,12 @@ bitflags! { const PRINT = 0b0000100000; const REMOVE = 0b0001000000; } + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CompOptFlags: u32 { + const DEFAULT = 0b0000000001; + const DIRNAMES = 0b0000000010; + const NOSPACE = 0b0000000100; + } } #[derive(Default, Debug, Clone)] @@ -95,7 +109,8 @@ pub struct CompOpts { pub func: Option, pub wordlist: Option>, pub action: Option, - pub flags: CompFlags + pub flags: CompFlags, + pub opt_flags: CompOptFlags, } pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { @@ -229,6 +244,20 @@ pub fn get_comp_opts(opts: Vec) -> ShResult { Opt::ShortWithArg('A',action) => { comp_opts.action = Some(action); } + Opt::ShortWithArg('o', opt_flag) => { + match opt_flag.as_str() { + "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, + "dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, + "nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE, + _ => { + return Err(ShErr::full( + ShErrKind::InvalidOpt, + format!("complete: invalid option: {}", opt_flag), + Default::default() + )); + } + } + } Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 5479b7b..a427b31 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,9 +1,7 @@ use std::fmt::Display; use crate::{ - libsh::term::{Style, Styled}, - parse::lex::Span, - prelude::*, + getopt::Opt, libsh::term::{Style, Styled}, parse::lex::Span, prelude::* }; pub type ShResult = Result; @@ -395,6 +393,7 @@ impl From for ShErr { #[derive(Debug, Clone)] pub enum ShErrKind { IoErr(io::ErrorKind), + InvalidOpt, SyntaxErr, ParseErr, InternalErr, @@ -421,6 +420,7 @@ impl Display for ShErrKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let output = match self { Self::IoErr(e) => &format!("I/O Error: {e}"), + Self::InvalidOpt => &format!("Invalid option"), Self::SyntaxErr => "Syntax Error", Self::ParseErr => "Parse Error", Self::InternalErr => "Internal Error", diff --git a/src/libsh/flog.rs b/src/libsh/flog.rs index 5f49fa1..d3c5160 100644 --- a/src/libsh/flog.rs +++ b/src/libsh/flog.rs @@ -29,7 +29,7 @@ impl Display for ShedLogLevel { pub fn log_level() -> ShedLogLevel { use ShedLogLevel::*; - let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default(); + let level = std::env::var("SHED_LOG_LEVEL").unwrap_or_default(); match level.to_lowercase().as_str() { "error" => ERROR, "warn" => WARN, diff --git a/src/main.rs b/src/main.rs index aaf6206..f3689db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,7 @@ fn kickstart_lazy_evals() { fn setup_panic_handler() { let default_panic_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - let _ = state::FERN.try_with(|shed| { + let _ = state::SHED.try_with(|shed| { if let Ok(mut jobs) = shed.jobs.try_borrow_mut() { jobs.hang_up(); } @@ -185,7 +185,7 @@ fn shed_interactive() -> ShResult<()> { match e.kind() { ShErrKind::ClearReadline => { // Ctrl+C - clear current input and show new prompt - readline.reset(); + readline.reset(false)?; } ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -269,7 +269,7 @@ fn shed_interactive() -> ShResult<()> { readline.writer.flush_write("\n")?; // Reset for next command with fresh prompt - readline.reset(); + readline.reset(true)?; let real_end = start.elapsed(); log::info!("Total round trip time: {:.2?}", real_end); } diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 1fcad52..39340c8 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -1,9 +1,9 @@ use std::{collections::HashSet, env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; use crate::{ - builtin::{BUILTINS, complete::{CompFlags, CompOpts}}, + builtin::{BUILTINS, complete::{CompFlags, CompOptFlags, CompOpts}}, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::TkVecUtils}, - parse::{execute::{VarCtxGuard, exec_input}, lex::{self, LexFlags, Tk, TkFlags, TkRule}}, + parse::{execute::{VarCtxGuard, exec_input}, lex::{self, LexFlags, Tk, TkFlags, TkRule, ends_with_unescaped}}, readline::{ Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}, @@ -167,6 +167,12 @@ fn complete_filename(start: &str) -> Vec { candidates } +pub enum CompSpecResult { + NoSpec, // No compspec registered + NoMatch { flags: CompOptFlags }, // Compspec found but no candidates matched, returns behavior flags + Match(CompResult) // Compspec found and candidates returned +} + #[derive(Default,Debug,Clone)] pub struct BashCompSpec { /// -F: The name of a function to generate the possible completions. @@ -186,6 +192,7 @@ pub struct BashCompSpec { /// -A signal: complete signal names pub signals: bool, + pub flags: CompOptFlags, /// The original command pub source: String } @@ -231,7 +238,7 @@ impl BashCompSpec { self } pub fn from_comp_opts(opts: CompOpts) -> Self { - let CompOpts { func, wordlist, action: _, flags } = opts; + let CompOpts { func, wordlist, action: _, flags, opt_flags } = opts; Self { function: func, wordlist, @@ -240,6 +247,7 @@ impl BashCompSpec { commands: flags.contains(CompFlags::CMDS), users: flags.contains(CompFlags::USERS), vars: flags.contains(CompFlags::VARS), + flags: opt_flags, signals: false, // TODO: implement signal completion source: String::new() } @@ -320,11 +328,18 @@ impl CompSpec for BashCompSpec { fn source(&self) -> &str { &self.source } + + fn get_flags(&self) -> CompOptFlags { + self.flags + } } pub trait CompSpec: Debug + CloneCompSpec { fn complete(&self, ctx: &CompContext) -> ShResult>; fn source(&self) -> &str; + fn get_flags(&self) -> CompOptFlags { + CompOptFlags::empty() + } } pub trait CloneCompSpec { @@ -376,23 +391,20 @@ impl CompResult { } } +#[derive(Default,Debug,Clone)] pub struct Completer { pub candidates: Vec, pub selected_idx: usize, pub original_input: String, pub token_span: (usize, usize), pub active: bool, + pub dirs_only: bool, + pub no_space: bool } impl Completer { pub fn new() -> Self { - Self { - candidates: vec![], - selected_idx: 0, - original_input: String::new(), - token_span: (0, 0), - active: false, - } + Self::default() } pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) { @@ -487,11 +499,29 @@ impl Completer { self.get_completed_line() } + pub fn add_spaces(&mut self) { + if !self.no_space { + self.candidates = std::mem::take(&mut self.candidates) + .into_iter() + .map(|c| { + if !ends_with_unescaped(&c, "/") // directory + && !ends_with_unescaped(&c, "=") // '='-type arg + && !ends_with_unescaped(&c, " ") { // already has a space + format!("{} ", c) + } else { + c + } + }) + .collect() + } + } + pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult> { let result = self.get_candidates(line.clone(), cursor_pos)?; match result { CompResult::Many { candidates } => { self.candidates = candidates.clone(); + self.add_spaces(); self.selected_idx = 0; self.original_input = line; self.active = true; @@ -500,6 +530,7 @@ impl Completer { } CompResult::Single { result } => { self.candidates = vec![result.clone()]; + self.add_spaces(); self.selected_idx = 0; self.original_input = line; self.active = false; @@ -578,20 +609,20 @@ impl Completer { Ok(ctx) } - pub fn try_comp_spec(&self, ctx: &CompContext) -> ShResult { + pub fn try_comp_spec(&self, ctx: &CompContext) -> ShResult { let Some(cmd) = ctx.cmd() else { - return Ok(CompResult::NoMatch); + return Ok(CompSpecResult::NoSpec); }; let Some(spec) = read_meta(|m| m.get_comp_spec(cmd)) else { - return Ok(CompResult::NoMatch); + return Ok(CompSpecResult::NoSpec); }; let candidates = spec.complete(ctx)?; if candidates.is_empty() { - Ok(CompResult::NoMatch) + Ok(CompSpecResult::NoMatch { flags: spec.get_flags() }) } else { - Ok(CompResult::from_candidates(candidates)) + Ok(CompSpecResult::Match(CompResult::from_candidates(candidates))) } } @@ -610,10 +641,26 @@ impl Completer { } // Try programmable completion first - let res = self.try_comp_spec(&ctx)?; - if !matches!(res, CompResult::NoMatch) { - return Ok(res); - } + + match self.try_comp_spec(&ctx)? { + CompSpecResult::NoMatch { flags } => { + if flags.contains(CompOptFlags::DIRNAMES) { + self.dirs_only = true; + } else if flags.contains(CompOptFlags::DEFAULT) { + /* fall through */ + } else { + return Ok(CompResult::NoMatch); + } + + if flags.contains(CompOptFlags::NOSPACE) { + self.no_space = true; + } + } + CompSpecResult::Match(comp_result) => { + return Ok(comp_result); + } + CompSpecResult::NoSpec => { /* carry on */ } + } // Get the current token from CompContext let Some(mut cur_token) = ctx.words.get(ctx.cword).cloned() else { @@ -648,6 +695,7 @@ impl Completer { let last_marker = marker_ctx.last().copied(); let mut candidates = match marker_ctx.pop() { + _ if self.dirs_only => complete_dirs(&expanded), Some(markers::COMMAND) => complete_commands(&expanded), Some(markers::VAR_SUB) => { let var_candidates = complete_vars(&raw_tk); @@ -682,9 +730,3 @@ impl Completer { } } - -impl Default for Completer { - fn default() -> Self { - Self::new() - } -} diff --git a/src/readline/highlight.rs b/src/readline/highlight.rs index d3d3302..7127130 100644 --- a/src/readline/highlight.rs +++ b/src/readline/highlight.rs @@ -292,19 +292,19 @@ impl Highlighter { fn is_valid(command: &str) -> bool { let cmd_path = Path::new(&command); - if cmd_path.is_absolute() { - // the user has given us an absolute path - if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { - // this is a directory and autocd is enabled - true - } else { - let Ok(meta) = cmd_path.metadata() else { - return false; - }; - // this is a file that is executable by someone - meta.permissions().mode() & 0o111 != 0 - } - } else { + if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { + // this is a directory and autocd is enabled + return true; + } + + if cmd_path.is_absolute() { + // the user has given us an absolute path + let Ok(meta) = cmd_path.metadata() else { + return false; + }; + // this is a file that is executable by someone + meta.permissions().mode() & 0o111 != 0 + } else { read_meta(|m| m.cached_cmds().get(command).is_some()) } } diff --git a/src/readline/history.rs b/src/readline/history.rs index cfb3ffb..322862b 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -224,7 +224,7 @@ impl History { pub fn new() -> ShResult { let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes); let max_hist = crate::state::read_shopts(|s| s.core.max_hist); - let path = PathBuf::from(env::var("FERNHIST").unwrap_or({ + let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({ let home = env::var("HOME").unwrap(); format!("{home}/.shed_history") })); diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 8dfb2b9..d664b5d 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -771,6 +771,14 @@ impl LineBuf { } Some(self.line_bounds(line_no)) } + pub fn this_line_exclusive(&mut self) -> (usize, usize) { + let line_no = self.cursor_line_number(); + let (start, mut end) = self.line_bounds(line_no); + if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") { + end = end.saturating_sub(1); + } + (start, end) + } pub fn this_line(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); self.line_bounds(line_no) @@ -781,6 +789,9 @@ impl LineBuf { pub fn end_of_line(&mut self) -> usize { self.this_line().1 } + pub fn end_of_line_exclusive(&mut self) -> usize { + self.this_line_exclusive().1 + } pub fn select_lines_up(&mut self, n: usize) -> Option<(usize, usize)> { if self.start_of_line() == 0 { return None; @@ -1932,7 +1943,7 @@ impl LineBuf { for tk in tokens { if tk.flags.contains(TkFlags::KEYWORD) { match tk.as_str() { - "then" | "do" => level += 1, + "then" | "do" | "in" => level += 1, "done" | "fi" | "esac" => level = level.saturating_sub(1), _ => { /* Continue */ } } @@ -2476,7 +2487,7 @@ impl LineBuf { log::debug!("self.grapheme_indices().len(): {}", self.grapheme_indices().len()); let mut do_indent = false; - if verb == Verb::Change && (start,end) == self.this_line() { + if verb == Verb::Change && (start,end) == self.this_line_exclusive() { do_indent = read_shopts(|o| o.prompt.auto_indent); } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index ade2fdb..fbf045e 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -257,14 +257,19 @@ impl ShedVi { /// Reset readline state for a new prompt - pub fn reset(&mut self) { + pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { + // Clear old display before resetting state — old_layout must survive + // so print_line can call clear_rows with the full multi-line layout self.prompt = Prompt::new(); self.editor = Default::default(); self.mode = Box::new(ViInsert::new()); - self.old_layout = None; self.needs_redraw = true; + if full_redraw { + self.old_layout = None; + } self.history.pending = None; self.history.reset(); + self.print_line(false) } pub fn prompt(&self) -> &Prompt { @@ -276,6 +281,9 @@ impl ShedVi { } fn should_submit(&mut self) -> ShResult { + if self.mode.report_mode() == ModeReport::Normal { + return Ok(true); + } let input = Arc::new(self.editor.buffer.clone()); self.editor.calc_indent_level(); let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::>>(); @@ -562,6 +570,7 @@ impl ShedVi { self.writer.flush_write(&self.mode.cursor_style())?; self.old_layout = Some(new_layout); + self.needs_redraw = false; Ok(()) } diff --git a/src/readline/vimode.rs b/src/readline/vimode.rs index fc78950..dd1db21 100644 --- a/src/readline/vimode.rs +++ b/src/readline/vimode.rs @@ -675,7 +675,6 @@ impl ViNormal { // Double inputs ('?', Some(VerbCmd(_, Verb::Rot13))) | ('d', Some(VerbCmd(_, Verb::Delete))) - | ('c', Some(VerbCmd(_, Verb::Change))) | ('y', Some(VerbCmd(_, Verb::Yank))) | ('=', Some(VerbCmd(_, Verb::Equalize))) | ('u', Some(VerbCmd(_, Verb::ToLower))) @@ -685,6 +684,9 @@ impl ViNormal { | ('<', Some(VerbCmd(_, Verb::Dedent))) => { break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive)); } + ('c', Some(VerbCmd(_, Verb::Change))) => { + break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); + } ('W', Some(VerbCmd(_, Verb::Change))) => { // Same with 'W' break 'motion_parse Some(MotionCmd( diff --git a/src/state.rs b/src/state.rs index 5f1636a..4bab78a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -378,7 +378,7 @@ impl ScopeStack { } thread_local! { - pub static FERN: Shed = Shed::new(); + pub static SHED: Shed = Shed::new(); } /// A shell function @@ -751,8 +751,8 @@ impl VarTab { env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); env::set_var("HOME", home.clone()); env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); - env::set_var("FERN_HIST", format!("{}/.shedhist", home)); - env::set_var("FERN_RC", format!("{}/.shedrc", home)); + env::set_var("SHED_HIST", format!("{}/.shedhist", home)); + env::set_var("SHED_RC", format!("{}/.shedrc", home)); } } pub fn init_sh_argv(&mut self) { @@ -1131,22 +1131,22 @@ impl MetaTab { /// Read from the job table pub fn read_jobs T>(f: F) -> T { - FERN.with(|shed| f(&shed.jobs.borrow())) + SHED.with(|shed| f(&shed.jobs.borrow())) } /// Write to the job table pub fn write_jobs T>(f: F) -> T { - FERN.with(|shed| f(&mut shed.jobs.borrow_mut())) + SHED.with(|shed| f(&mut shed.jobs.borrow_mut())) } /// Read from the var scope stack pub fn read_vars T>(f: F) -> T { - FERN.with(|shed| f(&shed.var_scopes.borrow())) + SHED.with(|shed| f(&shed.var_scopes.borrow())) } /// Write to the variable table pub fn write_vars T>(f: F) -> T { - FERN.with(|shed| f(&mut shed.var_scopes.borrow_mut())) + SHED.with(|shed| f(&mut shed.var_scopes.borrow_mut())) } /// Parse `arr[idx]` into (name, raw_index_expr). Pure parsing, no expansion. @@ -1211,30 +1211,30 @@ pub fn expand_arr_index(idx_raw: &str) -> ShResult { } pub fn read_meta T>(f: F) -> T { - FERN.with(|shed| f(&shed.meta.borrow())) + SHED.with(|shed| f(&shed.meta.borrow())) } /// Write to the meta table pub fn write_meta T>(f: F) -> T { - FERN.with(|shed| f(&mut shed.meta.borrow_mut())) + SHED.with(|shed| f(&mut shed.meta.borrow_mut())) } /// Read from the logic table pub fn read_logic T>(f: F) -> T { - FERN.with(|shed| f(&shed.logic.borrow())) + SHED.with(|shed| f(&shed.logic.borrow())) } /// Write to the logic table pub fn write_logic T>(f: F) -> T { - FERN.with(|shed| f(&mut shed.logic.borrow_mut())) + SHED.with(|shed| f(&mut shed.logic.borrow_mut())) } pub fn read_shopts T>(f: F) -> T { - FERN.with(|shed| f(&shed.shopts.borrow())) + SHED.with(|shed| f(&shed.shopts.borrow())) } pub fn write_shopts T>(f: F) -> T { - FERN.with(|shed| f(&mut shed.shopts.borrow_mut())) + SHED.with(|shed| f(&mut shed.shopts.borrow_mut())) } pub fn descend_scope(argv: Option>) { @@ -1261,7 +1261,7 @@ pub fn set_status(code: i32) { } pub fn source_rc() -> ShResult<()> { - let path = if let Ok(path) = env::var("FERN_RC") { + let path = if let Ok(path) = env::var("SHED_RC") { PathBuf::from(&path) } else { let home = env::var("HOME").unwrap();