Skip to content

Commit

Permalink
Further refactor code of <C-a> and <C-x>
Browse files Browse the repository at this point in the history
Current behavior of <C-a> and <C-x> is almost the same as original VIM's
  • Loading branch information
ldm0 committed Jan 4, 2020
1 parent 353041f commit cd58b1e
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 36 deletions.
25 changes: 12 additions & 13 deletions src/actions/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3920,14 +3920,15 @@ abstract class IncrementDecrementNumberAction extends BaseCommand {
}
// Strict number parsing so "1a" doesn't silently get converted to "1"
do {
const num = NumericString.parse(word);
if (num === undefined) {
const result = NumericString.parse(word);
if (result === undefined) {
break;
}
if (
position.character <
start.character + num.prefix.length + num.value.toString().length
) {
const num = result[0];
const suffixOffset = result[1];

// Use suffix offset to check if current cursor is in or before detected number.
if (position.character < start.character + suffixOffset) {
vimState.cursorStopPosition = await this.replaceNum(
num,
this.offset * (vimState.recordedState.count || 1),
Expand All @@ -3939,11 +3940,9 @@ abstract class IncrementDecrementNumberAction extends BaseCommand {
);
return vimState;
} else {
word = word.slice(num.prefix.length + num.value.toString().length);
start = new Position(
start.line,
start.character + num.prefix.length + num.value.toString().length
);
// For situation like this: xyz1999em199[cursor]9m
word = word.slice(suffixOffset);
start = new Position(start.line, start.character + suffixOffset);
}
} while (true);
}
Expand All @@ -3957,7 +3956,7 @@ abstract class IncrementDecrementNumberAction extends BaseCommand {
startPos: Position,
endPos: Position
): Promise<Position> {
const oldWidth = start.toString().length;
const oldWidth = endPos.character - startPos.character + 1;
start.value += offset;
const newNum = start.toString();

Expand All @@ -3970,7 +3969,7 @@ abstract class IncrementDecrementNumberAction extends BaseCommand {
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));
endPos = new Position(endPos.line, startPos.character + newNum.length - 1);
}

return endPos;
Expand Down
117 changes: 100 additions & 17 deletions src/common/number/numericString.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
/**
* aaaa0x111bbbbbb
* |-------------| => NumericString
* |--| => prefix
* |---| => core
* |----| => suffix
* || => numPreffix
* |-| => num
*
* Use eager parsing, leftmost matching is the best and if begins at same
* position, matching with biggest span wins.
* When begin and span are both the same, use following priority sequence:
*
* (decimal => octal => hexadecimal)
*
* Example:
* core | What we got | Rather than |
* Leftmost rule: 010xff | (010)xff [octal] | 01(0xff) [hex] |
* Biggest span rule: 0xff | (0xff) [hex] | (0)xff [decimal] |
* Priority rule: 00007 | (00007) [octal] | (00009) [hex] |
*
* Side Effect:
* 0 will be parsed as (0)[octal], it's trivial
* -0xf will be parsed as (-0)xf, workaround is capture '-' in hexadecimal
* to make begin index at '-' using hexNegative boolean value
*/
export class NumericString {
radix: number;
value: number;
numLength: number;
prefix: string;
suffix: string;
// Is there ancilla negative sign
negative: boolean;

private static matchings: { regex: RegExp; radix: number; prefix: string }[] = [
{ regex: /([-+])?0([0-7]+)/, radix: 8, prefix: '0' },
{ regex: /([-+])?(\d+)/, radix: 10, prefix: '' },
{ regex: /([-+])?0x([\da-fA-F]+)/, radix: 16, prefix: '0x' },
// Map radix to number prefix
private static numPrefix = {
8: '0',
10: '',
16: '0x',
};

// Keep octal top of decimal to avoid regarding 0000007 as decimal.
// Make 000009 match deicmal
// Make 000007 match octal
// Make -0xf match hex rather than decimal '-0'
private static matchings: { regex: RegExp; radix: number }[] = [
{ regex: /-?0[0-7]+/, radix: 8 },
{ regex: /-?\d+/, radix: 10 },
{ regex: /-?0x[\da-fA-F]+/, radix: 16 },
];

public static parse(input: string): NumericString | undefined {
// Return parse result and suffixOffset
public static parse(input: string): [NumericString, number] | undefined {
// Find core numeric part of input
let coreBegin = -1;
let coreLength = -1;
let coreRadix = -1;
let numPrefix = '';
for (const { regex, radix, prefix } of NumericString.matchings) {
for (const { regex, radix } of NumericString.matchings) {
const match = regex.exec(input);
if (match != null) {
// Get the left and large possible match
Expand All @@ -28,7 +68,6 @@ export class NumericString {
coreBegin = match.index;
coreLength = match[0].length;
coreRadix = radix;
numPrefix = prefix;
}
}
}
Expand All @@ -39,28 +78,72 @@ export class NumericString {

const coreEnd = coreBegin + coreLength;

const corePrefix = input.slice(0, coreBegin) + numPrefix;
const prefix = input.slice(0, coreBegin);
const core = input.slice(coreBegin, coreEnd);
const coreSuffix = input.slice(coreEnd, input.length);
const suffix = input.slice(coreEnd, input.length);

let value = parseInt(core, coreRadix);

// 0x00ff: numLength = 4
// 077: numLength = 2
// -0999: numLength = 3

return new NumericString(parseInt(core, coreRadix), coreRadix, corePrefix, coreSuffix);
// The numLength is only useful for non-decimal. Decimal with leading
// zero will be trimmed. If value is negative, remove the negative
// sign's length.
const numLength = coreLength - NumericString.numPrefix[coreRadix].length - (value < 0 ? 1 : 0);

// According to original vim's behavior, for 'hexadecimal' and 'octal',
// leading '-' *should* be captured and preseved but *should not* be
// regarded as part of number.
let negative = false;
if (coreRadix !== 10 && value < 0) {
value = -value;
negative = true;
}

return [new NumericString(value, coreRadix, numLength, prefix, suffix, negative), coreEnd];
}

private constructor(value: number, radix: number, prefix: string, suffix: string) {
private constructor(
value: number,
radix: number,
numLength: number,
prefix: string,
suffix: string,
negative: boolean
) {
this.value = value;
this.radix = radix;
this.numLength = numLength;
this.prefix = prefix;
this.suffix = suffix;
this.negative = negative;
}

public toString(): string {
// Allow signed hex represented as twos complement
if (this.radix === 16) {
if (this.value < 0) {
this.value = 0xffffffff + this.value + 1;
// For decreased octal and hexadecimal
if (this.radix !== 10) {
const max = 0xffffffff;
while (this.value < 0) {
this.value = max + this.value + 1;
}
}

// Gen num part
const absValue = Math.abs(this.value);
let num = absValue.toString(this.radix);
// According to original vim's behavior, numLength of decimal *should not*
// be preserved.
if (this.radix !== 10) {
const diff = this.numLength - num.length;
if (diff > 0) {
// Preserve num length if narrower.
num = '0'.repeat(diff) + num;
}
}

return this.prefix + this.value.toString(this.radix) + this.suffix;
const sign = this.negative || this.value < 0 ? '-' : '';
return this.prefix + sign + NumericString.numPrefix[this.radix] + num + this.suffix;
}
}
42 changes: 42 additions & 0 deletions test/mode/modeNormal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,48 @@ suite('Mode Normal', () => {
end: ['0|1xf'],
});

newTest({
title: 'can ctrl-a preserve leading zeros of octal',
start: ['|000007'],
keysPressed: '<C-a>',
end: ['00001|0'],
});

newTest({
title: 'can ctrl-a trim leading zeros of decimal',
start: ['|000009'],
keysPressed: '<C-a>',
end: ['1|0'],
});

newTest({
title: 'can ctrl-a on octal ignore negative sign',
start: ['|test-0116'],
keysPressed: '<C-a>',
end: ['test-011|7'],
});

newTest({
title: 'can ctrl-a on octal ignore positive sign',
start: ['|test+0116'],
keysPressed: '<C-a>',
end: ['test+011|7'],
});

newTest({
title: 'can ctrl-a on hex number ignore negative sign',
start: ['|test-0xf'],
keysPressed: '<C-a>',
end: ['test-0x1|0'],
});

newTest({
title: 'can ctrl-a on hex number ignore positive sign',
start: ['|test+0xf'],
keysPressed: '<C-a>',
end: ['test+0x1|0'],
});

newTest({
title: 'can ctrl-x correctly behind a word',
start: ['|one 10'],
Expand Down
12 changes: 6 additions & 6 deletions test/number/numericString.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ suite('numeric string', () => {

test('handles hex round trip', () => {
const input = '0xa1';
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
// run each assertion twice to make sure that regex state doesn't cause failures
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
});

test('handles decimal round trip', () => {
const input = '9';
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
});

test('handles octal trip', () => {
const input = '07';
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
assert.strictEqual(input, NumericString.parse(input)?.[0].toString());
});
});

0 comments on commit cd58b1e

Please sign in to comment.