diff --git a/src/Actions/Register.ts b/src/Actions/Register.ts index f0d6ba5e..e1218b73 100644 --- a/src/Actions/Register.ts +++ b/src/Actions/Register.ts @@ -266,7 +266,7 @@ export class ActionRegister { } else if (stash.lineCount > 1) { return ActionMoveCursor.byMotions({motions: [ - MotionDirection.previous({n: textToPut.length - args.n!}), + MotionDirection.prev({n: textToPut.length - args.n!}), ]}); } else { diff --git a/src/Dispatcher.ts b/src/Dispatcher.ts index eb8a8874..c04b355a 100644 --- a/src/Dispatcher.ts +++ b/src/Dispatcher.ts @@ -76,10 +76,10 @@ export class Dispatcher { } private switchMode(id: ModeID): void { - const previousMode = this._currentMode; + const lastMode = this._currentMode; - if (previousMode) { - previousMode.exit(); + if (lastMode) { + lastMode.exit(); } this._currentMode = this.modes[id]; @@ -88,8 +88,8 @@ export class Dispatcher { commands.executeCommand('setContext', 'amVim.mode', this._currentMode.name); // For use in repeat command - if (previousMode) { - this._currentMode.onDidRecordFinish(previousMode.recordedCommandMaps, previousMode.id); + if (lastMode) { + this._currentMode.onDidRecordFinish(lastMode.recordedCommandMaps, lastMode.id); } } diff --git a/src/Mappers/SpecialKeys/Motion.ts b/src/Mappers/SpecialKeys/Motion.ts index 9f92d2a0..6b5f6946 100644 --- a/src/Mappers/SpecialKeys/Motion.ts +++ b/src/Mappers/SpecialKeys/Motion.ts @@ -9,6 +9,7 @@ import {MotionWord} from '../../Motions/Word'; import {MotionMatch} from '../../Motions/Match'; import {MotionMatchPair} from '../../Motions/MatchPair'; import {MotionLine} from '../../Motions/Line'; +import {MotionParagraph} from '../../Motions/Paragraph'; import {MotionDocument} from '../../Motions/Document'; interface MotionGenerator { @@ -53,18 +54,21 @@ export class SpecialKeyMotion extends GenericMapper implements SpecialKeyCommon { keys: '0', motionGenerators: [MotionLine.start] }, { keys: '$', motionGenerators: [MotionLine.end] }, - { keys: '-', motionGenerators: [MotionCharacter.up, MotionLine.firstNonBlank] }, - { keys: '+', motionGenerators: [MotionCharacter.down, MotionLine.firstNonBlank] }, + { keys: '-', motionGenerators: [MotionCharacter.up, MotionLine.firstNonBlank] }, + { keys: '+', motionGenerators: [MotionCharacter.down, MotionLine.firstNonBlank] }, { keys: '_', motionGenerators: [ (args: {n?: number}) => MotionCharacter.down({ n: (args.n === undefined ? 0 : args.n - 1) }), MotionLine.firstNonBlank ] }, + { keys: '{', motionGenerators: [MotionParagraph.prev] }, + { keys: '}', motionGenerators: [MotionParagraph.next] }, + { keys: 'g g', motionGenerators: [MotionDocument.toLineOrFirst, MotionLine.firstNonBlank] }, { keys: 'G', motionGenerators: [MotionDocument.toLineOrLast, MotionLine.firstNonBlank] }, { keys: 'space', motionGenerators: [MotionDirection.next] }, - { keys: 'backspace', motionGenerators: [MotionDirection.previous] }, + { keys: 'backspace', motionGenerators: [MotionDirection.prev] }, ]; constructor() { diff --git a/src/Modes/Mode.ts b/src/Modes/Mode.ts index 83c904a4..5692011a 100644 --- a/src/Modes/Mode.ts +++ b/src/Modes/Mode.ts @@ -109,7 +109,7 @@ export abstract class Mode { /** * Override this to do something after recording ends. */ - onDidRecordFinish(recordedCommandMaps: CommandMap[], previousModeID: ModeID): void {} + onDidRecordFinish(recordedCommandMaps: CommandMap[], lastModeID: ModeID): void {} protected execute(): void { if (this.executing) { diff --git a/src/Modes/Normal.ts b/src/Modes/Normal.ts index f39734cc..5dbe93cf 100644 --- a/src/Modes/Normal.ts +++ b/src/Modes/Normal.ts @@ -269,12 +269,12 @@ export class ModeNormal extends Mode { return Promise.resolve(true); } - onDidRecordFinish(recordedCommandMaps: CommandMap[], previousModeID: ModeID): void { + onDidRecordFinish(recordedCommandMaps: CommandMap[], lastModeID: ModeID): void { if (! recordedCommandMaps || recordedCommandMaps.length === 0) { return; } - if (previousModeID === ModeID.INSERT) { + if (lastModeID === ModeID.INSERT) { recordedCommandMaps.forEach(map => map.isRepeating = true); if (this._recordedCommandMaps === undefined) { diff --git a/src/Motions/Direction.ts b/src/Motions/Direction.ts index bc6e4be3..6a79bc17 100644 --- a/src/Motions/Direction.ts +++ b/src/Motions/Direction.ts @@ -1,7 +1,7 @@ import {window, Position} from 'vscode'; import {Motion} from './Motion'; -enum Direction {Previous, Next} +enum Direction {Prev, Next} export class MotionDirection extends Motion { @@ -17,17 +17,17 @@ export class MotionDirection extends Motion { this.n = args.n; } - static previous(args: {n?: number} = {}): Motion { + static prev(args: {n?: number} = {}): Motion { return new MotionDirection({ - direction: Direction.Previous, - n: args.n + direction: Direction.Prev, + n: args.n, }); } static next(args: {n?: number} = {}): Motion { return new MotionDirection({ direction: Direction.Next, - n: args.n + n: args.n, }); } @@ -36,7 +36,7 @@ export class MotionDirection extends Motion { const activeTextEditor = window.activeTextEditor; - if (! activeTextEditor || this.direction === undefined) { + if (! activeTextEditor || this.direction === undefined || this.n === undefined) { return from; } @@ -50,7 +50,7 @@ export class MotionDirection extends Motion { return _lengthByLine[line]; }; - const offset = this.direction === Direction.Previous ? -1 : +1; + const offset = this.direction === Direction.Prev ? -1 : +1; let toLine = from.line; let toCharacter = from.character; diff --git a/src/Motions/Paragraph.ts b/src/Motions/Paragraph.ts new file mode 100644 index 00000000..500a2db1 --- /dev/null +++ b/src/Motions/Paragraph.ts @@ -0,0 +1,128 @@ +import {window, TextDocument, Position} from 'vscode'; +import {Motion} from './Motion'; + +enum Direction {Prev, Next} + +export class MotionParagraph extends Motion { + + private direction: Direction; + private n: number; + + constructor(args: {direction: Direction, n?: number}) { + args.n = args.n === undefined ? 1 : args.n; + + super(); + + this.direction = args.direction; + this.n = args.n; + } + + static prev(args: {n?: number}): Motion { + return new MotionParagraph({ + direction: Direction.Prev, + n: args.n, + }); + } + + static next(args: {n?: number}): Motion { + return new MotionParagraph({ + direction: Direction.Next, + n: args.n, + }); + } + + apply(from: Position): Position { + from = super.apply(from); + + const activeTextEditor = window.activeTextEditor; + + if (! activeTextEditor || this.direction === undefined || this.n === undefined) { + return from; + } + + const document = activeTextEditor.document; + + for (let i = 0; i < this.n; i++) { + const result = this.applyOnce(document, from); + + from = result.to; + + if (result.shouldStop) { + break; + } + } + + return from; + } + + private applyOnce( + document: TextDocument, + from: Position, + ): { + to: Position, + shouldStop: boolean, + } { + let toLine: number | undefined = undefined; + let toCharacter = 0; + let shouldStop = false; + + // Skip first group of empty lines if currently on empty line. + let shouldSkip = MotionParagraph.isLineEmpty(document, from.line); + + if (this.direction === Direction.Prev) { + for (let i = from.line - 1; i >= 0; i--) { + const isLineEmpty = MotionParagraph.isLineEmpty(document, i); + + if (shouldSkip) { + if (!isLineEmpty) { + shouldSkip = false; + } + continue; + } + + if (isLineEmpty) { + toLine = i; + break; + } + } + + if (toLine === undefined) { + shouldStop = true; + toLine = 0; + } + } + else { + for (let i = from.line + 1; i < document.lineCount; i++) { + const isLineEmpty = MotionParagraph.isLineEmpty(document, i); + + if (shouldSkip) { + if (!isLineEmpty) { + shouldSkip = false; + } + continue; + } + + if (isLineEmpty) { + toLine = i; + break; + } + } + + if (toLine === undefined) { + shouldStop = true; + toLine = document.lineCount - 1; + toCharacter = document.lineAt(toLine).text.length; + } + } + + return { + to: new Position(toLine, toCharacter), + shouldStop: shouldStop, + }; + } + + private static isLineEmpty(document: TextDocument, line: number): boolean { + return document.lineAt(line).text === ''; + } + +} diff --git a/src/Motions/Word.ts b/src/Motions/Word.ts index f1d2d10e..0d372e7a 100644 --- a/src/Motions/Word.ts +++ b/src/Motions/Word.ts @@ -2,7 +2,7 @@ import {window, TextDocument, Position} from 'vscode'; import {Motion} from './Motion'; import {WordCharacterKind, UtilWord} from '../Utils/Word'; -enum MotionWordDirection {Previous, Next} +enum MotionWordDirection {Prev, Next} enum MotionWordMatchKind {Start, End, Both} export class MotionWord extends Motion { @@ -47,7 +47,7 @@ export class MotionWord extends Motion { useBlankSeparatedStyle?: boolean, } = {}): Motion { const obj = new MotionWord(args); - obj.direction = MotionWordDirection.Previous; + obj.direction = MotionWordDirection.Prev; obj.matchKind = MotionWordMatchKind.Start; return obj; } @@ -57,7 +57,7 @@ export class MotionWord extends Motion { useBlankSeparatedStyle?: boolean, } = {}): Motion { const obj = new MotionWord(args); - obj.direction = MotionWordDirection.Previous; + obj.direction = MotionWordDirection.Prev; obj.matchKind = MotionWordMatchKind.End; return obj; } @@ -112,7 +112,7 @@ export class MotionWord extends Motion { return from; } - applyOnce( + private applyOnce( document: TextDocument, from: Position, matchKind: MotionWordMatchKind, @@ -125,8 +125,8 @@ export class MotionWord extends Motion { shouldStop: boolean, } { let line = from.line; - let previousPosition: Position | undefined; - let previousCharacterKind: WordCharacterKind | undefined; + let lastPosition: Position | undefined; + let lastCharacterKind: WordCharacterKind | undefined; if (this.direction === MotionWordDirection.Next) { while (line < document.lineCount) { @@ -137,15 +137,15 @@ export class MotionWord extends Motion { const currentCharacterKind = UtilWord.getCharacterKind( text.charCodeAt(character), this.useBlankSeparatedStyle); - if (previousPosition !== undefined && previousCharacterKind !== currentCharacterKind) { + if (lastPosition !== undefined && lastCharacterKind !== currentCharacterKind) { let startPosition: Position | undefined; let endPosition: Position | undefined; if (currentCharacterKind !== WordCharacterKind.Blank) { startPosition = new Position(line, character); } - if (previousCharacterKind !== WordCharacterKind.Blank) { - endPosition = previousPosition; + if (lastCharacterKind !== WordCharacterKind.Blank) { + endPosition = lastPosition; if (endPosition.isEqual(from)) { endPosition = undefined; } @@ -188,8 +188,8 @@ export class MotionWord extends Motion { } } - previousPosition = new Position(line, character); - previousCharacterKind = currentCharacterKind; + lastPosition = new Position(line, character); + lastCharacterKind = currentCharacterKind; character++; } @@ -209,7 +209,7 @@ export class MotionWord extends Motion { shouldStop: true, }; } - else if (this.direction === MotionWordDirection.Previous) { + else if (this.direction === MotionWordDirection.Prev) { while (line >= 0) { const text = document.lineAt(line).text + '\n'; let character = line === from.line ? from.character : text.length - 1; @@ -218,12 +218,12 @@ export class MotionWord extends Motion { const currentCharacterKind = UtilWord.getCharacterKind( text.charCodeAt(character), this.useBlankSeparatedStyle); - if (previousPosition !== undefined && previousCharacterKind !== currentCharacterKind) { + if (lastPosition !== undefined && lastCharacterKind !== currentCharacterKind) { let startPosition: Position | undefined; let endPosition: Position | undefined; - if (previousCharacterKind !== WordCharacterKind.Blank) { - startPosition = previousPosition; + if (lastCharacterKind !== WordCharacterKind.Blank) { + startPosition = lastPosition; if (startPosition.isEqual(from)) { startPosition = undefined; } @@ -264,8 +264,8 @@ export class MotionWord extends Motion { } } - previousPosition = new Position(line, character); - previousCharacterKind = currentCharacterKind; + lastPosition = new Position(line, character); + lastCharacterKind = currentCharacterKind; character--; } diff --git a/test/ModeNormal/{.test.ts b/test/ModeNormal/{.test.ts new file mode 100644 index 00000000..dc903572 --- /dev/null +++ b/test/ModeNormal/{.test.ts @@ -0,0 +1,60 @@ +import * as BlackBox from '../Framework/BlackBox'; + +suite('Normal: {', () => { + const testCases: BlackBox.TestCase[] = [ + { + from: '[]\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '{', + to: '[]\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n[]\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '{', + to: '[]\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '{', + to: '[]\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n\n[]Foo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '{', + to: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\n[]Bar end\n\n\nDoo end\n\n\n', + inputs: '{', + to: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + inputs: '{', + to: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n[]\nDoo end\n\n\n', + inputs: '{', + to: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\n[]Doo end\n\n\n', + inputs: '{', + to: '\n\n\nFoo end\nBar end\n\n[]\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\nDoo end\n[]\n\n', + inputs: '{', + to: '\n\n\nFoo end\nBar end\n\n[]\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\nDoo end\n\n[]\n', + inputs: '{', + to: '\n\n\nFoo end\nBar end\n\n[]\nDoo end\n\n\n', + }, + ]; + + for (let i = 0; i < testCases.length; i++) { + BlackBox.run(testCases[i]); + } +}); diff --git a/test/ModeNormal/}.test.ts b/test/ModeNormal/}.test.ts new file mode 100644 index 00000000..d26d45ad --- /dev/null +++ b/test/ModeNormal/}.test.ts @@ -0,0 +1,65 @@ +import * as BlackBox from '../Framework/BlackBox'; + +suite('Normal: }', () => { + const testCases: BlackBox.TestCase[] = [ + { + from: '[]\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + }, + { + from: '\n[]\n\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + }, + { + from: '\n\n[]\nFoo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + }, + { + from: '\n\n\n[]Foo end\nBar end\n\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\n[]Bar end\n\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n[]\n\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n\n\nDoo end\n[]\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n[]\nDoo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n\n\nDoo end\n[]\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\n[]Doo end\n\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n\n\nDoo end\n[]\n\n', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\nDoo end\n[]\n\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n[]', + }, + { + from: '\n\n\nFoo end\nBar end\n\n\nDoo end\n\n[]\n', + inputs: '}', + to: '\n\n\nFoo end\nBar end\n\n\nDoo end\n\n\n[]', + }, + { + from: 'Foo end\n[]\nBar end\nDoo end', + inputs: '}', + to: 'Foo end\n\nBar end\nDoo en[]d', + }, + ]; + + for (let i = 0; i < testCases.length; i++) { + BlackBox.run(testCases[i]); + } +});