diff --git a/Cargo.toml b/Cargo.toml index 92b4cd953..d3f87d557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyline" -version = "14.0.0" +version = "15.0.0" authors = ["Katsu Kawakami "] edition = "2021" description = "Rustyline, a readline implementation based on Antirez's Linenoise" @@ -11,10 +11,7 @@ keywords = ["readline"] license = "MIT" categories = ["command-line-interface"] -exclude = [ - "/.github/*", - "/rustfmt.toml", -] +exclude = ["/.github/*", "/rustfmt.toml"] [badges] maintenance = { status = "actively-developed" } @@ -29,20 +26,29 @@ cfg-if = "1.0" home = { version = "0.5.4", optional = true } # For History fd-lock = { version = "4.0.0", optional = true } -rusqlite = { version = "0.32.0", optional = true, default-features = false, features = ["bundled", "backup"] } +rusqlite = { version = "0.32.0", optional = true, default-features = false, features = [ + "bundled", + "backup", +] } libc = "0.2.155" log = "0.4.22" -unicode-width = "0.1.13" +unicode-width = "0.2.0" unicode-segmentation = "1.0" memchr = "2.7" # For custom bindings radix_trie = { version = "0.2", optional = true } regex = { version = "1.10", optional = true } # For derive -rustyline-derive = { version = "0.10.0", optional = true, path = "rustyline-derive" } +rustyline-derive = { version = "0.11.0", optional = true, path = "rustyline-derive" } [target.'cfg(unix)'.dependencies] -nix = { version = "0.29", default-features = false, features = ["fs", "ioctl", "poll", "signal", "term"] } +nix = { version = "0.29", default-features = false, features = [ + "fs", + "ioctl", + "poll", + "signal", + "term", +] } utf8parse = "0.2" skim = { version = "0.10", optional = true, default-features = false } signal-hook = { version = "0.3", optional = true, default-features = false } @@ -50,7 +56,13 @@ termios = { version = "0.3.3", optional = true } buffer-redux = { version = "1.0", optional = true, default-features = false } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_System_Console", "Win32_Security", "Win32_System_Threading", "Win32_UI_Input_KeyboardAndMouse"] } +windows-sys = { version = "0.59.0", features = [ + "Win32_Foundation", + "Win32_System_Console", + "Win32_Security", + "Win32_System_Threading", + "Win32_UI_Input_KeyboardAndMouse", +] } clipboard-win = "5.0" [dev-dependencies] @@ -96,7 +108,13 @@ name = "sqlite_history" required-features = ["with-sqlite-history"] [package.metadata.docs.rs] -features = ["custom-bindings", "derive", "with-dirs", "with-file-history", "with-fuzzy"] +features = [ + "custom-bindings", + "derive", + "with-dirs", + "with-file-history", + "with-fuzzy", +] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/README.md b/README.md index 47d4e1b79..f95d14020 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ to your `Cargo.toml`: ```toml [dependencies] -rustyline = "14.0.0" +rustyline = "15.0.0" ``` ## Features @@ -188,7 +188,7 @@ For all modes: [Readline vi Editing Mode Cheat Sheet](http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf) -[Terminal codes (ANSI/VT100)](http://wiki.bash-hackers.org/scripting/terminalcodes) +[ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code) ## Wine @@ -252,3 +252,7 @@ literal newline to be added to the input buffer. The way to achieve multi-line editing is to implement the `Validator` trait. + +## Minimum supported Rust version (MSRV) + +Latest stable Rust version at the time of release. It might compile with older versions. diff --git a/examples/example.rs b/examples/example.rs index cc3e2a0cc..d258064c6 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -2,7 +2,7 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; -use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::highlight::{CmdKind, Highlighter, MatchingBracketHighlighter}; use rustyline::hint::HistoryHinter; use rustyline::validate::MatchingBracketValidator; use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent}; @@ -41,8 +41,8 @@ impl Highlighter for MyHelper { self.highlighter.highlight(line, pos) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - self.highlighter.highlight_char(line, pos, forced) + fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool { + self.highlighter.highlight_char(line, pos, kind) } } diff --git a/examples/read_password.rs b/examples/read_password.rs index d669d511b..d93418ae2 100644 --- a/examples/read_password.rs +++ b/examples/read_password.rs @@ -1,7 +1,7 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::config::Configurer; -use rustyline::highlight::Highlighter; +use rustyline::highlight::{CmdKind, Highlighter}; use rustyline::{ColorMode, Editor, Result}; use rustyline::{Completer, Helper, Hinter, Validator}; @@ -20,12 +20,16 @@ impl Highlighter for MaskingHighlighter { } } - fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool { - self.masking + fn highlight_char(&self, _line: &str, _pos: usize, kind: CmdKind) -> bool { + match kind { + CmdKind::MoveCursor => false, + _ => self.masking, + } } } fn main() -> Result<()> { + env_logger::init(); println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; let mut rl = Editor::new()?; diff --git a/rustyline-derive/Cargo.toml b/rustyline-derive/Cargo.toml index d1596578c..6713b789c 100644 --- a/rustyline-derive/Cargo.toml +++ b/rustyline-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyline-derive" -version = "0.10.0" +version = "0.11.0" authors = ["gwenn"] edition = "2018" description = "Rustyline macros implementation of #[derive(Completer, Helper, Hinter, Highlighter)]" @@ -19,6 +19,11 @@ maintenance = { status = "actively-developed" } proc-macro = true [dependencies] -syn = { version = "2.0.72", default-features = false, features = ["derive", "parsing", "printing", "proc-macro"] } +syn = { version = "2.0.72", default-features = false, features = [ + "derive", + "parsing", + "printing", + "proc-macro", +] } quote = { version = "1.0.36", default-features = false } proc-macro2 = { version = "1.0.86", default-features = false } diff --git a/rustyline-derive/src/lib.rs b/rustyline-derive/src/lib.rs index c25e6165b..e7082ab6d 100644 --- a/rustyline-derive/src/lib.rs +++ b/rustyline-derive/src/lib.rs @@ -10,7 +10,7 @@ fn get_field_by_attr<'a>(data: &'a Data, ident: &str) -> Option<(usize, &'a Fiel attr.path().is_ident("rustyline") && attr .parse_args::() - .map_or(false, |arg| arg.is_ident(ident)) + .is_ok_and(|arg| arg.is_ident(ident)) }) }); @@ -126,8 +126,8 @@ pub fn highlighter_macro_derive(input: TokenStream) -> TokenStream { ::rustyline::highlight::Highlighter::highlight_candidate(&self.#field_name_or_index, candidate, completion) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - ::rustyline::highlight::Highlighter::highlight_char(&self.#field_name_or_index, line, pos, forced) + fn highlight_char(&self, line: &str, pos: usize, kind: ::rustyline::highlight::CmdKind) -> bool { + ::rustyline::highlight::Highlighter::highlight_char(&self.#field_name_or_index, line, pos, kind) } } } diff --git a/src/command.rs b/src/command.rs index f0185e2cb..4eb215134 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2,6 +2,7 @@ use crate::complete_hint_line; use crate::config::Config; use crate::edit::State; use crate::error; +use crate::highlight::CmdKind; use crate::history::SearchDirection; use crate::keymap::{Anchor, At, Cmd, Movement, Word}; use crate::keymap::{InputState, Refresher}; @@ -28,9 +29,7 @@ pub fn execute( if s.has_hint() || !s.is_default_prompt() || s.highlight_char { // Force a refresh without hints to leave the previous // line as the user typed it after a newline. - s.forced_refresh = true; - s.refresh_line_with_msg(None)?; - s.forced_refresh = false; + s.refresh_line_with_msg(None, CmdKind::ForcedRefresh)?; } } _ => {} @@ -190,7 +189,7 @@ pub fn execute( } Cmd::Move(Movement::EndOfBuffer) => { // Move to the end of the buffer. - s.edit_move_buffer_end()?; + s.edit_move_buffer_end(CmdKind::MoveCursor)?; } Cmd::DowncaseWord => { // lowercase word after point diff --git a/src/completion.rs b/src/completion.rs index f34befb4d..9566f7845 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -14,44 +14,13 @@ pub trait Candidate { fn replacement(&self) -> &str; } -impl Candidate for String { +impl> Candidate for T { fn display(&self) -> &str { - self.as_str() + self.as_ref() } fn replacement(&self) -> &str { - self.as_str() - } -} - -/// #[deprecated = "Unusable"] -impl Candidate for str { - fn display(&self) -> &str { - self - } - - fn replacement(&self) -> &str { - self - } -} - -impl Candidate for &'_ str { - fn display(&self) -> &str { - self - } - - fn replacement(&self) -> &str { - self - } -} - -impl Candidate for Rc { - fn display(&self) -> &str { - self - } - - fn replacement(&self) -> &str { - self + self.as_ref() } } @@ -113,22 +82,6 @@ impl Completer for () { } } -impl<'c, C: ?Sized + Completer> Completer for &'c C { - type Candidate = C::Candidate; - - fn complete( - &self, - line: &str, - pos: usize, - ctx: &Context<'_>, - ) -> Result<(usize, Vec)> { - (**self).complete(line, pos, ctx) - } - - fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { - (**self).update(line, start, elected, cl); - } -} macro_rules! box_completer { ($($id: ident)*) => { $( @@ -211,7 +164,6 @@ impl FilenameCompleter { /// partial path to be completed. pub fn complete_path(&self, line: &str, pos: usize) -> Result<(usize, Vec)> { let (start, mut matches) = self.complete_path_unsorted(line, pos)?; - #[allow(clippy::unnecessary_sort_by)] matches.sort_by(|a, b| a.display().cmp(b.display())); Ok((start, matches)) } @@ -415,6 +367,7 @@ fn normalize(s: &str) -> Cow { /// Given a `line` and a cursor `pos`ition, /// try to find backward the start of a word. +/// /// Return (0, `line[..pos]`) if no break char has been found. /// Return the word and its start position (idx, `line[idx..pos]`) otherwise. #[must_use] @@ -543,6 +496,8 @@ fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> { #[cfg(test)] mod tests { + use super::{Completer, FilenameCompleter}; + #[test] pub fn extract_word() { let break_chars = super::default_break_chars; @@ -647,4 +602,27 @@ mod tests { pub fn normalize() { assert_eq!(super::normalize("Windows"), "windows") } + + #[test] + pub fn candidate_impls() { + struct StrCmp; + impl Completer for StrCmp { + type Candidate = &'static str; + } + struct RcCmp; + impl Completer for RcCmp { + type Candidate = std::rc::Rc; + } + struct ArcCmp; + impl Completer for ArcCmp { + type Candidate = std::sync::Arc; + } + } + + #[test] + pub fn completer_impls() { + struct Wrapper(T); + let boxed = Box::new(FilenameCompleter::new()); + let _ = Wrapper(boxed); + } } diff --git a/src/config.rs b/src/config.rs index a4c3d12bc..b0092dcce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,9 +28,9 @@ pub struct Config { /// Whether to use stdio or not behavior: Behavior, /// Horizontal space taken by a tab. - tab_stop: usize, + tab_stop: u8, /// Indentation size for indent/dedent commands - indent_size: usize, + indent_size: u8, /// Check if cursor position is at leftmost before displaying prompt check_cursor_position: bool, /// Bracketed paste on unix platform @@ -160,11 +160,11 @@ impl Config { /// /// By default, 8. #[must_use] - pub fn tab_stop(&self) -> usize { + pub fn tab_stop(&self) -> u8 { self.tab_stop } - pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) { + pub(crate) fn set_tab_stop(&mut self, tab_stop: u8) { self.tab_stop = tab_stop; } @@ -180,11 +180,11 @@ impl Config { /// /// By default, 2. #[must_use] - pub fn indent_size(&self) -> usize { + pub fn indent_size(&self) -> u8 { self.indent_size } - pub(crate) fn set_indent_size(&mut self, indent_size: usize) { + pub(crate) fn set_indent_size(&mut self, indent_size: u8) { self.indent_size = indent_size; } @@ -433,7 +433,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: u8) -> Self { self.set_tab_stop(tab_stop); self } @@ -451,7 +451,7 @@ impl Builder { /// /// By default, `2` #[must_use] - pub fn indent_size(mut self, indent_size: usize) -> Self { + pub fn indent_size(mut self, indent_size: u8) -> Self { self.set_indent_size(indent_size); self } @@ -568,7 +568,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: u8) { self.config_mut().set_tab_stop(tab_stop); } @@ -581,7 +581,7 @@ pub trait Configurer { /// Indentation size for indent/dedent commands /// /// By default, `2` - fn set_indent_size(&mut self, size: usize) { + fn set_indent_size(&mut self, size: u8) { self.config_mut().set_indent_size(size); } diff --git a/src/edit.rs b/src/edit.rs index c231e4a42..9d85ab6b5 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -3,16 +3,15 @@ use log::debug; use std::fmt; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::error::ReadlineError; -use crate::highlight::Highlighter; +use crate::highlight::{CmdKind, Highlighter}; 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::{cwidh, Layout, Position}; use crate::line_buffer::{ ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE, }; @@ -36,7 +35,6 @@ pub struct State<'out, 'prompt, H: Helper> { pub ctx: Context<'out>, // Give access to history for `hinter` pub hint: Option>, // last hint displayed pub highlight_char: bool, // `true` if a char has been highlighted - pub forced_refresh: bool, // `true` if line is redraw without hint or highlight_char } enum Info<'m> { @@ -66,7 +64,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { ctx, hint: None, highlight_char: false, - forced_refresh: false, } } @@ -122,7 +119,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { ); } - pub fn move_cursor(&mut self) -> Result<()> { + pub fn move_cursor(&mut self, kind: CmdKind) -> Result<()> { // calculate the desired position of the cursor let cursor = self .out @@ -130,7 +127,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { if self.layout.cursor == cursor { return Ok(()); } - if self.highlight_char() { + if self.highlight_char(kind) { let prompt_size = self.prompt_size; self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; } else { @@ -205,10 +202,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } - fn highlight_char(&mut self) -> bool { + fn highlight_char(&mut self, kind: CmdKind) -> bool { if let Some(highlighter) = self.highlighter() { - let highlight_char = - highlighter.highlight_char(&self.line, self.line.pos(), self.forced_refresh); + let highlight_char = highlighter.highlight_char(&self.line, self.line.pos(), kind); if highlight_char { self.highlight_char = true; true @@ -240,12 +236,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { if corrected || self.has_hint() || msg.is_some() { // Force a refresh without hints to leave the previous // line as the user typed it after a newline. - self.refresh_line_with_msg(msg.as_deref())?; + self.refresh_line_with_msg(msg.as_deref(), CmdKind::ForcedRefresh)?; } } ValidationResult::Invalid(ref msg) => { if corrected || self.has_hint() || msg.is_some() { - self.refresh_line_with_msg(msg.as_deref())?; + self.refresh_line_with_msg(msg.as_deref(), CmdKind::Other)?; } } } @@ -256,31 +252,31 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { +impl Invoke for State<'_, '_, H> { fn input(&self) -> &str { self.line.as_str() } } -impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { +impl Refresher for State<'_, '_, H> { fn refresh_line(&mut self) -> Result<()> { let prompt_size = self.prompt_size; self.hint(); - self.highlight_char(); + self.highlight_char(CmdKind::Other); self.refresh(self.prompt, prompt_size, true, Info::Hint) } - fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { + fn refresh_line_with_msg(&mut self, msg: Option<&str>, kind: CmdKind) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; - self.highlight_char(); + self.highlight_char(kind); self.refresh(self.prompt, prompt_size, true, Info::Msg(msg)) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { let prompt_size = self.out.calculate_position(prompt, Position::default()); self.hint(); - self.highlight_char(); + self.highlight_char(CmdKind::Other); self.refresh(prompt, prompt_size, false, Info::Hint) } @@ -328,7 +324,7 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { +impl fmt::Debug for State<'_, '_, H> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("State") .field("prompt", &self.prompt) @@ -341,7 +337,7 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { +impl State<'_, '_, H> { pub fn clear_screen(&mut self) -> Result<()> { self.out.clear_screen()?; self.layout.cursor = Position::default(); @@ -356,12 +352,12 @@ 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 = cwidh(ch); if n == 1 && width != 0 // Ctrl-V + \t or \n ... && self.layout.cursor.col + width < self.out.get_columns() && (self.hint.is_none() && no_previous_hint) // TODO refresh only current line - && !self.highlight_char() + && !self.highlight_char(CmdKind::Other) { // Avoid a full update of the line in the trivial case. self.layout.cursor.col += width; @@ -385,7 +381,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_replace_char(&mut self, ch: char, n: RepeatCount) -> Result<()> { self.changes.begin(); let succeed = if let Some(chars) = self.line.delete(n, &mut self.changes) { - let count = chars.graphemes(true).count(); + let count = RepeatCount::try_from(chars.graphemes(true).count()).unwrap(); self.line.insert(ch, count, &mut self.changes); self.line.move_backward(1); true @@ -454,7 +450,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Move cursor on the left. pub fn edit_move_backward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_backward(n) { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -463,7 +459,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Move cursor on the right. pub fn edit_move_forward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_forward(n) { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -472,7 +468,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Move cursor to the start of the line. pub fn edit_move_home(&mut self) -> Result<()> { if self.line.move_home() { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -481,7 +477,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Move cursor to the end of the line. pub fn edit_move_end(&mut self) -> Result<()> { if self.line.move_end() { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -490,16 +486,16 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Move cursor to the start of the buffer. pub fn edit_move_buffer_start(&mut self) -> Result<()> { if self.line.move_buffer_start() { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } } /// Move cursor to the end of the buffer. - pub fn edit_move_buffer_end(&mut self) -> Result<()> { + pub fn edit_move_buffer_end(&mut self, kind: CmdKind) -> Result<()> { if self.line.move_buffer_end() { - self.move_cursor() + self.move_cursor(kind) } else { Ok(()) } @@ -565,7 +561,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_prev_word(word_def, n) { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -573,7 +569,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_next_word(at, word_def, n) { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -581,8 +577,8 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Moves the cursor to the same column in the line above pub fn edit_move_line_up(&mut self, n: RepeatCount) -> Result { - if self.line.move_to_line_up(n) { - self.move_cursor()?; + if self.line.move_to_line_up(n, &self.layout) { + self.move_cursor(CmdKind::MoveCursor)?; Ok(true) } else { Ok(false) @@ -591,8 +587,8 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Moves the cursor to the same column in the line above pub fn edit_move_line_down(&mut self, n: RepeatCount) -> Result { - if self.line.move_to_line_down(n) { - self.move_cursor()?; + if self.line.move_to_line_down(n, &self.layout) { + self.move_cursor(CmdKind::MoveCursor)?; Ok(true) } else { Ok(false) @@ -601,7 +597,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_move_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { if self.line.move_to(cs, n) { - self.move_cursor() + self.move_cursor(CmdKind::MoveCursor) } else { Ok(()) } @@ -735,7 +731,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } /// Change the indentation of the lines covered by movement - pub fn edit_indent(&mut self, mvt: &Movement, amount: usize, dedent: bool) -> Result<()> { + pub fn edit_indent(&mut self, mvt: &Movement, amount: u8, dedent: bool) -> Result<()> { if self.line.indent(mvt, amount, dedent, &mut self.changes) { self.refresh_line() } else { @@ -765,7 +761,6 @@ pub fn init_state<'out, H: Helper>( ctx: Context::new(history), hint: Some(Box::new("hint".to_owned())), highlight_char: false, - forced_refresh: false, } } diff --git a/src/error.rs b/src/error.rs index b75101199..038ae4b2e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,7 +8,7 @@ use std::io; /// The error type for Rustyline errors that can arise from /// I/O related errors or Errno when using the nix-rust library // #[non_exhaustive] -#[allow(clippy::module_name_repetitions)] +#[expect(clippy::module_name_repetitions)] #[derive(Debug)] #[non_exhaustive] pub enum ReadlineError { diff --git a/src/highlight.rs b/src/highlight.rs index d5c0e8967..6cc718a07 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -4,9 +4,19 @@ use crate::config::CompletionType; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; +/// Describe which kind of action has been triggering the call to `Highlighter`. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CmdKind { + /// Cursor moved + MoveCursor, + /// Other action + Other, + /// Forced / final refresh (no auto-suggestion / hint, no matching bracket + /// highlighted, ...) + ForcedRefresh, +} + /// 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). /// /// Currently, the highlighted version *must* have the same display width as /// the original input. @@ -49,49 +59,17 @@ pub trait Highlighter { } /// Tells if `line` needs to be highlighted when a specific char is typed or /// when cursor is moved under a specific char. - /// `forced` flag is `true` mainly when user presses Enter (i.e. transient - /// vs final highlight). /// /// Used to optimize refresh when a character is inserted or the cursor is /// moved. - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - let _ = (line, pos, forced); + fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool { + let _ = (line, pos, kind); false } } impl Highlighter for () {} -impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - (**self).highlight(line, pos) - } - - fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, - prompt: &'p str, - default: bool, - ) -> Cow<'b, str> { - (**self).highlight_prompt(prompt, default) - } - - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - (**self).highlight_hint(hint) - } - - fn highlight_candidate<'c>( - &self, - candidate: &'c str, - completion: CompletionType, - ) -> Cow<'c, str> { - (**self).highlight_candidate(candidate, completion) - } - - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - (**self).highlight_char(line, pos, forced) - } -} - // TODO versus https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=HighlightMatchingBracketProcessor#prompt_toolkit.layout.processors.HighlightMatchingBracketProcessor /// Highlight matching bracket when typed or cursor moved on. @@ -126,8 +104,8 @@ impl Highlighter for MatchingBracketHighlighter { Borrowed(line) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - if forced { + fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool { + if kind == CmdKind::ForcedRefresh { self.bracket.set(None); return false; } diff --git a/src/hint.rs b/src/hint.rs index efff9357c..5543f2e8c 100644 --- a/src/hint.rs +++ b/src/hint.rs @@ -40,14 +40,6 @@ impl Hinter for () { type Hint = String; } -impl<'r, H: ?Sized + Hinter> Hinter for &'r H { - type Hint = H::Hint; - - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { - (**self).hint(line, pos, ctx) - } -} - /// Add suggestion based on previous history entries matching current user /// input. #[derive(Default)] diff --git a/src/history.rs b/src/history.rs index 0dc289db7..51abe637f 100644 --- a/src/history.rs +++ b/src/history.rs @@ -69,8 +69,14 @@ pub trait History { // reedline: fn append(&mut self, entry: &str); /// Add a new entry in the history. + /// + /// Return false if the `line` has been ignored (blank line / duplicate / + /// ...). fn add(&mut self, line: &str) -> Result; /// Add a new entry in the history. + /// + /// Return false if the `line` has been ignored (blank line / duplicate / + /// ...). fn add_owned(&mut self, line: String) -> Result; // TODO check AsRef + Into vs object safe /// Return the number of entries in the history. @@ -253,7 +259,7 @@ impl MemHistory { return true; } if line.is_empty() - || (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace)) + || (self.ignore_space && line.chars().next().is_none_or(char::is_whitespace)) { return true; } diff --git a/src/keymap.rs b/src/keymap.rs index 784c91016..8c29f89ef 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -2,6 +2,7 @@ use log::debug; use super::Result; +use crate::highlight::CmdKind; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::tty::{self, RawReader, Term, Terminal}; use crate::{Config, EditMode}; @@ -9,7 +10,7 @@ use crate::{Config, EditMode}; use crate::{Event, EventContext, EventHandler}; /// The number of times one command should be repeated. -pub type RepeatCount = usize; +pub type RepeatCount = u16; /// Commands #[derive(Debug, Clone, Eq, PartialEq)] @@ -131,7 +132,6 @@ impl Cmd { /// Tells if current command should reset kill ring. #[must_use] pub const fn should_reset_kill_ring(&self) -> bool { - #[allow(clippy::match_same_arms)] match *self { Self::Kill(Movement::BackwardChar(_) | Movement::ForwardChar(_)) => true, Self::ClearScreen @@ -183,7 +183,10 @@ impl Cmd { let last_insert = wrt.last_insert(); if let Movement::ForwardChar(0) = mvt { Self::Replace( - Movement::ForwardChar(last_insert.as_ref().map_or(0, String::len)), + Movement::ForwardChar( + RepeatCount::try_from(last_insert.as_ref().map_or(0, String::len)) + .unwrap(), + ), last_insert, ) } else { @@ -347,7 +350,7 @@ pub enum InputMode { /// Transform key(s) to commands based on current input mode pub struct InputState<'b> { pub(crate) mode: EditMode, - #[cfg_attr(not(feature = "custom-bindings"), allow(dead_code))] + #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))] custom_bindings: &'b Bindings, pub(crate) input_mode: InputMode, // vi only ? // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 @@ -375,7 +378,7 @@ pub trait Refresher { /// cursor position, and number of columns of the terminal. fn refresh_line(&mut self) -> Result<()>; /// Same as [`refresh_line`] with a specific message instead of hint - fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()>; + fn refresh_line_with_msg(&mut self, msg: Option<&str>, kind: CmdKind) -> Result<()>; /// Same as `refresh_line` but with a dynamic prompt. fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>; /// Vi only, switch to insert mode. @@ -389,12 +392,12 @@ pub trait Refresher { /// Returns `true` if there is a hint displayed. fn has_hint(&self) -> bool; /// Returns the hint text that is shown after the current cursor position. - #[cfg_attr(not(feature = "custom-bindings"), allow(dead_code))] + #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))] fn hint_text(&self) -> Option<&str>; /// currently edited line fn line(&self) -> &str; /// Current cursor position (byte position) - #[cfg_attr(not(feature = "custom-bindings"), allow(dead_code))] + #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))] fn pos(&self) -> usize; /// Display `msg` above currently edited line. fn external_print(&mut self, msg: String) -> Result<()>; @@ -441,6 +444,8 @@ impl<'b> InputState<'b> { tty::Event::ExternalPrint(msg) => { wrt.external_print(msg)?; } + #[cfg(target_os = "macos")] + _ => {} } } } @@ -474,7 +479,7 @@ impl<'b> InputState<'b> { wrt: &mut dyn Refresher, digit: char, ) -> Result { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] match digit { '0'..='9' => { self.num_args = digit.to_digit(10).unwrap() as i16; @@ -487,7 +492,7 @@ impl<'b> InputState<'b> { loop { wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?; let key = rdr.next_key(true)?; - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] match key { E(K::Char(digit @ '0'..='9'), m) if m == M::NONE || m == M::ALT => { if self.num_args == -1 { @@ -657,7 +662,7 @@ impl<'b> InputState<'b> { Ok(cmd) } - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn vi_arg_digit( &mut self, rdr: &mut R, @@ -911,7 +916,6 @@ impl<'b> InputState<'b> { }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { - #[allow(clippy::if_same_then_else)] if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // replacing... } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { @@ -1101,7 +1105,7 @@ impl<'b> InputState<'b> { num_args } - #[allow(clippy::cast_sign_loss)] + #[expect(clippy::cast_sign_loss)] fn emacs_num_args(&mut self) -> (RepeatCount, bool) { let num_args = self.num_args(); if num_args < 0 { @@ -1115,7 +1119,6 @@ impl<'b> InputState<'b> { } } - #[allow(clippy::cast_sign_loss)] fn vi_num_args(&mut self) -> RepeatCount { let num_args = self.num_args(); if num_args < 0 { @@ -1127,7 +1130,7 @@ impl<'b> InputState<'b> { } #[cfg(feature = "custom-bindings")] -impl<'b> InputState<'b> { +impl InputState<'_> { /// Application customized binding fn custom_binding( &self, diff --git a/src/keys.rs b/src/keys.rs index b8c378628..7135d385d 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -24,7 +24,6 @@ impl KeyEvent { } return E(K::Char(c), mods); } - #[allow(clippy::match_same_arms)] match c { '\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' '\x01' => E(K::Char('A'), mods | M::CTRL), diff --git a/src/layout.rs b/src/layout.rs index 5aa8834fa..ba8d603f7 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,9 +1,22 @@ use std::cmp::Ordering; +/// Height, width +pub type Unit = u16; +/// Character width / number of columns +pub(crate) fn cwidh(c: char) -> Unit { + use unicode_width::UnicodeWidthChar; + Unit::try_from(c.width().unwrap_or(0)).unwrap() +} +/// String width / number of columns +pub(crate) fn swidth(s: &str) -> Unit { + use unicode_width::UnicodeWidthStr; + Unit::try_from(s.width()).unwrap() +} + #[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: Unit, // The leftmost column is number 0. + pub row: Unit, // The highest row is number 0. } impl PartialOrd for Position { diff --git a/src/lib.rs b/src/lib.rs index 10fd4b68a..664366880 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,6 @@ use log::debug; #[cfg(feature = "derive")] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] pub use rustyline_derive::{Completer, Helper, Highlighter, Hinter, Validator}; -use unicode_width::UnicodeWidthStr; use crate::tty::{Buffer, RawMode, RawReader, Renderer, Term, Terminal}; @@ -59,13 +58,14 @@ use crate::completion::{longest_common_prefix, Candidate, Completer}; pub use crate::config::{Behavior, ColorMode, CompletionType, Config, EditMode, HistoryDuplicates}; use crate::edit::State; use crate::error::ReadlineError; -use crate::highlight::Highlighter; +use crate::highlight::{CmdKind, Highlighter}; use crate::hint::Hinter; use crate::history::{DefaultHistory, History, SearchDirection}; pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word}; use crate::keymap::{Bindings, InputState, Refresher}; pub use crate::keys::{KeyCode, KeyEvent, Modifiers}; use crate::kill_ring::KillRing; +use crate::layout::{swidth, Unit}; pub use crate::tty::ExternalPrinter; pub use crate::undo::Changeset; use crate::validate::Validator; @@ -292,15 +292,16 @@ fn page_completions( cols, candidates .iter() - .map(|s| s.display().width()) + .map(|s| swidth(s.display())) .max() .unwrap() + min_col_pad, ); let num_cols = cols / max_width; + let nbc = u16::try_from(candidates.len()).unwrap(); let mut pause_row = s.out.get_rows() - 1; - let num_rows = (candidates.len() + num_cols - 1) / num_cols; + let num_rows = nbc.div_ceil(num_cols); let mut ab = String::new(); for row in 0..num_rows { if row == pause_row { @@ -334,15 +335,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 = swidth(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(' '); } @@ -444,7 +445,7 @@ fn reverse_incremental_search( struct Guard<'m>(&'m tty::Mode); -#[allow(unused_must_use)] +#[expect(unused_must_use)] impl Drop for Guard<'_> { fn drop(&mut self) { let Guard(mode) = *self; @@ -550,8 +551,6 @@ where impl Helper for () {} -impl<'h, H: ?Sized + Helper> Helper for &'h H {} - /// Completion/suggestion context pub struct Context<'h> { history: &'h dyn History, @@ -596,7 +595,6 @@ pub struct Editor { /// Default editor with no helper and `DefaultHistory` pub type DefaultEditor = Editor<(), DefaultHistory>; -#[allow(clippy::new_without_default)] impl Editor { /// Create an editor with the default configuration pub fn new() -> Result { @@ -779,7 +777,7 @@ impl Editor { cmd, Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } ) { - self.term.cursor = s.layout.cursor.col; + self.term.cursor = s.layout.cursor.col as usize; } // Execute things can be done solely on a state object @@ -791,9 +789,7 @@ impl Editor { // Move to end, in case cursor was in the middle of the line, so that // next thing application prints goes after the input - s.forced_refresh = true; - s.edit_move_buffer_end()?; - s.forced_refresh = false; + s.edit_move_buffer_end(CmdKind::ForcedRefresh)?; if cfg!(windows) { let _ = original_mode; // silent warning @@ -899,7 +895,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<(Unit, Unit)> { if self.term.is_output_tty() { let out = self.term.create_writer(); Some((out.get_columns(), out.get_rows())) @@ -972,7 +968,7 @@ struct Iter<'a, H: Helper, I: History> { prompt: &'a str, } -impl<'a, H: Helper, I: History> Iterator for Iter<'a, H, I> { +impl Iterator for Iter<'_, H, I> { type Item = Result; fn next(&mut self) -> Option> { diff --git a/src/line_buffer.rs b/src/line_buffer.rs index ac9c68821..38c8812ff 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -1,5 +1,6 @@ //! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; +use crate::layout::{swidth, Layout}; use std::cmp::min; use std::fmt; use std::iter; @@ -202,7 +203,7 @@ impl LineBuffer { } self.buf[self.pos..] .grapheme_indices(true) - .take(n) + .take(usize::from(n)) .last() .map(|(i, s)| i + self.pos + s.len()) } @@ -216,7 +217,7 @@ impl LineBuffer { self.buf[..self.pos] .grapheme_indices(true) .rev() - .take(n) + .take(usize::from(n)) .last() .map(|(i, _)| i) } @@ -231,6 +232,7 @@ impl LineBuffer { n: RepeatCount, cl: &mut C, ) -> Option { + let n = usize::from(n); let shift = ch.len_utf8() * n; if self.must_truncate(self.buf.len() + shift) { return None; @@ -257,6 +259,7 @@ impl LineBuffer { n: RepeatCount, cl: &mut C, ) -> Option { + let n = usize::from(n); let shift = text.len() * n; if text.is_empty() || self.must_truncate(self.buf.len() + shift) { return None; @@ -582,10 +585,10 @@ impl LineBuffer { } /// Moves the cursor to the same column in the line above - pub fn move_to_line_up(&mut self, n: RepeatCount) -> bool { + pub fn move_to_line_up(&mut self, n: RepeatCount, layout: &Layout) -> bool { match self.buf[..self.pos].rfind('\n') { Some(off) => { - let column = self.buf[off + 1..self.pos].graphemes(true).count(); + let column = swidth(&self.buf[off + 1..self.pos]); let mut dest_start = self.buf[..off].rfind('\n').map_or(0, |n| n + 1); let mut dest_end = off; @@ -596,9 +599,14 @@ impl LineBuffer { dest_end = dest_start - 1; dest_start = self.buf[..dest_end].rfind('\n').map_or(0, |n| n + 1); } + let offset = if dest_start == 0 { + layout.prompt_size.col + } else { + 0 + }; let gidx = self.buf[dest_start..dest_end] .grapheme_indices(true) - .nth(column); + .nth(column.saturating_sub(offset) as usize); self.pos = gidx.map_or(off, |(idx, _)| dest_start + idx); // if there's no enough columns true @@ -652,11 +660,16 @@ impl LineBuffer { } /// Moves the cursor to the same column in the line above - pub fn move_to_line_down(&mut self, n: RepeatCount) -> bool { + pub fn move_to_line_down(&mut self, n: RepeatCount, layout: &Layout) -> bool { match self.buf[self.pos..].find('\n') { Some(off) => { let line_start = self.buf[..self.pos].rfind('\n').map_or(0, |n| n + 1); - let column = self.buf[line_start..self.pos].graphemes(true).count(); + let offset = if line_start == 0 { + layout.prompt_size.col + } else { + 0 + }; + let column = swidth(&self.buf[line_start..self.pos]) + offset; let mut dest_start = self.pos + off + 1; let mut dest_end = self.buf[dest_start..] .find('\n') @@ -672,7 +685,7 @@ impl LineBuffer { } self.pos = self.buf[dest_start..dest_end] .grapheme_indices(true) - .nth(column) + .nth(column as usize) .map_or(dest_end, |(idx, _)| dest_start + idx); // if there's no enough columns debug_assert!(self.pos <= self.buf.len()); true @@ -682,6 +695,7 @@ impl LineBuffer { } fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { + let n = usize::from(n); let mut shift = 0; let search_result = match cs { CharSearch::Backward(c) | CharSearch::BackwardAfter(c) => self.buf[..self.pos] @@ -1083,7 +1097,7 @@ impl LineBuffer { pub fn indent( &mut self, mvt: &Movement, - amount: usize, + amount: u8, dedent: bool, cl: &mut C, ) -> bool { @@ -1108,6 +1122,7 @@ impl LineBuffer { Movement::LineUp(n) => self.n_lines_up(n), Movement::LineDown(n) => self.n_lines_down(n), }; + let amount = usize::from(amount); let (start, end) = pair.unwrap_or((self.pos, self.pos)); let start = self.buf[..start].rfind('\n').map_or(0, |pos| pos + 1); let end = self.buf[end..] @@ -1115,7 +1130,7 @@ impl LineBuffer { .map_or_else(|| self.buf.len(), |pos| end + pos); let mut index = start; if dedent { - #[allow(clippy::unnecessary_to_owned)] + #[expect(clippy::unnecessary_to_owned)] for line in self.buf[start..end].to_string().split('\n') { let max = line.len() - line.trim_start().len(); let deleting = min(max, amount); @@ -1131,7 +1146,7 @@ impl LineBuffer { index += line.len() + 1 - deleting; } } else { - #[allow(clippy::unnecessary_to_owned)] + #[expect(clippy::unnecessary_to_owned)] for line in self.buf[start..end].to_string().split('\n') { for off in (0..amount).step_by(INDENT.len()) { self.insert_str(index, &INDENT[..min(amount - off, INDENT.len())], cl); @@ -1182,7 +1197,10 @@ mod test { use super::{ ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE, }; - use crate::keymap::{At, CharSearch, Word}; + use crate::{ + keymap::{At, CharSearch, Word}, + layout::Layout, + }; struct Listener { deleted_str: Option, @@ -1819,40 +1837,50 @@ mod test { fn move_by_line() { let text = "aa123\nsdf bc\nasdf"; let mut s = LineBuffer::init(text, 14); + let mut layout = Layout::default(); // move up - let ok = s.move_to_line_up(1); + let ok = s.move_to_line_up(1, &layout); assert_eq!(7, s.pos); assert!(ok); - let ok = s.move_to_line_up(1); + let ok = s.move_to_line_up(1, &layout); assert_eq!(1, s.pos); assert!(ok); - let ok = s.move_to_line_up(1); + let ok = s.move_to_line_up(1, &layout); assert_eq!(1, s.pos); assert!(!ok); // move down - let ok = s.move_to_line_down(1); + let ok = s.move_to_line_down(1, &layout); assert_eq!(7, s.pos); assert!(ok); - let ok = s.move_to_line_down(1); + let ok = s.move_to_line_down(1, &layout); assert_eq!(14, s.pos); assert!(ok); - let ok = s.move_to_line_down(1); + let ok = s.move_to_line_down(1, &layout); assert_eq!(14, s.pos); assert!(!ok); // move by multiple steps - let ok = s.move_to_line_up(2); + let ok = s.move_to_line_up(2, &layout); assert_eq!(1, s.pos); assert!(ok); - let ok = s.move_to_line_down(2); + let ok = s.move_to_line_down(2, &layout); assert_eq!(14, s.pos); assert!(ok); + + // non-empty prompt + layout.prompt_size.col = 2; + s.move_to_line_up(1, &layout); + assert_eq!(7, s.pos); + s.move_to_line_up(1, &layout); + assert_eq!(0, s.pos); + s.move_to_line_down(1, &layout); + assert_eq!(8, s.pos); } #[test] diff --git a/src/sqlite_history.rs b/src/sqlite_history.rs index 867f5b7d5..3d1fd8c95 100644 --- a/src/sqlite_history.rs +++ b/src/sqlite_history.rs @@ -162,7 +162,7 @@ COMMIT; return true; } if line.is_empty() - || (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace)) + || (self.ignore_space && line.chars().next().is_none_or(char::is_whitespace)) { return true; } @@ -372,7 +372,7 @@ PRAGMA incremental_vacuum; // TODO check that there is no memory entries (session_id == 0) ? self.reset(path)?; self.check_schema()?; - } else if self.path.as_ref().map_or(true, |p| p != path) { + } else if self.path.as_ref().is_none_or(|p| p != path) { self.reset(path)?; self.check_schema()?; } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 62484f042..abc02ccec 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,11 +1,12 @@ //! This module implements and describes common TTY methods & traits -use unicode_width::UnicodeWidthStr; +/// Unsupported Terminals that don't support RAW mode +const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"]; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::KeyEvent; -use crate::layout::{Layout, Position}; +use crate::layout::{swidth, Layout, Position, Unit}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; @@ -19,6 +20,8 @@ pub trait RawMode: Sized { pub enum Event { KeyPress(KeyEvent), ExternalPrint(String), + #[cfg(target_os = "macos")] + Timeout(bool), } /// Translate bytes read from stdin to keys. @@ -46,7 +49,6 @@ pub trait Renderer { fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>; /// Display `prompt`, line and cursor in terminal output - #[allow(clippy::too_many_arguments)] fn refresh_line( &mut self, prompt: &str, @@ -109,9 +111,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) -> Unit; /// Get the number of rows in the current terminal. - fn get_rows(&self) -> usize; + fn get_rows(&self) -> Unit; /// Check if output supports colors. fn colors_enabled(&self) -> bool; @@ -119,68 +121,8 @@ pub trait Renderer { fn move_cursor_at_leftmost(&mut self, rdr: &mut Self::Reader) -> Result<()>; } -impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { - type Reader = R::Reader; - - fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { - (**self).move_cursor(old, new) - } - - fn refresh_line( - &mut self, - prompt: &str, - line: &LineBuffer, - hint: Option<&str>, - old_layout: &Layout, - new_layout: &Layout, - highlighter: Option<&dyn Highlighter>, - ) -> Result<()> { - (**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 write_and_flush(&mut self, buf: &str) -> Result<()> { - (**self).write_and_flush(buf) - } - - fn beep(&mut self) -> Result<()> { - (**self).beep() - } - - fn clear_screen(&mut self) -> Result<()> { - (**self).clear_screen() - } - - fn clear_rows(&mut self, layout: &Layout) -> Result<()> { - (**self).clear_rows(layout) - } - - fn update_size(&mut self) { - (**self).update_size(); - } - - fn get_columns(&self) -> usize { - (**self).get_columns() - } - - fn get_rows(&self) -> usize { - (**self).get_rows() - } - - fn colors_enabled(&self) -> bool { - (**self).colors_enabled() - } - - fn move_cursor_at_leftmost(&mut self, rdr: &mut R::Reader) -> Result<()> { - (**self).move_cursor_at_leftmost(rdr) - } -} - // ignore ANSI escape sequence -fn width(s: &str, esc_seq: &mut u8) -> usize { +fn width(s: &str, esc_seq: &mut u8) -> Unit { if *esc_seq == 1 { if s == "[" { // CSI @@ -206,7 +148,7 @@ fn width(s: &str, esc_seq: &mut u8) -> usize { } else if s == "\n" { 0 } else { - s.width() + swidth(s) } } @@ -229,7 +171,7 @@ pub trait Term { fn new( color_mode: ColorMode, behavior: Behavior, - tab_stop: usize, + tab_stop: u8, bell_style: BellStyle, enable_bracketed_paste: bool, enable_signals: bool, @@ -261,6 +203,22 @@ pub trait Term { fn set_cursor_visibility(&mut self, visible: bool) -> Result>; } +/// Check TERM environment variable to see if current term is in our +/// unsupported list +fn is_unsupported_term() -> bool { + match std::env::var("TERM") { + Ok(term) => { + for iter in &UNSUPPORTED_TERM { + if (*iter).eq_ignore_ascii_case(&term) { + return true; + } + } + false + } + Err(_) => false, + } +} + // If on Windows platform import Windows TTY module // and re-export into mod.rs scope #[cfg(all(windows, not(target_arch = "wasm32")))] @@ -279,3 +237,15 @@ pub use self::unix::*; mod test; #[cfg(any(test, target_arch = "wasm32"))] pub use self::test::*; + +#[cfg(test)] +mod test_ { + #[test] + fn test_unsupported_term() { + std::env::set_var("TERM", "xterm"); + assert!(!super::is_unsupported_term()); + + std::env::set_var("TERM", "dumb"); + assert!(super::is_unsupported_term()); + } +} diff --git a/src/tty/test.rs b/src/tty/test.rs index 00ef42152..97470dcec 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -7,7 +7,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::{Layout, Position, Unit}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; @@ -21,7 +21,7 @@ impl RawMode for Mode { } } -impl<'a> RawReader for Iter<'a, KeyEvent> { +impl RawReader for Iter<'_, KeyEvent> { type Buffer = Buffer; fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { @@ -114,7 +114,7 @@ impl Renderer for Sink { fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; - pos.col += s.len(); + pos.col += u16::try_from(s.len()).unwrap(); pos } @@ -136,11 +136,11 @@ impl Renderer for Sink { fn update_size(&mut self) {} - fn get_columns(&self) -> usize { + fn get_columns(&self) -> Unit { 80 } - fn get_rows(&self) -> usize { + fn get_rows(&self) -> Unit { 24 } @@ -183,7 +183,7 @@ impl Term for DummyTerminal { fn new( color_mode: ColorMode, _behavior: Behavior, - _tab_stop: usize, + _tab_stop: u8, bell_style: BellStyle, _enable_bracketed_paste: bool, _enable_signals: bool, diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 995e05a51..f63366a93 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -1,6 +1,4 @@ //! Unix specific definitions -#[cfg(feature = "buffer-redux")] -use buffer_redux::BufReader; use std::cmp; use std::collections::HashMap; use std::fs::{File, OpenOptions}; @@ -13,12 +11,15 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, SyncSender}; use std::sync::{Arc, Mutex}; +#[cfg(feature = "buffer-redux")] +use buffer_redux::BufReader; use log::{debug, warn}; use nix::errno::Errno; use nix::poll::{self, PollFlags, PollTimeout}; use nix::sys::select::{self, FdSet}; #[cfg(not(feature = "termios"))] use nix::sys::termios::Termios; +use nix::sys::time::TimeValLike; use nix::unistd::{close, isatty, read, write}; #[cfg(feature = "termios")] use termios::Termios; @@ -29,20 +30,16 @@ 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, KeyEvent as E, Modifiers as M}; -use crate::layout::{Layout, Position}; +use crate::layout::{Layout, Position, Unit}; use crate::line_buffer::LineBuffer; use crate::{error, Cmd, ReadlineError, Result}; -/// Unsupported Terminals that don't support RAW mode -const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"]; - const BRACKETED_PASTE_ON: &str = "\x1b[?2004h"; 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) -> (Unit, Unit) { use std::mem::zeroed; if cfg!(test) { @@ -57,15 +54,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 + Unit::MAX } else { - size.ws_row as usize + size.ws_row }; (cols, rows) } @@ -74,22 +67,6 @@ fn get_win_size(fd: RawFd) -> (usize, usize) { } } -/// Check TERM environment variable to see if current term is in our -/// unsupported list -fn is_unsupported_term() -> bool { - match std::env::var("TERM") { - Ok(term) => { - for iter in &UNSUPPORTED_TERM { - if (*iter).eq_ignore_ascii_case(&term) { - return true; - } - } - false - } - Err(_) => false, - } -} - /// Return whether or not STDIN, STDOUT or STDERR is a TTY fn is_a_tty(fd: RawFd) -> bool { isatty(fd).unwrap_or(false) @@ -158,7 +135,7 @@ impl Read for TtyIn { return Err(error); } } else { - #[allow(clippy::cast_sign_loss)] + #[expect(clippy::cast_sign_loss)] return Ok(res as usize); } } @@ -195,6 +172,8 @@ pub struct PosixRawReader { key_map: PosixKeyMap, // external print reader pipe_reader: Option, + #[cfg(target_os = "macos")] + is_dev_tty: bool, } impl AsFd for PosixRawReader { @@ -243,6 +222,7 @@ impl PosixRawReader { config: &Config, key_map: PosixKeyMap, pipe_reader: Option, + #[cfg(target_os = "macos")] is_dev_tty: bool, ) -> Self { let inner = TtyIn { fd, sigwinch_pipe }; #[cfg(any(not(feature = "buffer-redux"), test))] @@ -259,6 +239,8 @@ impl PosixRawReader { parser: Parser::new(), key_map, pipe_reader, + #[cfg(target_os = "macos")] + is_dev_tty, } } @@ -306,9 +288,8 @@ impl PosixRawReader { match self.poll(timeout) { // Ignore poll errors, it's very likely we'll pick them up on // the next read anyway. - Ok(0) | Err(_) => Ok(E::ESC), - Ok(n) => { - debug_assert!(n > 0, "{}", n); + Ok(false) | Err(_) => Ok(E::ESC), + Ok(true) => { // recurse, and add the alt modifier. let E(k, m) = self._do_escape_sequence(false)?; Ok(E(k, m | M::ALT)) @@ -377,7 +358,7 @@ impl PosixRawReader { } /// Handle \E[ escape sequences - #[allow(clippy::cognitive_complexity)] + #[expect(clippy::cognitive_complexity)] fn extended_escape(&mut self, seq2: char) -> Result { let seq3 = self.next_char()?; if seq3 == '~' { @@ -702,38 +683,52 @@ impl PosixRawReader { }) } - fn poll(&mut self, timeout_ms: PollTimeout) -> Result { + fn poll(&mut self, timeout: PollTimeout) -> Result { let n = self.tty_in.buffer().len(); if n > 0 { - return Ok(n as i32); + return Ok(true); } + #[cfg(target_os = "macos")] + if self.is_dev_tty { + // poll doesn't work for /dev/tty on MacOS but select does + return Ok(match self.select(Some(timeout), false /* ignored */)? { + Event::Timeout(true) => false, + _ => true, + }); + } + debug!(target: "rustyline", "poll with: {:?}", timeout); let mut fds = [poll::PollFd::new(self.as_fd(), PollFlags::POLLIN)]; - let r = poll::poll(&mut fds, timeout_ms); + let r = poll::poll(&mut fds, timeout); + debug!(target: "rustyline", "poll returns: {:?}", r); match r { - Ok(n) => Ok(n), + Ok(n) => Ok(n != 0), Err(Errno::EINTR) => { if self.tty_in.get_ref().sigwinch()? { Err(ReadlineError::WindowResized) } else { - Ok(0) // Ignore EINTR while polling + Ok(false) // Ignore EINTR while polling } } Err(e) => Err(e.into()), } } - fn select(&mut self, single_esc_abort: bool) -> Result { + // timeout is used only with /dev/tty on MacOs + fn select(&mut self, timeout: Option, single_esc_abort: bool) -> Result { let tty_in = self.as_fd(); let sigwinch_pipe = self .tty_in .get_ref() .sigwinch_pipe .map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }); - let pipe_reader = self - .pipe_reader - .as_ref() - .map(|pr| pr.lock().unwrap().0.as_raw_fd()) - .map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }); + let pipe_reader = if timeout.is_some() { + None + } else { + self.pipe_reader + .as_ref() + .map(|pr| pr.lock().unwrap().0.as_raw_fd()) + .map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }) + }; loop { let mut readfds = FdSet::new(); if let Some(sigwinch_pipe) = sigwinch_pipe { @@ -743,7 +738,14 @@ impl PosixRawReader { if let Some(pipe_reader) = pipe_reader { readfds.insert(pipe_reader); } - if let Err(err) = select::select(None, Some(&mut readfds), None, None, None) { + let mut timeout = match timeout { + Some(pt) => pt + .as_millis() + .map(|ms| nix::sys::time::TimeVal::milliseconds(ms as i64)), + None => None, + }; + if let Err(err) = select::select(None, Some(&mut readfds), None, None, timeout.as_mut()) + { if err == Errno::EINTR && self.tty_in.get_ref().sigwinch()? { return Err(ReadlineError::WindowResized); } else if err != Errno::EINTR { @@ -752,12 +754,21 @@ impl PosixRawReader { continue; } }; - if sigwinch_pipe.map_or(false, |fd| readfds.contains(fd)) { + if sigwinch_pipe.is_some_and(|fd| readfds.contains(fd)) { self.tty_in.get_ref().sigwinch()?; return Err(ReadlineError::WindowResized); } else if readfds.contains(tty_in) { + #[cfg(target_os = "macos")] + if timeout.is_some() { + return Ok(Event::Timeout(false)); + } // prefer user input over external print return self.next_key(single_esc_abort).map(Event::KeyPress); + } else if timeout.is_some() { + #[cfg(target_os = "macos")] + return Ok(Event::Timeout(true)); + #[cfg(not(target_os = "macos"))] + unreachable!() } else if let Some(ref pipe_reader) = self.pipe_reader { let mut guard = pipe_reader.lock().unwrap(); let mut buf = [0; 1]; @@ -776,14 +787,14 @@ impl RawReader for PosixRawReader { #[cfg(not(feature = "signal-hook"))] fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { match self.pipe_reader { - Some(_) => self.select(single_esc_abort), + Some(_) => self.select(None, single_esc_abort), None => self.next_key(single_esc_abort).map(Event::KeyPress), } } #[cfg(feature = "signal-hook")] fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { - self.select(single_esc_abort) + self.select(None, single_esc_abort) } fn next_key(&mut self, single_esc_abort: bool) -> Result { @@ -800,7 +811,7 @@ impl RawReader for PosixRawReader { self.timeout_ms }; match self.poll(timeout_ms) { - Ok(0) => { + Ok(false) => { // single escape } Ok(_) => { @@ -883,7 +894,7 @@ impl Receiver for Utf8 { self.valid = true; } - /// Called when an invalid_sequence is detected + /// Called when an invalid sequence is detected fn invalid_sequence(&mut self) { self.c = None; self.valid = false; @@ -893,15 +904,15 @@ impl Receiver for Utf8 { /// Console output writer pub struct PosixRenderer { out: RawFd, - cols: usize, // Number of columns in terminal + cols: Unit, // Number of columns in terminal buffer: String, - tab_stop: usize, + tab_stop: Unit, 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: Unit, colors_enabled: bool, bell_style: BellStyle) -> Self { let (cols, _) = get_win_size(out); Self { out, @@ -1033,7 +1044,7 @@ impl Renderer for PosixRenderer { } // position the cursor within the line if cursor.col > 0 { - write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap(); + write!(self.buffer, "\r\x1b[{}C", cursor.col)?; } else { self.buffer.push('\r'); } @@ -1101,13 +1112,13 @@ impl Renderer for PosixRenderer { self.cols = cols; } - fn get_columns(&self) -> usize { + fn get_columns(&self) -> Unit { 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) -> Unit { let (_, rows) = get_win_size(self.out); rows } @@ -1117,7 +1128,7 @@ impl Renderer for PosixRenderer { } fn move_cursor_at_leftmost(&mut self, rdr: &mut PosixRawReader) -> Result<()> { - if rdr.poll(PollTimeout::ZERO)? != 0 { + if rdr.poll(PollTimeout::ZERO)? { // TODO fill input buffer instead debug!(target: "rustyline", "cannot request cursor location"); return Ok(()); @@ -1125,7 +1136,7 @@ impl Renderer for PosixRenderer { /* Report cursor location */ self.write_and_flush("\x1b[6n")?; /* Read the response: ESC [ rows ; cols R */ - if rdr.poll(PollTimeout::from(100u8))? == 0 + if !rdr.poll(PollTimeout::from(100u8))? || rdr.next_char()? != '\x1b' || rdr.next_char()? != '[' || read_digits_until(rdr, ';')?.is_none() @@ -1265,7 +1276,7 @@ pub struct PosixTerminal { is_out_a_tty: bool, close_on_drop: bool, pub(crate) color_mode: ColorMode, - tab_stop: usize, + tab_stop: u8, bell_style: BellStyle, enable_bracketed_paste: bool, raw_mode: Arc, @@ -1299,7 +1310,7 @@ impl Term for PosixTerminal { fn new( color_mode: ColorMode, behavior: Behavior, - tab_stop: usize, + tab_stop: u8, bell_style: BellStyle, enable_bracketed_paste: bool, enable_signals: bool, @@ -1329,8 +1340,7 @@ impl Term for PosixTerminal { false, ) }; - let unsupported = is_unsupported_term(); - #[allow(unused_variables)] + let unsupported = super::is_unsupported_term(); let sigwinch = if !unsupported && is_in_a_tty && is_out_a_tty { Some(SigWinCh::install_sigwinch_handler()?) } else { @@ -1422,13 +1432,15 @@ impl Term for PosixTerminal { config, key_map, self.pipe_reader.clone(), + #[cfg(target_os = "macos")] + self.close_on_drop, ) } fn create_writer(&self) -> PosixRenderer { PosixRenderer::new( self.tty_out, - self.tab_stop, + Unit::from(self.tab_stop), self.colors_enabled(), self.bell_style, ) @@ -1473,7 +1485,7 @@ impl Term for PosixTerminal { } } -#[allow(unused_must_use)] +#[expect(unused_must_use)] impl Drop for PosixTerminal { fn drop(&mut self) { if self.close_on_drop { @@ -1657,15 +1669,6 @@ mod test { assert_eq!(0, pos.row); } - #[test] - fn test_unsupported_term() { - std::env::set_var("TERM", "xterm"); - assert!(!super::is_unsupported_term()); - - std::env::set_var("TERM", "dumb"); - assert!(super::is_unsupported_term()); - } - #[test] fn test_send() { fn assert_send() {} diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 7574403f0..dc714031e 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -1,5 +1,5 @@ //! Windows specific definitions -#![allow(clippy::try_err)] // suggested fix does not work (cannot infer...) +#![expect(clippy::try_err)] // suggested fix does not work (cannot infer...) use std::fs::OpenOptions; use std::io; @@ -13,7 +13,6 @@ use std::sync::Arc; use log::{debug, warn}; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; use windows_sys::Win32::Foundation::{self as foundation, BOOL, FALSE, HANDLE, TRUE}; use windows_sys::Win32::System::Console as console; use windows_sys::Win32::System::Threading as threading; @@ -23,7 +22,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::{swidth, Layout, Position, Unit}; use crate::line_buffer::LineBuffer; use crate::{error, Cmd, Result}; @@ -52,13 +51,13 @@ fn check(rc: BOOL) -> io::Result<()> { } } -fn get_win_size(handle: HANDLE) -> (usize, usize) { +fn get_win_size(handle: HANDLE) -> (Unit, Unit) { let mut info = unsafe { mem::zeroed() }; match unsafe { console::GetConsoleScreenBufferInfo(handle, &mut info) } { FALSE => (80, 24), _ => ( - info.dwSize.X as usize, - (1 + info.srWindow.Bottom - info.srWindow.Top) as usize, + Unit::try_from(info.dwSize.X).unwrap(), + Unit::try_from(1 + info.srWindow.Bottom - info.srWindow.Top).unwrap(), ), // (info.srWindow.Right - info.srWindow.Left + 1) } } @@ -286,7 +285,7 @@ fn read_input(handle: HANDLE, max_count: u32) -> Result { pub struct ConsoleRenderer { conout: HANDLE, - cols: usize, // Number of columns in terminal + cols: Unit, // Number of columns in terminal buffer: String, utf16: Vec, colors_enabled: bool, @@ -343,7 +342,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: Unit) -> Unit { let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { @@ -501,7 +500,7 @@ impl Renderer for ConsoleRenderer { pos.col = 0; pos.row += 1; } else { - let cw = c.width(); + let cw = swidth(c); pos.col += cw; if pos.col > self.cols { pos.row += 1; @@ -544,13 +543,13 @@ impl Renderer for ConsoleRenderer { self.cols = cols; } - fn get_columns(&self) -> usize { + fn get_columns(&self) -> Unit { 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) -> Unit { let (_, rows) = get_win_size(self.conout); rows } @@ -659,7 +658,7 @@ impl Term for Console { fn new( color_mode: ColorMode, behavior: Behavior, - _tab_stop: usize, + _tab_stop: u8, bell_style: BellStyle, _enable_bracketed_paste: bool, _enable_signals: bool, @@ -719,9 +718,8 @@ impl Term for Console { }) } - /// Checking for an unsupported TERM in windows is a no-op fn is_unsupported(&self) -> bool { - false + super::is_unsupported_term() } fn is_input_tty(&self) -> bool { diff --git a/src/undo.rs b/src/undo.rs index 924319f24..cd5cae27e 100644 --- a/src/undo.rs +++ b/src/undo.rs @@ -147,7 +147,7 @@ impl Changeset { pub(crate) fn insert(&mut self, idx: usize, c: char) { debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c); self.redos.clear(); - if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) { + if !c.is_alphanumeric() || !self.undos.last().is_some_and(|lc| lc.insert_seq(idx)) { self.undos.push(Self::insert_char(idx, c)); return; } @@ -188,7 +188,7 @@ impl Changeset { || !self .undos .last() - .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len())) + .is_some_and(|lc| lc.delete_seq(indx, string.as_ref().len())) { self.undos.push(Change::Delete { idx: indx, @@ -217,9 +217,10 @@ impl Changeset { fn single_char(s: &str) -> bool { let mut graphemes = s.graphemes(true); - graphemes.next().map_or(false, |grapheme| { - grapheme.chars().all(char::is_alphanumeric) - }) && graphemes.next().is_none() + graphemes + .next() + .is_some_and(|grapheme| grapheme.chars().all(char::is_alphanumeric)) + && graphemes.next().is_none() } pub(crate) fn replace + Into + Debug>( @@ -231,7 +232,7 @@ impl Changeset { debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_); self.redos.clear(); - if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) { + if !self.undos.last().is_some_and(|lc| lc.replace_seq(indx)) { self.undos.push(Change::Replace { idx: indx, old: old_.into(), diff --git a/src/validate.rs b/src/validate.rs index e3bf2e247..f1475cd47 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -48,10 +48,11 @@ impl<'i> ValidationContext<'i> { } /// This trait provides an extension interface for determining whether -/// the current input buffer is valid. Rustyline uses the method -/// provided by this trait to decide whether hitting the enter key -/// will end the current editing session and return the current line -/// buffer to the caller of `Editor::readline` or variants. +/// the current input buffer is valid. +/// +/// Rustyline uses the method provided by this trait to decide whether hitting +/// the enter key will end the current editing session and return the current +/// line buffer to the caller of `Editor::readline` or variants. pub trait Validator { /// Takes the currently edited `input` and returns a /// `ValidationResult` indicating whether it is valid or not along @@ -85,16 +86,6 @@ pub trait Validator { impl Validator for () {} -impl<'v, V: ?Sized + Validator> Validator for &'v V { - fn validate(&self, ctx: &mut ValidationContext) -> Result { - (**self).validate(ctx) - } - - fn validate_while_typing(&self) -> bool { - (**self).validate_while_typing() - } -} - /// Simple matching bracket validator. #[derive(Default)] pub struct MatchingBracketValidator {