Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep track of layout of input buffer spans #820

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}
Expand All @@ -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(())
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -363,7 +362,6 @@ impl<H: Helper> 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)
Expand Down Expand Up @@ -578,17 +576,17 @@ impl<H: Helper> State<'_, '_, H> {

/// Moves the cursor to the same column in the line above
pub fn edit_move_line_up(&mut self, n: RepeatCount) -> Result<bool> {
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 {
Ok(false)
}
}

/// 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<bool> {
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 {
Expand Down
128 changes: 125 additions & 3 deletions src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpanLayout>,
}

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
}
}
Loading