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

Macro #894

Merged
merged 13 commits into from
Oct 18, 2016
153 changes: 149 additions & 4 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VimState } from './../mode/modeHandler';
import { VimState, RecordedState } from './../mode/modeHandler';
import { SearchState, SearchDirection } from './../state/searchState';
import { ReplaceState } from './../state/replaceState';
import { VisualBlockMode } from './../mode/modeVisualBlock';
Expand Down Expand Up @@ -614,6 +614,14 @@ class CommandInsertRegisterContent extends BaseCommand {

if (register.text instanceof Array) {
text = (register.text as string []).join("\n");
} else if (register.text instanceof RecordedState) {
vimState.recordedState.transformations.push({
Copy link
Member

Choose a reason for hiding this comment

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

Can we just immediately produce the stringified macro right here instead of shipping it off to some text transformation thing? The transformations are supposed to only be when you're actually changing the content in the text editor.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was planing to do so but later on I found we are doing this way for dot command, which is similar to Macros. Besides, actually I do want to delegate back the real content change/ action execution back to modeHandler, any ideas to do so if we don't leverage transformations? I use transformations as a way of lazy execution.

Copy link
Member

Choose a reason for hiding this comment

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

If we're actually performing a macro at this point, then we absolutely should use a transformation.

But it looked to me like we were just storing the content of the macro in a register, which doesn't require any text transformations.

Copy link
Member Author

Choose a reason for hiding this comment

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

hmn, I agree. In this particular case, we just need to insert all the keystrokes.

Copy link
Member Author

Choose a reason for hiding this comment

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

@johnfn just played with Vim again and found that running Ctrl-R {register} is actually performing the macro stored in {register}.

For example, you recored ab<BS>c<Esc> in register a, which represents Enter Insert Mode, insert b, backspace, insert c. Repeating this macro in Normal mode will just insert c and return back to Normal Mode. If you are in Insert Mode and run <C-r> a, what gets inserted is ac. The first a is translated to Insert a as it's Insert Mode and then perform all following keystrokes.

type: "macro",
register: vimState.recordedState.registerName,
replay: "keystrokes"
});

return vimState;
} else {
text = register.text;
}
Expand Down Expand Up @@ -644,6 +652,112 @@ class CommandInsertRegisterContent extends BaseCommand {

}

@RegisterAction
class CommandRecordMacro extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
keys = ["q", "<character>"];

public async exec(position: Position, vimState: VimState): Promise<VimState> {
const register = this.keysPressed[1];
vimState.recordedMacro = new RecordedState();
vimState.recordedMacro.registerName = register.toLocaleLowerCase();

if (!/^[A-Z]+$/.test(register) || !Register.has(register)) {
// If register name is upper case, it means we are appending commands to existing register instead of overriding.
let newRegister = new RecordedState();
newRegister.registerName = register;
Register.putByKey(newRegister, register);
}

vimState.isRecordingMacro = true;
return vimState;
}

public doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
const register = this.keysPressed[1];

return super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register);
}

public couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
const register = this.keysPressed[1];

return super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register);
}
}

@RegisterAction
export class CommandQuitRecordMacro extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
keys = ["q"];

public async exec(position: Position, vimState: VimState): Promise<VimState> {
let existingMacro = (await Register.getByKey(vimState.recordedMacro.registerName)).text as RecordedState;
existingMacro.actionsRun = existingMacro.actionsRun.concat(vimState.recordedMacro.actionsRun);
vimState.isRecordingMacro = false;
return vimState;
}

public doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
return super.doesActionApply(vimState, keysPressed) && vimState.isRecordingMacro;
}

public couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
return super.couldActionApply(vimState, keysPressed) && vimState.isRecordingMacro;
}
}

@RegisterAction
class CommandExecuteMacro extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
keys = ["@", "<character>"];
runsOnceForEachCountPrefix = true;

public async exec(position: Position, vimState: VimState): Promise<VimState> {
const register = this.keysPressed[1];
vimState.recordedState.transformations.push({
type: "macro",
register: register,
replay: "contentChange"
});

return vimState;
}

public doesActionApply(vimState: VimState, keysPressed: string[]): boolean {
const register = keysPressed[1];

return super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register);
}

public couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
const register = keysPressed[1];

return super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register);
}
}

@RegisterAction
class CommandExecuteLastMacro extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
keys = ["@", "@"];
runsOnceForEachCountPrefix = true;

public async exec(position: Position, vimState: VimState): Promise<VimState> {
let lastInvokedMacro = vimState.historyTracker.lastInvokedMacro;

if (lastInvokedMacro) {
vimState.recordedState.transformations.push({
type: "macro",
register: lastInvokedMacro.registerName,
replay: "contentChange"
});
}

return vimState;
}
}

@RegisterAction
class CommandEsc extends BaseCommand {
modes = [
Expand Down Expand Up @@ -812,7 +926,8 @@ export class CommandInsertPreviousText extends BaseCommand {
keys = ["<C-a>"];

public async exec(position: Position, vimState: VimState): Promise<VimState> {
let actions = Register.lastContentChange.actionsRun.slice(0);
let actions = ((await Register.getByKey('.')).text as RecordedState).actionsRun.slice(0);
// let actions = Register.lastContentChange.actionsRun.slice(0);
// The first action is entering Insert Mode, which is not necessary in this case
actions.shift();
// The last action is leaving Insert Mode, which is not necessary in this case
Expand Down Expand Up @@ -1731,7 +1846,20 @@ export class PutCommand extends BaseCommand {
const register = await Register.get(vimState);
const dest = after ? position : position.getRight();

if (typeof register.text === "object") {
if (register.text instanceof RecordedState) {
/**
* Paste content from recordedState. This one is actually complex as Vim has internal key code for key strokes.
* For example, Backspace is stored as `<80>kb`. So if you replay a macro, which is stored in a register as `a1<80>kb2`, you
* shall just get `2` inserted as `a` represents entering Insert Mode, `<80>bk` represents Backspace. However here, we shall
* insert the plain text content of the register, which is `a1<80>kb2`.
*/
vimState.recordedState.transformations.push({
type: "macro",
register: vimState.recordedState.registerName,
replay: "keystrokes"
});
return vimState;
} else if (typeof register.text === "object") {
return await this.execVisualBlockPaste(register.text, position, vimState, after);
}

Expand Down Expand Up @@ -1881,6 +2009,15 @@ export class GPutCommand extends BaseCommand {
const register = await Register.get(vimState);
let addedLinesCount: number;

if (register.text instanceof RecordedState) {
vimState.recordedState.transformations.push({
type: "macro",
register: vimState.recordedState.registerName,
replay: "keystrokes"
});

return vimState;
}
if (typeof register.text === "object") { // visual block mode
addedLinesCount = register.text.length * vimState.recordedState.count;
} else {
Expand Down Expand Up @@ -2025,7 +2162,15 @@ export class GPutBeforeCommand extends BaseCommand {
const register = await Register.get(vimState);
let addedLinesCount: number;

if (typeof register.text === "object") { // visual block mode
if (register.text instanceof RecordedState) {
vimState.recordedState.transformations.push({
type: "macro",
register: vimState.recordedState.registerName,
replay: "keystrokes"
});

return vimState;
} else if (typeof register.text === "object") { // visual block mode
addedLinesCount = register.text.length * vimState.recordedState.count;
} else {
addedLinesCount = register.text.split('\n').length;
Expand Down
4 changes: 3 additions & 1 deletion src/cmd_line/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import * as vscode from "vscode";
import * as node from "../node";
import {ModeHandler} from "../../mode/modeHandler";
import {ModeHandler, RecordedState} from "../../mode/modeHandler";
import { Register} from '../../register/register';

export interface IRegisterCommandArguments extends node.ICommandArgs {
Expand All @@ -26,6 +26,8 @@ export class RegisterCommand extends node.CommandBase {
let result = (await Register.getByKey(register)).text;
if (result instanceof Array) {
result = result.join("\n").substr(0, 100);
} else if (result instanceof RecordedState) {
// TODO
}

return result;
Expand Down
3 changes: 3 additions & 0 deletions src/history/historyTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as _ from "lodash";

import { Position } from './../motion/position';
import { TextEditor } from './../textEditor';
import { RecordedState } from './../mode/modeHandler';

import DiffMatchPatch = require("diff-match-patch");

Expand Down Expand Up @@ -156,6 +157,8 @@ export class HistoryTracker {
public lastContentChanges: vscode.TextDocumentContentChangeEvent[];
public currentContentChanges: vscode.TextDocumentContentChangeEvent[];

public lastInvokedMacro: RecordedState;

/**
* The entire Undo/Redo stack.
*/
Expand Down
Loading