work on improving parameter expansion logic

This commit is contained in:
2026-03-04 18:05:48 -05:00
parent 77a1dc740f
commit 7b5337367b
11 changed files with 702 additions and 64 deletions

491
flamegraph.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 820 KiB

BIN
perf.data Normal file

Binary file not shown.

BIN
perf.data.old Normal file

Binary file not shown.

View File

@@ -1026,7 +1026,7 @@ pub fn unescape_str(raw: &str) -> String {
result.push(next_ch); result.push(next_ch);
} }
} }
'$' => { '$' if chars.peek() != Some(&'\'') => {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);
if chars.peek() == Some(&'(') { if chars.peek() == Some(&'(') {
chars.next(); chars.next();
@@ -1058,6 +1058,76 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
} }
'$' => {
log::debug!("Found ANSI-C quoting");
chars.next();
while let Some(q_ch) = chars.next() {
match q_ch {
'\'' => {
break;
}
'\\' => {
if let Some(esc) = chars.next() {
match esc {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'\'' => result.push('\''),
'\\' => result.push('\\'),
'a' => result.push('\x07'),
'b' => result.push('\x08'),
'e' | 'E' => result.push('\x1b'),
'v' => result.push('\x0b'),
'x' => {
let mut hex = String::new();
if let Some(h1) = chars.next() {
hex.push(h1);
} else {
result.push_str("\\x");
continue;
}
if let Some(h2) = chars.next() {
hex.push(h2);
} else {
result.push_str(&format!("\\x{hex}"));
continue;
}
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push_str(&format!("\\x{hex}"));
continue;
}
}
'o' => {
let mut oct = String::new();
for _ in 0..3 {
if let Some(o) = chars.peek() {
if o.is_digit(8) {
oct.push(*o);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(byte) = u8::from_str_radix(&oct, 8) {
result.push(byte as char);
} else {
result.push_str(&format!("\\o{oct}"));
continue;
}
}
_ => result.push(esc),
}
}
}
_ => result.push(q_ch),
}
}
}
'"' => { '"' => {
result.push(markers::DUB_QUOTE); result.push(markers::DUB_QUOTE);
break; break;
@@ -1145,6 +1215,7 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
'$' if chars.peek() == Some(&'\'') => { '$' if chars.peek() == Some(&'\'') => {
log::debug!("Found ANSI-C quoting");
chars.next(); chars.next();
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
@@ -1318,6 +1389,8 @@ impl FromStr for ParamExp {
) ) ) )
}; };
log::debug!("Parsing parameter expansion: '{:?}'", s);
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') { if let Some(var) = s.strip_prefix('!') {
if var.ends_with('*') || var.ends_with('@') { if var.ends_with('*') || var.ends_with('@') {
@@ -1333,6 +1406,7 @@ impl FromStr for ParamExp {
return Ok(RemShortestPrefix(rest.to_string())); return Ok(RemShortestPrefix(rest.to_string()));
} }
if let Some(rest) = s.strip_prefix("%%") { if let Some(rest) = s.strip_prefix("%%") {
log::debug!("Matched longest suffix pattern: '{}'", rest);
return Ok(RemLongestSuffix(rest.to_string())); return Ok(RemLongestSuffix(rest.to_string()));
} else if let Some(rest) = s.strip_prefix('%') { } else if let Some(rest) = s.strip_prefix('%') {
return Ok(RemShortestSuffix(rest.to_string())); return Ok(RemShortestSuffix(rest.to_string()));
@@ -1512,7 +1586,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::RemShortestPrefix(prefix) => { ParamExp::RemShortestPrefix(prefix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&prefix).unwrap(); let unescaped = unescape_str(&prefix);
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix);
let pattern = Pattern::new(&expanded).unwrap();
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[..i]; let sliced = &value[..i];
if pattern.matches(sliced) { if pattern.matches(sliced) {
@@ -1523,7 +1599,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::RemLongestPrefix(prefix) => { ParamExp::RemLongestPrefix(prefix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&prefix).unwrap(); let unescaped = unescape_str(&prefix);
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix);
let pattern = Pattern::new(&expanded).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[..i]; let sliced = &value[..i];
if pattern.matches(sliced) { if pattern.matches(sliced) {
@@ -1534,7 +1612,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::RemShortestSuffix(suffix) => { ParamExp::RemShortestSuffix(suffix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&suffix).unwrap(); let unescaped = unescape_str(&suffix);
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix);
let pattern = Pattern::new(&expanded).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[i..]; let sliced = &value[i..];
if pattern.matches(sliced) { if pattern.matches(sliced) {
@@ -1545,7 +1625,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::RemLongestSuffix(suffix) => { ParamExp::RemLongestSuffix(suffix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&suffix).unwrap(); let unescaped = unescape_str(&suffix);
let expanded_suffix = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone());
let pattern = Pattern::new(&expanded_suffix).unwrap();
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[i..]; let sliced = &value[i..];
if pattern.matches(sliced) { if pattern.matches(sliced) {
@@ -1556,12 +1638,16 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::ReplaceFirstMatch(search, replace) => { ParamExp::ReplaceFirstMatch(search, replace) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let regex = glob_to_regex(&search, false); // unanchored pattern let search = unescape_str(&search);
let replace = unescape_str(&replace);
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
let regex = glob_to_regex(&expanded_search, false); // unanchored pattern
if let Some(mat) = regex.find(&value) { if let Some(mat) = regex.find(&value) {
let before = &value[..mat.start()]; let before = &value[..mat.start()];
let after = &value[mat.end()..]; let after = &value[mat.end()..];
let result = format!("{}{}{}", before, replace, after); let result = format!("{}{}{}", before, expanded_replace, after);
Ok(result) Ok(result)
} else { } else {
Ok(value) Ok(value)
@@ -1569,13 +1655,17 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::ReplaceAllMatches(search, replace) => { ParamExp::ReplaceAllMatches(search, replace) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let regex = glob_to_regex(&search, false); let search = unescape_str(&search);
let replace = unescape_str(&replace);
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
let regex = glob_to_regex(&expanded_search, false);
let mut result = String::new(); let mut result = String::new();
let mut last_match_end = 0; let mut last_match_end = 0;
for mat in regex.find_iter(&value) { for mat in regex.find_iter(&value) {
result.push_str(&value[last_match_end..mat.start()]); result.push_str(&value[last_match_end..mat.start()]);
result.push_str(&replace); result.push_str(&expanded_replace);
last_match_end = mat.end(); last_match_end = mat.end();
} }
@@ -1585,22 +1675,30 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::ReplacePrefix(search, replace) => { ParamExp::ReplacePrefix(search, replace) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&search).unwrap(); let search = unescape_str(&search);
let replace = unescape_str(&replace);
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
let pattern = Pattern::new(&expanded_search).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[..i]; let sliced = &value[..i];
if pattern.matches(sliced) { if pattern.matches(sliced) {
return Ok(format!("{}{}", replace, &value[i..])); return Ok(format!("{}{}", expanded_replace, &value[i..]));
} }
} }
Ok(value) Ok(value)
} }
ParamExp::ReplaceSuffix(search, replace) => { ParamExp::ReplaceSuffix(search, replace) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let pattern = Pattern::new(&search).unwrap(); let search = unescape_str(&search);
let replace = unescape_str(&replace);
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
let pattern = Pattern::new(&expanded_search).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[i..]; let sliced = &value[i..];
if pattern.matches(sliced) { if pattern.matches(sliced) {
return Ok(format!("{}{}", &value[..i], replace)); return Ok(format!("{}{}", &value[..i], expanded_replace));
} }
} }
Ok(value) Ok(value)

View File

@@ -215,13 +215,12 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Restore cursor to saved row before clearing, since the terminal // Restore cursor to saved row before clearing, since the terminal
// may have moved it during resize/rewrap // may have moved it during resize/rewrap
readline.writer.update_t_cols(); readline.writer.update_t_cols();
readline.prompt_mut().refresh()?;
readline.mark_dirty(); readline.mark_dirty();
} }
if JOB_DONE.swap(false, Ordering::SeqCst) { if JOB_DONE.swap(false, Ordering::SeqCst) {
// update the prompt so any job count escape sequences update dynamically // update the prompt so any job count escape sequences update dynamically
readline.prompt_mut().refresh()?; readline.prompt_mut().refresh();
} }
readline.print_line(false)?; readline.print_line(false)?;
@@ -388,6 +387,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(true)?; readline.reset(true)?;
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }

View File

@@ -424,18 +424,13 @@ impl Dispatcher {
'outer: for block in case_blocks { 'outer: for block in case_blocks {
let CaseNode { pattern, body } = block; let CaseNode { pattern, body } = block;
let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim(); let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim();
log::debug!("[case] raw block pattern: {:?}", block_pattern_raw);
// Split at '|' to allow for multiple patterns like `foo|bar)` // Split at '|' to allow for multiple patterns like `foo|bar)`
let block_patterns = block_pattern_raw.split('|'); let block_patterns = block_pattern_raw.split('|');
for pattern in block_patterns { for pattern in block_patterns {
log::debug!("[case] testing pattern {:?} against input {:?}", pattern, pattern_raw);
let pattern_exp = Expander::from_raw(pattern)?.expand()?.join(" "); let pattern_exp = Expander::from_raw(pattern)?.expand()?.join(" ");
log::debug!("[case] expanded pattern: {:?}", pattern_exp);
let pattern_regex = glob_to_regex(&pattern_exp, false); let pattern_regex = glob_to_regex(&pattern_exp, false);
log::debug!("[case] testing input {:?} against pattern {:?} (regex: {:?})", pattern_raw, pattern_exp, pattern_regex);
if pattern_regex.is_match(&pattern_raw) { if pattern_regex.is_match(&pattern_raw) {
log::debug!("[case] matched pattern {:?}", pattern_exp);
for node in &body { for node in &body {
s.dispatch_node(node.clone())?; s.dispatch_node(node.clone())?;
} }

View File

@@ -26,6 +26,7 @@ pub struct Highlighter {
style_stack: Vec<StyleSet>, style_stack: Vec<StyleSet>,
last_was_reset: bool, last_was_reset: bool,
in_selection: bool, in_selection: bool,
only_hl_visual: bool
} }
impl Highlighter { impl Highlighter {
@@ -38,8 +39,12 @@ impl Highlighter {
style_stack: Vec::new(), style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false, in_selection: false,
only_hl_visual: false
} }
} }
pub fn only_visual(&mut self, only_visual: bool) {
self.only_hl_visual = only_visual;
}
/// Loads raw input text and annotates it with syntax markers /// Loads raw input text and annotates it with syntax markers
/// ///
@@ -61,6 +66,26 @@ impl Highlighter {
out out
} }
pub fn expand_control_chars(&mut self) {
let mut expanded = String::new();
let mut chars = self.input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\n' | '\t' | '\r' => expanded.push(ch),
c if c as u32 <= 0x1F => {
let display = (c as u8 + b'@') as char;
expanded.push_str("\x1b[7m^");
expanded.push(display);
expanded.push_str("\x1b[0m");
}
_ => expanded.push(ch),
}
}
self.input = expanded;
}
/// Processes the annotated input and generates ANSI-styled output /// Processes the annotated input and generates ANSI-styled output
/// ///
/// Walks through the input character by character, interpreting markers and /// Walks through the input character by character, interpreting markers and
@@ -79,6 +104,9 @@ impl Highlighter {
self.reapply_style(); self.reapply_style();
self.in_selection = false; self.in_selection = false;
} }
_ if self.only_hl_visual => {
self.output.push(ch);
}
markers::STRING_DQ_END markers::STRING_DQ_END
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::VAR_SUB_END | markers::VAR_SUB_END

View File

@@ -2885,6 +2885,8 @@ impl LineBuf {
Verb::Insert(string) => { Verb::Insert(string) => {
self.push_str(&string); self.push_str(&string);
let graphemes = string.graphemes(true).count(); let graphemes = string.graphemes(true).count();
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
log::debug!("buffer after insert: {:?}", self.buffer);
self.cursor.add(graphemes); self.cursor.add(graphemes);
} }
Verb::Indent => { Verb::Indent => {

View File

@@ -140,6 +140,7 @@ pub struct Prompt {
ps1_raw: String, ps1_raw: String,
psr_expanded: Option<String>, psr_expanded: Option<String>,
psr_raw: Option<String>, psr_raw: Option<String>,
dirty: bool,
} }
impl Prompt { impl Prompt {
@@ -170,32 +171,54 @@ impl Prompt {
ps1_raw, ps1_raw,
psr_expanded, psr_expanded,
psr_raw, psr_raw,
dirty: false,
} }
} }
pub fn get_ps1(&self) -> &str { pub fn get_ps1(&mut self) -> &str {
if self.dirty {
self.refresh_now();
}
&self.ps1_expanded &self.ps1_expanded
} }
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> { pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw; self.ps1_raw = ps1_raw;
self.dirty = true;
Ok(()) Ok(())
} }
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> { pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw); self.psr_raw = Some(psr_raw);
self.dirty = true;
Ok(()) Ok(())
} }
pub fn get_psr(&self) -> Option<&str> { pub fn get_psr(&mut self) -> Option<&str> {
if self.dirty {
self.refresh_now();
}
self.psr_expanded.as_deref() self.psr_expanded.as_deref()
} }
pub fn refresh(&mut self) -> ShResult<()> { /// Mark the prompt as needing re-expansion on next access.
self.ps1_expanded = expand_prompt(&self.ps1_raw)?; pub fn invalidate(&mut self) {
if let Some(psr_raw) = &self.psr_raw { self.dirty = true;
self.psr_expanded = Some(expand_prompt(psr_raw)?);
} }
Ok(())
fn refresh_now(&mut self) {
let saved_status = state::get_status();
if let Ok(expanded) = expand_prompt(&self.ps1_raw) {
self.ps1_expanded = expanded;
}
if let Some(psr_raw) = &self.psr_raw {
if let Ok(expanded) = expand_prompt(psr_raw) {
self.psr_expanded = Some(expanded);
}
}
state::set_status(saved_status);
self.dirty = false;
}
pub fn refresh(&mut self) {
self.invalidate();
} }
} }
@@ -207,6 +230,7 @@ impl Default for Prompt {
ps1_raw: Self::DEFAULT_PS1.to_string(), ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_expanded: None, psr_expanded: None,
psr_raw: None, psr_raw: None,
dirty: false,
} }
} }
} }
@@ -253,7 +277,7 @@ impl ShedVi {
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(new.mode.report_mode().to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(new.mode.report_mode().to_string()), VarFlags::NONE))?;
new.prompt.refresh()?; new.prompt.refresh();
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?; new.print_line(false)?;
Ok(new) Ok(new)
@@ -296,7 +320,7 @@ impl ShedVi {
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
// Clear old display before resetting state — old_layout must survive // Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout // so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new(); self.prompt.refresh();
self.editor = Default::default(); self.editor = Default::default();
self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>)); self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>));
self.needs_redraw = true; self.needs_redraw = true;
@@ -598,14 +622,6 @@ impl ShedVi {
Ok(None) Ok(None)
} }
pub fn update_layout(&mut self) {
let text = self.line_text();
let new = self.get_layout(&text);
if let Some(old) = self.old_layout.as_mut() {
*old = new;
}
}
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(*TTY_FILENO); let (cols, _) = get_win_size(*TTY_FILENO);
@@ -654,8 +670,7 @@ impl ShedVi {
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE))) && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
} }
ModeReport::Ex => false, ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false,
_ => unimplemented!(),
} }
} else { } else {
false false
@@ -677,21 +692,19 @@ impl ShedVi {
pub fn line_text(&mut self) -> String { pub fn line_text(&mut self) -> String {
let line = self.editor.to_string(); let line = self.editor.to_string();
let hint = self.editor.get_hint_text(); let hint = self.editor.get_hint_text();
if crate::state::read_shopts(|s| s.prompt.highlight) { let do_hl = state::read_shopts(|s| s.prompt.highlight);
self self.highlighter.only_visual(!do_hl);
.highlighter self.highlighter.load_input(&line, self.editor.cursor_byte_pos());
.load_input(&line, self.editor.cursor_byte_pos()); self.highlighter.expand_control_chars();
self.highlighter.highlight(); self.highlighter.highlight();
let highlighted = self.highlighter.take(); let highlighted = self.highlighter.take();
format!("{highlighted}{hint}") format!("{highlighted}{hint}")
} else {
format!("{line}{hint}")
}
} }
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
let line = self.line_text(); let line = self.line_text();
let mut new_layout = self.get_layout(&line); let mut new_layout = self.get_layout(&line);
let pending_seq = self.mode.pending_seq(); let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone(); let mut prompt_string_right = self.prompt.psr_expanded.clone();
@@ -710,7 +723,7 @@ impl ShedVi {
.get_ps1() .get_ps1()
.lines() .lines()
.next() .next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0)) .map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0, false))
.map(|p| p.col) .map(|p| p.col)
.unwrap_or_default() as usize; .unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0; let one_line = new_layout.end.row == 0;
@@ -769,7 +782,7 @@ impl ShedVi {
// Record where the PSR ends so clear_rows can account for wrapping // Record where the PSR ends so clear_rows can account for wrapping
// if the terminal shrinks. // if the terminal shrinks.
let psr_start = Pos { row: new_layout.end.row, col: to_col }; let psr_start = Pos { row: new_layout.end.row, col: to_col };
new_layout.psr_end = Some(Layout::calc_pos(self.writer.t_cols, &psr, psr_start, 0)); new_layout.psr_end = Some(Layout::calc_pos(self.writer.t_cols, &psr, psr_start, 0, false));
} }
if let ModeReport::Ex = self.mode.report_mode() { if let ModeReport::Ex = self.mode.report_mode() {
@@ -781,6 +794,7 @@ impl ShedVi {
write!(buf, "{}", &self.mode.cursor_style()).unwrap(); write!(buf, "{}", &self.mode.cursor_style()).unwrap();
self.writer.flush_write(&buf)?; self.writer.flush_write(&buf)?;
// Tell the completer the width of the prompt line above its \n so it can // Tell the completer the width of the prompt line above its \n so it can
// account for wrapping when clearing after a resize. // account for wrapping when clearing after a resize.
let preceding_width = if new_layout.psr_end.is_some() { let preceding_width = if new_layout.psr_end.is_some() {
@@ -808,7 +822,7 @@ impl ShedVi {
std::mem::swap(&mut self.mode, mode); std::mem::swap(&mut self.mode, mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok(); write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
self.prompt.refresh().ok(); self.prompt.refresh();
let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange)); let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange));
post_mode_change.exec(); post_mode_change.exec();
@@ -820,7 +834,7 @@ impl ShedVi {
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { let mut mode: Box<dyn ViMode> = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
if let Some(saved) = self.saved_mode.take() { if let Some(saved) = self.saved_mode.take() {
saved saved
} else { } else {
@@ -874,7 +888,7 @@ impl ShedVi {
if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) { if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) {
self.saved_mode = Some(mode); self.saved_mode = Some(mode);
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?;
self.prompt.refresh()?; self.prompt.refresh();
return Ok(()); return Ok(());
} }
@@ -899,7 +913,7 @@ impl ShedVi {
} }
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?;
self.prompt.refresh()?; self.prompt.refresh();
return Ok(()); return Ok(());
@@ -1055,7 +1069,6 @@ pub fn annotate_input(input: &str) -> String {
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
.collect(); .collect();
log::debug!("Annotating input with tokens: {tokens:#?}");
for tk in tokens.into_iter().rev() { for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk); let insertions = annotate_token(tk);

View File

@@ -736,9 +736,9 @@ impl Layout {
} }
} }
pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self { pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self {
let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 }, 0); let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 }, 0, false);
let cursor = Self::calc_pos(term_width, to_cursor, prompt_end, prompt_end.col); let cursor = Self::calc_pos(term_width, to_cursor, prompt_end, prompt_end.col, true);
let end = Self::calc_pos(term_width, to_end, prompt_end, prompt_end.col); let end = Self::calc_pos(term_width, to_end, prompt_end, prompt_end.col, false);
Layout { Layout {
prompt_end, prompt_end,
cursor, cursor,
@@ -748,7 +748,15 @@ impl Layout {
} }
} }
pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16) -> Pos { fn is_ctl_char(gr: &str) -> bool {
gr.len() > 0 &&
gr.as_bytes()[0] <= 0x1F &&
gr != "\n" &&
gr != "\t" &&
gr != "\r"
}
pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos {
const TAB_STOP: u16 = 8; const TAB_STOP: u16 = 8;
let mut pos = orig; let mut pos = orig;
let mut esc_seq = 0; let mut esc_seq = 0;
@@ -759,6 +767,8 @@ impl Layout {
} }
let c_width = if c == "\t" { let c_width = if c == "\t" {
TAB_STOP - (pos.col % TAB_STOP) TAB_STOP - (pos.col % TAB_STOP)
} else if raw_calc && Self::is_ctl_char(c) {
2
} else { } else {
width(c, &mut esc_seq) width(c, &mut esc_seq)
}; };
@@ -1002,7 +1012,7 @@ impl LineWriter for TermWriter {
self.buffer.push_str(prompt); self.buffer.push_str(prompt);
let multiline = line.contains('\n'); let multiline = line.contains('\n');
if multiline { if multiline {
let prompt_end = Layout::calc_pos(self.t_cols, prompt, Pos { col: 0, row: 0 }, 0); let prompt_end = Layout::calc_pos(self.t_cols, prompt, Pos { col: 0, row: 0 }, 0, false);
let display_line = enumerate_lines(line, prompt_end.col as usize); let display_line = enumerate_lines(line, prompt_end.col as usize);
self.buffer.push_str(&display_line); self.buffer.push_str(&display_line);
} else { } else {

View File

@@ -24,6 +24,7 @@ impl ViMode for ViVerbatim {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key { match key {
E(K::Verbatim(seq),_mods) => { E(K::Verbatim(seq),_mods) => {
log::debug!("Received verbatim key sequence: {:?}", seq);
let cmd = ViCmd { register: RegisterName::default(), let cmd = ViCmd { register: RegisterName::default(),
verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))), verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))),
motion: None, motion: None,
@@ -61,6 +62,6 @@ impl ViMode for ViVerbatim {
Some(To::End) Some(To::End)
} }
fn report_mode(&self) -> ModeReport { fn report_mode(&self) -> ModeReport {
ModeReport::Insert ModeReport::Verbatim
} }
} }