implemented proper cursor placement for line editor

This commit is contained in:
2025-05-27 03:32:08 -04:00
parent 1e3715d353
commit 0e95e006d8
4 changed files with 84 additions and 19 deletions

View File

@@ -142,6 +142,7 @@ pub struct LineBuf {
cursor: usize, cursor: usize,
clamp_cursor: bool, clamp_cursor: bool,
first_line_offset: usize, first_line_offset: usize,
saved_col: Option<usize>,
merge_edit: bool, merge_edit: bool,
undo_stack: Vec<Edit>, undo_stack: Vec<Edit>,
redo_stack: Vec<Edit>, redo_stack: Vec<Edit>,
@@ -155,6 +156,9 @@ impl LineBuf {
self.buffer = initial.to_string(); self.buffer = initial.to_string();
self self
} }
pub fn set_first_line_offset(&mut self, offset: usize) {
self.first_line_offset = offset
}
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.buffer &self.buffer
} }
@@ -177,7 +181,7 @@ impl LineBuf {
// Insert mode does let you set on the edge though, so that you can append new characters // Insert mode does let you set on the edge though, so that you can append new characters
// This method is used in Normal mode // This method is used in Normal mode
dbg!("clamping"); dbg!("clamping");
if self.cursor == self.byte_len() { if self.cursor == self.byte_len() || self.grapheme_at_cursor() == Some("\n") {
self.cursor_back(1); self.cursor_back(1);
} }
} }
@@ -370,12 +374,12 @@ impl LineBuf {
} }
(lines,col) (lines,col)
} }
pub fn cursor_display_coords(&self, first_ln_offset: usize, term_width: usize) -> (usize,usize) { pub fn cursor_display_coords(&self, term_width: usize) -> (usize,usize) {
let (d_line,mut d_col) = self.display_coords(term_width); let (d_line,mut d_col) = self.display_coords(term_width);
let line = self.count_display_lines(first_ln_offset, term_width) - d_line; let line = self.count_display_lines(self.first_line_offset, term_width) - d_line;
if line == self.count_lines() { if line == self.count_lines() {
d_col += first_ln_offset; d_col += self.first_line_offset;
} }
(line,d_col) (line,d_col)
@@ -409,6 +413,40 @@ impl LineBuf {
let end = self.end_of_line(); let end = self.end_of_line();
self.move_to(end) self.move_to(end)
} }
pub fn find_prev_line_pos(&mut self) -> Option<usize> {
if self.start_of_line() == 0 {
return None
};
let mut col = self.saved_col.unwrap_or(self.cursor_column());
let line = self.line_no();
if line == 1 {
col = col.saturating_sub(self.first_line_offset.saturating_sub(1))
}
if self.saved_col.is_none() {
self.saved_col = Some(col);
}
let (start,end) = self.select_line(line - 1).unwrap();
Some((start + col).min(end.saturating_sub(1)))
}
pub fn find_next_line_pos(&mut self) -> Option<usize> {
if self.end_of_line() == self.byte_len() {
return None
};
let mut col = self.saved_col.unwrap_or(self.cursor_column());
let line = self.line_no();
if line == 0 {
col += self.first_line_offset.saturating_sub(1);
}
if self.saved_col.is_none() {
self.saved_col = Some(col);
}
let (start,end) = self.select_line(line + 1).unwrap();
Some((start + col).min(end.saturating_sub(1)))
}
pub fn cursor_column(&self) -> usize {
let line_start = self.start_of_line();
self.buffer[line_start..self.cursor].graphemes(true).count()
}
pub fn start_of_line(&self) -> usize { pub fn start_of_line(&self) -> usize {
if let Some(i) = self.slice_to_cursor().rfind('\n') { if let Some(i) = self.slice_to_cursor().rfind('\n') {
i + 1 // Land on start of this line, instead of the end of the last one i + 1 // Land on start of this line, instead of the end of the last one
@@ -788,7 +826,7 @@ impl LineBuf {
} }
None None
} }
pub fn eval_motion(&self, motion: Motion) -> MotionKind { pub fn eval_motion(&mut self, motion: Motion) -> MotionKind {
match motion { match motion {
Motion::WholeLine => { Motion::WholeLine => {
let (start,end) = self.this_line(); let (start,end) = self.this_line();
@@ -841,8 +879,18 @@ impl LineBuf {
} }
Motion::BackwardChar => MotionKind::Backward(1), Motion::BackwardChar => MotionKind::Backward(1),
Motion::ForwardChar => MotionKind::Forward(1), Motion::ForwardChar => MotionKind::Forward(1),
Motion::LineUp => todo!(), Motion::LineUp => {
Motion::LineDown => todo!(), match self.find_prev_line_pos() {
None => MotionKind::Null,
Some(pos) => MotionKind::To(pos)
}
}
Motion::LineDown => {
match self.find_next_line_pos() {
None => MotionKind::Null,
Some(pos) => MotionKind::To(pos)
}
}
Motion::WholeBuffer => todo!(), Motion::WholeBuffer => todo!(),
Motion::BeginningOfBuffer => MotionKind::To(0), Motion::BeginningOfBuffer => MotionKind::To(0),
Motion::EndOfBuffer => MotionKind::To(self.byte_len()), Motion::EndOfBuffer => MotionKind::To(self.byte_len()),
@@ -856,7 +904,7 @@ impl LineBuf {
} }
} }
Motion::Range(_, _) => todo!(), Motion::Range(_, _) => todo!(),
Motion::Builder(motion_builder) => todo!(), Motion::Builder(_) => todo!(),
Motion::RepeatMotion => todo!(), Motion::RepeatMotion => todo!(),
Motion::RepeatMotionRev => todo!(), Motion::RepeatMotionRev => todo!(),
Motion::Null => todo!(), Motion::Null => todo!(),
@@ -866,20 +914,19 @@ impl LineBuf {
match verb { match verb {
Verb::Change | Verb::Change |
Verb::Delete => { Verb::Delete => {
let deleted; let deleted = match motion {
match motion {
MotionKind::Forward(n) => { MotionKind::Forward(n) => {
let Some(pos) = self.next_pos(n) else { let Some(pos) = self.next_pos(n) else {
return Ok(()) return Ok(())
}; };
let range = self.cursor..pos; let range = self.cursor..pos;
assert!(range.end < self.byte_len()); assert!(range.end < self.byte_len());
deleted = self.buffer.drain(range); self.buffer.drain(range)
} }
MotionKind::To(n) => { MotionKind::To(n) => {
let range = mk_range(self.cursor, n); let range = mk_range(self.cursor, n);
assert!(range.end < self.byte_len()); assert!(range.end < self.byte_len());
deleted = self.buffer.drain(range); self.buffer.drain(range)
} }
MotionKind::Backward(n) => { MotionKind::Backward(n) => {
let Some(back) = self.prev_pos(n) else { let Some(back) = self.prev_pos(n) else {
@@ -887,14 +934,14 @@ impl LineBuf {
}; };
let range = back..self.cursor; let range = back..self.cursor;
dbg!(&range); dbg!(&range);
deleted = self.buffer.drain(range); self.buffer.drain(range)
} }
MotionKind::Range(range) => { MotionKind::Range(range) => {
deleted = self.buffer.drain(range.0..range.1); self.buffer.drain(range.0..range.1)
} }
MotionKind::Line(n) => { MotionKind::Line(n) => {
let (start,end) = match n.cmp(&0) { let (start,end) = match n.cmp(&0) {
Ordering::Less => self.select_lines_up(n.abs() as usize), Ordering::Less => self.select_lines_up(n.unsigned_abs()),
Ordering::Equal => self.this_line(), Ordering::Equal => self.this_line(),
Ordering::Greater => self.select_lines_down(n as usize) Ordering::Greater => self.select_lines_down(n as usize)
}; };
@@ -903,7 +950,7 @@ impl LineBuf {
Verb::Delete => start..end.saturating_add(1), Verb::Delete => start..end.saturating_add(1),
_ => unreachable!() _ => unreachable!()
}; };
deleted = self.buffer.drain(range); self.buffer.drain(range)
} }
MotionKind::ToLine(n) => { MotionKind::ToLine(n) => {
let (start,end) = self.select_lines_to(n); let (start,end) = self.select_lines_to(n);
@@ -912,11 +959,11 @@ impl LineBuf {
Verb::Delete => start..end.saturating_add(1), Verb::Delete => start..end.saturating_add(1),
_ => unreachable!() _ => unreachable!()
}; };
deleted = self.buffer.drain(range); self.buffer.drain(range)
} }
MotionKind::Null => return Ok(()), MotionKind::Null => return Ok(()),
MotionKind::ToScreenPos(n) => todo!(), MotionKind::ToScreenPos(n) => todo!(),
} };
register.write_to_register(deleted.collect()); register.write_to_register(deleted.collect());
self.apply_motion(motion); self.apply_motion(motion);
} }
@@ -1058,6 +1105,7 @@ impl LineBuf {
flog!(DEBUG, cmd); flog!(DEBUG, cmd);
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert()); let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
let is_line_motion = cmd.is_line_motion();
let is_undo_op = cmd.is_undo_op(); let is_undo_op = cmd.is_undo_op();
// Merge character inserts into one edit // Merge character inserts into one edit
@@ -1097,6 +1145,10 @@ impl LineBuf {
self.handle_edit(before, after, cursor_pos); self.handle_edit(before, after, cursor_pos);
} }
if !is_line_motion {
self.saved_col = None;
}
if is_char_insert { if is_char_insert {
self.merge_edit = true; self.merge_edit = true;
} }

View File

@@ -89,12 +89,14 @@ impl FernVi {
self.term.unwrite()?; self.term.unwrite()?;
} }
let offset = self.calculate_prompt_offset(); let offset = self.calculate_prompt_offset();
self.line.set_first_line_offset(offset);
let mut line_buf = self.prompt.clone(); let mut line_buf = self.prompt.clone();
line_buf.push_str(self.line.as_str()); line_buf.push_str(self.line.as_str());
self.term.recorded_write(&line_buf, offset)?; self.term.recorded_write(&line_buf, offset)?;
self.term.position_cursor(self.line.cursor_display_coords(offset,width))?; self.term.position_cursor(self.line.cursor_display_coords(width))?;
self.term.write(&self.mode.cursor_style());
Ok(()) Ok(())
} }
pub fn calculate_prompt_offset(&self) -> usize { pub fn calculate_prompt_offset(&self) -> usize {

View File

@@ -530,6 +530,14 @@ impl ViMode for ViNormal {
flog!(DEBUG, key); flog!(DEBUG, key);
match key { match key {
E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => {
Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(),
})
}
E(K::Char('R'), M::CTRL) => { E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable(); let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1); let count = self.parse_count(&mut chars).unwrap_or(1);

View File

@@ -100,6 +100,9 @@ impl ViCmd {
pub fn is_undo_op(&self) -> bool { pub fn is_undo_op(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
} }
pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown))
}
pub fn is_mode_transition(&self) -> bool { pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| { self.verb.as_ref().is_some_and(|v| {
matches!(v.1, matches!(v.1,