mirror of
https://github.com/VSCodeVim/Vim.git
synced 2024-08-16 15:50:35 +03:00
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:
parent
0f27b42c77
commit
1ef304e5dd
10
ROADMAP.md
10
ROADMAP.md
@ -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} |
|
||||
|
10
extension.ts
10
extension.ts
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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[] = [];
|
||||
|
@ -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,
|
||||
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
@ -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 :');
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user