Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "extensionHost",
"request": "launch",
"name": "Launch Extension",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: compile"
}

]
}
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ To load it, import the preset using URI directly:

## How it works

This extension can only capture normal characters typed in modes except for insert mode.
This extension captures normal characters typed in all modes.
In insert mode, characters are still inserted by VS Code first,
then optional insert-mode keybindings (for example `jj` to leave insert mode) are matched.
For special keys like `Esc`, `Ctrl` or `Alt`, they are handled by VS Code directly.
So if you want to bind those keys to commands,
you can directly map them in `keybindings.json`.
Expand Down Expand Up @@ -197,8 +199,14 @@ There are 4 predefined modes (`normal`, `insert`, `select`, `command`) in this e
but you are free to add more modes.
Note that the mode name shouldn't start with underscore `_` as it is reserved for other config.

Keybindings can be defined for all modes except for insert mode,
because this extension will handle over to VS Code in insert mode.
Keybindings can be defined for all modes.

For insert mode, keybindings are primarily useful for transition sequences
(for example `jj` / `kk` to switch to normal mode).
Characters are inserted first, and when a configured insert-mode sequence matches,
the matched characters are removed and the mapped command is executed.
By default, insert-mode sequence matching times out after `200ms`
(configurable via `modalEditor.misc.insertKeybindingTimeout`).

Each key sequence can be prefixed with a number indicating the count.
The count value will be stored in the `CommandContext`,
Expand Down Expand Up @@ -228,7 +236,7 @@ If you need map a key sequence to a command, you can use a recursive keymap.
Your config file should export a `Keybindings` object.

There's a special mode `""` in `Keybindings` which means common keybindings.
It is shared by all the modes (except insert mode),
It is shared by all modes except command mode,
and it can be overwritten by other modes.

There's also a special key `""` in `Keymap` which represents a wildcard character
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@
"type": "boolean",
"description": "Parse leading number as prefix instead of normal keys",
"default": true
},
"modalEditor.misc.insertKeybindingTimeout": {
"type": "number",
"description": "Timeout in milliseconds for insert-mode keybinding sequences (<= 0 disables timeout)",
"default": 200
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion presets/helix.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ module.exports = {
v: "modalEditor.setSelectMode"
},

// insert-mode escape chords
insert: {
j: {
j: "modalEditor.setNormalMode"
},
k: {
k: "modalEditor.setNormalMode"
}
},

select: {
// cursor movement
h: repeatable("cursorLeftSelect"),
Expand Down Expand Up @@ -407,4 +417,3 @@ module.exports = {
w: "workbench.action.files.save"
}
};

105 changes: 103 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as vscode from "vscode";
import {
isCommand,
isCommandList,
isSimpleCommand,
isComplexCommand
} from "./actions.guard";
import { KeyEventHandler } from "./keybindings";
import { KeyEventHandler, Keymap } from "./keybindings";
import { Config, getStyle, cursorStyleMap } from "./config";
import { KeyError } from "./error";

Expand Down Expand Up @@ -93,6 +94,12 @@ export class AppState {
anchors: vscode.Position[];
/// selection before last command
lastSelections: readonly vscode.Selection[] | undefined;
/// Current sub-keymap while parsing insert-mode key sequences
insertCurrentKeymap: Keymap | undefined;
/// Buffered insert-mode keys while parsing key sequences
insertKeys: string;
/// Timeout used for insert-mode key sequence matching
insertKeyTimer: NodeJS.Timeout | undefined;

constructor(
mode: string,
Expand All @@ -104,9 +111,101 @@ export class AppState {
this.registers = {};
this.records = {};
this.anchors = [];
this.insertCurrentKeymap = undefined;
this.insertKeys = "";
this.insertKeyTimer = undefined;
this.setMode(mode);
}

getFromKeymap(keymap: Keymap | undefined, key: string) {
if (keymap) {
if (key in keymap)
return keymap[key];
// wildcard
if ("" in keymap)
return keymap[""];
}
return undefined;
}

resetInsertKeys() {
if (this.insertKeyTimer) {
clearTimeout(this.insertKeyTimer);
this.insertKeyTimer = undefined;
}
this.insertCurrentKeymap = this.config.keybindings[INSERT];
this.insertKeys = "";
}

setInsertKeyTimeout() {
if (this.insertKeyTimer) {
clearTimeout(this.insertKeyTimer);
}

const timeout = this.config.misc.insertKeybindingTimeout;
if (timeout <= 0) {
return;
}

this.insertKeyTimer = setTimeout(() => {
this.resetInsertKeys();
}, timeout);
}

async undoTypedText(length: number) {
for (let i = 0; i < length; ++i) {
await this.executeVSCommand("deleteLeft");
}
}

/**
* Parse insert-mode bindings.
*
* This allows sequences like "jj" to switch mode while still
* keeping normal typing behavior for non-matching sequences.
*/
async handleInsertKeybinding(key: string) {
const insertKeymap = this.config.keybindings[INSERT];
if (!insertKeymap) {
return;
}

let value = this.getFromKeymap(this.insertCurrentKeymap, key);
if (value) {
this.insertKeys += key;
if (isCommand(value)) {
const keys = this.insertKeys;
this.resetInsertKeys();
await this.undoTypedText(keys.length);
await this.executeCommand(value, { keys });
}
else {
this.insertCurrentKeymap = value;
this.setInsertKeyTimeout();
}
return;
}

this.resetInsertKeys();

// Re-try with the current key as a fresh sequence.
value = this.getFromKeymap(this.insertCurrentKeymap, key);
if (!value) {
return;
}

this.insertKeys = key;
if (isCommand(value)) {
this.resetInsertKeys();
await this.undoTypedText(1);
await this.executeCommand(value, { keys: key });
}
else {
this.insertCurrentKeymap = value;
this.setInsertKeyTimeout();
}
}

/// Update cursor and status bar
updateStatus(editor?: vscode.TextEditor) {
if (editor) {
Expand Down Expand Up @@ -144,6 +243,7 @@ export class AppState {

setMode(mode: string) {
this.mode = mode;
this.resetInsertKeys();
this.updateStatus(vscode.window.activeTextEditor);
if (mode === SELECT) {
// record anchor
Expand Down Expand Up @@ -181,9 +281,10 @@ export class AppState {
}

// call default handler for type
vscode.commands.executeCommand("default:type", {
await vscode.commands.executeCommand("default:type", {
text: key
});
await this.handleInsertKeybinding(key);
return;
}

Expand Down
10 changes: 6 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ export type Misc = {
defaultMode: string,
/// Clear selections when running command setInsertMode (deprecated)
clearSelectionsOnInsertMode: boolean,
/// Parse leading number as prefix instead of normal keys
parseNumberPrefix: boolean,
/// Parse leading number as prefix instead of normal keys
parseNumberPrefix: boolean,
/// Timeout in milliseconds for insert-mode keybinding sequences
insertKeybindingTimeout: number,
};

/**
Expand Down Expand Up @@ -109,7 +111,8 @@ const defaultMisc: Misc = {
autoloadPreset: "",
defaultMode: NORMAL,
clearSelectionsOnInsertMode: true,
parseNumberPrefix: true,
parseNumberPrefix: true,
insertKeybindingTimeout: 200,
};

export function getStyle(mode: string, styles: Styles) {
Expand Down Expand Up @@ -203,4 +206,3 @@ export async function readConfig() {
misc
} as Config;
}