From a46ebe6868cf4a2d00ea248fca9ffd6cd2d417e4 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Fri, 13 Mar 2026 11:18:57 -0400 Subject: [PATCH] Use COMP_WORDBREAKS for completion word breaking, fix cursor row in vi command mode, and append completion suffix instead of replacing full token --- src/readline/complete.rs | 31 ++++++++++++++++++------------- src/readline/mod.rs | 1 + src/state.rs | 7 ++++++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/readline/complete.rs b/src/readline/complete.rs index b716379..e77735d 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -6,6 +6,7 @@ use std::{ }; use nix::sys::signal::Signal; +use unicode_width::UnicodeWidthStr; use crate::{ builtin::complete::{CompFlags, CompOptFlags, CompOpts}, expand::escape_str, libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, parse::{ @@ -1172,10 +1173,11 @@ impl Completer for FuzzyCompleter { log::debug!("Getting completed line for candidate: {}", _candidate); let selected = self.selector.selected_candidate().unwrap_or_default(); - let escaped = escape_str(&selected, false); - log::debug!("Selected candidate: {}", selected); - let (start, end) = self.completer.token_span; - log::debug!("Token span: ({}, {})", start, end); + let (mut start, end) = self.completer.token_span; + let slice = self.completer.original_input.get(start..end).unwrap_or_default(); + start += slice.width(); + let completion = selected.strip_prefix(slice).unwrap_or(&selected); + let escaped = escape_str(completion, false); let ret = format!( "{}{}{}", &self.completer.original_input[..start], @@ -1432,8 +1434,11 @@ impl SimpleCompleter { } let selected = &self.candidates[self.selected_idx]; - let escaped = escape_str(selected, false); - let (start, end) = self.token_span; + let (mut start, end) = self.token_span; + let slice = self.original_input.get(start..end).unwrap_or(""); + start += slice.width(); + let completion = selected.strip_prefix(slice).unwrap_or(selected); + let escaped = escape_str(completion, false); format!( "{}{}{}", &self.original_input[..start], @@ -1596,14 +1601,14 @@ impl SimpleCompleter { .set_range(self.token_span.0..self.token_span.1); } - // If token contains '=', only complete after the '=' + // If token contains any COMP_WORDBREAKS, break the word let token_str = cur_token.span.as_str(); - if let Some(eq_pos) = token_str.rfind('=') { - self.token_span.0 = cur_token.span.range().start + eq_pos + 1; - cur_token - .span - .set_range(self.token_span.0..self.token_span.1); - } + + let word_breaks = read_vars(|v| v.try_get_var("COMP_WORDBREAKS")).unwrap_or("=".into()); + if let Some(break_pos) = token_str.rfind(|c: char| word_breaks.contains(c)) { + self.token_span.0 = cur_token.span.range().start + break_pos + 1; + cur_token.span.set_range(self.token_span.0..self.token_span.1); + } let raw_tk = cur_token.as_str().to_string(); let expanded_tk = cur_token.expand()?; diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 77fa9cd..165ba1a 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -1055,6 +1055,7 @@ impl ShedVi { let pending_seq = self.mode.pending_seq().unwrap_or_default(); write!(buf, "\n: {pending_seq}").unwrap(); new_layout.end.row += 1; + new_layout.cursor.row += 1; } write!(buf, "{}", &self.mode.cursor_style()).unwrap(); diff --git a/src/state.rs b/src/state.rs index 4ecac86..2788cef 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1045,7 +1045,7 @@ impl VarTab { } } pub fn new() -> Self { - let vars = HashMap::new(); + let vars = Self::init_sh_vars(); let params = Self::init_params(); Self::init_env(); let mut var_tab = Self { @@ -1064,6 +1064,11 @@ impl VarTab { params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any) params } + fn init_sh_vars() -> HashMap { + let mut vars = HashMap::new(); + vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into()); + vars + } fn init_env() { let pathbuf_to_string = |pb: Result| pb.unwrap_or_default().to_string_lossy().to_string();