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/README.md b/README.md index 1fc52e654e7a..0a950a8841c8 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: @@ -23,40 +25,40 @@ 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` * 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. + ```bash + $ 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 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 diff --git a/extension.ts b/extension.ts index da774200cba4..4e5742e32dd7 100644 --- a/extension.ts +++ b/extension.ts @@ -6,7 +6,7 @@ import {showCmdLine} from './src/cmd_line/main'; import * as cc from './src/cmd_line/lexer'; import ModeHandler from "./src/mode/modeHandler"; import {ModeName} from "./src/mode/mode"; -import Cursor from "./src/cursor"; +import Cursor from "./src/cursor/cursor"; var modeHandler : ModeHandler; @@ -87,16 +87,18 @@ export function activate(context: vscode.ExtensionContext) { registerCommand(context, 'extension.vim_6', () => handleKeyEvent("6")); registerCommand(context, 'extension.vim_7', () => handleKeyEvent("7")); registerCommand(context, 'extension.vim_8', () => handleKeyEvent("8")); - registerCommand(context, 'extension.vim_9', () => handleKeyEvent("9")); + registerCommand(context, 'extension.vim_9', () => handleKeyEvent("9")); registerCommand(context, 'extension.vim_$', () => handleKeyEvent("$")); registerCommand(context, 'extension.vim_^', () => handleKeyEvent("^")); registerCommand(context, 'extension.vim_ctrl_r', () => handleKeyEvent("ctrl+r")); registerCommand(context, 'extension.vim_ctrl_[', () => handleKeyEvent("ctrl+[")); - + 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) { @@ -106,4 +108,4 @@ function registerCommand(context: vscode.ExtensionContext, command: string, call function handleKeyEvent(key:string) { modeHandler.handleKeyEvent(key); -} \ No newline at end of file +} 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..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", @@ -133,6 +144,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/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/cursor.ts b/src/cursor.ts deleted file mode 100644 index 5d9e67086497..000000000000 --- a/src/cursor.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as vscode from "vscode"; -import TextEditor from "./textEditor"; -import {ModeName} from './mode/mode'; -import ModeHandler from "./mode/modeHandler"; - -const blockCursorDecoration = vscode.window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(224, 224, 224, 0.5)', - borderColor: 'rgba(240, 240, 240, 0.8)' - }, - light: { - backgroundColor: 'rgba(32, 32, 32, 0.5)', - borderColor: 'rgba(16, 16, 16, 0.8)' - }, - borderStyle: 'solid', - borderWidth: '1px' -}); - -export default class Cursor { - private static prevColumn: number = 0; - - static move(position: vscode.Position) { - const newSelection = new vscode.Selection(position, position); - vscode.window.activeTextEditor.selection = newSelection; - } - - 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--; - } - - this.prevColumn = 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) { - column++; - } - - this.prevColumn = column; - return new vscode.Position(pos.line, column); - } - - static down() : vscode.Position { - let pos = this.currentPosition(); - let line = pos.line; - let column = this.prevColumn; - - if (!Cursor.isLastLine(line)) { - let nextLineMaxColumn = TextEditor.ReadLine(++line).length - 1; - - if (nextLineMaxColumn < 0) { - nextLineMaxColumn = 0; - } - - if (nextLineMaxColumn < this.prevColumn) { - column = nextLineMaxColumn; - } - } - - return new vscode.Position(line, column); - } - - static up() : vscode.Position { - let pos = this.currentPosition(); - let line = pos.line; - let column = this.prevColumn; - - if (line !== 0) { - let nextLineMaxColumn = TextEditor.ReadLine(--line).length - 1; - - if (nextLineMaxColumn < 0) { - nextLineMaxColumn = 0; - } - - if (nextLineMaxColumn < this.prevColumn) { - column = nextLineMaxColumn; - } - } - - return new vscode.Position(line, column); - } - - static lineBegin() : vscode.Position { - 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; - - 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 blockCursor(modeHandler: ModeHandler) : void { - vscode.window.onDidChangeTextEditorSelection((e) => { - if (modeHandler.currentMode.Name !== ModeName.Normal) { - return; - } - if (e.selections.length === 1) { - let sel = e.selections[0]; - if (sel.start.isEqual(sel.end)) { - let range = new vscode.Range(sel.start, sel.end.translate(0, 1)); - e.textEditor.setDecorations(blockCursorDecoration, [range]); - } - } - }); - modeHandler.onModeChanged((mode) => { - if (mode.Name !== ModeName.Normal) { - vscode.window.activeTextEditor.setDecorations(blockCursorDecoration, []); - } - }); - } -} - 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/cursor.ts b/src/cursor/cursor.ts new file mode 100644 index 000000000000..1732ef0ec4f9 --- /dev/null +++ b/src/cursor/cursor.ts @@ -0,0 +1,282 @@ +import * as _ from "lodash"; +import * as vscode from "vscode"; +import TextEditor from "./../textEditor"; +import {ModeName} from './../mode/mode'; +import ModeHandler from "./../mode/modeHandler"; + +const blockCursorDecoration = vscode.window.createTextEditorDecorationType({ + dark: { + backgroundColor: 'rgba(224, 224, 224, 0.5)', + borderColor: 'rgba(240, 240, 240, 0.8)' + }, + light: { + backgroundColor: 'rgba(32, 32, 32, 0.5)', + borderColor: 'rgba(16, 16, 16, 0.8)' + }, + borderStyle: 'solid', + borderWidth: '1px' +}); + +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; + } + 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; + } + + static currentPosition(): vscode.Position { + return vscode.window.activeTextEditor.selection.active; + } + + static left() : vscode.Position { + let pos = this.currentPosition(); + let column = pos.character; + + 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 (!this.isLineEnd(pos)) { + column++; + } + + return new vscode.Position(pos.line, column); + } + + static down() : vscode.Position { + let pos = this.currentPosition(); + let line = pos.line; + let column = this.prevColumn; + + if (!Cursor.isLastLine(pos)) { + let nextLineMaxColumn = TextEditor.readLine(++line).length - 1; + + if (nextLineMaxColumn < 0) { + nextLineMaxColumn = 0; + } + + if (nextLineMaxColumn < this.prevColumn) { + column = nextLineMaxColumn; + } + } + + return new vscode.Position(line, column); + } + + static up() : vscode.Position { + let pos = this.currentPosition(); + let line = pos.line; + let column = this.prevColumn; + + if (!this.isFirstLine(pos)) { + let nextLineMaxColumn = TextEditor.readLine(--line).length - 1; + + 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)) { + 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(); + return new vscode.Position(pos.line, 0); + } + + static lineEnd() : vscode.Position { + let pos = this.currentPosition(); + let lineLength = this.maxLineLength(pos.line); + + 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); + } + + 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); + } + + static blockCursor(modeHandler: ModeHandler) : void { + vscode.window.onDidChangeTextEditorSelection((e) => { + if (modeHandler.currentMode.Name !== ModeName.Normal) { + return; + } + if (e.selections.length === 1) { + let sel = e.selections[0]; + if (sel.start.isEqual(sel.end)) { + let range = new vscode.Range(sel.start, sel.end.translate(0, 1)); + e.textEditor.setDecorations(blockCursorDecoration, [range]); + } + } + }); + modeHandler.onModeChanged((mode) => { + if (mode.Name !== ModeName.Normal) { + vscode.window.activeTextEditor.setDecorations(blockCursorDecoration, []); + } + }); + } + + private static isLineBeginning(position : vscode.Position) : boolean { + return position.character === 0; + } + + private static isLineEnd(position : vscode.Position) : boolean { + let lineEnd = this.maxLineLength(position.line); + 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); + } + + 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; + } + + private static posOfFirstNonBlankChar(line: number): number { + return TextEditor.readLine(line).match(/^\s*/)[0].length; + } +} 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/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/mode.ts b/src/mode/mode.ts index 565ce3338b42..4703975352c3 100644 --- a/src/mode/mode.ts +++ b/src/mode/mode.ts @@ -26,14 +26,14 @@ export abstract class Mode { set IsActive(val : boolean) { this.isActive = val; } - + public HandleDeactivation() : void { this.keyHistory = []; } 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/modeHandler.ts b/src/mode/modeHandler.ts index d7798c348688..2c2d1149ff0e 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -5,6 +5,7 @@ import {Mode, ModeName} from './mode'; import NormalMode from './modeNormal'; import InsertMode from './modeInsert'; import VisualMode from './modeVisual'; +import Configuration from '../configuration'; type ModeChangedHandler = (mode: Mode) => void; @@ -12,8 +13,11 @@ export default class ModeHandler { private modes : Mode[]; private modeChangedHandlers: ModeChangedHandler[] = []; private statusBarItem : vscode.StatusBarItem; + configuration : Configuration; constructor() { + this.configuration = Configuration.fromUserFile(); + this.modes = [ new NormalMode(), new InsertMode(), @@ -38,27 +42,31 @@ export default class ModeHandler { var statusBarText = (this.currentMode.Name === ModeName.Normal) ? '' : ModeName[modeName]; this.setupStatusBarItem(statusBarText.toUpperCase()); - + for (let handler of this.modeChangedHandlers) { handler(this.currentMode); - } + } } handleKeyEvent(key : string) : void { + // 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); - + _.forEach(inactiveModes, (m, i) => { if (m.ShouldBeActivated(key, currentModeName)) { nextMode = m; - } + } }); - + if (nextMode) { this.currentMode.HandleDeactivation(); - + nextMode.HandleActivation(key); this.setCurrentModeByName(nextMode.Name); return; @@ -69,7 +77,7 @@ export default class ModeHandler { onModeChanged(handler: (newMode: Mode) => void) : void { this.modeChangedHandlers.push(handler); - } + } private setupStatusBarItem(text : string) : void { if (!this.statusBarItem) { diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index 5343e9a36c78..52c561e0aef1 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -1,54 +1,57 @@ 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 { - private activationKeyHandler : { [ key : string] : () => void; } = {}; - + + private activationKeyHandler : { [ key : string] : () => Thenable | 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" : () => { - vscode.commands.executeCommand("editor.action.insertLineAfter"); + + // open blank line below current line + "o" : () => { + return vscode.commands.executeCommand("editor.action.insertLineAfter"); }, - - // open blank line above current line - "O" : () => { - vscode.commands.executeCommand("editor.action.insertLineBefore"); - } + + // open blank line above current line + "O" : () => { + return vscode.commands.executeCommand("editor.action.insertLineBefore"); + } }; } ShouldBeActivated(key : string, currentMode : ModeName) : boolean { 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. // 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 26de802bf835..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,27 +15,29 @@ 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()); }, - "w" : () => { vscode.commands.executeCommand("cursorWordRight"); }, - "b" : () => { vscode.commands.executeCommand("cursorWordLeft"); }, + "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"); }, "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); } }; } ShouldBeActivated(key : string, currentMode : ModeName) : boolean { if (key === 'esc' || key === 'ctrl+[') { - Cursor.move(Cursor.left()); + Caret.move(Caret.left()); return true; } } @@ -48,7 +50,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,18 +59,22 @@ export default class CommandMode extends Mode { break; } } - + if (keyHandled) { this.keyHistory = []; } } 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 = Caret.currentPosition(); + let end = pos.translate(0, n); + let range : vscode.Range = new vscode.Range(pos, end); + TextEditor.delete(range).then(function() { + let lineEnd = Caret.lineEnd(); + + if (pos.character === lineEnd.character + 1) { + Caret.move(Caret.left()); + } }); } -} \ No newline at end of file +} 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 2fe64b64c7c8..d8dd86d10a0e 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -1,38 +1,47 @@ import * as vscode from "vscode"; -export default class TextEditor { - static Insert(text: string, position: vscode.Position = null) : Thenable { +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); }); } - 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 readFile(): string { + return vscode.window.activeTextEditor.document.getText(); + } + + 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; } + + static getLineAt(position: vscode.Position): vscode.TextLine { + return vscode.window.activeTextEditor.document.lineAt(position); + } } diff --git a/test/caret.test.ts b/test/caret.test.ts new file mode 100644 index 000000000000..20d298dd8b90 --- /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/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 new file mode 100644 index 000000000000..e146e39d5221 --- /dev/null +++ b/test/cursor.test.ts @@ -0,0 +1,270 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import TextEditor from './../src/textEditor'; +import Cursor from './../src/cursor/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 NOT 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.notEqual(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(); + 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); + }); + + 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); + }); + }); +}); 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 5e827bb072bd..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 @@ -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/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? +}); 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/modeInsert.test.ts b/test/mode/modeInsert.test.ts new file mode 100644 index 000000000000..6b0d84660fd2 --- /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/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/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/testUtils.ts b/test/testUtils.ts new file mode 100644 index 000000000000..155e63944a71 --- /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/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 new file mode 100644 index 000000000000..fcdcc2fda9ca --- /dev/null +++ b/test/textEditor.test.ts @@ -0,0 +1,71 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import TextEditor from './../src/textEditor'; +import Cursor from './../src/cursor/cursor'; + +import * as testUtils from './testUtils'; + +suite("text editor", () => { + suiteSetup(done => { + testUtils.clearTextEditor() + .then(done); + }); + + suiteTeardown(done => { + testUtils.clearTextEditor() + .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); + }); +}); 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 +}