From f7a27f3292985cc796612a30fbd7dc108dffd33c Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Tue, 1 Dec 2015 11:57:38 -0800 Subject: [PATCH 01/11] Add cursor and textEditor tests. Fixed a couple of bugs --- src/cursor.ts | 50 ++++++----- src/mode/modeInsert.ts | 2 +- src/mode/modeNormal.ts | 16 ++-- src/textEditor.ts | 13 +-- test/cursor.test.ts | 186 ++++++++++++++++++++++++++++++++++++++++ test/index.ts | 4 +- test/textEditor.test.ts | 69 +++++++++++++++ 7 files changed, 304 insertions(+), 36 deletions(-) create mode 100644 test/cursor.test.ts create mode 100644 test/textEditor.test.ts diff --git a/src/cursor.ts b/src/cursor.ts index 9d0f513d26e0..79b3c580e23e 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -4,8 +4,14 @@ import TextEditor from "./textEditor"; export default class Cursor { private static prevColumn: number = 0; - static move(position: vscode.Position) { - const newSelection = new vscode.Selection(position, position); + static move(newPosition: vscode.Position) { + let curPosition = this.currentPosition(); + + if (newPosition.line === curPosition.line) { + this.prevColumn = newPosition.character; + } + + const newSelection = new vscode.Selection(newPosition, newPosition); vscode.window.activeTextEditor.selection = newSelection; } @@ -20,8 +26,7 @@ export default class Cursor { if (column > 0) { column--; } - - this.prevColumn = column; + return new vscode.Position(pos.line, column); } @@ -29,11 +34,10 @@ export default class Cursor { let pos = this.currentPosition(); let column = pos.character; - if (column < TextEditor.ReadLine(pos.line).length - 1) { + if (column < TextEditor.readLine(pos.line).length - 1) { column++; } - - this.prevColumn = column; + return new vscode.Position(pos.line, column); } @@ -43,7 +47,7 @@ export default class Cursor { let column = this.prevColumn; if (!Cursor.isLastLine(line)) { - let nextLineMaxColumn = TextEditor.ReadLine(++line).length - 1; + let nextLineMaxColumn = TextEditor.readLine(++line).length - 1; if (nextLineMaxColumn < 0) { nextLineMaxColumn = 0; @@ -63,7 +67,7 @@ export default class Cursor { let column = this.prevColumn; if (line !== 0) { - let nextLineMaxColumn = TextEditor.ReadLine(--line).length - 1; + let nextLineMaxColumn = TextEditor.readLine(--line).length - 1; if (nextLineMaxColumn < 0) { nextLineMaxColumn = 0; @@ -84,23 +88,27 @@ export default class Cursor { static lineEnd() : vscode.Position { let pos = this.currentPosition(); - const lineLength = TextEditor.ReadLine(pos.line).length; + const lineLength = TextEditor.readLine(pos.line).length; return new vscode.Position(pos.line, lineLength); - } - - private static isLastLine(line: number): boolean { - return (vscode.window.activeTextEditor.document.lineCount - 1) === line; } - static checkLineEnd() : void { - let pos = this.currentPosition(); - const lineLength = TextEditor.ReadLine(pos.line).length; - if (pos.character === 0 || lineLength === 0) { - return; - } else if (pos.character >= lineLength) { - this.move(pos.translate(0, -1)); + static documentBegin() : vscode.Position { + return new vscode.Position(0, 0); + } + + static documentEnd() : vscode.Position { + let line = vscode.window.activeTextEditor.document.lineCount - 1; + if (line < 0) { + line = 0; } + + let column = TextEditor.readLine(line).length; + return new vscode.Position(line, column); + } + + private static isLastLine(line: number): boolean { + return (vscode.window.activeTextEditor.document.lineCount - 1) === line; } } diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index 5343e9a36c78..8e3a3bb315b2 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -44,7 +44,7 @@ export default class InsertMode extends Mode { HandleKeyEvent(key : string) : void { this.keyHistory.push(key); - TextEditor.Insert(this.ResolveKeyValue(key)); + TextEditor.insert(this.ResolveKeyValue(key)); vscode.commands.executeCommand("editor.action.triggerSuggest"); } diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index 26de802bf835..883031e127bb 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -29,7 +29,7 @@ export default class CommandMode extends Mode { "dw" : () => { vscode.commands.executeCommand("deleteWordRight"); }, "db" : () => { vscode.commands.executeCommand("deleteWordLeft"); }, "esc": () => { vscode.commands.executeCommand("workbench.action.closeMessages"); }, - "x" : () => { this.CommandDelete(1); } + "x" : () => { this.CommandDelete(1); } }; } @@ -64,11 +64,15 @@ export default class CommandMode extends Mode { } private CommandDelete(n: number) : void { - var pos : vscode.Position = Cursor.currentPosition(); - var end : vscode.Position = pos.translate(0, n); - var range : vscode.Range = new vscode.Range(pos, end); - TextEditor.Delete(range).then(function() { - Cursor.checkLineEnd(); + let pos = Cursor.currentPosition(); + let end = pos.translate(0, n); + let range : vscode.Range = new vscode.Range(pos, end); + TextEditor.delete(range).then(function() { + let lineEnd = Cursor.lineEnd(); + + if (pos.character === lineEnd.character) { + Cursor.move(Cursor.left()); + } }); } } \ No newline at end of file diff --git a/src/textEditor.ts b/src/textEditor.ts index 2fe64b64c7c8..ce6754dd8d99 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; export default class TextEditor { - static Insert(text: string, position: vscode.Position = null) : Thenable { + static insert(text: string, position: vscode.Position = null) : Thenable { if (position === null) { position = vscode.window.activeTextEditor.selection.active; } @@ -11,27 +11,28 @@ export default class TextEditor { }); } - static Delete(range: vscode.Range) : Thenable { + static delete(range: vscode.Range) : Thenable { return vscode.window.activeTextEditor.edit((editBuilder) => { editBuilder.delete(range); }); } + - static Replace(range: vscode.Range, text: string) : Thenable { + static replace(range: vscode.Range, text: string) : Thenable { return vscode.window.activeTextEditor.edit((editBuilder) => { editBuilder.replace(range, text); }); } - static ReadLine(lineNo: number = null): string { + static readLine(lineNo: number = null): string { if (lineNo === null) { lineNo = vscode.window.activeTextEditor.selection.active.line; } - if (vscode.window.activeTextEditor.document.lineCount < lineNo) { + if (lineNo >= vscode.window.activeTextEditor.document.lineCount) { throw new RangeError(); } - + return vscode.window.activeTextEditor.document.lineAt(lineNo).text; } } diff --git a/test/cursor.test.ts b/test/cursor.test.ts new file mode 100644 index 000000000000..381c420cdbd4 --- /dev/null +++ b/test/cursor.test.ts @@ -0,0 +1,186 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import TextEditor from './../src/textEditor'; +import Cursor from './../src/cursor'; + +suite("cursor", () => { + let text : Array = [ + "mary had", + "a", + "little lamb" + ]; + + setup(done => { + TextEditor.insert(text.join('\n')).then(() => done()); + }); + + teardown(done => { + let range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); + TextEditor.delete(range).then(() => done()); + }); + + test("left should move cursor one column left", () => { + Cursor.move(new vscode.Position(0, 5)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 5); + + let left = Cursor.left(); + assert.equal(left.line, 0); + assert.equal(left.character, 4); + }); + + test("left on left-most column should stay at the same location", () => { + Cursor.move(new vscode.Position(0, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 0); + + let left = Cursor.left(); + assert.equal(left.line, 0); + assert.equal(left.character, 0); + }); + + test("right should move cursor one column right", () => { + Cursor.move(new vscode.Position(0, 5)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 5); + + let right = Cursor.right(); + assert.equal(right.line, 0); + assert.equal(right.character, 6); + }); + + test("right on right-most column should stay at the same location", () => { + Cursor.move(new vscode.Position(0, 7)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 7); + + let right = Cursor.right(); + assert.equal(right.line, 0); + assert.equal(right.character, 7); + }); + + test("down should move cursor one line down", () => { + Cursor.move(new vscode.Position(1, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 1); + assert.equal(current.character, 0); + + let down = Cursor.down(); + console.log(down.character); + assert.equal(down.line, 2); + assert.equal(down.character, 0); + }); + + test("down on bottom-most line should stay at the same location", () => { + Cursor.move(new vscode.Position(2, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 2); + assert.equal(current.character, 0); + + let down = Cursor.down(); + assert.equal(down.line, 2); + assert.equal(down.character, 0); + }); + + test("up should move cursor one line up", () => { + Cursor.move(new vscode.Position(1, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 1); + assert.equal(current.character, 0); + + let up = Cursor.up(); + assert.equal(up.line, 0); + assert.equal(up.character, 0); + }); + + test("up on top-most line should stay at the same location", () => { + Cursor.move(new vscode.Position(0, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 0); + + let up = Cursor.up(); + assert.equal(up.line, 0); + assert.equal(up.character, 0); + }); + + test("keep same column as up/down", () => { + Cursor.move(new vscode.Position(0, 0)); + Cursor.move(Cursor.right()); + Cursor.move(Cursor.right()); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 2); + + Cursor.move(Cursor.down()); + + current = Cursor.currentPosition(); + assert.equal(current.line, 1); + assert.equal(current.character, 0); + + Cursor.move(Cursor.down()); + + current = Cursor.currentPosition(); + assert.equal(current.line, 2); + assert.equal(current.character, 2); + }); + + test("get line begin cursor", () => { + Cursor.move(new vscode.Position(0, 0)); + + let pos = Cursor.lineBegin(); + + assert.equal(pos.line, 0); + assert.equal(pos.character, 0); + + Cursor.move(Cursor.down()); + + pos = Cursor.lineBegin(); + + assert.equal(pos.line, 1); + assert.equal(pos.character, 0); + }); + + test("get line end cursor", () => { + Cursor.move(new vscode.Position(0, 0)); + + let pos = Cursor.lineEnd(); + + assert.equal(pos.line, 0); + assert.equal(pos.character, text[0].length); + + Cursor.move(Cursor.down()); + + pos = Cursor.lineEnd(); + + assert.equal(pos.line, 1); + assert.equal(pos.character, text[1].length); + }); + + test("get document begin cursor", () => { + var cursor = Cursor.documentBegin(); + + assert.equal(cursor.line, 0); + assert.equal(cursor.character, 0); + }); + + test("get document end cursor", () => { + var cursor = Cursor.documentEnd(); + + assert.equal(cursor.line, 2); + assert.equal(cursor.character, text[2].length); + }); +}); diff --git a/test/index.ts b/test/index.ts index 5e827bb072bd..f94c09903cf0 100644 --- a/test/index.ts +++ b/test/index.ts @@ -15,8 +15,8 @@ var testRunner = require('vscode/lib/testrunner'); // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true // colored output from test results + ui: 'tdd', + useColors: true }); module.exports = testRunner; diff --git a/test/textEditor.test.ts b/test/textEditor.test.ts new file mode 100644 index 000000000000..8ef254129989 --- /dev/null +++ b/test/textEditor.test.ts @@ -0,0 +1,69 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import TextEditor from './../src/textEditor'; +import Cursor from './../src/cursor'; + +suite("text editor", () => { + suiteSetup(done => { + let range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); + TextEditor.delete(range).then(done()); + }); + + suiteTeardown(done => { + var range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); + TextEditor.delete(range).then(done()); + }); + + test("insert 'Hello World'", done => { + let expectedText = "Hello World"; + + TextEditor.insert(expectedText).then(x => { + assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); + let actualText = TextEditor.readLine(0); + assert.equal(actualText, expectedText); + }).then(done, done); + }); + + test("replace 'World' with 'Foo Bar'", done => { + let newText = "Foo Bar"; + let start = new vscode.Position(0, 6); + let end = new vscode.Position(0, 11); + let range : vscode.Range = new vscode.Range(start, end); + + TextEditor.replace(range, newText).then( x => { + assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); + + let actualText = TextEditor.readLine(0); + assert.equal(actualText, "Hello Foo Bar"); + }).then(done, done); + }); + + test("delete `Hello`", done => { + assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); + + var end = new vscode.Position(0, 5); + var range = new vscode.Range(Cursor.documentBegin(), end); + + TextEditor.delete(range).then( x => { + let actualText = TextEditor.readLine(0); + assert.equal(actualText, " Foo Bar"); + }).then(done, done); + }); + + test("delete the whole line", done => { + assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); + + var range = vscode.window.activeTextEditor.document.lineAt(0).range; + + TextEditor.delete(range).then( x => { + let actualText = TextEditor.readLine(0); + assert.equal(actualText, ""); + }).then(done, done); + }); + + test("try to read lines that don't exist", () => { + assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); + assert.throws(() => TextEditor.readLine(1), RangeError); + assert.throws(() => TextEditor.readLine(2), RangeError); + }); +}); From 573d0fa09dadbcc23e92a41c9f25044371580cb3 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Tue, 1 Dec 2015 12:14:03 -0800 Subject: [PATCH 02/11] Fixes #73 and #24. Making the personal token public in order to properly build PRs. --- .travis.yml | 3 +++ appveyor.yml | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 97ec1a205c67..98851977c764 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ language: node_js node_js: - "4.1.1" +env: + - TSD_GITHUB_TOKEN=8742b29e67faa29aa25564ff7317b50fd7c1327d + install: - npm install before_script: diff --git a/appveyor.yml b/appveyor.yml index a33a1ce3bc09..172b15daeaa8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,7 @@ environment: node_js_version: "4.1.1" - TSD_GITHUB_TOKEN: "4c7f278997af83ba584aaa9c5722d5ecbbcb1dd9" - -clone_depth: 1 + TSD_GITHUB_TOKEN: "8742b29e67faa29aa25564ff7317b50fd7c1327d" + clone_depth: 1 install: - ps: start-filedownload https://az764295.vo.msecnd.net/public/0.10.1-release/VSCode-win32.zip From 8b3f668fc84514c4888942b1ddab117ad3c64e11 Mon Sep 17 00:00:00 2001 From: Adriaan Putter Date: Tue, 1 Dec 2015 23:25:22 +0200 Subject: [PATCH 03/11] implement correct w,b motions --- src/cursor.ts | 118 ++++++++++++++++++++++++++++---- src/mode/modeNormal.ts | 4 +- src/textEditor.ts | 4 ++ test/cursor.test.ts | 148 +++++++++++++++++++++++++++++------------ 4 files changed, 215 insertions(+), 59 deletions(-) diff --git a/src/cursor.ts b/src/cursor.ts index 79b3c580e23e..e2152f1db2f0 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -1,3 +1,4 @@ +import * as _ from "lodash"; import * as vscode from "vscode"; import TextEditor from "./textEditor"; @@ -5,6 +6,9 @@ export default class Cursor { private static prevColumn: number = 0; static move(newPosition: vscode.Position) { + if (newPosition === null) { + return; + } let curPosition = this.currentPosition(); if (newPosition.line === curPosition.line) { @@ -17,12 +21,12 @@ export default class Cursor { static currentPosition(): vscode.Position { return vscode.window.activeTextEditor.selection.active; - } - + } + static left() : vscode.Position { let pos = this.currentPosition(); let column = pos.character; - + if (column > 0) { column--; } @@ -40,7 +44,7 @@ export default class Cursor { return new vscode.Position(pos.line, column); } - + static down() : vscode.Position { let pos = this.currentPosition(); let line = pos.line; @@ -72,20 +76,47 @@ export default class Cursor { if (nextLineMaxColumn < 0) { nextLineMaxColumn = 0; } - + if (nextLineMaxColumn < this.prevColumn) { column = nextLineMaxColumn; - } - } + } + } return new vscode.Position(line, column); } - + + static wordRight() : vscode.Position { + let pos = this.currentPosition(); + if (pos.character === this.lineEnd().character) { + if (this.isLastLine(pos.line)) { + return null; + } + let line = TextEditor.getLineAt(pos.translate(1)); + return new vscode.Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex); + } + let nextPos = this.getNextWordPosition(); + if (nextPos === null) { + return this.lineEnd(); + } + return nextPos; + } + + static wordLeft(): vscode.Position { + let pos = this.currentPosition(); + let currentLine = TextEditor.getLineAt(pos); + if (pos.character <= currentLine.firstNonWhitespaceCharacterIndex && pos.line !== 0) { + let line = TextEditor.getLineAt(pos.translate(-1)); + return new vscode.Position(line.lineNumber, line.range.end.character); + } + let nextPos = this.getPreviousWordPosition(); + return nextPos; + } + static lineBegin() : vscode.Position { - let pos = this.currentPosition(); + let pos = this.currentPosition(); return new vscode.Position(pos.line, 0); - } - + } + static lineEnd() : vscode.Position { let pos = this.currentPosition(); const lineLength = TextEditor.readLine(pos.line).length; @@ -108,7 +139,68 @@ export default class Cursor { } private static isLastLine(line: number): boolean { - return (vscode.window.activeTextEditor.document.lineCount - 1) === line; + return (vscode.window.activeTextEditor.document.lineCount) === line + 1; } -} + private static _nonWordCharacters = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"; + + private static getNextWordPosition(): vscode.Position { + let segments = ["(^[\t ]*$)"]; + segments.push(`([^\\s${_.escapeRegExp(this._nonWordCharacters) }]+)`); + segments.push(`[\\s${_.escapeRegExp(this._nonWordCharacters) }]+`); + let reg = new RegExp(segments.join("|"), "g"); + let pos = this.currentPosition(); + let line = TextEditor.getLineAt(pos); + let words = line.text.match(reg); + + let startWord: number; + let endWord: number; + + if (words) { + for (var index = 0; index < words.length; index++) { + var word = words[index].trim(); + if (word.length > 0) { + startWord = line.text.indexOf(word, endWord); + endWord = startWord + word.length; + + if (pos.character < startWord) { + return new vscode.Position(pos.line, startWord); + } + } + } + } + + return null; + } + + private static getPreviousWordPosition(): vscode.Position { + let segments = ["(^[\t ]*$)"]; + segments.push(`([^\\s${_.escapeRegExp(this._nonWordCharacters) }]+)`); + segments.push(`[\\s${_.escapeRegExp(this._nonWordCharacters) }]+`); + let reg = new RegExp(segments.join("|"), "g"); + let pos = this.currentPosition(); + let line = TextEditor.getLineAt(pos); + let words = line.text.match(reg); + + let startWord: number; + let endWord: number; + + if (words) { + words = words.reverse(); + endWord = line.range.end.character; + for (var index = 0; index < words.length; index++) { + endWord = endWord - words[index].length; + var word = words[index].trim(); + if (word.length > 0) { + startWord = line.text.indexOf(word, endWord); + + if (startWord !== -1 && pos.character > startWord) { + return new vscode.Position(pos.line, startWord); + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index 883031e127bb..d10085c2815a 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -21,8 +21,8 @@ export default class CommandMode extends Mode { "l" : () => { Cursor.move(Cursor.right()); }, "$" : () => { Cursor.move(Cursor.lineEnd()); }, "^" : () => { Cursor.move(Cursor.lineBegin()); }, - "w" : () => { vscode.commands.executeCommand("cursorWordRight"); }, - "b" : () => { vscode.commands.executeCommand("cursorWordLeft"); }, + "w" : () => { Cursor.move(Cursor.wordRight()); }, + "b" : () => { Cursor.move(Cursor.wordLeft()); }, ">>" : () => { vscode.commands.executeCommand("editor.action.indentLines"); }, "<<" : () => { vscode.commands.executeCommand("editor.action.outdentLines"); }, "dd" : () => { vscode.commands.executeCommand("editor.action.deleteLines"); }, diff --git a/src/textEditor.ts b/src/textEditor.ts index ce6754dd8d99..d455f8ed750f 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -35,5 +35,9 @@ export default class TextEditor { return vscode.window.activeTextEditor.document.lineAt(lineNo).text; } + + static getLineAt(position: vscode.Position): vscode.TextLine { + return vscode.window.activeTextEditor.document.lineAt(position); + } } diff --git a/test/cursor.test.ts b/test/cursor.test.ts index 381c420cdbd4..9bd559990386 100644 --- a/test/cursor.test.ts +++ b/test/cursor.test.ts @@ -4,7 +4,7 @@ import TextEditor from './../src/textEditor'; import Cursor from './../src/cursor'; suite("cursor", () => { - let text : Array = [ + let text: Array = [ "mary had", "a", "little lamb" @@ -14,173 +14,233 @@ suite("cursor", () => { TextEditor.insert(text.join('\n')).then(() => done()); }); - teardown(done => { + teardown(done => { let range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); TextEditor.delete(range).then(() => done()); }); - + test("left should move cursor one column left", () => { Cursor.move(new vscode.Position(0, 5)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 5); - + let left = Cursor.left(); assert.equal(left.line, 0); assert.equal(left.character, 4); }); - + test("left on left-most column should stay at the same location", () => { Cursor.move(new vscode.Position(0, 0)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 0); - + let left = Cursor.left(); assert.equal(left.line, 0); assert.equal(left.character, 0); }); - + test("right should move cursor one column right", () => { Cursor.move(new vscode.Position(0, 5)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 5); - + let right = Cursor.right(); assert.equal(right.line, 0); assert.equal(right.character, 6); }); - + test("right on right-most column should stay at the same location", () => { Cursor.move(new vscode.Position(0, 7)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 7); - + let right = Cursor.right(); assert.equal(right.line, 0); assert.equal(right.character, 7); }); - + test("down should move cursor one line down", () => { Cursor.move(new vscode.Position(1, 0)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 1); assert.equal(current.character, 0); - + let down = Cursor.down(); console.log(down.character); assert.equal(down.line, 2); assert.equal(down.character, 0); }); - + test("down on bottom-most line should stay at the same location", () => { Cursor.move(new vscode.Position(2, 0)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 2); assert.equal(current.character, 0); - + let down = Cursor.down(); assert.equal(down.line, 2); assert.equal(down.character, 0); }); - + test("up should move cursor one line up", () => { Cursor.move(new vscode.Position(1, 0)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 1); assert.equal(current.character, 0); - + let up = Cursor.up(); assert.equal(up.line, 0); assert.equal(up.character, 0); }); - + test("up on top-most line should stay at the same location", () => { Cursor.move(new vscode.Position(0, 0)); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 0); - + let up = Cursor.up(); assert.equal(up.line, 0); assert.equal(up.character, 0); }); - + test("keep same column as up/down", () => { Cursor.move(new vscode.Position(0, 0)); Cursor.move(Cursor.right()); Cursor.move(Cursor.right()); - + let current = Cursor.currentPosition(); assert.equal(current.line, 0); assert.equal(current.character, 2); - + Cursor.move(Cursor.down()); current = Cursor.currentPosition(); assert.equal(current.line, 1); assert.equal(current.character, 0); - + Cursor.move(Cursor.down()); current = Cursor.currentPosition(); assert.equal(current.line, 2); assert.equal(current.character, 2); }); - + test("get line begin cursor", () => { Cursor.move(new vscode.Position(0, 0)); - + let pos = Cursor.lineBegin(); - + assert.equal(pos.line, 0); assert.equal(pos.character, 0); - + Cursor.move(Cursor.down()); - + pos = Cursor.lineBegin(); - + assert.equal(pos.line, 1); assert.equal(pos.character, 0); }); - + test("get line end cursor", () => { Cursor.move(new vscode.Position(0, 0)); - + let pos = Cursor.lineEnd(); - + assert.equal(pos.line, 0); assert.equal(pos.character, text[0].length); - + Cursor.move(Cursor.down()); - + pos = Cursor.lineEnd(); - + assert.equal(pos.line, 1); assert.equal(pos.character, text[1].length); }); - + test("get document begin cursor", () => { var cursor = Cursor.documentBegin(); assert.equal(cursor.line, 0); assert.equal(cursor.character, 0); }); - + test("get document end cursor", () => { var cursor = Cursor.documentEnd(); assert.equal(cursor.line, 2); assert.equal(cursor.character, text[2].length); }); + + test("wordRight should move cursor word right", () => { + Cursor.move(new vscode.Position(0, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 0); + + var wordRight = Cursor.wordRight(); + assert.equal(wordRight.line, 0); + assert.equal(wordRight.character, 5); + }); + + test("wordLeft should move cursor word left", () => { + Cursor.move(new vscode.Position(0, 3)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 3); + + var wordLeft = Cursor.wordLeft(); + assert.equal(wordLeft.line, 0); + assert.equal(wordLeft.character, 0); + }); + + test("wordRight on last word should stay on line at last character", () => { + Cursor.move(new vscode.Position(0, 6)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 6); + + var pos = Cursor.wordRight(); + assert.equal(pos.line, 0); + assert.equal(pos.character, 8); + }); + + test("wordRight on end of line should move to next word on next line", () => { + Cursor.move(new vscode.Position(0, 8)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 8); + + var pos = Cursor.wordRight(); + assert.equal(pos.line, 1); + assert.equal(pos.character, 0); + }); + + test("wordLeft on first word should move to previous line of end of line", () => { + Cursor.move(new vscode.Position(2, 0)); + + let current = Cursor.currentPosition(); + assert.equal(current.line, 2); + assert.equal(current.character, 0); + + var pos = Cursor.wordLeft(); + assert.equal(pos.line, 1); + assert.equal(pos.character, 1); + }); }); From db66ce44dcbcef0c02de733db10bc87f0a2c329a Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Tue, 1 Dec 2015 17:38:29 -0800 Subject: [PATCH 04/11] refactor: use helper functions to make it easier what we are checking for --- src/cursor.ts | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/cursor.ts b/src/cursor.ts index e2152f1db2f0..a8b95128a445 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -27,18 +27,18 @@ export default class Cursor { let pos = this.currentPosition(); let column = pos.character; - if (column > 0) { + if (!this.isLineBeginning(pos)) { column--; } - + return new vscode.Position(pos.line, column); } static right() : vscode.Position { let pos = this.currentPosition(); let column = pos.character; - - if (column < TextEditor.readLine(pos.line).length - 1) { + + if (!this.isLineEnd(pos)) { column++; } @@ -50,7 +50,7 @@ export default class Cursor { let line = pos.line; let column = this.prevColumn; - if (!Cursor.isLastLine(line)) { + if (!Cursor.isLastLine(pos)) { let nextLineMaxColumn = TextEditor.readLine(++line).length - 1; if (nextLineMaxColumn < 0) { @@ -70,7 +70,7 @@ export default class Cursor { let line = pos.line; let column = this.prevColumn; - if (line !== 0) { + if (!this.isFirstLine(pos)) { let nextLineMaxColumn = TextEditor.readLine(--line).length - 1; if (nextLineMaxColumn < 0) { @@ -88,7 +88,7 @@ export default class Cursor { static wordRight() : vscode.Position { let pos = this.currentPosition(); if (pos.character === this.lineEnd().character) { - if (this.isLastLine(pos.line)) { + if (this.isLastLine(pos)) { return null; } let line = TextEditor.getLineAt(pos.translate(1)); @@ -137,9 +137,30 @@ export default class Cursor { let column = TextEditor.readLine(line).length; return new vscode.Position(line, column); } + + private static isLineBeginning(position : vscode.Position) : boolean { + return position.character === 0; + } + + private static isLineEnd(position : vscode.Position) : boolean { + let lineEnd = TextEditor.readLine(position.line).length - 1; + if (lineEnd < 0) { + lineEnd = 0; + } + + if (position.character > lineEnd) { + throw new RangeError; + } + + return position.character === lineEnd; + } - private static isLastLine(line: number): boolean { - return (vscode.window.activeTextEditor.document.lineCount) === line + 1; + private static isFirstLine(position : vscode.Position) : boolean { + return position.line === 0; + } + + private static isLastLine(position : vscode.Position): boolean { + return position.line === (vscode.window.activeTextEditor.document.lineCount - 1); } private static _nonWordCharacters = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"; From 592ce43a5a448b49d10ea5aabf9de86191a73f60 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Tue, 1 Dec 2015 17:53:31 -0800 Subject: [PATCH 05/11] gulp: add trim-whitespace task --- gulpfile.js | 25 ++++++++++++------- package.json | 1 + src/cmd_line/commands/quit.ts | 8 +++---- src/cmd_line/commands/write.ts | 10 ++++---- src/cmd_line/main.ts | 10 ++++---- src/cmd_line/node.ts | 10 ++++---- src/cmd_line/subparser.ts | 4 ++-- src/cursor.ts | 32 ++++++++++++------------- src/error.ts | 16 ++++++------- src/mode/mode.ts | 2 +- src/mode/modeHandler.ts | 10 ++++---- src/mode/modeInsert.ts | 36 ++++++++++++++-------------- src/mode/modeNormal.ts | 6 ++--- src/mode/modeVisual.ts | 4 ++-- src/textEditor.ts | 12 +++++----- test/cmd_line/lexer.test.ts | 2 +- test/cmd_line/subparser.quit.test.ts | 12 +++++----- test/cmd_line/subparser.test.ts | 6 ++--- test/cursor.test.ts | 6 ++--- test/error.test.ts | 4 ++-- test/extension.test.ts | 8 +++---- test/index.ts | 6 ++--- test/mode/modeHandler.test.ts | 6 ++--- test/mode/modeNormal.test.ts | 4 ++-- test/textEditor.test.ts | 12 +++++----- tslint.json | 4 ++-- 26 files changed, 133 insertions(+), 123 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 42cb321744c6..abf7d9477daf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,8 +1,9 @@ -var gulp = require('gulp'); -var tslint = require('gulp-tslint'); -var tsd = require('gulp-tsd'); -var shell = require('gulp-shell'); -var mocha = require('gulp-mocha'); +var gulp = require('gulp'), + tslint = require('gulp-tslint'), + tsd = require('gulp-tsd'), + shell = require('gulp-shell'), + mocha = require('gulp-mocha'), + trimlines = require('gulp-trimlines'); var paths = { scripts_ts: "src/**/*.ts", @@ -21,11 +22,19 @@ gulp.task('tsd', function (callback) { }, callback)); }); -gulp.task('compile', shell.task([ +gulp.task('trim-whitespace', function() { + return gulp.src([paths.scripts_ts, paths.tests_ts], { base: "./" }) + .pipe(trimlines({ + leading: false + })) + .pipe(gulp.dest('./')); +}); + +gulp.task('compile', ['trim-whitespace'], shell.task([ 'node ./node_modules/vscode/bin/compile -p ./', ])); -gulp.task('tslint', function() { +gulp.task('tslint', ['trim-whitespace'], function() { return gulp.src([paths.scripts_ts, paths.tests_ts]) .pipe(tslint()) .pipe(tslint.report('prose', { @@ -44,4 +53,4 @@ gulp.task('test', ['compile'], function () { }); gulp.task('init', ['tsd']); -gulp.task('default', ['tslint', 'test']); +gulp.task('default', ['trim-whitespace', 'tslint', 'test']); diff --git a/package.json b/package.json index 99a2dfc0c309..0919a94bbb9f 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "gulp-shell": "^0.5.1", "gulp-tsd": "0.0.4", "gulp-tslint": "^3.6.0", + "gulp-trimlines": "^1.0.0", "gulp-typescript": "^2.9.2", "tsd": "^0.6.5", "typescript": "^1.6.2", diff --git a/src/cmd_line/commands/quit.ts b/src/cmd_line/commands/quit.ts index 1b562a79864d..e959eca1bd22 100644 --- a/src/cmd_line/commands/quit.ts +++ b/src/cmd_line/commands/quit.ts @@ -21,14 +21,14 @@ export class QuitCommand extends node.CommandBase { this._shortName = 'q'; this._arguments = args; } - + get arguments() : QuitCommandArguments { return this._arguments; } - + execute() : void { this.quit(); - } + } private quit() { // See https://github.com/Microsoft/vscode/issues/723 @@ -36,7 +36,7 @@ export class QuitCommand extends node.CommandBase { && !this.arguments.bang) { throw error.VimError.fromCode(error.ErrorCode.E37); } - + vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }; } diff --git a/src/cmd_line/commands/write.ts b/src/cmd_line/commands/write.ts index 97bbe4f682fe..26f101ad5d19 100644 --- a/src/cmd_line/commands/write.ts +++ b/src/cmd_line/commands/write.ts @@ -28,7 +28,7 @@ export class WriteCommand extends node.CommandBase { this._shortName = 'w'; this._arguments = args; } - + get arguments() : WriteCommandArguments { return this._arguments; } @@ -47,10 +47,10 @@ export class WriteCommand extends node.CommandBase { util.showError("Not implemented."); return; } - + if (this.activeTextEditor.document.isUntitled) { throw error.VimError.fromCode(error.ErrorCode.E32); - } + } fs.access(this.activeTextEditor.document.fileName, fs.W_OK, (accessErr) => { if (accessErr) { @@ -71,7 +71,7 @@ export class WriteCommand extends node.CommandBase { }); } - private save() { + private save() { this.activeTextEditor.document.save().then( (ok) => { if (ok) { @@ -82,5 +82,5 @@ export class WriteCommand extends node.CommandBase { }, (e) => util.showError(e) ); - } + } } diff --git a/src/cmd_line/main.ts b/src/cmd_line/main.ts index b56a7bb84759..13969869cd22 100644 --- a/src/cmd_line/main.ts +++ b/src/cmd_line/main.ts @@ -8,7 +8,7 @@ export function showCmdLine(initialText = "") { util.showInfo("No active document."); return; } - + const options : vscode.InputBoxOptions = { prompt: "Vim command line", value: initialText @@ -23,14 +23,14 @@ function runCmdLine(s : string) : void { if (!(s && s.trim())) { return; } - + try { var cmd = parser.parse(s); } catch (e) { util.showError(e); return; } - + if (cmd.isEmpty) { return; } @@ -44,7 +44,7 @@ function runCmdLine(s : string) : void { } catch (ee) { // ignore } - - util.showError(e); + + util.showError(e); } } diff --git a/src/cmd_line/node.ts b/src/cmd_line/node.ts index 4993036bd7e7..e2ed083d8276 100644 --- a/src/cmd_line/node.ts +++ b/src/cmd_line/node.ts @@ -101,16 +101,16 @@ export interface CommandArgs { } export abstract class CommandBase { - + protected get activeTextEditor() { return vscode.window.activeTextEditor; } - + get name() : string { return this._name; } protected _name : string; - + get shortName() : string { return this._shortName; } @@ -119,7 +119,7 @@ export abstract class CommandBase { get arguments() : CommandArgs { return this._arguments; } - protected _arguments : CommandArgs; - + protected _arguments : CommandArgs; + abstract execute() : void; } diff --git a/src/cmd_line/subparser.ts b/src/cmd_line/subparser.ts index 717f8263959f..a49310e150a4 100644 --- a/src/cmd_line/subparser.ts +++ b/src/cmd_line/subparser.ts @@ -3,10 +3,10 @@ import {parseWriteCommandArgs} from './subparsers/write'; // TODO: add type for this dict. // maps command names to parsers for said commands. -export const commandParsers = { +export const commandParsers = { 'w': parseWriteCommandArgs, 'write': parseWriteCommandArgs, - + 'quit': parseQuitCommandArgs, 'q': parseQuitCommandArgs }; diff --git a/src/cursor.ts b/src/cursor.ts index a8b95128a445..f8222fc42652 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -1,4 +1,4 @@ -import * as _ from "lodash"; +import * as _ from "lodash"; import * as vscode from "vscode"; import TextEditor from "./textEditor"; @@ -10,7 +10,7 @@ export default class Cursor { return; } let curPosition = this.currentPosition(); - + if (newPosition.line === curPosition.line) { this.prevColumn = newPosition.character; } @@ -31,7 +31,7 @@ export default class Cursor { column--; } - return new vscode.Position(pos.line, column); + return new vscode.Position(pos.line, column); } static right() : vscode.Position { @@ -52,7 +52,7 @@ export default class Cursor { if (!Cursor.isLastLine(pos)) { let nextLineMaxColumn = TextEditor.readLine(++line).length - 1; - + if (nextLineMaxColumn < 0) { nextLineMaxColumn = 0; } @@ -70,7 +70,7 @@ export default class Cursor { let line = pos.line; let column = this.prevColumn; - if (!this.isFirstLine(pos)) { + if (!this.isFirstLine(pos)) { let nextLineMaxColumn = TextEditor.readLine(--line).length - 1; if (nextLineMaxColumn < 0) { @@ -120,45 +120,45 @@ export default class Cursor { static lineEnd() : vscode.Position { let pos = this.currentPosition(); const lineLength = TextEditor.readLine(pos.line).length; - + return new vscode.Position(pos.line, lineLength); } - + static documentBegin() : vscode.Position { return new vscode.Position(0, 0); } - + static documentEnd() : vscode.Position { let line = vscode.window.activeTextEditor.document.lineCount - 1; if (line < 0) { line = 0; } - - let column = TextEditor.readLine(line).length; + + let column = TextEditor.readLine(line).length; return new vscode.Position(line, column); } - + private static isLineBeginning(position : vscode.Position) : boolean { return position.character === 0; - } - + } + private static isLineEnd(position : vscode.Position) : boolean { let lineEnd = TextEditor.readLine(position.line).length - 1; if (lineEnd < 0) { lineEnd = 0; } - + if (position.character > lineEnd) { throw new RangeError; } - + return position.character === lineEnd; } private static isFirstLine(position : vscode.Position) : boolean { return position.line === 0; } - + private static isLastLine(position : vscode.Position): boolean { return position.line === (vscode.window.activeTextEditor.document.lineCount - 1); } diff --git a/src/error.ts b/src/error.ts index 0fbc3241b48b..189378fb7acd 100644 --- a/src/error.ts +++ b/src/error.ts @@ -18,36 +18,36 @@ const errors : VimErrors = { export class VimError extends Error { - + private _code : number; private _message : string; - + constructor(code : number, message : string) { super(); this._code = code; this._message = message; } - + static fromCode(code : ErrorCode) : VimError { if (errors[code]) { return new VimError(code, errors[code]); } - + throw new Error("unknown error code: " + code); } - + get code() : number { return this._code; } - + get message() : string { return this._message; } - + display() : void { util.showError(this.toString()); } - + toString() : string { return "E" + this.code.toString() + ": " + this.message; } diff --git a/src/mode/mode.ts b/src/mode/mode.ts index 565ce3338b42..83d33e75bac8 100644 --- a/src/mode/mode.ts +++ b/src/mode/mode.ts @@ -26,7 +26,7 @@ export abstract class Mode { set IsActive(val : boolean) { this.isActive = val; } - + public HandleDeactivation() : void { this.keyHistory = []; } diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index f06c63521e6b..6fc2b5229b14 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -39,19 +39,19 @@ export default class ModeHandler { handleKeyEvent(key : string) : void { var currentModeName = this.currentMode.Name; - + var nextMode : Mode; var inactiveModes = _.filter(this.modes, (m) => !m.IsActive); - + _.forEach(inactiveModes, (m, i) => { if (m.ShouldBeActivated(key, currentModeName)) { nextMode = m; - } + } }); - + if (nextMode) { this.currentMode.HandleDeactivation(); - + nextMode.HandleActivation(key); this.setCurrentModeByName(nextMode.Name); return; diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index 8e3a3bb315b2..8451c69562d2 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -5,50 +5,50 @@ import Cursor from './../cursor'; export default class InsertMode extends Mode { private activationKeyHandler : { [ key : string] : () => void; } = {}; - + constructor() { super(ModeName.Insert); - + this.activationKeyHandler = { // insert at cursor "i" : () => { Cursor.move(Cursor.currentPosition()); }, - - // insert at the beginning of the line + + // insert at the beginning of the line "I" : () => { Cursor.move(Cursor.lineBegin()); }, - - // append after the cursor + + // append after the cursor "a" : () => { Cursor.move(Cursor.right()); }, - - // append at the end of the line + + // append at the end of the line "A" : () => { Cursor.move(Cursor.lineEnd()); }, - - // open blank line below current line - "o" : () => { + + // open blank line below current line + "o" : () => { vscode.commands.executeCommand("editor.action.insertLineAfter"); }, - - // open blank line above current line - "O" : () => { + + // open blank line above current line + "O" : () => { vscode.commands.executeCommand("editor.action.insertLineBefore"); - } + } }; } ShouldBeActivated(key : string, currentMode : ModeName) : boolean { return key in this.activationKeyHandler; } - + HandleActivation(key : string) : void { this.activationKeyHandler[key](); } - + HandleKeyEvent(key : string) : void { this.keyHistory.push(key); TextEditor.insert(this.ResolveKeyValue(key)); vscode.commands.executeCommand("editor.action.triggerSuggest"); } - + // Some keys have names that are different to their value. // TODO: we probably need to put this somewhere else. private ResolveKeyValue(key : string) : string { diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index d10085c2815a..ea633f73575f 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -48,7 +48,7 @@ export default class CommandMode extends Mode { this.keyHistory.push(key); let keyHandled = false; - + for (let window = this.keyHistory.length; window > 0; window--) { let keysPressed = _.takeRight(this.keyHistory, window).join(''); if (this.keyHandler[keysPressed] !== undefined) { @@ -57,7 +57,7 @@ export default class CommandMode extends Mode { break; } } - + if (keyHandled) { this.keyHistory = []; } @@ -69,7 +69,7 @@ export default class CommandMode extends Mode { let range : vscode.Range = new vscode.Range(pos, end); TextEditor.delete(range).then(function() { let lineEnd = Cursor.lineEnd(); - + if (pos.character === lineEnd.character) { Cursor.move(Cursor.left()); } diff --git a/src/mode/modeVisual.ts b/src/mode/modeVisual.ts index c87a61ce4040..01291e8017a6 100644 --- a/src/mode/modeVisual.ts +++ b/src/mode/modeVisual.ts @@ -9,11 +9,11 @@ export default class VisualMode extends Mode { // TODO: improve this logic for "V". return (key === "v" || key === "V") && (currentMode === ModeName.Normal); } - + HandleActivation(key : string) : void { // do nothing } - + HandleKeyEvent(key : string) : void { this.keyHistory.push(key); } diff --git a/src/textEditor.ts b/src/textEditor.ts index d455f8ed750f..4c1c7b5f8620 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -1,11 +1,11 @@ import * as vscode from "vscode"; -export default class TextEditor { +export default class TextEditor { static insert(text: string, position: vscode.Position = null) : Thenable { if (position === null) { position = vscode.window.activeTextEditor.selection.active; } - + return vscode.window.activeTextEditor.edit((editBuilder) => { editBuilder.insert(position, text); }); @@ -16,26 +16,26 @@ export default class TextEditor { editBuilder.delete(range); }); } - + static replace(range: vscode.Range, text: string) : Thenable { return vscode.window.activeTextEditor.edit((editBuilder) => { editBuilder.replace(range, text); }); } - + static readLine(lineNo: number = null): string { if (lineNo === null) { lineNo = vscode.window.activeTextEditor.selection.active.line; } - + if (lineNo >= vscode.window.activeTextEditor.document.lineCount) { throw new RangeError(); } return vscode.window.activeTextEditor.document.lineAt(lineNo).text; } - + static getLineAt(position: vscode.Position): vscode.TextLine { return vscode.window.activeTextEditor.document.lineAt(position); } diff --git a/test/cmd_line/lexer.test.ts b/test/cmd_line/lexer.test.ts index 83fc19790ea4..863dfcc1296e 100644 --- a/test/cmd_line/lexer.test.ts +++ b/test/cmd_line/lexer.test.ts @@ -85,7 +85,7 @@ suite("command-line lexer", () => { var tokens = lexer.lex("q something"); assert.equal(tokens[0].content, new Token(TokenType.CommandName, "q").content); assert.equal(tokens[1].content, new Token(TokenType.CommandArgs, " something").content); - }); + }); test("can lex long command name and args", () => { var tokens = lexer.lex("write12 something here"); diff --git a/test/cmd_line/subparser.quit.test.ts b/test/cmd_line/subparser.quit.test.ts index 0a9b27ef02da..a7c364513be0 100644 --- a/test/cmd_line/subparser.quit.test.ts +++ b/test/cmd_line/subparser.quit.test.ts @@ -4,9 +4,9 @@ import * as assert from 'assert'; import {commandParsers} from '../../src/cmd_line/subparser'; suite(":quit args parser", () => { - + test("has all aliases", () => { - assert.equal(commandParsers.quit.name, commandParsers.q.name); + assert.equal(commandParsers.quit.name, commandParsers.q.name); }); test("can parse empty args", () => { @@ -14,12 +14,12 @@ suite(":quit args parser", () => { assert.equal(args.arguments.bang, undefined); assert.equal(args.arguments.range, undefined); }); - + test("ignores trailing white space", () => { var args = commandParsers.quit(" "); assert.equal(args.arguments.bang, undefined); assert.equal(args.arguments.range, undefined); - }); + }); test("can parse !", () => { var args = commandParsers.quit("!"); @@ -37,8 +37,8 @@ suite(":quit args parser", () => { assert.equal(args.arguments.bang, true); assert.equal(args.arguments.range, undefined); }); - + test("throws if bad input", () => { assert.throws(() => commandParsers.quit("x")); - }); + }); }); diff --git a/test/cmd_line/subparser.test.ts b/test/cmd_line/subparser.test.ts index 88814efaf1c9..672f4b8790ac 100644 --- a/test/cmd_line/subparser.test.ts +++ b/test/cmd_line/subparser.test.ts @@ -4,9 +4,9 @@ import * as assert from 'assert'; import {commandParsers} from '../../src/cmd_line/subparser'; suite(":write args parser", () => { - + test("has all aliases", () => { - assert.equal(commandParsers.write.name, commandParsers.w.name); + assert.equal(commandParsers.write.name, commandParsers.w.name); }); test("can parse empty args", () => { @@ -51,7 +51,7 @@ suite(":write args parser", () => { assert.equal(args.arguments.optValue, undefined); assert.equal(args.arguments.range, undefined); }); - + test("can parse ' !cmd'", () => { var args = commandParsers.write(" !foo"); diff --git a/test/cursor.test.ts b/test/cursor.test.ts index 9bd559990386..0ba7c0871476 100644 --- a/test/cursor.test.ts +++ b/test/cursor.test.ts @@ -207,7 +207,7 @@ suite("cursor", () => { assert.equal(wordLeft.line, 0); assert.equal(wordLeft.character, 0); }); - + test("wordRight on last word should stay on line at last character", () => { Cursor.move(new vscode.Position(0, 6)); @@ -219,7 +219,7 @@ suite("cursor", () => { assert.equal(pos.line, 0); assert.equal(pos.character, 8); }); - + test("wordRight on end of line should move to next word on next line", () => { Cursor.move(new vscode.Position(0, 8)); @@ -231,7 +231,7 @@ suite("cursor", () => { assert.equal(pos.line, 1); assert.equal(pos.character, 0); }); - + test("wordLeft on first word should move to previous line of end of line", () => { Cursor.move(new vscode.Position(2, 0)); diff --git a/test/error.test.ts b/test/error.test.ts index b3cb90e81801..8fb7f805942a 100644 --- a/test/error.test.ts +++ b/test/error.test.ts @@ -30,6 +30,6 @@ suite("vimError", () => { e = VimError.fromCode(ErrorCode.E488); assert.equal(e.code, 488); - assert.equal(e.message, "Trailing characters"); - }); + assert.equal(e.message, "Trailing characters"); + }); }); \ No newline at end of file diff --git a/test/extension.test.ts b/test/extension.test.ts index 8b92343aedf9..90732a208eb3 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -6,7 +6,7 @@ suite("setup", () => { test("all keys have handlers", (done) => { let pkg = require(__dirname + '/../../package.json'); assert.ok(pkg); - + vscode.commands.getCommands() .then(registeredCommands => { let keybindings = pkg.contributes.keybindings; @@ -14,14 +14,14 @@ suite("setup", () => { for (let i = 0; i < keybindings.length; i++) { let keybinding = keybindings[i]; - + var found = registeredCommands.indexOf(keybinding.command) >= -1; assert.ok(found, "Missing handler for key=" + keybinding.key + ". Expected handler=" + keybinding.command); } }) .then(done, done); }); - + test("duplicate keybindings", () => { let pkg = require(__dirname + '/../../package.json'); assert.ok(pkg); @@ -30,7 +30,7 @@ suite("setup", () => { let duplicateKeys = _.filter(keys, function(x, i, array) { return _.includes(array, x, i + 1); }); - + assert.equal(duplicateKeys.length, 0, "Duplicate Keybindings: " + duplicateKeys.join(',')); }); }); diff --git a/test/index.ts b/test/index.ts index f94c09903cf0..38729a564eee 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,9 +1,9 @@ -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING // // This file is providing the test runner to use when running extension tests. // By default the test runner in use is Mocha based. -// +// // You can provide your own test runner if you want to override it by exporting // a function run(testRoot: string, clb: (error:Error) => void) that the extension // host can call to run the tests. The test runner is expected to use console.log diff --git a/test/mode/modeHandler.test.ts b/test/mode/modeHandler.test.ts index 49d205dd13e4..78cacffdb512 100644 --- a/test/mode/modeHandler.test.ts +++ b/test/mode/modeHandler.test.ts @@ -6,11 +6,11 @@ suite("Mode Handler", () => { test("ctor", () => { var modeHandler = new ModeHandler(); - + assert.equal(modeHandler.currentMode.Name, ModeName.Normal); assert.equal(modeHandler.currentMode.IsActive, true); }); - + test("can set current mode", () => { var modeHandler = new ModeHandler(); @@ -21,6 +21,6 @@ suite("Mode Handler", () => { assert.equal(modeHandler.currentMode.Name, ModeName.Insert); modeHandler.setCurrentModeByName(ModeName.Visual); - assert.equal(modeHandler.currentMode.Name, ModeName.Visual); + assert.equal(modeHandler.currentMode.Name, ModeName.Visual); }); }); diff --git a/test/mode/modeNormal.test.ts b/test/mode/modeNormal.test.ts index c1ddda037bec..a2485e6eaafe 100644 --- a/test/mode/modeNormal.test.ts +++ b/test/mode/modeNormal.test.ts @@ -6,9 +6,9 @@ import {ModeName} from '../../src/mode/mode'; suite("Mode Normal", () => { test("can be activated", () => { let activationKeys = ['esc', 'ctrl+[']; - + let modeHandler = new ModeNormal(); - + for (let i = 0; i < activationKeys.length; i++) { let key = activationKeys[i]; assert.equal(modeHandler.ShouldBeActivated(key, ModeName.Insert), true, key); diff --git a/test/textEditor.test.ts b/test/textEditor.test.ts index 8ef254129989..e64b71c43e48 100644 --- a/test/textEditor.test.ts +++ b/test/textEditor.test.ts @@ -13,17 +13,17 @@ suite("text editor", () => { var range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); TextEditor.delete(range).then(done()); }); - + test("insert 'Hello World'", done => { let expectedText = "Hello World"; - + TextEditor.insert(expectedText).then(x => { assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); let actualText = TextEditor.readLine(0); assert.equal(actualText, expectedText); }).then(done, done); }); - + test("replace 'World' with 'Foo Bar'", done => { let newText = "Foo Bar"; let start = new vscode.Position(0, 6); @@ -37,7 +37,7 @@ suite("text editor", () => { assert.equal(actualText, "Hello Foo Bar"); }).then(done, done); }); - + test("delete `Hello`", done => { assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); @@ -49,7 +49,7 @@ suite("text editor", () => { assert.equal(actualText, " Foo Bar"); }).then(done, done); }); - + test("delete the whole line", done => { assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); @@ -60,7 +60,7 @@ suite("text editor", () => { assert.equal(actualText, ""); }).then(done, done); }); - + test("try to read lines that don't exist", () => { assert.equal(vscode.window.activeTextEditor.document.lineCount, 1); assert.throws(() => TextEditor.readLine(1), RangeError); diff --git a/tslint.json b/tslint.json index 62299a05c2eb..faed56a1a36c 100644 --- a/tslint.json +++ b/tslint.json @@ -35,7 +35,7 @@ "no-string-literal": true, "no-switch-case-fall-through": true, "no-trailing-comma": true, - "no-trailing-whitespace": false, + "no-trailing-whitespace": true, "no-unused-expression": true, "no-unused-variable": true, "no-unreachable": true, @@ -60,4 +60,4 @@ "check-type" ] } -} \ No newline at end of file +} From 0c7a002f283739b5ea7a833c31abafc01653a01b Mon Sep 17 00:00:00 2001 From: Benjamin Romano Date: Tue, 1 Dec 2015 20:12:43 -0600 Subject: [PATCH 06/11] Created tests for modeInsert fixed linter errors Fixed compile error due to uppercase in require statement --- src/mode/mode.ts | 2 +- src/mode/modeInsert.ts | 17 +++-- src/textEditor.ts | 4 ++ test/cursor.test.ts | 1 - test/mode/modeInsert.test.ts | 128 +++++++++++++++++++++++++++++++++++ test/testUtils.ts | 22 ++++++ test/textEditor.test.ts | 10 +-- 7 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 test/mode/modeInsert.test.ts create mode 100644 test/testUtils.ts diff --git a/src/mode/mode.ts b/src/mode/mode.ts index 83d33e75bac8..4703975352c3 100644 --- a/src/mode/mode.ts +++ b/src/mode/mode.ts @@ -33,7 +33,7 @@ export abstract class Mode { abstract ShouldBeActivated(key : string, currentMode : ModeName) : boolean; - abstract HandleActivation(key : string) : void; + abstract HandleActivation(key : string) : Thenable | void; abstract HandleKeyEvent(key : string) : void; } \ No newline at end of file diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index 8451c69562d2..b3d99e8a1a45 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -4,7 +4,8 @@ import TextEditor from './../textEditor'; import Cursor from './../cursor'; export default class InsertMode extends Mode { - private activationKeyHandler : { [ key : string] : () => void; } = {}; + + private activationKeyHandler : { [ key : string] : () => Thenable | void; }; constructor() { super(ModeName.Insert); @@ -24,12 +25,12 @@ export default class InsertMode extends Mode { // open blank line below current line "o" : () => { - vscode.commands.executeCommand("editor.action.insertLineAfter"); + return vscode.commands.executeCommand("editor.action.insertLineAfter"); }, // open blank line above current line "O" : () => { - vscode.commands.executeCommand("editor.action.insertLineBefore"); + return vscode.commands.executeCommand("editor.action.insertLineBefore"); } }; } @@ -38,15 +39,17 @@ export default class InsertMode extends Mode { return key in this.activationKeyHandler; } - HandleActivation(key : string) : void { - this.activationKeyHandler[key](); + HandleActivation(key : string) : Thenable | void { + return this.activationKeyHandler[key](); } - HandleKeyEvent(key : string) : void { + HandleKeyEvent(key : string) : Thenable { this.keyHistory.push(key); - TextEditor.insert(this.ResolveKeyValue(key)); + var thenable = TextEditor.insert(this.ResolveKeyValue(key)); vscode.commands.executeCommand("editor.action.triggerSuggest"); + + return thenable; } // Some keys have names that are different to their value. diff --git a/src/textEditor.ts b/src/textEditor.ts index 4c1c7b5f8620..d8dd86d10a0e 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -24,6 +24,10 @@ export default class TextEditor { }); } + static readFile(): string { + return vscode.window.activeTextEditor.document.getText(); + } + static readLine(lineNo: number = null): string { if (lineNo === null) { lineNo = vscode.window.activeTextEditor.selection.active.line; diff --git a/test/cursor.test.ts b/test/cursor.test.ts index 0ba7c0871476..e33c747b77e7 100644 --- a/test/cursor.test.ts +++ b/test/cursor.test.ts @@ -75,7 +75,6 @@ suite("cursor", () => { assert.equal(current.character, 0); let down = Cursor.down(); - console.log(down.character); assert.equal(down.line, 2); assert.equal(down.character, 0); }); diff --git a/test/mode/modeInsert.test.ts b/test/mode/modeInsert.test.ts new file mode 100644 index 000000000000..a96331c729c9 --- /dev/null +++ b/test/mode/modeInsert.test.ts @@ -0,0 +1,128 @@ +import * as assert from 'assert'; + +import ModeInsert from '../../src/mode/modeInsert'; +import {ModeName} from '../../src/mode/mode'; +import Cursor from '../../src/cursor'; +import TextEditor from '../../src/textEditor'; + +import * as testUtils from '../testUtils'; + +import * as vscode from 'vscode'; + +let modeHandler: ModeInsert = null; + +suite("Mode Insert", () => { + setup((done) => { + modeHandler = new ModeInsert(); + + testUtils.clearTextEditor() + .then(done); + }); + + teardown((done) => { + modeHandler = null; + + testUtils.clearTextEditor() + .then(done); + }); + + test("can be activated", () => { + let activationKeys = ['i', 'I', 'o', 'O', 'a', 'A']; + + for (let i = 0; i < activationKeys.length; i++) { + let key = activationKeys[i]; + assert.equal(modeHandler.ShouldBeActivated(key, ModeName.Insert), true, key); + } + }); + + test("can handle key events", (done) => { + let expected = "!"; + + modeHandler.HandleKeyEvent("!") + .then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'o'", (done) => { + let expected = "text\n"; + + TextEditor.insert("text") + .then(() => { + return modeHandler.HandleActivation("o"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'O'", (done) => { + let expected = "\ntext"; + + TextEditor.insert("text") + .then(() => { + return modeHandler.HandleActivation("O"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'i'", (done) => { + let expected = "text!text"; + + TextEditor.insert("texttext") + .then(() => { + Cursor.move(new vscode.Position(0, 4)); + }).then(() => { + return modeHandler.HandleActivation("i"); + }).then(() => { + return modeHandler.HandleKeyEvent("!"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'I'", (done) => { + let expected = "!text"; + + TextEditor.insert("text") + .then(() => { + Cursor.move(new vscode.Position(0, 4)); + }).then(() => { + return modeHandler.HandleActivation("I"); + }).then(() => { + return modeHandler.HandleKeyEvent("!"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'a'", (done) => { + let expected = "textt!ext"; + + TextEditor.insert("texttext") + .then(() => { + Cursor.move(new vscode.Position(0, 4)); + }).then(() => { + return modeHandler.HandleActivation("a"); + }).then(() => { + return modeHandler.HandleKeyEvent("!"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); + + test("Can handle 'A'", (done) => { + let expected = "text!"; + + TextEditor.insert("text") + .then(() => { + Cursor.move(new vscode.Position(0, 0)); + }).then(() => { + return modeHandler.HandleActivation("A"); + }).then(() => { + return modeHandler.HandleKeyEvent("!"); + }).then(() => { + return testUtils.assertTextEditorText(expected); + }).then(done); + }); +}); diff --git a/test/testUtils.ts b/test/testUtils.ts new file mode 100644 index 000000000000..0f946ad28a98 --- /dev/null +++ b/test/testUtils.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; +import TextEditor from '../src/textEditor'; +import Cursor from '../src/cursor'; +import * as assert from 'assert'; + +export function clearTextEditor(): Thenable { + let range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); + return TextEditor.delete(range).then(() => { + return; + }); +} + +export function assertTextEditorText(expected: string, lineNo?: number) { + let actual: string; + if (isNaN(lineNo) || typeof lineNo === 'undefined') { + actual = TextEditor.readFile(); + } else { + actual = TextEditor.readLine(lineNo); + } + + assert.equal(actual, expected); +} diff --git a/test/textEditor.test.ts b/test/textEditor.test.ts index e64b71c43e48..fa3603066ec0 100644 --- a/test/textEditor.test.ts +++ b/test/textEditor.test.ts @@ -3,15 +3,17 @@ import * as vscode from 'vscode'; import TextEditor from './../src/textEditor'; import Cursor from './../src/cursor'; +import * as testUtils from './testUtils'; + suite("text editor", () => { suiteSetup(done => { - let range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); - TextEditor.delete(range).then(done()); + testUtils.clearTextEditor() + .then(done); }); suiteTeardown(done => { - var range = new vscode.Range(Cursor.documentBegin(), Cursor.documentEnd()); - TextEditor.delete(range).then(done()); + testUtils.clearTextEditor() + .then(done); }); test("insert 'Hello World'", done => { From 90879b7e7792ec146b7e9e374c3658c6cd1a5694 Mon Sep 17 00:00:00 2001 From: Shuping LIU Date: Tue, 1 Dec 2015 22:44:46 +0800 Subject: [PATCH 07/11] Add commands support for 'gg': go to first line, on first non-blank character; 'G': go to last line, on first non-blank character --- README.md | 2 +- src/cursor.ts | 17 ++++++++++++++++- src/mode/modeNormal.ts | 4 +++- test/cursor.test.ts | 25 +++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1fc52e654e7a..ec9fe2366a69 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Vim emulation for Visual Studio Code. * Commands: * Command Palette: `:` - * Navigation: `h`, `j`, `k`, `l` + * Navigation: `h`, `j`, `k`, `l`, `w`, `b`, `gg`, `G` * Indentation: `>>`, `<<` * Deletion: `dd`, `dw`, `db` * Editing: `u`, `ctrl+r` diff --git a/src/cursor.ts b/src/cursor.ts index f8222fc42652..3262210d1868 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -124,6 +124,17 @@ export default class Cursor { return new vscode.Position(pos.line, lineLength); } + static firstLineNonBlankChar() : vscode.Position { + let character = Cursor.posOfFirstNonBlankChar(0); + return new vscode.Position(0, character); + } + + static lastLineNonBlankChar() : vscode.Position { + const line = vscode.window.activeTextEditor.document.lineCount - 1; + let character = Cursor.posOfFirstNonBlankChar(line); + return new vscode.Position(line, character); + } + static documentBegin() : vscode.Position { return new vscode.Position(0, 0); } @@ -224,4 +235,8 @@ export default class Cursor { return null; } -} \ No newline at end of file + + private static posOfFirstNonBlankChar(line: number): number { + return TextEditor.readLine(line).match(/^\s*/)[0].length; + } +} diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index ea633f73575f..55a855bed0c5 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -21,6 +21,8 @@ export default class CommandMode extends Mode { "l" : () => { Cursor.move(Cursor.right()); }, "$" : () => { Cursor.move(Cursor.lineEnd()); }, "^" : () => { Cursor.move(Cursor.lineBegin()); }, + "gg" : () => { Cursor.move(Cursor.firstLineNonBlankChar()); }, + "G" : () => { Cursor.move(Cursor.lastLineNonBlankChar()); }, "w" : () => { Cursor.move(Cursor.wordRight()); }, "b" : () => { Cursor.move(Cursor.wordLeft()); }, ">>" : () => { vscode.commands.executeCommand("editor.action.indentLines"); }, @@ -75,4 +77,4 @@ export default class CommandMode extends Mode { } }); } -} \ No newline at end of file +} diff --git a/test/cursor.test.ts b/test/cursor.test.ts index e33c747b77e7..6b7b714c6d62 100644 --- a/test/cursor.test.ts +++ b/test/cursor.test.ts @@ -242,4 +242,29 @@ suite("cursor", () => { assert.equal(pos.line, 1); assert.equal(pos.character, 1); }); + + test("get first line begin cursor on first non-blank character", () => { + let cursor = Cursor.firstLineNonBlankChar(); + + assert.equal(cursor.line, 0); + assert.equal(cursor.character, 0); + + TextEditor.insert(" ", new vscode.Position(0, 0)).then(() => { + assert.equal(cursor.line, 0); + assert.equal(cursor.character, 2); + }); + }); + + test("get last line begin cursor on first non-blank character", () => { + let cursor = Cursor.lastLineNonBlankChar(); + + assert.equal(cursor.line, 2); + assert.equal(cursor.character, 0); + + let line = Cursor.documentEnd().line; + TextEditor.insert(" ", new vscode.Position(line, 0)).then(() => { + assert.equal(cursor.line, 2); + assert.equal(cursor.character, 2); + }); + }); }); From 01c7ef7474644baf523bb50ad1340cb8d750d226 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Wed, 2 Dec 2015 01:56:34 -0800 Subject: [PATCH 08/11] readme: update more detailed contributing info --- README.md | 54 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1fc52e654e7a..bc3d54ef14e3 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ -[![Build Status](https://travis-ci.org/VSCodeVim/Vim.svg?branch=master)](https://travis-ci.org/VSCodeVim/Vim) [![Build status](https://ci.appveyor.com/api/projects/status/0t6ljij7g5h0ddx8?svg=true)](https://ci.appveyor.com/project/guillermooo/vim) [![Slack Status](http://slackin.westus.cloudapp.azure.com/badge.svg)](http://slackin.westus.cloudapp.azure.com) - # Vim -Vim emulation for Visual Studio Code. +[![Build Status](https://travis-ci.org/VSCodeVim/Vim.svg?branch=master)](https://travis-ci.org/VSCodeVim/Vim) [![Build status](https://ci.appveyor.com/api/projects/status/0t6ljij7g5h0ddx8?svg=true)](https://ci.appveyor.com/project/guillermooo/vim) [![Slack Status](http://slackin.westus.cloudapp.azure.com/badge.svg)](http://slackin.westus.cloudapp.azure.com) + +Vim (aka. VSCodeVim) is a [Visual Studio Code](https://code.visualstudio.com/) extension that enabling the use of the Vim keybinding experience within Visual Studio Code. ![Screenshot](images/screen.png) -## Installation +## Install -1. Install [Visual Studio Code](https://code.visualstudio.com/) -2. Open the command palette (`Ctrl-Shift-P` or `Cmd-Shift-P`) select `Install Extension` and search for **vim**. Alternatively, run `ext install vim` +1. Within Visual Studio Code, open the command palette (`Ctrl-Shift-P` / `Cmd-Shift-P`) +2. Select `Install Extension` and search for 'vim' *or* run `ext install vim` ## Project Status +See our [release notes](https://github.com/VSCodeVim/Vim/releases) for full details. + ### Completed * Modes: @@ -29,34 +31,34 @@ Vim emulation for Visual Studio Code. * Editing: `u`, `ctrl+r` * File Operations: `:q`, `:w` -### Planned +## Contributing -In no particular order: +Contributions are extremely welcomed! +Take a look at [Extension API](https://code.visualstudio.com/docs/extensionAPI/overview) on how to get started and our current [issues](https://github.com/VSCodeVim/Vim/issues) to see what we are working on next. -* Search: `/` -* Support Macros -* Buffers -* Neovim Integration +### Developing -## Contributions +1. Install prerequisites: + * latest [Visual Studio Code](https://code.visualstudio.com/) + * [Node.js](https://nodejs.org/) v4.0.0 or higher +2. Fork and clone the repo, then -Contributions are extremely welcomed! -Take a look at [Extension API](https://code.visualstudio.com/docs/extensionAPI/overview) on how to get started and our current [issues](https://github.com/VSCodeVim/Vim/issues) to see what we are working on next. + ``` + $ npm install + $ npm install -g gulp + $ gulp init + ``` + +3. Open the folder in VS Code + +#### Submitting a PR -### Getting started +You've made some changes, and you are ready to submit a PR? Please make sure: -1. Install [Visual Studio Code](https://code.visualstudio.com/). -2. Install [Node.js](https://nodejs.org/) with version > 4.0.0. -3. Fork the repo. -4. `npm install` -5. `gulp init` - * This step will install type definitions (using [tsd](http://definitelytyped.org/tsd/)). -6. Create a topic branch. -7. Ensure tests pass: +1. Tests pass: * `gulp`: run tslint and tests * [Launch tests within VS Code](https://code.visualstudio.com/docs/extensions/testing-extensions) -8. Squash your commits. -9. Submit your PR. +2. Commits are squashed ## License From 13ff3bcac9e9ba56b58859e5be8cb4b37df88683 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Wed, 2 Dec 2015 02:18:16 -0800 Subject: [PATCH 09/11] readme: set code syntax highlighting to bash --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e9593b63d05..0a950a8841c8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Take a look at [Extension API](https://code.visualstudio.com/docs/extensionAPI/o * [Node.js](https://nodejs.org/) v4.0.0 or higher 2. Fork and clone the repo, then - ``` + ```bash $ npm install $ npm install -g gulp $ gulp init From 158cc9460d22fa430c419fae3d0588e38be18217 Mon Sep 17 00:00:00 2001 From: guillermooo Date: Mon, 30 Nov 2015 08:50:03 +0100 Subject: [PATCH 10/11] map keys from US keyboard to other layouts --- extension.ts | 2 ++ package.json | 15 +++++++++-- src/configuration.ts | 15 +++++++++++ src/keyboard.ts | 58 +++++++++++++++++++++++++++++++++++++++++ src/mode/modeHandler.ts | 10 ++++++- test/keyboard.test.ts | 40 ++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/configuration.ts create mode 100644 src/keyboard.ts create mode 100644 test/keyboard.test.ts diff --git a/extension.ts b/extension.ts index cc0fd23cb070..f0087cf417c3 100644 --- a/extension.ts +++ b/extension.ts @@ -95,6 +95,8 @@ export function activate(context: vscode.ExtensionContext) { registerCommand(context, 'extension.vim_<', () => handleKeyEvent("<")); registerCommand(context, 'extension.vim_>', () => handleKeyEvent(">")); + + registerCommand(context, 'extension.vim_backslash', () => handleKeyEvent("\\")); } function registerCommand(context: vscode.ExtensionContext, command: string, callback: (...args: any[]) => any) { diff --git a/package.json b/package.json index 0919a94bbb9f..83950e5ef901 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ { "key": "Shift+;", "command": "extension.vim_colon", "when": "editorTextFocus" }, { "key": ":", "command": "extension.vim_colon", "when": "editorTextFocus" }, { "key": "space", "command": "extension.vim_space", "when": "editorTextFocus" }, + { "key": "\\", "command": "extension.vim_backslash", "when": "editorTextFocus" }, { "key": "a", "command": "extension.vim_a", "when": "editorTextFocus" }, { "key": "b", "command": "extension.vim_b", "when": "editorTextFocus" }, @@ -120,8 +121,18 @@ { "key": "Shift+,", "command": "extension.vim_<", "when": "editorTextFocus" }, { "key": "Shift+.", "command": "extension.vim_>", "when": "editorTextFocus" } - - ] + ], + "configuration": { + "title": "Vim Configuration", + "type": "object", + "properties": { + "vim.keyboardLayout": { + "default": "en-US (QWERTY)", + "type": "string", + "description": "Keyboard layout to use to translated key presses." + } + } + } }, "scripts": { "vscode:prepublish": "node ./node_modules/vscode/bin/compile", diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 000000000000..7abf7325d93c --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,15 @@ +import {KeyboardLayout} from "./keyboard"; + +export default class Configuration { + + keyboardLayout : KeyboardLayout; + + constructor(keyboard : KeyboardLayout) { + this.keyboardLayout = keyboard; + } + + static fromUserFile() { + // TODO: read .vimrc or a similar file. + return new Configuration(KeyboardLayout.fromUserConfiguration()); + } +} \ No newline at end of file diff --git a/src/keyboard.ts b/src/keyboard.ts new file mode 100644 index 000000000000..4396610b0eb5 --- /dev/null +++ b/src/keyboard.ts @@ -0,0 +1,58 @@ +import * as vscode from "vscode"; + +export class KeyboardLayout { + private mapper : KeyMapper; + + constructor(mapper? : KeyMapper) { + this.mapper = mapper; + } + + get name() : string { + return this.mapper ? this.mapper.name : 'en-US (QWERTY)'; + } + + translate (key : string) : string { + return this.mapper ? this.mapper.get(key) : key; + } + + static fromUserConfiguration() : KeyboardLayout { + const layout = vscode.workspace.getConfiguration('vim').get("keyboardLayout"); + + console.log("Using Vim keyboard layout: " + layout); + + switch (layout) { + case 'es-ES (QWERTY)': + return new KeyboardLayout(new KeyMapperEsEsQwerty()); + + default: + return new KeyboardLayout(); + } + } +} + +export interface KeyMapper { + name : string; + get(key : string) : string; +} + +class KeyMapperEsEsQwerty implements KeyMapper { + + private mappings = {}; + + constructor() { + this.mappings = { + '>': ':', + // '\\': '<', // doesn't really work; in my keyboard there are two keys for \ in US + ';': 'ñ', + "'": "´" + }; + } + + get name() : string { + return 'es-ES (QWERTY)'; + } + + get(key : string) : string { + return this.mappings[key] || key; + } +} diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index 6fc2b5229b14..6886b88cd444 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -5,12 +5,16 @@ import {Mode, ModeName} from './mode'; import NormalMode from './modeNormal'; import InsertMode from './modeInsert'; import VisualMode from './modeVisual'; +import Configuration from '../configuration'; export default class ModeHandler { private modes : Mode[]; private statusBarItem : vscode.StatusBarItem; + configuration : Configuration; constructor() { + this.configuration = Configuration.fromUserFile(); + this.modes = [ new NormalMode(), new InsertMode(), @@ -38,8 +42,12 @@ export default class ModeHandler { } handleKeyEvent(key : string) : void { - var currentModeName = this.currentMode.Name; + // 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. + // https://github.com/Microsoft/vscode/issues/713 + key = this.configuration.keyboardLayout.translate(key); + var currentModeName = this.currentMode.Name; var nextMode : Mode; var inactiveModes = _.filter(this.modes, (m) => !m.IsActive); diff --git a/test/keyboard.test.ts b/test/keyboard.test.ts new file mode 100644 index 000000000000..60a1fce026f7 --- /dev/null +++ b/test/keyboard.test.ts @@ -0,0 +1,40 @@ +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; +import {KeyboardLayout, KeyMapper} from '../src/keyboard'; + +suite("KeyboardLayout", () => { + + test("ctor", () => { + const layout = new KeyboardLayout(); + assert.equal(layout.name, "en-US (QWERTY)"); + }); + + test("lets keys through if using default layout", () => { + const layout = new KeyboardLayout(); + assert.equal(layout.translate('>'), '>'); + assert.equal(layout.translate(':'), ':'); + assert.equal(layout.translate('.'), '.'); + }); + + test("can use custom mapper", () => { + class FakeMapper implements KeyMapper { + get name() : string { + return "fake mapper"; + } + + get(key : string) : string { + return "fake key"; + } + } + + const layout = new KeyboardLayout(new FakeMapper()); + assert.equal(layout.name, "fake mapper"); + assert.equal(layout.translate('>'), 'fake key'); + assert.equal(layout.translate(':'), 'fake key'); + assert.equal(layout.translate('.'), 'fake key'); + }); +}); + +suite("KeyMapperEsEsQwerty", () => { + // TODO: cannot set settings from api? +}); From 7fa55b81c14b9511b5cc17026e3a6112759c60ae Mon Sep 17 00:00:00 2001 From: kimitake Date: Tue, 1 Dec 2015 23:56:24 -0800 Subject: [PATCH 11/11] Add Caret class for normal mode's cursor "right on right-most column should stay at the same location" is for Caret not Cursor --- src/cursor/caret.ts | 12 ++++++++++++ src/{ => cursor}/cursor.ts | 11 ++++++++--- src/mode/modeInsert.ts | 2 +- src/mode/modeNormal.ts | 32 ++++++++++++++++---------------- test/caret.test.ts | 33 +++++++++++++++++++++++++++++++++ test/cursor.test.ts | 6 +++--- test/mode/modeInsert.test.ts | 2 +- test/testUtils.ts | 2 +- test/textEditor.test.ts | 2 +- 9 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 src/cursor/caret.ts rename src/{ => cursor}/cursor.ts (95%) create mode 100644 test/caret.test.ts diff --git a/src/cursor/caret.ts b/src/cursor/caret.ts new file mode 100644 index 000000000000..e3d1e46d15e3 --- /dev/null +++ b/src/cursor/caret.ts @@ -0,0 +1,12 @@ +import Cursor from "./cursor"; + +export default class Caret extends Cursor { + + protected static maxLineLength(line: number) : number { + let len = super.maxLineLength(line) - 1; + if (len < 0) { + len = 0; + } + return len; + } +} \ No newline at end of file diff --git a/src/cursor.ts b/src/cursor/cursor.ts similarity index 95% rename from src/cursor.ts rename to src/cursor/cursor.ts index 3262210d1868..888a031a9d02 100644 --- a/src/cursor.ts +++ b/src/cursor/cursor.ts @@ -1,10 +1,15 @@ import * as _ from "lodash"; import * as vscode from "vscode"; -import TextEditor from "./textEditor"; +import TextEditor from "./../textEditor"; export default class Cursor { private static prevColumn: number = 0; + // overrride this function between cursor mode and caret mode + protected static maxLineLength(line: number) : number { + return TextEditor.readLine(line).length; + } + static move(newPosition: vscode.Position) { if (newPosition === null) { return; @@ -119,7 +124,7 @@ export default class Cursor { static lineEnd() : vscode.Position { let pos = this.currentPosition(); - const lineLength = TextEditor.readLine(pos.line).length; + let lineLength = this.maxLineLength(pos.line); return new vscode.Position(pos.line, lineLength); } @@ -154,7 +159,7 @@ export default class Cursor { } private static isLineEnd(position : vscode.Position) : boolean { - let lineEnd = TextEditor.readLine(position.line).length - 1; + let lineEnd = this.maxLineLength(position.line); if (lineEnd < 0) { lineEnd = 0; } diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index b3d99e8a1a45..52c561e0aef1 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import {ModeName, Mode} from './mode'; import TextEditor from './../textEditor'; -import Cursor from './../cursor'; +import Cursor from './../cursor/cursor'; export default class InsertMode extends Mode { diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index 55a855bed0c5..d40624663510 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import * as vscode from 'vscode'; import {ModeName, Mode} from './mode'; import {showCmdLine} from './../cmd_line/main'; -import Cursor from './../cursor'; +import Caret from './../cursor/caret'; import TextEditor from './../textEditor'; export default class CommandMode extends Mode { @@ -15,16 +15,16 @@ export default class CommandMode extends Mode { ":" : () => { showCmdLine(); }, "u" : () => { vscode.commands.executeCommand("undo"); }, "ctrl+r" : () => { vscode.commands.executeCommand("redo"); }, - "h" : () => { Cursor.move(Cursor.left()); }, - "j" : () => { Cursor.move(Cursor.down()); }, - "k" : () => { Cursor.move(Cursor.up()); }, - "l" : () => { Cursor.move(Cursor.right()); }, - "$" : () => { Cursor.move(Cursor.lineEnd()); }, - "^" : () => { Cursor.move(Cursor.lineBegin()); }, - "gg" : () => { Cursor.move(Cursor.firstLineNonBlankChar()); }, - "G" : () => { Cursor.move(Cursor.lastLineNonBlankChar()); }, - "w" : () => { Cursor.move(Cursor.wordRight()); }, - "b" : () => { Cursor.move(Cursor.wordLeft()); }, + "h" : () => { Caret.move(Caret.left()); }, + "j" : () => { Caret.move(Caret.down()); }, + "k" : () => { Caret.move(Caret.up()); }, + "l" : () => { Caret.move(Caret.right()); }, + "$" : () => { Caret.move(Caret.lineEnd()); }, + "^" : () => { Caret.move(Caret.lineBegin()); }, + "gg" : () => { Caret.move(Caret.firstLineNonBlankChar()); }, + "G" : () => { Caret.move(Caret.lastLineNonBlankChar()); }, + "w" : () => { Caret.move(Caret.wordRight()); }, + "b" : () => { Caret.move(Caret.wordLeft()); }, ">>" : () => { vscode.commands.executeCommand("editor.action.indentLines"); }, "<<" : () => { vscode.commands.executeCommand("editor.action.outdentLines"); }, "dd" : () => { vscode.commands.executeCommand("editor.action.deleteLines"); }, @@ -37,7 +37,7 @@ export default class CommandMode extends Mode { ShouldBeActivated(key : string, currentMode : ModeName) : boolean { if (key === 'esc' || key === 'ctrl+[') { - Cursor.move(Cursor.left()); + Caret.move(Caret.left()); return true; } } @@ -66,14 +66,14 @@ export default class CommandMode extends Mode { } private CommandDelete(n: number) : void { - let pos = Cursor.currentPosition(); + let pos = Caret.currentPosition(); let end = pos.translate(0, n); let range : vscode.Range = new vscode.Range(pos, end); TextEditor.delete(range).then(function() { - let lineEnd = Cursor.lineEnd(); + let lineEnd = Caret.lineEnd(); - if (pos.character === lineEnd.character) { - Cursor.move(Cursor.left()); + if (pos.character === lineEnd.character + 1) { + Caret.move(Caret.left()); } }); } diff --git a/test/caret.test.ts b/test/caret.test.ts new file mode 100644 index 000000000000..6f7b937a6a9f --- /dev/null +++ b/test/caret.test.ts @@ -0,0 +1,33 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import TextEditor from './../src/textEditor'; +import Caret from './../src/cursor/caret'; + +suite("caret", () => { + let text: Array = [ + "mary had", + "a", + "little lamb" + ]; + + setup(done => { + TextEditor.insert(text.join('\n')).then(() => done()); + }); + + teardown(done => { + let range = new vscode.Range(Caret.documentBegin(), Caret.documentEnd()); + TextEditor.delete(range).then(() => done()); + }); + + test("right on right-most column should stay at the same location", () => { + Caret.move(new vscode.Position(0, 7)); + + let current = Caret.currentPosition(); + assert.equal(current.line, 0); + assert.equal(current.character, 7); + + let right = Caret.right(); + assert.equal(right.line, 0); + assert.equal(right.character, 7); + }); +}); \ No newline at end of file diff --git a/test/cursor.test.ts b/test/cursor.test.ts index 6b7b714c6d62..e146e39d5221 100644 --- a/test/cursor.test.ts +++ b/test/cursor.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import TextEditor from './../src/textEditor'; -import Cursor from './../src/cursor'; +import Cursor from './../src/cursor/cursor'; suite("cursor", () => { let text: Array = [ @@ -55,7 +55,7 @@ suite("cursor", () => { assert.equal(right.character, 6); }); - test("right on right-most column should stay at the same location", () => { + test("right on right-most column should NOT stay at the same location", () => { Cursor.move(new vscode.Position(0, 7)); let current = Cursor.currentPosition(); @@ -64,7 +64,7 @@ suite("cursor", () => { let right = Cursor.right(); assert.equal(right.line, 0); - assert.equal(right.character, 7); + assert.notEqual(right.character, 7); }); test("down should move cursor one line down", () => { diff --git a/test/mode/modeInsert.test.ts b/test/mode/modeInsert.test.ts index a96331c729c9..6b0d84660fd2 100644 --- a/test/mode/modeInsert.test.ts +++ b/test/mode/modeInsert.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import ModeInsert from '../../src/mode/modeInsert'; import {ModeName} from '../../src/mode/mode'; -import Cursor from '../../src/cursor'; +import Cursor from '../../src/cursor/cursor'; import TextEditor from '../../src/textEditor'; import * as testUtils from '../testUtils'; diff --git a/test/testUtils.ts b/test/testUtils.ts index 0f946ad28a98..155e63944a71 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import TextEditor from '../src/textEditor'; -import Cursor from '../src/cursor'; +import Cursor from '../src/cursor/cursor'; import * as assert from 'assert'; export function clearTextEditor(): Thenable { diff --git a/test/textEditor.test.ts b/test/textEditor.test.ts index fa3603066ec0..fcdcc2fda9ca 100644 --- a/test/textEditor.test.ts +++ b/test/textEditor.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import TextEditor from './../src/textEditor'; -import Cursor from './../src/cursor'; +import Cursor from './../src/cursor/cursor'; import * as testUtils from './testUtils';