Skip to content
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

vim: Add exchange #24678

Merged
merged 21 commits into from
Feb 22, 2025
Merged

Conversation

thomasheartman
Copy link
Contributor

@thomasheartman thomasheartman commented Feb 11, 2025

Implements vim-exchange functionality.

Lets you swap the content of one selection/object/motion with another.

The default key bindings are the same as in exchange:

  • cx to begin the exchange in normal mode. Visual mode does not have a default binding due to conflicts.
  • cxx selects the current line
  • cxc clears the selection
  • If the previous operation was an exchange, . will repeat that operation.

Closes #22759

Overlapping regions

According to the vim exchange readme:

If one region is fully contained within the other, it will replace the containing region.

Zed does the following:

  • If one range is completely contained within another: the smaller region replaces the larger region (as in exchange.vim)
  • If the ranges only partially overlap, then we abort and cancel the exchange. I don't think we can do anything sensible with that. Not sure what the original does, evil-exchange aborts.

Not implemented: cross-window exchange

Emacs's evil-exchange allows you to exchange across buffers. There is no code to accommodate that in this PR. Personally, it'd never occurred to me before working on this and I've never needed it. As such, I'll leave that implementation for whomever needs it.

As an upside; this allows you to have concurrent exchange states per buffer, which may come in handy.

Bonus

Also adds "replace with register" for the full line with grr 🐕 This was an oversight from a previous PR.

Release notes:

  • Added an implementation of vim-exchange
  • Fixed: Added missing default key binding for Vim::CurrentLine for replace with register mode (grr)

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Feb 11, 2025
@maxdeviant maxdeviant changed the title feat: vim-exchange vim: Add exchange Feb 11, 2025
@maxdeviant maxdeviant marked this pull request as draft February 11, 2025 18:40
@thomasheartman thomasheartman marked this pull request as ready for review February 11, 2025 22:03
@thomasheartman
Copy link
Contributor Author

@ConradIrwin Thanks for the pairing sesh earlier. I added some docs and some inital tests for this -- at least as far as I've seen tests in other PRs here. It might not be the right place or way of writing it, so please let me know if they should go somewhere else.

Two of the tests deal with the overlap logic which isn't currently implemented. I took a stab at it, but couldn't find the way to compare ranges for overlaps because Anchor doesn't impl PartialOrd. There's probably something obvious I'm missing though. If you can give me some pointers, I'd be happy to take another crack at it, but if you think it'll be faster/easier adding it yourself, that's totally fine too.

@ConradIrwin
Copy link
Member

@thomasheartman you have two choices, if you have a buffer snapshot you can do anchor.cmp(&other, &snapshot); or you can convert the anchor to an offset and use < and > as nature intended.

If you don't get to it, I'll pick this up in a few days.

@thomasheartman
Copy link
Contributor Author

Thanks! I think I've got it working as intended now (as far as I can tell). I've moved the tests into the same source file, and they run just fine.

One thing though, that I .. might like to resolve:

In evil exchange, you end up with the cursor at the beginning of the last selected object, whereas in Zed, you're currently at the end. Is there a function to "go to point in buffer" or something like that? I tried looking at the editor struct, but couldn't find a specific "go to X" function?

In other words, the first test currently expects this:

"world helloˇ"

But I'd actually expect this:

"world ˇhello"

@ConradIrwin
Copy link
Member

@thomasheartman nice!

Selections are a bit fussy (because in general you might want to have many). Probably the right thing to do in this case is to store the anchor at the start of the range, and then select it. If you're lucky you may be able to re-use the helpers from the indent code:

let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count {
editor.indent(&Default::default(), window, cx);
}
vim.restore_selection_cursors(editor, window, cx, original_positions);
});

But if you need something slightly different, feel free to copy and modify.

@zed-industries-bot
Copy link

zed-industries-bot commented Feb 13, 2025

Warnings
⚠️

This PR is missing release notes.

Please add a "Release Notes" section that describes the change:

Release Notes:

- Added/Fixed/Improved ...

If your change is not user-facing, you can use "N/A" for the entry:

Release Notes:

- N/A

Generated by 🚫 dangerJS against af9682b

@thomasheartman
Copy link
Contributor Author

Thanks! I'll have a look later 🙋🏼 Been out of commission for a couple of days due to a back injury, but I'm at least doing well enough to sit in a chair now 😅 I'll let you know if I can't figure it out 🙌🏼

@thomasheartman
Copy link
Contributor Author

thomasheartman commented Feb 17, 2025

@ConradIrwin Thanks for again for the pointers! I went digging a bit and found editor.change_selections. Made an implementation that does what I expect and also added an extra test case to check the behavior for full overlaps regardless of selection order.

With that out of the way, I believe this PR is ready to go, so feel free to have a look and let me know if you want any changes (or feel free to make them yourself).

@thomasheartman
Copy link
Contributor Author

thomasheartman commented Feb 17, 2025

I've also added a release notes section to the PR description using the format that the bot suggested. Again, feel free to lemme know or edit yourself if the format isn't correct.

@thomasheartman
Copy link
Contributor Author

@ConradIrwin Sorry for the ping, but just so that this doesn't go too stale: is there anything I need to / can do to move this forward? Any remaining work you'd like to see? The one thing I can think of is what to do in visual mode, where shift-x is already bound to delete line. I don't think there's a different canonical shortcut for exchange in that setting, so might be best to just take it out and make a note of it in the docs? Happy to do that, if so.

@ConradIrwin
Copy link
Member

No worries, sorry for putting this on the back burner. Happy to ship without the visual mode keybinding conflict.

@thomasheartman
Copy link
Contributor Author

No worries at all! I removed the keybinding and added a note for it under the optional keybindings section (and added a note about it higher up.

@ConradIrwin ConradIrwin enabled auto-merge (squash) February 22, 2025 20:19
@ConradIrwin ConradIrwin merged commit 084a023 into zed-industries:main Feb 22, 2025
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-signed The user has signed the Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for evil-exchange / vim-exchange
4 participants