Skip to content

Commit

Permalink
Merge pull request #144 from johnfn/visual-mode
Browse files Browse the repository at this point in the history
Visual Mode + Rudimentary Operators
  • Loading branch information
johnfn committed Feb 24, 2016
2 parents b1cf10e + 3c332d6 commit d0eb62a
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
out
node_modules
typings
*.swp
*.sw?
22 changes: 0 additions & 22 deletions src/action/deleteAction.ts

This file was deleted.

27 changes: 25 additions & 2 deletions src/mode/mode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

import {Motion} from './../motion/motion';
import {Position} from './../motion/position';

export enum ModeName {
Normal,
Expand Down Expand Up @@ -45,9 +46,31 @@ export abstract class Mode {
this.keyHistory = [];
}

protected keyToNewPosition: { [key: string]: (motion: Position) => Promise<Position>; } = {
"h" : async (c) => { return c.getLeft(); },
"j" : async (c) => { return c.getDown(0); },
"k" : async (c) => { return c.getUp(0); },
"l" : async (c) => { return c.getRight(); },
// "^" : async () => { return vscode.commands.executeCommand("cursorHome"); },
"gg" : async (c) => {
return new Position(0, Position.getFirstNonBlankCharAtLine(0), null); },
"G" : async (c) => {
const lastLine = c.getDocumentEnd().line;

return new Position(lastLine, Position.getFirstNonBlankCharAtLine(lastLine), null);
},
"$" : async (c) => { return c.getLineEnd(); },
"0" : async (c) => { return c.getLineBegin(); },
"w" : async (c) => { return c.getWordRight(); },
"e" : async (c) => { return c.getCurrentWordEnd(); },
"b" : async (c) => { return c.getWordLeft(); },
"}" : async (c) => { return c.getCurrentParagraphEnd(); },
"{" : async (c) => { return c.getCurrentParagraphBeginning(); }
};

abstract shouldBeActivated(key : string, currentMode : ModeName) : boolean;

abstract handleActivation(key : string) : Promise<{}>;
abstract handleActivation(key : string) : Promise<void>;

abstract handleKeyEvent(key : string) : Promise<{}>;
abstract handleKeyEvent(key : string) : Promise<void>;
}
16 changes: 7 additions & 9 deletions src/mode/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Mode, ModeName} from './mode';
import {Motion, MotionMode} from './../motion/motion';
import {NormalMode} from './modeNormal';
import {InsertMode} from './modeInsert';
import {VisualMode} from './modeVisual';
import {Configuration} from '../configuration';

export class ModeHandler implements vscode.Disposable {
Expand All @@ -20,25 +21,22 @@ export class ModeHandler implements vscode.Disposable {

this._motion = new Motion(null);
this._modes = [
new NormalMode(this._motion),
new NormalMode(this._motion, this),
new InsertMode(this._motion),
new VisualMode(this._motion, this),
];

this.setCurrentModeByName(ModeName.Normal);
}

get currentMode() : Mode {
let currentMode = this._modes.find((mode, index) => {
return mode.isActive;
});

return currentMode;
return this._modes.find(mode => mode.isActive);
}

setCurrentModeByName(modeName : ModeName) {
this._modes.forEach(mode => {
for (let mode of this._modes) {
mode.isActive = (mode.name === modeName);
});
}

switch (modeName) {
case ModeName.Insert:
Expand Down Expand Up @@ -88,7 +86,7 @@ export class ModeHandler implements vscode.Disposable {
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
}

this._statusBarItem.text = (text) ? '-- ' + text + ' --' : '';
this._statusBarItem.text = text ? `--${text}--` : '';
this._statusBarItem.show();
}

Expand Down
9 changes: 4 additions & 5 deletions src/mode/modeInsert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,15 @@ export class InsertMode extends Mode {
return key in this.activationKeyHandler;
}

handleActivation(key : string) : Promise<{}> {
return this.activationKeyHandler[key](this.motion);
async handleActivation(key : string): Promise<void> {
await this.activationKeyHandler[key](this.motion);
}

async handleKeyEvent(key : string) : Promise<{}> {
async handleKeyEvent(key : string) : Promise<void> {
this.keyHistory.push(key);

await TextEditor.insert(this.resolveKeyValue(key));

return vscode.commands.executeCommand("editor.action.triggerSuggest");
await vscode.commands.executeCommand("editor.action.triggerSuggest");
}

// Some keys have names that are different to their value.
Expand Down
23 changes: 13 additions & 10 deletions src/mode/modeNormal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import * as vscode from 'vscode';
import {ModeName, Mode} from './mode';
import {showCmdLine} from './../cmd_line/main';
import {Motion} from './../motion/motion';
import {DeleteAction} from './../action/deleteAction';
import {ModeHandler} from './modeHandler';
import {DeleteOperator} from './../operator/delete';

export class NormalMode extends Mode {
private keyHandler : { [key : string] : (motion : Motion) => Promise<{}>; } = {
protected keyHandler : { [key : string] : (motion : Motion) => Promise<{}>; } = {
":" : async () => { return showCmdLine(""); },
"u" : async () => { return vscode.commands.executeCommand("undo"); },
"ctrl+r" : async () => { return vscode.commands.executeCommand("redo"); },
Expand Down Expand Up @@ -37,30 +38,32 @@ export class NormalMode extends Mode {
"dd" : async () => { return vscode.commands.executeCommand("editor.action.deleteLines"); },
"dw" : async () => { return vscode.commands.executeCommand("deleteWordRight"); },
"db" : async () => { return vscode.commands.executeCommand("deleteWordLeft"); },
"x" : async (m) => { return DeleteAction.Character(m); },
"x" : async (m) => { await new DeleteOperator(this._modeHandler).run(m.position, m.position.getRight()); return {}; },
"X" : async (m) => { return vscode.commands.executeCommand("deleteLeft"); },
"esc": async () => { return vscode.commands.executeCommand("workbench.action.closeMessages"); }
};

constructor(motion : Motion) {
private _modeHandler: ModeHandler;

constructor(motion : Motion, modeHandler: ModeHandler) {
super(ModeName.Normal, motion);

this._modeHandler = modeHandler;
}

shouldBeActivated(key : string, currentMode : ModeName) : boolean {
return (key === 'esc' || key === 'ctrl+[' || key === "ctrl+c");
}

async handleActivation(key : string): Promise<{}> {
async handleActivation(key : string): Promise<void> {
this.motion.left().move();

return this.motion;
}

async handleKeyEvent(key : string): Promise<{}> {
async handleKeyEvent(key : string): Promise<void> {
this.keyHistory.push(key);

let keyHandled = false;
let keysPressed : string;
let keysPressed: string;

for (let window = this.keyHistory.length; window > 0; window--) {
keysPressed = _.takeRight(this.keyHistory, window).join('');
Expand All @@ -72,7 +75,7 @@ export class NormalMode extends Mode {

if (keyHandled) {
this.keyHistory = [];
return this.keyHandler[keysPressed](this.motion);
await this.keyHandler[keysPressed](this.motion);
}
}
}
140 changes: 140 additions & 0 deletions src/mode/modeVisual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use strict";

import * as _ from 'lodash';

import { ModeName, Mode } from './mode';
import { Motion} from './../motion/motion';
import { Position } from './../motion/position';
import { Operator } from './../operator/operator';
import { DeleteOperator } from './../operator/delete';
import { ModeHandler } from './modeHandler.ts';
import { ChangeOperator } from './../operator/change';

export class VisualMode extends Mode {
/**
* The part of the selection that stays in the same place when motions are applied.
*/
private _selectionStart: Position;

/**
* The part of the selection that moves.
*/
private _selectionStop : Position;
private _modeHandler : ModeHandler;

private _keysToOperators: { [key: string]: Operator };

constructor(motion: Motion, modeHandler: ModeHandler) {
super(ModeName.Visual, motion);

this._modeHandler = modeHandler;
this._keysToOperators = {
// TODO: use DeleteOperator.key()

// TODO: Don't pass in mode handler to DeleteOperators,
// simply allow the operators to say what mode they transition into.
'd': new DeleteOperator(modeHandler),
'x': new DeleteOperator(modeHandler),
'c': new ChangeOperator(modeHandler)
};
}

shouldBeActivated(key: string, currentMode: ModeName): boolean {
return key === "v";
}

async handleActivation(key: string): Promise<void> {
this._selectionStart = this.motion.position;
this._selectionStop = this._selectionStart;

this.motion.select(this._selectionStart, this._selectionStop);
}

handleDeactivation(): void {
super.handleDeactivation();

this.motion.moveTo(this._selectionStop.line, this._selectionStop.character);
}

/**
* TODO:
*
* Eventually, the following functions should be moved into a unified
* key handler and dispatcher thing.
*/

private async _handleMotion(): Promise<boolean> {
let keyHandled = false;
let keysPressed: string;

for (let window = this.keyHistory.length; window > 0; window--) {
keysPressed = _.takeRight(this.keyHistory, window).join('');
if (this.keyToNewPosition[keysPressed] !== undefined) {
keyHandled = true;
break;
}
}

if (keyHandled) {
this._selectionStop = await this.keyToNewPosition[keysPressed](this._selectionStop);

this.motion.moveTo(this._selectionStart.line, this._selectionStart.character);

/**
* Always select the letter that we started visual mode on, no matter
* if we are in front or behind it. Imagine that we started visual mode
* with some text like this:
*
* abc|def
*
* (The | represents the cursor.) If we now press w, we'll select def,
* but if we hit b we expect to select abcd, so we need to getRight() on the
* start of the selection when it precedes where we started visual mode.
*/

// TODO this could be abstracted out
if (this._selectionStart.compareTo(this._selectionStop) <= 0) {
this.motion.select(this._selectionStart, this._selectionStop);
} else {
this.motion.select(this._selectionStart.getRight(), this._selectionStop);
}

this.keyHistory = [];
}

return keyHandled;
}

private async _handleOperator(): Promise<boolean> {
let keysPressed: string;
let operator: Operator;

for (let window = this.keyHistory.length; window > 0; window--) {
keysPressed = _.takeRight(this.keyHistory, window).join('');
if (this._keysToOperators[keysPressed] !== undefined) {
operator = this._keysToOperators[keysPressed];
break;
}
}

if (operator) {
if (this._selectionStart.compareTo(this._selectionStop) <= 0) {
await operator.run(this._selectionStart, this._selectionStop.getRight());
} else {
await operator.run(this._selectionStart.getRight(), this._selectionStop);
}
}

return !!operator;
}

async handleKeyEvent(key: string): Promise<void> {
this.keyHistory.push(key);

const wasMotion = await this._handleMotion();

if (!wasMotion) {
await this._handleOperator();
}
}
}
31 changes: 29 additions & 2 deletions src/motion/motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class Motion implements vscode.Disposable {
return this._position;
}

public set position(val: Position) {
this._position = val;
}

public constructor(mode: MotionMode) {
// initialize to current position
let currentPosition = vscode.window.activeTextEditor.selection.active;
Expand Down Expand Up @@ -100,7 +104,24 @@ export class Motion implements vscode.Disposable {
let selection = new vscode.Selection(this.position, this.position);
vscode.window.activeTextEditor.selection = selection;

let range = new vscode.Range(this.position, this.position.translate(0, 1));
this.highlightBlock(this.position);

return this;
}

/**
* Allows us to simulate a block cursor by highlighting a 1 character
* space at the provided position in a lighter color.
*/
private highlightBlock(start: Position): void {
this.highlightRange(start, start.getRight());
}

/**
* Highlights the range from start to end in the color of a block cursor.
*/
private highlightRange(start: Position, end: Position): void {
let range = new vscode.Range(start, end);
vscode.window.activeTextEditor.revealRange(range, vscode.TextEditorRevealType.InCenterIfOutsideViewport);

switch (this._motionMode) {
Expand All @@ -114,8 +135,14 @@ export class Motion implements vscode.Disposable {
vscode.window.activeTextEditor.setDecorations(this._caretDecoration, []);
break;
}
}

return this;
public select(from: Position, to: Position): void {
let selection = new vscode.Selection(from, to);

vscode.window.activeTextEditor.selection = selection;

this.highlightBlock(to);
}

public left() : Motion {
Expand Down
Loading

0 comments on commit d0eb62a

Please sign in to comment.