johtela / vscode-modaledit

Configurable Modal Editing in VS Code
https://johtela.github.io/vscode-modaledit/docs/README.html
Other
89 stars 8 forks source link

Programmatically editing keymaps? #6

Closed ktnyt closed 4 years ago

ktnyt commented 4 years ago

Hello, I stumbled across this extension while looking for an alternative for the Dance extension (a Kakoune-like keybinding extension). Dance is mostly good, but tinkers with some configurations under the hood and seems to have some related bugs lurking within. I can replicate most of the Kakoune behavior using ModalEdit excluding the "count" mechanism which is an essential part of the Kakoune UX. I can add this functionality this by writing an extension myself and then calling the extension commands from the ModalEdit keymaps, but would prefer to be able to also configure the keymaps solely from within the extension. Would adding an exported API for editing keymaps programmatically be feasible? If so, I would be happy to contribute if necessary.

johtela commented 4 years ago

Hi. I'll try to recap your request to check if I understood it correctly. This is going to be a lengthy reply. Sorry for that.

First of all, I gathered that you would like to implement count based commands as described in Kakoune documentation:

Most selection commands also support counts, which are entered before the command itself.

For example, 3W selects 3 consecutive words and 3w select the third word on the right of selection end.

This is an interesting use case, as it requires iteration or looping. ModalEdit currently supports conditional commands, and I have wondered if there was a use case for loop command. Here is one.

I have a suggestion how to implement this. Please comment, if it would meet your needs. There would be three new features added to ModalEdit.

1. Key ranges

The configuration currently allows only mapping a single character to a command. To create a key sequence, you need to define nested mappings. If there was a way to map character range to a command, you could define a mapping for any number key like this:

"0-9": {
    <command or nested mapping>
}

The range would be an ASCII range, e.g. "a-z" or "0-9". With this feature, you would not have to repeat the mappings for each count command variant.

2. Expose __keySequence variable to JS expressions

Next, we need access to the complete key sequence that was typed to invoke a command. This can be implemented very easily by exposing a module level variable that already exists:

__keySequence: string[]

With that you can access the complete key sequence that was pressed to invoke a command.

3. Repeating a command

Last, we need the new looping construct. That could be implemented also easily by adding a new optional property to commands with options:

"<key>":  {
    "command": "<command>",
    "args": { ... } | "{ ... }"
    "repeat": "<JS expression that evaluates to number>",
}

If the repeat property is missing or does not evaluate to a number, the command would be run once as currently. Otherwise it would be run repeat times.

Example

With these additions, you could implement Kakoune's count commands like this. Assume that you have a mappings w which moves to (or selects) next word:

        "w": {
            "condition": "__selecting",
            "true": "cursorWordStartRightSelect",
            "false": "cursorWordStartRight"
        }

Now you can define command variants that support count using the modaledit.typeNormalKeys command with the new repeat property:

        "1-9": {
            "W": [
                "modaledit.toggleSelection"
                {
                    "command": "modaledit.typeNormalKeys",
                    "args": {
                        "keys": "w"
                    },
                    "repeat": "Number(__keySequence[0])"
                }
            ],
           "w": [
                "modaledit.cancelSelection",
                {
                    "command": "modaledit.typeNormalKeys",
                    "args": {
                        "keys": "w"
                    },
                    "repeat": "Number(__keySequence[0]) - 1"
                },
                "modaledit.toggleSelection",
                {
                    "command": "modaledit.typeNormalKeys",
                    "args": {
                        "keys": "w"
                    }
                }
            ]
        }

I like this idea, so I probably implement it anyway 😉, but would that work you?

Creating new extensions based on ModalEdit

I also like the idea that you could extend ModalEdit to create new extensions that implement emulators for Vim, Kakoune, or just provide new key mappings. What you can do right now, of course, is fork the repo and modify the code in whatever way you want.

It would be cool, though, to be able to build these key mappings as extension bundles. I have to test this idea, but I think this could be done by creating a new empty extension and adding just a dependency to ModalEdit in package.json:

"extensionDependencies": [
    "johtela.vscode-modaledit"
]

This setting will cause VSCode to automatically install ModalEdit along with your own extension. You can of course add any other extensions as dependency too, for example to provide additional commands.

The API to ModalEdit is actually the modaledit.keybindings section in the settings.json. You should be able to programmatically add keybindings in your extension's activation event (something) like this:

let newMappings = {
    "d": "deleteRight",
    "f": [
        "deleteRight",
        "modaledit.enterInsert"
    ],
    ...
}

export function activate(context: vscode.ExtensionContext) {
    vscode.workspace.getConfiguration("modaledit").update("keybindings", newMappings,
        vscode.ConfigurationTarget.Global)
    ...
}

This should add your own keybindings to ModalEdit when your extension is loaded.

I haven't tested this code in practice, but I will do that. If it is viable approach, I can write a tutorial on how to write new extensions on top of ModalEdit. The added benefit of writing a new extension is that you can also define new standard VSCode key bindings in the package.json, so your extension can also remap Ctrl-<key> and Alt-<key> commands.

Hope this was roughly what you were asking for. If not, please elaborate on what you need.

ktnyt commented 4 years ago

Wow! I very much appreciate the effort you put in to elaborating on my needs. Thank you so much.

On the topic of repeats: I think this should satisfy most of my needs. One question I have is whether or not ModalEdit can configure arbitrarily deep command nests. Kakoune actually allows the user to input multiple digits, which I find particularly useful in navigation (e.g. typing 42g moves the cursor to line 42 and gh80l takes the cursor to the 80th character in the line). It feels like it Is possible I wasn't too sure about how this might be configured in ModalEdit.

Regarding developing third-party extensions based on ModalEdit: I think this approach, if possible, should suffice. I somehow believed that the configurations from external extensions are read-only in VSCode. One possible caveat that I can foresee is that the extension must be careful when overwriting existing keybindings as users may want to map keys in addition to and/or differing from the keybindings that the extension provides. Maybe a helper function for not touching existing user-defined settings may be helpful? I'm not sure if that is a feasible approach though. All told, this seems like a brilliant idea.

It would be very nice if these features are added to ModalEdit! I love the overall concept of the extension so I'm looking forward to seeing these features added! In the meantime I'll try creating a simple Kakoune-like keybinding extension myself to test out if what you suggested is possible.

ktnyt commented 4 years ago

A quick update: the suggested method for creating extensions based on ModalEdit seems to be working! I'm struggling to get some keys to work but I think that's on my behalf to figure it out.

johtela commented 4 years ago

Just published version 1.5 which has the changes I described in my previous comment. Here is an excerpt from the change log:

So, it was not enough to implement just the changes I originally outlined. There was one more limitation that had to be lifted: you could only define fixed length key sequences. To enable entering arbitrary long numbers (like in your examples) I devised the concept of recursive keymaps.

To give a concrete example how you could define the 42g command, here is my solution:

        "1-9": {
            "id": 1,
            "help": "Enter count followed by [g]",
            "0-9": 1,
            "g": [
                {
                    "command": "revealLine",
                    "args": "{ lineNumber: Number(__keySequence.slice(0, -1).join('')) - 1, at: 'top' }"
                },
                {
                    "command": "cursorMove",
                    "args": {
                        "to": "viewPortTop"
                    }
                }
            ]
        },

There is no built-in "goto line" command in VS Code which takes the line number as an argument, so I had to use both revealLine and cursorMove to implement that. The recursive part is easy to define, the keymap kind of "calls itself" through the id reference as long as you type number keys. Getting the number from the __keySequence variable is bit involved, but this JS expression is what I found easiest to use:

Number(__keySequence.slice(0, -1).join(''))

Reason why I subtract one from the number in revealLine command uses zero-based line number whereas in UI the lines start from one.

One particularly nice trick that I found was to reuse the cursor movement commands easily by giving last character in the __keySequence directly to the modaledit.typeNormalKeys command. This makes implementing repeated movement commands very easy. The example is in the README document.

The changes made the keybinding system somewhat more complex. I think the keybinding definition "language" might by Turing complete now 😉 So it's probably easier to make mistakes too. To make debugging of keybindings easier I added validation and logging that hopefully helps figuring out the errors more easily. Also the help property in keymaps can be used to give an indication when the count mode is on.

Didn't have time to test the extension mechanism yet myself. Great, if it works as you said. I guess there might be some oddities on how multiple levels of configuration (global, user-level, and project-level) are merged together in the case of ModalEdit's keybindings structure. I'll test that at some point to understand the mechanism better.

ktnyt commented 4 years ago

This is wonderful! I was hesitant of requesting a feature that greatly impacts the core logic of the extension, but if you as the owner are comfortable this will indeed greatly help me with defining the necessary keybindings. Thank you so much! And on that note, since the original question is resolved, I'll be closing this issue.