From 56af3b1830687ffe31e3233597edb0913113c7a0 Mon Sep 17 00:00:00 2001 From: gwenn Date: Tue, 12 Apr 2022 18:57:48 +0200 Subject: [PATCH 1/5] Limit screen dimensions to u16 --- src/config.rs | 10 +++++----- src/edit.rs | 5 ++--- src/layout.rs | 19 +++++++++++++++++-- src/lib.rs | 16 ++++++++-------- src/test/mod.rs | 5 +++-- src/tty/mod.rs | 18 ++++++++---------- src/tty/test.rs | 12 ++++++------ src/tty/unix.rs | 31 +++++++++++++++---------------- src/tty/windows.rs | 14 +++++++------- 9 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/config.rs b/src/config.rs index f7f84715db..e59468bdb4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,7 +27,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 @@ -157,11 +157,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; } @@ -424,7 +424,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 } @@ -548,7 +548,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); } diff --git a/src/edit.rs b/src/edit.rs index 1fa0d3eb62..e1b6a19552 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -5,7 +5,6 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::highlight::Highlighter; @@ -13,7 +12,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::{LineBuffer, WordAction, MAX_LINE}; use crate::tty::{Renderer, Term, Terminal}; use crate::undo::Changeset; @@ -341,7 +340,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() diff --git a/src/layout.rs b/src/layout.rs index 5679ec14ce..106f9dc284 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,9 +1,24 @@ use std::cmp::{Ord, Ordering, PartialOrd}; +use std::convert::TryFrom; +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 { diff --git a/src/lib.rs b/src/lib.rs index d4156a2924..6d0a12ce8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,6 @@ use std::sync::{Arc, Mutex}; use log::debug; use radix_trie::Trie; -use unicode_width::UnicodeWidthStr; use crate::tty::{RawMode, Renderer, Term, Terminal}; @@ -280,15 +279,16 @@ fn page_completions( 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 { @@ -322,15 +322,15 @@ fn page_completions( 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(' '); } @@ -870,7 +870,7 @@ impl Editor { /// 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())) diff --git a/src/test/mod.rs b/src/test/mod.rs index c2214d2b86..96ed9f0043 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -9,6 +9,7 @@ use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::keymap::{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, Editor, Helper, Result}; @@ -102,7 +103,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 @@ -122,7 +123,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); } } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 2dc583ad2a..4b5dddb4f1 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,11 +1,9 @@ //! This module implements and describes common TTY methods & traits -use unicode_width::UnicodeWidthStr; - use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::KeyEvent; -use crate::layout::{Layout, Position}; +use crate::layout::{self, Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; @@ -108,9 +106,9 @@ pub trait Renderer { /// Update the number of columns/rows in the current terminal. fn update_size(&mut self); /// Get the number of columns in the current terminal. - fn get_columns(&self) -> usize; + fn get_columns(&self) -> u16; /// Get the number of rows in the current terminal. - fn get_rows(&self) -> usize; + fn get_rows(&self) -> u16; /// Check if output supports colors. fn colors_enabled(&self) -> bool; @@ -165,11 +163,11 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { (**self).update_size(); } - fn get_columns(&self) -> usize { + fn get_columns(&self) -> u16 { (**self).get_columns() } - fn get_rows(&self) -> usize { + fn get_rows(&self) -> u16 { (**self).get_rows() } @@ -183,7 +181,7 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { } // ignore ANSI escape sequence -fn width(s: &str, esc_seq: &mut u8) -> usize { +fn width(s: &str, esc_seq: &mut u8) -> u16 { if *esc_seq == 1 { if s == "[" { // CSI @@ -209,7 +207,7 @@ fn width(s: &str, esc_seq: &mut u8) -> usize { } else if s == "\n" { 0 } else { - s.width() + layout::width(s) } } @@ -230,7 +228,7 @@ pub trait Term { fn new( color_mode: ColorMode, behavior: Behavior, - tab_stop: usize, + tab_stop: u16, bell_style: BellStyle, enable_bracketed_paste: bool, ) -> Self; diff --git a/src/tty/test.rs b/src/tty/test.rs index 50eecbf239..d324d281ec 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -8,7 +8,7 @@ use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::keys::KeyEvent; -use crate::layout::{Layout, Position}; +use crate::layout::{self, Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; @@ -102,7 +102,7 @@ impl Renderer for Sink { fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; - pos.col += s.len(); + pos.col += layout::try_from(s.len()); pos } @@ -128,11 +128,11 @@ impl Renderer for Sink { fn update_size(&mut self) {} - fn get_columns(&self) -> usize { + fn get_columns(&self) -> u16 { 80 } - fn get_rows(&self) -> usize { + fn get_rows(&self) -> u16 { 24 } @@ -158,7 +158,7 @@ pub type Terminal = DummyTerminal; #[derive(Clone, Debug)] pub struct DummyTerminal { pub keys: Vec, - pub cursor: usize, // cursor position before last command + pub cursor: u16, // cursor position before last command pub color_mode: ColorMode, pub bell_style: BellStyle, } @@ -173,7 +173,7 @@ impl Term for DummyTerminal { fn new( color_mode: ColorMode, _behavior: Behavior, - _tab_stop: usize, + _tab_stop: u16, bell_style: BellStyle, _enable_bracketed_paste: bool, ) -> DummyTerminal { diff --git a/src/tty/unix.rs b/src/tty/unix.rs index eeb7bb5cac..2f859ca386 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -35,7 +35,7 @@ const BRACKETED_PASTE_OFF: &str = "\x1b[?2004l"; nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); #[allow(clippy::useless_conversion)] -fn get_win_size(fd: RawFd) -> (usize, usize) { +fn get_win_size(fd: RawFd) -> (u16, u16) { use std::mem::zeroed; if cfg!(test) { @@ -50,15 +50,11 @@ fn get_win_size(fd: RawFd) -> (usize, usize) { // zero. If host application didn't initialize the correct // size before start we treat zero size as 80 columns and // infinite rows - let cols = if size.ws_col == 0 { - 80 - } else { - size.ws_col as usize - }; + let cols = if size.ws_col == 0 { 80 } else { size.ws_col }; let rows = if size.ws_row == 0 { - usize::MAX + u16::MAX } else { - size.ws_row as usize + size.ws_row }; (cols, rows) } @@ -822,15 +818,15 @@ impl Receiver for Utf8 { /// Console output writer pub struct PosixRenderer { out: RawFd, - cols: usize, // Number of columns in terminal + cols: u16, // Number of columns in terminal buffer: String, - tab_stop: usize, + tab_stop: u16, colors_enabled: bool, bell_style: BellStyle, } impl PosixRenderer { - fn new(out: RawFd, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle) -> Self { + fn new(out: RawFd, tab_stop: u16, colors_enabled: bool, bell_style: BellStyle) -> Self { let (cols, _) = get_win_size(out); Self { out, @@ -1039,13 +1035,13 @@ impl Renderer for PosixRenderer { self.cols = cols; } - fn get_columns(&self) -> usize { + fn get_columns(&self) -> u16 { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. - fn get_rows(&self) -> usize { + fn get_rows(&self) -> u16 { let (_, rows) = get_win_size(self.out); rows } @@ -1147,7 +1143,7 @@ pub struct PosixTerminal { is_out_a_tty: bool, close_on_drop: bool, pub(crate) color_mode: ColorMode, - tab_stop: usize, + tab_stop: u16, bell_style: BellStyle, enable_bracketed_paste: bool, raw_mode: Arc, @@ -1177,7 +1173,7 @@ impl Term for PosixTerminal { fn new( color_mode: ColorMode, behavior: Behavior, - tab_stop: usize, + tab_stop: u16, bell_style: BellStyle, enable_bracketed_paste: bool, ) -> Self { @@ -1449,7 +1445,10 @@ mod test { assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor); assert_eq!(old_layout.cursor, old_layout.end); - assert_eq!(Some(true), line.insert('a', out.cols - prompt_size.col + 1)); + assert_eq!( + Some(true), + line.insert('a', (out.cols - prompt_size.col + 1).into()) + ); let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); assert_eq!(new_layout.cursor, new_layout.end); diff --git a/src/tty/windows.rs b/src/tty/windows.rs index c5a237df21..e2ace78ba0 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -54,13 +54,13 @@ fn check(rc: BOOL) -> io::Result<()> { } } -fn get_win_size(handle: HANDLE) -> (usize, usize) { +fn get_win_size(handle: HANDLE) -> (u16, u16) { let mut info = unsafe { mem::zeroed() }; match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { FALSE => (80, 24), _ => ( - info.dwSize.X as usize, - (1 + info.srWindow.Bottom - info.srWindow.Top) as usize, + info.dwSize.X as u16, + (1 + info.srWindow.Bottom - info.srWindow.Top) as u16, ), // (info.srWindow.Right - info.srWindow.Left + 1) } } @@ -286,7 +286,7 @@ fn read_input(handle: HANDLE, max_count: u32) -> Result { pub struct ConsoleRenderer { conout: HANDLE, - cols: usize, // Number of columns in terminal + cols: u16, // Number of columns in terminal buffer: String, utf16: Vec, colors_enabled: bool, @@ -526,13 +526,13 @@ impl Renderer for ConsoleRenderer { self.cols = cols; } - fn get_columns(&self) -> usize { + fn get_columns(&self) -> u16 { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. - fn get_rows(&self) -> usize { + fn get_rows(&self) -> u16 { let (_, rows) = get_win_size(self.conout); rows } @@ -641,7 +641,7 @@ impl Term for Console { fn new( color_mode: ColorMode, behavior: Behavior, - _tab_stop: usize, + _tab_stop: u16, bell_style: BellStyle, _enable_bracketed_paste: bool, ) -> Console { From 2c7335aabbcfcdaf747e04c5ebe377fbae0ba98b Mon Sep 17 00:00:00 2001 From: gwenn Date: Tue, 12 Apr 2022 19:40:26 +0200 Subject: [PATCH 2/5] Remember the number of rows --- src/tty/unix.rs | 10 ++++++---- src/tty/windows.rs | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 2f859ca386..394fbe0af0 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -819,6 +819,7 @@ impl Receiver for Utf8 { pub struct PosixRenderer { out: RawFd, cols: u16, // Number of columns in terminal + rows: u16, // Number of rows in terminal buffer: String, tab_stop: u16, colors_enabled: bool, @@ -827,10 +828,11 @@ pub struct PosixRenderer { impl PosixRenderer { fn new(out: RawFd, tab_stop: u16, colors_enabled: bool, bell_style: BellStyle) -> Self { - let (cols, _) = get_win_size(out); + let (cols, rows) = get_win_size(out); Self { out, cols, + rows, buffer: String::with_capacity(1024), tab_stop, colors_enabled, @@ -1031,8 +1033,9 @@ impl Renderer for PosixRenderer { /// Try to update the number of columns in the current terminal, fn update_size(&mut self) { - let (cols, _) = get_win_size(self.out); + let (cols, rows) = get_win_size(self.out); self.cols = cols; + self.rows = rows; } fn get_columns(&self) -> u16 { @@ -1042,8 +1045,7 @@ impl Renderer for PosixRenderer { /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> u16 { - let (_, rows) = get_win_size(self.out); - rows + self.rows } fn colors_enabled(&self) -> bool { diff --git a/src/tty/windows.rs b/src/tty/windows.rs index e2ace78ba0..1366e6e0c7 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -287,6 +287,7 @@ fn read_input(handle: HANDLE, max_count: u32) -> Result { pub struct ConsoleRenderer { conout: HANDLE, cols: u16, // Number of columns in terminal + rows: u16, // Number of rows in terminal buffer: String, utf16: Vec, colors_enabled: bool, @@ -296,10 +297,11 @@ pub struct ConsoleRenderer { impl ConsoleRenderer { fn new(conout: HANDLE, colors_enabled: bool, bell_style: BellStyle) -> ConsoleRenderer { // Multi line editing is enabled by ENABLE_WRAP_AT_EOL_OUTPUT mode - let (cols, _) = get_win_size(conout); + let (cols, rows) = get_win_size(conout); ConsoleRenderer { conout, cols, + rows, buffer: String::with_capacity(1024), utf16: Vec::with_capacity(1024), colors_enabled, @@ -522,8 +524,9 @@ impl Renderer for ConsoleRenderer { /// Try to get the number of columns in the current terminal, /// or assume 80 if it fails. fn update_size(&mut self) { - let (cols, _) = get_win_size(self.conout); + let (cols, rows) = get_win_size(self.conout); self.cols = cols; + self.rows = rows; } fn get_columns(&self) -> u16 { @@ -533,8 +536,7 @@ impl Renderer for ConsoleRenderer { /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> u16 { - let (_, rows) = get_win_size(self.conout); - rows + self.rows } fn colors_enabled(&self) -> bool { From 9bd1f79b6701c4739e73906f5e3886b52c8cc27a Mon Sep 17 00:00:00 2001 From: gwenn Date: Wed, 20 Apr 2022 18:48:07 +0200 Subject: [PATCH 3/5] Vertical scroll --- src/edit.rs | 29 +++++++++-------- src/layout.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 3 +- src/tty/mod.rs | 51 ++++++++++++++++++++++++------ src/tty/test.rs | 2 +- src/tty/unix.rs | 39 ++++++++++++++--------- src/tty/windows.rs | 27 +++++++++++++--- 7 files changed, 184 insertions(+), 45 deletions(-) diff --git a/src/edit.rs b/src/edit.rs index e1b6a19552..bf42fc8c54 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -48,7 +48,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, @@ -84,9 +84,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let rc = input_state.next_cmd(rdr, self, single_esc_abort, ignore_external_print); if rc.is_err() && self.out.sigwinch() { self.out.update_size(); - 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; } @@ -111,13 +111,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 { @@ -133,6 +133,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; @@ -264,7 +267,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) @@ -304,8 +309,7 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn external_print(&mut self, rdr: &mut ::Reader, 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())?; self.move_cursor_at_leftmost(rdr)?; self.refresh_line() @@ -328,8 +332,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(()) } diff --git a/src/layout.rs b/src/layout.rs index 106f9dc284..0f895caed8 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,5 +1,6 @@ use std::cmp::{Ord, Ordering, PartialOrd}; use std::convert::TryFrom; +use std::ops::Index; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; #[inline] @@ -43,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, +} + +impl Index 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; + } } diff --git a/src/lib.rs b/src/lib.rs index 6d0a12ce8c..90f9529a93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,8 +340,7 @@ fn page_completions( 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) } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 4b5dddb4f1..891e029217 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -62,33 +62,61 @@ pub trait Renderer { line: &LineBuffer, info: Option<&str>, ) -> Layout { + let max_rows = self.get_rows(); + let mut breaks = Vec::with_capacity(max_rows as usize); // FIXME prompt breaks missing + breaks.push(0); // calculate the desired position of the cursor let pos = line.pos(); - let cursor = self.calculate_position(&line[..pos], prompt_size); + let cursor = self.calculate_position(&line[..pos], prompt_size, Some(&mut breaks)); // calculate the position of the end of the input line - let mut end = if pos == line.len() { + let end_input = if pos == line.len() { cursor } else { - self.calculate_position(&line[pos..], cursor) + self.calculate_position(&line[pos..], cursor, Some(&mut breaks)) }; - if let Some(info) = info { - end = self.calculate_position(info, end); + let end = if let Some(info) = info { + self.calculate_position(info, end_input, Some(&mut breaks)) + } else { + end_input + }; + + let rows = end.row + 1; + // ensure cursor is visible + let mut first_row = 0; + let mut last_row = end.row; + if max_rows <= 1 { + first_row = cursor.row; + last_row = first_row; + } else if rows > max_rows { + first_row = cursor.row.saturating_sub(max_rows - 1); + last_row = (first_row + max_rows).saturating_sub(1); } + debug_assert!(last_row - first_row < max_rows); let new_layout = Layout { prompt_size, default_prompt, cursor, + end_input, end, + first_row, + last_row, + breaks, }; debug_assert!(new_layout.prompt_size <= new_layout.cursor); - debug_assert!(new_layout.cursor <= new_layout.end); + debug_assert!(new_layout.cursor <= new_layout.end_input); + debug_assert!(new_layout.end_input <= new_layout.end); new_layout } /// Calculate the number of columns and rows used to display `s` on a /// `cols` width terminal starting at `orig`. - fn calculate_position(&self, s: &str, orig: Position) -> Position; + fn calculate_position( + &self, + s: &str, + orig: Position, + breaks: Option<&mut Vec>, + ) -> Position; fn write_and_flush(&mut self, buf: &str) -> Result<()>; @@ -135,8 +163,13 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { - (**self).calculate_position(s, orig) + fn calculate_position( + &self, + s: &str, + orig: Position, + breaks: Option<&mut Vec>, + ) -> Position { + (**self).calculate_position(s, orig, breaks) } fn write_and_flush(&mut self, buf: &str) -> Result<()> { diff --git a/src/tty/test.rs b/src/tty/test.rs index d324d281ec..23523196fe 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -100,7 +100,7 @@ impl Renderer for Sink { Ok(()) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position(&self, s: &str, orig: Position, _: Option<&mut Vec>) -> Position { let mut pos = orig; pos.col += layout::try_from(s.len()); pos diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 394fbe0af0..dc26beec14 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -842,17 +842,13 @@ impl PosixRenderer { fn clear_old_rows(&mut self, layout: &Layout) { use std::fmt::Write; - let current_row = layout.cursor.row; - let old_rows = layout.end.row; - // old_rows < cursor_row if the prompt spans multiple lines and if - // this is the default State. - let cursor_row_movement = old_rows.saturating_sub(current_row); + let cursor_row_movement = layout.lines_below_cursor(); // move the cursor down as required if cursor_row_movement > 0 { write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap(); } // clear old rows - for _ in 0..old_rows { + for _ in layout.first_row..layout.last_row { self.buffer.push_str("\r\x1b[K\x1b[A"); } // clear the line @@ -923,14 +919,14 @@ impl Renderer for PosixRenderer { let end_pos = new_layout.end; self.clear_old_rows(old_layout); - + let prompt = new_layout.visible_prompt(prompt); + let (line, pos) = new_layout.visible_line(line, line.pos()); if let Some(highlighter) = highlighter { // display the prompt self.buffer .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); // display the input line - self.buffer - .push_str(&highlighter.highlight(line, line.pos())); + self.buffer.push_str(&highlighter.highlight(line, pos)); } else { // display the prompt self.buffer.push_str(prompt); @@ -939,6 +935,7 @@ impl Renderer for PosixRenderer { } // display hint if let Some(hint) = hint { + let hint = new_layout.visible_hint(hint); if let Some(highlighter) = highlighter { self.buffer.push_str(&highlighter.highlight_hint(hint)); } else { @@ -955,7 +952,7 @@ impl Renderer for PosixRenderer { self.buffer.push('\n'); } // position the cursor - let new_cursor_row_movement = end_pos.row - cursor.row; + let new_cursor_row_movement = new_layout.lines_below_cursor(); // move the cursor up as required if new_cursor_row_movement > 0 { write!(self.buffer, "\x1b[{}A", new_cursor_row_movement)?; @@ -978,13 +975,21 @@ impl Renderer for PosixRenderer { /// Control characters are treated as having zero width. /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position( + &self, + s: &str, + orig: Position, + mut breaks: Option<&mut Vec>, + ) -> Position { let mut pos = orig; let mut esc_seq = 0; - for c in s.graphemes(true) { + for (offset, c) in s.grapheme_indices(true) { if c == "\n" { pos.row += 1; pos.col = 0; + if let Some(ref mut breaks) = breaks { + breaks.push(offset + 1); + } continue; } let cw = if c == "\t" { @@ -996,11 +1001,17 @@ impl Renderer for PosixRenderer { if pos.col > self.cols { pos.row += 1; pos.col = cw; + if let Some(ref mut breaks) = breaks { + breaks.push(offset); + } } } if pos.col == self.cols { pos.col = 0; pos.row += 1; + if let Some(ref mut breaks) = breaks { + breaks.push(s.len()); + } } pos } @@ -1409,7 +1420,7 @@ mod test { #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(libc::STDOUT_FILENO, 4, true, BellStyle::default()); - let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default()); + let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), None); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } @@ -1440,7 +1451,7 @@ mod test { let mut out = PosixRenderer::new(libc::STDOUT_FILENO, 4, true, BellStyle::default()); let prompt = "> "; let default_prompt = true; - let prompt_size = out.calculate_position(prompt, Position::default()); + let prompt_size = out.calculate_position(prompt, Position::default(), None); let mut line = LineBuffer::init("", 0, None); let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None); diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 1366e6e0c7..0f2173985a 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -365,8 +365,8 @@ impl ConsoleRenderer { // position at the start of the prompt, clear to end of previous input fn clear_old_rows(&mut self, info: &CONSOLE_SCREEN_BUFFER_INFO, layout: &Layout) -> Result<()> { - let current_row = layout.cursor.row; - let old_rows = layout.end.row; + let current_row = layout.cursor.row.saturating_sub(layout.first_row); + let old_rows = layout.last_row.saturating_sub(layout.first_row); let mut coord = info.dwCursorPosition; coord.X = 0; coord.Y -= current_row as i16; @@ -426,12 +426,14 @@ impl Renderer for ConsoleRenderer { self.buffer.clear(); let mut col = 0; + let prompt = new_layout.visible_prompt(prompt); + let (line, pos) = new_layout.visible_line(line, line.pos()); if let Some(highlighter) = highlighter { // TODO handle ansi escape code (SetConsoleTextAttribute) // append the prompt col = self.wrap_at_eol(&highlighter.highlight_prompt(prompt, default_prompt), col); // append the input line - col = self.wrap_at_eol(&highlighter.highlight(line, line.pos()), col); + col = self.wrap_at_eol(&highlighter.highlight(line, pos), col); } else { // append the prompt self.buffer.push_str(prompt); @@ -440,6 +442,7 @@ impl Renderer for ConsoleRenderer { } // append hint if let Some(hint) = hint { + let hint = new_layout.visible_hint(hint); if let Some(highlighter) = highlighter { self.wrap_at_eol(&highlighter.highlight_hint(hint), col); } else { @@ -472,24 +475,38 @@ impl Renderer for ConsoleRenderer { } /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position( + &self, + s: &str, + orig: Position, + breaks: Option<&mut Vec>, + ) -> Position { let mut pos = orig; - for c in s.graphemes(true) { + for (offset, c) in s.grapheme_indices(true) { if c == "\n" { pos.col = 0; pos.row += 1; + if let Some(ref mut breaks) = breaks { + breaks.push(offset + 1); + } } else { let cw = c.width(); pos.col += cw; if pos.col > self.cols { pos.row += 1; pos.col = cw; + if let Some(ref mut breaks) = breaks { + breaks.push(offset); + } } } } if pos.col == self.cols { pos.col = 0; pos.row += 1; + if let Some(ref mut breaks) = breaks { + breaks.push(s.len()); + } } pos } From 9c4959e345c196676f026434cc54fb755a16a9d1 Mon Sep 17 00:00:00 2001 From: gwenn Date: Thu, 21 Apr 2022 06:52:43 +0000 Subject: [PATCH 4/5] Fix windows build errors --- src/tty/windows.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 0f2173985a..3a0ce14ce2 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -12,7 +12,6 @@ use std::sync::Arc; use log::{debug, warn}; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE, WORD}; use winapi::shared::winerror; use winapi::um::handleapi::{self, CloseHandle, INVALID_HANDLE_VALUE}; @@ -25,7 +24,7 @@ use super::{width, Event, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::{KeyCode as K, KeyEvent, Modifiers as M}; -use crate::layout::{Layout, Position}; +use crate::layout::{self, Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{error, Cmd, Result}; @@ -341,7 +340,7 @@ impl ConsoleRenderer { // You can't have both ENABLE_WRAP_AT_EOL_OUTPUT and // ENABLE_VIRTUAL_TERMINAL_PROCESSING. So we need to wrap manually. - fn wrap_at_eol(&mut self, s: &str, mut col: usize) -> usize { + fn wrap_at_eol(&mut self, s: &str, mut col: u16) -> u16 { let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { @@ -479,7 +478,7 @@ impl Renderer for ConsoleRenderer { &self, s: &str, orig: Position, - breaks: Option<&mut Vec>, + mut breaks: Option<&mut Vec>, ) -> Position { let mut pos = orig; for (offset, c) in s.grapheme_indices(true) { @@ -490,7 +489,7 @@ impl Renderer for ConsoleRenderer { breaks.push(offset + 1); } } else { - let cw = c.width(); + let cw = layout::width(c); pos.col += cw; if pos.col > self.cols { pos.row += 1; From b04dacd16ada02a3bcbb4220c9753be5854e6ee1 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 25 Jun 2023 13:03:53 +0200 Subject: [PATCH 5/5] Rustfmt --- src/edit.rs | 5 +++-- src/tty/unix.rs | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/edit.rs b/src/edit.rs index 40b7436e48..f9a3ad0465 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -91,9 +91,10 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let new_cols = self.out.get_columns(); if new_cols != old_cols && (self.layout.end.row > 0 || self.layout.end.col >= new_cols) - {self.prompt_size = + { + self.prompt_size = self.out - .calculate_position(self.prompt, Position::default(), None); + .calculate_position(self.prompt, Position::default(), None); self.refresh_line()?; } continue; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index db7fc3c668..c22b217104 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -1549,7 +1549,11 @@ mod test { assert_eq!( Some(true), - line.insert('a', (out.cols - prompt_size.col + 1).into(), &mut NoListener) + line.insert( + 'a', + (out.cols - prompt_size.col + 1).into(), + &mut NoListener + ) ); let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor);