diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index e537646..3b5c7df 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -1,37 +1,32 @@ use std::{ - collections::HashSet, fmt::Display, - ops::{Index, IndexMut, Range, RangeBounds, RangeFull, RangeInclusive}, + ops::{Index, IndexMut}, slice::SliceIndex, }; -use itertools::Itertools; use smallvec::SmallVec; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthChar; use super::vicmd::{ - Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, + Anchor, Bound, Dest, Direction, Motion, MotionCmd, TextObj, To, Verb, ViCmd, Word, }; use crate::{ expand::expand_cmd_sub, - libsh::{error::ShResult, guards::var_ctx_guard}, + libsh::error::ShResult, parse::{ Redir, RedirType, execute::exec_input, - lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule}, + lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, }, prelude::*, procio::{IoFrame, IoMode, IoStack}, readline::{ - history::History, markers, - register::{RegisterContent, write_register}, - term::{RawModeGuard, get_win_size}, - vicmd::{ReadSrc, VerbCmd, WriteDest}, + register::RegisterContent, vicmd::{ReadSrc, VerbCmd, WriteDest}, }, - state::{VarFlags, VarKind, read_shopts, write_meta, write_vars}, + state::{read_vars, write_meta}, }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -124,6 +119,14 @@ pub fn to_lines(s: impl ToString) -> Vec { s.split("\n").map(to_graphemes).map(Line::from).collect() } +pub fn join_lines(lines: &[Line]) -> String { + lines + .iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n") +} + pub fn trim_lines(lines: &mut Vec) { while lines.last().is_some_and(|line| line.is_empty()) { lines.pop(); @@ -138,10 +141,10 @@ pub fn split_lines_at(lines: &mut Vec, pos: Pos) -> Vec { } pub fn attach_lines(lines: &mut Vec, other: &mut Vec) { - if other.len() == 0 { + if other.is_empty() { return; } - if lines.len() == 0 { + if lines.is_empty() { lines.append(other); return; } @@ -328,13 +331,6 @@ pub enum SelectMode { Block(Pos), } -#[derive(Debug, Clone, PartialEq, Eq)] -enum CaseTransform { - Toggle, - Lower, - Upper, -} - #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Pos { pub row: usize, @@ -371,6 +367,7 @@ pub enum MotionKind { Line { start: usize, end: usize, + inclusive: bool }, Block { start: Pos, @@ -424,7 +421,6 @@ impl Edit { pub struct IndentCtx { depth: usize, ctx: Vec, - in_escaped_line: bool, } impl IndentCtx { @@ -459,16 +455,12 @@ impl IndentCtx { self.descend(tk); } else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) { 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 { self.depth = 0; self.ctx.clear(); - self.in_escaped_line = false; let input_arc = Arc::new(input.to_string()); let Ok(tokens) = @@ -482,11 +474,6 @@ impl IndentCtx { self.check_tk(tk); } - if input.ends_with("\\\n") { - self.in_escaped_line = true; - self.depth += 1; - } - self.depth } } @@ -651,8 +638,6 @@ impl LineBuf { self.cursor.pos = pos; } fn set_row(&mut self, row: usize) { - let target_col = self.saved_col.unwrap_or(self.cursor.pos.col); - self.set_cursor(Pos { row, col: self.saved_col.unwrap_or(self.cursor.pos.col), @@ -676,11 +661,19 @@ impl LineBuf { } fn break_line(&mut self) { let (row, col) = self.row_col(); - let rest = self.lines[row].split_off(col); + let level = self.calc_indent_level(); + log::debug!("level: {level}"); + let mut rest = self.lines[row].split_off(col); + let mut col = 0; + for tab in std::iter::repeat_n(Grapheme::from('\t'), level) { + rest.insert(0, tab); + col += 1; + } + self.lines.insert(row + 1, rest); self.cursor.pos = Pos { row: row + 1, - col: 0, + col, }; } fn verb_shell_cmd(&self, cmd: &str) -> ShResult<()> { @@ -706,7 +699,7 @@ impl LineBuf { self.break_line(); } else { self.insert(gr); - self.cursor.pos = self.offset_cursor(0, 1); + self.cursor.pos.col += 1; } } } @@ -954,7 +947,7 @@ impl LineBuf { let mut last = first_non_ws; while let Some((_, c)) = classes.peek() { - if c.is_other_class_or_ws(&first_non_ws.1) { + if c.is_ws() { return Some(last.0); } last = classes.next()?; @@ -1031,14 +1024,16 @@ impl LineBuf { fn dispatch_text_obj(&mut self, count: u16, obj: TextObj) -> Option { match obj { // text structures - TextObj::Word(word, bound) => todo!(), + TextObj::Word(word, bound) => self.text_obj_word(count, word, obj, bound), TextObj::Sentence(direction) => todo!(), TextObj::Paragraph(direction) => todo!(), TextObj::WholeSentence(bound) => todo!(), TextObj::WholeParagraph(bound) => todo!(), // quote stuff - TextObj::DoubleQuote(bound) | TextObj::SingleQuote(bound) | TextObj::BacktickQuote(bound) => { + TextObj::DoubleQuote(bound) | + TextObj::SingleQuote(bound) | + TextObj::BacktickQuote(bound) => { self.text_obj_quote(count, obj, bound) } @@ -1052,7 +1047,165 @@ impl LineBuf { TextObj::Custom(_) => todo!(), } } - fn text_obj_quote(&mut self, count: u16, obj: TextObj, bound: Bound) -> Option { + fn text_obj_word( + &mut self, + count: u16, + word: Word, + obj: TextObj, + bound: Bound, + ) -> Option { + use CharClass as C; + let mut fwd_classes = self.char_classes_forward(); + let first_class = fwd_classes.next()?; + match first_class { + (pos,C::Whitespace) => { + match bound { + Bound::Inside => { + let mut fwd_classes = self.char_classes_forward_from(pos).peekable(); + let mut bkwd_classes = self.char_classes_backward_from(pos).peekable(); + let mut first = (pos,C::Whitespace); + let mut last = (pos,C::Whitespace); + while let Some((_,c)) = bkwd_classes.peek() { + if !c.is_ws() { + break; + } + first = bkwd_classes.next()?; + } + + while let Some((_,c)) = fwd_classes.peek() { + if !c.is_ws() { + break; + } + last = fwd_classes.next()?; + } + + Some(MotionKind::Char { + start: first.0, + end: last.0, + inclusive: true + }) + } + Bound::Around => { + let mut fwd_classes = self.char_classes_forward_from(pos).peekable(); + let mut bkwd_classes = self.char_classes_backward_from(pos).peekable(); + let mut first = (pos,C::Whitespace); + let mut last = (pos,C::Whitespace); + while let Some((_,cl)) = bkwd_classes.peek() { + if !cl.is_ws() { + break; + } + first = bkwd_classes.next()?; + } + + while let Some((_,cl)) = fwd_classes.peek() { + if !cl.is_ws() { + break; + } + last = fwd_classes.next()?; + } + let word_class = fwd_classes.next()?.1; + while let Some((_,cl)) = fwd_classes.peek() { + match word { + Word::Big => { + if cl.is_ws() { + break + } + } + Word::Normal => { + if cl.is_other_class_or_ws(&word_class) { + break + } + } + } + last = fwd_classes.next()?; + } + + Some(MotionKind::Char { + start: first.0, + end: last.0, + inclusive: true + }) + } + } + } + (pos, c) => { + let break_cond = |cl: &C, c: &C| -> bool { + match word { + Word::Big => cl.is_ws(), + Word::Normal => cl.is_other_class(c), + } + }; + match bound { + Bound::Inside => { + let mut fwd_classes = self.char_classes_forward_from(pos).peekable(); + let mut bkwd_classes = self.char_classes_backward_from(pos).peekable(); + let mut first = (pos,c); + let mut last = (pos,c); + + while let Some((_,cl)) = bkwd_classes.peek() { + if break_cond(cl, &c) { + break; + } + first = bkwd_classes.next()?; + } + + while let Some((_,cl)) = fwd_classes.peek() { + if break_cond(cl, &c) { + break; + } + last = fwd_classes.next()?; + } + + Some(MotionKind::Char { + start: first.0, + end: last.0, + inclusive: true + }) + } + Bound::Around => { + let mut fwd_classes = self.char_classes_forward_from(pos).peekable(); + let mut bkwd_classes = self.char_classes_backward_from(pos).peekable(); + let mut first = (pos,c); + let mut last = (pos,c); + + while let Some((_,cl)) = bkwd_classes.peek() { + if break_cond(cl, &c) { + break; + } + first = bkwd_classes.next()?; + } + + while let Some((_,cl)) = fwd_classes.peek() { + if break_cond(cl, &c) { + break; + } + last = fwd_classes.next()?; + } + + // Include trailing whitespace + while let Some((_,cl)) = fwd_classes.peek() { + if !cl.is_ws() { + break; + } + last = fwd_classes.next()?; + } + + Some(MotionKind::Char { + start: first.0, + end: last.0, + inclusive: true + }) + } + } + } + } + } + fn text_obj_quote( + &mut self, + count: u16, + obj: TextObj, + bound: Bound, + ) -> Option { let q_ch = match obj { TextObj::DoubleQuote(_) => '"', TextObj::SingleQuote(_) => '\'', @@ -1104,11 +1257,6 @@ impl LineBuf { TextObj::Angle(_) => ('<', '>'), _ => unreachable!(), }; - log::debug!( - "Finding text object delimited by '{}' and '{}'", - opener, - closer - ); let mut depth = 0; let start_pos = self .scan_backward(|g| { @@ -1124,7 +1272,6 @@ impl LineBuf { false }) .or_else(|| self.scan_forward(|g| g.as_char() == Some(opener)))?; - log::debug!("Found opener at {:?}", start_pos); depth = 0; let end_pos = self.scan_forward_from(start_pos, |g| { @@ -1136,7 +1283,6 @@ impl LineBuf { } depth == 0 })?; - log::debug!("Found closer at {:?}", end_pos); match bound { Bound::Around => Some(MotionKind::Char { @@ -1155,6 +1301,124 @@ impl LineBuf { } } } + fn gr_at(&self, pos: Pos) -> Option<&Grapheme> { + self.lines.get(pos.row)?.0.get(pos.col) + } + fn clamp_pos(&self, mut pos: Pos) -> Pos { + pos.clamp_row(&self.lines); + pos.clamp_col(&self.lines[pos.row].0, false); + pos + } + fn number_at_cursor(&self) -> Option<(Pos,Pos)> { + self.number_at(self.cursor.pos) + } + /// Returns the start/end span of a number at a given position, if any + fn number_at(&self, mut pos: Pos) -> Option<(Pos,Pos)> { + let is_number_char = |gr: &Grapheme| gr.as_char().is_some_and(|c| c == '.' || c == '-' || c.is_ascii_digit()); + let is_digit = |gr: &Grapheme| gr.as_char().is_some_and(|c| c.is_ascii_digit()); + + pos = self.clamp_pos(pos); + if !is_number_char(self.gr_at(pos)?) { + return None; + } + + let mut start = self.scan_backward_from(pos, |g| !is_digit(g)) + .map(|pos| Pos { row: pos.row, col: pos.col + 1 }) + .unwrap_or(Pos::MIN); + let end = self.scan_forward_from(pos, |g| !is_digit(g)) + .map(|pos| Pos { row: pos.row, col: pos.col.saturating_sub(1) }) + .unwrap_or(Pos { row: pos.row, col: self.lines[pos.row].len().saturating_sub(1) }); + + if start > Pos::MIN && self.lines[start.row][start.col.saturating_sub(1)].as_char() == Some('-') { + start.col -= 1; + } + + Some((start, end)) + } + fn adjust_number(&mut self, inc: i64) -> Option<()> { + let (s,e) = if let Some(range) = self.select_range() { + match range { + Motion::CharRange(s, e) => (s,e), + _ => return None, + } + } else if let Some((s,e)) = self.number_at_cursor() { + (s,e) + } else { + return None; + }; + + let word = self.pos_slice_str(s,e); + + let num_fmt = if word.starts_with("0x") { + let body = word.strip_prefix("0x").unwrap(); + let width = body.len(); + let num = i64::from_str_radix(body, 16).ok()?; + let new_num = num + inc; + format!("0x{new_num:0>width$x}") + } else if word.starts_with("0b") { + let body = word.strip_prefix("0b").unwrap(); + let width = body.len(); + let num = i64::from_str_radix(body, 2).ok()?; + let new_num = num + inc; + format!("0b{new_num:0>width$b}") + } else if word.starts_with("0o") { + let body = word.strip_prefix("0o").unwrap(); + let width = body.len(); + let num = i64::from_str_radix(body, 8).ok()?; + let new_num = num + inc; + format!("0o{new_num:0>width$o}") + } else if let Ok(num) = word.parse::() { + let width = word.len(); + let new_num = num + inc; + if new_num < 0 { + let abs = new_num.unsigned_abs(); + let digit_width = if num < 0 { width - 1 } else { width }; + format!("-{abs:0>digit_width$}") + } else if num < 0 { + let digit_width = width - 1; + format!("{new_num:0>digit_width$}") + } else { + format!("{new_num:0>width$}") + } + } else { return None }; + + self.replace_range(s, e, &num_fmt); + Some(()) + } + fn replace_range(&mut self, s: Pos, e: Pos, new: &str) -> Vec { + let motion = MotionKind::Char { start: s, end: e, inclusive: true }; + let content = self.extract_range(&motion); + self.set_cursor(s); + self.insert_str(new); + content + } + fn pos_slice_str(&self, s: Pos, e: Pos) -> String { + let (s,e) = ordered(s,e); + if s.row == e.row { + self.lines[s.row].0[s.col..=e.col] + .iter() + .map(|g| g.to_string()) + .collect() + } else { + let mut result = String::new(); + // First line from s.col to end + for g in &self.lines[s.row].0[s.col..] { + result.push_str(&g.to_string()); + } + // Middle lines + for line in &self.lines[s.row + 1..e.row] { + result.push('\n'); + result.push_str(&line.to_string()); + } + // Last line from start to e.col + result.push('\n'); + for g in &self.lines[e.row].0[..=e.col] { + result.push_str(&g.to_string()); + } + result + } + } + /// Wrapper for eval_motion_inner that calls it with `check_hint: false` fn eval_motion(&mut self, cmd: &ViCmd) -> Option { self.eval_motion_inner(cmd, false) } @@ -1168,10 +1432,11 @@ impl LineBuf { let kind = match motion { Motion::WholeLine => { - let row = self.row(); + let row = (self.row() + (count.saturating_sub(1))).min(self.lines.len().saturating_sub(1)); Some(MotionKind::Line { start: row, end: row, + inclusive: true }) } Motion::TextObj(text_obj) => self.dispatch_text_obj(*count as u16, text_obj.clone()), @@ -1191,7 +1456,7 @@ impl LineBuf { inclusive: true, }) } - Motion::BeginningOfFirstWord => { + Motion::StartOfFirstWord => { let mut target = Pos { row: self.row(), col: 0, @@ -1210,17 +1475,17 @@ impl LineBuf { inclusive: true, }) } - dir @ (Motion::BeginningOfLine | Motion::EndOfLine) => { - let off = match dir { - Motion::BeginningOfLine => isize::MIN, - Motion::EndOfLine => isize::MAX, + dir @ (Motion::StartOfLine | Motion::EndOfLine) => { + let (inclusive,off) = match dir { + Motion::StartOfLine => (false,isize::MIN), + Motion::EndOfLine => (true,isize::MAX), _ => unreachable!(), }; let target = self.offset_cursor(0, off); (target != self.cursor.pos).then_some(MotionKind::Char { start: self.cursor.pos, end: target, - inclusive: true, + inclusive, }) } Motion::WordMotion(to, word, dir) => { @@ -1239,11 +1504,10 @@ impl LineBuf { Motion::CharSearch(dir, dest, char) => { let off = self.search_char(dir, dest, char); let target = self.offset_cursor(0, off); - let inclusive = matches!(dest, Dest::On); (target != self.cursor.pos).then_some(MotionKind::Char { start: self.cursor.pos, end: target, - inclusive, + inclusive: true, }) } dir @ (Motion::BackwardChar | Motion::ForwardChar) @@ -1277,7 +1541,7 @@ impl LineBuf { let row = self.row(); let target_row = self.offset_row(off); let (s, e) = ordered(row, target_row); - Some(MotionKind::Line { start: s, end: e }) + Some(MotionKind::Line { start: s, end: e, inclusive: true }) } else { if self.saved_col.is_none() { self.saved_col = Some(self.cursor.pos.col); @@ -1307,7 +1571,7 @@ impl LineBuf { let row = self.row(); let target_row = self.offset_row(off); let (s, e) = ordered(row, target_row); - Some(MotionKind::Line { start: s, end: e }) + Some(MotionKind::Line { start: s, end: e, inclusive: false }) } else { let target = self.offset_cursor(off, 0); (target != self.cursor.pos).then_some(MotionKind::Char { @@ -1320,6 +1584,7 @@ impl LineBuf { Motion::WholeBuffer => Some(MotionKind::Line { start: 0, end: self.lines.len().saturating_sub(1), + inclusive: false }), Motion::ToColumn => todo!(), Motion::ToDelimMatch => todo!(), @@ -1336,7 +1601,7 @@ impl LineBuf { } Motion::LineRange(s, e) => { let (s, e) = ordered(*s, *e); - Some(MotionKind::Line { start: s, end: e }) + Some(MotionKind::Line { start: s, end: e, inclusive: false }) } Motion::BlockRange(s, e) => { let (s, e) = ordered(*s, *e); @@ -1352,15 +1617,11 @@ impl LineBuf { self.lines = buffer; kind } + /// Wrapper for apply_motion_inner that calls it with `accept_hint: false` fn apply_motion(&mut self, motion: MotionKind) -> ShResult<()> { self.apply_motion_inner(motion, false) } fn apply_motion_inner(&mut self, motion: MotionKind, accept_hint: bool) -> ShResult<()> { - log::debug!( - "Applying motion: {:?}, current cursor: {:?}", - motion, - self.cursor.pos - ); match motion { MotionKind::Char { end, .. } => { if accept_hint && self.has_hint() && end >= self.end_pos() { @@ -1397,7 +1658,14 @@ impl LineBuf { self.lines = buf; extracted } - MotionKind::Line { start, end } => self.lines.drain(*start..=*end).collect(), + MotionKind::Line { start, end, inclusive } => { + let end = if *inclusive { + *end + } else { + end.saturating_sub(1) + }; + self.lines.drain(*start..=end).collect() + } MotionKind::Block { start, end } => { let (s, e) = ordered(*start, *end); (s.row..=e.row) @@ -1415,6 +1683,7 @@ impl LineBuf { extracted } fn yank_range(&self, motion: &MotionKind) -> Vec { + log::debug!("Yanking range: {:?}", motion); let mut tmp = Self { lines: self.lines.clone(), cursor: self.cursor, @@ -1425,6 +1694,17 @@ impl LineBuf { fn delete_range(&mut self, motion: &MotionKind) -> Vec { self.extract_range(motion) } + pub fn calc_indent_level(&mut self) -> usize { + self.calc_indent_level_for_pos(self.cursor.pos) + } + pub fn calc_indent_level_for_pos(&mut self, pos: Pos) -> usize { + let mut lines = self.lines.clone(); + split_lines_at(&mut lines, pos); + let raw = join_lines(&lines); + log::debug!("Calculating indent level for pos {:?} with raw text:\n{:?}", pos, raw); + + self.indent_ctx.calculate(&raw) + } fn motion_mutation(&mut self, motion: MotionKind, f: impl Fn(&Grapheme) -> Grapheme) { match motion { MotionKind::Char { @@ -1464,7 +1744,8 @@ impl LineBuf { self.lines[e.row][col] = f(&self.lines[e.row][col]); } } - MotionKind::Line { start, end } => { + MotionKind::Line { start, end, inclusive } => { + let end = if inclusive { end } else { end.saturating_sub(1) }; let end = end.min(self.lines.len().saturating_sub(1)); for row in start..=end { let line = self.line_mut(row); @@ -1500,6 +1781,7 @@ impl LineBuf { motion, .. } = cmd; + log::debug!("Executing verb: {:?} with motion: {:?}", verb, motion); let Some(VerbCmd(_, verb)) = verb else { // For verb-less motions in insert mode, merge hint before evaluating // so motions like `w` can see into the hint text @@ -1518,6 +1800,14 @@ impl LineBuf { }; let content = if *verb == Verb::Yank { self.yank_range(&motion) + } else if *verb == Verb::Change && matches!(motion, MotionKind::Line {..}) { + let n_lines = self.lines.len(); + let content = self.delete_range(&motion); + let row = self.row(); + if n_lines > 1 { + self.lines.insert(row, Line::default()); + } + content } else { self.delete_range(&motion) }; @@ -1533,9 +1823,21 @@ impl LineBuf { let (s, _) = ordered(start, end); self.set_cursor(s); } - MotionKind::Line { start, end } => { + MotionKind::Line { start, end, inclusive } => { + let end = if inclusive { end } else { end.saturating_sub(1) }; let (s, _) = ordered(start, end); self.set_row(s); + if *verb == Verb::Change { + // we've gotta indent + let level = self.calc_indent_level(); + let line = self.cur_line_mut(); + let mut col = 0; + for tab in std::iter::repeat_n(Grapheme::from('\t'), level) { + line.0.insert(col, tab); + col += 1; + } + self.cursor.pos = self.offset_cursor(0, col as isize); + } } MotionKind::Block { start, .. } => { let (s, _) = ordered(self.cursor.pos, start); @@ -1580,8 +1882,8 @@ impl LineBuf { .unwrap_or_else(|| gr.clone()) }); } - Verb::IncrementNumber(_) => todo!(), - Verb::DecrementNumber(_) => todo!(), + Verb::IncrementNumber(n) => { self.adjust_number(*n as i64); }, + Verb::DecrementNumber(n) => { self.adjust_number(-(*n as i64)); }, Verb::ToLower => { let Some(motion) = self.eval_motion(cmd) else { return Ok(()); @@ -1625,11 +1927,14 @@ impl LineBuf { }; match content { RegisterContent::Span(lines) => { + let move_cursor = lines.len() == 1 && lines[0].len() > 1; + let content_len: usize = lines.iter().map(|l| l.len()).sum(); let row = self.row(); let col = match anchor { Anchor::After => (self.col() + 1).min(self.cur_line().len()), Anchor::Before => self.col(), }; + let start_len = self.lines[row].len(); let mut right = self.lines[row].split_off(col); let mut lines = lines.clone(); @@ -1645,6 +1950,14 @@ impl LineBuf { // Reattach right half to the last inserted line self.lines[row + last].append(&mut right); + + let end_len = self.lines[row].len(); + let delta = end_len.saturating_sub(start_len); + if move_cursor { + self.cursor.pos = self.offset_cursor(0, delta as isize); + } else if content_len > 1 { + self.cursor.pos = self.offset_cursor(0, 1); + } } RegisterContent::Line(lines) => { let row = match anchor { @@ -1665,9 +1978,18 @@ impl LineBuf { let row = self.row(); let target = (row + 1).min(self.lines.len()); self.lines.insert(target, Line::default()); + + let level = self.calc_indent_level_for_pos(Pos { row: target, col: 0 }); + let line = self.line_mut(target); + let mut col = 0; + for tab in std::iter::repeat_n(Grapheme::from('\t'), level) { + line.insert(0, tab); + col += 1; + } + self.cursor.pos = Pos { row: row + 1, - col: 0, + col, }; } Anchor::Before => { @@ -1724,10 +2046,23 @@ impl LineBuf { self.cursor.exclusive = old_exclusive; } Verb::InsertChar(ch) => { + let level = self.calc_indent_level(); self.insert(Grapheme::from(*ch)); if let Some(motion) = self.eval_motion(cmd) { self.apply_motion(motion)?; } + let new_level = self.calc_indent_level(); + if new_level < level { + let delta = level - new_level; + let line = self.cur_line_mut(); + for _ in 0..delta { + if line.0.first().is_some_and(|c| c.as_char() == Some('\t')) { + line.0.remove(0); + } else { + break + } + } + } } Verb::Insert(s) => self.insert_str(s), Verb::Indent => todo!(), @@ -1745,7 +2080,7 @@ impl LineBuf { write_meta(|m| m.post_system_message(format!("{} is not a file", path_buf.display()))); return Ok(()); } - let Ok(contents) = std::fs::read_to_string(&path_buf) else { + let Ok(contents) = std::fs::read_to_string(path_buf) else { write_meta(|m| { m.post_system_message(format!("Failed to read file {}", path_buf.display())) }); @@ -1810,8 +2145,16 @@ impl LineBuf { } }, Verb::Edit(path) => { - let input = format!("$EDITOR {}", path.display()); - exec_input(input, None, true, Some("ex edit".into()))?; + if read_vars(|v| v.try_get_var("EDITOR")).is_none() { + write_meta(|m| { + m.post_system_message( + "$EDITOR is unset. Aborting edit.".into(), + ) + }); + } else { + let input = format!("$EDITOR {}", path.display()); + exec_input(input, None, true, Some("ex edit".into()))?; + } } Verb::Complete @@ -1919,11 +2262,6 @@ impl LineBuf { } pub fn fix_cursor(&mut self) { - log::debug!( - "Fixing cursor, exclusive: {}, current pos: {:?}", - self.cursor.exclusive, - self.cursor.pos - ); if self.cursor.pos.row >= self.lines.len() { self.cursor.pos.row = self.lines.len().saturating_sub(1); } @@ -2210,6 +2548,10 @@ impl LineBuf { offset + pos.col.min(self.lines[row].len()) } + pub fn cursor_to_flat(&self) -> usize { + self.pos_to_flat(self.cursor.pos) + } + /// Compat shim: attempt history expansion. Stub that returns false. pub fn attempt_history_expansion(&mut self, _history: &super::history::History) -> bool { // TODO: implement history expansion for 2D buffer @@ -2239,12 +2581,6 @@ impl LineBuf { result } - /// Compat shim: calculate indent level. - pub fn calc_indent_level(&mut self) { - let joined = self.joined(); - self.indent_ctx.calculate(&joined); - } - /// Compat shim: mark where insert mode started. pub fn mark_insert_mode_start_pos(&mut self) { self.insert_mode_start_pos = Some(self.cursor.pos); diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 89df8f2..a9fef54 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -901,7 +901,12 @@ impl ShedVi { return Ok(Some(ReadlineEvent::Eof)); } - let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); + // check if it's an edit + // we don't count Verb::Change since its possible for it to be called and not actually change anything + // e.g. 'cc' on an empty line, 'C' at the end of a line, etc. + // this is only used for ringing the bell + let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit() && v.1 != Verb::Change); + let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_))); let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); if is_shell_cmd { @@ -1315,6 +1320,12 @@ impl ShedVi { } pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> { + if cmd.verb().is_some() && let Some(range) = self.editor.select_range() { + cmd.motion = Some(MotionCmd(1, range)) + } else { + log::warn!("You're in visual mode with no select range??"); + }; + if cmd.is_mode_transition() { return self.exec_mode_transition(cmd, from_replay); } else if cmd.is_cmd_repeat() { diff --git a/src/readline/tests.rs b/src/readline/tests.rs index 71a6d44..6a89acf 100644 --- a/src/readline/tests.rs +++ b/src/readline/tests.rs @@ -24,7 +24,7 @@ macro_rules! vi_test { vi.feed_bytes($op.as_bytes()); vi.process_input().unwrap(); assert_eq!(vi.editor.joined(), $expected_text); - assert_eq!(vi.editor.cursor.get(), $expected_cursor); + assert_eq!(vi.editor.cursor_to_flat(), $expected_cursor); } )* @@ -513,6 +513,6 @@ fn vi_auto_indent() { assert_eq!( vi.editor.joined(), - "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}" + "func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\tbar \\\n\t\t\t\tbiz \\\n\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}" ); } diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index 790911e..e5c7129 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -319,8 +319,8 @@ pub enum Motion { WholeLine, TextObj(TextObj), EndOfLastWord, - BeginningOfFirstWord, - BeginningOfLine, + StartOfFirstWord, + StartOfLine, EndOfLine, WordMotion(To, Word, Direction), CharSearch(Direction, Dest, Grapheme), @@ -369,8 +369,8 @@ impl Motion { pub fn is_exclusive(&self) -> bool { matches!( &self, - Self::BeginningOfLine - | Self::BeginningOfFirstWord + Self::StartOfLine + | Self::StartOfFirstWord | Self::ToColumn | Self::TextObj(TextObj::Sentence(_)) | Self::TextObj(TextObj::Paragraph(_)) diff --git a/src/readline/vimode/mod.rs b/src/readline/vimode/mod.rs index b88c613..e9a62fc 100644 --- a/src/readline/vimode/mod.rs +++ b/src/readline/vimode/mod.rs @@ -110,7 +110,7 @@ pub trait ViMode { pub fn common_cmds(key: E) -> Option { let mut pending_cmd = ViCmd::new(); match key { - E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BeginningOfLine)), + E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::StartOfLine)), E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::EndOfLine)), E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)), E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)), diff --git a/src/readline/vimode/normal.rs b/src/readline/vimode/normal.rs index 847df89..132d8fd 100644 --- a/src/readline/vimode/normal.rs +++ b/src/readline/vimode/normal.rs @@ -332,7 +332,7 @@ impl ViNormal { return Some(ViCmd { register, verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), + motion: Some(MotionCmd(1, Motion::StartOfFirstWord)), raw_seq: self.take_cmd(), flags: self.flags(), }); @@ -583,11 +583,11 @@ impl ViNormal { } '^' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord)); + break 'motion_parse Some(MotionCmd(count, Motion::StartOfFirstWord)); } '0' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); + break 'motion_parse Some(MotionCmd(count, Motion::StartOfLine)); } '$' => { chars = chars_clone; diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs index f66c9f6..723f7a8 100644 --- a/src/readline/vimode/visual.rs +++ b/src/readline/vimode/visual.rs @@ -287,7 +287,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::BeginningOfLine)), + motion: Some(MotionCmd(1, Motion::StartOfLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -473,7 +473,7 @@ impl ViVisual { } '0' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); + break 'motion_parse Some(MotionCmd(count, Motion::StartOfLine)); } '$' => { chars = chars_clone;