diff --git a/.vscode/launch.json b/.vscode/launch.json index 213356475a9..20594863246 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ // "runtimeArgs": ["--harmony-default-parameters", "--harmony-rest-parameters"], "stopOnEntry": false, "sourceMaps": true, - "outDir": "out/src", + "outDir": "${workspaceRoot}/out/src", "preLaunchTask": "npm" }, { @@ -23,7 +23,7 @@ // "runtimeArgs": ["--js-flags=\"--harmony --harmony-default-parameters\""], "stopOnEntry": false, "sourceMaps": true, - "outDir": "out/test", + "outDir": "${workspaceRoot}/out/test", "preLaunchTask": "npm" } ] diff --git a/README.md b/README.md index bab826ddab6..7a83aba33b2 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Status | Key | Description :white_check_mark: | w | words forward :white_check_mark: | W | N blank-separated WORDS forward :white_check_mark: | e | forward to the end of the word - | E | forward to the end of the Nth blank-separated WORD +:white_check_mark: | E | forward to the end of the Nth blank-separated WORD :white_check_mark: | b | words backward :white_check_mark: | B | N blank-separated WORDS backward | ge | backward to the end of the Nth word diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index 440fffe37a9..4217aa84a66 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -26,6 +26,7 @@ export class NormalMode extends Mode { "w" : async (c) => { return c.wordRight().move(); }, "W" : async (c) => { return c.bigWordRight().move(); }, "e" : async (c) => { return c.goToEndOfCurrentWord().move(); }, + "E" : async (c) => { return c.goToEndOfCurrentBigWord().move(); }, "b" : async (c) => { return c.wordLeft().move(); }, "B" : async (c) => { return c.bigWordLeft().move(); }, "}" : async (c) => { return c.goToEndOfCurrentParagraph().move(); }, diff --git a/src/motion/motion.ts b/src/motion/motion.ts index f1baf01d34c..836844d4b0d 100644 --- a/src/motion/motion.ts +++ b/src/motion/motion.ts @@ -236,6 +236,12 @@ export class Motion implements vscode.Disposable { return this; } + public goToEndOfCurrentBigWord(): Motion { + this._position = this.position.getCurrentBigWordEnd(); + this._desiredColumn = this._position.character; + return this; + } + public goToEndOfCurrentParagraph(): Motion { this._position = this.position.getCurrentParagraphEnd(); this._desiredColumn = this.position.character; diff --git a/src/motion/position.ts b/src/motion/position.ts index 88fcb56998b..a3655147cc1 100644 --- a/src/motion/position.ts +++ b/src/motion/position.ts @@ -12,8 +12,6 @@ export enum PositionOptions { export class Position extends vscode.Position { private static NonWordCharacters = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"; private static NonBigWordCharacters = ""; - private static WordDelimiters: string[] = ["(", ")", "[", "]", "{", "}", ":", " ", - "=", "<", ">", "|", "/", "'", "\"", "~", "`", "@", "*", "+", "-", "?", ",", ".", ";"]; private _nonWordCharRegex : RegExp; private _nonBigWordCharRegex : RegExp; @@ -95,25 +93,11 @@ export class Position extends vscode.Position { } public getCurrentWordEnd(): Position { - if (!TextEditor.isLastLine(this) && this.character === this.getLineEnd().character) { - // go to next line - let line = TextEditor.getLineAt(this.translate(1)); - return new Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex, this.positionOptions); - } - - let line = TextEditor.getLineAt(this); - - if (Position.WordDelimiters.indexOf(line.text.charAt(this.character)) !== -1) { - return new Position(this.line, this.character + 1, this.positionOptions); - } - - for (var index = this.character; index < line.text.length; index++) { - if (Position.WordDelimiters.indexOf(line.text.charAt(index)) !== -1) { - return new Position(this.line, index, this.positionOptions); - } - } + return this.getCurrentWordEndWithRegex(this._nonWordCharRegex); + } - return this.getLineEnd(); + public getCurrentBigWordEnd(): Position { + return this.getCurrentWordEndWithRegex(this._nonBigWordCharRegex); } /** @@ -233,82 +217,85 @@ export class Position extends vscode.Position { private makeWordRegex(characterSet: string) : RegExp { let escaped = characterSet && _.escapeRegExp(characterSet); - let segments = ["(^[\t ]*$)"]; + let segments = []; segments.push(`([^\\s${escaped}]+)`); segments.push(`[${escaped}]+`); - return new RegExp(segments.join("|"), "g"); - } + segments.push(`$^`); + let result = new RegExp(segments.join("|"), "g"); - private getWordLeftWithRegex(regex: RegExp) : Position { - var workingPosition = new Position(this.line, this.character, this.positionOptions); - var currentLine = TextEditor.getLineAt(this); - var currentCharacter = this.character; - - if (!TextEditor.isFirstLine(this) && this.character <= currentLine.firstNonWhitespaceCharacterIndex) { - // perform search from very end of previous line (after last character) - workingPosition = new Position(this.line - 1, this.character, this.positionOptions); - currentLine = TextEditor.getLineAt(workingPosition); - currentCharacter = workingPosition.getLineEnd().character + 1; - } + return result; + } - let positions = []; + private getAllPositions(line: string, regex: RegExp): number[] { + let positions: number[] = []; + let result = regex.exec(line); - regex.lastIndex = 0; - while (true) { - let result = regex.exec(currentLine.text); - if (result === null) { - break; - } + while (result) { positions.push(result.index); + + // Handles the case where an empty string match causes lastIndex not to advance, + // which gets us in an infinite loop. + if (result.index === regex.lastIndex) { regex.lastIndex++; } + result = regex.exec(line); } - for (var index = 0; index < positions.length; index++) { - let position = positions[positions.length - 1 - index]; - if (currentCharacter > position) { - return new Position(workingPosition.line, position, workingPosition.positionOptions); + return positions; + } + + private getAllEndPositions(line: string, regex: RegExp): number[] { + let positions: number[] = []; + let result = regex.exec(line); + + while (result) { + if (result[0].length) { + positions.push(result.index + result[0].length - 1); } - } - if (this.line === 0) { - return this.getLineBegin(); - } else { - let prevLine = new Position(this.line - 1, 0, this.positionOptions); - return prevLine.getLineEnd(); + // Handles the case where an empty string match causes lastIndex not to advance, + // which gets us in an infinite loop. + if (result.index === regex.lastIndex) { regex.lastIndex++; } + result = regex.exec(line); } + + return positions; } - private getWordRightWithRegex(regex: RegExp) : Position { - if (!TextEditor.isLastLine(this) && this.character >= this.getLineEnd().character) { - // go to next line - let line = TextEditor.getLineAt(this.translate(1)); - return new Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex, this.positionOptions); + private getWordLeftWithRegex(regex: RegExp) : Position { + for (let currentLine = this.line; currentLine >= 0; currentLine--) { + let positions = this.getAllPositions(TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, regex); + let newCharacter = _.find(positions.reverse(), index => index < this.character || currentLine !== this.line); + + if (newCharacter !== undefined) { + return new Position(currentLine, newCharacter, this.positionOptions); + } } - let currentLine = TextEditor.getLineAt(this); - let positions = []; + return new Position(0, 0, this.positionOptions).getLineBegin(); + } + + private getWordRightWithRegex(regex: RegExp): Position { + for (let currentLine = this.line; currentLine < TextEditor.getLineCount(); currentLine++) { + let positions = this.getAllPositions(TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, regex); + let newCharacter = _.find(positions, index => index > this.character || currentLine !== this.line); - regex.lastIndex = 0; - while (true) { - let result = regex.exec(currentLine.text); - if (result === null) { - break; + if (newCharacter !== undefined) { + return new Position(currentLine, newCharacter, this.positionOptions); } - positions.push(result.index); } - for (var index = 0; index < positions.length; index++) { - let position = positions[index]; - if (this.character < position) { - return new Position(this.line, position, this.positionOptions); + return new Position(TextEditor.getLineCount() - 1, 0, this.positionOptions).getLineEnd(); + } + + private getCurrentWordEndWithRegex(regex: RegExp) : Position { + for (let currentLine = this.line; currentLine < TextEditor.getLineCount(); currentLine++) { + let positions = this.getAllEndPositions(TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, regex); + let newCharacter = _.find(positions, index => index > this.character || currentLine !== this.line); + + if (newCharacter !== undefined) { + return new Position(currentLine, newCharacter, this.positionOptions); } } - if (this.line === this.getDocumentEnd().line) { - return this.getLineEnd(); - } else { - // go to next line - let line = TextEditor.getLineAt(this.translate(1)); - return new Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex, this.positionOptions); - } + return new Position(TextEditor.getLineCount() - 1, 0, this.positionOptions).getLineEnd(); } } \ No newline at end of file diff --git a/test/motion.test.ts b/test/motion.test.ts index 94f84b0975e..833b1ff87ec 100644 --- a/test/motion.test.ts +++ b/test/motion.test.ts @@ -212,7 +212,9 @@ suite("word motion", () => { "if (true) {", " return true;", "} else {", + "", " return false;", + " ", "} // endif" ]; @@ -237,9 +239,21 @@ suite("word motion", () => { assert.equal(motion.position.character, 2); }); + test("last word should move to next line stops on empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(2, 7).wordRight(); + assert.equal(motion.position.line, 3); + assert.equal(motion.position.character, 0); + }); + + test("last word should move to next line skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 14).wordRight(); + assert.equal(motion.position.line, 6); + assert.equal(motion.position.character, 0); + }); + test("last word on last line should go to end of document (special case!)", () => { - let motion = new Motion(MotionMode.Caret).moveTo(4, 6).wordRight(); - assert.equal(motion.position.line, 4); + let motion = new Motion(MotionMode.Caret).moveTo(6, 6).wordRight(); + assert.equal(motion.position.line, 6); assert.equal(motion.position.character, 9); }); @@ -264,6 +278,17 @@ suite("word motion", () => { assert.equal(motion.position.character, 10); }); + test("first word should move to previous line, stops on empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 2).wordLeft(); + assert.equal(motion.position.line, 3); + assert.equal(motion.position.character, 0); + }); + + test("first word should move to previous line, skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(6, 0).wordLeft(); + assert.equal(motion.position.line, 4); + assert.equal(motion.position.character, 14); + }); }); suite("WORD right", () => { @@ -278,6 +303,18 @@ suite("word motion", () => { assert.equal(motion.position.line, 2); assert.equal(motion.position.character, 0); }); + + test("last WORD should move to next line stops on empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(2, 7).bigWordRight(); + assert.equal(motion.position.line, 3); + assert.equal(motion.position.character, 0); + }); + + test("last WORD should move to next line skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 12).bigWordRight(); + assert.equal(motion.position.line, 6); + assert.equal(motion.position.character, 0); + }); }); suite("WORD left", () => { @@ -298,18 +335,93 @@ suite("word motion", () => { assert.equal(motion.position.line, 1); assert.equal(motion.position.character, 9); }); + + test("first WORD should move to previous line, stops on empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 2).bigWordLeft(); + assert.equal(motion.position.line, 3); + assert.equal(motion.position.character, 0); + }); + + test("first WORD should move to previous line, skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(6, 0).bigWordLeft(); + assert.equal(motion.position.line, 4); + assert.equal(motion.position.character, 9); + }); + }); + + suite("end of word right", () => { + test("move to end of current word right", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 4).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 0); + assert.equal(motion.position.character, 7); + }); + + test("move to end of next word right", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 7).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 0); + assert.equal(motion.position.character, 8); + }); + + test("end of last word should move to next line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 10).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 1); + assert.equal(motion.position.character, 7); + }); + + test("end of last word should move to next line skips empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(2, 7).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 4); + assert.equal(motion.position.character, 7); + }); + + test("end of last word should move to next line skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 14).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 6); + assert.equal(motion.position.character, 0); + }); }); + suite("end of WORD right", () => { + test("move to end of current WORD right", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 4).goToEndOfCurrentBigWord(); + assert.equal(motion.position.line, 0); + assert.equal(motion.position.character, 8); + }); + + test("move to end of next WORD right", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 8).goToEndOfCurrentBigWord(); + assert.equal(motion.position.line, 0); + assert.equal(motion.position.character, 10); + }); + + test("end of last WORD should move to next line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(0, 10).goToEndOfCurrentBigWord(); + assert.equal(motion.position.line, 1); + assert.equal(motion.position.character, 7); + }); + + test("end of last WORD should move to next line skips empty line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(2, 7).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 4); + assert.equal(motion.position.character, 7); + }); + + test("end of last WORD should move to next line skips whitespace only line", () => { + let motion = new Motion(MotionMode.Caret).moveTo(4, 14).goToEndOfCurrentWord(); + assert.equal(motion.position.line, 6); + assert.equal(motion.position.character, 0); + }); + }); test("line begin cursor on first non-blank character", () => { - let motion = new Motion(MotionMode.Caret).moveTo(3, 3).firstLineNonBlankChar(); + let motion = new Motion(MotionMode.Caret).moveTo(4, 3).firstLineNonBlankChar(); assert.equal(motion.position.line, 0); assert.equal(motion.position.character, 0); }); test("last line begin cursor on first non-blank character", () => { let motion = new Motion(MotionMode.Caret).moveTo(0, 0).lastLineNonBlankChar(); - assert.equal(motion.position.line, 4); + assert.equal(motion.position.line, 6); assert.equal(motion.position.character, 0); }); });