Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visual Mode + Rudimentary Operators #144

Merged
merged 5 commits into from
Feb 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd prefer forEach :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? IMO, for of is much better than forEach - it reads way nicer, and you can do for (const ... of) which is not possible with forEach. (Are you sure you haven't confused it with for in?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙆‍♀️ Not that strongly attached to it so let's just go with for of

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha. The closer I get to bike shedding, the more argumentative I get, or something. 🌈

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Awesome, this is the pattern in which I was envisioning as well but never got around to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then later we turn on --experimentalDecorators and this list autopopulates and its magic and unicorns everywhere! ⭐ 🎆 👯

'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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great first cut. Would it be worthwhile to move selection to it's own class? That is what I had envisioned for the motion class and the moveTo function would take care of doing the proper styling.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Well, to start, I still don't fully grasp the concept of Motion. It seems to me like it's just a wrapper around Position to update VSCode, but I think those are actually 2 separate responsibilities that don't need to be in the same class.

This seems like a good place to discuss some of my thoughts about structuring this code. My idea is to pass around a state object which could look like this (very rough)

{
    cursorStart: Position(0, 5),
    cursorEnd: Position(0, 7),
    selecting: true,
    mode: Mode.Visual,
    textInEditor: "hello world" // to facilitate undo - maybe stored as diffs to save memory, idk?
}

All motions would just take a state and return a new state object. When you pressed a key the object would be passed through some transformations and eventually you'd get a new one. When the process is done you would then update VSCode to show the new selections and cursor positions.

The idea is to make stuff like macros/number commands easier - if, say, the user goes into visual mode, makes 2w into a macro and does 100@@, we don't redraw the selection 200 times.

This would also make proper undo (e.g., stepping back through full actions rather than what we have now) way more straightforward, since actions are nicely discretized.

Anywho, I still have to dig more into the source of Vintageous and VSVim before I feel confident about making sweeping changes like this, but those are my general thoughts. What do you think?

(I also am a fan of separating out the motions into classes like VSVim and Vintageous do - otherwise the Motion and Position classes would eventually become monstrous!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that's what I had initially intended for Motion. Essentially what you described, some sort of state that would be passed around as this is super simliar to how VsVim does it (Vim for Visual Studio).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah... that makes more sense now!

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