From 25a57e4920e109cca6003ea33bdbedc5324b6297 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Wed, 9 Nov 2022 23:26:14 -0500 Subject: [PATCH 1/9] Add PasteType struct --- helix-term/src/commands.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 446337411..026daa3b3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4624,6 +4624,15 @@ enum Paste { static LINE_ENDING_REGEX: Lazy = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); +/// Enumerates the possible ways to paste +#[derive(Copy, Clone)] +enum PasteType { + /// Paste one value in the register per selection + Default, + /// Paste every value in the register for every selection + All, +} + fn paste_impl( values: &[String], doc: &mut Document, @@ -4631,6 +4640,7 @@ fn paste_impl( action: Paste, count: usize, mode: Mode, + paste_type: PasteType, ) { if values.is_empty() { return; @@ -4714,7 +4724,15 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) { Mode::Normal => Paste::Before, }; let (view, doc) = current!(cx.editor); - paste_impl(&[contents], doc, view, paste, count, cx.editor.mode); + paste_impl( + &[contents], + doc, + view, + paste, + count, + cx.editor.mode, + PasteType::Default, + ); exit_select_mode(cx); } @@ -4801,14 +4819,14 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) { exit_select_mode(cx); } -fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) { +fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize, paste_type: PasteType) { let Some(values) = editor.registers.read(register, editor) else { return; }; let values: Vec<_> = values.map(|value| value.to_string()).collect(); let (view, doc) = current!(editor); - paste_impl(&values, doc, view, pos, count, editor.mode); + paste_impl(&values, doc, view, pos, count, editor.mode, paste_type); } fn paste_after(cx: &mut Context) { @@ -4818,6 +4836,7 @@ fn paste_after(cx: &mut Context) { .unwrap_or(cx.editor.config().default_yank_register), Paste::After, cx.count(), + PasteType::Default, ); exit_select_mode(cx); } @@ -4829,6 +4848,7 @@ fn paste_before(cx: &mut Context) { .unwrap_or(cx.editor.config().default_yank_register), Paste::Before, cx.count(), + PasteType::Default, ); exit_select_mode(cx); } @@ -5693,6 +5713,7 @@ fn insert_register(cx: &mut Context) { .unwrap_or(cx.editor.config().default_yank_register), Paste::Cursor, cx.count(), + PasteType::Default, ); } }) From 9bb9ce8e77b8024c992b9e7c75e8b0cabbe04037 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Wed, 9 Nov 2022 23:26:27 -0500 Subject: [PATCH 2/9] Add paste_after_all and paste_before_all --- helix-term/src/commands.rs | 94 +++++++++++++++++++++++++++----- helix-term/src/keymap/default.rs | 4 +- 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 026daa3b3..769a82e35 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -488,6 +488,8 @@ impl MappableCommand { replace_with_yanked, "Replace with yanked text", replace_selections_with_clipboard, "Replace selections by clipboard content", replace_selections_with_primary_clipboard, "Replace selections by primary clipboard", + paste_after_all, "Paste all after selection", + paste_before_all, "Paste all before selection", paste_after, "Paste after selection", paste_before, "Paste before selection", paste_clipboard_after, "Paste clipboard after selections", @@ -4669,7 +4671,7 @@ fn paste_impl( map_value(values.last().unwrap()), ); - let mut values = values.iter().map(|value| map_value(value)).chain(repeat); + let mut values_iter = values.iter().map(|value| map_value(value)).chain(repeat); let text = doc.text(); let selection = doc.selection(view.id); @@ -4677,6 +4679,15 @@ fn paste_impl( let mut offset = 0; let mut ranges = SmallVec::with_capacity(selection.len()); + // Precompute the combined tendril value if we will be doing a PasteType::All + let combined_tendril_value = match paste_type { + PasteType::All => Some(values.iter().fold(Tendril::default(), |mut acc, v| { + acc = acc + v; + acc + })), + PasteType::Default => None, + }; + let mut transaction = Transaction::change_by_selection(text, selection, |range| { let pos = match (action, linewise) { // paste linewise before @@ -4694,17 +4705,38 @@ fn paste_impl( (Paste::Cursor, _) => range.cursor(text.slice(..)), }; - let value = values.next(); + let value = match paste_type { + PasteType::All => { + // Iterate over every value and add its range + for value in values { + let value_len = value.chars().count(); + let anchor = offset + pos; - let value_len = value - .as_ref() - .map(|content| content.chars().count()) - .unwrap_or_default(); - let anchor = offset + pos; + let new_range = + Range::new(anchor, anchor + value_len).with_direction(range.direction()); + ranges.push(new_range); + offset += value_len; + } - let new_range = Range::new(anchor, anchor + value_len).with_direction(range.direction()); - ranges.push(new_range); - offset += value_len; + // Return the precomputed tendril value + combined_tendril_value.clone() + } + PasteType::Default => { + let value: Option = values_iter.next(); + + let value_len = value + .as_ref() + .map(|content| content.chars().count()) + .unwrap_or_default(); + let anchor = offset + pos; + + let new_range = + Range::new(anchor, anchor + value_len).with_direction(range.direction()); + ranges.push(new_range); + offset += value_len; + value + } + }; (pos, pos, value) }); @@ -4737,22 +4769,34 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) { } fn paste_clipboard_after(cx: &mut Context) { - paste(cx.editor, '+', Paste::After, cx.count()); + paste(cx.editor, '+', Paste::After, cx.count(), PasteType::Default); exit_select_mode(cx); } fn paste_clipboard_before(cx: &mut Context) { - paste(cx.editor, '+', Paste::Before, cx.count()); + paste( + cx.editor, + '+', + Paste::Before, + cx.count(), + PasteType::Default, + ); exit_select_mode(cx); } fn paste_primary_clipboard_after(cx: &mut Context) { - paste(cx.editor, '*', Paste::After, cx.count()); + paste(cx.editor, '*', Paste::After, cx.count(), PasteType::Default); exit_select_mode(cx); } fn paste_primary_clipboard_before(cx: &mut Context) { - paste(cx.editor, '*', Paste::Before, cx.count()); + paste( + cx.editor, + '*', + Paste::Before, + cx.count(), + PasteType::Default, + ); exit_select_mode(cx); } @@ -4829,6 +4873,28 @@ fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize, paste_ty paste_impl(&values, doc, view, pos, count, editor.mode, paste_type); } +fn paste_after_all(cx: &mut Context) { + paste( + cx, + cx.register + .unwrap_or(cx.editor.config().default_yank_register), + Paste::After, + PasteType::All, + ); + exit_select_mode(cx); +} + +fn paste_before_all(cx: &mut Context) { + paste( + cx, + cx.register + .unwrap_or(cx.editor.config().default_yank_register), + Paste::Before, + PasteType::All, + ); + exit_select_mode(cx); +} + fn paste_after(cx: &mut Context) { paste( cx.editor, diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 82baf336f..ad856ce0e 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -152,8 +152,10 @@ pub fn default() -> HashMap { "y" => yank, // yank_all "p" => paste_after, - // paste_all + // TODO: This is a terrible mapping, but "A-p" coincides with "select_prev_sibling" + "C-p" => paste_after_all, "P" => paste_before, + "A-P" => paste_before_all, "Q" => record_macro, "q" => replay_macro, From f7e2620987d8d42ad692276f8e2207dae0f4b473 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Wed, 9 Nov 2022 23:34:59 -0500 Subject: [PATCH 3/9] Use a collector for the tendril over a manual fold --- helix-term/src/commands.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 769a82e35..e84d244b1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4681,10 +4681,7 @@ fn paste_impl( // Precompute the combined tendril value if we will be doing a PasteType::All let combined_tendril_value = match paste_type { - PasteType::All => Some(values.iter().fold(Tendril::default(), |mut acc, v| { - acc = acc + v; - acc - })), + PasteType::All => Some(values.iter().collect::()), PasteType::Default => None, }; From 4f681e89279fad26c259b453cf22a63036670765 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Wed, 9 Nov 2022 23:37:59 -0500 Subject: [PATCH 4/9] Update keymap --- book/src/keymap.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/keymap.md b/book/src/keymap.md index 10257378c..3891aa404 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -86,6 +86,8 @@ Normal mode is the default mode when you launch helix. You can return to it from | `y` | Yank selection | `yank` | | `p` | Paste after selection | `paste_after` | | `P` | Paste before selection | `paste_before` | +| `Alt-p` | Paste all after selection | `paste_after_all` +| `Alt-P` | Paste all before selection | `paste_before_all` | `"` `` | Select a register to yank to or paste from | `select_register` | | `>` | Indent selection | `indent` | | `<` | Unindent selection | `unindent` | From 0be042c7eeee3bbfa593c271aacc1774c213e1c5 Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Thu, 5 Jun 2025 19:55:47 -0700 Subject: [PATCH 5/9] Update paste_all. --- helix-term/src/commands.rs | 14 ++++++++------ helix-term/src/commands/typed.rs | 8 ++++---- helix-term/src/keymap/default.rs | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e84d244b1..14638d2b8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -488,8 +488,8 @@ impl MappableCommand { replace_with_yanked, "Replace with yanked text", replace_selections_with_clipboard, "Replace selections by clipboard content", replace_selections_with_primary_clipboard, "Replace selections by primary clipboard", - paste_after_all, "Paste all after selection", - paste_before_all, "Paste all before selection", + paste_all_selections_after, "Paste all after selection", + paste_all_selections_before, "Paste all before selection", paste_after, "Paste after selection", paste_before, "Paste before selection", paste_clipboard_after, "Paste clipboard after selections", @@ -4870,23 +4870,25 @@ fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize, paste_ty paste_impl(&values, doc, view, pos, count, editor.mode, paste_type); } -fn paste_after_all(cx: &mut Context) { +fn paste_all_selections_after(cx: &mut Context) { paste( - cx, + cx.editor, cx.register .unwrap_or(cx.editor.config().default_yank_register), Paste::After, + cx.count(), PasteType::All, ); exit_select_mode(cx); } -fn paste_before_all(cx: &mut Context) { +fn paste_all_selections_before(cx: &mut Context) { paste( - cx, + cx.editor, cx.register .unwrap_or(cx.editor.config().default_yank_register), Paste::Before, + cx.count(), PasteType::All, ); exit_select_mode(cx); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2013a9d81..12a2b1e5a 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1039,7 +1039,7 @@ fn paste_clipboard_after( return Ok(()); } - paste(cx.editor, '+', Paste::After, 1); + paste(cx.editor, '+', Paste::After, 1, PasteType::Default); Ok(()) } @@ -1052,7 +1052,7 @@ fn paste_clipboard_before( return Ok(()); } - paste(cx.editor, '+', Paste::Before, 1); + paste(cx.editor, '+', Paste::Before, 1, PasteType::Default); Ok(()) } @@ -1065,7 +1065,7 @@ fn paste_primary_clipboard_after( return Ok(()); } - paste(cx.editor, '*', Paste::After, 1); + paste(cx.editor, '*', Paste::After, 1, PasteType::Default); Ok(()) } @@ -1078,7 +1078,7 @@ fn paste_primary_clipboard_before( return Ok(()); } - paste(cx.editor, '*', Paste::Before, 1); + paste(cx.editor, '*', Paste::Before, 1, PasteType::Default); Ok(()) } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index ad856ce0e..38d65558f 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -153,9 +153,9 @@ pub fn default() -> HashMap { // yank_all "p" => paste_after, // TODO: This is a terrible mapping, but "A-p" coincides with "select_prev_sibling" - "C-p" => paste_after_all, + "C-p" => paste_all_selections_after, "P" => paste_before, - "A-P" => paste_before_all, + "A-P" => paste_all_selections_before, "Q" => record_macro, "q" => replay_macro, From e9975c19399be41cae42f73137e6b55305f3c580 Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Thu, 5 Jun 2025 19:58:14 -0700 Subject: [PATCH 6/9] Choose a more meaningful primary index after the operation. --- helix-term/src/commands.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 14638d2b8..08e033011 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4739,7 +4739,16 @@ fn paste_impl( }); if mode == Mode::Normal { - transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); + transaction = transaction.with_selection(Selection::new( + ranges, + match paste_type { + PasteType::All => { + // Last selection pasted for the previous primary selection + (selection.primary_index() * values.len()) + (values.len() - 1) + } + PasteType::Default => selection.primary_index(), + }, + )); } doc.apply(&transaction, view.id); From 3458b7b0375a39d77d4285ffc4efc6561ae55102 Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Thu, 5 Jun 2025 21:43:39 -0700 Subject: [PATCH 7/9] Add unit tests for paste_all_selections_(before|after). --- helix-term/tests/test/commands.rs | 1 + .../test/commands/paste_all_selections.rs | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 helix-term/tests/test/commands/paste_all_selections.rs diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 29f76cfb8..bb5487f02 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -4,6 +4,7 @@ use super::*; mod insert; mod movement; +mod paste_all_selections; mod write; #[tokio::test(flavor = "multi_thread")] diff --git a/helix-term/tests/test/commands/paste_all_selections.rs b/helix-term/tests/test/commands/paste_all_selections.rs new file mode 100644 index 000000000..43458c87e --- /dev/null +++ b/helix-term/tests/test/commands/paste_all_selections.rs @@ -0,0 +1,59 @@ +use super::*; + +use helix_core::hashmap; +use helix_term::keymap; +use helix_view::document::Mode; + +#[tokio::test(flavor = "multi_thread")] +async fn paste_all_selections() -> anyhow::Result<()> { + let mut config = Config::default(); + config.keys.insert( + Mode::Normal, + keymap!({"Normal Mode" + "A-P" => paste_all_selections_before, + "A-p" => paste_all_selections_after, + }), + ); + + // Test paste_all_selections_before + // Selection direction matches paste target selections. + test_with_config( + AppBuilder::new().with_config(config.clone()), + ( + indoc! {"\ + #(|one)#_cat + #(|two)#_dog + #[|three]#_cow + "}, + "y", + indoc! {"\ + #(|one)##(|two)##(|three)#one_cat + #(|one)##(|two)##(|three)#two_dog + #(|one)##(|two)##[|three]#three_cow + "}, + ), + ) + .await?; + + // Test paste_all_selections_after + // Primary selection is last selection pasted for previous primary. + test_with_config( + AppBuilder::new().with_config(config.clone()), + ( + indoc! {"\ + #(one|)#_cat + #[two|]#_dog + #(three|)#_cow + "}, + "y", + indoc! {"\ + one#(one|)##(two|)##(three|)#_cat + two#(one|)##(two|)##[three|]#_dog + three#(one|)##(two|)##(three|)#_cow + "}, + ), + ) + .await?; + + Ok(()) +} From 398b5d0b106003784d758f27ece892074225d41a Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Thu, 5 Jun 2025 21:57:34 -0700 Subject: [PATCH 8/9] Add documentation. --- book/src/generated/static-cmd.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index fa9123219..e56dc8d99 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -192,6 +192,8 @@ | `replace_selections_with_primary_clipboard` | Replace selections by primary clipboard | | | `paste_after` | Paste after selection | normal: `` p ``, select: `` p `` | | `paste_before` | Paste before selection | normal: `` P ``, select: `` P `` | +| `paste_all_selections_after` | Paste all selections after each selection, presering selections | | +| `paste_all_selections_before` | Paste all selections before each selection, presering selections | | | `paste_clipboard_after` | Paste clipboard after selections | normal: `` p ``, select: `` p `` | | `paste_clipboard_before` | Paste clipboard before selections | normal: `` P ``, select: `` P `` | | `paste_primary_clipboard_after` | Paste primary clipboard after selections | | From f8457af260feb4d8b2387041a1335f4558720554 Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Thu, 5 Jun 2025 22:00:05 -0700 Subject: [PATCH 9/9] Do not add default mappings. --- book/src/keymap.md | 2 -- helix-term/src/keymap/default.rs | 3 --- 2 files changed, 5 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 3891aa404..10257378c 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -86,8 +86,6 @@ Normal mode is the default mode when you launch helix. You can return to it from | `y` | Yank selection | `yank` | | `p` | Paste after selection | `paste_after` | | `P` | Paste before selection | `paste_before` | -| `Alt-p` | Paste all after selection | `paste_after_all` -| `Alt-P` | Paste all before selection | `paste_before_all` | `"` `` | Select a register to yank to or paste from | `select_register` | | `>` | Indent selection | `indent` | | `<` | Unindent selection | `unindent` | diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 38d65558f..52e0ca7bf 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -152,10 +152,7 @@ pub fn default() -> HashMap { "y" => yank, // yank_all "p" => paste_after, - // TODO: This is a terrible mapping, but "A-p" coincides with "select_prev_sibling" - "C-p" => paste_all_selections_after, "P" => paste_before, - "A-P" => paste_all_selections_before, "Q" => record_macro, "q" => replay_macro,