Skip to content

Commit

Permalink
Overhaul remapping logic (#4735)
Browse files Browse the repository at this point in the history
This is a pretty massive change; see pull request #4735 for full details

Most notably:
- Support for operator-pending mode, including remaps and a half-cursor decoration
- Correct handling of ambiguous remaps with timeout
- Correct handling of recursive special case when the RHS starts with the LHS
- Correct handling of multi-key remaps in insert mode
- Failed movements that occur partway through a remap stop & discard the rest of the remap
- Implement `unmap` and `mapclear` in .vimrc

Refs #463, refs #4908
Fixes #1261, fixes #1398, fixes #1579, fixes #1821, fixes #1835
Fixes #1870, fixes #1883, fixes #2041, fixes #2234, fixes #2466
Fixes #2897, fixes #2955, fixes #2975, fixes #3082, fixes #3086
Fixes #3171, fixes #3373, fixes #3413, fixes #3742, fixes #3768
Fixes #3988, fixes #4057, fixes #4118, fixes #4236, fixes #4353
Fixes #4464, fixes #4530, fixes #4532, fixes #4563, fixes #4674
Fixes #4756, fixes #4883, fixes #4928, fixes #4991, fixes #5016
Fixes #5057, fixes #5067, fixes #5084, fixes #5125
  • Loading branch information
berknam authored Aug 16, 2020
1 parent 2254e8e commit 91ca71f
Show file tree
Hide file tree
Showing 27 changed files with 4,033 additions and 239 deletions.
88 changes: 65 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ Here's some ideas on what you can do with neovim integration:

Custom remappings are defined on a per-mode basis.

#### `"vim.insertModeKeyBindings"`/`"vim.normalModeKeyBindings"`/`"vim.visualModeKeyBindings"`
#### `"vim.insertModeKeyBindings"`/`"vim.normalModeKeyBindings"`/`"vim.visualModeKeyBindings"`/`"vim.operatorPendingModeKeyBindings"`

- Keybinding overrides to use for insert, normal, and visual modes.
- Keybinding overrides to use for insert, normal, operatorPending and visual modes.
- Bind `jj` to `<Esc>` in insert mode:

```json
Expand All @@ -179,7 +179,7 @@ Custom remappings are defined on a per-mode basis.
- Bind `:` to show the command palette:

```json
"vim.normalModeKeyBindingsNonRecursive": [
"vim.normalModeKeyBindings": [
{
"before": [":"],
"commands": [
Expand All @@ -192,7 +192,7 @@ Custom remappings are defined on a per-mode basis.
- Bind `<leader>m` to add a bookmark and `<leader>b` to open the list of all bookmarks (using the [Bookmarks](https://marketplace.visualstudio.com/items?itemName=alefragnani.Bookmarks) extension):

```json
"vim.normalModeKeyBindingsNonRecursive": [
"vim.normalModeKeyBindings": [
{
"before": ["<leader>", "m"],
"commands": [
Expand All @@ -211,7 +211,7 @@ Custom remappings are defined on a per-mode basis.
- Bind `ZZ` to the vim command `:wq` (save and close the current file):

```json
"vim.normalModeKeyBindingsNonRecursive": [
"vim.normalModeKeyBindings": [
{
"before": ["Z", "Z"],
"commands": [
Expand All @@ -224,7 +224,7 @@ Custom remappings are defined on a per-mode basis.
- Bind `ctrl+n` to turn off search highlighting and `<leader>w` to save the current file:

```json
"vim.normalModeKeyBindingsNonRecursive": [
"vim.normalModeKeyBindings": [
{
"before":["<C-n>"],
"commands": [
Expand All @@ -240,28 +240,36 @@ Custom remappings are defined on a per-mode basis.
]
```

- Bind `p` in visual mode to paste without overriding the current register
- Bind `{` to `w` in operator pending mode makes `y{` and `d{` work like `yw` and `dw` respectively.

```json
"vim.visualModeKeyBindingsNonRecursive": [
"vim.operatorPendingModeKeyBindings": [
{
"before": [
"p",
],
"after": [
"p",
"g",
"v",
"y"
]
"before": ["{"],
"after": ["w"]
}
],
]
```

- Bind `L` to `$` and `H` to `^` in operator pending mode makes `yL` and `dH` work like `yL` and `d^` respectively.

```json
"vim.operatorPendingModeKeyBindings": [
{
"before": ["L"],
"after": ["$"]
},
{
"before": ["H"],
"after": ["^"]
}
]
```

- Bind `>` and `<` in visual mode to indent/outdent lines (repeatable)

```json
"vim.visualModeKeyBindingsNonRecursive": [
"vim.visualModeKeyBindings": [
{
"before": [
">"
Expand All @@ -284,7 +292,7 @@ Custom remappings are defined on a per-mode basis.
- Bind `<leader>vim` to clone this repository to the selected location.

```json
"vim.visualModeKeyBindingsNonRecursive": [
"vim.visualModeKeyBindings": [
{
"before": [
"<leader>", "v", "i", "m"
Expand All @@ -299,20 +307,53 @@ Custom remappings are defined on a per-mode basis.
]
```

#### `"vim.insertModeKeyBindingsNonRecursive"`/`"normalModeKeyBindingsNonRecursive"`/`"visualModeKeyBindingsNonRecursive"`
#### `"vim.insertModeKeyBindingsNonRecursive"`/`"normalModeKeyBindingsNonRecursive"`/`"visualModeKeyBindingsNonRecursive"`/`"operatorPendingModeKeyBindingsNonRecursive"`

- Non-recursive keybinding overrides to use for insert, normal, and visual modes
- _Example:_ Bind `j` to `gj`. Notice that if you attempted this binding normally, the j in gj would be expanded into gj, on and on forever. Stop this recursive expansion using insertModeKeyBindingsNonRecursive and/or normalModeKeyBindingNonRecursive.
- _Example:_ Exchange the meaning of two keys like `j` to `k` and `k` to `j` to exchange the cursor up and down commands. Notice that if you attempted this binding normally, the `j` would be replaced with `k` and the `k` would be replaced with `j`, on and on forever. When this happens 'maxmapdepth' times (default 1000) the error message 'E223 Recursive Mapping' will be thrown. Stop this recursive expansion using the NonRecursive variation of the keybindings.

```json
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": ["j"],
"after": ["g", "j"]
"after": ["k"]
},
{
"before": ["k"],
"after": ["j"]
}
]
```

- Bind `(` to 'i(' in operator pending mode makes 'y(' and 'c(' work like 'yi(' and 'ci(' respectively.

```json
"vim.operatorPendingModeKeyBindingsNonRecursive": [
{
"before": ["("],
"after": ["i("]
}
]
```

- Bind `p` in visual mode to paste without overriding the current register

```json
"vim.visualModeKeyBindingsNonRecursive": [
{
"before": [
"p",
],
"after": [
"p",
"g",
"v",
"y"
]
}
],
```

#### Debugging Remappings

1. Are your configurations correct?
Expand Down Expand Up @@ -361,6 +402,7 @@ Configuration settings that have been copied from vim. Vim settings are loaded i
| vim.smartcase | Override the 'ignorecase' setting if search pattern contains uppercase characters | Boolean | true |
| vim.textwidth | Width to word-wrap when using `gq` | Number | 80 |
| vim.timeout | Timeout in milliseconds for remapped commands | Number | 1000 |
| vim.maxmapdepth | Maximum number of times a mapping is done without resulting in a character to be used. This normally catches endless mappings, like ":map x y" with ":map y x". It still does not catch ":map g wg", because the 'w' is used before the next mapping is done. | Number | 1000 |
| vim.whichwrap | Controls wrapping at beginning and end of line. Comma-separated set of keys that should wrap to next/previous line. Arrow keys are represented by `[` and `]` in insert mode, `<` and `>` in normal and visual mode. To wrap "everything", set this to `h,l,<,>,[,]`. | String | `` |
| vim.report | Threshold for reporting number of lines changed. | Number | 2 |

Expand Down
22 changes: 19 additions & 3 deletions extensionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { configuration } from './src/configuration/configuration';
import { globalState } from './src/state/globalState';
import { taskQueue } from './src/taskQueue';
import { Register } from './src/register/register';
import { SpecialKeys } from './src/util/specialKeys';

let extensionContext: vscode.ExtensionContext;
let previousActiveEditorId: EditorIdentity | undefined = undefined;
Expand Down Expand Up @@ -434,7 +435,13 @@ export async function activate(
});

for (const boundKey of configuration.boundKeyCombinations) {
registerCommand(context, boundKey.command, () => handleKeyEvent(`${boundKey.key}`));
registerCommand(context, boundKey.command, () => {
if (['<Esc>', '<C-c>'].includes(boundKey.key)) {
checkIfRecursiveRemapping(`${boundKey.key}`);
} else {
handleKeyEvent(`${boundKey.key}`);
}
});
}

// Initialize mode handler for current active Text Editor at startup.
Expand Down Expand Up @@ -468,11 +475,11 @@ async function toggleExtension(isDisabled: boolean, compositionState: Compositio
}
let mh = await getAndUpdateModeHandler();
if (isDisabled) {
await mh.handleKeyEvent('<ExtensionDisable>');
await mh.handleKeyEvent(SpecialKeys.ExtensionDisable);
compositionState.reset();
ModeHandlerMap.clear();
} else {
await mh.handleKeyEvent('<ExtensionEnable>');
await mh.handleKeyEvent(SpecialKeys.ExtensionEnable);
}
}

Expand Down Expand Up @@ -546,6 +553,15 @@ async function handleKeyEvent(key: string): Promise<void> {
});
}

async function checkIfRecursiveRemapping(key: string): Promise<void> {
const mh = await getAndUpdateModeHandler();
if (mh.vimState.isCurrentlyPerformingRecursiveRemapping) {
mh.vimState.forceStopRecursiveRemapping = true;
} else {
handleKeyEvent(key);
}
}

function handleContentChangedFromDisk(document: vscode.TextDocument): void {
ModeHandlerMap.getAll()
.filter((modeHandler) => modeHandler.vimState.identity.fileName === document.fileName)
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,14 @@
"type": "array",
"markdownDescription": "Non-recursive remapped keys in Normal mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details."
},
"vim.operatorPendingModeKeyBindings": {
"type": "array",
"markdownDescription": "Remapped keys in OperatorPending mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details."
},
"vim.operatorPendingModeKeyBindingsNonRecursive": {
"type": "array",
"markdownDescription": "Non-recursive remapped keys in OperatorPending mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details."
},
"vim.useCtrlKeys": {
"type": "boolean",
"markdownDescription": "Enable some Vim Ctrl key commands that override otherwise common operations, like `Ctrl+C`.",
Expand Down Expand Up @@ -500,6 +508,12 @@
"default": 1000,
"minimum": 0
},
"vim.maxmapdepth": {
"type": "number",
"description": "Maximum number of times a mapping is done without resulting in a character to be used.",
"default": 1000,
"minimum": 0
},
"vim.scroll": {
"type": "number",
"markdownDescription": "Number of lines to scroll with `Ctrl-U` and `Ctrl-D` commands. Set to 0 to use a half page scroll.",
Expand Down
6 changes: 4 additions & 2 deletions src/actions/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export abstract class BaseAction {
public doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
if (
this.mustBeFirstKey &&
vimState.recordedState.commandWithoutCountPrefix.length > keysPressed.length
(vimState.recordedState.commandWithoutCountPrefix.length > keysPressed.length ||
vimState.recordedState.operator)
) {
return false;
}
Expand All @@ -80,7 +81,8 @@ export abstract class BaseAction {

if (
this.mustBeFirstKey &&
vimState.recordedState.commandWithoutCountPrefix.length > keysPressed.length
(vimState.recordedState.commandWithoutCountPrefix.length > keysPressed.length ||
vimState.recordedState.operator)
) {
return false;
}
Expand Down
13 changes: 8 additions & 5 deletions src/actions/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { StatusBar } from '../../statusBar';
import { reportFileInfo, reportSearch } from '../../util/statusBarTextUtils';
import { globalState } from '../../state/globalState';
import { VimError, ErrorCode } from '../../error';
import { SpecialKeys } from '../../util/specialKeys';
import _ = require('lodash');

export class DocumentContentChangeAction extends BaseAction {
Expand Down Expand Up @@ -184,7 +185,7 @@ class DisableExtension extends BaseCommand {
Mode.EasyMotionInputMode,
Mode.SurroundInputMode,
];
keys = ['<ExtensionDisable>'];
keys = [SpecialKeys.ExtensionDisable];

public async exec(position: Position, vimState: VimState): Promise<VimState> {
await vimState.setCurrentMode(Mode.Disabled);
Expand All @@ -195,7 +196,7 @@ class DisableExtension extends BaseCommand {
@RegisterAction
class EnableExtension extends BaseCommand {
modes = [Mode.Disabled];
keys = ['<ExtensionEnable>'];
keys = [SpecialKeys.ExtensionEnable];

public async exec(position: Position, vimState: VimState): Promise<VimState> {
await vimState.setCurrentMode(Mode.Normal);
Expand Down Expand Up @@ -1176,7 +1177,7 @@ export class CommandShowSearchHistory extends BaseCommand {
}

public async exec(position: Position, vimState: VimState): Promise<VimState> {
if (vimState.recordedState.commandList.includes('?')) {
if (this.keysPressed.includes('?')) {
this.direction = SearchDirection.Backward;
}
vimState.recordedState.transformations.push({
Expand Down Expand Up @@ -2458,8 +2459,10 @@ export class ActionDeleteCharWithDeleteKey extends BaseCommand {
// http://vimdoc.sourceforge.net/htmldoc/change.html#<Del>
if (vimState.recordedState.count !== 0) {
vimState.recordedState.count = Math.floor(vimState.recordedState.count / 10);
vimState.recordedState.actionKeys = vimState.recordedState.count.toString().split('');
vimState.recordedState.commandList = vimState.recordedState.count.toString().split('');

// Change actionsRunPressedKeys so that showCmd updates correctly
vimState.recordedState.actionsRunPressedKeys =
vimState.recordedState.count > 0 ? vimState.recordedState.count.toString().split('') : [];
this.isCompleteAction = false;
return vimState;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cmd_line/commands/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export class SubstituteCommand extends node.CommandBase {
];

vimState.editor.revealRange(new vscode.Range(line, 0, line, 0));
vimState.editor.setDecorations(decoration.SearchHighlight, searchRanges);
vimState.editor.setDecorations(decoration.searchHighlight, searchRanges);

const prompt = `Replace with ${replacement} (${validSelections.join('/')})?`;
await vscode.window.showInputBox(
Expand Down
20 changes: 16 additions & 4 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class Configuration implements IConfiguration {

this.leader = Notation.NormalizeKey(this.leader, this.leaderDefault);

this.clearKeyBindingsMaps();

const validatorResults = await configurationValidator.validate(configuration);

// wrap keys
Expand Down Expand Up @@ -160,6 +162,15 @@ class Configuration implements IConfiguration {
return this.cursorTypeMap[cursorStyle];
}

clearKeyBindingsMaps() {
// Clear the KeyBindingsMaps so that the previous configuration maps don't leak to this one
this.normalModeKeyBindingsMap = new Map<string, IKeyRemapping>();
this.insertModeKeyBindingsMap = new Map<string, IKeyRemapping>();
this.visualModeKeyBindingsMap = new Map<string, IKeyRemapping>();
this.commandLineModeKeyBindingsMap = new Map<string, IKeyRemapping>();
this.operatorPendingModeKeyBindingsMap = new Map<string, IKeyRemapping>();
}

handleKeys: IHandleKeys[] = [];

useSystemClipboard = false;
Expand Down Expand Up @@ -220,6 +231,8 @@ class Configuration implements IConfiguration {

timeout = 1000;

maxmapdepth = 1000;

showcmd = true;

showmodename = true;
Expand Down Expand Up @@ -368,19 +381,18 @@ class Configuration implements IConfiguration {
insertModeKeyBindingsNonRecursive: IKeyRemapping[] = [];
normalModeKeyBindings: IKeyRemapping[] = [];
normalModeKeyBindingsNonRecursive: IKeyRemapping[] = [];
operatorPendingModeKeyBindings: IKeyRemapping[] = [];
operatorPendingModeKeyBindingsNonRecursive: IKeyRemapping[] = [];
visualModeKeyBindings: IKeyRemapping[] = [];
visualModeKeyBindingsNonRecursive: IKeyRemapping[] = [];
commandLineModeKeyBindings: IKeyRemapping[] = [];
commandLineModeKeyBindingsNonRecursive: IKeyRemapping[] = [];

insertModeKeyBindingsMap: Map<string, IKeyRemapping>;
insertModeKeyBindingsNonRecursiveMap: Map<string, IKeyRemapping>;
normalModeKeyBindingsMap: Map<string, IKeyRemapping>;
normalModeKeyBindingsNonRecursiveMap: Map<string, IKeyRemapping>;
operatorPendingModeKeyBindingsMap: Map<string, IKeyRemapping>;
visualModeKeyBindingsMap: Map<string, IKeyRemapping>;
visualModeKeyBindingsNonRecursiveMap: Map<string, IKeyRemapping>;
commandLineModeKeyBindingsMap: Map<string, IKeyRemapping>;
commandLineModeKeyBindingsNonRecursiveMap: Map<string, IKeyRemapping>;

private static unproxify(obj: Object): Object {
let result = {};
Expand Down
Loading

0 comments on commit 91ca71f

Please sign in to comment.