diff --git a/doc_src/interactive.rst b/doc_src/interactive.rst index cda77a711..3f5d5e2cf 100644 --- a/doc_src/interactive.rst +++ b/doc_src/interactive.rst @@ -557,6 +557,10 @@ Visual mode - :kbd:`~` toggles the case (upper/lower) on the selection, and enters :ref:`command mode `. +- :kbd:`g,u` lowercases the selection, and enters :ref:`command mode `. + +- :kbd:`g,U` uppercases the selection, and enters :ref:`command mode `. + - :kbd:`",*,y` copies the selection to the clipboard, and enters :ref:`command mode `. .. _custom-binds: diff --git a/share/functions/fish_vi_key_bindings.fish b/share/functions/fish_vi_key_bindings.fish index 83242aafb..6b8e7026d 100644 --- a/share/functions/fish_vi_key_bindings.fish +++ b/share/functions/fish_vi_key_bindings.fish @@ -317,7 +317,8 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind -s --preset -M visual -m default '",*,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" bind -s --preset -M visual -m default '",+,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" bind -s --preset -M visual -m default '~' togglecase-selection end-selection repaint-mode - bind -s --preset -M visual -m default g,U togglecase-selection end-selection repaint-mode + bind -s --preset -M visual -m default g,u downcase-selection end-selection repaint-mode + bind -s --preset -M visual -m default g,U upcase-selection end-selection repaint-mode bind -s --preset -M visual -m default ctrl-c end-selection repaint-mode bind -s --preset -M visual -m default escape end-selection repaint-mode diff --git a/src/input.rs b/src/input.rs index bf87aa6f7..ee0e2dac4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -144,6 +144,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat make_md(L!("delete-char"), ReadlineCmd::DeleteChar), make_md(L!("delete-or-exit"), ReadlineCmd::DeleteOrExit), make_md(L!("down-line"), ReadlineCmd::DownLine), + make_md(L!("downcase-selection"), ReadlineCmd::DowncaseSelection), make_md(L!("downcase-word"), ReadlineCmd::DowncaseWord), make_md(L!("end-of-buffer"), ReadlineCmd::EndOfBuffer), make_md(L!("end-of-history"), ReadlineCmd::EndOfHistory), @@ -205,6 +206,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat make_md(L!("transpose-words"), ReadlineCmd::TransposeWords), make_md(L!("undo"), ReadlineCmd::Undo), make_md(L!("up-line"), ReadlineCmd::UpLine), + make_md(L!("upcase-selection"), ReadlineCmd::UpcaseSelection), make_md(L!("upcase-word"), ReadlineCmd::UpcaseWord), make_md(L!("yank"), ReadlineCmd::Yank), make_md(L!("yank-pop"), ReadlineCmd::YankPop), diff --git a/src/input_common.rs b/src/input_common.rs index 9cc69e802..49f00c892 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -108,6 +108,8 @@ pub enum ReadlineCmd { DowncaseWord, CapitalizeWord, TogglecaseChar, + UpcaseSelection, + DowncaseSelection, TogglecaseSelection, Execute, BeginningOfBuffer, diff --git a/src/reader.rs b/src/reader.rs index f6201fdb9..0f5dad50a 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -3571,6 +3571,33 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { self.update_buff_pos(elt, Some(buff_pos)); } } + rl::UpcaseSelection | rl::DowncaseSelection => { + let (elt, el) = self.active_edit_line(); + + // Check that we have an active selection and get the bounds. + if let Some(selection) = self.get_selection() { + let text = &el.text().as_char_slice()[selection.clone()]; + let replacement = if c == rl::UpcaseSelection { + WString::from_iter(text.iter().flat_map(|c| c.to_uppercase())) + } else { + WString::from_iter(text.iter().flat_map(|c| c.to_lowercase())) + }; + + let buff_pos = el.position(); + self.replace_substring(elt, selection, replacement); + + // Restore the buffer position since replace_substring moves + // the buffer position ahead of the replaced text. + // Note: This does not take string length changes into account. + // E.g.: When the cursor was at the right of the selection, + // the selection contains 'ẞ', which is uppercased into 'SS', + // the cursor will stay at the same offset, but it will not be on the same + // character as before. + // The position calculations work on codepoints rather than graphemes, which can + // result in additional issues. + self.update_buff_pos(elt, Some(buff_pos)); + } + } rl::UpcaseWord | rl::DowncaseWord | rl::CapitalizeWord => { let (elt, el) = self.active_edit_line(); // For capitalize_word, whether we've capitalized a character so far.