completion now prefers completing variable names before trying comp specs

improved color picking for error messages
This commit is contained in:
2026-03-01 11:02:13 -05:00
parent ffe78620a9
commit 84aed128d6
8 changed files with 82 additions and 36 deletions

View File

@@ -1,6 +1,9 @@
use std::collections::VecDeque;
use ariadne::Span;
use crate::{ use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, write_vars} getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, read_vars, write_vars}
}; };
use super::setup_builtin; use super::setup_builtin;
@@ -102,15 +105,19 @@ fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: En
return Err(ShErr::at(ShErrKind::ExecFail, blame, "push: missing array name".to_string())); return Err(ShErr::at(ShErrKind::ExecFail, blame, "push: missing array name".to_string()));
}; };
for (val, _) in argv { for (val, span) in argv {
let push_val = val.clone(); let push_val = val.clone();
if let Err(e) = write_vars(|v| v.get_arr_mut(&name).map(|arr| match end { write_vars(|v| {
if let Ok(arr) = v.get_arr_mut(&name) {
match end {
End::Front => arr.push_front(push_val), End::Front => arr.push_front(push_val),
End::Back => arr.push_back(push_val), End::Back => arr.push_back(push_val),
})) { }
state::set_status(1); Ok(())
return Err(e); } else {
}; v.set_var(&name, VarKind::Arr(VecDeque::from([push_val])), VarFlags::NONE)
}
}).blame(span)?;
} }
state::set_status(0); state::set_status(0);

View File

@@ -13,7 +13,9 @@ use crate::{
pub type ShResult<T> = Result<T, ShErr>; pub type ShResult<T> = Result<T, ShErr>;
pub struct ColorRng; pub struct ColorRng {
last_color: Option<Color>,
}
impl ColorRng { impl ColorRng {
fn get_colors() -> &'static [Color] { fn get_colors() -> &'static [Color] {
@@ -39,6 +41,16 @@ impl ColorRng {
Color::Fixed(105), // medium purple Color::Fixed(105), // medium purple
] ]
} }
pub fn last_color(&mut self) -> Color {
if let Some(color) = self.last_color.take() {
return color;
} else {
let color = self.next().unwrap_or(Color::White);
self.last_color = Some(color);
color
}
}
} }
impl Iterator for ColorRng { impl Iterator for ColorRng {
@@ -51,11 +63,19 @@ impl Iterator for ColorRng {
} }
thread_local! { thread_local! {
static COLOR_RNG: RefCell<ColorRng> = const { RefCell::new(ColorRng) }; static COLOR_RNG: RefCell<ColorRng> = const { RefCell::new(ColorRng { last_color: None }) };
} }
pub fn next_color() -> Color { pub fn next_color() -> Color {
COLOR_RNG.with(|rng| rng.borrow_mut().next().unwrap()) COLOR_RNG.with(|rng| {
let color = rng.borrow_mut().next().unwrap();
rng.borrow_mut().last_color = Some(color);
color
})
}
pub fn last_color() -> Color {
COLOR_RNG.with(|rng| rng.borrow_mut().last_color())
} }
pub trait ShResultExt { pub trait ShResultExt {
@@ -145,14 +165,14 @@ impl ShErr {
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()] } Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()] }
} }
pub fn at(kind: ShErrKind, span: Span, msg: impl Into<String>) -> Self { pub fn at(kind: ShErrKind, span: Span, msg: impl Into<String>) -> Self {
let color = next_color(); let color = last_color(); // use last_color to ensure the same color is used for the label and the message given
let src = span.span_source().clone(); let src = span.span_source().clone();
let msg: String = msg.into(); let msg: String = msg.into();
Self::new(kind, span.clone()) Self::new(kind, span.clone())
.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) .with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg))
} }
pub fn labeled(self, span: Span, msg: impl Into<String>) -> Self { pub fn labeled(self, span: Span, msg: impl Into<String>) -> Self {
let color = next_color(); let color = last_color();
let src = span.span_source().clone(); let src = span.span_source().clone();
let msg: String = msg.into(); let msg: String = msg.into();
self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg))
@@ -230,7 +250,7 @@ impl ShErr {
} }
pub fn print_error(&self) { pub fn print_error(&self) {
let default = || { let default = || {
eprintln!("{}", self.kind); eprintln!("\n{}", self.kind);
for note in &self.notes { for note in &self.notes {
eprintln!("note: {note}"); eprintln!("note: {note}");
} }
@@ -245,6 +265,7 @@ impl ShErr {
.cloned() .cloned()
.ok_or_else(|| format!("Failed to fetch source '{}'", src.name())) .ok_or_else(|| format!("Failed to fetch source '{}'", src.name()))
}); });
eprintln!();
if report.eprint(cache).is_err() { if report.eprint(cache).is_err() {
default(); default();
} }

View File

@@ -315,6 +315,7 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> {
let blame = subsh.get_span().clone();
let NdRule::Command { assignments, argv } = subsh.class else { let NdRule::Command { assignments, argv } = subsh.class else {
unreachable!() unreachable!()
}; };
@@ -328,7 +329,7 @@ impl Dispatcher {
let mut argv = match prepare_argv(argv) { let mut argv = match prepare_argv(argv) {
Ok(argv) => argv, Ok(argv) => argv,
Err(e) => { Err(e) => {
e.print_error(); e.try_blame(blame).print_error();
return; return;
} }
}; };
@@ -376,7 +377,7 @@ impl Dispatcher {
blame.rename(func_name.clone()); blame.rename(func_name.clone());
let argv = prepare_argv(argv)?; let argv = prepare_argv(argv).try_blame(blame.clone())?;
let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) {
let _guard = ScopeGuard::exclusive_scope(Some(argv)); let _guard = ScopeGuard::exclusive_scope(Some(argv));
func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().propagate_context(func_ctx);
@@ -833,6 +834,7 @@ impl Dispatcher {
} }
} }
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
let blame = cmd.get_span().clone();
let context = cmd.context.clone(); let context = cmd.context.clone();
let NdRule::Command { assignments, argv } = cmd.class else { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
@@ -856,7 +858,7 @@ impl Dispatcher {
self.io_stack.append_to_frame(cmd.redirs); self.io_stack.append_to_frame(cmd.redirs);
let exec_args = ExecArgs::new(argv)?; let exec_args = ExecArgs::new(argv).blame(blame)?;
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let job = self.job_stack.curr_job_mut().unwrap(); let job = self.job_stack.curr_job_mut().unwrap();

View File

@@ -7,7 +7,7 @@ use lex::{LexFlags, LexStream, Span, SpanSource, Tk, TkFlags, TkRule};
use crate::{ use crate::{
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, next_color}, error::{ShErr, ShErrKind, ShResult, last_color, next_color},
utils::{NodeVecUtils, TkVecUtils}, utils::{NodeVecUtils, TkVecUtils},
}, },
prelude::*, prelude::*,
@@ -135,7 +135,7 @@ impl Node {
} }
} }
pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) { pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) {
let color = next_color(); let color = last_color();
let span = self.get_span().clone(); let span = self.get_span().clone();
( (
span.clone().source().clone(), span.clone().source().clone(),
@@ -1745,7 +1745,7 @@ pub fn get_redir_file(class: RedirType, path: PathBuf) -> ShResult<File> {
} }
fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr { fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr {
let color = next_color(); let color = last_color();
ShErr::new(ShErrKind::ParseErr, blame.clone()) ShErr::new(ShErrKind::ParseErr, blame.clone())
.with_label( .with_label(
blame.span_source().clone(), blame.span_source().clone(),

View File

@@ -433,6 +433,13 @@ impl CompSpec for BashCompSpec {
if self.function.is_some() { if self.function.is_some() {
candidates.extend(self.exec_comp_func(ctx)?); candidates.extend(self.exec_comp_func(ctx)?);
} }
candidates = candidates.into_iter()
.map(|c| {
let stripped = c.strip_prefix(&expanded).unwrap_or_default();
format!("{prefix}{stripped}")
})
.collect();
candidates.sort_by_key(|c| c.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically candidates.sort_by_key(|c| c.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically
Ok(candidates) Ok(candidates)
@@ -766,8 +773,25 @@ impl Completer {
self.token_span = (cursor_pos, cursor_pos); self.token_span = (cursor_pos, cursor_pos);
} }
// Try programmable completion first // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB
// inside a token). Run this before comp specs so variable completions take
// priority over programmable completion.
let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos);
if marker_ctx.last() == Some(&markers::VAR_SUB) {
if let Some(cur) = ctx.words.get(ctx.cword) {
self.token_span.0 = token_start;
let mut span = cur.span.clone();
span.set_range(token_start..self.token_span.1);
let raw_tk = span.as_str();
let candidates = complete_vars(raw_tk);
if !candidates.is_empty() {
return Ok(CompResult::from_candidates(candidates));
}
}
}
// Try programmable completion
match self.try_comp_spec(&ctx)? { match self.try_comp_spec(&ctx)? {
CompSpecResult::NoMatch { flags } => { CompSpecResult::NoMatch { flags } => {
if flags.contains(CompOptFlags::DIRNAMES) { if flags.contains(CompOptFlags::DIRNAMES) {
@@ -801,9 +825,6 @@ impl Completer {
self.token_span = (cur_token.span.range().start, cur_token.span.range().end); self.token_span = (cur_token.span.range().start, cur_token.span.range().end);
// Use marker-based context detection for sub-token awareness (e.g. VAR_SUB
// inside a token)
let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos);
self.token_span.0 = token_start; self.token_span.0 = token_start;
cur_token cur_token
.span .span
@@ -828,12 +849,9 @@ impl Completer {
_ if self.dirs_only => complete_dirs(&expanded), _ if self.dirs_only => complete_dirs(&expanded),
Some(markers::COMMAND) => complete_commands(&expanded), Some(markers::COMMAND) => complete_commands(&expanded),
Some(markers::VAR_SUB) => { Some(markers::VAR_SUB) => {
let var_candidates = complete_vars(&raw_tk); // Variable completion already tried above and had no matches,
if var_candidates.is_empty() { // fall through to filename completion
complete_filename(&expanded) complete_filename(&expanded)
} else {
var_candidates
}
} }
Some(markers::ARG) => complete_filename(&expanded), Some(markers::ARG) => complete_filename(&expanded),
_ => complete_filename(&expanded), _ => complete_filename(&expanded),

View File

@@ -341,7 +341,7 @@ impl ShedVi {
match self.completer.complete(line, cursor_pos, direction) { match self.completer.complete(line, cursor_pos, direction) {
Err(e) => { Err(e) => {
self.writer.flush_write(&format!("\n{e}\n\n"))?; e.print_error();
// Printing the error invalidates the layout // Printing the error invalidates the layout
self.old_layout = None; self.old_layout = None;

View File

@@ -780,7 +780,7 @@ impl AsFd for TermReader {
} }
} }
#[derive(Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Layout { pub struct Layout {
pub prompt_end: Pos, pub prompt_end: Pos,
pub cursor: Pos, pub cursor: Pos,

View File

@@ -467,9 +467,7 @@ thread_local! {
/// A shell function /// A shell function
/// ///
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers /// Wraps the BraceGrp Node that forms the body of the function, and provides some helper methods to extract it from the parse tree
/// to. The Node must be stored with the ParsedSrc because the tokens of the
/// node contain an Arc<String> Which refers to the String held in ParsedSrc
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ShFunc(Node); pub struct ShFunc(Node);