fixed the $0 parameter not being populated correctly
This commit is contained in:
@@ -554,8 +554,12 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
pub fn expand_glob(raw: &str) -> ShResult<String> {
|
||||
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
|
||||
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 =
|
||||
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 mut segments = pathbuf.iter().count();
|
||||
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();
|
||||
segments -= 1;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ use super::{
|
||||
ParsedSrc, Redir, RedirType,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
pub struct ScopeGuard;
|
||||
|
||||
|
||||
@@ -105,7 +109,12 @@ impl ExecArgs {
|
||||
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> {
|
||||
let log_tab = read_logic(|l| l.clone());
|
||||
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() {
|
||||
for error in errors {
|
||||
eprintln!("{error}");
|
||||
@@ -170,6 +179,10 @@ impl Dispatcher {
|
||||
self.exec_builtin(node)
|
||||
} else if is_subsh(node.get_command().cloned()) {
|
||||
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 {
|
||||
self.exec_cmd(node)
|
||||
}
|
||||
@@ -266,6 +279,21 @@ impl Dispatcher {
|
||||
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 _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 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));
|
||||
|
||||
if let Err(e) = self.exec_brc_grp((*func).clone()) {
|
||||
match e.kind() {
|
||||
ShErrKind::FuncReturn(code) => {
|
||||
state::set_status(*code);
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
_ => return Err(e),
|
||||
_ => Err(e),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
format!("Failed to find function '{}'", func_name),
|
||||
blame,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
|
||||
result
|
||||
}
|
||||
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
|
||||
let NdRule::BraceGrp { body } = brc_grp.class else {
|
||||
|
||||
@@ -156,10 +156,10 @@ pub struct LexStream {
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LexFlags: u32 {
|
||||
/// Return comment tokens
|
||||
const LEX_COMMENTS = 0b000000001;
|
||||
/// The lexer is operating in interactive mode
|
||||
const INTERACTIVE = 0b000000001;
|
||||
/// Allow unfinished input
|
||||
const LEX_UNFINISHED = 0b000000010;
|
||||
/// 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)
|
||||
}
|
||||
'#' => {
|
||||
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
|
||||
let ch_idx = self.cursor;
|
||||
self.cursor += 1;
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ macro_rules! try_match {
|
||||
pub struct ParsedSrc {
|
||||
pub src: Arc<String>,
|
||||
pub ast: Ast,
|
||||
pub lex_flags: LexFlags,
|
||||
}
|
||||
|
||||
impl ParsedSrc {
|
||||
@@ -51,11 +52,16 @@ impl ParsedSrc {
|
||||
Self {
|
||||
src,
|
||||
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>> {
|
||||
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 {
|
||||
Ok(token) => tokens.push(token),
|
||||
Err(error) => return Err(vec![error]),
|
||||
|
||||
@@ -263,7 +263,7 @@ impl DerefMut for IoFrame {
|
||||
/// redirection
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IoStack {
|
||||
stack: Vec<IoFrame>,
|
||||
pub stack: Vec<IoFrame>,
|
||||
}
|
||||
|
||||
impl IoStack {
|
||||
|
||||
@@ -87,8 +87,8 @@ impl Completer {
|
||||
ctx.push(markers::VAR_SUB);
|
||||
}
|
||||
}
|
||||
markers::ARG => {
|
||||
log::debug!("Found argument marker at position {}", pos);
|
||||
markers::ARG | markers::ASSIGNMENT => {
|
||||
log::debug!("Found argument/assignment marker at position {}", pos);
|
||||
if last_priority < 1 {
|
||||
ctx_start = pos;
|
||||
ctx.push(markers::ARG);
|
||||
@@ -328,6 +328,8 @@ impl Completer {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
||||
candidates.truncate(limit);
|
||||
|
||||
Ok(CompResult::from_candidates(candidates))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
///
|
||||
@@ -214,16 +214,31 @@ impl Highlighter {
|
||||
fn is_valid(command: &str) -> bool {
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':');
|
||||
if PathBuf::from(&command).exists() {
|
||||
return true;
|
||||
let cmd_path = PathBuf::from(&command);
|
||||
|
||||
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 {
|
||||
// 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 {
|
||||
let path = PathBuf::from(path).join(command);
|
||||
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());
|
||||
if found {
|
||||
return true;
|
||||
|
||||
@@ -217,11 +217,17 @@ pub struct History {
|
||||
|
||||
impl History {
|
||||
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 home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
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
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
entries.push(HistEntry {
|
||||
@@ -238,8 +244,8 @@ impl History {
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
ignore_dups,
|
||||
max_size: Some(max_hist as u32),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,10 @@ impl FernVi {
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
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.writer.flush_write("\n")?;
|
||||
let buf = self.editor.take_buf();
|
||||
// Save command to history
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
// Save command to history if auto_hist is enabled
|
||||
if crate::state::read_shopts(|s| s.core.auto_hist) {
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
}
|
||||
}
|
||||
return Ok(ReadlineEvent::Line(buf));
|
||||
}
|
||||
@@ -283,7 +288,8 @@ 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(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) {
|
||||
/*
|
||||
@@ -360,15 +366,16 @@ impl FernVi {
|
||||
}
|
||||
|
||||
pub fn line_text(&mut self) -> String {
|
||||
let start = Instant::now();
|
||||
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 complete = format!("{highlighted}{hint}");
|
||||
let end = start.elapsed();
|
||||
complete
|
||||
if crate::state::read_shopts(|s| s.prompt.highlight) {
|
||||
self.highlighter.load_input(&line);
|
||||
self.highlighter.highlight();
|
||||
let highlighted = self.highlighter.take();
|
||||
format!("{highlighted}{hint}")
|
||||
} else {
|
||||
format!("{line}{hint}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_line(&mut self) -> ShResult<()> {
|
||||
|
||||
54
src/shopt.rs
54
src/shopt.rs
@@ -111,7 +111,7 @@ impl ShOpts {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
"shopt: Expected 'core' or 'prompt' in shopt key",
|
||||
"shopt: expected 'core' or 'prompt' in shopt key",
|
||||
)
|
||||
.with_note(
|
||||
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
||||
@@ -384,9 +384,8 @@ pub struct ShOptPrompt {
|
||||
pub trunc_prompt_path: usize,
|
||||
pub edit_mode: FernEditMode,
|
||||
pub comp_limit: usize,
|
||||
pub prompt_highlight: bool,
|
||||
pub highlight: bool,
|
||||
pub tab_stop: usize,
|
||||
pub custom: HashMap<String, ShFunc>, // Contains functions for prompt modules
|
||||
}
|
||||
|
||||
impl ShOptPrompt {
|
||||
@@ -419,14 +418,14 @@ impl ShOptPrompt {
|
||||
};
|
||||
self.comp_limit = val;
|
||||
}
|
||||
"prompt_highlight" => {
|
||||
"highlight" => {
|
||||
let Ok(val) = val.parse::<bool>() else {
|
||||
return Err(ShErr::simple(
|
||||
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" => {
|
||||
let Ok(val) = val.parse::<usize>() else {
|
||||
@@ -444,19 +443,17 @@ impl ShOptPrompt {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
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(
|
||||
Note::new("'core' contains the following options").with_sub_notes(vec![
|
||||
"dotglob",
|
||||
"autocd",
|
||||
"hist_ignore_dupes",
|
||||
"max_hist",
|
||||
"interactive_comments",
|
||||
"auto_hist",
|
||||
"bell_style",
|
||||
"max_recurse_depth",
|
||||
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||
"trunc_prompt_path",
|
||||
"edit_mode",
|
||||
"comp_limit",
|
||||
"highlight",
|
||||
"tab_stop",
|
||||
"custom",
|
||||
]),
|
||||
),
|
||||
)
|
||||
@@ -489,10 +486,10 @@ impl ShOptPrompt {
|
||||
output.push_str(&format!("{}", self.comp_limit));
|
||||
Ok(Some(output))
|
||||
}
|
||||
"prompt_highlight" => {
|
||||
"highlight" => {
|
||||
let mut output =
|
||||
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))
|
||||
}
|
||||
"tab_stop" => {
|
||||
@@ -500,16 +497,6 @@ impl ShOptPrompt {
|
||||
output.push_str(&format!("{}", self.tab_stop));
|
||||
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(
|
||||
ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
@@ -540,12 +527,8 @@ impl Display for ShOptPrompt {
|
||||
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
|
||||
output.push(format!("edit_mode = {}", self.edit_mode));
|
||||
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(String::from("prompt modules: "));
|
||||
for key in self.custom.keys() {
|
||||
output.push(format!(" - {key}"));
|
||||
}
|
||||
|
||||
let final_output = output.join("\n");
|
||||
|
||||
@@ -559,9 +542,8 @@ impl Default for ShOptPrompt {
|
||||
trunc_prompt_path: 4,
|
||||
edit_mode: FernEditMode::Vi,
|
||||
comp_limit: 100,
|
||||
prompt_highlight: true,
|
||||
highlight: true,
|
||||
tab_stop: 4,
|
||||
custom: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
src/state.rs
10
src/state.rs
@@ -121,6 +121,8 @@ impl ScopeStack {
|
||||
pub fn new() -> Self {
|
||||
let mut new = Self::default();
|
||||
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
|
||||
}
|
||||
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
||||
@@ -482,14 +484,6 @@ impl VarTab {
|
||||
fn init_params() -> HashMap<ShellParam, String> {
|
||||
let mut params = HashMap::new();
|
||||
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::LastJob, "".into()); // PID of the last background job (if any)
|
||||
params
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::prompt::readline::complete::Completer;
|
||||
use crate::prompt::readline::markers;
|
||||
use crate::state::{write_logic, write_vars, VarFlags};
|
||||
|
||||
use super::*;
|
||||
@@ -320,12 +321,12 @@ fn context_detection_command_position() {
|
||||
let completer = Completer::new();
|
||||
|
||||
// At the beginning - command context
|
||||
let (in_cmd, _) = completer.get_completion_context("ech", 3);
|
||||
assert!(in_cmd, "Should be in command context at start");
|
||||
let (ctx, _) = completer.get_completion_context("ech", 3);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
|
||||
|
||||
// After whitespace - still command if no command yet
|
||||
let (in_cmd, _) = completer.get_completion_context(" ech", 5);
|
||||
assert!(in_cmd, "Should be in command context after whitespace");
|
||||
let (ctx, _) = completer.get_completion_context(" ech", 5);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,11 +334,11 @@ fn context_detection_argument_position() {
|
||||
let completer = Completer::new();
|
||||
|
||||
// After a complete command - argument context
|
||||
let (in_cmd, _) = completer.get_completion_context("echo hello", 10);
|
||||
assert!(!in_cmd, "Should be in argument context after command");
|
||||
let (ctx, _) = completer.get_completion_context("echo hello", 10);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
|
||||
|
||||
let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11);
|
||||
assert!(!in_cmd, "Should be in argument context");
|
||||
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -345,12 +346,12 @@ fn context_detection_nested_command_sub() {
|
||||
let completer = Completer::new();
|
||||
|
||||
// Inside $() - should be command context
|
||||
let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11);
|
||||
assert!(in_cmd, "Should be in command context inside $()");
|
||||
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
|
||||
|
||||
// After command in $() - argument context
|
||||
let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
||||
assert!(!in_cmd, "Should be in argument context inside $()");
|
||||
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -358,8 +359,8 @@ fn context_detection_pipe() {
|
||||
let completer = Completer::new();
|
||||
|
||||
// After pipe - command context
|
||||
let (in_cmd, _) = completer.get_completion_context("ls | gre", 8);
|
||||
assert!(in_cmd, "Should be in command context after pipe");
|
||||
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -367,12 +368,74 @@ fn context_detection_command_sep() {
|
||||
let completer = Completer::new();
|
||||
|
||||
// After semicolon - command context
|
||||
let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11);
|
||||
assert!(in_cmd, "Should be in command context after semicolon");
|
||||
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
|
||||
|
||||
// After && - command context
|
||||
let (in_cmd, _) = completer.get_completion_context("true && l", 9);
|
||||
assert!(in_cmd, "Should be in command context after &&");
|
||||
let (ctx, _) = completer.get_completion_context("true && l", 9);
|
||||
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");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user