diff --git a/ROADMAP.md b/ROADMAP.md index 61a24264bd0..e71fd385a30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -54,8 +54,8 @@ Status | Command | Description :white_check_mark: |:1234: F{char} | to the Nth occurrence of {char} to the left :white_check_mark: |:1234: t{char} | till before the Nth occurrence of {char} to the right :white_check_mark: |:1234: T{char} | till before the Nth occurrence of {char} to the left - |:1234: ; | repeat the last "f", "F", "t", or "T" N times - |:1234: , | repeat the last "f", "F", "t", or "T" N times in opposite direction +:white_check_mark: |:1234: ; | repeat the last "f", "F", "t", or "T" N times +:white_check_mark: |:1234: , | repeat the last "f", "F", "t", or "T" N times in opposite direction ## Up-down motions diff --git a/src/actions/actions.ts b/src/actions/actions.ts index e63d0c24473..32c49421d3d 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -156,6 +156,13 @@ export abstract class BaseMovement extends BaseAction { canBePrefixedWithCount = false; + /** + * Whether we should change lastRepeatableMovement in VimState. + */ + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return false; + } + /** * Whether we should change desiredColumn in VimState. */ @@ -1992,6 +1999,10 @@ class MoveFindForward extends BaseMovement { return result; } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } } @RegisterAction @@ -2009,6 +2020,10 @@ class MoveFindBackward extends BaseMovement { return result; } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } } @@ -2031,6 +2046,10 @@ class MoveTilForward extends BaseMovement { return result; } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } } @RegisterAction @@ -2048,6 +2067,59 @@ class MoveTilBackward extends BaseMovement { return result; } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } +} + +@RegisterAction +class MoveRepeat extends BaseMovement { + keys = [";"]; + + public async execActionWithCount(position: Position, vimState: VimState, count: number): Promise { + const movement = VimState.lastRepeatableMovement; + if (movement) { + const result = await movement.execActionWithCount(position, vimState, count); + /** + * For t and T commands vim executes ; as 2; + * This way the cursor will get to the next instance of + */ + if (result instanceof Position && position.isEqual(result) && count <= 1) { + return await movement.execActionWithCount(position, vimState, 2); + } + return result; + } + return position; + } +} + + +@RegisterAction +class MoveRepeatReversed extends BaseMovement { + keys = [","]; + static reverseMotionMapping : Map BaseMovement> = new Map([ + [MoveFindForward, () => new MoveFindBackward()], + [MoveFindBackward, () => new MoveFindForward()], + [MoveTilForward, () => new MoveTilBackward()], + [MoveTilBackward, () => new MoveTilForward()] + ]); + + public async execActionWithCount(position: Position, vimState: VimState, count: number): Promise { + const movement = VimState.lastRepeatableMovement; + if (movement) { + const reverse = MoveRepeatReversed.reverseMotionMapping.get(movement.constructor)(); + reverse.keysPressed = [reverse.keys[0], movement.keysPressed[1]]; + + let result = await reverse.execActionWithCount(position, vimState, count); + // For t and T commands vim executes ; as 2; + if (result instanceof Position && position.isEqual(result) && count <= 1) { + result = await reverse.execActionWithCount(position, vimState, 2); + } + return result; + } + return position; + } } @RegisterAction diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index 1e00fa9d971..60e0d104e64 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -54,6 +54,8 @@ export class VimState { public historyTracker: HistoryTracker; + public static lastRepeatableMovement : BaseMovement | undefined = undefined; + /** * The keystroke sequence that made up our last complete action (that can be * repeated with '.'). @@ -850,6 +852,10 @@ export class ModeHandler implements vscode.Disposable { } } + if (movement.canBeRepeatedWithSemicolon(vimState, result)) { + VimState.lastRepeatableMovement = movement; + } + vimState.recordedState.count = 0; let stop = vimState.cursorPosition; diff --git a/test/index.ts b/test/index.ts index 94753f3d397..0a53dcb3423 100644 --- a/test/index.ts +++ b/test/index.ts @@ -23,7 +23,7 @@ Globals.isTesting = true; testRunner.configure({ ui: 'tdd', useColors: true, - timeout: 2500, + timeout: 4000, }); module.exports = testRunner; diff --git a/test/register/register.test.ts b/test/register/register.test.ts index 3e1ada972ec..6f67906566d 100644 --- a/test/register/register.test.ts +++ b/test/register/register.test.ts @@ -44,7 +44,7 @@ suite("register", () => { newTest({ title: "Can use two registers together", start: ['|one', "two"], - keysPressed: '"*yyjyy"*pp', + keysPressed: '"ayyj"byy"ap"bp', end: ["one", "two", "one", "|two"], }); diff --git a/test/register/repeatableMovement.test.ts b/test/register/repeatableMovement.test.ts new file mode 100644 index 00000000000..0ac8a07eba3 --- /dev/null +++ b/test/register/repeatableMovement.test.ts @@ -0,0 +1,49 @@ +"use strict"; + +import { ModeHandler } from "../../src/mode/modeHandler"; +import { setupWorkspace, cleanUpWorkspace, assertEqualLines } from '../testUtils'; +import { getTestingFunctions } from '../testSimplifier'; + +suite("register", () => { + let modeHandler: ModeHandler = new ModeHandler(); + + let { + newTest, + newTestOnly, + } = getTestingFunctions(modeHandler); + + setup(async () => { + await setupWorkspace(); + }); + + suiteTeardown(cleanUpWorkspace); + + newTest({ + title: "Can repeat f", + start: ['|abc abc abc'], + keysPressed: 'fa;', + end: ['abc abc |abc'], + }); + + newTest({ + title: "Can repeat reversed F", + start: ['|abc abc abc'], + keysPressed: 'fa$,', + end: ['abc abc |abc'], + }); + + newTest({ + title: "Can repeat t", + start: ['|abc abc abc'], + keysPressed: 'tc;', + end: ['abc a|bc abc'], + }); + + newTest({ + title: "Can repeat N times reversed t", + start: ['|abc abc abc abc'], + keysPressed: 'tc$3,', + end: ['abc| abc abc abc'], + }); + +}); \ No newline at end of file