Skip to content

feat: New command :paste-join #13600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

nik-rev
Copy link
Contributor

@nik-rev nik-rev commented May 23, 2025

Sometimes I have like 10 selections, yank them with y only to realize i want them all in a single place. So what I do is usually select everything again and use :yank-join, which joins all of my selections so I can paste it with just 1 cursor

The above experience would be more pleasant if I could take all of my 10 yanked selections and paste them into 1 place. The :paste-join command does exactly that.

In this file

[one]
[two
[three]
four
five

If you have the first 3 words selected, as outlined by [...] and you press y to yank it then place your cursor at [ ]:

one
two
three
[ ]
four
five

using :paste-join will join whatever you copied with a newline

one
two
three
[one
two
three]
four
five

Without having to create multiple cursors, or use yank-join!

New typed command

┌────────────────────────────────────────────────────────────────────────────────────────┐
│ Join selections with a separator and paste                                             │
│ Aliases: pj                                                                            │
│ Flags:                                                                                 │
│   --separator/-s <arg>  Separator between joined selections (Default: newline)         │
│   --count/-c <arg>      How many times to paste                                        │
│   --position/-p <arg>   Location of where to paste                                     │
│   --register/-r <arg>   Paste from this register                                       │
└────────────────────────────────────────────────────────────────────────────────────────┘
:paste-join

New static commands

Name Description Default keybinds
paste_before_joined_with_newline Join all selections with a newline and paste before cursor
paste_after_joined_with_newline Join all selections with a newline and paste after cursor
replace_joined_with_newline Replace selection with all selections joined with a newline

Supercedes #4694

@nik-rev nik-rev force-pushed the paste-join branch 2 times, most recently from f156d3f to d1893b8 Compare May 23, 2025 22:27
@pascalkuthe
Copy link
Member

Looks great, something from kakoune I have missed often.

Could you also make a normal command version of this? Whrn mapping these commands a normal command where you can set the register with " would ve more useful.

I would like to replicate the kaklune bindings:

  • a-p paste-all after cursor
  • a-P paster-all before cursor
  • a-R replace-all

(Potentially these three variants can also be flags for the typable command)

@nik-rev
Copy link
Contributor Author

nik-rev commented May 24, 2025

Looks great, something from kakoune I have missed often.

Could you also make a normal command version of this? Whrn mapping these commands a normal command where you can set the register with " would ve more useful.

I would like to replicate the kaklune bindings:

* a-p paste-all after cursor

* a-P paster-all before cursor

* a-R replace-all

(Potentially these three variants can also be flags for the typable command)

I made normal versions of the 3 commands you mentioned

  • paste_after_joined_with_newline bounded to C-p
  • paste_before_joined_with_newline bounded to C-P
  • replace_joined_with_newline bounded to C-R

The typed command now accepts "replace" which means the above commands can also be imitated by using the typed commands

I deviated from Kakoune's A-p and A-P because currently select_prev_sibling is bound to A-p which would create a conflict.

And both C-p and C-P were available.

Deviated from A-R for replace, to be consistent with the above C-p and C-P

I also added default mapping C-y for yank_joined for consistency

@nik-rev nik-rev force-pushed the paste-join branch 4 times, most recently from db65c31 to 13b4df5 Compare May 24, 2025 17:31
@nik-rev nik-rev changed the title feat: New command :paste-join feat: New command :paste-join, 3 new default keybindings May 24, 2025
@nik-rev nik-rev changed the title feat: New command :paste-join, 3 new default keybindings feat: New command :paste-join + 3 new default keybindings May 24, 2025
Copy link
Member

@pascalkuthe pascalkuthe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great thisnwill be very useful

return;
};
let scrolloff = editor.config().scrolloff;
let (view, doc) = current_ref!(editor);
let yanked = values.map(|value| value.to_string()).collect::<Vec<_>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why collect to a vec here instead of accepting impl Iterator<Item=&str>

Copy link
Contributor Author

@nik-rev nik-rev May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that here:

    let Some(values) = editor.registers.read(register, editor) else {
        return;
    };

values from borrows from the editor.
We then need to let (view, doc) = current!(editor); which mutably borrows from Editor to create &mut View and &mut Document

To call replace_impl with something like impl Iterator<Item=&str>, each &str would need to borrow from Editor and the View/Document would need to mutably borrow

I also tried accepting impl Iterator<Item = String>, but it did not work because the lifetime of the iterator is still bound to the editor, which causes the issues mentioned above

// `values` is asserted to have at least one entry above.
let last = values_rev.peek().unwrap();
let mut values_rev = values.iter().rev().peekable();
let Some(last) = values_rev.peek() else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this from an unwrap? As far as I can see there is still always atleast one value?

And if there isn't I would rather unwrap instead of silently doing nothing (or atleast log an error)

Copy link
Contributor Author

@nik-rev nik-rev May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we had registers.read().filter(|values| values.len() > 0) and would return if it's empty. So unwrap was ok. This all happened in the body of the same function

But now, I've moved registers.read().filter(|values| values.len() > 0) outside of replace_impl. Keeping the unwrap means placing an extra constraint on the caller and I think it'd be better to do the validation inside of replace_impl, so I removed call to .filter

The effect is the same as before: Do nothing if it's empty.

It can be empty when the register contains nothing. E.g., "m<C-R> would do nothing if m is empty.

This behaviour (including not logging anything) is consistent with what would happen if you did "mp, "mP, "mR etc if the m register is empty

@nik-rev nik-rev changed the title feat: New command :paste-join + 3 new default keybindings feat: New command :paste-join + 4 new default keybindings May 24, 2025
@darshanCommits
Copy link
Contributor

darshanCommits commented May 25, 2025

Running into a bug where it broke the existing commands : [replace_with_yanked, replace_selections_with_keyboard]

I am yanking this #[word]
when i replace #[this] using `R`

I should get

I am yanking this #[word]
when i replace #[word] using `R`

But I'm getting

I am yanking this #[word]
when i replace #[wordwordwordwordword] using `R`

same behaviour with space-R
that is word is copied 5 times over.running into a bug where the commands : [replace_with_yanked, replace_selections_with_keyboard] are misbehaving.

@nik-rev
Copy link
Contributor Author

nik-rev commented May 25, 2025

Running into a bug where it broke the existing commands : [replace_with_yanked, replace_selections_with_keyboard]

I am yanking this #[word] when i replace #[this] using `R`

I should get

I am yanking this #[word] when i replace #[word] using `R`

But I'm getting

I am yanking this #[word] when i replace #[wordwordwordwordword] using `R`

same behaviour with space-R that is word is copied 5 times over.running into a bug where the commands : [replace_with_yanked, replace_selections_with_keyboard] are misbehaving.

Thanks for noticing it. The issue was passing 2 usize parameters in an incorrect order

@the-mikedavis
Copy link
Member

Since there is a typable command I don't think we need to add default keybindings for these. Or at least I'd prefer to discuss that in a separate PR. The keymap space is limited and this would occupy four spots for commands you can execute in command mode

@nik-rev nik-rev changed the title feat: New command :paste-join + 4 new default keybindings feat: New command :paste-join May 27, 2025
@nik-rev
Copy link
Contributor Author

nik-rev commented May 28, 2025

Ok, I've removed the default mappings

@DerSaidin
Copy link

This is a popular feature :)

Imo #4694 would be greatly preferable to this PR because:

  • it preserves separate selections
  • it is a more atomic operation, not mixing in logically separate behavior (inserting newlines)

With #4694 I can produce this separate lines outcome by doing the paste join, and then leveraging the selections to insert the newlines. With this PR we don't have away of reconstructing the functionality of having multiple selections.

I have updated (and added to) #4694 at: #13698

@nik-rev
Copy link
Contributor Author

nik-rev commented Jun 6, 2025

This is a popular feature :)

Imo #4694 would be greatly preferable to this PR because:

* it preserves separate selections

* it is a more atomic operation, not mixing in logically separate behavior (inserting newlines)

With #4694 I can produce this separate lines outcome by doing the paste join, and then leveraging the selections to insert the newlines. With this PR we don't have away of reconstructing the functionality of having multiple selections.

I have updated (and added to) #4694 at: #13698

This is a popular feature :)

Imo #4694 would be greatly preferable to this PR because:

* it preserves separate selections

* it is a more atomic operation, not mixing in logically separate behavior (inserting newlines)

With #4694 I can produce this separate lines outcome by doing the paste join, and then leveraging the selections to insert the newlines. With this PR we don't have away of reconstructing the functionality of having multiple selections.

I have updated (and added to) #4694 at: #13698

The benefit of this PR is that it adds typed commands for more granular control

The static commands are simply shorthands for the typed commands with newline as the separator

I'm not sure what is the benefit of the PR you sent, when the changes you mentioned can be implemented here quite easily (paste_joined_before and paste_joined_after) without a newline?

@DerSaidin
Copy link

I'm not sure what is the benefit of the PR you sent, when the changes you mentioned can be implemented here quite easily (paste_joined_before and paste_joined_after) without a newline?

The key feature that is missing here is not static commands to paste without a newline, it is pasting without joining the selections.

See how many selections result: https://github.com/helix-editor/helix/pull/13698/files#diff-de6d53c9114aa1dd8644d67767043ed85f30780e0a0197e49501fc9967443da1

This state preserving the selections enables you to do many more things. You can compose other arbitrary edit actions, instead of only being able to do what the typed command flags support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-command Area: Commands
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants