diff --git a/package.json b/package.json index bf8b3b9f5a6..e5c43f000aa 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "description": "Keybinding overrides to use for normal mode." }, "vim.insertModeKeyBindings": { - "type": "object", + "type": "array", "description": "Keybinding overrides to use for insert mode." }, "vim.useSolidBlockCursor": { diff --git a/src/history/historyTracker.ts b/src/history/historyTracker.ts index aa3505a94b6..cea1f01c96a 100644 --- a/src/history/historyTracker.ts +++ b/src/history/historyTracker.ts @@ -344,6 +344,21 @@ export class HistoryTracker { this.oldText = newText; } + /** + * Both undoes and completely removes the last n changes applied. + */ + async undoAndRemoveChanges(n: number): Promise { + if (this.currentHistoryStep.changes.length < n) { + console.log("Something bad happened in removeChange"); + + return; + } + + for (let i = 0; i < n; i++) { + this.currentHistoryStep.changes.pop().undo(); + } + } + /** * Tells the HistoryTracker that although the document has changed, we should simply * ignore that change. Most often used when the change was itself triggered by diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index 0780807b856..5e068655367 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -5,6 +5,7 @@ import * as _ from 'lodash'; import { getAndUpdateModeHandler } from './../../extension'; import { Mode, ModeName, VSCodeVimCursorType } from './mode'; +import { InsertModeRemapper, OtherModesRemapper } from './remapper'; import { NormalMode } from './modeNormal'; import { InsertMode } from './modeInsert'; import { VisualMode } from './modeVisual'; @@ -333,6 +334,8 @@ export class ModeHandler implements vscode.Disposable { private _statusBarItem: vscode.StatusBarItem; private _configuration: Configuration; private _vimState: VimState; + private _insertModeRemapper: InsertModeRemapper; + private _otherModesRemapper: OtherModesRemapper; public get vimState(): VimState { return this._vimState; @@ -374,6 +377,8 @@ export class ModeHandler implements vscode.Disposable { this._configuration = Configuration.fromUserFile(); this._vimState = new VimState(); + this._insertModeRemapper = new InsertModeRemapper(); + this._otherModesRemapper = new OtherModesRemapper(); this._modes = [ new NormalMode(this), new InsertMode(), @@ -500,10 +505,23 @@ export class ModeHandler implements vscode.Disposable { async handleKeyEvent(key: string): Promise { if (key === "") { key = "ctrl+r"; } // TODO - temporary hack for tests only! + // Due to a limitation in Electron, en-US QWERTY char codes are used in international keyboards. + // We'll try to mitigate this problem until it's fixed upstream. + // https://github.com/Microsoft/vscode/issues/713 + + key = this._configuration.keyboardLayout.translate(key); + this._vimState.cursorPositionJustBeforeAnythingHappened = this._vimState.cursorPosition; try { - this._vimState = await this.handleKeyEventHelper(key, this._vimState); + let handled = false; + + handled = handled || await this._insertModeRemapper.sendKey(key, this, this.vimState); + handled = handled || await this._otherModesRemapper.sendKey(key, this, this.vimState); + + if (!handled) { + this._vimState = await this.handleKeyEventHelper(key, this._vimState); + } } catch (e) { console.log('error.stack'); console.log(e); @@ -518,12 +536,6 @@ export class ModeHandler implements vscode.Disposable { } async handleKeyEventHelper(key: string, vimState: VimState): Promise { - // Due to a limitation in Electron, en-US QWERTY char codes are used in international keyboards. - // We'll try to mitigate this problem until it's fixed upstream. - // https://github.com/Microsoft/vscode/issues/713 - - key = this._configuration.keyboardLayout.translate(key); - let recordedState = vimState.recordedState; recordedState.actionKeys.push(key); diff --git a/src/mode/remapper.ts b/src/mode/remapper.ts new file mode 100644 index 00000000000..a1089528195 --- /dev/null +++ b/src/mode/remapper.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; +import * as _ from 'lodash'; + +import { ModeName } from './mode'; +import { ModeHandler, VimState } from './modeHandler'; + +interface IKeybinding { + before: string[]; + after : string[]; +} + +class Remapper { + private _mostRecentKeys: string[] = []; + + private _remappings: IKeybinding[] = []; + + private _isInsertModeRemapping = false; + + constructor(configKey: string, insertModeRemapping = false) { + this._isInsertModeRemapping = insertModeRemapping; + this._remappings = vscode.workspace.getConfiguration('vim') + .get(configKey, []); + } + + private _longestKeySequence(): number { + if (this._remappings.length > 0) { + return _.maxBy(this._remappings, map => map.before.length).before.length; + } else { + return 1; + } + } + + public async sendKey(key: string, modeHandler: ModeHandler, vimState: VimState): Promise { + if ((vimState.currentMode === ModeName.Insert && !this._isInsertModeRemapping) || + (vimState.currentMode !== ModeName.Insert && this._isInsertModeRemapping)) { + + this._reset(); + + return false; + } + + const longestKeySequence = this._longestKeySequence(); + + this._mostRecentKeys.push(key); + this._mostRecentKeys = this._mostRecentKeys.slice(-longestKeySequence); + + for (let sliceLength = 1; sliceLength <= longestKeySequence; sliceLength++) { + const slice = this._mostRecentKeys.slice(-sliceLength); + const remapping = _.find(this._remappings, map => map.before.join("") === slice.join("")); + + if (remapping) { + if (this._isInsertModeRemapping) { + vimState.historyTracker.undoAndRemoveChanges(this._mostRecentKeys.length); + } + + await modeHandler.handleMultipleKeyEvents(remapping.after); + + this._mostRecentKeys = []; + + return true; + } + } + + return false; + } + + private _reset(): void { + this._mostRecentKeys = []; + } +} + +export class InsertModeRemapper extends Remapper { + constructor() { + super("insertModeKeyBindings", true); + } +} + +export class OtherModesRemapper extends Remapper { + constructor() { + super("normalModeKeyBindings", false); + } +} \ No newline at end of file