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

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

Open
Arcitec opened this issue May 18, 2023 · 25 comments
Open

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

Arcitec opened this issue May 18, 2023 · 25 comments

Comments

@Arcitec
Copy link

Arcitec commented May 18, 2023

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

#281 (comment)

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
Copy link
Owner

71 commented May 18, 2023

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). 38001b1 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
Copy link
Author

Arcitec commented May 18, 2023

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
Copy link
Owner

71 commented May 19, 2023

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

* | 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)                      | `[".seek.word", { shift: "extend", ... }]` |
+| Extend to next word start | `word.extend` | `s-w` (kakoune: normal), `w` (helix: extend) | `[".seek.word", { shift: "extend", ... }]` |

@gouegd
Copy link
Contributor

gouegd commented Jun 4, 2023

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
Copy link

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

@71
Copy link
Owner

71 commented Jun 18, 2023

#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'"
},

38001b1 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
Copy link

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
Copy link

ilyagr commented Jul 22, 2023

@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
Copy link

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

@imawizard
Copy link

imawizard commented Jul 28, 2023

@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:

  • apparently there's a bug when doing e.g. o ESC j c which makes the cursor jump unexpectedly
  • seek.enclosing doesn't honor quotes
  • / always searches case-sensitive (no case-insensitive or smart-case)
  • not being able to hit enter in / when there's no match is disrupting
  • having all modes with "selectionBehavior": "character" results in some inconsistencies like off-by-one selections, so some bindings like normal-l or select-w try to mitigate them

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
Copy link

cloudhan commented Aug 4, 2023

@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
Copy link

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
Copy link

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
Copy link

@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
Copy link

4rc0s commented Nov 6, 2023

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
Copy link

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
Copy link

forivall commented Nov 20, 2023

@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
Copy link

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
Copy link

ilyagr commented Nov 30, 2023

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
Copy link
Owner

71 commented Dec 1, 2023

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
Copy link
Contributor

petertheprocess commented Mar 20, 2024

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
Copy link

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

@neel04
Copy link

neel04 commented Aug 2, 2024

@imawizard thank you for the awesome bindings. I'm having a problem with using tab for co-pilot completions. While it's not really crucial for my workflow, it's a bit annoying not being able to autocomplete things immediately.

Could there be some workaround for this? perhaps if there are no LSP suggestions, tab defaults to GitHub co-pilot suggestion - otherwise it cycles through LSP recommendations...

@kabouzeid
Copy link
Contributor

I had exactly the same problem. Open keybindings.json (CMD + K CMD +S), then comment out the tab bindings in insert mode.

  //
  // Insert mode
  //

  // {
  //   "key": "tab",
  //   "command": "selectNextQuickFix",
  //   "when": "editorFocus && quickFixWidgetVisible"
  // },
  // {
  //   "key": "tab",
  //   "command": "selectNextSuggestion",
  //   "when": "editorTextFocus && suggestWidgetMultipleSuggestions && suggestWidgetVisible"
  // },
  // {
  //   "key": "shift+tab",
  //   "command": "selectPrevQuickFix",
  //   "when": "editorFocus && quickFixWidgetVisible"
  // },
  // {
  //   "key": "shift+tab",
  //   "command": "selectPrevSuggestion",
  //   "when": "editorTextFocus && suggestWidgetMultipleSuggestions && suggestWidgetVisible"
  // },

@neel04
Copy link

neel04 commented Aug 2, 2024

I'm on the latest commit for those keybindings.json, so its different and these lines aren't there. However, I commented out this snippet:

    { "key": "tab", "command": "dance.run", "args": { "code": [
        "let x = vscode.commands.executeCommand;",
        "let _ = Context.current;",
        "let ts = _.extension.treeSitter;",
        "let pos = _.mainSelection.start;",
        "if (pos.character <= _.document.lineAt(pos).firstNonWhitespaceCharacterIndex) {",
            "await x('tab');",
        "} else if (ts?.determineLanguage(_.document)) {",
            "await x('dance.seek.syntax.parent.experimental');",
            "await x('dance.selections.reduce');",
        "} else {",
            "await x('cursorLineEnd');",
        "}",
    ] }, "when": "editorTextFocus && dance.mode == 'insert'" },

and Co-pilot and normal LSP autocompletions work as expected, but I don't know what this piece of code was supposed to do 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests