From 0018705af4bd4e86adebc5446c1ee84fd3f2c6b7 Mon Sep 17 00:00:00 2001 From: Paul Colomiets Date: Wed, 22 Apr 2020 18:27:33 +0300 Subject: [PATCH 1/3] Implement optional rectangular prompt support This is a reimplementation of #338 which was reverted. When `Highligher::has_continuation_prompt` returns true, rustyline enables a special mode that passes the original prompt for every line to the highlighter, and highlighter is free to change color or characters of the prompt as long as it's length is the same (usual contract for the highlighter) Note: 1. Wrapped lines are not prefixed by prompt (this may be fixed in future releases either by prepending them, or by collapsing and scrolling lines wider than terminal). 2. Unlike #338 this PR doesn't change the meaning of `cursor` and `end` fields of the `Layout` structure. --- examples/example.rs | 21 ++++++-- src/edit.rs | 100 ++++++++++++++++++++++---------------- src/highlight.rs | 106 +++++++++++++++++++++++++++++++++++++++-- src/layout.rs | 5 +- src/test/highlight.rs | 22 +++++++++ src/test/mod.rs | 1 + src/tty/mod.rs | 108 ++++++++++++++++++++++++++++++++++++------ src/tty/test.rs | 7 ++- src/tty/unix.rs | 46 ++++++++---------- src/tty/windows.rs | 43 +++++++---------- 10 files changed, 342 insertions(+), 117 deletions(-) create mode 100644 src/test/highlight.rs diff --git a/examples/example.rs b/examples/example.rs index 2b3363fcd3..84d752cc18 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -4,6 +4,7 @@ use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::highlight::{PromptInfo}; use rustyline::hint::{Hinter, HistoryHinter}; use rustyline::validate::{self, MatchingBracketValidator, Validator}; use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress}; @@ -16,6 +17,7 @@ struct MyHelper { validator: MatchingBracketValidator, hinter: HistoryHinter, colored_prompt: String, + continuation_prompt: String, } impl Completer for MyHelper { @@ -41,15 +43,23 @@ impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - if default { - Borrowed(&self.colored_prompt) + if info.is_default() { + if info.line_no() > 0 { + Borrowed(&self.continuation_prompt) + } else { + Borrowed(&self.colored_prompt) + } } else { Borrowed(prompt) } } + fn has_continuation_prompt(&self) -> bool { + return true; + } + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } @@ -90,7 +100,8 @@ fn main() -> rustyline::Result<()> { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, - colored_prompt: "".to_owned(), + colored_prompt: " 0> ".to_owned(), + continuation_prompt: "\x1b[1;32m...> \x1b[0m".to_owned(), validator: MatchingBracketValidator::new(), }; let mut rl = Editor::with_config(config); @@ -102,7 +113,7 @@ fn main() -> rustyline::Result<()> { } let mut count = 1; loop { - let p = format!("{}> ", count); + let p = format!("{:>3}> ", count); rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); let readline = rl.readline(&p); match readline { diff --git a/src/edit.rs b/src/edit.rs index 0285c8ecb3..bb2cf042a1 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -18,12 +18,24 @@ use crate::tty::{Renderer, Term, Terminal}; use crate::undo::Changeset; use crate::validate::{ValidationContext, ValidationResult}; + +#[derive(Debug)] +pub struct Prompt<'prompt> { + /// Prompt to display (rl_prompt) + pub text: &'prompt str, + /// Prompt Unicode/visible width and height + pub size: Position, + /// Is this a default (user-defined) prompt, or temporary like `(arg: 0)`? + pub is_default: bool, + /// Is prompt rectangular or single line + pub has_continuation: bool, +} + /// Represent the state during line editing. /// Implement rendering. pub struct State<'out, 'prompt, H: Helper> { pub out: &'out mut ::Writer, - prompt: &'prompt str, // Prompt to display (rl_prompt) - prompt_size: Position, // Prompt Unicode/visible width and height + prompt: Prompt<'prompt>, pub line: LineBuffer, // Edited line buffer pub layout: Layout, saved_line_for_history: LineBuffer, // Current edited line before history browsing @@ -48,11 +60,18 @@ 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(), 0); + let has_continuation = helper + .map(|h| h.has_continuation_prompt()) + .unwrap_or(false); State { out, - prompt, - prompt_size, + prompt: Prompt { + text: prompt, + size: prompt_size, + is_default: true, + has_continuation, + }, line: LineBuffer::with_capacity(MAX_LINE).can_growth(true), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true), @@ -83,9 +102,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let rc = input_state.next_cmd(rdr, self, single_esc_abort); if rc.is_err() && self.out.sigwinch() { self.out.update_size(); - self.prompt_size = self + self.prompt.size = self .out - .calculate_position(self.prompt, Position::default()); + .calculate_position(self.prompt.text, Position::default(), 0); self.refresh_line()?; continue; } @@ -110,20 +129,17 @@ 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); - if self.layout.cursor == cursor { + let new_layout = self.out.compute_layout( + &self.prompt, &self.line, None); + if new_layout.cursor == self.layout.cursor { return Ok(()); } if self.highlight_char() { - let prompt_size = self.prompt_size; - self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; + self.refresh_default(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); + self.out.move_cursor(self.layout.cursor, new_layout.cursor)?; + self.layout.prompt_size = self.prompt.size; + self.layout.cursor = new_layout.cursor; debug_assert!(self.layout.cursor <= self.layout.end); } Ok(()) @@ -133,13 +149,16 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { self.out.move_cursor_at_leftmost(rdr) } - fn refresh( - &mut self, - prompt: &str, - prompt_size: Position, - default_prompt: bool, - info: Info<'_>, - ) -> Result<()> { + fn refresh(&mut self, prompt: &Prompt<'_>, info: Info<'_>) -> Result<()> { + self._refresh(Some(prompt), info) + } + fn refresh_default(&mut self, info: Info<'_>) -> Result<()> { + // We pass None, because we can't pass `&self.prompt` + // to the method having `&mut self` as a receiver + self._refresh(None, info) + } + fn _refresh(&mut self, non_default_prompt: Option<&Prompt<'_>>, info: Info<'_>) -> Result<()> { + let prompt = non_default_prompt.unwrap_or(&self.prompt); let info = match info { Info::NoHint => None, Info::Hint => self.hint.as_deref(), @@ -151,10 +170,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 = self.out.compute_layout(prompt, &self.line, info); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); self.out.refresh_line( @@ -239,24 +255,27 @@ impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line(&mut self) -> Result<()> { - let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh_default(Info::Hint) } fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()> { - let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Msg(msg.as_deref())) + self.refresh_default(Info::Msg(msg.as_deref())) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { - let prompt_size = self.out.calculate_position(prompt, Position::default()); + let prompt = Prompt { + text: prompt, + size: self.out.calculate_position(prompt, Position::default(), 0), + is_default: false, + has_continuation: false, + }; self.hint(); self.highlight_char(); - self.refresh(prompt, prompt_size, false, Info::Hint) + self.refresh(&prompt, Info::Hint) } fn doing_insert(&mut self) { @@ -284,7 +303,6 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("State") .field("prompt", &self.prompt) - .field("prompt_size", &self.prompt_size) .field("buf", &self.line) .field("cols", &self.out.get_columns()) .field("layout", &self.layout) @@ -305,7 +323,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { if let Some(push) = self.line.insert(ch, n) { if push { - let prompt_size = self.prompt_size; let no_previous_hint = self.hint.is_none(); self.hint(); let width = ch.width().unwrap_or(0); @@ -318,13 +335,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, 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); let bits = bits.as_bytes(); self.out.write_and_flush(bits) } else { - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh_default(Info::Hint) } } else { self.refresh_line() @@ -664,8 +680,12 @@ pub fn init_state<'out, H: Helper>( ) -> State<'out, 'static, H> { State { out, - prompt: "", - prompt_size: Position::default(), + prompt: Prompt { + text: "", + size: Position::default(), + is_default: true, + has_continuation: false, + }, line: LineBuffer::init(line, pos, None), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(100), diff --git a/src/highlight.rs b/src/highlight.rs index 4d62a63c09..7e018e983f 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,6 +5,15 @@ use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; +pub struct PromptInfo<'a> { + pub(crate) is_default: bool, + pub(crate) offset: usize, + pub(crate) cursor: Option, + pub(crate) input: &'a str, + pub(crate) line: &'a str, + pub(crate) line_no: usize, +} + /// Syntax highlighter with [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters). /// Rustyline will try to handle escape sequence for ANSI color on windows /// when not supported natively (windows <10). @@ -26,11 +35,18 @@ pub trait Highlighter { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - let _ = default; + let _ = info; Borrowed(prompt) } + + /// Returns `true` if prompt is rectangular rather than being present only + /// on the first line of input + fn has_continuation_prompt(&self) -> bool { + false + } + /// Takes the `hint` and /// returns the highlighted version (with ANSI color). fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { @@ -69,9 +85,9 @@ impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - (**self).highlight_prompt(prompt, default) + (**self).highlight_prompt(prompt, info) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { @@ -232,6 +248,88 @@ fn is_close_bracket(bracket: u8) -> bool { memchr(bracket, CLOSES).is_some() } +pub(crate) fn split_highlight<'a>(src: &'a str, offset: usize) + -> (Cow<'a, str>, Cow<'a, str>) +{ + let mut style_buffer = String::with_capacity(32); + let mut iter = src.char_indices(); + let mut non_escape_idx = 0; + while let Some((idx, c)) = iter.next() { + if c == '\x1b' { + match iter.next() { + Some((_, '[')) => {} + _ => continue, // unknown escape, skip + } + while let Some((end_idx, c)) = iter.next() { + match c { + 'm' => { + let slice = &src[idx..end_idx+1]; + if slice == "\x1b[0m" { + style_buffer.clear(); + } else { + style_buffer.push_str(slice); + } + break; + } + ';' | '0'..='9' => continue, + _ => break, // unknown escape, skip + } + } + continue; + } + if non_escape_idx >= offset { + if style_buffer.is_empty() { + return (src[..idx].into(), src[idx..].into()); + } else { + let mut left = String::with_capacity(idx + 4); + left.push_str(&src[..idx]); + left.push_str("\x1b[0m"); + let mut right = String::with_capacity( + src.len() - idx + style_buffer.len()); + right.push_str(&style_buffer); + right.push_str(&src[idx..]); + return (left.into(), right.into()); + } + } + non_escape_idx += c.len_utf8(); + } + return (src.into(), "".into()); +} + +impl PromptInfo<'_> { + /// Returns true if this is the default prompt + pub fn is_default(&self) -> bool { + self.is_default + } + /// Returns the byte offset where prompt is shown in the initial text + /// + /// This is a position right after the newline of the previous line + pub fn line_offset(&self) -> usize { + self.offset + } + + /// Returns the byte position of the cursor relative to `line_offset` if + /// the cursor is in the current line + pub fn cursor(&self) -> Option { + self.cursor + } + + /// Returns the zero-based line number of the current prompt line + pub fn line_no(&self) -> usize { + self.line_no + } + + /// Returns the line contents shown after the prompt + pub fn line(&self) -> &str { + self.line + } + + /// Returns the whole input (equal to `line` if input is the single line) + pub fn input(&self) -> &str { + self.input + } +} + #[cfg(test)] mod tests { #[test] diff --git a/src/layout.rs b/src/layout.rs index 5679ec14ce..e70061eac1 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -25,9 +25,10 @@ impl Ord for Position { pub struct Layout { /// Prompt Unicode/visible width and height pub prompt_size: Position, + pub left_margin: usize, pub default_prompt: bool, - /// Cursor position (relative to the start of the prompt) + /// Cursor position (relative to the end of the prompt) pub cursor: Position, - /// Number of rows used so far (from start of prompt to end of input) + /// Number of rows used so far (from end of prompt to end of input) pub end: Position, } diff --git a/src/test/highlight.rs b/src/test/highlight.rs new file mode 100644 index 0000000000..905197adc9 --- /dev/null +++ b/src/test/highlight.rs @@ -0,0 +1,22 @@ +use crate::highlight::split_highlight; + +#[test] +fn split_bold() { + let (a, b) = split_highlight("\x1b[1mword1 word2\x1b[0m", 5); + assert_eq!(a, "\x1b[1mword1\x1b[0m"); + assert_eq!(b, "\x1b[1m word2\x1b[0m"); +} + +#[test] +fn split_at_the_reset() { + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 5); + assert_eq!(a, "\x1b[1mword1\x1b[0m"); + assert_eq!(b, " word2"); +} + +#[test] +fn split_nowhere() { + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 6); + assert_eq!(a, "\x1b[1mword1\x1b[0m "); + assert_eq!(b, "word2"); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 365fe91a8b..ad7fc340ec 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -18,6 +18,7 @@ mod emacs; mod history; mod vi_cmd; mod vi_insert; +mod highlight; fn init_editor(mode: EditMode, keys: &[KeyPress]) -> Editor<()> { let config = Config::builder().edit_mode(mode).build(); diff --git a/src/tty/mod.rs b/src/tty/mod.rs index cfa393274d..151048d7f9 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -3,7 +3,8 @@ use unicode_width::UnicodeWidthStr; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; -use crate::highlight::Highlighter; +use crate::edit::Prompt; +use crate::highlight::{Highlighter, PromptInfo, split_highlight}; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; @@ -36,7 +37,7 @@ pub trait Renderer { #[allow(clippy::too_many_arguments)] fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -49,38 +50,44 @@ pub trait Renderer { /// wrapping may be applied. fn compute_layout( &self, - prompt_size: Position, - default_prompt: bool, + prompt: &Prompt, 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); + let left_margin = if prompt.has_continuation { + prompt.size.col + } else { + 0 + }; + let cursor = self.calculate_position(&line[..pos], + prompt.size, left_margin); // 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) + self.calculate_position(&line[pos..], cursor, left_margin) }; if let Some(info) = info { - end = self.calculate_position(&info, end); + end = self.calculate_position(&info, end, left_margin); } let new_layout = Layout { - prompt_size, - default_prompt, + prompt_size: prompt.size, + left_margin, + default_prompt: prompt.is_default, 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`. - fn calculate_position(&self, s: &str, orig: Position) -> Position; + fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) + -> Position; fn write_and_flush(&self, buf: &[u8]) -> Result<()>; @@ -115,7 +122,7 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -125,8 +132,10 @@ 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, left_margin: usize) + -> Position + { + (**self).calculate_position(s, orig, left_margin) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { @@ -224,6 +233,77 @@ pub trait Term { fn create_writer(&self) -> Self::Writer; } +fn add_prompt_and_highlight( + mut push_str: F, highlighter: Option<&dyn Highlighter>, + line: &LineBuffer, prompt: &Prompt) + where F: FnMut(&str), +{ + if let Some(highlighter) = highlighter { + if highlighter.has_continuation_prompt() { + if &line[..] == "" { + // line.lines() is an empty iterator for empty line so + // we need to treat it as a special case + let prompt = highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset: 0, + cursor: Some(0), + input: "", + line: "", + line_no: 0, + }); + push_str(&prompt); + } else { + let highlighted = highlighter.highlight(line, line.pos()); + let lines = line.split('\n'); + let mut highlighted_left = highlighted.to_string(); + let mut offset = 0; + for (line_no, orig) in lines.enumerate() { + let (hl, tail) = split_highlight(&highlighted_left, + orig.len()+1); + let has_cursor = + line.pos() > offset && line.pos() < orig.len(); + let prompt = highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset, + cursor: if has_cursor { + Some(line.pos() - offset) + } else { + None + }, + input: line, + line: orig, + line_no, + }); + push_str(&prompt); + push_str(&hl); + highlighted_left = tail.to_string(); + offset += orig.len() + 1; + } + } + } else { + // display the prompt + push_str(&highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset: 0, + cursor: Some(line.pos()), + input: line, + line: line, + line_no: 0, + })); + // display the input line + push_str(&highlighter.highlight(line, line.pos())); + } + } else { + // display the prompt + push_str(prompt.text); + // display the input line + push_str(line); + } +} + // If on Windows platform import Windows TTY module // and re-export into mod.rs scope #[cfg(all(windows, not(target_arch = "wasm32")))] diff --git a/src/tty/test.rs b/src/tty/test.rs index f8dbb02ad6..7ebd94d9ad 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -6,6 +6,7 @@ use std::vec::IntoIter; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error::ReadlineError; +use crate::edit::Prompt; use crate::highlight::Highlighter; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; @@ -77,7 +78,7 @@ impl Renderer for Sink { fn refresh_line( &mut self, - _prompt: &str, + _prompt: &Prompt, _line: &LineBuffer, _hint: Option<&str>, _old_layout: &Layout, @@ -87,7 +88,9 @@ impl Renderer for Sink { Ok(()) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position(&self, s: &str, orig: Position, _left_margin: usize) + -> Position + { let mut pos = orig; pos.col += s.len(); pos diff --git a/src/tty/unix.rs b/src/tty/unix.rs index e9c6c5a09f..58b96656e9 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -16,11 +16,13 @@ use utf8parse::{Parser, Receiver}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; +use crate::edit::Prompt; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; +use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; @@ -568,7 +570,7 @@ impl Renderer for PosixRenderer { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -578,25 +580,13 @@ impl Renderer for PosixRenderer { use std::fmt::Write; self.buffer.clear(); - let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; self.clear_old_rows(old_layout); - 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())); - } else { - // display the prompt - self.buffer.push_str(prompt); - // display the input line - self.buffer.push_str(line); - } + add_prompt_and_highlight(|s| self.buffer.push_str(s), + highlighter, line, prompt); // display hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { @@ -633,13 +623,15 @@ 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, left_margin: usize) + -> Position + { let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { pos.row += 1; - pos.col = 0; + pos.col = left_margin; continue; } let cw = if c == "\t" { @@ -915,12 +907,13 @@ mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::{BellStyle, OutputStreamType}; use crate::line_buffer::LineBuffer; + use crate::edit::Prompt; #[test] #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(OutputStreamType::Stdout, 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(), 0); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } @@ -949,20 +942,23 @@ mod test { #[test] fn test_line_wrap() { let mut out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default()); - let prompt = "> "; - let default_prompt = true; - let prompt_size = out.calculate_position(prompt, Position::default()); + let prompt = Prompt { + text: "> ", + is_default: true, + size: out.calculate_position("> ", Position::default(), 0), + has_continuation: false, + }; let mut line = LineBuffer::init("", 0, None); - let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + let old_layout = out.compute_layout(&prompt, &line, None); 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)); - let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + assert_eq!(Some(true), line.insert('a', out.cols - prompt.size.col + 1)); + let new_layout = out.compute_layout(&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) + out.refresh_line(&prompt, &line, None, &old_layout, &new_layout, None) .unwrap(); #[rustfmt::skip] assert_eq!( diff --git a/src/tty/windows.rs b/src/tty/windows.rs index ea499c7c25..c5bc4153fd 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -14,12 +14,14 @@ use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; +use crate::edit::Prompt; use crate::error; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; +use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; @@ -293,27 +295,26 @@ 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, col: &mut usize) { let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { - col = 0; + *col = 0; self.buffer.push_str(c); } else { let cw = width(c, &mut esc_seq); - col += cw; - if col > self.cols { + *col += cw; + if *col > self.cols { self.buffer.push('\n'); - col = cw; + *col = cw; } self.buffer.push_str(c); } } - if col == self.cols { + if *col == self.cols { self.buffer.push('\n'); - col = 0; + *col = 0; } - col } // position at the start of the prompt, clear to end of previous input @@ -366,35 +367,25 @@ impl Renderer for ConsoleRenderer { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { - let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; self.buffer.clear(); let mut col = 0; - 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); - } else { - // append the prompt - self.buffer.push_str(prompt); - // append the input line - self.buffer.push_str(line); - } + add_prompt_and_highlight(|s| { self.wrap_at_eol(s, &mut col); }, + highlighter, line, prompt); + // append hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { - self.wrap_at_eol(&highlighter.highlight_hint(hint), col); + self.wrap_at_eol(&highlighter.highlight_hint(hint), &mut col); } else { self.buffer.push_str(hint); } @@ -434,11 +425,13 @@ 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, left_margin: usize) + -> Position + { let mut pos = orig; for c in s.graphemes(true) { if c == "\n" { - pos.col = 0; + pos.col = left_margin; pos.row += 1; } else { let cw = c.width(); From 93910cadadb9b33e6066ba1b257c41a71453526d Mon Sep 17 00:00:00 2001 From: Paul Colomiets Date: Tue, 5 May 2020 16:23:20 +0300 Subject: [PATCH 2/3] Unify position calculation in unix and windows Previously the differences where: 1. Escape codes were skipped on windows 2. Tab stops weren't accounted on windows Actually windows supports both tab stops and escape codes (windows >= 10) so there is no good reason to keep the code different. --- src/edit.rs | 8 ++-- src/layout.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++- src/test/mod.rs | 4 +- src/tty/mod.rs | 51 +++++++++----------- src/tty/test.rs | 12 ++--- src/tty/unix.rs | 48 +++---------------- src/tty/windows.rs | 36 ++++---------- 7 files changed, 163 insertions(+), 112 deletions(-) diff --git a/src/edit.rs b/src/edit.rs index bb2cf042a1..fbbd2f02c8 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -60,7 +60,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(), 0); + let prompt_size = out.meter().update(prompt); let has_continuation = helper .map(|h| h.has_continuation_prompt()) .unwrap_or(false); @@ -102,9 +102,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let rc = input_state.next_cmd(rdr, self, single_esc_abort); if rc.is_err() && self.out.sigwinch() { self.out.update_size(); - self.prompt.size = self - .out - .calculate_position(self.prompt.text, Position::default(), 0); + self.prompt.size = self.out.meter().update(self.prompt.text); self.refresh_line()?; continue; } @@ -269,7 +267,7 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { let prompt = Prompt { text: prompt, - size: self.out.calculate_position(prompt, Position::default(), 0), + size: self.out.meter().update(prompt), is_default: false, has_continuation: false, }; diff --git a/src/layout.rs b/src/layout.rs index e70061eac1..cf8130372a 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,11 +1,30 @@ +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; use std::cmp::{Ord, Ordering, PartialOrd}; + #[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. } +#[derive(Debug, PartialEq, Clone)] +enum EscapeSeqState { + Initial, + EscapeChar, + BracketSequence, +} + +#[derive(Debug, Clone)] +pub struct Meter { + position: Position, + cols: usize, + tab_stop: usize, + left_margin: usize, + escape_seq_state: EscapeSeqState, +} + impl PartialOrd for Position { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -25,10 +44,105 @@ impl Ord for Position { pub struct Layout { /// Prompt Unicode/visible width and height pub prompt_size: Position, - pub left_margin: usize, pub default_prompt: bool, /// Cursor position (relative to the end of the prompt) pub cursor: Position, /// Number of rows used so far (from end of prompt to end of input) pub end: Position, } + +impl Meter { + pub fn new(cols: usize, tab_stop: usize) -> Meter { + Meter { + position: Position::default(), + cols, + tab_stop, + left_margin: 0, + escape_seq_state: EscapeSeqState::Initial, + } + } + pub fn left_margin(&mut self, value: usize) -> &mut Self { + debug_assert!(value < self.cols); + self.left_margin = value; + self + } + pub fn set_position(&mut self, pos: Position) { + self.position = pos; + } + pub fn get_position(&mut self) -> Position { + self.position + } + /// Control characters are treated as having zero width. + /// Characters with 2 column width are correctly handled (not split). + pub fn update(&mut self, text: &str) -> Position { + let mut pos = self.position; + for c in text.graphemes(true) { + if c == "\n" { + pos.row += 1; + pos.col = self.left_margin; + continue; + } + let cw = if c == "\t" { + self.tab_stop - (pos.col % self.tab_stop) + } else { + self.char_width(c) + }; + pos.col += cw; + if pos.col > self.cols { + pos.row += 1; + pos.col = cw; + } + } + if pos.col == self.cols { + pos.col = 0; + pos.row += 1; + } + if self.escape_seq_state != EscapeSeqState::Initial { + log::warn!("unfinished escape sequence in {:?}", text); + self.escape_seq_state = EscapeSeqState::Initial; + } + self.position = pos; + pos + } + // ignore ANSI escape sequence + fn char_width(&mut self, s: &str) -> usize { + use EscapeSeqState::*; + + if self.escape_seq_state == EscapeChar { + if s == "[" { + // CSI + self.escape_seq_state = BracketSequence; + } else { + // two-character sequence + self.escape_seq_state = Initial; + } + 0 + } else if self.escape_seq_state == BracketSequence { + if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') + { + /*} else if s == "m" { + // last + *esc_seq = 0;*/ + } else { + // not supported + self.escape_seq_state = Initial; + } + 0 + } else if s == "\x1b" { + self.escape_seq_state = EscapeChar; + 0 + } else if s == "\n" { + 0 + } else { + s.width() + } + } +} + +#[test] +#[ignore] +fn prompt_with_ansi_escape_codes() { + let pos = Meter::new(80, 4).update("\x1b[1;32m>>\x1b[0m "); + assert_eq!(3, pos.col); + assert_eq!(0, pos.row); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index ad7fc340ec..f0cde3962c 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::KeyPress; +use crate::layout::Meter; use crate::tty::Sink; use crate::validate::Validator; use crate::{Context, Editor, Helper, Result}; @@ -91,7 +92,8 @@ fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyPress], 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); + let expected_cursor = Meter::new(80, 8).update(expected.0); + assert_eq!(expected_cursor.col, editor.term.cursor); } // `entries`: history entries before `keys` pressed diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 151048d7f9..fe909662d6 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,15 +1,13 @@ //! This module implements and describes common TTY methods & traits - -use unicode_width::UnicodeWidthStr; - use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::edit::Prompt; use crate::highlight::{Highlighter, PromptInfo, split_highlight}; use crate::keys::KeyPress; -use crate::layout::{Layout, Position}; +use crate::layout::{Layout, Position, Meter}; use crate::line_buffer::LineBuffer; use crate::Result; + /// Terminal state pub trait RawMode: Sized { /// Disable RAW mode for the terminal. @@ -56,38 +54,32 @@ pub trait Renderer { ) -> Layout { // calculate the desired position of the cursor let pos = line.pos(); - let left_margin = if prompt.has_continuation { - prompt.size.col - } else { - 0 + let mut meter = self.meter(); + meter.set_position(prompt.size); + if prompt.has_continuation { + meter.left_margin(prompt.size.col); }; - let cursor = self.calculate_position(&line[..pos], - prompt.size, left_margin); + let cursor = meter.update(&line[..pos]); // 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, left_margin) - }; + meter.update(&line[pos..]); if let Some(info) = info { - end = self.calculate_position(&info, end, left_margin); + meter.left_margin(0); + meter.update(&info); } let new_layout = Layout { prompt_size: prompt.size, - left_margin, default_prompt: prompt.is_default, cursor, - end, + end: meter.get_position(), }; 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`. - fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) - -> Position; + fn meter(&self) -> Meter { + Meter::new(self.get_columns(), self.get_tab_stop()) + } fn write_and_flush(&self, buf: &[u8]) -> Result<()>; @@ -104,6 +96,8 @@ pub trait Renderer { fn update_size(&mut self); /// Get the number of columns in the current terminal. fn get_columns(&self) -> usize; + /// Get the tab stop in the current terminal. + fn get_tab_stop(&self) -> usize; /// Get the number of rows in the current terminal. fn get_rows(&self) -> usize; /// Check if output supports colors. @@ -132,12 +126,6 @@ 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, left_margin: usize) - -> Position - { - (**self).calculate_position(s, orig, left_margin) - } - fn write_and_flush(&self, buf: &[u8]) -> Result<()> { (**self).write_and_flush(buf) } @@ -162,6 +150,10 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { (**self).get_columns() } + fn get_tab_stop(&self) -> usize { + (**self).get_tab_stop() + } + fn get_rows(&self) -> usize { (**self).get_rows() } @@ -176,7 +168,10 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { } // ignore ANSI escape sequence +#[cfg(windows)] fn width(s: &str, esc_seq: &mut u8) -> usize { + use unicode_width::UnicodeWidthStr; + if *esc_seq == 1 { if s == "[" { // CSI diff --git a/src/tty/test.rs b/src/tty/test.rs index 7ebd94d9ad..09681a0dac 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -88,14 +88,6 @@ impl Renderer for Sink { Ok(()) } - fn calculate_position(&self, s: &str, orig: Position, _left_margin: usize) - -> Position - { - let mut pos = orig; - pos.col += s.len(); - pos - } - fn write_and_flush(&self, _: &[u8]) -> Result<()> { Ok(()) } @@ -118,6 +110,10 @@ impl Renderer for Sink { 80 } + fn get_tab_stop(&self) -> usize { + 8 + } + fn get_rows(&self) -> usize { 24 } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 58b96656e9..3bae5297aa 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -10,10 +10,9 @@ use nix::poll::{self, PollFlags}; use nix::sys::signal; use nix::sys::termios; use nix::sys::termios::SetArg; -use unicode_segmentation::UnicodeSegmentation; use utf8parse::{Parser, Receiver}; -use super::{width, RawMode, RawReader, Renderer, Term}; +use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; use crate::edit::Prompt; @@ -621,37 +620,6 @@ impl Renderer for PosixRenderer { write_and_flush(self.out, buf) } - /// 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, left_margin: usize) - -> Position - { - let mut pos = orig; - let mut esc_seq = 0; - for c in s.graphemes(true) { - if c == "\n" { - pos.row += 1; - pos.col = left_margin; - continue; - } - let cw = if c == "\t" { - self.tab_stop - (pos.col % self.tab_stop) - } else { - width(c, &mut esc_seq) - }; - pos.col += cw; - if pos.col > self.cols { - pos.row += 1; - pos.col = cw; - } - } - if pos.col == self.cols { - pos.col = 0; - pos.row += 1; - } - pos - } - fn beep(&mut self) -> Result<()> { match self.bell_style { BellStyle::Audible => { @@ -683,6 +651,10 @@ impl Renderer for PosixRenderer { self.cols } + fn get_tab_stop(&self) -> usize { + self.tab_stop + } + /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { @@ -909,14 +881,6 @@ mod test { use crate::line_buffer::LineBuffer; use crate::edit::Prompt; - #[test] - #[ignore] - fn prompt_with_ansi_escape_codes() { - let out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default()); - let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 0); - assert_eq!(3, pos.col); - assert_eq!(0, pos.row); - } #[test] fn test_unsupported_term() { @@ -945,7 +909,7 @@ mod test { let prompt = Prompt { text: "> ", is_default: true, - size: out.calculate_position("> ", Position::default(), 0), + size: Position { col: 2, row: 0 }, has_continuation: false, }; diff --git a/src/tty/windows.rs b/src/tty/windows.rs index c5bc4153fd..bcbd8de76f 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -7,7 +7,6 @@ use std::sync::atomic; use log::{debug, warn}; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE, WORD}; use winapi::um::winnt::{CHAR, HANDLE}; use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; @@ -248,6 +247,7 @@ pub struct ConsoleRenderer { buffer: String, colors_enabled: bool, bell_style: BellStyle, + tab_stop: usize, } impl ConsoleRenderer { @@ -266,6 +266,7 @@ impl ConsoleRenderer { buffer: String::with_capacity(1024), colors_enabled, bell_style, + tab_stop: 8, } } @@ -424,31 +425,6 @@ impl Renderer for ConsoleRenderer { Ok(()) } - /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) - -> Position - { - let mut pos = orig; - for c in s.graphemes(true) { - if c == "\n" { - pos.col = left_margin; - pos.row += 1; - } else { - let cw = c.width(); - pos.col += cw; - if pos.col > self.cols { - pos.row += 1; - pos.col = cw; - } - } - } - if pos.col == self.cols { - pos.col = 0; - pos.row += 1; - } - pos - } - fn beep(&mut self) -> Result<()> { match self.bell_style { BellStyle::Audible => { @@ -484,6 +460,10 @@ impl Renderer for ConsoleRenderer { self.cols } + fn get_tab_stop(&self) -> usize { + self.tab_stop + } + /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { @@ -531,6 +511,7 @@ pub struct Console { ansi_colors_supported: bool, stream_type: OutputStreamType, bell_style: BellStyle, + tab_stop: usize, } impl Console { @@ -552,7 +533,7 @@ impl Term for Console { fn new( color_mode: ColorMode, stream_type: OutputStreamType, - _tab_stop: usize, + tab_stop: usize, bell_style: BellStyle, ) -> Console { use std::ptr; @@ -587,6 +568,7 @@ impl Term for Console { ansi_colors_supported: false, stream_type, bell_style, + tab_stop, } } From 7a7355f370d4249ffef6ecf37786ca8f5329cba0 Mon Sep 17 00:00:00 2001 From: Paul Colomiets Date: Wed, 6 May 2020 18:25:12 +0300 Subject: [PATCH 3/3] Implement vertical scroll if text doesn't fit screen --- src/edit.rs | 8 +- src/highlight.rs | 8 +- src/layout.rs | 54 ++++++++++- src/test/highlight.rs | 6 +- src/tty/mod.rs | 209 +++++++++++++++++++++--------------------- src/tty/screen.rs | 139 ++++++++++++++++++++++++++++ src/tty/test.rs | 9 +- src/tty/unix.rs | 24 ++--- src/tty/windows.rs | 50 ++-------- 9 files changed, 332 insertions(+), 175 deletions(-) create mode 100644 src/tty/screen.rs diff --git a/src/edit.rs b/src/edit.rs index fbbd2f02c8..318c203fbc 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -128,11 +128,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn move_cursor(&mut self) -> Result<()> { // calculate the desired position of the cursor let new_layout = self.out.compute_layout( - &self.prompt, &self.line, None); + &self.prompt, &self.line, None, self.layout.scroll_top); if new_layout.cursor == self.layout.cursor { return Ok(()); } - if self.highlight_char() { + let scroll_changed = new_layout.scroll_top != self.layout.scroll_top; + if scroll_changed || self.highlight_char() { self.refresh_default(Info::NoHint)?; } else { self.out.move_cursor(self.layout.cursor, new_layout.cursor)?; @@ -168,7 +169,8 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { None }; - let new_layout = self.out.compute_layout(prompt, &self.line, info); + let new_layout = self.out.compute_layout(prompt, &self.line, info, + self.layout.scroll_top); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); self.out.refresh_line( diff --git a/src/highlight.rs b/src/highlight.rs index 7e018e983f..146f6dc4cd 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,6 +5,7 @@ use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; +/// Auxilliary structure that contains various data about prompt pub struct PromptInfo<'a> { pub(crate) is_default: bool, pub(crate) offset: usize, @@ -251,9 +252,11 @@ fn is_close_bracket(bracket: u8) -> bool { pub(crate) fn split_highlight<'a>(src: &'a str, offset: usize) -> (Cow<'a, str>, Cow<'a, str>) { + if offset == src.len() { + return (src.into(), "".into()); + } let mut style_buffer = String::with_capacity(32); let mut iter = src.char_indices(); - let mut non_escape_idx = 0; while let Some((idx, c)) = iter.next() { if c == '\x1b' { match iter.next() { @@ -277,7 +280,7 @@ pub(crate) fn split_highlight<'a>(src: &'a str, offset: usize) } continue; } - if non_escape_idx >= offset { + if idx >= offset { if style_buffer.is_empty() { return (src[..idx].into(), src[idx..].into()); } else { @@ -291,7 +294,6 @@ pub(crate) fn split_highlight<'a>(src: &'a str, offset: usize) return (left.into(), right.into()); } } - non_escape_idx += c.len_utf8(); } return (src.into(), "".into()); } diff --git a/src/layout.rs b/src/layout.rs index cf8130372a..ef2166f754 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,6 +1,7 @@ +use std::cmp::{Ord, Ordering, PartialOrd}; + use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use std::cmp::{Ord, Ordering, PartialOrd}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -49,6 +50,10 @@ pub struct Layout { pub cursor: Position, /// Number of rows used so far (from end of prompt to end of input) pub end: Position, + /// Number of first visible row + pub scroll_top: usize, + /// Number of visitble rows (not hidden by scroll) + pub screen_rows: usize, } impl Meter { @@ -69,9 +74,12 @@ impl Meter { pub fn set_position(&mut self, pos: Position) { self.position = pos; } - pub fn get_position(&mut self) -> Position { + pub fn get_position(&self) -> Position { self.position } + pub fn get_row(&self) -> usize { + self.position.row + } /// Control characters are treated as having zero width. /// Characters with 2 column width are correctly handled (not split). pub fn update(&mut self, text: &str) -> Position { @@ -104,6 +112,37 @@ impl Meter { self.position = pos; pos } + /// Same as update, but only updates up to a visual line be it + /// the number of columns filled or a newline character + /// + /// Returns the index of the newline character or the first character + /// that doesn't fit a line or None if whole text fits. + pub fn update_line(&mut self, text: &str) -> Option { + for (idx, c) in text.grapheme_indices(true) { + if c == "\n" { + return Some(idx); + } + let cw = if c == "\t" { + self.tab_stop - (self.position.col % self.tab_stop) + } else { + self.char_width(c) + }; + if self.position.col + cw > self.cols { + return Some(idx); + } + self.position.col += cw; + } + if self.escape_seq_state != EscapeSeqState::Initial { + log::warn!("unfinished escape sequence in {:?}", text); + self.escape_seq_state = EscapeSeqState::Initial; + } + return None; + } + /// A faster equivalent of self.update("\n"); + pub fn update_newline(&mut self) { + self.position.row += 1; + self.position.col = self.left_margin; + } // ignore ANSI escape sequence fn char_width(&mut self, s: &str) -> usize { use EscapeSeqState::*; @@ -139,6 +178,17 @@ impl Meter { } } +impl Layout { + /// Returns number of visible on the screen below the cursor + pub fn lines_below_cursor(&self) -> usize { + if self.end.row < self.screen_rows || self.screen_rows == 0 { + self.end.row - self.cursor.row + } else { + self.scroll_top + self.screen_rows - self.cursor.row - 1 + } + } +} + #[test] #[ignore] fn prompt_with_ansi_escape_codes() { diff --git a/src/test/highlight.rs b/src/test/highlight.rs index 905197adc9..5bb4a4898f 100644 --- a/src/test/highlight.rs +++ b/src/test/highlight.rs @@ -2,21 +2,21 @@ use crate::highlight::split_highlight; #[test] fn split_bold() { - let (a, b) = split_highlight("\x1b[1mword1 word2\x1b[0m", 5); + let (a, b) = split_highlight("\x1b[1mword1 word2\x1b[0m", 9); assert_eq!(a, "\x1b[1mword1\x1b[0m"); assert_eq!(b, "\x1b[1m word2\x1b[0m"); } #[test] fn split_at_the_reset() { - let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 5); + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 9); assert_eq!(a, "\x1b[1mword1\x1b[0m"); assert_eq!(b, " word2"); } #[test] fn split_nowhere() { - let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 6); + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 14); assert_eq!(a, "\x1b[1mword1\x1b[0m "); assert_eq!(b, "word2"); } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index fe909662d6..b569608d16 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,4 +1,6 @@ //! This module implements and describes common TTY methods & traits +use std::cmp::{min, max}; + use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::edit::Prompt; use crate::highlight::{Highlighter, PromptInfo, split_highlight}; @@ -7,6 +9,7 @@ use crate::layout::{Layout, Position, Meter}; use crate::line_buffer::LineBuffer; use crate::Result; +mod screen; /// Terminal state pub trait RawMode: Sized { @@ -51,6 +54,7 @@ pub trait Renderer { prompt: &Prompt, line: &LineBuffer, info: Option<&str>, + scroll_top: usize, ) -> Layout { // calculate the desired position of the cursor let pos = line.pos(); @@ -66,17 +70,109 @@ pub trait Renderer { meter.left_margin(0); meter.update(&info); } + let end = meter.get_position(); + + let screen_rows = self.get_rows(); + let scroll_top = if screen_rows <= 1 { + // Single line visible, ugly case but possible + cursor.row + } else if screen_rows > end.row { + // Whole data fits screen + 0 + } else { + let min_scroll = cursor.row.saturating_sub(screen_rows - 1); + let max_scroll = min(cursor.row, + end.row.saturating_sub(screen_rows - 1)); + max(min_scroll, min(max_scroll, scroll_top)) + }; let new_layout = Layout { prompt_size: prompt.size, default_prompt: prompt.is_default, cursor, - end: meter.get_position(), + end, + scroll_top, + screen_rows, }; debug_assert!(new_layout.cursor <= new_layout.end); new_layout } + fn render_screen(&mut self, + prompt: &Prompt, + line: &LineBuffer, + hint: Option<&str>, + new_layout: &Layout, + highlighter: Option<&dyn Highlighter>) + { + let rows = self.get_rows(); + let cols = self.get_columns(); + let tab_stop = self.get_tab_stop(); + let mut scr = screen::Screen::new(self.get_buffer(), + cols, rows, tab_stop, new_layout.scroll_top); + if let Some(highlighter) = highlighter { + if highlighter.has_continuation_prompt() { + let highlighted = highlighter.highlight(line, line.pos()); + let lines = line.split("\n"); + let mut highlighted_left = highlighted.to_string(); + let mut offset = 0; + for (line_no, orig) in lines.enumerate() { + let hl_line_len = highlighted_left.find('\n') + .map(|p| p + 1) + .unwrap_or(highlighted_left.len()); + let (hl, tail) = split_highlight(&highlighted_left, + hl_line_len); + let has_cursor = + line.pos() > offset && line.pos() <= orig.len(); + let prompt = highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset, + cursor: if has_cursor { + Some(line.pos() - offset) + } else { + None + }, + input: line, + line: orig, + line_no, + }); + scr.add_text(&prompt); + scr.add_text(&hl); + highlighted_left = tail.to_string(); + offset += orig.len() + 1; + } + } else { + scr.add_text(&highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset: 0, + cursor: Some(line.pos()), + input: line, + line: line, + line_no: 0, + })); + scr.add_text(&highlighter.highlight(line, line.pos())); + } + } else { + scr.add_text(prompt.text); + scr.add_text(line); + } + // append hint + if let Some(hint) = hint { + if let Some(highlighter) = highlighter { + scr.add_text(&highlighter.highlight_hint(hint)); + } else { + scr.add_text(hint); + } + } + if new_layout.cursor == new_layout.end && + new_layout.cursor.row > scr.get_position().row + { + scr.add_text("\n"); + } + } + fn meter(&self) -> Meter { Meter::new(self.get_columns(), self.get_tab_stop()) } @@ -102,6 +198,8 @@ pub trait Renderer { fn get_rows(&self) -> usize; /// Check if output supports colors. fn colors_enabled(&self) -> bool; + /// Returns rendering buffer. Internal. + fn get_buffer(&mut self) -> &mut String; /// Make sure prompt is at the leftmost edge of the screen fn move_cursor_at_leftmost(&mut self, rdr: &mut Self::Reader) -> Result<()>; @@ -158,6 +256,10 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { (**self).get_rows() } + fn get_buffer(&mut self) -> &mut String { + (**self).get_buffer() + } + fn colors_enabled(&self) -> bool { (**self).colors_enabled() } @@ -167,40 +269,6 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { } } -// ignore ANSI escape sequence -#[cfg(windows)] -fn width(s: &str, esc_seq: &mut u8) -> usize { - use unicode_width::UnicodeWidthStr; - - if *esc_seq == 1 { - if s == "[" { - // CSI - *esc_seq = 2; - } else { - // two-character sequence - *esc_seq = 0; - } - 0 - } else if *esc_seq == 2 { - if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { - /*} else if s == "m" { - // last - *esc_seq = 0;*/ - } else { - // not supported - *esc_seq = 0; - } - 0 - } else if s == "\x1b" { - *esc_seq = 1; - 0 - } else if s == "\n" { - 0 - } else { - s.width() - } -} - /// Terminal contract pub trait Term { type Reader: RawReader; // rl_instream @@ -228,77 +296,6 @@ pub trait Term { fn create_writer(&self) -> Self::Writer; } -fn add_prompt_and_highlight( - mut push_str: F, highlighter: Option<&dyn Highlighter>, - line: &LineBuffer, prompt: &Prompt) - where F: FnMut(&str), -{ - if let Some(highlighter) = highlighter { - if highlighter.has_continuation_prompt() { - if &line[..] == "" { - // line.lines() is an empty iterator for empty line so - // we need to treat it as a special case - let prompt = highlighter.highlight_prompt(prompt.text, - PromptInfo { - is_default: prompt.is_default, - offset: 0, - cursor: Some(0), - input: "", - line: "", - line_no: 0, - }); - push_str(&prompt); - } else { - let highlighted = highlighter.highlight(line, line.pos()); - let lines = line.split('\n'); - let mut highlighted_left = highlighted.to_string(); - let mut offset = 0; - for (line_no, orig) in lines.enumerate() { - let (hl, tail) = split_highlight(&highlighted_left, - orig.len()+1); - let has_cursor = - line.pos() > offset && line.pos() < orig.len(); - let prompt = highlighter.highlight_prompt(prompt.text, - PromptInfo { - is_default: prompt.is_default, - offset, - cursor: if has_cursor { - Some(line.pos() - offset) - } else { - None - }, - input: line, - line: orig, - line_no, - }); - push_str(&prompt); - push_str(&hl); - highlighted_left = tail.to_string(); - offset += orig.len() + 1; - } - } - } else { - // display the prompt - push_str(&highlighter.highlight_prompt(prompt.text, - PromptInfo { - is_default: prompt.is_default, - offset: 0, - cursor: Some(line.pos()), - input: line, - line: line, - line_no: 0, - })); - // display the input line - push_str(&highlighter.highlight(line, line.pos())); - } - } else { - // display the prompt - push_str(prompt.text); - // display the input line - push_str(line); - } -} - // If on Windows platform import Windows TTY module // and re-export into mod.rs scope #[cfg(all(windows, not(target_arch = "wasm32")))] diff --git a/src/tty/screen.rs b/src/tty/screen.rs new file mode 100644 index 0000000000..33f2599973 --- /dev/null +++ b/src/tty/screen.rs @@ -0,0 +1,139 @@ +use std::borrow::Cow; + +use crate::layout::{Meter, Position}; +use crate::highlight::split_highlight; + + +pub struct Screen<'a> { + buffer: &'a mut String, + meter: Meter, + rows: usize, + scroll_top: usize, +} + +impl<'a> Screen<'a> { + pub fn new(buffer: &'a mut String, + cols: usize, + rows: usize, + tab_stop: usize, + scroll_top: usize, + ) -> Screen { + Screen { + buffer, + meter: Meter::new(cols, tab_stop), + rows, + scroll_top, + } + } + fn skip_lines(&mut self, text: &str) -> usize { + let mut bytes = 0; + while self.meter.get_row() < self.scroll_top { + bytes += self.meter.update_line(&text[bytes..]) + .unwrap_or(text[bytes..].len()); + if bytes >= text.len() { + break; + } + self.meter.update_newline(); + if text[bytes..].starts_with('\n') { + bytes += 1; + } + } + return bytes; + } + pub fn get_position(&self) -> Position { + return self.meter.get_position(); + } + pub fn add_text(&mut self, text: &str) { + let max_row = self.scroll_top + self.rows; + if self.meter.get_row() >= self.scroll_top + self.rows { + return; + } + let mut text = Cow::from(text); + if self.meter.get_row() < self.scroll_top { + let skip_bytes = self.skip_lines(&text[..]); + let (_prefix, suffix) = split_highlight(&text[..], skip_bytes); + text = suffix.into_owned().into(); + } + let mut written = 0; + while written < text.len() && self.meter.get_row() < max_row { + if let Some(line) = self.meter.update_line(&text[written..]) { + self.buffer.push_str(&text[written..][..line]); + written += line; + self.meter.update_newline(); + if text[written..].starts_with('\n') { + written += 1; + } + if self.meter.get_row() >= max_row { + // break before inserting a newline + break; + } + self.buffer.push('\n'); + } else { + self.buffer.push_str(&text[written..]); + written = text.len(); + break; + } + } + if written > 0 { + let (with_colors, _) = split_highlight(&text[..], written); + if with_colors.len() > written { + // Reset highlight zero-length bytes + self.buffer.push_str(&with_colors[written..]); + } + } + } +} + + +#[test] +fn test_scroll() { + const TEXT: &str = "11111\n22222\n33333\n44444\n55555\n66666\n77777\n"; + const RESULTS:&[&str] = &[ + "11111\n22222\n33333\n44444", + "22222\n33333\n44444\n55555", + "33333\n44444\n55555\n66666", + "44444\n55555\n66666\n77777", + "55555\n66666\n77777\n", + ]; + for (scroll_top, result) in RESULTS.iter().enumerate() { + for idx in 0..TEXT.len() { + let mut buf = String::new(); + let mut scr = Screen::new(&mut buf, 10, 4, 2, scroll_top); + scr.add_text(&TEXT[..idx]); + scr.add_text(&TEXT[idx..]); + assert_eq!(&buf, result, + "scroll: {}, iteration: {}", scroll_top, idx); + } + } +} + +#[test] +fn test_scroll_prompt() { + const PROMPT: &str = "\x1b[1;32m > \x1b[0m"; + const ITEMS: &[&str] = &[ + "11111\n", + "22222\n", + "33333\n", + "44444\n", + "55555\n", + "66666\n", + "77777\n", + "", + ]; + let results = &[ + format!("{0}11111\n{0}22222\n{0}33333\n{0}44444", PROMPT), + format!("{0}22222\n{0}33333\n{0}44444\n{0}55555", PROMPT), + format!("{0}33333\n{0}44444\n{0}55555\n{0}66666", PROMPT), + format!("{0}44444\n{0}55555\n{0}66666\n{0}77777", PROMPT), + format!("{0}55555\n{0}66666\n{0}77777\n{0}", PROMPT), + ]; + for (scroll_top, result) in results.iter().enumerate() { + let mut buf = String::new(); + let mut scr = Screen::new(&mut buf, 10, 4, 2, scroll_top); + for (_, text) in ITEMS.iter().enumerate() { + scr.add_text(&PROMPT); + scr.add_text(text); + } + assert_eq!(&buf, result, "scroll: {}", scroll_top); + } +} diff --git a/src/tty/test.rs b/src/tty/test.rs index 09681a0dac..0997e9274d 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -61,11 +61,13 @@ impl RawReader for IntoIter { } } -pub struct Sink {} +pub struct Sink { + buffer: String +} impl Sink { pub fn new() -> Sink { - Sink {} + Sink { buffer: String::new() } } } @@ -117,6 +119,9 @@ impl Renderer for Sink { fn get_rows(&self) -> usize { 24 } + fn get_buffer(&mut self) -> &mut String { + &mut self.buffer + } fn colors_enabled(&self) -> bool { false diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 3bae5297aa..a4ccc574ba 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -21,7 +21,6 @@ use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; -use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; @@ -583,23 +582,14 @@ impl Renderer for PosixRenderer { let end_pos = new_layout.end; self.clear_old_rows(old_layout); + self.render_screen(prompt, line, hint, new_layout, highlighter); - add_prompt_and_highlight(|s| self.buffer.push_str(s), - highlighter, line, prompt); - // display hint - if let Some(hint) = hint { - if let Some(highlighter) = highlighter { - self.buffer.push_str(&highlighter.highlight_hint(hint)); - } else { - self.buffer.push_str(hint); - } - } // we have to generate our own newline on line wrap if end_pos.col == 0 && end_pos.row > 0 && !self.buffer.ends_with('\n') { self.buffer.push_str("\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).unwrap(); @@ -662,6 +652,10 @@ impl Renderer for PosixRenderer { rows } + fn get_buffer(&mut self) -> &mut String { + &mut self.buffer + } + fn colors_enabled(&self) -> bool { self.colors_enabled } @@ -914,19 +908,19 @@ mod test { }; let mut line = LineBuffer::init("", 0, None); - let old_layout = out.compute_layout(&prompt, &line, None); + let old_layout = out.compute_layout(&prompt, &line, None, 0); 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)); - let new_layout = out.compute_layout(&prompt, &line, None); + let new_layout = out.compute_layout(&prompt, &line, None, 0); 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) .unwrap(); #[rustfmt::skip] assert_eq!( - "\r\u{1b}[0K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", + "\r\u{1b}[0K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\na\r\u{1b}[1C", out.buffer ); } diff --git a/src/tty/windows.rs b/src/tty/windows.rs index bcbd8de76f..a66804a7e7 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -6,12 +6,11 @@ use std::mem; use std::sync::atomic; use log::{debug, warn}; -use unicode_segmentation::UnicodeSegmentation; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE, WORD}; use winapi::um::winnt::{CHAR, HANDLE}; use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; -use super::{width, RawMode, RawReader, Renderer, Term}; +use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::edit::Prompt; use crate::error; @@ -20,7 +19,6 @@ use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; -use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; @@ -294,30 +292,6 @@ impl ConsoleRenderer { set_cursor_visible(self.handle, visible) } - // 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, col: &mut usize) { - let mut esc_seq = 0; - for c in s.graphemes(true) { - if c == "\n" { - *col = 0; - self.buffer.push_str(c); - } else { - let cw = width(c, &mut esc_seq); - *col += cw; - if *col > self.cols { - self.buffer.push('\n'); - *col = cw; - } - self.buffer.push_str(c); - } - } - if *col == self.cols { - self.buffer.push('\n'); - *col = 0; - } - } - // position at the start of the prompt, clear to end of previous input fn clear_old_rows( &mut self, @@ -348,6 +322,7 @@ fn set_cursor_visible(handle: HANDLE, visible: BOOL) -> Result<()> { check(unsafe { wincon::SetConsoleCursorInfo(handle, &info) }) } + impl Renderer for ConsoleRenderer { type Reader = ConsoleRawReader; @@ -376,21 +351,10 @@ impl Renderer for ConsoleRenderer { highlighter: Option<&dyn Highlighter>, ) -> Result<()> { let cursor = new_layout.cursor; - let end_pos = new_layout.end; self.buffer.clear(); - let mut col = 0; - add_prompt_and_highlight(|s| { self.wrap_at_eol(s, &mut col); }, - highlighter, line, prompt); - - // append hint - if let Some(hint) = hint { - if let Some(highlighter) = highlighter { - self.wrap_at_eol(&highlighter.highlight_hint(hint), &mut col); - } else { - self.buffer.push_str(hint); - } - } + self.render_screen(prompt, line, hint, new_layout, highlighter); + let info = self.get_console_screen_buffer_info()?; self.set_cursor_visible(FALSE)?; // just to avoid flickering let handle = self.handle; @@ -405,7 +369,7 @@ impl Renderer for ConsoleRenderer { // position the cursor let mut coord = self.get_console_screen_buffer_info()?.dwCursorPosition; coord.X = cursor.col as i16; - coord.Y -= (end_pos.row - cursor.row) as i16; + coord.Y -= new_layout.lines_below_cursor() as i16; self.set_console_cursor_position(coord)?; Ok(()) @@ -471,6 +435,10 @@ impl Renderer for ConsoleRenderer { rows } + fn get_buffer(&mut self) -> &mut String { + &mut self.buffer + } + fn colors_enabled(&self) -> bool { self.colors_enabled }