Skip to content

Commit

Permalink
Various improvements to registers (#3728)
Browse files Browse the repository at this point in the history
* Implement / (search) register

Fixes #3542

* Implement read-only registers

Fixes #3604

* Implement % (file name) register

Refs #3605

* Implement : (command) register

Fixes #3605

* Do not display _ (black hole) register in :reg output

Fixes #3606

* :reg can take multiple arguments

When it does, it lists only the registers given as an argument.
Fixes #3610

* Allow the : (command) register to be used as a macro to repeat the command

Fixes #1775
  • Loading branch information
J-Fields authored and jpoon committed May 6, 2019
1 parent 0f27b42 commit 1ef304e
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 28 deletions.
10 changes: 3 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,9 @@ moving around:

## Copying and moving text

Miscellanea:

- We don't support read only registers.

| Status | Command | Description | Note |
| ------------------ | ---------------- | ------------------------------------------------------ | ------------------------------------- |
| :warning: | "{char} | use register {char} for the next delete, yank, or put | read only registers are not supported |
| Status | Command | Description |
| ------------------ | ---------------- | ------------------------------------------------------ |
| :white_check_mark: | "{char} | use register {char} for the next delete, yank, or put |
| :white_check_mark: | "\* | use register `*` to access system clipboard |
| :white_check_mark: | :reg | show the contents of all registers |
| :white_check_mark: | :reg {arg} | show the contents of registers mentioned in {arg} |
Expand Down
10 changes: 10 additions & 0 deletions extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { commandLine } from './src/cmd_line/commandLine';
import { configuration } from './src/configuration/configuration';
import { globalState } from './src/state/globalState';
import { taskQueue } from './src/taskQueue';
import { Register } from './src/register/register';

let extensionContext: vscode.ExtensionContext;
let previousActiveEditorId: EditorIdentity | null = null;
Expand Down Expand Up @@ -97,6 +98,11 @@ export async function activate(context: vscode.ExtensionContext) {
extensionContext = context;
extensionContext.subscriptions.push(StatusBar);

if (vscode.window.activeTextEditor) {
const filepathComponents = vscode.window.activeTextEditor.document.fileName.split(/\\|\//);
Register.putByKey(filepathComponents[filepathComponents.length - 1], '%', undefined, true);
}

// load state
await Promise.all([commandLine.load(), globalState.load()]);

Expand Down Expand Up @@ -217,9 +223,13 @@ export async function activate(context: vscode.ExtensionContext) {
lastClosedModeHandler = mhPrevious || lastClosedModeHandler;

if (vscode.window.activeTextEditor === undefined) {
Register.putByKey('', '%', undefined, true);
return;
}

const filepathComponents = vscode.window.activeTextEditor.document.fileName.split(/\\|\//);
Register.putByKey(filepathComponents[filepathComponents.length - 1], '%', undefined, true);

taskQueue.enqueueTask(async () => {
if (vscode.window.activeTextEditor !== undefined) {
const mh: ModeHandler = await getAndUpdateModeHandler(true);
Expand Down
4 changes: 4 additions & 0 deletions src/actions/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,8 @@ class CommandInsertInSearchMode extends BaseCommand {
vimState.cursorStopPosition
).pos;

Register.putByKey(searchState.searchString, '/', undefined, true);

return vimState;
} else if (key === '<up>') {
vimState.globalState.searchStateIndex -= 1;
Expand Down Expand Up @@ -1162,6 +1164,8 @@ async function createSearchStateAndMoveToMatch(args: {

vimState.globalState.addSearchStateToHistory(vimState.globalState.searchState);

Register.putByKey(vimState.globalState.searchState.searchString, '/', undefined, true);

return vimState;
}

Expand Down
9 changes: 9 additions & 0 deletions src/cmd_line/commandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { StatusBar } from '../statusBar';
import { VimError, ErrorCode } from '../error';
import { VimState } from '../state/vimState';
import { configuration } from '../configuration/configuration';
import { Register } from '../register/register';
import { RecordedState } from '../state/recordedState';

class CommandLine {
private _history: CommandLineHistory;
Expand Down Expand Up @@ -56,6 +58,13 @@ class CommandLine {
this._history.add(command);
this._commandLineHistoryIndex = this._history.get().length;

if (!command.startsWith('reg')) {
let recState = new RecordedState();
recState.registerName = ':';
recState.commandList = command.split('');
Register.putByKey(recState, ':', undefined, true);
}

try {
const cmd = parser.parse(command);
const useNeovim = configuration.enableNeovim && cmd.command && cmd.command.neovimCapable;
Expand Down
12 changes: 8 additions & 4 deletions src/cmd_line/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { RecordedState } from '../../state/recordedState';
import * as node from '../node';

export interface IRegisterCommandArguments extends node.ICommandArgs {
arg?: string;
registers: string[];
}
export class RegisterCommand extends node.CommandBase {
protected _arguments: IRegisterCommandArguments;
Expand Down Expand Up @@ -39,10 +39,14 @@ export class RegisterCommand extends node.CommandBase {
}

async execute(vimState: VimState): Promise<void> {
if (this.arguments.arg !== undefined && this.arguments.arg.length > 0) {
await this.displayRegisterValue(this.arguments.arg);
if (this.arguments.registers.length === 1) {
await this.displayRegisterValue(this.arguments.registers[0]);
} else {
const currentRegisterKeys = Register.getKeys();
const currentRegisterKeys = Register.getKeys().filter(
reg =>
reg !== '_' &&
(this.arguments.registers.length === 0 || this.arguments.registers.includes(reg))
);
const registerKeyAndContent = new Array<any>();

for (let registerKey of currentRegisterKeys) {
Expand Down
21 changes: 14 additions & 7 deletions src/cmd_line/subparsers/register.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import * as node from '../commands/register';
import { RegisterCommand } from '../commands/register';
import { Scanner } from '../scanner';

export function parseRegisterCommandArgs(args: string): node.RegisterCommand {
if (!args) {
return new node.RegisterCommand({});
export function parseRegisterCommandArgs(args: string): RegisterCommand {
if (!args || !args.trim()) {
return new RegisterCommand({
registers: [],
});
}

let scanner = new Scanner(args);
let name = scanner.nextWord();
let regs: string[] = [];
let reg = scanner.nextWord();
while (reg !== Scanner.EOF) {
regs.push(reg);
reg = scanner.nextWord();
}

return new node.RegisterCommand({
arg: name,
return new RegisterCommand({
registers: regs,
});
}
6 changes: 4 additions & 2 deletions src/mode/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ export class ModeHandler implements vscode.Disposable {
vimState.globalState.previousFullAction = vimState.recordedState;

if (recordedState.isInsertion) {
Register.putByKey(recordedState, '.');
Register.putByKey(recordedState, '.', undefined, true);
}
}

Expand Down Expand Up @@ -925,7 +925,9 @@ export class ModeHandler implements vscode.Disposable {

vimState.isReplayingMacro = true;

if (command.replay === 'contentChange') {
if (command.register === ':') {
await commandLine.Run(recordedMacro.commandString, vimState);
} else if (command.replay === 'contentChange') {
vimState = await this.runMacro(vimState, recordedMacro);
} else {
let keyStrokes: string[] = [];
Expand Down
27 changes: 19 additions & 8 deletions src/register/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,20 @@ export class Register {
/**
* The '"' is the unnamed register.
* The '*' and '+' are special registers for accessing the system clipboard.
* TODO: Read-Only registers
* '.' register has the last inserted text.
* '%' register has the current file path.
* ':' is the most recently executed command.
* '#' is the name of last edited file. (low priority)
* The '.' register has the last inserted text.
* The '%' register has the current file path.
* The ':' is the most recently executed command.
* The '#' is the name of last edited file. (low priority)
*/
private static registers: { [key: string]: IRegisterContent } = {
'"': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'.': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'*': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: true },
'+': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: true },
'-': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'/': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'%': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
':': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
_: { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'0': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
'1': { text: '', registerMode: RegisterMode.CharacterWise, isClipboardRegister: false },
Expand Down Expand Up @@ -74,7 +76,7 @@ export class Register {
}

public static isValidRegisterForMacro(register: string): boolean {
return /^[a-zA-Z0-9]+$/.test(register);
return /^[a-zA-Z0-9:]+$/.test(register);
}

/**
Expand All @@ -88,7 +90,7 @@ export class Register {
throw new Error(`Invalid register ${register}`);
}

if (Register.isBlackHoleRegister(register)) {
if (Register.isBlackHoleRegister(register) || Register.isReadOnlyRegister(register)) {
return;
}

Expand Down Expand Up @@ -116,6 +118,10 @@ export class Register {
return register && register.isClipboardRegister;
}

private static isReadOnlyRegister(registerName: string): boolean {
return ['.', '%', ':', '#', '/'].includes(registerName);
}

private static isValidLowercaseRegister(register: string): boolean {
return /^[a-z]+$/.test(register);
}
Expand Down Expand Up @@ -283,7 +289,8 @@ export class Register {
public static putByKey(
content: RegisterContent,
register = '"',
registerMode = RegisterMode.AscertainFromCurrentMode
registerMode = RegisterMode.AscertainFromCurrentMode,
force = false
): void {
if (!Register.isValidRegister(register)) {
throw new Error(`Invalid register ${register}`);
Expand All @@ -297,6 +304,10 @@ export class Register {
return;
}

if (Register.isReadOnlyRegister(register) && !force) {
return;
}

Register.registers[register] = {
text: content,
registerMode: registerMode || RegisterMode.AscertainFromCurrentMode,
Expand Down
7 changes: 7 additions & 0 deletions test/macro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,11 @@ suite('Record and execute a macro', () => {
keysPressed: 'qadd.q@a@a',
end: ['|test'],
});

newTest({
title: ': (command) register can be used as a macro',
start: ['|old', 'old', 'old'],
keysPressed: ':s/old/new\nj@:j@@',
end: ['new', 'new', '|new'],
});
});
85 changes: 85 additions & 0 deletions test/register/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { VimState } from '../../src/state/vimState';
import { Clipboard } from '../../src/util/clipboard';
import { getTestingFunctions } from '../testSimplifier';
import { assertEqual, assertEqualLines, cleanUpWorkspace, setupWorkspace } from '../testUtils';
import { RecordedState } from '../../src/state/recordedState';

suite('register', () => {
let modeHandler: ModeHandler;
Expand Down Expand Up @@ -303,4 +304,88 @@ suite('register', () => {

assertEqualLines(['st1', 'tteest2', 'test3']);
});

test('Search register (/) is set by forward search', async () => {
await modeHandler.handleMultipleKeyEvents(
'iWake up early in Karakatu, Alaska'.split('').concat(['<Esc>', '0'])
);

// Register changed by forward search
await modeHandler.handleMultipleKeyEvents('/katu\n'.split(''));
assert.equal((await Register.getByKey('/')).text, 'katu');

// Register changed even if search doesn't exist
await modeHandler.handleMultipleKeyEvents('0/notthere\n'.split(''));
assert.equal((await Register.getByKey('/')).text, 'notthere');

// Not changed if search is canceled
await modeHandler.handleMultipleKeyEvents('0/Alaska'.split('').concat(['<Esc>']));
assert.equal((await Register.getByKey('/')).text, 'notthere');
});

test('Search register (/) is set by backward search', async () => {
await modeHandler.handleMultipleKeyEvents(
'iWake up early in Karakatu, Alaska'.split('').concat(['<Esc>', '$'])
);

// Register changed by forward search
await modeHandler.handleMultipleKeyEvents('?katu\n'.split(''));
assert.equal((await Register.getByKey('/')).text, 'katu');

// Register changed even if search doesn't exist
await modeHandler.handleMultipleKeyEvents('$?notthere\n'.split(''));
assert.equal((await Register.getByKey('/')).text, 'notthere');

// Not changed if search is canceled
await modeHandler.handleMultipleKeyEvents('$?Alaska'.split('').concat(['<Esc>']));
assert.equal((await Register.getByKey('/')).text, 'notthere');
});

test('Search register (/) is set by star search', async () => {
await modeHandler.handleMultipleKeyEvents(
'iWake up early in Karakatu, Alaska'.split('').concat(['<Esc>', '0'])
);

await modeHandler.handleKeyEvent('*');
assert.equal((await Register.getByKey('/')).text, '\\bWake\\b');

await modeHandler.handleMultipleKeyEvents(['g', '*']);
assert.equal((await Register.getByKey('/')).text, 'Wake');

await modeHandler.handleKeyEvent('#');
assert.equal((await Register.getByKey('/')).text, '\\bWake\\b');

await modeHandler.handleMultipleKeyEvents(['g', '#']);
assert.equal((await Register.getByKey('/')).text, 'Wake');
});

test('Command register (:) is set by command line', async () => {
const command = '%s/old/new/g';
await modeHandler.handleMultipleKeyEvents((':' + command + '\n').split(''));

// :reg should not update the command register
await modeHandler.handleMultipleKeyEvents(':reg\n'.split(''));

const regStr = ((await Register.getByKey(':')).text as RecordedState).commandString;
assert.equal(regStr, command);
});

test('Read-only registers cannot be written to', async () => {
await modeHandler.handleMultipleKeyEvents('iShould not be copied'.split('').concat(['<Esc>']));

Register.putByKey('Expected for /', '/', undefined, true);
Register.putByKey('Expected for .', '.', undefined, true);
Register.putByKey('Expected for %', '%', undefined, true);
Register.putByKey('Expected for :', ':', undefined, true);

await modeHandler.handleMultipleKeyEvents('"/yy'.split(''));
await modeHandler.handleMultipleKeyEvents('".yy'.split(''));
await modeHandler.handleMultipleKeyEvents('"%yy'.split(''));
await modeHandler.handleMultipleKeyEvents('":yy'.split(''));

assert.equal((await Register.getByKey('/')).text, 'Expected for /');
assert.equal((await Register.getByKey('.')).text, 'Expected for .');
assert.equal((await Register.getByKey('%')).text, 'Expected for %');
assert.equal((await Register.getByKey(':')).text, 'Expected for :');
});
});

0 comments on commit 1ef304e

Please sign in to comment.