71 / dance

Make your cursors dance with Kakoune-like modal editing in VS Code.
https://marketplace.visualstudio.com/items?itemName=gregoire.dance
ISC License
424 stars 52 forks source link

feat: Custom keymaps (incl. Helix keymap) #299

Open Arcitec opened 1 year ago

Arcitec commented 1 year ago

Hi, I noticed that the project creator, @71, is considering custom keymaps and Helix bindings:

https://github.com/71/dance/issues/281#issuecomment-1303535792

I've been looking into ways of getting Helix editing in Vscode. While Kakoune is definitely better than Vim (which I used for 15 years), thanks to the ability to preview the motions BEFORE you commit the action, I noticed that Helix improves on Kakoune by fixing some very awkward keybinds (shift/alt gymnastics):

https://github.com/helix-editor/helix/wiki/Differences-from#kakoune

The biggest improvement for me is how easy it is to make multiple selections. In Kakoune, selecting a bunch of words means holding shift while tapping w/b until the correct length. If you release "shift" at any moment, your selection is LOST.

In Helix, you instead press "v" to enter "visual mode" (vim-like), and tap as much w/b as you want without needing to hold anything else. When you're done, just hit the action (such as d (delete)). Much easier than Kakoune.

I think Dance is in the best spot to implement Helix-style tweaks for Kakoune, given the fact that the project is already of extremely high quality, and Helix bindings are just a few small tweaks of Kakoune's movements. Although I suspect that refactoring Dance into a keymap-based system is the limiting factor here, not Helix's small tweaks themselves.

71 commented 1 year ago

Hey! Thanks for the link, somehow I had missed that part of the documentation and never understood how to grow selections in Helix. Knowing about v certainly helps 😅

I think Dance is in the best spot to implement Helix-style tweaks for Kakoune

I agree, especially since a lot of commands are shared (and Dance has many utilities for implementing the remaining commands). 38001b17bf5b05c2a4b4e67f7eb49c402f061708 adds the ability to use Tree Sitter trees within Dance (internally), with the intention to implement more Helix keybindings (see #297). With that said I'm fairly busy nowadays, so I mostly implemented this with the hope that users could send PRs adding commands based on this API.

Although I suspect that refactoring Dance into a keymap-based system is the limiting factor here

IMO that's a necessary step anyway (as mentioned in the comment you linked). In theory that's a "nice to have" feature, but it's actually necessary to support non-US/Mac keyboards.

Arcitec commented 1 year ago

This is really cool news. The most impressive thing is the tree-sitter implementation using WebAssembly.

I see that you wondered what the most important Helix features are. The v mode to begin selection would be it. But perhaps that's impossible to add until there's a formal way to switch keymaps in Dance, since v switches the "view" in Kakoune.

Man, I'd really love to get involved and help if I had more free time, but the amount of other projects I am part of is too overwhelming already. :(

Edit: I am already on the pre-release channel all the time, by the way (currently v0.5.12001) so if there's anything that needs testing, to see if it breaks during real-world usage, I'll help with that.

71 commented 1 year ago

IIRC I haven't released (at all) the version with Tree Sitter support, so you'd need to clone + debug the repository to try it out (+ debug, + add features).

With that said, in theory v can be achieved with the released version of Dance. You'd need a lot of custom keybindings and a custom v mode, but AFAIK that'd be enough to have this mode. IMO there are two ways to proceed here:

  1. Create custom keybindings / modes and publish them e.g. in a Gist.
  2. Add all Helix keybindings to Dance commands, and extract them somehow to generate the keybindings.json you need (as said earlier, in the future we'd have utilities within Dance to easily use these keybindings, but in the meantime we should only extract them and make them available IMO).

For solution 2., you'd go through Dance commands whose Kakoune and Helix keybindings are different, and add new helix: normal / helix: extend key bindings. For instance

https://github.com/71/dance/blob/73d32adb80497758f48a643266ba3488d0c08561/src/commands/seek.ts#L184

-| Extend to next word start | `word.extend` | `s-w` (kakoune: normal)                      | `[".seek.word", { shift: "extend", ... }]` |
+| Extend to next word start | `word.extend` | `s-w` (kakoune: normal), `w` (helix: extend) | `[".seek.word", { shift: "extend", ... }]` |
gouegd commented 1 year ago

Hi ! Interesting conversation, I adopted Helix ~10 days ago (2 days after I discovered Kakoune, which led me to it), and was thinking broadly the same: this extension could be used for Helix keybindings as well with much less effort than starting from scratch.

There's already a fork of dance that's achieved some good outcomes (the v visual mode seems to work fine), but it doesn't seem very active or as thorough in its development as the original extension (this). Maybe that fork can be a good base to get those keybindings for helix:normal, helix:visual, etc.

Besides v mode I also love Helix's features that rely on tree-sitter, namely "go to next/previous function" [f and ]f, and "view symbols" (space s for current file, space S for the whole project).

The latter two could perhaps be done by just reusing VSCode's builtin symbol navigation and/or outline view. For function navigation (and other LSP-based goodies), it seems the new tree sitter integration mentioned above is just what is needed. I may try to play a bit with it after I understand more about navigating this codebase.

zetashift commented 1 year ago

I see there have been some Helix-esque additions/options to dance, is there some doc that explains how to "enable" these?

71 commented 1 year ago

301 adds the select mode, which is not bound to any key by default, but can be accessed by creating a keybinding as such:

{
  "key": "v",
  "command": "dance.modes.set.select",
  "when": "editorTextFocus && dance.mode == 'normal'",
},
// And to avoid conflicts:
{
  "key": "v",
  "command": "-dance.openMenu",
  "when": "editorTextFocus && dance.mode == 'normal'"
},

38001b17bf5b05c2a4b4e67f7eb49c402f061708 adds a couple of Tree-Sitter based commands, but requires https://github.com/71/vscode-tree-sitter-api to be installed for the commands to be shown. Similarly, no keybindings exist by default, so you have to define them yourself or to use the command palette. They're also very basic; among other things, Dance doesn't have the Helix library of textobjects used by Helix for seeking to specific syntax nodes.

crabdancing commented 1 year ago

What about adding the next line down to the selection when the user hits 'x' again? How easy would that be to add via keybindings.json?

ilyagr commented 1 year ago

@alxpettit Here's what I use in recent versions of kakoune (for X; I actually like the new x/a-x behavior). I keep meaning to translate this to a dance config, but have never gotten around to it.

I can't remember why a-X is so complicated.

# From https://github.com/mawww/kakoune/wiki/Selections#how-to-make-x-select-lines-downward-and-x-select-lines-upward
# and https://github.com/mawww/kakoune/issues/1285
def -params 1 extend-line-down %{
      exec "<a-:>%arg{1}Jx"
}
def -params 1 extend-line-up %{
      exec "<a-:><a-;>%arg{1}K<a-;>"
        try %{
                exec -draft ';<a-K>\n<ret>'
                    exec X
        }
exec '<a-;><a-X>'
}
map global normal X     ':extend-line-down %val{count}<ret>'
map global normal <a-X> ':extend-line-up %val{count}<ret>'
crabdancing commented 1 year ago

Seems very script-y. I imagine there must be some way to embed js into keybindings in VSCode?

imawizard commented 12 months ago

@alxpettit Dance already has commands with equivalent functionality:

{ "key": "x",       "command": "dance.select.line.below.extend", "when": "editorTextFocus && dance.mode == 'normal' || editorTextFocus && dance.mode == 'select'" },
{ "key": "shift+x", "command": "dance.selections.expandToLines", "when": "editorTextFocus && dance.mode == 'normal' || editorTextFocus && dance.mode == 'select'" },

PS: Here's my config trying to imitate helix' keybindings:

Edit: There's still missing functionality in some menus (command contains ???). Additionally, some annoyances/differences I've come across are:

Also I have added some surround-bindings which are not present in the commits linked above:

        "match-hx": {
            "title": "Match",
            "items": {
                ...
                "s": { "text": "Surround add", "command": "dance.run", "args": [{ "code": [
                    "let pairs = ['()', '{}', '[]', '<>'];",
                    "let x = vscode.commands.executeCommand",
                    "let c = await keypress(Context.current);",
                    "let p = pairs.find((p) => p.includes(c));",
                    "await x('dance.selections.save');",
                    "Selections.updateWithFallbackByIndex((i, sel) => new vscode.Selection(sel.anchor, Positions.at(sel.active.line, sel.active.character + (sel.isReversed ? -1 : 1))));",
                    "await x('editor.action.insertSnippet', { 'snippet': (p?.at(0) || c) + '${TM_SELECTED_TEXT}' + (p?.at(1) || c) });",
                    "await x('dance.selections.restore');",
                ] }] },
                "r": { "text": "Surround replace", "command": "dance.run", "args": [{ "code": [
                    "let pairs = ['()', '{}', '[]', '<>'];",
                    "let x = vscode.commands.executeCommand",
                    "let c = await keypress(Context.current);",
                    "let p = pairs.find((p) => p.includes(c));",
                    "await x('dance.selections.save');",
                    "await x('dance.selections.faceBackward');",
                    "await x('dance.seek.included.extend.backward', { 'input': p?.at(0) || c });",
                    "await x('dance.selections.changeDirection');",
                    "await x('dance.seek.included.extend', { 'input': p?.at(1) || c });",
                    "await x('dance.selections.save', { 'register': 'surround' });",
                    "await x('dance.selections.reduce.edges');",
                    "c = await keypress(Context.current);",
                    "p = pairs.find((p) => p.includes(c));",
                    "await x('dance.edit.delete');",
                    "await x('dance.selections.restore', { 'register': 'surround' });",
                    "await x('dance.select.right.extend');",
                    "await x('editor.action.insertSnippet', { 'snippet': (p?.at(0) || c) + '${TM_SELECTED_TEXT}' + (p?.at(1) || c) });",
                    "await x('dance.selections.restore');",
                ] }] },
                "d": { "text": "Surround delete", "command": "dance.run", "args": [{ "code": [
                    "let pairs = ['()', '{}', '[]', '<>'];",
                    "let x = vscode.commands.executeCommand",
                    "let c = await keypress(Context.current);",
                    "let p = pairs.find((p) => p.includes(c));",
                    "await x('dance.selections.save');",
                    "await x('dance.selections.faceBackward');",
                    "await x('dance.seek.included.extend.backward', { 'input': p?.at(0) || c });",
                    "await x('dance.selections.changeDirection');",
                    "await x('dance.seek.included.extend', { 'input': p?.at(1) || c });",
                    "await x('dance.selections.reduce.edges');",
                    "await x('dance.edit.delete');",
                    "await x('dance.selections.restore');",
                ] }] },
                ...
            },
        },
cloudhan commented 11 months ago

@imawizard Great thanks for the sharing for config. Do you have any suggestion on the command mode disparity, say :wq, is there any equivalent for the similar commonly used commands? What is your practice?

imawizard commented 11 months ago

Not really, sorry. You could use custom menus, however, Dance's prompt-functions work with item keys of length 1, so you'd need to split e.g. "wq" among multiple menus or use vscode.window.createQuickPick directly.

I have mapped : to workbench.action.showCommands, so no :x, :w, :cd etc. But since I have auto-save enabled, hitting :<ESC> or switching to another split already saves the file. As for :wq I just use a global hotkey for closing programs (kind of like alt+f4) and auto-save does the rest.

Btw. I have updated some menu entries/keybindings: vscode/data/user-data/User

christianfosli commented 11 months ago

Sorry for cluttering up this issue with a not-really related comment 😅, it possibly answers @cloudhan latest question, and I was wondering the same thing..

I found a workaround for missing commands that I'm used to (i.e. :w, :q, ...). The "command alias" extension can be used to add command aliases in vscode.

So adding something like this to settings.json, in addition to having : mapped to workbench.action.showCommands I'm able to keep my muscle memory happy 😄

    "command aliases": {
        "workbench.action.files.saveAll": "write-all, wall",
        "workbench.action.files.save": "write, w",
        "workbench.action.closeAllEditors": "quit-all, qall",
        "workbench.action.closeActiveEditor": "quit, q"
    },
crabdancing commented 11 months ago

@christianfosli

Actually, I solved that awhile ago myself! You don't need anything besides Dance itself.

keybindings.json:

{
    "args": {
        "menu": "cmd-hx"
    },
    "command": "dance.openMenu",
    "key": "shift+;",
    "when": "editorTextFocus && (dance.mode != 'insert')"
},

settings.json:

{
    "dance": {
        "menus": {
            "cmd-hx": {
                "items": {
                    "w": {
                        "command": "saveAll",
                        "text": "Save document"
                    }
                },
                "title": "Quick command"
            },
        },
    },
}  
4rc0s commented 8 months ago

Thanks for your work to add some Helix-flavored options. I think Helix has a great workflow and would love to standardize some muscle memory. Down the line, it'd be awesome to have the option to select "classic" Kakoune mode or Helix mode.

luiswirth commented 8 months ago

Is this mature enough to replace the Dance - Helix Alpha extension?

And if yes, what do I have to do to set it up? Thanks for any help :)

forivall commented 8 months ago

@LU15W1R7H IMO, yeah, it's good enough as a replacement.

You just need to import @imawizard's keybindings and settings into your own.

I made a few tweaks of my own for a hybrid cursor/block mode that fits vscode's native selection logic better though (IMO, of course), and a few personal preferences. keybindings.json, settings.json

luiswirth commented 7 months ago

Is there some quick way to disable dance, without having to remove the keybindings? When I disable the extension, the keybindings persist and then I just get errors, because these dance functions are not available.

ilyagr commented 7 months ago

Is there some quick way to disable dance, without having to remove the keybindings?

One way is to create a dance "mode" that does nothing, and add commands to switch to and from that mode. I do wish Dance had built-in support for this, though. I'll post a config here if I find it.

71 commented 7 months ago

Defining and switching to a "disabled" mode that doesn't do anything is the recommended way to disable Dance, indeed, though I'm open to making this a first-class feature (which was actually the case some time ago, but I removed that feature when adding support for custom modes) or to include the "disabled" mode in the built-in Dance config (possibly with built-in commands to toggle it easily).

@LU15W1R7H Typically keybindings use dance.mode == '...', so if Dance is disabled the keybindings are disabled as well. This doesn't work with dance.mode != '...' because this condition will be true even if Dance is disabled. As an alternative, you can write editorTextFocus && dance.mode && dance.mode != 'insert'.

petertheprocess commented 4 months ago

IIRC I haven't released (at all) the version with Tree Sitter support, so you'd need to clone + debug the repository to try it out (+ debug, + add features).

https://github.com/71/dance/wiki/Helix-support Did you mean that even though I installed this tree-sitter-api, I need to build the Dance from source code to taste the new feature? Now my Dance version is 0.5.14, and I setup my settings and keybinding following the wiki, dance.seek.syntax.{next,previous,parent,child}.experimental works for me, but the textobjects liek (?#textobject=class) (?#textobject=function) does not. It post an error said unknown object:(?#textobject=class)

Some of the features below are only available if the tree-sitter-api extension (currently only available as a .vsix) is installed.

merlindru commented 1 week ago

I just wanna say that it's insane that things like this comment are possible, you've essentially built a scriptable editor within VSCode

this is insane (as in insanely cool)

thank you so much for all your work. this is my endgame setup