Implement increment and decrement operators (#515)

* Implement increment and decrement operators

This adds support for single or numeric-prefix control+x and control+a.

* Add numeric string helper

* Unit tests for numericString

* Handle octal

* Integration test for octal

* Guard control-a behind flag

* Use a better algorithm

* Extract IterateWords to Position

* Use destructuring

* Handle negatives

* Flag it up

* Re-add the hacks, even hackier this time

* Remove outdated comment
This commit is contained in:
Aiden Scandella 2016-07-25 23:30:22 -07:00 committed by Grant Mathews
parent cc59ae0f94
commit 00f094af7c
8 changed files with 206 additions and 1 deletions

View File

@ -179,7 +179,7 @@ export async function activate(context: vscode.ExtensionContext) {
showCmdLine("", modeHandlerToEditorIdentity[new EditorIdentity(vscode.window.activeTextEditor).toString()]);
});
'rfbducw['.split('').forEach(key => {
'rfbducw[ax'.split('').forEach(key => {
registerCommand(context, `extension.vim_ctrl+${key}`, () => handleKeyEvent(`ctrl+${key}`));
});

View File

@ -92,6 +92,16 @@
"command": "extension.vim_ctrl+c",
"when": "editorTextFocus && vim.useCtrlKeys"
},
{
"key": "ctrl+a",
"command": "extension.vim_ctrl+a",
"when": "editorTextFocus && vim.useCtrlKeys"
},
{
"key": "ctrl+x",
"command": "extension.vim_ctrl+x",
"when": "editorTextFocus && vim.useCtrlKeys"
},
{
"key": "left",
"command": "extension.vim_left",

View File

@ -2,6 +2,7 @@ import { VimSpecialCommands, VimState, SearchState } from './../mode/modeHandler
import { ModeName } from './../mode/mode';
import { TextEditor } from './../textEditor';
import { Register, RegisterMode } from './../register/register';
import { NumericString } from './../number/numericString';
import { Position } from './../motion/position';
import { PairMatcher } from './../matching/matcher';
import { QuoteMatcher } from './../matching/quoteMatcher';
@ -2694,3 +2695,63 @@ class ToggleCaseAndMoveForward extends BaseMovement {
return position.getRight();
}
}
abstract class IncrementDecrementNumberAction extends BaseMovement {
modes = [ModeName.Normal];
canBePrefixedWithCount = true;
offset: number;
public async execActionWithCount(position: Position, vimState: VimState, count: number): Promise<Position> {
count = count || 1;
const text = TextEditor.getLineAt(position).text;
for (let { start, end, word } of Position.IterateWords(position.getWordLeft(true))) {
// '-' doesn't count as a word, but is important to include in parsing the number
if (text[start.character - 1] === '-') {
start = start.getLeft();
word = text[start.character] + word;
}
// Strict number parsing so "1a" doesn't silently get converted to "1"
const num = NumericString.parse(word);
if (num !== null) {
return this.replaceNum(num, this.offset * count, start, end);
}
}
// No usable numbers, return the original position
return position;
}
public async replaceNum(start: NumericString, offset: number, startPos: Position, endPos: Position): Promise<Position> {
const oldWidth = start.toString().length;
start.value += offset;
const newNum = start.toString();
const range = new vscode.Range(startPos, endPos.getRight());
if (oldWidth === newNum.length) {
await TextEditor.replace(range, newNum);
} else {
// Can't use replace, since new number is a different width than old
await TextEditor.delete(range);
await TextEditor.insertAt(newNum, startPos);
// Adjust end position according to difference in width of number-string
endPos = new Position(endPos.line, endPos.character + (newNum.length - oldWidth));
}
return endPos;
}
}
@RegisterAction
class IncrementNumberAction extends IncrementDecrementNumberAction {
keys = ["ctrl+a"];
offset = +1;
}
@RegisterAction
class DecrementNumberAction extends IncrementDecrementNumberAction {
keys = ["ctrl+x"];
offset = -1;
}

View File

@ -504,6 +504,8 @@ export class ModeHandler implements vscode.Disposable {
async handleKeyEvent(key: string): Promise<Boolean> {
if (key === "<c-r>") { key = "ctrl+r"; } // TODO - temporary hack for tests only!
if (key === "<c-a>") { key = "ctrl+a"; } // TODO - temporary hack for tests only!
if (key === "<c-x>") { key = "ctrl+x"; } // 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.

View File

@ -70,6 +70,25 @@ export class Position extends vscode.Position {
}
}
public static *IterateWords(start: Position): Iterable<{ start: Position, end: Position, word: string }> {
const text = TextEditor.getLineAt(start).text;
let wordEnd = start.getCurrentWordEnd(true);
do {
const word = text.substring(start.character, wordEnd.character + 1);
yield {
start: start,
end: wordEnd,
word: word,
};
if (wordEnd.isLineEnd()) {
return;
}
start = start.getWordRight();
wordEnd = start.getCurrentWordEnd();
} while (true);
}
/**
* Returns which of the 2 provided Positions comes later in the document.
*/

View File

@ -0,0 +1,32 @@
export class NumericString {
radix: number;
value: number;
prefix: string;
private static matchings: { regex: RegExp, base: number, prefix: string }[] = [
{ regex: /^([-+])?0([0-7]+)$/, base: 8, prefix: "0"},
{ regex: /^([-+])?(\d+)$/, base: 10, prefix: ""},
{ regex: /^([-+])?0x([\da-fA-F]+)$/, base: 16, prefix: "0x"},
];
static parse(input: string): NumericString | null {
for (const { regex, base, prefix } of NumericString.matchings) {
const match = regex.exec(input);
if (match == null) {
continue;
}
return new NumericString(parseInt(match[0], base), base, prefix);
}
return null;
}
constructor(value: number, radix: number, prefix: string) {
this.value = value;
this.radix = radix;
this.prefix = prefix;
}
public toString(): string {
return this.prefix + this.value.toString(this.radix);
}
}

View File

@ -963,4 +963,60 @@ suite("Mode Normal", () => {
keysPressed: "dE",
end: ["one two| "]
});
newTest({
title: "can ctrl-a correctly behind a word",
start: ["|one 9"],
keysPressed: "<c-a>",
end: ["one 1|0"]
});
newTest({
title: "can ctrl-a on word",
start: ["one -|11"],
keysPressed: "<c-a>",
end: ["one -1|0"]
});
newTest({
title: "can ctrl-a on a hex number",
start: ["|0xf"],
keysPressed: "<c-a>",
end: ["0x1|0"]
});
newTest({
title: "can ctrl-a on decimal",
start: ["1|1.123"],
keysPressed: "<c-a>",
end: ["1|2.123"]
});
newTest({
title: "can ctrl-a with numeric prefix",
start: ["|-10"],
keysPressed: "15<c-a>",
end: ["|5"]
});
newTest({
title: "can ctrl-a on a decimal",
start: ["-10.|1"],
keysPressed: "10<c-a>",
end: ["-10.1|1"]
});
newTest({
title: "can ctrl-a on an octal ",
start: ["07|"],
keysPressed: "<c-a>",
end: ["01|0"]
});
newTest({
title: "can ctrl-x correctly behind a word",
start: ["|one 10"],
keysPressed: "<c-x>",
end: ["one |9"]
});
});

View File

@ -0,0 +1,25 @@
"use strict";
import * as assert from 'assert';
import { NumericString } from '../../src/number/numericString';
suite("numeric string", () => {
test("fails on non-string", () => {
assert.equal(null, NumericString.parse("hi"));
});
test("handles hex round trip", () => {
const input = "0xa1";
assert.equal(input, NumericString.parse(input)!.toString());
});
test("handles decimal round trip", () => {
const input = "9";
assert.equal(input, NumericString.parse(input)!.toString());
});
test("handles octal trip", () => {
const input = "07";
assert.equal(input, NumericString.parse(input)!.toString());
});
});