fixed the $0 parameter not being populated correctly

This commit is contained in:
2026-02-19 14:24:55 -05:00
parent 8cb8f20a35
commit 9483477edd
12 changed files with 210 additions and 99 deletions

View File

@@ -554,8 +554,12 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
pub fn expand_glob(raw: &str) -> ShResult<String> { pub fn expand_glob(raw: &str) -> ShResult<String> {
let mut words = vec![]; let mut words = vec![];
let opts = glob::MatchOptions {
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
..Default::default()
};
for entry in for entry in
glob::glob(raw).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
{ {
let entry = let entry =
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
@@ -1926,7 +1930,8 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
let pathbuf = PathBuf::from(&path); let pathbuf = PathBuf::from(&path);
let mut segments = pathbuf.iter().count(); let mut segments = pathbuf.iter().count();
let mut path_iter = pathbuf.iter(); let mut path_iter = pathbuf.iter();
while segments > 4 { let max_segments = crate::state::read_shopts(|s| s.prompt.trunc_prompt_path);
while segments > max_segments {
path_iter.next(); path_iter.next();
segments -= 1; segments -= 1;
} }

View File

@@ -20,6 +20,10 @@ use super::{
ParsedSrc, Redir, RedirType, ParsedSrc, Redir, RedirType,
}; };
thread_local! {
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub struct ScopeGuard; pub struct ScopeGuard;
@@ -105,7 +109,12 @@ impl ExecArgs {
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> { pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> {
let log_tab = read_logic(|l| l.clone()); let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input, HashSet::new(), &log_tab); let input = expand_aliases(input, HashSet::new(), &log_tab);
let mut parser = ParsedSrc::new(Arc::new(input)); let lex_flags = if interactive {
super::lex::LexFlags::INTERACTIVE
} else {
super::lex::LexFlags::empty()
};
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags);
if let Err(errors) = parser.parse_src() { if let Err(errors) = parser.parse_src() {
for error in errors { for error in errors {
eprintln!("{error}"); eprintln!("{error}");
@@ -170,6 +179,10 @@ impl Dispatcher {
self.exec_builtin(node) self.exec_builtin(node)
} else if is_subsh(node.get_command().cloned()) { } else if is_subsh(node.get_command().cloned()) {
self.exec_subsh(node) self.exec_subsh(node)
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() {
let dir = cmd.span.as_str().to_string();
let stack = IoStack { stack: self.io_stack.clone() };
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
} else { } else {
self.exec_cmd(node) self.exec_cmd(node)
} }
@@ -266,6 +279,21 @@ impl Dispatcher {
unreachable!() unreachable!()
}; };
let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth);
let depth = RECURSE_DEPTH.with(|d| {
let cur = d.get();
d.set(cur + 1);
cur + 1
});
if depth > max_depth {
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
return Err(ShErr::full(
ShErrKind::InternalErr,
format!("maximum recursion depth ({max_depth}) exceeded"),
blame,
));
}
let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?;
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect());
@@ -273,27 +301,30 @@ impl Dispatcher {
let func_name = argv.remove(0).span.as_str().to_string(); let func_name = argv.remove(0).span.as_str().to_string();
let argv = prepare_argv(argv)?; let argv = prepare_argv(argv)?;
if let Some(func) = read_logic(|l| l.get_func(&func_name)) { let result = if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
let _guard = ScopeGuard::exclusive_scope(Some(argv)); let _guard = ScopeGuard::exclusive_scope(Some(argv));
if let Err(e) = self.exec_brc_grp((*func).clone()) { if let Err(e) = self.exec_brc_grp((*func).clone()) {
match e.kind() { match e.kind() {
ShErrKind::FuncReturn(code) => { ShErrKind::FuncReturn(code) => {
state::set_status(*code); state::set_status(*code);
return Ok(()); Ok(())
} }
_ => return Err(e), _ => Err(e),
} }
} else {
Ok(())
} }
Ok(())
} else { } else {
Err(ShErr::full( Err(ShErr::full(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("Failed to find function '{}'", func_name), format!("Failed to find function '{}'", func_name),
blame, blame,
)) ))
} };
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
result
} }
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
let NdRule::BraceGrp { body } = brc_grp.class else { let NdRule::BraceGrp { body } = brc_grp.class else {

View File

@@ -156,10 +156,10 @@ pub struct LexStream {
} }
bitflags! { bitflags! {
#[derive(Debug)] #[derive(Debug, Clone, Copy)]
pub struct LexFlags: u32 { pub struct LexFlags: u32 {
/// Return comment tokens /// The lexer is operating in interactive mode
const LEX_COMMENTS = 0b000000001; const INTERACTIVE = 0b000000001;
/// Allow unfinished input /// Allow unfinished input
const LEX_UNFINISHED = 0b000000010; const LEX_UNFINISHED = 0b000000010;
/// The next string-type token is a command name /// The next string-type token is a command name
@@ -740,7 +740,7 @@ impl Iterator for LexStream {
} }
self.get_token(ch_idx..self.cursor, TkRule::Sep) self.get_token(ch_idx..self.cursor, TkRule::Sep)
} }
'#' => { '#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
let ch_idx = self.cursor; let ch_idx = self.cursor;
self.cursor += 1; self.cursor += 1;

View File

@@ -44,6 +44,7 @@ macro_rules! try_match {
pub struct ParsedSrc { pub struct ParsedSrc {
pub src: Arc<String>, pub src: Arc<String>,
pub ast: Ast, pub ast: Ast,
pub lex_flags: LexFlags,
} }
impl ParsedSrc { impl ParsedSrc {
@@ -51,11 +52,16 @@ impl ParsedSrc {
Self { Self {
src, src,
ast: Ast::new(vec![]), ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(),
} }
} }
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags;
self
}
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> { pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
let mut tokens = vec![]; let mut tokens = vec![];
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) { for lex_result in LexStream::new(self.src.clone(), self.lex_flags) {
match lex_result { match lex_result {
Ok(token) => tokens.push(token), Ok(token) => tokens.push(token),
Err(error) => return Err(vec![error]), Err(error) => return Err(vec![error]),

View File

@@ -263,7 +263,7 @@ impl DerefMut for IoFrame {
/// redirection /// redirection
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct IoStack { pub struct IoStack {
stack: Vec<IoFrame>, pub stack: Vec<IoFrame>,
} }
impl IoStack { impl IoStack {

View File

@@ -87,8 +87,8 @@ impl Completer {
ctx.push(markers::VAR_SUB); ctx.push(markers::VAR_SUB);
} }
} }
markers::ARG => { markers::ARG | markers::ASSIGNMENT => {
log::debug!("Found argument marker at position {}", pos); log::debug!("Found argument/assignment marker at position {}", pos);
if last_priority < 1 { if last_priority < 1 {
ctx_start = pos; ctx_start = pos;
ctx.push(markers::ARG); ctx.push(markers::ARG);
@@ -328,6 +328,8 @@ impl Completer {
}) })
.collect(); .collect();
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
candidates.truncate(limit);
Ok(CompResult::from_candidates(candidates)) Ok(CompResult::from_candidates(candidates))
} }

View File

@@ -1,6 +1,6 @@
use std::{env, path::{Path, PathBuf}}; use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::read_logic}; use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
/// Syntax highlighter for shell input using Unicode marker-based annotation /// Syntax highlighter for shell input using Unicode marker-based annotation
/// ///
@@ -214,16 +214,31 @@ impl Highlighter {
fn is_valid(command: &str) -> bool { fn is_valid(command: &str) -> bool {
let path = env::var("PATH").unwrap_or_default(); let path = env::var("PATH").unwrap_or_default();
let paths = path.split(':'); let paths = path.split(':');
if PathBuf::from(&command).exists() { let cmd_path = PathBuf::from(&command);
return true;
if cmd_path.exists() {
// 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
return true;
} else {
let Ok(meta) = cmd_path.metadata() else { return false };
// this is a file that is executable by someone
return meta.permissions().mode() & 0o111 == 0
}
} else { } else {
// they gave us a command name
// now we must traverse the PATH env var
// and see if we find any matches
for path in paths { for path in paths {
let path = PathBuf::from(path).join(command); let path = PathBuf::from(path).join(command);
if path.exists() { if path.exists() {
return true; let Ok(meta) = path.metadata() else { continue };
return meta.permissions().mode() & 0o111 != 0;
} }
} }
// also check shell functions and aliases for any matches
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some()); let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
if found { if found {
return true; return true;

View File

@@ -217,11 +217,17 @@ pub struct History {
impl History { impl History {
pub fn new() -> ShResult<Self> { pub fn new() -> ShResult<Self> {
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("FERNHIST").unwrap_or({
let home = env::var("HOME").unwrap(); let home = env::var("HOME").unwrap();
format!("{home}/.fern_history") format!("{home}/.fern_history")
})); }));
let mut entries = read_hist_file(&path)?; let mut entries = read_hist_file(&path)?;
// Enforce max_hist limit on loaded entries
if entries.len() > max_hist {
entries = entries.split_off(entries.len() - max_hist);
}
// Create pending entry for current input // Create pending entry for current input
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0); let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
entries.push(HistEntry { entries.push(HistEntry {
@@ -238,8 +244,8 @@ impl History {
search_mask, search_mask,
cursor, cursor,
search_direction: Direction::Backward, search_direction: Direction::Backward,
ignore_dups: true, ignore_dups,
max_size: None, max_size: Some(max_hist as u32),
}) })
} }

View File

@@ -212,7 +212,10 @@ impl FernVi {
self.editor.set_hint(hint); self.editor.set_hint(hint);
} }
None => { None => {
self.writer.flush_write("\x07")?; // Bell character match crate::state::read_shopts(|s| s.core.bell_style) {
crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; }
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
}
} }
} }
@@ -240,10 +243,12 @@ impl FernVi {
self.print_line()?; self.print_line()?;
self.writer.flush_write("\n")?; self.writer.flush_write("\n")?;
let buf = self.editor.take_buf(); let buf = self.editor.take_buf();
// Save command to history // Save command to history if auto_hist is enabled
self.history.push(buf.clone()); if crate::state::read_shopts(|s| s.core.auto_hist) {
if let Err(e) = self.history.save() { self.history.push(buf.clone());
eprintln!("Failed to save history: {e}"); if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}");
}
} }
return Ok(ReadlineEvent::Line(buf)); return Ok(ReadlineEvent::Line(buf));
} }
@@ -283,7 +288,8 @@ impl FernVi {
pub fn get_layout(&mut self, line: &str) -> Layout { pub fn get_layout(&mut self, line: &str) -> Layout {
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(STDIN_FILENO); let (cols, _) = get_win_size(STDIN_FILENO);
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, line) 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)
} }
pub fn scroll_history(&mut self, cmd: ViCmd) { pub fn scroll_history(&mut self, cmd: ViCmd) {
/* /*
@@ -360,15 +366,16 @@ impl FernVi {
} }
pub fn line_text(&mut self) -> String { pub fn line_text(&mut self) -> String {
let start = Instant::now();
let line = self.editor.to_string(); let line = self.editor.to_string();
self.highlighter.load_input(&line);
self.highlighter.highlight();
let highlighted = self.highlighter.take();
let hint = self.editor.get_hint_text(); let hint = self.editor.get_hint_text();
let complete = format!("{highlighted}{hint}"); if crate::state::read_shopts(|s| s.prompt.highlight) {
let end = start.elapsed(); self.highlighter.load_input(&line);
complete self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
} else {
format!("{line}{hint}")
}
} }
pub fn print_line(&mut self) -> ShResult<()> { pub fn print_line(&mut self) -> ShResult<()> {

View File

@@ -111,7 +111,7 @@ impl ShOpts {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key", "shopt: expected 'core' or 'prompt' in shopt key",
) )
.with_note( .with_note(
Note::new("'shopt' takes arguments separated by periods to denote namespaces") Note::new("'shopt' takes arguments separated by periods to denote namespaces")
@@ -384,9 +384,8 @@ pub struct ShOptPrompt {
pub trunc_prompt_path: usize, pub trunc_prompt_path: usize,
pub edit_mode: FernEditMode, pub edit_mode: FernEditMode,
pub comp_limit: usize, pub comp_limit: usize,
pub prompt_highlight: bool, pub highlight: bool,
pub tab_stop: usize, pub tab_stop: usize,
pub custom: HashMap<String, ShFunc>, // Contains functions for prompt modules
} }
impl ShOptPrompt { impl ShOptPrompt {
@@ -419,14 +418,14 @@ impl ShOptPrompt {
}; };
self.comp_limit = val; self.comp_limit = val;
} }
"prompt_highlight" => { "highlight" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for prompt_highlight value", "shopt: expected 'true' or 'false' for highlight value",
)); ));
}; };
self.prompt_highlight = val; self.highlight = val;
} }
"tab_stop" => { "tab_stop" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
@@ -444,19 +443,17 @@ impl ShOptPrompt {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'"), format!("shopt: Unexpected 'prompt' option '{opt}'"),
) )
.with_note(Note::new("options can be accessed like 'core.option_name'")) .with_note(Note::new("options can be accessed like 'prompt.option_name'"))
.with_note( .with_note(
Note::new("'core' contains the following options").with_sub_notes(vec![ Note::new("'prompt' contains the following options").with_sub_notes(vec![
"dotglob", "trunc_prompt_path",
"autocd", "edit_mode",
"hist_ignore_dupes", "comp_limit",
"max_hist", "highlight",
"interactive_comments", "tab_stop",
"auto_hist", "custom",
"bell_style",
"max_recurse_depth",
]), ]),
), ),
) )
@@ -489,10 +486,10 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.comp_limit)); output.push_str(&format!("{}", self.comp_limit));
Ok(Some(output)) Ok(Some(output))
} }
"prompt_highlight" => { "highlight" => {
let mut output = let mut output =
String::from("Whether to enable or disable syntax highlighting on the prompt\n"); String::from("Whether to enable or disable syntax highlighting on the prompt\n");
output.push_str(&format!("{}", self.prompt_highlight)); output.push_str(&format!("{}", self.highlight));
Ok(Some(output)) Ok(Some(output))
} }
"tab_stop" => { "tab_stop" => {
@@ -500,16 +497,6 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.tab_stop)); output.push_str(&format!("{}", self.tab_stop));
Ok(Some(output)) Ok(Some(output))
} }
"custom" => {
let mut output = String::from(
"A table of custom 'modules' executed as shell functions for prompt scripting\n",
);
output.push_str("Current modules: \n");
for key in self.custom.keys() {
output.push_str(&format!(" - {key}\n"));
}
Ok(Some(output.trim().to_string()))
}
_ => Err( _ => Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
@@ -540,12 +527,8 @@ impl Display for ShOptPrompt {
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path)); output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
output.push(format!("edit_mode = {}", self.edit_mode)); output.push(format!("edit_mode = {}", self.edit_mode));
output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("comp_limit = {}", self.comp_limit));
output.push(format!("prompt_highlight = {}", self.prompt_highlight)); output.push(format!("highlight = {}", self.highlight));
output.push(format!("tab_stop = {}", self.tab_stop)); output.push(format!("tab_stop = {}", self.tab_stop));
output.push(String::from("prompt modules: "));
for key in self.custom.keys() {
output.push(format!(" - {key}"));
}
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -559,9 +542,8 @@ impl Default for ShOptPrompt {
trunc_prompt_path: 4, trunc_prompt_path: 4,
edit_mode: FernEditMode::Vi, edit_mode: FernEditMode::Vi,
comp_limit: 100, comp_limit: 100,
prompt_highlight: true, highlight: true,
tab_stop: 4, tab_stop: 4,
custom: HashMap::new(),
} }
} }
} }

View File

@@ -121,6 +121,8 @@ impl ScopeStack {
pub fn new() -> Self { pub fn new() -> Self {
let mut new = Self::default(); let mut new = Self::default();
new.scopes.push(VarTab::new()); new.scopes.push(VarTab::new());
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
new new
} }
pub fn descend(&mut self, argv: Option<Vec<String>>) { pub fn descend(&mut self, argv: Option<Vec<String>>) {
@@ -482,14 +484,6 @@ impl VarTab {
fn init_params() -> HashMap<ShellParam, String> { fn init_params() -> HashMap<ShellParam, String> {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters
params.insert(
ShellParam::Pos(0),
std::env::current_exe()
.unwrap()
.to_str()
.unwrap()
.to_string(),
); // Name of the shell
params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any) params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
params params

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
use crate::prompt::readline::complete::Completer; use crate::prompt::readline::complete::Completer;
use crate::prompt::readline::markers;
use crate::state::{write_logic, write_vars, VarFlags}; use crate::state::{write_logic, write_vars, VarFlags};
use super::*; use super::*;
@@ -320,12 +321,12 @@ fn context_detection_command_position() {
let completer = Completer::new(); let completer = Completer::new();
// At the beginning - command context // At the beginning - command context
let (in_cmd, _) = completer.get_completion_context("ech", 3); let (ctx, _) = completer.get_completion_context("ech", 3);
assert!(in_cmd, "Should be in command context at start"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
// After whitespace - still command if no command yet // After whitespace - still command if no command yet
let (in_cmd, _) = completer.get_completion_context(" ech", 5); let (ctx, _) = completer.get_completion_context(" ech", 5);
assert!(in_cmd, "Should be in command context after whitespace"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
} }
#[test] #[test]
@@ -333,11 +334,11 @@ fn context_detection_argument_position() {
let completer = Completer::new(); let completer = Completer::new();
// After a complete command - argument context // After a complete command - argument context
let (in_cmd, _) = completer.get_completion_context("echo hello", 10); let (ctx, _) = completer.get_completion_context("echo hello", 10);
assert!(!in_cmd, "Should be in argument context after command"); assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11); let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
assert!(!in_cmd, "Should be in argument context"); assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
} }
#[test] #[test]
@@ -345,12 +346,12 @@ fn context_detection_nested_command_sub() {
let completer = Completer::new(); let completer = Completer::new();
// Inside $() - should be command context // Inside $() - should be command context
let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11); let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
assert!(in_cmd, "Should be in command context inside $()"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
// After command in $() - argument context // After command in $() - argument context
let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17); let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
assert!(!in_cmd, "Should be in argument context inside $()"); assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
} }
#[test] #[test]
@@ -358,8 +359,8 @@ fn context_detection_pipe() {
let completer = Completer::new(); let completer = Completer::new();
// After pipe - command context // After pipe - command context
let (in_cmd, _) = completer.get_completion_context("ls | gre", 8); let (ctx, _) = completer.get_completion_context("ls | gre", 8);
assert!(in_cmd, "Should be in command context after pipe"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
} }
#[test] #[test]
@@ -367,12 +368,74 @@ fn context_detection_command_sep() {
let completer = Completer::new(); let completer = Completer::new();
// After semicolon - command context // After semicolon - command context
let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11); let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
assert!(in_cmd, "Should be in command context after semicolon"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
// After && - command context // After && - command context
let (in_cmd, _) = completer.get_completion_context("true && l", 9); let (ctx, _) = completer.get_completion_context("true && l", 9);
assert!(in_cmd, "Should be in command context after &&"); assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&");
}
#[test]
fn context_detection_variable_substitution() {
let completer = Completer::new();
// $VAR at argument position - VAR_SUB should take priority over ARG
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM");
// $VAR at command position - VAR_SUB should take priority over COMMAND
let (ctx, _) = completer.get_completion_context("$HOM", 4);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM");
}
#[test]
fn context_detection_variable_in_double_quotes() {
let completer = Completer::new();
// $VAR inside double quotes
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes");
}
#[test]
fn context_detection_stack_base_is_null() {
let completer = Completer::new();
// Empty input - only NULL on the stack
let (ctx, _) = completer.get_completion_context("", 0);
assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker");
}
#[test]
fn context_detection_context_start_position() {
let completer = Completer::new();
// Command at start - ctx_start should be 0
let (_, ctx_start) = completer.get_completion_context("ech", 3);
assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0");
// Argument after command - ctx_start should be at arg position
let (_, ctx_start) = completer.get_completion_context("echo hel", 8);
assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start");
// Variable sub - ctx_start should point to the $
let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $");
}
#[test]
fn context_detection_priority_ordering() {
let completer = Completer::new();
// COMMAND (priority 2) should override ARG (priority 1)
// After a pipe, the next token is a command even though it looks like an arg
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe");
// VAR_SUB (priority 3) should override COMMAND (priority 2)
let (ctx, _) = completer.get_completion_context("$PA", 3);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND");
} }
// ============================================================================ // ============================================================================