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

Vertical scroll #622

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
10 changes: 5 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct Config {
/// Whether to use stdio or not
behavior: Behavior,
/// Horizontal space taken by a tab.
tab_stop: usize,
tab_stop: u16,
/// Indentation size for indent/dedent commands
indent_size: usize,
/// Check if cursor position is at leftmost before displaying prompt
Expand Down Expand Up @@ -158,11 +158,11 @@ impl Config {
///
/// By default, 8.
#[must_use]
pub fn tab_stop(&self) -> usize {
pub fn tab_stop(&self) -> u16 {
self.tab_stop
}

pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) {
pub(crate) fn set_tab_stop(&mut self, tab_stop: u16) {
self.tab_stop = tab_stop;
}

Expand Down Expand Up @@ -418,7 +418,7 @@ impl Builder {
///
/// By default, `8`
#[must_use]
pub fn tab_stop(mut self, tab_stop: usize) -> Self {
pub fn tab_stop(mut self, tab_stop: u16) -> Self {
self.set_tab_stop(tab_stop);
self
}
Expand Down Expand Up @@ -544,7 +544,7 @@ pub trait Configurer {
/// Horizontal space taken by a tab.
///
/// By default, `8`
fn set_tab_stop(&mut self, tab_stop: usize) {
fn set_tab_stop(&mut self, tab_stop: u16) {
self.config_mut().set_tab_stop(tab_stop);
}

Expand Down
34 changes: 18 additions & 16 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
use log::debug;
use std::fmt;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;

use super::{Context, Helper, Result};
use crate::error::ReadlineError;
Expand All @@ -12,7 +11,7 @@ use crate::hint::Hint;
use crate::history::SearchDirection;
use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Invoke, Refresher};
use crate::layout::{Layout, Position};
use crate::layout::{self, Layout, Position};
use crate::line_buffer::{
ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE,
};
Expand Down Expand Up @@ -51,7 +50,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
helper: Option<&'out H>,
ctx: Context<'out>,
) -> State<'out, 'prompt, H> {
let prompt_size = out.calculate_position(prompt, Position::default());
let prompt_size = out.calculate_position(prompt, Position::default(), None);
State {
out,
prompt,
Expand Down Expand Up @@ -93,9 +92,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
if new_cols != old_cols
&& (self.layout.end.row > 0 || self.layout.end.col >= new_cols)
{
self.prompt_size = self
.out
.calculate_position(self.prompt, Position::default());
self.prompt_size =
self.out
.calculate_position(self.prompt, Position::default(), None);
self.refresh_line()?;
}
continue;
Expand All @@ -122,13 +121,13 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {

pub fn move_cursor(&mut self) -> Result<()> {
// calculate the desired position of the cursor
let cursor = self
.out
.calculate_position(&self.line[..self.line.pos()], self.prompt_size);
let cursor =
self.out
.calculate_position(&self.line[..self.line.pos()], self.prompt_size, None);
if self.layout.cursor == cursor {
return Ok(());
}
if self.highlight_char() {
if self.highlight_char() || self.layout.scroll(cursor) {
let prompt_size = self.prompt_size;
self.refresh(self.prompt, prompt_size, true, Info::NoHint)?;
} else {
Expand All @@ -144,6 +143,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
pub fn move_cursor_to_end(&mut self) -> Result<()> {
if self.layout.cursor == self.layout.end {
return Ok(());
} else if self.layout.scroll(self.layout.end) {
let prompt_size = self.prompt_size;
return self.refresh(self.prompt, prompt_size, true, Info::NoHint);
}
self.out.move_cursor(self.layout.cursor, self.layout.end)?;
self.layout.cursor = self.layout.end;
Expand Down Expand Up @@ -275,7 +277,9 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {
}

fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
let prompt_size = self.out.calculate_position(prompt, Position::default());
let prompt_size = self
.out
.calculate_position(prompt, Position::default(), None);
self.hint();
self.highlight_char();
self.refresh(prompt, prompt_size, false, Info::Hint)
Expand Down Expand Up @@ -315,8 +319,7 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {

fn external_print(&mut self, msg: String) -> Result<()> {
self.out.clear_rows(&self.layout)?;
self.layout.end.row = 0;
self.layout.cursor.row = 0;
self.layout.reset_rows();
self.out.write_and_flush(msg.as_str())?;
if !msg.ends_with('\n') {
self.out.write_and_flush("\n")?;
Expand All @@ -341,8 +344,7 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> {
impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
pub fn clear_screen(&mut self) -> Result<()> {
self.out.clear_screen()?;
self.layout.cursor = Position::default();
self.layout.end = Position::default();
self.layout.reset();
Ok(())
}

Expand All @@ -353,7 +355,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
let prompt_size = self.prompt_size;
let no_previous_hint = self.hint.is_none();
self.hint();
let width = ch.width().unwrap_or(0);
let width = layout::cwidth(ch);
if n == 1
&& width != 0 // Ctrl-V + \t or \n ...
&& self.layout.cursor.col + width < self.out.get_columns()
Expand Down
97 changes: 94 additions & 3 deletions src/layout.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
use std::cmp::{Ord, Ordering, PartialOrd};
use std::convert::TryFrom;
use std::ops::Index;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

#[inline]
pub fn try_from(w: usize) -> u16 {
u16::try_from(w).unwrap()
}
#[inline]
pub fn width(s: &str) -> u16 {
u16::try_from(s.width()).unwrap()
}
#[inline]
pub fn cwidth(ch: char) -> u16 {
ch.width().map(|w| w as u16).unwrap_or(0)
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Position {
pub col: usize, // The leftmost column is number 0.
pub row: usize, // The highest row is number 0.
pub col: u16, // The leftmost column is number 0.
pub row: u16, // The highest row is number 0.
}

impl PartialOrd for Position {
Expand All @@ -28,6 +44,81 @@ pub struct Layout {
pub default_prompt: bool,
/// Cursor position (relative to the start of the prompt)
pub cursor: Position,
/// Number of rows used so far (from start of prompt to end of input)
pub end_input: Position,
/// Number of rows used so far (from start of prompt to end of input + some
/// info)
pub end: Position,
/// First visible row (such as cursor is visible if prompt + line + some
/// info > screen height)
pub first_row: u16, // relative to the start of the prompt (= 0 when all rows are visible)
/// Last visible row (such as cursor is visible if prompt + line + some info
/// > screen height)
pub last_row: u16, // relative to the start of the prompt (= end.row when all rows are visible)
/// start of ith row => byte offset of prompt / line / info
pub breaks: Vec<usize>,
}

impl Index<u16> for Layout {
type Output = usize;

fn index(&self, index: u16) -> &usize {
self.breaks.index(index as usize)
}
}

impl Layout {
/// Return `true` if we need to scroll to make `cursor` visible
pub fn scroll(&self, cursor: Position) -> bool {
self.first_row > cursor.row || self.last_row < cursor.row
}

pub fn visible_prompt<'p>(&self, prompt: &'p str) -> &'p str {
if self.first_row > self.prompt_size.row {
return ""; // prompt not visible
} else if self.first_row == 0 {
return prompt;
}
&prompt[self[self.first_row]..]
}

pub fn visible_line<'l>(&self, line: &'l str, pos: usize) -> (&'l str, usize) {
if self.first_row <= self.prompt_size.row {
if self.end_input.row <= self.last_row {
return (line, pos);
}
} else if self.end_input.row <= self.last_row {
let offset = self[self.first_row];
return (&line[offset..], pos.saturating_sub(offset));
}
let start = self[self.first_row];
let end = self[self.last_row];
(&line[start..end], pos.saturating_sub(start))
}

pub fn visible_hint<'h>(&self, hint: &'h str) -> &'h str {
if self.end.row == self.last_row {
return hint;
} else if self.last_row < self.end_input.row {
return ""; // hint not visible
}
let end = self[self.last_row];
&hint[..end]
}

/// Number of visible rows under cursor
pub fn lines_below_cursor(&self) -> u16 {
self.last_row.saturating_sub(self.cursor.row)
}

pub fn reset_rows(&mut self) {
self.last_row = 0;
self.cursor.row = 0;
}

pub fn reset(&mut self) {
self.cursor = Position::default();
self.end = Position::default();
self.first_row = 0;
self.last_row = 0;
}
}
19 changes: 9 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ use log::debug;
#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use rustyline_derive::{Completer, Helper, Highlighter, Hinter, Validator};
use unicode_width::UnicodeWidthStr;

use crate::tty::{RawMode, RawReader, Renderer, Term, Terminal};

Expand Down Expand Up @@ -293,15 +292,16 @@ fn page_completions<C: Candidate, H: Helper>(
cols,
candidates
.iter()
.map(|s| s.display().width())
.map(|s| layout::width(s.display()))
.max()
.unwrap()
+ min_col_pad,
);
let num_cols = cols / max_width;
let nbc: u16 = layout::try_from(candidates.len());

let mut pause_row = s.out.get_rows() - 1;
let num_rows = (candidates.len() + num_cols - 1) / num_cols;
let num_rows = (nbc + num_cols - 1) / num_cols;
let mut ab = String::new();
for row in 0..num_rows {
if row == pause_row {
Expand Down Expand Up @@ -335,15 +335,15 @@ fn page_completions<C: Candidate, H: Helper>(
ab.clear();
for col in 0..num_cols {
let i = (col * num_rows) + row;
if i < candidates.len() {
let candidate = &candidates[i].display();
let width = candidate.width();
if i < nbc {
let candidate = &candidates[i as usize].display();
let width = layout::width(candidate);
if let Some(highlighter) = s.highlighter() {
ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List));
} else {
ab.push_str(candidate);
}
if ((col + 1) * num_rows) + row < candidates.len() {
if ((col + 1) * num_rows) + row < nbc {
for _ in width..max_width {
ab.push(' ');
}
Expand All @@ -353,8 +353,7 @@ fn page_completions<C: Candidate, H: Helper>(
s.out.write_and_flush(ab.as_str())?;
}
s.out.write_and_flush("\n")?;
s.layout.end.row = 0; // dirty way to make clear_old_rows do nothing
s.layout.cursor.row = 0;
s.layout.reset_rows(); // dirty way to make clear_old_rows do nothing
s.refresh_line()?;
Ok(None)
}
Expand Down Expand Up @@ -892,7 +891,7 @@ impl<H: Helper, I: History> Editor<H, I> {

/// If output stream is a tty, this function returns its width and height as
/// a number of characters.
pub fn dimensions(&mut self) -> Option<(usize, usize)> {
pub fn dimensions(&mut self) -> Option<(u16, u16)> {
if self.term.is_output_tty() {
let out = self.term.create_writer();
Some((out.get_columns(), out.get_rows()))
Expand Down
5 changes: 3 additions & 2 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::hint::Hinter;
use crate::history::History;
use crate::keymap::{Bindings, Cmd, InputState};
use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M};
use crate::layout;
use crate::tty::Sink;
use crate::validate::Validator;
use crate::{apply_backspace_direct, readline_direct, Context, DefaultEditor, Helper, Result};
Expand Down Expand Up @@ -101,7 +102,7 @@ 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!(layout::try_from(expected.0.len()), editor.term.cursor);
}

// `entries`: history entries before `keys` pressed
Expand All @@ -121,7 +122,7 @@ fn assert_history(
let actual_line = editor.readline(prompt).unwrap();
assert_eq!(expected.0.to_owned() + expected.1, actual_line);
if prompt.is_empty() {
assert_eq!(expected.0.len(), editor.term.cursor);
assert_eq!(layout::try_from(expected.0.len()), editor.term.cursor);
}
}

Expand Down
Loading