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

[Idea] Define keybindings with JavaScript instead of JSON #9

Closed brillout closed 3 years ago

brillout commented 4 years ago

One of my favorite VIM feature are the "c", "d", and "y" keys, e.g. "ci(" or "y$", which I'm currently implementing with ModalEdit.

What would help enormously would be able to use JavaScript to define keybindings.

For example:

const KEY_DELETE = 'd';
const KEY_YANK = 'y';
const KEY_INSERT = 'c';

const keybindings = {};

[KEY_DELETE, KEY_YANK, KEY_INSERT]
.forEach(key => {
  const commands = [
    {
      command: 'modaledit.selectBetween',
      args: {
        from: '(',
        to: ')',
        // I'm using a JS string expression here, but maybe we could
        // design an even nicer solution using JS directly :).
        inclusive: "__keys[1] == 'a'",
      },
    },
    'editor.action.clipboardCopyAction',
  ];

  if( key !== KEY_YANK ){
    commands.push('deleteLeft');
  }

  if( key === KEY_INSERT ){
    commands.push('modaledit.enterInsert');
  }

  keybindings[key] = {
    'a,i': {
      '(,)': commands,
    }
  };
});

module.exports = {keybindings};

What do you think?

johtela commented 4 years ago

I can see your point of why this would be useful. The bindings become verbose quickly, if you have to write each of them separately. Luckily there is another way; I have implemented delete, yank, and change commands a bit differently using selectBetween command. I have bound selection to v key. For example vi( selects the text inside parenthesis.

        "v": {
            "help": "Select [j|l] left|right, [w] word [d] line [f|b] forwards|backwards [i|a] inside|around",
            ...
            "a,i": {
                "help": "Select around/inside _",
               ...
                "(,)": [
                    "modaledit.cancelSelection",
                    {
                        "command": "modaledit.selectBetween",
                        "args": "{ from: '(', to: ')', inclusive: __rkeys[1] == 'a' }"
                    }
                ],

So, basically I use JS only in args property. The same binding also implements select around ca(. As in your example, I examine the second character in the key sequence, and set the inclusive argument based on that. Now, I can implement ci( like this:

        "c": {
            "help": "Change [j|l] left|right, [w] word [f|b] forwards|backwards [i|a] inside|around",
            ...
            "a,i": {
                "help": "Delete around/inside _",
                "w, -/,:-@,[-`,{-~": [
                    {
                        "command": "modaledit.typeNormalKeys",
                        "args": "{ keys: ['v', ...__keys.slice(-2)].join('') }"
                    },
                    "editor.action.clipboardCutAction"
                ]
            }

I basically call the vi( command by using the typeNormalKeys command, and then just cut the selected text to clipboard. You can find bindings using the same trick for change and yank commands in my bindings.

Coming back to your original request, there is a way to define the bindings in JS too, but that involves you creating your own VS code extension and including ModalEdit as a dependency. The extension would be very simple; you can just set your bindings in the activation event using JS/TS. See issue #6 for more information. The requestor of that issue tested that the idea works.

brillout commented 4 years ago

Neat, this seems to be working, thanks!


To the original request: using JS instead of JSON could open up new interesting possibilities, beyond defining the c, d and y commands.

For example:

const keybindings = {
  'H': args => jumpToWhitespace(args, true),
  'L': args => jumpToWhitespace(args, false),
};

function jumpToWhitespace({line, cursorPosition}, backwards) {
  let column;
  if( backwards ){
    const line__relevant = line.slice(0, cursorPosition.column);
    column = line__relevant.split('').lastIndexOf(' ');
  } else {
    const line__relevant = line.slice(cursorPosition.column, line.length);
    column = line__relevant.split('').indexOf(' ');
  }

  if( column===-1 ){
    return {
      warningMessage: 'No whitespace on the '+(backwards?'left':'right')+' found.',
    };
  }

  return {
     cursorPosition: {
       column,
     },
  };
}

module.exports = {keybindings};

And this is just one example; even only a tiny ModalEdit JS API would enable ModalEdit users to achieve all kinds of keybindings.

I have couple of keybindings in mind that I would be thrilled to implement that way :blush:.

Thoughts?

johtela commented 4 years ago

Well, in principle having the bindings in JS file would be relatively easy to implement. There are some consequences of having the bindings outside the settings.json file though.

  1. The bindings would not be automatically refreshed when you save the JS file. The event for that triggers only when settings.json file changes. So, there would have to be some kind of file watcher for the JS file as well. And of course, if you save a file that has any errors, the bindings would disappear.

    Also a question arises, where the JS file should be located? Under workspace, or in the user home directory, or in a VS Code's system directory? This is more of a user problem than problem of ModalEdit, if we just require to have the absolute path of the JS file in the settings. Nevertheless, there should be probably be some recommendations, at least.

  2. The support for multiple settings levels would break. Currently you can define bindings in three levels: workspace, user, and default. The keybindings defined in these files are actually merged. If a same key is bound in multiple files, the precedence (lowest to highest) is: default, user, workspace. This merging and overriding of key bindings would not work. Effectively, bindings would exist only in one place.

A way to get around problem 1. would be to put JS code inside a setting.json file as a string, but editing code there would be cumbersome. Also problem 2. would still exist with this solution. In my opinion, creating a new extension is a cleaner approach when you want to have this option. You avoid the problems above, and have a nicer experience editing the code as "normal" TypeScript file.

johtela commented 3 years ago

In version 2.0 you can import keybindings from a JS file. I think it is cleaner solution than messing with the settings.json file. The JS code is run at import time, so effectively you can "compile" or expand your bindings into the global settings.json where they are stored in normal JSON format.