work on implementing screen-wise motions

This commit is contained in:
2025-06-01 02:18:22 -04:00
parent 275d902849
commit 92482da8a7
4 changed files with 181 additions and 34 deletions

View File

@@ -512,13 +512,22 @@ impl LineBuf {
} }
} }
} }
if col == term_width {
lines += 1;
// Don't ask why col has to be set to zero here but one everywhere else
// I don't know either
// All I know is that it only finds the correct cursor position
// if I set col to 0 here, and 1 everywhere else
// Thank you linux terminal :)
col = 0;
}
(lines, col) (lines, col)
} }
pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize { pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize {
let (lines, _) = Self::compute_display_positions( let (lines, _) = Self::compute_display_positions(
self.buffer.graphemes(true), self.buffer.graphemes(true),
offset.max(1), offset,
self.tab_stop, self.tab_stop,
term_width, term_width,
); );
@@ -528,7 +537,7 @@ impl LineBuf {
pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize { pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize {
let (lines, _) = Self::compute_display_positions( let (lines, _) = Self::compute_display_positions(
self.slice_to_cursor().graphemes(true), self.slice_to_cursor().graphemes(true),
offset.max(1), offset,
self.tab_stop, self.tab_stop,
term_width, term_width,
); );
@@ -546,11 +555,16 @@ impl LineBuf {
pub fn cursor_display_coords(&self, 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 total_lines = self.count_display_lines(self.first_line_offset, term_width); let total_lines = self.count_display_lines(0, term_width);
let logical_line = total_lines - d_line; let is_first_line = self.start_of_line() == 0;
let mut logical_line = total_lines - d_line;
if logical_line == self.count_lines() { if is_first_line {
d_col += self.first_line_offset; d_col += self.first_line_offset;
if d_col > term_width {
logical_line = logical_line.saturating_sub(1);
d_col -= term_width;
}
} }
(logical_line, d_col) (logical_line, d_col)
@@ -594,7 +608,6 @@ impl LineBuf {
} }
pub fn accept_hint(&mut self) { pub fn accept_hint(&mut self) {
if let Some(hint) = self.hint.take() { if let Some(hint) = self.hint.take() {
flog!(DEBUG, "accepting hint");
let old_buf = self.buffer.clone(); let old_buf = self.buffer.clone();
self.buffer.push_str(&hint); self.buffer.push_str(&hint);
let new_buf = self.buffer.clone(); let new_buf = self.buffer.clone();
@@ -938,6 +951,68 @@ impl LineBuf {
TextObj::Custom(_) => todo!(), TextObj::Custom(_) => todo!(),
} }
} }
pub fn get_screen_line_positions(&self) -> Vec<usize> {
let (start,end) = self.this_line();
let mut screen_starts = vec![start];
let line = &self.buffer[start..end];
let term_width = self.term_dims.1;
let mut col = 1;
if start == 0 {
col = self.first_line_offset
}
for (byte, grapheme) in line.grapheme_indices(true) {
let width = grapheme.width();
if col + width > term_width {
screen_starts.push(start + byte);
col = width;
} else {
col += width;
}
}
screen_starts
}
pub fn start_of_screen_line(&self) -> usize {
let screen_starts = self.get_screen_line_positions();
let mut screen_start = screen_starts[0];
let start_of_logical_line = self.start_of_line();
flog!(DEBUG,screen_starts);
flog!(DEBUG,self.cursor);
for (i,pos) in screen_starts.iter().enumerate() {
if *pos > self.cursor {
break
} else {
screen_start = screen_starts[i];
}
}
if screen_start != start_of_logical_line {
screen_start += 1; // FIXME: doesn't account for grapheme bounds
}
screen_start
}
pub fn this_screen_line(&self) -> (usize,usize) {
let screen_starts = self.get_screen_line_positions();
let mut screen_start = screen_starts[0];
let mut screen_end = self.end_of_line().saturating_sub(1);
let start_of_logical_line = self.start_of_line();
flog!(DEBUG,screen_starts);
flog!(DEBUG,self.cursor);
for (i,pos) in screen_starts.iter().enumerate() {
if *pos > self.cursor {
screen_end = screen_starts[i].saturating_sub(1);
break;
} else {
screen_start = screen_starts[i];
}
}
if screen_start != start_of_logical_line {
screen_start += 1; // FIXME: doesn't account for grapheme bounds
}
(screen_start,screen_end)
}
pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option<usize> { pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option<usize> {
// FIXME: This uses a lot of hardcoded +1/-1 offsets, but they need to account for grapheme boundaries // FIXME: This uses a lot of hardcoded +1/-1 offsets, but they need to account for grapheme boundaries
let mut pos = self.cursor; let mut pos = self.cursor;
@@ -1328,10 +1403,46 @@ impl LineBuf {
let end = end.clamp(0, self.byte_len().saturating_sub(1)); let end = end.clamp(0, self.byte_len().saturating_sub(1));
MotionKind::range(mk_range(start, end)) MotionKind::range(mk_range(start, end))
} }
Motion::EndOfLastWord => {
let Some(search_start) = self.next_pos(1) else {
return MotionKind::Null
};
let mut last_graph_pos = None;
for (i,graph) in self.buffer[search_start..].grapheme_indices(true) {
flog!(DEBUG, last_graph_pos);
flog!(DEBUG, graph);
if graph == "\n" && last_graph_pos.is_some() {
return MotionKind::On(search_start + last_graph_pos.unwrap())
} else if !is_whitespace(graph) {
last_graph_pos = Some(i)
}
}
flog!(DEBUG,self.byte_len());
last_graph_pos
.map(|pos| MotionKind::On(search_start + pos))
.unwrap_or(MotionKind::Null)
}
Motion::BeginningOfScreenLine => {
let screen_start = self.start_of_screen_line();
MotionKind::On(screen_start)
}
Motion::FirstGraphicalOnScreenLine => {
let (start,end) = self.this_screen_line();
flog!(DEBUG,start,end);
let slice = &self.buffer[start..=end];
for (i,grapheme) in slice.grapheme_indices(true) {
if !is_whitespace(grapheme) {
return MotionKind::On(start + i)
}
}
MotionKind::On(start)
}
Motion::HalfOfScreen => todo!(),
Motion::HalfOfScreenLineText => todo!(),
Motion::Builder(_) => todo!(), Motion::Builder(_) => todo!(),
Motion::RepeatMotion => todo!(), Motion::RepeatMotion => todo!(),
Motion::RepeatMotionRev => todo!(), Motion::RepeatMotionRev => todo!(),
Motion::Null => MotionKind::Null Motion::Null => MotionKind::Null,
} }
} }
pub fn calculate_display_offset(&self, n_lines: isize) -> Option<usize> { pub fn calculate_display_offset(&self, n_lines: isize) -> Option<usize> {
@@ -1702,7 +1813,6 @@ impl LineBuf {
}; };
let line = &self.buffer[start..end]; let line = &self.buffer[start..end];
let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace
flog!(DEBUG,next_line);
let replace_newline_with_space = !line.ends_with([' ', '\t']); let replace_newline_with_space = !line.ends_with([' ', '\t']);
self.cursor = end; self.cursor = end;
if replace_newline_with_space { if replace_newline_with_space {
@@ -1881,14 +1991,10 @@ impl LineBuf {
SelectionMode::Block(anchor) => todo!(), SelectionMode::Block(anchor) => todo!(),
} }
if start >= end { if start >= end {
flog!(DEBUG, "inverting anchor");
mode.invert_anchor(); mode.invert_anchor();
flog!(DEBUG,start,end);
std::mem::swap(&mut start, &mut end); std::mem::swap(&mut start, &mut end);
self.select_mode = Some(mode); self.select_mode = Some(mode);
flog!(DEBUG,start,end);
flog!(DEBUG,mode);
} }
self.selected_range = Some(start..end); self.selected_range = Some(start..end);
} }
@@ -1913,7 +2019,6 @@ impl LineBuf {
self.undo_stack.push(edit); self.undo_stack.push(edit);
} else { } else {
let diff = Edit::diff(&old, &new, curs_pos); let diff = Edit::diff(&old, &new, curs_pos);
flog!(DEBUG, diff);
if !diff.is_empty() { if !diff.is_empty() {
self.undo_stack.push(diff); self.undo_stack.push(diff);
} }
@@ -1952,7 +2057,6 @@ impl LineBuf {
.unwrap_or(MotionKind::Null) .unwrap_or(MotionKind::Null)
}); });
flog!(DEBUG,self.hint);
if let Some(verb) = verb.clone() { if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion_eval, register)?; self.exec_verb(verb.1, motion_eval, register)?;
} else if self.has_hint() { } else if self.has_hint() {
@@ -1960,7 +2064,6 @@ impl LineBuf {
.clone() .clone()
.map(|m| self.eval_motion_with_hint(m.1)) .map(|m| self.eval_motion_with_hint(m.1))
.unwrap_or(MotionKind::Null); .unwrap_or(MotionKind::Null);
flog!(DEBUG, "applying motion with hint");
self.apply_motion_with_hint(motion_eval); self.apply_motion_with_hint(motion_eval);
} else { } else {
self.apply_motion(/*forced*/ false,motion_eval); self.apply_motion(/*forced*/ false,motion_eval);
@@ -1987,8 +2090,6 @@ impl LineBuf {
} }
} }
flog!(DEBUG, self.select_mode);
flog!(DEBUG, self.selected_range);
if self.clamp_cursor { if self.clamp_cursor {
self.clamp_cursor(); self.clamp_cursor();

View File

@@ -639,39 +639,55 @@ impl ViNormal {
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
} }
'_' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
}
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
}
_ => return self.quit_parse() _ => return self.quit_parse()
} }
} else { } else {
break 'motion_parse None break 'motion_parse None
} }
} }
'G' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
}
'f' => { 'f' => {
let Some(ch) = chars_clone.peek() else { let Some(ch) = chars_clone.peek() else {
break 'motion_parse None break 'motion_parse None
}; };
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into()))) break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch)))
} }
'F' => { 'F' => {
let Some(ch) = chars_clone.peek() else { let Some(ch) = chars_clone.peek() else {
break 'motion_parse None break 'motion_parse None
}; };
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into()))) break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch)))
} }
't' => { 't' => {
let Some(ch) = chars_clone.peek() else { let Some(ch) = chars_clone.peek() else {
break 'motion_parse None break 'motion_parse None
}; };
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into()))) break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch)))
} }
'T' => { 'T' => {
let Some(ch) = chars_clone.peek() else { let Some(ch) = chars_clone.peek() else {
break 'motion_parse None break 'motion_parse None
}; };
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into()))) break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch)))
} }
';' => { ';' => {
chars = chars_clone; chars = chars_clone;
@@ -685,6 +701,10 @@ impl ViNormal {
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
} }
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord));
}
'0' => { '0' => {
chars = chars_clone; chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));

View File

@@ -187,25 +187,40 @@ impl Terminal {
self.unposition_cursor()?; self.unposition_cursor()?;
let WriteMap { lines, cols, offset } = self.write_records; let WriteMap { lines, cols, offset } = self.write_records;
for _ in 0..lines { for _ in 0..lines {
self.write("\x1b[2K\x1b[A") self.write_unrecorded("\x1b[2K\x1b[A")
} }
let col = offset; let col = offset;
self.write(&format!("\x1b[{col}G\x1b[0K")); self.write_unrecorded(&format!("\x1b[{col}G\x1b[0K"));
self.reset_records(); self.reset_records();
Ok(()) Ok(())
} }
pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> { pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> {
flog!(DEBUG,lines); flog!(DEBUG,lines);
flog!(DEBUG,col);
self.cursor_records.lines = lines; self.cursor_records.lines = lines;
self.cursor_records.cols = col; self.cursor_records.cols = col;
self.cursor_records.offset = self.cursor_pos().1; self.cursor_records.offset = self.cursor_pos().1;
for _ in 0..lines { for _ in 0..lines {
self.write("\x1b[A") self.write_unrecorded("\x1b[A")
} }
self.write(&format!("\x1b[{col}G")); let (_, width) = self.get_dimensions().unwrap();
// holy hack spongebob
// basically if we've written to the edge of the terminal
// and the cursor is at term_width + 1 (column 1 on the next line)
// then we are going to manually write a newline
// to position the cursor correctly
if self.write_records.cols == width && self.cursor_records.cols == 1 {
self.cursor_records.lines += 1;
self.write_records.lines += 1;
self.cursor_records.cols = 1;
self.write_records.cols = 1;
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, b"\n").expect("Failed to write to stdout");
}
self.write_unrecorded(&format!("\x1b[{col}G"));
Ok(()) Ok(())
} }
@@ -215,16 +230,16 @@ impl Terminal {
let WriteMap { lines, cols, offset } = self.cursor_records; let WriteMap { lines, cols, offset } = self.cursor_records;
for _ in 0..lines { for _ in 0..lines {
self.write("\x1b[B") self.write_unrecorded("\x1b[B")
} }
self.write(&format!("\x1b[{offset}G")); self.write_unrecorded(&format!("\x1b[{offset}G"));
Ok(()) Ok(())
} }
pub fn write_bytes(&mut self, buf: &[u8]) { pub fn write_bytes(&mut self, buf: &[u8], record: bool) {
if self.recording { if self.recording && record { // The function parameter allows us to make sneaky writes while the terminal is recording
let (_, width) = self.get_dimensions().unwrap(); let (_, width) = self.get_dimensions().unwrap();
let mut bytes = buf.iter().map(|&b| b as char).peekable(); let mut bytes = buf.iter().map(|&b| b as char).peekable();
while let Some(ch) = bytes.next() { while let Some(ch) = bytes.next() {
@@ -263,29 +278,35 @@ impl Terminal {
_ => { _ => {
let ch_width = ch.width().unwrap_or(0); let ch_width = ch.width().unwrap_or(0);
if self.write_records.cols + ch_width > width { if self.write_records.cols + ch_width > width {
flog!(DEBUG,ch_width,self.write_records.cols,width,self.write_records.lines);
self.write_records.lines += 1; self.write_records.lines += 1;
self.write_records.cols = 0; self.write_records.cols = ch_width;
} }
self.write_records.cols += ch_width; self.write_records.cols += ch_width;
} }
} }
} }
flog!(DEBUG,self.write_records.cols);
} }
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout"); write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout");
} }
pub fn write(&mut self, s: &str) { pub fn write(&mut self, s: &str) {
self.write_bytes(s.as_bytes()); self.write_bytes(s.as_bytes(), true);
}
pub fn write_unrecorded(&mut self, s: &str) {
self.write_bytes(s.as_bytes(), false);
} }
pub fn writeln(&mut self, s: &str) { pub fn writeln(&mut self, s: &str) {
self.write(s); self.write(s);
self.write_bytes(b"\n"); self.write_bytes(b"\n", true);
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.write_bytes(b"\x1b[2J\x1b[H"); self.write_bytes(b"\x1b[2J\x1b[H", false);
} }
pub fn read_key(&self) -> KeyEvent { pub fn read_key(&self) -> KeyEvent {
@@ -412,7 +433,7 @@ impl Terminal {
} }
pub fn cursor_pos(&mut self) -> (usize, usize) { pub fn cursor_pos(&mut self) -> (usize, usize) {
self.write("\x1b[6n"); self.write_unrecorded("\x1b[6n");
let mut buf = [0u8;32]; let mut buf = [0u8;32];
let n = self.read_byte(&mut buf); let n = self.read_byte(&mut buf);

View File

@@ -247,6 +247,7 @@ impl Verb {
pub enum Motion { pub enum Motion {
WholeLine, WholeLine,
TextObj(TextObj, Bound), TextObj(TextObj, Bound),
EndOfLastWord,
BeginningOfFirstWord, BeginningOfFirstWord,
BeginningOfLine, BeginningOfLine,
EndOfLine, EndOfLine,
@@ -259,6 +260,10 @@ pub enum Motion {
ScreenLineUp, ScreenLineUp,
LineDown, LineDown,
ScreenLineDown, ScreenLineDown,
BeginningOfScreenLine,
FirstGraphicalOnScreenLine,
HalfOfScreen,
HalfOfScreenLineText,
WholeBuffer, WholeBuffer,
BeginningOfBuffer, BeginningOfBuffer,
EndOfBuffer, EndOfBuffer,