2016-07-15 12:39:15 +02:00
|
|
|
/*---------------------------------------------------------------------------------------------
|
|
|
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
|
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2016-07-25 16:59:01 +02:00
|
|
|
import * as vscode from 'vscode';
|
2016-07-15 12:39:15 +02:00
|
|
|
import {
|
|
|
|
|
TextEditorCursorStyle,
|
|
|
|
|
Position,
|
|
|
|
|
Range,
|
|
|
|
|
Selection,
|
|
|
|
|
TextEditor,
|
|
|
|
|
TextEditorRevealType
|
|
|
|
|
} from 'vscode';
|
|
|
|
|
|
|
|
|
|
import {Words} from './words';
|
|
|
|
|
import {MotionState, Motion} from './motions';
|
2016-08-04 18:08:09 +02:00
|
|
|
import {Mode, IController, DeleteRegister, Command, ModifierKeys} from './common';
|
2016-07-15 12:39:15 +02:00
|
|
|
import {Mappings} from './mappings';
|
|
|
|
|
|
|
|
|
|
export interface ITypeResult {
|
|
|
|
|
hasConsumedInput: boolean;
|
|
|
|
|
executeEditorCommand: Command;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class Controller implements IController {
|
|
|
|
|
|
|
|
|
|
private _currentMode: Mode;
|
|
|
|
|
private _currentInput: string;
|
|
|
|
|
private _motionState: MotionState;
|
|
|
|
|
private _isVisual: boolean;
|
|
|
|
|
|
|
|
|
|
public get motionState(): MotionState { return this._motionState; }
|
|
|
|
|
public findMotion(input: string): Motion { return Mappings.findMotion(input); }
|
|
|
|
|
public isMotionPrefix(input: string): boolean { return Mappings.isMotionPrefix(input); }
|
|
|
|
|
|
|
|
|
|
private _deleteRegister:DeleteRegister;
|
|
|
|
|
public setDeleteRegister(register:DeleteRegister): void { this._deleteRegister = register; }
|
|
|
|
|
public getDeleteRegister(): DeleteRegister { return this._deleteRegister; }
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this._motionState = new MotionState();
|
|
|
|
|
this._deleteRegister = null;
|
|
|
|
|
this.setVisual(false);
|
|
|
|
|
this.setMode(Mode.NORMAL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setWordSeparators(wordSeparators: string): void {
|
|
|
|
|
this._motionState.wordCharacterClass = Words.createWordCharacters(wordSeparators);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ensureNormalModePosition(editor: TextEditor): void {
|
|
|
|
|
if (this._currentMode !== Mode.NORMAL) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-07-28 09:56:09 +02:00
|
|
|
if (this._isVisual) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-07-15 12:39:15 +02:00
|
|
|
let sel = editor.selection;
|
|
|
|
|
let pos = sel.active;
|
|
|
|
|
let doc = editor.document;
|
|
|
|
|
let lineContent = doc.lineAt(pos.line).text;
|
|
|
|
|
if (lineContent.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let maxCharacter = lineContent.length - 1;
|
|
|
|
|
if (pos.character > maxCharacter) {
|
2016-07-28 09:56:09 +02:00
|
|
|
setPositionAndReveal(editor, pos.line, maxCharacter);
|
2016-07-15 12:39:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public hasInput(): boolean {
|
|
|
|
|
return this._currentInput.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public clearInput(): void {
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getMode(): Mode {
|
|
|
|
|
return this._currentMode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setMode(newMode: Mode): void {
|
|
|
|
|
if (newMode !== this._currentMode) {
|
|
|
|
|
this._currentMode = newMode;
|
|
|
|
|
this._motionState.cursorDesiredCharacter = -1; // uninitialized
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setVisual(newVisual: boolean): void {
|
|
|
|
|
if (this._isVisual !== newVisual) {
|
|
|
|
|
this._isVisual = newVisual;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getVisual(): boolean {
|
|
|
|
|
return this._isVisual;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getCursorStyle(): TextEditorCursorStyle {
|
|
|
|
|
if (this._currentMode === Mode.NORMAL) {
|
|
|
|
|
if (/^([1-9]\d*)?(r|c)/.test(this._currentInput)) {
|
|
|
|
|
return TextEditorCursorStyle.Underline;
|
|
|
|
|
}
|
|
|
|
|
return TextEditorCursorStyle.Block;
|
|
|
|
|
}
|
|
|
|
|
if (this._currentMode === Mode.REPLACE) {
|
|
|
|
|
return TextEditorCursorStyle.Underline;
|
|
|
|
|
}
|
|
|
|
|
return TextEditorCursorStyle.Line;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _getModeLabel(): string {
|
|
|
|
|
if (this._currentMode === Mode.NORMAL) {
|
|
|
|
|
if (this._isVisual) {
|
|
|
|
|
return '-- VISUAL --';
|
|
|
|
|
}
|
|
|
|
|
return '-- NORMAL --';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._currentMode === Mode.REPLACE) {
|
|
|
|
|
if (this._isVisual) {
|
|
|
|
|
return '-- (replace) VISUAL --';
|
|
|
|
|
}
|
|
|
|
|
return '-- REPLACE --';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._isVisual) {
|
|
|
|
|
return '-- (insert) VISUAL --';
|
|
|
|
|
}
|
|
|
|
|
return '-- INSERT --';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getStatusText(): string {
|
|
|
|
|
let label = this._getModeLabel();
|
|
|
|
|
return `VIM:> ${label}` + (this._currentInput ? ` >${this._currentInput}` : ``);
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 18:08:09 +02:00
|
|
|
public type(editor: TextEditor, text: string, modifierKeys: ModifierKeys): Thenable<ITypeResult> {
|
2016-07-15 12:39:15 +02:00
|
|
|
if (this._currentMode !== Mode.NORMAL && this._currentMode !== Mode.REPLACE) {
|
2016-07-25 16:59:01 +02:00
|
|
|
return Promise.resolve({
|
2016-07-15 12:39:15 +02:00
|
|
|
hasConsumedInput: false,
|
|
|
|
|
executeEditorCommand: null
|
2016-07-25 16:59:01 +02:00
|
|
|
});
|
2016-07-15 12:39:15 +02:00
|
|
|
}
|
|
|
|
|
if (this._currentMode === Mode.REPLACE) {
|
|
|
|
|
let pos = editor.selection.active;
|
|
|
|
|
editor.edit((builder) => {
|
|
|
|
|
builder.replace(new Range(pos.line, pos.character, pos.line, pos.character + 1), text);
|
|
|
|
|
}).then(() => {
|
|
|
|
|
setPositionAndReveal(editor, pos.line, pos.character + 1);
|
|
|
|
|
});
|
|
|
|
|
|
2016-07-25 16:59:01 +02:00
|
|
|
return Promise.resolve({
|
2016-07-15 12:39:15 +02:00
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: null
|
2016-07-25 16:59:01 +02:00
|
|
|
});
|
2016-07-15 12:39:15 +02:00
|
|
|
}
|
|
|
|
|
this._currentInput += text;
|
2016-08-04 18:08:09 +02:00
|
|
|
return this._interpretNormalModeInput(editor, modifierKeys);
|
2016-07-15 12:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public replacePrevChar(editor: TextEditor, text: string, replaceCharCnt: number): boolean {
|
|
|
|
|
if (this._currentMode !== Mode.NORMAL && this._currentMode !== Mode.REPLACE) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (this._currentMode === Mode.REPLACE) {
|
|
|
|
|
let pos = editor.selection.active;
|
|
|
|
|
editor.edit((builder) => {
|
|
|
|
|
builder.replace(new Range(pos.line, pos.character - replaceCharCnt, pos.line, pos.character), text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Not supporting IME building in NORMAL mode
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 18:08:09 +02:00
|
|
|
private _interpretNormalModeInput(editor: TextEditor, modifierKeys: ModifierKeys): Thenable<ITypeResult> {
|
2016-07-25 16:59:01 +02:00
|
|
|
if (this._currentInput.startsWith(':')) {
|
|
|
|
|
return vscode.window.showInputBox({value: 'tabm'}).then((value) => {
|
2016-08-04 18:08:09 +02:00
|
|
|
let result = this._findMapping(value || '', editor, modifierKeys);
|
2016-07-25 17:42:04 +02:00
|
|
|
return Promise.resolve(result);
|
2016-07-25 16:59:01 +02:00
|
|
|
});
|
|
|
|
|
}
|
2016-08-04 18:08:09 +02:00
|
|
|
let result = this._findMapping(this._currentInput, editor, modifierKeys);
|
2016-07-25 16:59:01 +02:00
|
|
|
return Promise.resolve(result);
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 18:08:09 +02:00
|
|
|
private _findMapping(input: string, editor: TextEditor, modifierKeys: ModifierKeys): ITypeResult {
|
|
|
|
|
let command = Mappings.findCommand(input, modifierKeys);
|
2016-07-15 12:39:15 +02:00
|
|
|
if (command) {
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: command
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 18:08:09 +02:00
|
|
|
let operator = Mappings.findOperator(input, modifierKeys);
|
2016-07-15 12:39:15 +02:00
|
|
|
if (operator) {
|
|
|
|
|
if (this._isVisual) {
|
|
|
|
|
if (operator.runVisual(this, editor)) {
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Mode.NORMAL
|
|
|
|
|
if (operator.runNormal(this, editor)) {
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 18:08:09 +02:00
|
|
|
let motionCommand = Mappings.findMotionCommand(input, this._isVisual, modifierKeys);
|
2016-07-15 12:39:15 +02:00
|
|
|
if (motionCommand) {
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: motionCommand
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-25 16:59:01 +02:00
|
|
|
let motion = Mappings.findMotion(input);
|
2016-07-15 12:39:15 +02:00
|
|
|
if (motion) {
|
|
|
|
|
let newPos = motion.run(editor.document, editor.selection.active, this._motionState);
|
|
|
|
|
if (this._isVisual) {
|
|
|
|
|
setSelectionAndReveal(editor, this._motionState.anchor, newPos.line, newPos.character);
|
|
|
|
|
} else {
|
|
|
|
|
// Mode.NORMAL
|
|
|
|
|
setPositionAndReveal(editor, newPos.line, newPos.character);
|
|
|
|
|
}
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// is it motion building
|
2016-07-25 16:59:01 +02:00
|
|
|
if (this.isMotionPrefix(input)) {
|
2016-07-15 12:39:15 +02:00
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-25 17:42:04 +02:00
|
|
|
// INVALID INPUT - beep!!
|
|
|
|
|
this._currentInput = '';
|
|
|
|
|
return {
|
|
|
|
|
hasConsumedInput: true,
|
|
|
|
|
executeEditorCommand: null
|
|
|
|
|
};
|
2016-07-15 12:39:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setSelectionAndReveal(editor:TextEditor, anchor:Position, line: number, char: number): void {
|
|
|
|
|
editor.selection = new Selection(anchor, new Position(line, char));
|
|
|
|
|
revealPosition(editor, line, char);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setPositionAndReveal(editor: TextEditor, line: number, char: number): void {
|
|
|
|
|
editor.selection = new Selection(new Position(line, char), new Position(line, char));
|
|
|
|
|
revealPosition(editor, line, char);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function revealPosition(editor: TextEditor, line: number, char: number): void {
|
|
|
|
|
editor.revealRange(new Range(line, char, line, char), TextEditorRevealType.Default);
|
|
|
|
|
}
|