From 30e25bf3413b9bf786bdea434182efa7562349fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Tue, 22 Oct 2024 14:35:17 +0200 Subject: [PATCH] layout spans and move_to_line --- src/edit.rs | 18 +++--- src/layout.rs | 128 +++++++++++++++++++++++++++++++++++- src/line_buffer.rs | 157 +++++++++++++++++++++++++++------------------ src/test/mod.rs | 5 +- src/tty/mod.rs | 35 +--------- src/tty/unix.rs | 5 +- 6 files changed, 237 insertions(+), 111 deletions(-) diff --git a/src/edit.rs b/src/edit.rs index b18de9e2e..74d6576ec 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -122,9 +122,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn move_cursor(&mut self, kind: CmdKind) -> Result<()> { // calculate the desired position of the cursor + + let (_, span) = self.layout.find_span_by_offset(self.line.pos()); let cursor = self .out - .calculate_position(&self.line[..self.line.pos()], self.prompt_size); + .calculate_position(&self.line[span.offset..self.line.pos()], span.pos); + if self.layout.cursor == cursor { return Ok(()); } @@ -133,9 +136,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; } else { self.out.move_cursor(self.layout.cursor, cursor)?; - self.layout.prompt_size = self.prompt_size; self.layout.cursor = cursor; - debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); } Ok(()) @@ -172,9 +173,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { None }; - let new_layout = self - .out - .compute_layout(prompt_size, default_prompt, &self.line, info); + let new_layout = Layout::compute(self.out, prompt_size, default_prompt, &self.line, info); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); @@ -363,7 +362,6 @@ impl State<'_, '_, H> { // Avoid a full update of the line in the trivial case. self.layout.cursor.col += width; self.layout.end.col += width; - debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); let bits = ch.encode_utf8(&mut self.byte_buffer); self.out.write_and_flush(bits) @@ -578,7 +576,7 @@ impl State<'_, '_, H> { /// Moves the cursor to the same column in the line above pub fn edit_move_line_up(&mut self, n: RepeatCount) -> Result { - if self.line.move_to_line_up(n) { + if self.line.move_to_line(-(n as isize), &self.layout) { self.move_cursor(CmdKind::MoveCursor)?; Ok(true) } else { @@ -586,9 +584,9 @@ impl State<'_, '_, H> { } } - /// Moves the cursor to the same column in the line above + /// Moves the cursor to the same column in the line below pub fn edit_move_line_down(&mut self, n: RepeatCount) -> Result { - if self.line.move_to_line_down(n) { + if self.line.move_to_line(n as isize, &self.layout) { self.move_cursor(CmdKind::MoveCursor)?; Ok(true) } else { diff --git a/src/layout.rs b/src/layout.rs index 5aa8834fa..a860f9625 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -21,13 +21,135 @@ impl Ord for Position { } } +/// Layout of a substring of the input buffer +#[derive(Debug)] +pub struct SpanLayout { + /// Offset of the start of the substring in the input buffer + pub offset: usize, + + /// Position on of the start of the span + pub pos: Position, +} + +/// All positions are relative to start of the prompt == origin (col: 0, row: 0) #[derive(Debug, Default)] pub struct Layout { - /// Prompt Unicode/visible width and height - pub prompt_size: Position, pub default_prompt: bool, + /// Cursor position (relative to the start of the prompt) + /// - cursor.row >= spans[0].pos.row + /// - if cursor.row > spans[0].pos.row then cursor.col >= spans[0].pos.col pub cursor: Position, - /// Number of rows used so far (from start of prompt to end of input) + + /// Number of rows used so far (from start of prompt to end + /// of input or hint) + /// - cursor <= end pub end: Position, + + /// Layout of the input buffer, broken into spans. + /// - non-empty, + /// - first element has offset 0, + pub spans: Vec, +} + +impl Layout { + pub fn find_span_by_row(&self, row: usize) -> Option<(usize, &SpanLayout)> { + match self.spans.binary_search_by_key(&row, |span| span.pos.row) { + Ok(i) => Some((i, &self.spans[i])), + Err(_) => None, + } + } + + /// Find the span of an offset in input buffer. + pub fn find_span_by_offset(&self, offset: usize) -> (usize, &SpanLayout) { + match self.spans.binary_search_by_key(&offset, |span| span.offset) { + Ok(i) => (i, &self.spans[i]), + Err(mut i) => { + if i == 0 { + unreachable!("first span must have offset 0") + } + i -= 1; + (i, &self.spans[i]) + } + } + } + + /// Compute layout for rendering prompt + line + some info (either hint, + /// validation msg, ...). on the screen. Depending on screen width, line + /// wrapping may be applied. + pub fn compute( + renderer: &impl crate::tty::Renderer, + prompt_size: Position, + default_prompt: bool, + line: &crate::line_buffer::LineBuffer, + info: Option<&str>, + ) -> Layout { + let mut spans = Vec::with_capacity(line.len()); + + let buf = line.as_str(); + let cursor_offset = line.pos(); + + let mut cursor = None; + let mut curr_position = prompt_size; + + // iterate over input buffer lines + let mut line_start_offset = 0; + let end = loop { + spans.push(SpanLayout { + offset: line_start_offset, + pos: curr_position, + }); + + // find the end of input line + let line_end_offset = buf[line_start_offset..] + .find('\n') + .map_or(buf.len(), |x| x + line_start_offset); + + // find cursor position + if line_start_offset <= cursor_offset { + cursor = Some( + renderer + .calculate_position(&line[line_start_offset..cursor_offset], curr_position), + ); + } + + // find end of line position + let line_end = if cursor_offset == line_end_offset { + // optimization + cursor.unwrap() + } else { + renderer + .calculate_position(&line[line_start_offset..line_end_offset], curr_position) + }; + + if line_end_offset == buf.len() { + break line_end; + } else { + curr_position = Position { + row: line_end.row + 1, + col: 0, + }; + line_start_offset = line_end_offset + 1; + } + }; + let cursor = cursor.unwrap_or(end); + + // layout info after the input + let end = if let Some(info) = info { + renderer.calculate_position(info, end) + } else { + end + }; + + let new_layout = Layout { + default_prompt, + cursor, + end, + spans, + }; + debug_assert!(!new_layout.spans.is_empty()); + debug_assert!(new_layout.spans[0].offset == 0); + debug_assert!(new_layout.cursor <= new_layout.end); + new_layout + } } diff --git a/src/line_buffer.rs b/src/line_buffer.rs index 3f2dda9a5..43897a02a 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -1,5 +1,6 @@ //! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; +use crate::layout::Layout; use std::cmp::min; use std::fmt; use std::iter; @@ -581,35 +582,10 @@ impl LineBuffer { } } - /// Moves the cursor to the same column in the line above - pub fn move_to_line_up(&mut self, n: RepeatCount) -> bool { - match self.buf[..self.pos].rfind('\n') { - Some(off) => { - let column = self.buf[off + 1..self.pos].graphemes(true).count(); - - let mut dest_start = self.buf[..off].rfind('\n').map_or(0, |n| n + 1); - let mut dest_end = off; - for _ in 1..n { - if dest_start == 0 { - break; - } - dest_end = dest_start - 1; - dest_start = self.buf[..dest_end].rfind('\n').map_or(0, |n| n + 1); - } - let gidx = self.buf[dest_start..dest_end] - .grapheme_indices(true) - .nth(column); - - self.pos = gidx.map_or(off, |(idx, _)| dest_start + idx); // if there's no enough columns - true - } - None => false, - } - } - /// N lines up starting from the current one /// /// Fails if the cursor is on the first line + // FIXME fn n_lines_up(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut start = if let Some(off) = self.buf[..self.pos].rfind('\n') { off + 1 @@ -633,6 +609,7 @@ impl LineBuffer { /// N lines down starting from the current one /// /// Fails if the cursor is on the last line + // FIXME fn n_lines_down(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut end = if let Some(off) = self.buf[self.pos..].find('\n') { self.pos + off + 1 @@ -651,34 +628,42 @@ impl LineBuffer { Some((start, end)) } - /// Moves the cursor to the same column in the line above - pub fn move_to_line_down(&mut self, n: RepeatCount) -> bool { - match self.buf[self.pos..].find('\n') { - Some(off) => { - let line_start = self.buf[..self.pos].rfind('\n').map_or(0, |n| n + 1); - let column = self.buf[line_start..self.pos].graphemes(true).count(); - let mut dest_start = self.pos + off + 1; - let mut dest_end = self.buf[dest_start..] - .find('\n') - .map_or_else(|| self.buf.len(), |v| dest_start + v); - for _ in 1..n { - if dest_end == self.buf.len() { - break; - } - dest_start = dest_end + 1; - dest_end = self.buf[dest_start..] - .find('\n') - .map_or_else(|| self.buf.len(), |v| dest_start + v); - } - self.pos = self.buf[dest_start..dest_end] - .grapheme_indices(true) - .nth(column) - .map_or(dest_end, |(idx, _)| dest_start + idx); // if there's no enough columns - debug_assert!(self.pos <= self.buf.len()); - true - } - None => false, + /// Moves the cursor to line `current_line + n` the buffer, into the same column. + /// Positive n mean down, negative mean up. + /// Returns false when no move was performed, true otherwise. + pub fn move_to_line(&mut self, n: isize, layout: &Layout) -> bool { + let current_pos = layout.cursor; + + if n == 0 || current_pos.row == 0 && n < 0 { + return false; } + + let target_row = (current_pos.row as isize).saturating_add(n).max(0) as usize; + let (target_span, target_span_end) = + if let Some((i, target_span)) = layout.find_span_by_row(target_row) { + let target_span_end = layout + .spans + .get(i + 1) + .map(|s| s.offset - 1) + .unwrap_or_else(|| self.buf.len()); + + (target_span, target_span_end) + } else { + (layout.spans.last().unwrap(), self.buf.len()) + }; + + let target_offset = self.buf[target_span.offset..target_span_end] + .grapheme_indices(true) + .nth(current_pos.col.saturating_sub(target_span.pos.col)) + .map(|(offset, _)| target_span.offset + offset) + .unwrap_or(target_span_end); + + if self.pos == target_offset { + return false; + } + + self.pos = target_offset; + true } fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { @@ -1183,6 +1168,8 @@ mod test { ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE, }; use crate::keymap::{At, CharSearch, Word}; + use crate::layout::{Layout, Position}; + use crate::tty::Sink; struct Listener { deleted_str: Option, @@ -1817,42 +1804,90 @@ mod test { #[test] fn move_by_line() { + fn move_to_line(s: &mut LineBuffer, prompt_size: Position, n: isize) -> bool { + let renderer = Sink {}; + let layout = Layout::compute(&renderer, prompt_size, false, s, None); + s.move_to_line(n, &layout) + } + + let prompt_size = Position { row: 0, col: 0 }; let text = "aa123\nsdf bc\nasdf"; let mut s = LineBuffer::init(text, 14); + // move up - let ok = s.move_to_line_up(1); + let ok = move_to_line(&mut s, prompt_size, -1); assert_eq!(7, s.pos); assert!(ok); - let ok = s.move_to_line_up(1); + let ok = move_to_line(&mut s, prompt_size, -1); assert_eq!(1, s.pos); assert!(ok); - let ok = s.move_to_line_up(1); + let ok = move_to_line(&mut s, prompt_size, -1); assert_eq!(1, s.pos); assert!(!ok); // move down - let ok = s.move_to_line_down(1); + let ok = move_to_line(&mut s, prompt_size, 1); assert_eq!(7, s.pos); assert!(ok); - let ok = s.move_to_line_down(1); + let ok = move_to_line(&mut s, prompt_size, 1); assert_eq!(14, s.pos); assert!(ok); - let ok = s.move_to_line_down(1); + let ok = move_to_line(&mut s, prompt_size, 1); assert_eq!(14, s.pos); assert!(!ok); // move by multiple steps - let ok = s.move_to_line_up(2); + let ok = move_to_line(&mut s, prompt_size, -2); assert_eq!(1, s.pos); assert!(ok); - let ok = s.move_to_line_down(2); + let ok = move_to_line(&mut s, prompt_size, 2); assert_eq!(14, s.pos); assert!(ok); + + // non-empty prompt + let prompt_size = Position { row: 0, col: 3 }; + let text = "aa123\nsdf bc\nasdf"; + let mut s = LineBuffer::init(text, 14); + + // move up + let ok = move_to_line(&mut s, prompt_size, -1); + assert_eq!(7, s.pos); + assert!(ok); + + let ok = move_to_line(&mut s, prompt_size, -1); + assert_eq!(0, s.pos); + assert!(ok); + + let ok = move_to_line(&mut s, prompt_size, -1); + assert_eq!(0, s.pos); + assert!(!ok); + + // move down + let ok = move_to_line(&mut s, prompt_size, 1); + assert_eq!(9, s.pos); + assert!(ok); + + let ok = move_to_line(&mut s, prompt_size, 1); + assert_eq!(16, s.pos); + assert!(ok); + + let ok = move_to_line(&mut s, prompt_size, 1); + assert_eq!(16, s.pos); + assert!(!ok); + + // move by multiple steps + let ok = move_to_line(&mut s, prompt_size, -2); + assert_eq!(0, s.pos); + assert!(ok); + + let ok = move_to_line(&mut s, prompt_size, 2); + assert_eq!(16, s.pos); + assert!(ok); } #[test] diff --git a/src/test/mod.rs b/src/test/mod.rs index 14ff9052b..4c25e7d98 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -129,7 +129,10 @@ fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyEvent], expec let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial("", initial).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); - assert_eq!(expected.0.len(), editor.term.cursor); + assert_eq!( + expected.0.len() - expected.0.rfind('\n').map_or(0, |x| x + 1), + editor.term.cursor + ); } // `entries`: history entries before `keys` pressed diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 22dcb8a2b..be5922f11 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -56,42 +56,9 @@ pub trait Renderer { highlighter: Option<&dyn Highlighter>, ) -> Result<()>; - /// Compute layout for rendering prompt + line + some info (either hint, - /// validation msg, ...). on the screen. Depending on screen width, line - /// wrapping may be applied. - fn compute_layout( - &self, - prompt_size: Position, - default_prompt: bool, - line: &LineBuffer, - info: Option<&str>, - ) -> Layout { - // calculate the desired position of the cursor - let pos = line.pos(); - let cursor = self.calculate_position(&line[..pos], prompt_size); - // calculate the position of the end of the input line - let mut end = if pos == line.len() { - cursor - } else { - self.calculate_position(&line[pos..], cursor) - }; - if let Some(info) = info { - end = self.calculate_position(info, end); - } - - let new_layout = Layout { - prompt_size, - default_prompt, - cursor, - end, - }; - debug_assert!(new_layout.prompt_size <= new_layout.cursor); - debug_assert!(new_layout.cursor <= new_layout.end); - new_layout - } - /// Calculate the number of columns and rows used to display `s` on a /// `cols` width terminal starting at `orig`. + // FIXME fn calculate_position(&self, s: &str, orig: Position) -> Position; fn write_and_flush(&mut self, buf: &str) -> Result<()>; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 054b4f118..bb56937a2 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -1642,6 +1642,7 @@ mod termios_ { mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::BellStyle; + use crate::layout::Layout; use crate::line_buffer::{LineBuffer, NoListener}; #[test] @@ -1682,7 +1683,7 @@ mod test { let prompt_size = out.calculate_position(prompt, Position::default()); let mut line = LineBuffer::init("", 0); - let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + let old_layout = Layout::compute(&out, prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor); assert_eq!(old_layout.cursor, old_layout.end); @@ -1690,7 +1691,7 @@ mod test { Some(true), line.insert('a', out.cols - prompt_size.col + 1, &mut NoListener) ); - let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + let new_layout = Layout::compute(&out, prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); assert_eq!(new_layout.cursor, new_layout.end); out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None)