diff --git a/README.md b/README.md index c2e762db0..d44e8ba84 100644 --- a/README.md +++ b/README.md @@ -173,45 +173,46 @@ The tool has built-in help that can be accessed by using the `?` key. Key bindings can be customized, see [configuration](readme/customization.md#key-bindings) for all key bindings and information on configuring. -| Key | Mode | Description | -|-------------|-------------|-------------------------------------------| -| `?` | All | Show help | -| Up | Normal/Diff | Move selection up | -| Down | Normal/Diff | Move selection down | -| Page Up | Normal/Diff | Move selection up five lines | -| Page Down | Normal/Diff | Move selection down five lines | -| Home | Normal/Diff | Move selection to start of list | -| End | Normal/Diff | Move selection to home of list | -| `q` | Normal/Diff | Abort interactive rebase | -| `Q` | Normal/Diff | Immediately abort interactive rebase | -| `w` | Normal/Diff | Write interactive rebase file | -| `W` | Normal/Diff | Immediately write interactive rebase file | -| `j` | Normal/Diff | Move selected commit(s) down | -| `k` | Normal/Diff | Move selected commit(s) up | -| `b` | Normal | Toggle break action | -| `p` | Normal/Diff | Set selected commit(s) to be picked | -| `r` | Normal/Diff | Set selected commit(s) to be reworded | -| `e` | Normal/Diff | Set selected commit(s) to be edited | -| `s` | Normal/Diff | Set selected commit(s) to be squashed | -| `f` | Normal/Diff | Set selected commit(s) to be fixed-up | -| `d` | Normal | Set selected commit(s) to be dropped | -| `d` | Diff | Show full commit diff | -| `E` | Normal | Edit the command of an editable action | -| `v` | Normal/Diff | Enter and exit visual mode (for selection)| -| `I` | Normal | Insert a new line | -| `Delete` | Normal/Diff | Remove selected lines | -| `!` | Normal/Diff | Open todo file in external editor | -| `Control+z` | Normal/Diff | Undo the previous change | -| `Control+y` | Normal/Diff | Redo the previously undone change | -| `c` | Normal/Diff | Show commit information | -| Down | Diff | Scroll view down | -| Up | Diff | Scroll view up | -| Left | Diff | Scroll view left | -| Right | Diff | Scroll view right | -| Home | Diff | Scroll view to the top | -| End | Diff | Scroll view to the end | -| PageUp | Diff | Scroll view a step up | -| PageDown | Diff | Scroll view a step down | +| Key | Mode | Description | +|-------------|-------------|--------------------------------------------| +| `?` | All | Show help | +| Up | Normal/Diff | Move selection up | +| Down | Normal/Diff | Move selection down | +| Page Up | Normal/Diff | Move selection up five lines | +| Page Down | Normal/Diff | Move selection down five lines | +| Home | Normal/Diff | Move selection to start of list | +| End | Normal/Diff | Move selection to home of list | +| `q` | Normal/Diff | Abort interactive rebase | +| `Q` | Normal/Diff | Immediately abort interactive rebase | +| `w` | Normal/Diff | Write interactive rebase file | +| `W` | Normal/Diff | Immediately write interactive rebase file | +| `j` | Normal/Diff | Move selected commit(s) down | +| `k` | Normal/Diff | Move selected commit(s) up | +| `b` | Normal | Toggle break action | +| `p` | Normal/Diff | Set selected commit(s) to be picked | +| `r` | Normal/Diff | Set selected commit(s) to be reworded | +| `e` | Normal/Diff | Set selected commit(s) to be edited | +| `s` | Normal/Diff | Set selected commit(s) to be squashed | +| `f` | Normal/Diff | Set selected commit(s) to be fixed-up | +| `d` | Normal | Set selected commit(s) to be dropped | +| `d` | Diff | Show full commit diff | +| `E` | Normal | Edit the command of an editable action | +| `v` | Normal/Diff | Enter and exit visual mode (for selection) | +| `I` | Normal | Insert a new line | +| `Control+d` | Normal | Duplicate the selected line | +| `Delete` | Normal/Diff | Remove selected lines | +| `!` | Normal/Diff | Open todo file in external editor | +| `Control+z` | Normal/Diff | Undo the previous change | +| `Control+y` | Normal/Diff | Redo the previously undone change | +| `c` | Normal/Diff | Show commit information | +| Down | Diff | Scroll view down | +| Up | Diff | Scroll view up | +| Left | Diff | Scroll view left | +| Right | Diff | Scroll view right | +| Home | Diff | Scroll view to the top | +| End | Diff | Scroll view to the end | +| PageUp | Diff | Scroll view a step up | +| PageDown | Diff | Scroll view a step down | ## Supported Platforms diff --git a/readme/customization.md b/readme/customization.md index c57573daa..792b5127a 100644 --- a/readme/customization.md +++ b/readme/customization.md @@ -122,6 +122,7 @@ Most keys can be changed to any printable character or supported special charact | `inputForceRebase` | W | String | Key for forcing a rebase | | `inputHelp` | ? | String | Key for showing the help | | `inputInsertLine` | I | String | Key for inserting a new line | +| `inputDuplicateLine` | Control+d | String | Key for duplicating the selected line | | `inputMoveDown` | Down | String | Key for moving the cursor down | | `inputMoveEnd` | End | String | Key for moving the cursor to the end of the list | | `inputMoveHome` | Home | String | Key for moving the cursor to the top of the list | diff --git a/src/config/key_bindings.rs b/src/config/key_bindings.rs index 4fd52b0f5..8bceb13bf 100644 --- a/src/config/key_bindings.rs +++ b/src/config/key_bindings.rs @@ -46,6 +46,8 @@ pub(crate) struct KeyBindings { pub(crate) help: Vec, /// Key bindings for inserting a line. pub(crate) insert_line: Vec, + /// Key bindings for duplicating a line. + pub(crate) duplicate_line: Vec, /// Key bindings for moving down. pub(crate) move_down: Vec, @@ -139,6 +141,7 @@ impl KeyBindings { force_rebase: get_input(git_config, "interactive-rebase-tool.inputForceRebase", "W")?, help: get_input(git_config, "interactive-rebase-tool.inputHelp", "?")?, insert_line: get_input(git_config, "interactive-rebase-tool.insertLine", "I")?, + duplicate_line: get_input(git_config, "interactive-rebase-tool.inputDuplicateLine", "control+d")?, move_down: get_input(git_config, "interactive-rebase-tool.inputMoveDown", "Down")?, move_end: get_input(git_config, "interactive-rebase-tool.inputMoveEnd", "End")?, move_home: get_input(git_config, "interactive-rebase-tool.inputMoveHome", "Home")?, @@ -253,6 +256,7 @@ mod tests { config_test!(force_rebase, "inputForceRebase", "W"); config_test!(help, "inputHelp", "?"); config_test!(insert_line, "insertLine", "I"); + config_test!(duplicate_line, "inputDuplicateLine", "Controld"); config_test!(move_down, "inputMoveDown", "Down"); config_test!(move_end, "inputMoveEnd", "End"); config_test!(move_home, "inputMoveHome", "Home"); diff --git a/src/input/key_bindings.rs b/src/input/key_bindings.rs index e77778145..33542fa8e 100644 --- a/src/input/key_bindings.rs +++ b/src/input/key_bindings.rs @@ -62,6 +62,8 @@ pub(crate) struct KeyBindings { pub(crate) force_rebase: Vec, /// Key bindings for inserting a line. pub(crate) insert_line: Vec, + /// Key bindings for inserting a line. + pub(crate) duplicate_line: Vec, /// Key bindings for moving down. pub(crate) move_down: Vec, /// Key bindings for moving down a step. @@ -131,6 +133,7 @@ impl KeyBindings { force_abort: map_keybindings(&key_bindings.force_abort), force_rebase: map_keybindings(&key_bindings.force_rebase), insert_line: map_keybindings(&key_bindings.insert_line), + duplicate_line: map_keybindings(&key_bindings.duplicate_line), move_down: map_keybindings(&key_bindings.move_down), move_down_step: map_keybindings(&key_bindings.move_down_step), move_end: map_keybindings(&key_bindings.move_end), diff --git a/src/input/standard_event.rs b/src/input/standard_event.rs index 6543558a2..eed6ec7c8 100644 --- a/src/input/standard_event.rs +++ b/src/input/standard_event.rs @@ -95,6 +95,8 @@ pub(crate) enum StandardEvent { ToggleVisualMode, /// The insert line meta event. InsertLine, + /// The duplicate line meta event. + DuplicateLine, /// Fixup specific action to toggle the c option. FixupKeepMessage, /// Fixup specific action to toggle the C option. diff --git a/src/modules/list.rs b/src/modules/list.rs index 4f586232d..5bd7fea9e 100644 --- a/src/modules/list.rs +++ b/src/modules/list.rs @@ -421,6 +421,18 @@ impl List { results.state(State::Insert); } + fn duplicate_line(&mut self) { + let mut todo_file = self.todo_file.lock(); + + if let Some(selected_line) = todo_file.get_selected_line() { + if selected_line.is_duplicatable() { + let new_line = selected_line.clone(); + let selected_line_index = todo_file.get_selected_line_index(); + todo_file.add_line(selected_line_index + 1, new_line); + } + } + } + fn update_list_view_data(&mut self, context: &RenderContext) -> &ViewData { let todo_file = self.todo_file.lock(); let is_visual_mode = self.state == ListState::Visual; @@ -558,6 +570,7 @@ impl List { e if key_bindings.force_abort.contains(&e) => Event::from(StandardEvent::ForceAbort), e if key_bindings.force_rebase.contains(&e) => Event::from(StandardEvent::ForceRebase), e if key_bindings.insert_line.contains(&e) => Event::from(StandardEvent::InsertLine), + e if key_bindings.duplicate_line.contains(&e) => Event::from(StandardEvent::DuplicateLine), e if key_bindings.move_down.contains(&e) => Event::from(StandardEvent::MoveCursorDown), e if key_bindings.move_down_step.contains(&e) => Event::from(StandardEvent::MoveCursorPageDown), e if key_bindings.move_end.contains(&e) => Event::from(StandardEvent::MoveCursorEnd), @@ -695,6 +708,7 @@ impl List { StandardEvent::ActionBreak => self.action_break(), StandardEvent::Edit => self.edit(), StandardEvent::InsertLine => self.insert_line(&mut results), + StandardEvent::DuplicateLine => self.duplicate_line(), StandardEvent::ShowCommit => self.show_commit(&mut results), StandardEvent::FixupKeepMessage => self.toggle_option("-C"), StandardEvent::FixupKeepMessageWithEditor => self.toggle_option("-c"), diff --git a/src/modules/list/tests.rs b/src/modules/list/tests.rs index af0c56c31..092a35fda 100644 --- a/src/modules/list/tests.rs +++ b/src/modules/list/tests.rs @@ -1,6 +1,7 @@ mod abort_and_rebase; mod activate; mod change_action; +mod duplicate_line; mod edit_mode; mod external_editor; mod help; diff --git a/src/modules/list/tests/duplicate_line.rs b/src/modules/list/tests/duplicate_line.rs new file mode 100644 index 000000000..5942dce72 --- /dev/null +++ b/src/modules/list/tests/duplicate_line.rs @@ -0,0 +1,43 @@ +use super::*; +use crate::{action_line, assert_rendered_output, assert_results, process::Artifact}; + +#[test] +fn duplicate_line_duplicatable() { + testers::module( + &["pick aaa c1"], + &[Event::from(StandardEvent::DuplicateLine)], + None, + |mut test_context| { + let mut module = List::new(&test_context.app_data()); + assert_results!( + test_context.handle_event(&mut module), + Artifact::Event(Event::from(StandardEvent::DuplicateLine)) + ); + assert_rendered_output!( + Body test_context.build_view_data(&mut module), + action_line!(Selected Pick "aaa", "c1"), + action_line!(Pick "aaa", "c1") + ); + }, + ); +} + +#[test] +fn duplicate_line_not_duplicatable() { + testers::module( + &["break"], + &[Event::from(StandardEvent::DuplicateLine)], + None, + |mut test_context| { + let mut module = List::new(&test_context.app_data()); + assert_results!( + test_context.handle_event(&mut module), + Artifact::Event(Event::from(StandardEvent::DuplicateLine)) + ); + assert_rendered_output!( + Body test_context.build_view_data(&mut module), + action_line!(Selected Break) + ); + }, + ); +} diff --git a/src/modules/list/tests/help.rs b/src/modules/list/tests/help.rs index 1ebf7083e..aa828780a 100644 --- a/src/modules/list/tests/help.rs +++ b/src/modules/list/tests/help.rs @@ -43,6 +43,7 @@ fn normal_mode_help() { " d |Set selected commits to be dropped", " E |Edit an exec, label, reset or merge action's content", " I |Insert a new line", + " Controld|Duplicate selected line", " Delete |Completely remove the selected lines", " Controlz|Undo the last change", " Controly|Redo the previous undone change", diff --git a/src/modules/list/utils.rs b/src/modules/list/utils.rs index 78f76b324..165d49522 100644 --- a/src/modules/list/utils.rs +++ b/src/modules/list/utils.rs @@ -137,6 +137,11 @@ fn build_help_lines(key_bindings: &KeyBindings, selector: HelpLinesSelector) -> "Insert a new line", HelpLinesSelector::Normal, ), + ( + &key_bindings.duplicate_line, + "Duplicate selected line", + HelpLinesSelector::Normal, + ), ( &key_bindings.remove_line, "Completely remove the selected lines", diff --git a/src/test_helpers/create_test_keybindings.rs b/src/test_helpers/create_test_keybindings.rs index dcc51e0ec..b3b4b9ea1 100644 --- a/src/test_helpers/create_test_keybindings.rs +++ b/src/test_helpers/create_test_keybindings.rs @@ -31,6 +31,7 @@ pub(crate) fn create_test_keybindings() -> KeyBindings { force_abort: map_keybindings(&[String::from("Q")]), force_rebase: map_keybindings(&[String::from("W")]), insert_line: map_keybindings(&[String::from("I")]), + duplicate_line: map_keybindings(&[String::from("ControlD")]), move_down: map_keybindings(&[String::from("Down")]), move_down_step: map_keybindings(&[String::from("PageDown")]), move_end: map_keybindings(&[String::from("End")]), diff --git a/src/todo_file/line.rs b/src/todo_file/line.rs index 3acdf1e14..818904138 100644 --- a/src/todo_file/line.rs +++ b/src/todo_file/line.rs @@ -202,6 +202,25 @@ impl Line { } } + /// Can this line be duplicated. + #[must_use] + pub(crate) const fn is_duplicatable(&self) -> bool { + match self.action { + Action::Exec + | Action::Label + | Action::Reset + | Action::Merge + | Action::UpdateRef + | Action::Drop + | Action::Edit + | Action::Fixup + | Action::Pick + | Action::Reword + | Action::Squash => true, + Action::Break | Action::Noop => false, + } + } + /// Has this line been modified #[must_use] pub(crate) fn is_modified(&self) -> bool { @@ -563,6 +582,25 @@ mod tests { assert_eq!(line.is_editable(), editable); } + #[rstest] + #[case::drop(Action::Break, false)] + #[case::drop(Action::Drop, true)] + #[case::edit(Action::Edit, true)] + #[case::exec(Action::Exec, true)] + #[case::fixup(Action::Fixup, true)] + #[case::pick(Action::Noop, false)] + #[case::pick(Action::Pick, true)] + #[case::reword(Action::Reword, true)] + #[case::squash(Action::Squash, true)] + #[case::label(Action::Label, true)] + #[case::reset(Action::Reset, true)] + #[case::merge(Action::Merge, true)] + #[case::update_ref(Action::UpdateRef, true)] + fn is_duplicatable(#[case] from: Action, #[case] duplicatable: bool) { + let line = Line::parse(format!("{from} aaa bbb").as_str()).unwrap(); + assert_eq!(line.is_duplicatable(), duplicatable); + } + #[rstest] #[case::break_action("break")] #[case::drop("drop aaa comment")]