diff --git a/examples/example.rs b/examples/example.rs index 9614fc09ea..2b3363fcd3 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -3,7 +3,7 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; -use rustyline::highlight::{Highlighter, MatchingBracketHighlighter, PromptInfo}; +use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::{Hinter, HistoryHinter}; use rustyline::validate::{self, MatchingBracketValidator, Validator}; use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress}; @@ -16,7 +16,6 @@ struct MyHelper { validator: MatchingBracketValidator, hinter: HistoryHinter, colored_prompt: String, - continuation_prompt: String, } impl Completer for MyHelper { @@ -42,23 +41,15 @@ impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - info: PromptInfo<'_>, + default: bool, ) -> Cow<'b, str> { - if info.default() { - if info.line_no() > 0 { - Borrowed(&self.continuation_prompt) - } else { - Borrowed(&self.colored_prompt) - } + if default { + Borrowed(&self.colored_prompt) } else { Borrowed(prompt) } } - fn has_continuation_prompt(&self) -> bool { - true - } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } @@ -99,8 +90,7 @@ fn main() -> rustyline::Result<()> { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, - colored_prompt: " 0> ".to_owned(), - continuation_prompt: "\x1b[1;32m...> \x1b[0m".to_owned(), + colored_prompt: "".to_owned(), validator: MatchingBracketValidator::new(), }; let mut rl = Editor::with_config(config); @@ -112,7 +102,7 @@ fn main() -> rustyline::Result<()> { } let mut count = 1; loop { - let p = format!("{:>3}> ", count); + let p = format!("{}> ", 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 0c93725a34..bc8a8a182e 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -112,7 +112,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { // calculate the desired position of the cursor let cursor = self .out - .calculate_position(&self.line[..self.line.pos()], Position::default()); + .calculate_position(&self.line[..self.line.pos()], self.prompt_size); if self.layout.cursor == cursor { return Ok(()); } @@ -123,6 +123,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { self.out.move_cursor(self.layout.cursor, cursor)?; self.layout.prompt_size = self.prompt_size; self.layout.cursor = cursor; + debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); } Ok(()) @@ -152,9 +153,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { // calculate the desired position of the cursor let pos = self.line.pos(); - let cursor = self - .out - .calculate_position(&self.line[..pos], Position::default()); + let cursor = self.out.calculate_position(&self.line[..pos], prompt_size); // calculate the position of the end of the input line let mut end = if pos == self.line.len() { cursor @@ -171,6 +170,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { cursor, end, }; + debug_assert!(new_layout.prompt_size <= new_layout.cursor); debug_assert!(new_layout.cursor <= new_layout.end); debug!(target: "rustyline", "old layout: {:?}", self.layout); @@ -336,6 +336,7 @@ 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(); diff --git a/src/highlight.rs b/src/highlight.rs index 07475f0fd2..5349a07e43 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,15 +5,6 @@ use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; -pub struct PromptInfo<'a> { - pub(crate) 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). @@ -35,18 +26,11 @@ pub trait Highlighter { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - info: PromptInfo<'_>, + default: bool, ) -> Cow<'b, str> { - let _ = info; + let _ = default; 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> { @@ -85,9 +69,9 @@ impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - info: PromptInfo<'_>, + default: bool, ) -> Cow<'b, str> { - (**self).highlight_prompt(prompt, info) + (**self).highlight_prompt(prompt, default) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { @@ -246,86 +230,6 @@ fn is_close_bracket(bracket: u8) -> bool { memchr(bracket, CLOSES).is_some() } -pub(crate) fn split_highlight(src: &str, offset: usize) -> (Cow<'_, str>, Cow<'_, 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(); - } - (src.into(), "".into()) -} - -impl PromptInfo<'_> { - /// Returns true if this is the default prompt - pub fn default(&self) -> bool { - self.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 3488588588..6e317c3064 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -26,8 +26,8 @@ pub struct Layout { /// Prompt Unicode/visible width and height pub prompt_size: Position, pub default_prompt: bool, - /// Cursor position (relative to the end of the prompt) + /// Cursor position (relative to the start of the prompt) pub cursor: Position, - /// Number of rows used so far (from end of prompt to end of input) + /// Number of rows used so far (from start of prompt to end of input) pub end: Position, } diff --git a/src/test/highlight.rs b/src/test/highlight.rs deleted file mode 100644 index 905197adc9..0000000000 --- a/src/test/highlight.rs +++ /dev/null @@ -1,22 +0,0 @@ -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 3ed6e8c274..365fe91a8b 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -15,7 +15,6 @@ use crate::{Context, Editor, Helper, Result}; mod common; mod emacs; -mod highlight; mod history; mod vi_cmd; mod vi_insert; diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 51fe160bb7..3f6097ef23 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -156,102 +156,6 @@ pub trait Term { fn create_writer(&self) -> Self::Writer; } -#[cfg(not(any(test, target_arch = "wasm32")))] -fn add_prompt_and_highlight( - buffer: &mut String, - highlighter: Option<&dyn Highlighter>, - line: &LineBuffer, - prompt: &str, - default_prompt: bool, - layout: &Layout, - cursor: &mut Position, -) { - use crate::highlight::{split_highlight, PromptInfo}; - - 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, - PromptInfo { - default: default_prompt, - offset: 0, - cursor: Some(0), - input: "", - line: "", - line_no: 0, - }, - ); - buffer.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 prompt = highlighter.highlight_prompt( - prompt, - PromptInfo { - default: default_prompt, - offset, - cursor: if line.pos() > offset && line.pos() < orig.len() { - Some(line.pos() - offset) - } else { - None - }, - input: line, - line: orig, - line_no, - }, - ); - buffer.push_str(&prompt); - buffer.push_str(&hl); - highlighted_left = tail.to_string(); - offset += orig.len() + 1; - } - } - cursor.col += layout.prompt_size.col; - } else { - // display the prompt - buffer.push_str(&highlighter.highlight_prompt( - prompt, - PromptInfo { - default: default_prompt, - offset: 0, - cursor: Some(line.pos()), - input: line, - line, - line_no: 0, - }, - )); - // display the input line - buffer.push_str(&highlighter.highlight(line, line.pos())); - // we have to generate our own newline on line wrap - if layout.end.col == 0 && layout.end.row > 0 && !buffer.ends_with('\n') { - buffer.push_str("\n"); - } - if cursor.row == 0 { - cursor.col += layout.prompt_size.col; - } - } - } else { - // display the prompt - buffer.push_str(prompt); - // display the input line - buffer.push_str(line); - // we have to generate our own newline on line wrap - if layout.end.col == 0 && layout.end.row > 0 && !buffer.ends_with('\n') { - buffer.push_str("\n"); - } - if cursor.row == 0 { - cursor.col += layout.prompt_size.col; - } - } -} - cfg_if::cfg_if! { if #[cfg(any(test, target_arch = "wasm32"))] { mod test; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 80c45437d7..a39a0bde14 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -21,7 +21,6 @@ use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::tty::add_prompt_and_highlight; use crate::Result; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; @@ -542,7 +541,7 @@ impl Renderer for PosixRenderer { self.buffer.clear(); let default_prompt = new_layout.default_prompt; - let mut cursor = new_layout.cursor; + let cursor = new_layout.cursor; let end_pos = new_layout.end; let current_row = old_layout.cursor.row; let old_rows = old_layout.end.row; @@ -561,15 +560,19 @@ impl Renderer for PosixRenderer { // clear the line self.buffer.push_str("\r\x1b[0K"); - add_prompt_and_highlight( - &mut self.buffer, - highlighter, - line, - prompt, - default_prompt, - &new_layout, - &mut cursor, - ); + 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); + } // display hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { @@ -578,6 +581,10 @@ impl Renderer for PosixRenderer { 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; // move the cursor up as required @@ -585,10 +592,10 @@ impl Renderer for PosixRenderer { write!(self.buffer, "\x1b[{}A", new_cursor_row_movement).unwrap(); } // position the cursor within the line - if cursor.col == 0 { - self.buffer.push('\r'); - } else { + if cursor.col > 0 { write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap(); + } else { + self.buffer.push('\r'); } self.write_and_flush(self.buffer.as_bytes())?; diff --git a/src/tty/windows.rs b/src/tty/windows.rs index f4f852d903..302504ecc6 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -16,7 +16,6 @@ use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::tty::add_prompt_and_highlight; use crate::Result; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; @@ -327,22 +326,26 @@ impl Renderer for ConsoleRenderer { highlighter: Option<&dyn Highlighter>, ) -> Result<()> { let default_prompt = new_layout.default_prompt; - let mut cursor = new_layout.cursor; + let cursor = new_layout.cursor; let end_pos = new_layout.end; let current_row = old_layout.cursor.row; let old_rows = old_layout.end.row; self.buffer.clear(); - add_prompt_and_highlight( - &mut self.buffer, - highlighter, - line, - prompt, - default_prompt, - &new_layout, - &mut cursor, - ); - + if let Some(highlighter) = highlighter { + // TODO handle ansi escape code (SetConsoleTextAttribute) + // append the prompt + self.buffer + .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); + // append the input line + self.buffer + .push_str(&highlighter.highlight(line, line.pos())); + } else { + // append the prompt + self.buffer.push_str(prompt); + // append the input line + self.buffer.push_str(line); + } // append hint if let Some(hint) = hint { if let Some(highlighter) = highlighter {