mirror of
https://github.com/VSCodeVim/Vim.git
synced 2024-09-11 12:25:59 +03:00
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:
parent
cc59ae0f94
commit
00f094af7c
@ -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}`));
|
||||
});
|
||||
|
||||
|
10
package.json
10
package.json
@ -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",
|
||||
|
@ -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;
|
||||
}
|
@ -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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
32
src/number/numericString.ts
Normal file
32
src/number/numericString.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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"]
|
||||
});
|
||||
});
|
25
test/number/numericString.test.ts
Normal file
25
test/number/numericString.test.ts
Normal 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());
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user