Compare commits

...

1 Commits

Author SHA1 Message Date
3a5b56fcd3 more improvements to auto indent depth tracking
added test cases for the auto indent/dedent feature
2026-03-14 01:04:03 -04:00
3 changed files with 310 additions and 235 deletions

View File

@@ -15,7 +15,7 @@ use crate::{
libsh::{error::ShResult, guards::var_ctx_guard}, libsh::{error::ShResult, guards::var_ctx_guard},
parse::{ parse::{
execute::exec_input, execute::exec_input,
lex::{LexFlags, LexStream, QuoteState, Tk}, lex::{LexFlags, LexStream, QuoteState, Tk, TkRule},
}, },
prelude::*, prelude::*,
readline::{ readline::{
@@ -351,14 +351,23 @@ impl ClampedUsize {
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct DepthCalc { pub struct IndentCtx {
depth: usize, depth: usize,
ctx: Vec<Tk>, ctx: Vec<Tk>,
in_escaped_line: bool
} }
impl DepthCalc { impl IndentCtx {
pub fn new() -> Self { Self::default() } pub fn new() -> Self { Self::default() }
pub fn depth(&self) -> usize {
self.depth
}
pub fn ctx(&self) -> &[Tk] {
&self.ctx
}
pub fn descend(&mut self, tk: Tk) { pub fn descend(&mut self, tk: Tk) {
self.ctx.push(tk); self.ctx.push(tk);
self.depth += 1; self.depth += 1;
@@ -369,20 +378,28 @@ impl DepthCalc {
self.ctx.pop(); self.ctx.pop();
} }
pub fn reset(&mut self) {
std::mem::take(self);
}
pub fn check_tk(&mut self, tk: Tk) { pub fn check_tk(&mut self, tk: Tk) {
if tk.is_opener() { if tk.is_opener() {
self.descend(tk); self.descend(tk);
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) { } else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
self.ascend(); self.ascend();
} else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line {
self.in_escaped_line = false;
self.depth = self.depth.saturating_sub(1);
} }
} }
pub fn calculate(&mut self, input: &str) -> usize { pub fn calculate(&mut self, input: &str) -> usize {
if input.ends_with("\\\n") { self.depth = 0;
self.depth += 1; // Line continuation, so we need to add an extra level self.ctx.clear();
} self.in_escaped_line = false;
let input = Arc::new(input.to_string());
let Ok(tokens) = LexStream::new(input.clone(), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else { let input_arc = Arc::new(input.to_string());
let Ok(tokens) = LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
log::error!("Lexing failed during depth calculation: {:?}", input); log::error!("Lexing failed during depth calculation: {:?}", input);
return 0; return 0;
}; };
@@ -391,6 +408,11 @@ impl DepthCalc {
self.check_tk(tk); self.check_tk(tk);
} }
if input.ends_with("\\\n") {
self.in_escaped_line = true;
self.depth += 1;
}
self.depth self.depth
} }
} }
@@ -408,7 +430,7 @@ pub struct LineBuf {
pub insert_mode_start_pos: Option<usize>, pub insert_mode_start_pos: Option<usize>,
pub saved_col: Option<usize>, pub saved_col: Option<usize>,
pub auto_indent_level: usize, pub indent_ctx: IndentCtx,
pub undo_stack: Vec<Edit>, pub undo_stack: Vec<Edit>,
pub redo_stack: Vec<Edit>, pub redo_stack: Vec<Edit>,
@@ -662,6 +684,17 @@ impl LineBuf {
pub fn read_slice_to_cursor(&self) -> Option<&str> { pub fn read_slice_to_cursor(&self) -> Option<&str> {
self.read_slice_to(self.cursor.get()) self.read_slice_to(self.cursor.get())
} }
pub fn cursor_is_escaped(&mut self) -> bool {
let Some(to_cursor) = self.slice_to_cursor() else {
return false;
};
// count the number of backslashes
let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len();
// an even number of backslashes means each one is escaped
delta % 2 != 0
}
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> { pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
self.slice_to(self.cursor.ret_add(1)) self.slice_to(self.cursor.ret_add(1))
} }
@@ -2076,15 +2109,13 @@ impl LineBuf {
let end = start + (new.len().max(gr.len())); let end = start + (new.len().max(gr.len()));
self.buffer.replace_range(start..end, new); self.buffer.replace_range(start..end, new);
} }
pub fn calc_indent_level(&mut self) { pub fn calc_indent_level(&mut self) -> usize {
let to_cursor = self let to_cursor = self
.slice_to_cursor() .slice_to_cursor()
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or(self.buffer.clone()); .unwrap_or(self.buffer.clone());
let mut calc = DepthCalc::new(); self.indent_ctx.calculate(&to_cursor)
self.auto_indent_level = calc.calculate(&to_cursor);
} }
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone(); let buffer = self.buffer.clone();
@@ -2661,8 +2692,8 @@ impl LineBuf {
register.write_to_register(register_content); register.write_to_register(register_content);
self.cursor.set(start); self.cursor.set(start);
if do_indent { if do_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..depth).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
@@ -2897,17 +2928,29 @@ impl LineBuf {
}; };
end = end.saturating_sub(1); end = end.saturating_sub(1);
let mut last_was_whitespace = false; let mut last_was_whitespace = false;
for i in start..end { let mut last_was_escape = false;
let mut i = start;
while i < end {
let Some(gr) = self.grapheme_at(i) else { let Some(gr) = self.grapheme_at(i) else {
i += 1;
continue; continue;
}; };
if gr == "\n" { if gr == "\n" {
if last_was_whitespace { if last_was_whitespace {
self.remove(i); self.remove(i);
end -= 1;
} else { } else {
self.force_replace_at(i, " "); self.force_replace_at(i, " ");
} }
if last_was_escape {
// if we are here, then we just joined an escaped newline
// semantically, echo foo\\nbar == echo foo bar
// so a joined line should remove the escape.
self.remove(i - 1);
end -= 1;
}
last_was_whitespace = false; last_was_whitespace = false;
last_was_escape = false;
let strip_pos = if self.grapheme_at(i) == Some(" ") { let strip_pos = if self.grapheme_at(i) == Some(" ") {
i + 1 i + 1
} else { } else {
@@ -2915,22 +2958,39 @@ impl LineBuf {
}; };
while self.grapheme_at(strip_pos) == Some("\t") { while self.grapheme_at(strip_pos) == Some("\t") {
self.remove(strip_pos); self.remove(strip_pos);
end -= 1;
} }
self.cursor.set(i); self.cursor.set(i);
i += 1;
continue; continue;
} else if gr == "\\" {
if last_was_whitespace && last_was_escape {
// if we are here, then the pattern of the last three chars was this:
// ' \\', a space and two backslashes.
// This means the "last" was an escaped backslash, not whitespace.
last_was_whitespace = false;
} }
last_was_escape = !last_was_escape;
} else {
last_was_whitespace = is_whitespace(gr); last_was_whitespace = is_whitespace(gr);
last_was_escape = false;
}
i += 1;
} }
Ok(()) Ok(())
} }
fn verb_insert_char(&mut self, ch: char) { fn verb_insert_char(&mut self, ch: char) {
self.insert_at_cursor(ch); self.insert_at_cursor(ch);
self.cursor.add(1); self.cursor.add(1);
let before = self.auto_indent_level; let before_escaped = self.indent_ctx.in_escaped_line;
let before = self.indent_ctx.depth();
if read_shopts(|o| o.prompt.auto_indent) { if read_shopts(|o| o.prompt.auto_indent) {
self.calc_indent_level(); let after = self.calc_indent_level();
if self.auto_indent_level < before { // Only dedent if the depth decrease came from a closer, not from
let delta = before - self.auto_indent_level; // a line continuation bonus going away
if after < before
&& !(before_escaped && !self.indent_ctx.in_escaped_line) {
let delta = before - after;
let line_start = self.start_of_line(); let line_start = self.start_of_line();
for _ in 0..delta { for _ in 0..delta {
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
@@ -3021,8 +3081,8 @@ impl LineBuf {
Anchor::After => { Anchor::After => {
self.push('\n'); self.push('\n');
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.push('\t'); self.push('\t');
} }
} }
@@ -3031,8 +3091,8 @@ impl LineBuf {
} }
Anchor::Before => { Anchor::Before => {
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.insert_at(0, '\t'); self.insert_at(0, '\t');
} }
} }
@@ -3059,8 +3119,8 @@ impl LineBuf {
self.insert_at_cursor('\n'); self.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.insert_at_cursor('\t'); self.insert_at_cursor('\t');
self.cursor.add(1); self.cursor.add(1);
} }

View File

@@ -253,7 +253,6 @@ pub struct ShedVi {
pub repeat_action: Option<CmdReplay>, pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub next_is_escaped: bool,
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub history: History, pub history: History,
@@ -271,7 +270,6 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
@@ -303,7 +301,6 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
@@ -417,7 +414,7 @@ impl ShedVi {
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 = let lex_result2 =
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0; let is_top_level = self.editor.indent_ctx.ctx().is_empty();
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => { (true, true) => {
@@ -808,14 +805,6 @@ impl ShedVi {
} }
} }
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
&& !self.next_is_escaped
{
self.next_is_escaped = true;
} else {
self.next_is_escaped = false;
}
let Ok(cmd) = self.mode.handle_key_fallible(key) else { let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error // it's an ex mode error
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>; self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
@@ -834,8 +823,7 @@ impl ShedVi {
} }
if cmd.is_submit_action() if cmd.is_submit_action()
&& !self.next_is_escaped && !self.editor.cursor_is_escaped()
&& !self.editor.buffer.ends_with('\\')
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{ {
if self.editor.attempt_history_expansion(&self.history) { if self.editor.attempt_history_expansion(&self.history) {

View File

@@ -233,3 +233,30 @@ vi_test! {
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1; vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1;
vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8 vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8
} }
#[test]
fn vi_auto_indent() {
let (mut vi, _g) = test_vi("");
// Type each line and press Enter separately so auto-indent triggers
let lines = [
"func() {",
"case foo in",
"bar)",
"while true; do",
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}"
];
for (i,line) in lines.iter().enumerate() {
vi.feed_bytes(line.as_bytes());
if i != lines.len() - 1 {
vi.feed_bytes(b"\r");
}
vi.process_input().unwrap();
}
assert_eq!(
vi.editor.as_str(),
"func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}"
);
}