Various improvements to registers (#3728)

* 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
This commit is contained in:
Jason Fields 2019-05-06 00:06:42 -04:00 committed by Jason Poon
parent 0f27b42c77
commit 1ef304e5dd
10 changed files with 163 additions and 28 deletions

View File

@ -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} |

View File

@ -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;
@ -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()]);
@ -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);

View File

@ -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;
@ -1162,6 +1164,8 @@ async function createSearchStateAndMoveToMatch(args: {
vimState.globalState.addSearchStateToHistory(vimState.globalState.searchState);
Register.putByKey(vimState.globalState.searchState.searchString, '/', undefined, true);
return vimState;
}

View File

@ -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;
@ -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;

View File

@ -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;
@ -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) {

View File

@ -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,
});
}

View File

@ -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);
}
}
@ -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[] = [];

View File

@ -35,11 +35,10 @@ 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 },
@ -47,6 +46,9 @@ export class Register {
'*': { 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 },
@ -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);
}
/**
@ -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;
}
@ -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);
}
@ -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}`);
@ -297,6 +304,10 @@ export class Register {
return;
}
if (Register.isReadOnlyRegister(register) && !force) {
return;
}
Register.registers[register] = {
text: content,
registerMode: registerMode || RegisterMode.AscertainFromCurrentMode,

View File

@ -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'],
});
});

View File

@ -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;
@ -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 :');
});
});