diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44d4996e..209a54bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,5 +49,8 @@ jobs: - name: Tests run: cargo llvm-cov nextest --all ${{ matrix.flags }} --lcov --output-path lcov.info + - name: Check lockfile + run: cargo check --locked ${{ matrix.flags }} --all-targets --all + - name: Doctests run: cargo test --doc ${{ matrix.flags }} diff --git a/Cargo.lock b/Cargo.lock index b2aa8d37..aa6e41b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,15 +198,15 @@ checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.5.0", "crossterm_winapi", - "libc", "mio", "parking_lot", + "rustix", "serde", "signal-hook", "signal-hook-mio", @@ -364,6 +364,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "home" version = "0.5.9" @@ -502,14 +508,15 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -684,7 +691,7 @@ dependencies = [ [[package]] name = "reedline" -version = "0.32.0" +version = "0.34.0" dependencies = [ "arboard", "chrono", @@ -795,9 +802,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -879,9 +886,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", diff --git a/Cargo.toml b/Cargo.toml index 80efd50f..a2dd3a5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" name = "reedline" repository = "https://github.com/nushell/reedline" rust-version = "1.63.0" -version = "0.32.0" +version = "0.34.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -21,7 +21,7 @@ chrono = { version = "0.4.19", default-features = false, features = [ "serde", ] } crossbeam = { version = "0.8.2", optional = true } -crossterm = { version = "0.27.0", features = ["serde"] } +crossterm = { version = "0.28.1", features = ["serde"] } fd-lock = "4.0.2" itertools = "0.12.0" nu-ansi-term = "0.50.0" diff --git a/README.md b/README.md index 4b485e9b..f60f3204 100644 --- a/README.md +++ b/README.md @@ -234,12 +234,6 @@ Reedline has now all the basic features to become the primary line editor for [n For more ideas check out the [feature discussion](https://github.com/nushell/reedline/issues/63) or hop on the `#reedline` channel of the [nushell discord](https://discordapp.com/invite/NtAbbGn). -### Development history - -If you want to follow along with the history of how reedline got started, you can watch the [recordings](https://youtube.com/playlist?list=PLP2yfE2-FXdQw0I6O4YdIX_mzBeF5TDdv) of [JT](https://github.com/jntrnr)'s [live-coding streams](https://www.twitch.tv/jntrnr). - -[Playlist: Creating a line editor in Rust](https://youtube.com/playlist?list=PLP2yfE2-FXdQw0I6O4YdIX_mzBeF5TDdv) - ### Alternatives For currently more mature Rust line editing check out: diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index e7bf0426..9c4f0711 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -212,10 +212,10 @@ impl Command { }, Self::Change => { let op = match motion { - Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::ClearToLineEnd)]), + Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)]), Motion::Line => Some(vec![ - ReedlineOption::Edit(EditCommand::MoveToStart { select: false }), - ReedlineOption::Edit(EditCommand::ClearToLineEnd), + ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }), + ReedlineOption::Edit(EditCommand::CutToLineEnd), ]), Motion::NextWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]), Motion::NextBigWord => { diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index d3d9289d..149b5eb9 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -510,6 +510,34 @@ mod tests { #[case(&['d', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight])]))] #[case(&['d', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft])]))] #[case(&['d', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft])]))] + #[case(&['c', 'c'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] + #[case(&['c', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] + #[case(&['c', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] + #[case(&['c', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] + #[case(&['c', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft]), ReedlineEvent::Repaint]))] + #[case(&['c', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft]), ReedlineEvent::Repaint]))] + #[case(&['d', 'h'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Backspace])]))] + #[case(&['d', 'l'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Delete])]))] + #[case(&['2', 'd', 'd'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] + // #[case(&['d', 'j'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] + // #[case(&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] + #[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight])]))] + #[case(&['d', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))] + #[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))] + #[case(&['d', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]))] + #[case(&['d', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] + #[case(&['d', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')])]))] + #[case(&['d', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] + #[case(&['d', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')])]))] + #[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] + #[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))] + #[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))] + #[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] + #[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] + #[case(&['c', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), ReedlineEvent::Repaint]))] + #[case(&['c', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] + #[case(&['c', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')]), ReedlineEvent::Repaint]))] fn test_reedline_move(#[case] input: &[char], #[case] expected: ReedlineEvent) { let mut vi = Vi::default(); let res = vi_parse(input); @@ -518,6 +546,42 @@ mod tests { assert_eq!(output, expected); } + #[rstest] + #[case(&['f', 'a'], &[';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveRightUntil{c: 'a',select: false}])]))] + #[case(&['f', 'a'], &[','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLeftUntil{c: 'a', select: false}])]))] + #[case(&['F', 'a'], &[','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveRightUntil{c: 'a', select: false}])]))] + #[case(&['F', 'a'], &[';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLeftUntil{c: 'a', select: false}])]))] + #[case(&['f', 'a'], &['d', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] + #[case(&['f', 'a'], &['d', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] + #[case(&['F', 'a'], &['d', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] + #[case(&['F', 'a'], &['d', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] + #[case(&['f', 'a'], &['c', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] + #[case(&['f', 'a'], &['c', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] + #[case(&['F', 'a'], &['c', ','], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] + #[case(&['F', 'a'], &['c', ';'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] + fn test_reedline_memory_move( + #[case] before: &[char], + #[case] now: &[char], + #[case] expected: ReedlineEvent, + ) { + let mut vi = Vi::default(); + let _ = vi_parse(before).to_reedline_event(&mut vi); + let output = vi_parse(now).to_reedline_event(&mut vi); + + assert_eq!(output, expected); + } + + #[rstest] + #[case(&['c', 'w'], &['c', 'e'])] + #[case(&['c', 'W'], &['c', 'E'])] + fn test_reedline_move_synonm(#[case] synonym: &[char], #[case] original: &[char]) { + let mut vi = Vi::default(); + let output = vi_parse(synonym).to_reedline_event(&mut vi); + let expected = vi_parse(original).to_reedline_event(&mut vi); + + assert_eq!(output, expected); + } + #[rstest] #[case(&['2', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::UntilFound(vec![ ReedlineEvent::MenuUp, diff --git a/src/engine.rs b/src/engine.rs index 172b9e1f..1c3dd426 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1706,10 +1706,10 @@ impl Reedline { match &mut self.buffer_editor { Some(BufferEditor { ref mut command, - temp_file, + ref temp_file, }) => { { - let mut file = File::create(&temp_file)?; + let mut file = File::create(temp_file)?; write!(file, "{}", self.editor.get_buffer())?; } { diff --git a/src/lib.rs b/src/lib.rs index 4d14261b..35eccceb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,7 @@ //! //! ## Are we prompt yet? (Development status) //! -//! Nushell has now all the basic features to become the primary line editor for [nushell](https://github.com/nushell/nushell +//! Reedline has now all the basic features to become the primary line editor for [nushell](https://github.com/nushell/nushell //! ) //! //! - General editing functionality, that should feel familiar coming from other shells (e.g. bash, fish, zsh). diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index fac1feab..2c11fa32 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -512,7 +512,11 @@ impl IdeMenu { }; if use_ansi_coloring { - let match_len = self.working_details.shortest_base_string.len(); + let match_len = self + .working_details + .shortest_base_string + .len() + .min(string.len()); // Split string so the match text can be styled let (match_str, remaining_str) = string.split_at(match_len); @@ -1054,10 +1058,7 @@ fn truncate_string_list(list: &mut [String], truncation_chars: &str) { let mut new_line = String::new(); for grapheme in chars.into_iter().rev() { if to_replace > 0 { - new_line.insert_str( - 0, - &truncation_chars[truncation_len - to_replace].to_string(), - ); + new_line.insert(0, truncation_chars[truncation_len - to_replace]); to_replace -= 1; } else { new_line.insert_str(0, grapheme); @@ -1404,4 +1405,40 @@ mod tests { "cursor should be at the end after completion" ); } + + #[test] + fn test_regression_panic_on_long_item() { + let commands = vec![ + "hello world 2".into(), + "hello another very large option for hello word that will force one column".into(), + "this is the reedline crate".into(), + "abaaabas".into(), + "abaaacas".into(), + ]; + + let mut completer = Box::new(crate::DefaultCompleter::new_with_wordlen(commands, 2)); + + let mut menu = IdeMenu::default().with_name("testmenu"); + menu.working_details = IdeMenuDetails { + cursor_col: 50, + menu_width: 50, + completion_width: 50, + description_width: 50, + description_is_right: true, + space_left: 50, + space_right: 50, + description_offset: 50, + shortest_base_string: String::new(), + }; + let mut editor = Editor::default(); + // backtick at the end of the line + editor.set_buffer( + "hello another very large option for hello word that will force one colu".to_string(), + UndoBehavior::CreateUndoPoint, + ); + + menu.update_values(&mut editor, &mut *completer); + + menu.menu_string(500, true); + } } diff --git a/src/painting/painter.rs b/src/painting/painter.rs index f9c600a7..1718e7fe 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -500,23 +500,18 @@ impl Painter { /// Clear the screen by printing enough whitespace to start the prompt or /// other output back at the first line of the terminal. pub(crate) fn clear_screen(&mut self) -> Result<()> { - self.stdout.queue(cursor::Hide)?; - let (_, num_lines) = terminal::size()?; - for _ in 0..2 * num_lines { - self.stdout.queue(Print("\n"))?; - } - self.stdout.queue(MoveTo(0, 0))?; - self.stdout.queue(cursor::Show)?; - - self.stdout.flush()?; + self.stdout + .queue(Clear(ClearType::All))? + .queue(MoveTo(0, 0))? + .flush()?; self.initialize_prompt_position(None) } pub(crate) fn clear_scrollback(&mut self) -> Result<()> { self.stdout - .queue(crossterm::terminal::Clear(ClearType::All))? - .queue(crossterm::terminal::Clear(ClearType::Purge))? - .queue(cursor::MoveTo(0, 0))? + .queue(Clear(ClearType::All))? + .queue(Clear(ClearType::Purge))? + .queue(MoveTo(0, 0))? .flush()?; self.initialize_prompt_position(None) }