more work on linebuf reimpl

This commit is contained in:
2026-03-19 19:45:54 -04:00
parent 22113cbdfd
commit c4ba89f83a
7 changed files with 444 additions and 97 deletions

View File

@@ -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<Line> {
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::<Vec<String>>()
.join("\n")
}
pub fn trim_lines(lines: &mut Vec<Line>) {
while lines.last().is_some_and(|line| line.is_empty()) {
lines.pop();
@@ -138,10 +141,10 @@ pub fn split_lines_at(lines: &mut Vec<Line>, pos: Pos) -> Vec<Line> {
}
pub fn attach_lines(lines: &mut Vec<Line>, other: &mut Vec<Line>) {
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<Tk>,
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<MotionKind> {
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<MotionKind> {
fn text_obj_word(
&mut self,
count: u16,
word: Word,
obj: TextObj,
bound: Bound,
) -> Option<MotionKind> {
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<MotionKind> {
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::<i64>() {
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<Line> {
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<MotionKind> {
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<Line> {
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<Line> {
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);

View File

@@ -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() {

View File

@@ -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}"
);
}

View File

@@ -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(_))

View File

@@ -110,7 +110,7 @@ pub trait ViMode {
pub fn common_cmds(key: E) -> Option<ViCmd> {
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)),

View File

@@ -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;

View File

@@ -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;