From 49adbd4c530fd9e48c89e43af56b76905f9ae4c5 Mon Sep 17 00:00:00 2001 From: gwenn <gtreguier@gmail.com> Date: Sat, 19 May 2018 13:36:22 +0200 Subject: [PATCH] Fix vi-redo Reuse undo::Changeset to retrieve last inserted text. Introduce Cmd::Replace(Movement, Option<String>). Refactor Cmd::Kill handling. --- TODO.md | 2 +- src/edit.rs | 67 +++++++----------------------- src/keymap.rs | 101 +++++++++++++++++++++------------------------ src/kill_ring.rs | 13 +++--- src/lib.rs | 54 ++++-------------------- src/line_buffer.rs | 65 +++++++++++++++++++++++++++-- src/tty/unix.rs | 1 + src/undo.rs | 28 +++++++++++++ 8 files changed, 167 insertions(+), 164 deletions(-) diff --git a/TODO.md b/TODO.md index 62637d6d..50efdb42 100644 --- a/TODO.md +++ b/TODO.md @@ -52,7 +52,7 @@ Movement - [ ] Move to the corresponding opening/closing bracket Redo -- [ ] redo substitue +- [X] redo substitue Repeat - [x] dynamic prompt (arg: ?) diff --git a/src/edit.rs b/src/edit.rs index 71cf8184..045ed3a0 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -9,7 +9,7 @@ use unicode_width::UnicodeWidthChar; use super::Result; use hint::Hinter; use history::{Direction, History}; -use keymap::{Anchor, At, CharSearch, Cmd, RepeatCount, Word}; +use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; use keymap::{InputState, Refresher}; use line_buffer::{LineBuffer, WordAction, MAX_LINE}; use tty::{Position, RawReader, Renderer}; @@ -69,6 +69,9 @@ impl<'out, 'prompt> State<'out, 'prompt> { try!(self.refresh_line()); continue; } + if let Ok(Cmd::Replace(_, _)) = rc { + self.changes.borrow_mut().begin(); + } return rc; } } @@ -136,12 +139,12 @@ impl<'out, 'prompt> Refresher for State<'out, 'prompt> { fn doing_insert(&mut self) { self.changes.borrow_mut().begin(); } - fn doing_replace(&mut self) { - self.changes.borrow_mut().begin(); - } fn done_inserting(&mut self) { self.changes.borrow_mut().end(); } + fn last_insert(&self) -> Option<String> { + self.changes.borrow().last_insert() + } } impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { @@ -291,38 +294,26 @@ impl<'out, 'prompt> State<'out, 'prompt> { } } - /// Delete the character at the right of the cursor without altering the - /// cursor position. Basically this is what happens with the "Delete" - /// keyboard key. - pub fn edit_delete(&mut self, n: RepeatCount) -> Result<()> { - if self.line.delete(n).is_some() { - self.refresh_line() - } else { - Ok(()) - } - } - - /// Backspace implementation. - pub fn edit_backspace(&mut self, n: RepeatCount) -> Result<()> { - if self.line.backspace(n) { + pub fn edit_kill(&mut self, mvt: &Movement) -> Result<()> { + if self.line.kill(mvt) { self.refresh_line() } else { Ok(()) } } - /// Kill the text from point to the end of the line. - pub fn edit_kill_line(&mut self) -> Result<()> { - if self.line.kill_line() { + pub fn edit_insert_text(&mut self, text: &str) -> Result<()> { + if !text.is_empty() { + let cursor = self.line.pos(); + self.line.insert_str(cursor, text); self.refresh_line() } else { Ok(()) } } - /// Kill backward from point to the beginning of the line. - pub fn edit_discard_line(&mut self) -> Result<()> { - if self.line.discard_line() { + pub fn edit_delete(&mut self, n: RepeatCount) -> Result<()> { + if self.line.delete(n).is_some() { self.refresh_line() } else { Ok(()) @@ -349,16 +340,6 @@ impl<'out, 'prompt> State<'out, 'prompt> { } } - /// Delete the previous word, maintaining the cursor at the start of the - /// current word. - pub fn edit_delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Result<()> { - if self.line.delete_prev_word(word_def, n) { - self.refresh_line() - } else { - Ok(()) - } - } - 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() @@ -375,24 +356,6 @@ impl<'out, 'prompt> State<'out, 'prompt> { } } - /// Kill from the cursor to the end of the current word, or, if between - /// words, to the end of the next word. - pub fn edit_delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Result<()> { - if self.line.delete_word(at, word_def, n) { - self.refresh_line() - } else { - Ok(()) - } - } - - pub fn edit_delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { - if self.line.delete_to(cs, n) { - self.refresh_line() - } else { - Ok(()) - } - } - pub fn edit_word(&mut self, a: WordAction) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = self.line.edit_word(a); diff --git a/src/keymap.rs b/src/keymap.rs index 450b511b..9656e3ac 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -43,7 +43,7 @@ pub enum Cmd { Interrupt, /// backward-delete-char, backward-kill-line, backward-kill-word /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout, - /// vi-change-to, vi-delete, vi-delete-to, vi-rubout, vi-subst + /// vi-delete, vi-delete-to, vi-rubout Kill(Movement), /// backward-char, backward-word, beginning-of-line, end-of-line, /// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word, @@ -59,7 +59,9 @@ pub enum Cmd { /// quoted-insert QuotedInsert, /// vi-change-char - Replace(RepeatCount, char), + ReplaceChar(RepeatCount, char), + /// vi-change-to, vi-substitute + Replace(Movement, Option<String>), /// reverse-search-history ReverseSearchHistory, /// self-insert @@ -88,6 +90,7 @@ impl Cmd { Cmd::Kill(Movement::BackwardChar(_)) | Cmd::Kill(Movement::ForwardChar(_)) => true, Cmd::ClearScreen | Cmd::Kill(_) + | Cmd::Replace(_, _) | Cmd::Noop | Cmd::Suspend | Cmd::Yank(_, _) @@ -100,6 +103,7 @@ impl Cmd { match *self { Cmd::Insert(_, _) | Cmd::Kill(_) + | Cmd::ReplaceChar(_, _) | Cmd::Replace(_, _) | Cmd::SelfInsert(_, _) | Cmd::ViYankTo(_) @@ -115,15 +119,30 @@ impl Cmd { } } - fn redo(&self, new: Option<RepeatCount>) -> Cmd { + // Replay this command with a possible different `RepeatCount`. + fn redo(&self, new: Option<RepeatCount>, wrt: &Refresher) -> Cmd { match *self { Cmd::Insert(previous, ref text) => { Cmd::Insert(repeat_count(previous, new), text.clone()) } Cmd::Kill(ref mvt) => Cmd::Kill(mvt.redo(new)), Cmd::Move(ref mvt) => Cmd::Move(mvt.redo(new)), - Cmd::Replace(previous, c) => Cmd::Replace(repeat_count(previous, new), c), - Cmd::SelfInsert(previous, c) => Cmd::SelfInsert(repeat_count(previous, new), c), + Cmd::ReplaceChar(previous, c) => Cmd::ReplaceChar(repeat_count(previous, new), c), + Cmd::Replace(ref mvt, ref text) => { + if text.is_none() { + Cmd::Replace(mvt.redo(new), wrt.last_insert()) + } else { + Cmd::Replace(mvt.redo(new), text.clone()) + } + } + Cmd::SelfInsert(previous, c) => { + // consecutive char inserts are repeatable not only the last one... + if let Some(text) = wrt.last_insert() { + Cmd::Insert(repeat_count(previous, new), text) + } else { + Cmd::SelfInsert(repeat_count(previous, new), c) + } + } // Cmd::TransposeChars => Cmd::TransposeChars, Cmd::ViYankTo(ref mvt) => Cmd::ViYankTo(mvt.redo(new)), Cmd::Yank(previous, anchor) => Cmd::Yank(repeat_count(previous, new), anchor), @@ -210,6 +229,7 @@ pub enum Movement { } impl Movement { + // Replay this movement with a possible different `RepeatCount`. fn redo(&self, new: Option<RepeatCount>) -> Movement { match *self { Movement::WholeLine => Movement::WholeLine, @@ -248,8 +268,7 @@ pub struct InputState { input_mode: InputMode, // vi only ? // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 num_args: i16, - last_cmd: Cmd, // vi only - consecutive_insert: bool, + last_cmd: Cmd, // vi only last_char_search: Option<CharSearch>, // vi only } @@ -261,10 +280,10 @@ pub trait Refresher { fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>; /// Vi only, switch to insert mode. fn doing_insert(&mut self); - /// Vi only, start replacing text. - fn doing_replace(&mut self); /// Vi only, switch to command mode. fn done_inserting(&mut self); + /// Vi only, last text inserted. + fn last_insert(&self) -> Option<String>; } impl InputState { @@ -278,7 +297,6 @@ impl InputState { input_mode: InputMode::Insert, num_args: 0, last_cmd: Cmd::Noop, - consecutive_insert: false, last_char_search: None, } } @@ -359,7 +377,7 @@ impl InputState { if let Some(cmd) = self.custom_bindings.borrow().get(&key) { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { - cmd.redo(Some(n)) + cmd.redo(Some(n), wrt) } else { cmd.clone() }); @@ -477,9 +495,9 @@ impl InputState { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { if no_num_args { - cmd.redo(None) + cmd.redo(None, wrt) } else { - cmd.redo(Some(n)) + cmd.redo(Some(n), wrt) } } else { cmd.clone() @@ -490,9 +508,9 @@ impl InputState { KeyPress::End => Cmd::Move(Movement::EndOfLine), KeyPress::Char('.') => { // vi-redo (repeat last command) if no_num_args { - self.last_cmd.redo(None) + self.last_cmd.redo(None, wrt) } else { - self.last_cmd.redo(Some(n)) + self.last_cmd.redo(Some(n), wrt) } }, // TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing bracket @@ -514,16 +532,14 @@ impl InputState { KeyPress::Char('B') => Cmd::Move(Movement::BackwardWord(n, Word::Big)), KeyPress::Char('c') => { self.input_mode = InputMode::Insert; - wrt.doing_replace(); match try!(self.vi_cmd_motion(rdr, wrt, key, n)) { - Some(mvt) => Cmd::Kill(mvt), + Some(mvt) => Cmd::Replace(mvt, None), None => Cmd::Unknown, } } KeyPress::Char('C') => { self.input_mode = InputMode::Insert; - wrt.doing_replace(); - Cmd::Kill(Movement::EndOfLine) + Cmd::Replace(Movement::EndOfLine, None) } KeyPress::Char('d') => { match try!(self.vi_cmd_motion(rdr, wrt, key, n)) { @@ -574,7 +590,7 @@ impl InputState { // vi-replace-char: let ch = try!(rdr.next_key(false)); match ch { - KeyPress::Char(c) => Cmd::Replace(n, c), + KeyPress::Char(c) => Cmd::ReplaceChar(n, c), KeyPress::Esc => Cmd::Noop, _ => Cmd::Unknown, } @@ -582,20 +598,17 @@ impl InputState { KeyPress::Char('R') => { // vi-replace-mode (overwrite-mode) self.input_mode = InputMode::Replace; - wrt.doing_replace(); - Cmd::Noop + Cmd::Noop // FIXME no redo possible } KeyPress::Char('s') => { // vi-substitute-char: self.input_mode = InputMode::Insert; - wrt.doing_replace(); - Cmd::Kill(Movement::ForwardChar(n)) + Cmd::Replace(Movement::ForwardChar(n), None) } KeyPress::Char('S') => { // vi-substitute-line: self.input_mode = InputMode::Insert; - wrt.doing_replace(); - Cmd::Kill(Movement::WholeLine) + Cmd::Replace(Movement::WholeLine, None) } KeyPress::Char('u') => Cmd::Undo(n), // KeyPress::Char('U') => Cmd::???, // revert-line @@ -636,7 +649,7 @@ impl InputState { }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { - self.update_last_cmd(cmd.clone()); + self.last_cmd = cmd.clone(); } Ok(cmd) } @@ -646,7 +659,7 @@ impl InputState { if let Some(cmd) = self.custom_bindings.borrow().get(&key) { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { - cmd.redo(None) + cmd.redo(None, wrt) } else { cmd.clone() }); @@ -669,12 +682,12 @@ impl InputState { }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { - self.update_last_cmd(cmd.clone()); + if let (Cmd::Replace(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) { + // replacing... + } else { + self.last_cmd = cmd.clone(); + } } - self.consecutive_insert = match cmd { - Cmd::SelfInsert(_, _) => true, - _ => false, - }; Ok(cmd) } @@ -858,26 +871,4 @@ impl InputState { num_args.abs() as RepeatCount } } - - fn update_last_cmd(&mut self, new: Cmd) { - // consecutive char inserts are repeatable not only the last one... - if !self.consecutive_insert { - self.last_cmd = new; - } else if let Cmd::SelfInsert(_, c) = new { - match self.last_cmd { - Cmd::SelfInsert(_, pc) => { - let mut text = String::new(); - text.push(pc); - text.push(c); - self.last_cmd = Cmd::Insert(1, text); - } - Cmd::Insert(_, ref mut text) => { - text.push(c); - } - _ => self.last_cmd = new, - } - } else { - self.last_cmd = new; - } - } } diff --git a/src/kill_ring.rs b/src/kill_ring.rs index b587b81d..15b66494 100644 --- a/src/kill_ring.rs +++ b/src/kill_ring.rs @@ -34,13 +34,6 @@ impl KillRing { } } - pub fn start_killing(&mut self) { - self.killing = true; - } - pub fn stop_killing(&mut self) { - self.killing = false; - } - /// Reset `last_action` state. pub fn reset(&mut self) { self.last_action = Action::Other; @@ -113,6 +106,9 @@ impl KillRing { } impl DeleteListener for KillRing { + fn start_killing(&mut self) { + self.killing = true; + } fn delete(&mut self, _: usize, string: &str, dir: Direction) { if !self.killing { return; @@ -123,6 +119,9 @@ impl DeleteListener for KillRing { }; self.kill(string, mode); } + fn stop_killing(&mut self) { + self.killing = false; + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index c7d610bf..e898b343 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -422,12 +422,12 @@ fn readline_edit<H: Helper>( // Move back a character. try!(s.edit_move_backward(n)) } - Cmd::Kill(Movement::ForwardChar(n)) => { - // Delete (forward) one character at point. - try!(s.edit_delete(n)) - } - Cmd::Replace(n, c) => { - try!(s.edit_replace_char(c, n)); + Cmd::ReplaceChar(n, c) => try!(s.edit_replace_char(c, n)), + Cmd::Replace(mvt, text) => { + try!(s.edit_kill(&mvt)); + if let Some(text) = text { + try!(s.edit_insert_text(&text)) + } } Cmd::Overwrite(c) => { try!(s.edit_overwrite_char(c)); @@ -448,22 +448,6 @@ fn readline_edit<H: Helper>( // Move forward a character. try!(s.edit_move_forward(n)) } - Cmd::Kill(Movement::BackwardChar(n)) => { - // Delete one character backward. - try!(s.edit_backspace(n)) - } - Cmd::Kill(Movement::EndOfLine) => { - // Kill the text from point to the end of the line. - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_kill_line()); - editor.kill_ring.borrow_mut().stop_killing(); - } - Cmd::Kill(Movement::WholeLine) => { - try!(s.edit_move_home()); - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_kill_line()); - editor.kill_ring.borrow_mut().stop_killing(); - } Cmd::ClearScreen => { // Clear the screen leaving the current line at the top of the screen. try!(s.out.clear_screen()); @@ -487,12 +471,6 @@ fn readline_edit<H: Helper>( // Exchange the char before cursor with the character at cursor. try!(s.edit_transpose_chars()) } - Cmd::Kill(Movement::BeginningOfLine) => { - // Kill backward from point to the beginning of the line. - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_discard_line()); - editor.kill_ring.borrow_mut().stop_killing(); - } #[cfg(unix)] Cmd::QuotedInsert => { // Quoted insert @@ -505,7 +483,7 @@ fn readline_edit<H: Helper>( try!(s.edit_yank(&input_state, text, anchor, n)) } } - Cmd::ViYankTo(mvt) => if let Some(text) = s.line.copy(mvt) { + Cmd::ViYankTo(ref mvt) => if let Some(text) = s.line.copy(mvt) { editor.kill_ring.borrow_mut().kill(&text, Mode::Append) }, // TODO CTRL-_ // undo @@ -520,12 +498,6 @@ fn readline_edit<H: Helper>( } break; } - Cmd::Kill(Movement::BackwardWord(n, word_def)) => { - // kill one word backward (until start of word) - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_delete_prev_word(word_def, n)); - editor.kill_ring.borrow_mut().stop_killing(); - } Cmd::BeginningOfHistory => { // move to first entry in history try!(s.edit_history(&editor.history, true)) @@ -542,11 +514,8 @@ fn readline_edit<H: Helper>( // capitalize word after point try!(s.edit_word(WordAction::CAPITALIZE)) } - Cmd::Kill(Movement::ForwardWord(n, at, word_def)) => { - // kill one word forward (until start/end of word) - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_delete_word(at, word_def, n)); - editor.kill_ring.borrow_mut().stop_killing(); + Cmd::Kill(ref mvt) => { + try!(s.edit_kill(mvt)); } Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { // move forwards one word @@ -571,11 +540,6 @@ fn readline_edit<H: Helper>( } } Cmd::Move(Movement::ViCharSearch(n, cs)) => try!(s.edit_move_to(cs, n)), - Cmd::Kill(Movement::ViCharSearch(n, cs)) => { - editor.kill_ring.borrow_mut().start_killing(); - try!(s.edit_delete_to(cs, n)); - editor.kill_ring.borrow_mut().stop_killing(); - } Cmd::Undo(n) => { s.line.remove_change_listener(); if s.changes.borrow_mut().undo(&mut s.line, n) { diff --git a/src/line_buffer.rs b/src/line_buffer.rs index af7a4b3b..31fd672a 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -34,7 +34,9 @@ impl Default for Direction { /// Listener to be notified when some text is deleted. pub trait DeleteListener { + fn start_killing(&mut self); fn delete(&mut self, idx: usize, string: &str, dir: Direction); + fn stop_killing(&mut self); } /// Listener to be notified when the line is modified. @@ -275,8 +277,8 @@ impl LineBuffer { } /// Delete the character at the right of the cursor without altering the - /// cursor - /// position. Basically this is what happens with the "Delete" keyboard key. + /// cursor position. Basically this is what happens with the "Delete" + /// keyboard key. /// Return the number of characters deleted. pub fn delete(&mut self, n: RepeatCount) -> Option<String> { match self.next_pos(n) { @@ -689,11 +691,11 @@ impl LineBuffer { /// Return the content between current cursor position and `mvt` position. /// Return `None` when the buffer is empty or when the movement fails. - pub fn copy(&self, mvt: Movement) -> Option<String> { + pub fn copy(&self, mvt: &Movement) -> Option<String> { if self.is_empty() { return None; } - match mvt { + match *mvt { Movement::WholeLine => Some(self.buf.clone()), Movement::BeginningOfLine => if self.pos == 0 { None @@ -757,6 +759,59 @@ impl LineBuffer { }, } } + + pub fn kill(&mut self, mvt: &Movement) -> bool { + let notify = match *mvt { + Movement::ForwardChar(_) => false, + Movement::BackwardChar(_) => false, + _ => true, + }; + if notify { + if let Some(dl) = self.dl.as_ref() { + dl.borrow_mut().start_killing() + } + } + let killed = match *mvt { + Movement::ForwardChar(n) => { + // Delete (forward) `n` characters at point. + self.delete(n).is_some() + } + Movement::BackwardChar(n) => { + // Delete `n` characters backward. + self.backspace(n) + } + Movement::EndOfLine => { + // Kill the text from point to the end of the line. + self.kill_line() + } + Movement::WholeLine => { + self.move_home(); + self.kill_line() + } + Movement::BeginningOfLine => { + // Kill backward from point to the beginning of the line. + self.discard_line() + } + Movement::BackwardWord(n, word_def) => { + // kill `n` words backward (until start of word) + self.delete_prev_word(word_def, n) + } + Movement::ForwardWord(n, at, word_def) => { + // kill `n` words forward (until start/end of word) + self.delete_word(at, word_def, n) + } + Movement::ViCharSearch(n, cs) => self.delete_to(cs, n), + Movement::ViFirstPrint => { + false // TODO + } + }; + if notify { + if let Some(dl) = self.dl.as_ref() { + dl.borrow_mut().stop_killing() + } + } + killed + } } impl Deref for LineBuffer { @@ -814,9 +869,11 @@ mod test { } impl DeleteListener for Listener { + fn start_killing(&mut self) {} fn delete(&mut self, _: usize, string: &str, _: Direction) { self.deleted_str = Some(string.to_owned()); } + fn stop_killing(&mut self) {} } impl ChangeListener for Listener { fn insert_char(&mut self, _: usize, _: char) {} diff --git a/src/tty/unix.rs b/src/tty/unix.rs index aaba2416..b1d5b788 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -263,6 +263,7 @@ impl PosixRawReader { } // https://tools.ietf.org/html/rfc3629 +#[cfg_attr(rustfmt, rustfmt_skip)] static UTF8_CHAR_WIDTH: [u8; 256] = [ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1F diff --git a/src/undo.rs b/src/undo.rs index a0a2553a..8e865b83 100644 --- a/src/undo.rs +++ b/src/undo.rs @@ -310,12 +310,29 @@ impl Changeset { } redone } + + pub fn last_insert(&self) -> Option<String> { + for change in self.undos.iter().rev() { + match change { + Change::Insert { ref text, .. } => return Some(text.clone()), + Change::End => { + continue; + } + _ => { + return None; + } + } + } + None + } } impl DeleteListener for Changeset { + fn start_killing(&mut self) {} fn delete(&mut self, idx: usize, string: &str, _: Direction) { self.delete(idx, string); } + fn stop_killing(&mut self) {} } impl ChangeListener for Changeset { fn insert_char(&mut self, idx: usize, c: char) { @@ -436,4 +453,15 @@ mod tests { cs.redo(&mut buf); assert_eq!(buf.as_str(), "Hi, world!"); } + + #[test] + fn test_last_insert() { + let mut cs = Changeset::new(); + cs.begin(); + cs.delete(0, "Hello"); + cs.insert_str(0, "Bye"); + cs.end(); + let insert = cs.last_insert(); + assert_eq!(Some("Bye".to_owned()), insert); + } } -- GitLab