Skip to content

Commit

Permalink
fix: enable remapping of numbers. closes #2759
Browse files Browse the repository at this point in the history
  • Loading branch information
jpoon committed Jun 30, 2018
1 parent 24cb297 commit e6a2b8a
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 101 deletions.
10 changes: 2 additions & 8 deletions src/actions/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,7 @@ export class BaseAction {
if (!compareKeypressSequence(this.keys, keysPressed)) {
return false;
}
if (
this.mustBeFirstKey &&
vimState.recordedState.numberOfKeysInCommandWithoutCountPrefix - keysPressed.length > 0
) {
if (this.mustBeFirstKey && vimState.recordedState.isPendingCommandPrefixedWithCount) {
return false;
}

Expand All @@ -156,10 +153,7 @@ export class BaseAction {
return false;
}

if (
this.mustBeFirstKey &&
vimState.recordedState.numberOfKeysInCommandWithoutCountPrefix - keysPressed.length > 0
) {
if (this.mustBeFirstKey && vimState.recordedState.isPendingCommandPrefixedWithCount) {
return false;
}

Expand Down
10 changes: 2 additions & 8 deletions src/actions/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ export class BaseOperator extends BaseAction {
if (!compareKeypressSequence(this.keys, keysPressed)) {
return false;
}
if (
this.mustBeFirstKey &&
vimState.recordedState.numberOfKeysInCommandWithoutCountPrefix - keysPressed.length > 0
) {
if (this.mustBeFirstKey && vimState.recordedState.isPendingCommandPrefixedWithCount) {
return false;
}
if (this instanceof BaseOperator && vimState.recordedState.operator) {
Expand All @@ -55,10 +52,7 @@ export class BaseOperator extends BaseAction {
if (!compareKeypressSequence(this.keys.slice(0, keysPressed.length), keysPressed)) {
return false;
}
if (
this.mustBeFirstKey &&
vimState.recordedState.numberOfKeysInCommandWithoutCountPrefix - keysPressed.length > 0
) {
if (this.mustBeFirstKey && vimState.recordedState.isPendingCommandPrefixedWithCount) {
return false;
}
if (this instanceof BaseOperator && vimState.recordedState.operator) {
Expand Down
121 changes: 76 additions & 45 deletions src/configuration/remapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class Remappers implements IRemapper {
}
}

class Remapper implements IRemapper {
export class Remapper implements IRemapper {
private readonly _configKey: string;
private readonly _remappedModes: ModeName[];
private readonly _recursive: boolean;
Expand Down Expand Up @@ -81,30 +81,11 @@ class Remapper implements IRemapper {

const userDefinedRemappings = this._getRemappings();

// Check to see if the keystrokes match any user-specified remapping.
let remapping: IKeyRemapping | undefined;
if (vimState.currentMode === ModeName.Insert) {
// In insert mode, we allow users to precede remapped commands
// with extraneous keystrokes (e.g. "hello world jj")
const longestKeySequence = Remapper._getLongestedRemappedKeySequence(userDefinedRemappings);
for (let sliceLength = 1; sliceLength <= longestKeySequence; sliceLength++) {
const slice = keys.slice(-sliceLength);
const result = _.find(
userDefinedRemappings,
remap => remap.before.join('') === slice.join('')
);

if (result) {
remapping = result;
break;
}
}
} else {
// In other modes, we have to precisely match the entire keysequence
remapping = _.find(userDefinedRemappings, map => {
return map.before.join('') === keys.join('');
});
}
let remapping: IKeyRemapping | undefined = Remapper._findMatchingRemap(
userDefinedRemappings,
keys,
vimState.currentMode
);

if (remapping) {
logger.debug(
Expand All @@ -117,25 +98,25 @@ class Remapper implements IRemapper {
vimState.isCurrentlyPerformingRemapping = true;
}

// Record length of remapped command
vimState.recordedState.numberOfRemappedKeys += remapping.before.length;

const numToRemove = remapping.before.length - 1;
const numCharsToRemove = remapping.before.length - 1;
// Revert previously inserted characters
// (e.g. jj remapped to esc, we have to revert the inserted "jj")
if (vimState.currentMode === ModeName.Insert) {
// Revert every single inserted character.
// We subtract 1 because we haven't actually applied the last key.
await vimState.historyTracker.undoAndRemoveChanges(
Math.max(0, numToRemove * vimState.allCursors.length)
Math.max(0, numCharsToRemove * vimState.allCursors.length)
);
vimState.cursorPosition = vimState.cursorPosition.getLeft(numToRemove);
vimState.cursorPosition = vimState.cursorPosition.getLeft(numCharsToRemove);
}

// We need to remove the keys that were remapped into different keys
// from the state.
vimState.recordedState.actionKeys = vimState.recordedState.actionKeys.slice(0, -numToRemove);
vimState.keyHistory = vimState.keyHistory.slice(0, -numToRemove);
vimState.recordedState.actionKeys = vimState.recordedState.actionKeys.slice(
0,
-numCharsToRemove
);
vimState.keyHistory = vimState.keyHistory.slice(0, -numCharsToRemove);

if (remapping.after) {
const count = vimState.recordedState.count || 1;
Expand Down Expand Up @@ -170,8 +151,8 @@ class Remapper implements IRemapper {
}

// Check to see if a remapping could potentially be applied when more keys are received
for (let remap of userDefinedRemappings) {
if (keys.join('') === remap.before.slice(0, keys.length).join('')) {
for (let remap of Object.keys(userDefinedRemappings)) {
if (keys.join('') === remap.slice(0, keys.length)) {
this._isPotentialRemap = true;
break;
}
Expand All @@ -180,8 +161,8 @@ class Remapper implements IRemapper {
return false;
}

private _getRemappings(): IKeyRemapping[] {
let remappings: IKeyRemapping[] = [];
private _getRemappings(): { [key: string]: IKeyRemapping } {
let remappings: { [key: string]: IKeyRemapping } = {};
for (let remapping of configuration[this._configKey] as IKeyRemapping[]) {
let debugMsg = `before=${remapping.before}. `;

Expand All @@ -197,22 +178,72 @@ class Remapper implements IRemapper {

if (!remapping.after && !remapping.commands) {
logger.error(
`Remapper: ${this._configKey}. Invalid configuration. Missing remapped 'after' key or 'command'. ${debugMsg}`
`Remapper: ${
this._configKey
}. Invalid configuration. Missing 'after' key or 'command'. ${debugMsg}`
);
} else {
logger.debug(`Remapper: ${this._configKey}. ${debugMsg}`);
remappings.push(remapping);
continue;
}

const keys = remapping.before.join('');
if (keys in remappings) {
logger.error(`Remapper: ${this._configKey}. Duplicate configuration. ${debugMsg}`);
continue;
}

logger.debug(`Remapper: ${this._configKey}. ${debugMsg}`);
remappings[keys] = remapping;
}

return remappings;
}

private static _getLongestedRemappedKeySequence(remappings: IKeyRemapping[]): number {
if (remappings.length === 0) {
return 1;
protected static _findMatchingRemap(
userDefinedRemappings: { [key: string]: IKeyRemapping },
inputtedKeys: string[],
currentMode: ModeName
) {
let remapping: IKeyRemapping | undefined;

let range = Remapper._getRemappedKeysLengthRange(userDefinedRemappings);
const startingSliceLength = Math.max(range[1], inputtedKeys.length);
for (let sliceLength = startingSliceLength; sliceLength >= range[0]; sliceLength--) {
const keySlice = inputtedKeys.slice(-sliceLength).join('');

if (keySlice in userDefinedRemappings) {
// In Insert mode, we allow users to precede remapped commands
// with extraneous keystrokes (eg. "hello world jj")
// In other modes, we have to precisely match the keysequence
// unless the preceding keys are numbers
if (currentMode !== ModeName.Insert) {
const precedingKeys = inputtedKeys
.slice(0, inputtedKeys.length - keySlice.length)
.join('');
if (precedingKeys.length > 0 && !/^[0-9]+$/.test(precedingKeys)) {
break;
}
}

remapping = userDefinedRemappings[keySlice];
break;
}
}

return remapping;
}

/**
* Given list of remappings, returns the length of the shortest and longest remapped keys
* @param remappings
*/
protected static _getRemappedKeysLengthRange(remappings: {
[key: string]: IKeyRemapping;
}): [number, number] {
const keys = Object.keys(remappings);
if (keys.length === 0) {
return [0, 0];
}
return _.maxBy(remappings, map => map.before.length)!.before.length;
return [_.minBy(keys, m => m.length)!.length, _.maxBy(keys, m => m.length)!.length];
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/mode/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ export class ModeHandler implements vscode.Disposable {

try {
// Take the count prefix out to perform the correct remapping.
const keys = this.vimState.recordedState.getCurrentCommandWithoutCountPrefix();
const withinTimeout = now - this.vimState.lastKeyPressedTimestamp < configuration.timeout;

let handled = false;
Expand All @@ -316,8 +315,15 @@ export class ModeHandler implements vscode.Disposable {
* 1) We are not already performing a nonrecursive remapping.
* 2) We haven't timed out of our previous remapping.
*/
if (!this.vimState.isCurrentlyPerformingRemapping && (withinTimeout || keys.length === 1)) {
handled = await this._remappers.sendKey(keys, this, this.vimState);
if (
!this.vimState.isCurrentlyPerformingRemapping &&
(withinTimeout || this.vimState.recordedState.commandList.length === 1)
) {
handled = await this._remappers.sendKey(
this.vimState.recordedState.commandList,
this,
this.vimState
);
}

if (handled) {
Expand Down
34 changes: 6 additions & 28 deletions src/state/recordedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ export class RecordedState {
*/
public commandList: string[] = [];

/**
* The number of keys the user has pressed that have been remapped.
*/
public numberOfRemappedKeys: number = 0;

/**
* String representation of the exact keys that the user entered. Used for
* showcmd.
Expand All @@ -63,40 +58,23 @@ export class RecordedState {

return result;
}

/**
* get the current command without the prefixed count.
* For instance: if the current commandList is ['2', 'h'], returns only ['h'].
* Determines if the current command list is prefixed with a count
*/
public getCurrentCommandWithoutCountPrefix(): string[] {
const commandList = this.commandList;
const result: string[] = [];
let previousWasCount = true;

for (const commandKey of commandList) {
if (previousWasCount && commandKey.match(/[0-9]/)) {
continue;
} else {
previousWasCount = false;
result.push(commandKey);
}
public get isCommandPrefixedWithCount() {
if (this.commandList.length === 0) {
return false;
}

return result;
}

/**
* lenth of the current command with remappings and the prefixed count excluded.
*/
public get numberOfKeysInCommandWithoutCountPrefix() {
return this.getCurrentCommandWithoutCountPrefix().length - this.numberOfRemappedKeys;
return /^[0-9]+$/.test(this.commandList[0]);
}

/**
* Reset the command list.
*/
public resetCommandList() {
this.commandList = [];
this.numberOfRemappedKeys = 0;
}

/**
Expand Down
Loading

0 comments on commit e6a2b8a

Please sign in to comment.