Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 88 additions & 62 deletions src/cmd_line/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,116 @@ import * as vscode from 'vscode';

// eslint-disable-next-line id-denylist
import { Parser, any, optWhitespace } from 'parsimmon';
import { ErrorCode, VimError } from '../../error';
import { Register } from '../../register/register';
import { Register, RegisterContent } from '../../register/register';
import { RecordedState } from '../../state/recordedState';
import { VimState } from '../../state/vimState';
import { StatusBar } from '../../statusBar';
import { ExCommand } from '../../vimscript/exCommand';
import { PutExCommand } from './put';

class RegisterDisplayItem implements vscode.QuickPickItem {
public readonly label: string;
public readonly description: string;
public readonly buttons: readonly vscode.QuickInputButton[];

public readonly key: string;
public readonly content: RegisterContent | undefined;
public readonly stringContent: string;

constructor(registerKey: string, content: RegisterContent | undefined) {
this.label = registerKey;
this.key = registerKey;

this.content = content;
this.stringContent = '';
this.description = '';
this.buttons = [];

if (typeof content === 'string') {
this.stringContent = content;
this.description = this.stringContent;
} else if (content instanceof RecordedState) {
this.description = content.actionsRun.map((x) => x.keysPressed.join('')).join('');
}

if (this.description.length > 100) {
// maximum length of 100 characters for the description
this.description = this.description.slice(0, 97) + '...';
}

if (this.stringContent !== '') {
this.buttons = [
{
tooltip: 'Paste',
iconPath: new vscode.ThemeIcon('clippy'),
},
];
}
}
}

export class RegisterCommand extends ExCommand {
public override isRepeatableWithDot: boolean = false;
private readonly registerKeys: string[];

public static readonly argParser: Parser<RegisterCommand> = optWhitespace.then(
// eslint-disable-next-line id-denylist
any.sepBy(optWhitespace).map((registers) => new RegisterCommand(registers)),
);

private readonly registers: string[];
constructor(registers: string[]) {
super();
this.registers = registers;
}

private async getRegisterDisplayValue(register: string): Promise<string | undefined> {
let result = (await Register.get(register))?.text;
if (result instanceof Array) {
result = result.join('\n').substr(0, 100);
} else if (result instanceof RecordedState) {
result = result.actionsRun.map((x) => x.keysPressed.join('')).join('');
}
this.registerKeys = Register.getKeysSorted().filter((r) => !Register.isBlackHoleRegister(r));

return result;
if (registers.length > 0) {
this.registerKeys = this.registerKeys.filter((r) => registers.includes(r));
}
}

async displayRegisterValue(vimState: VimState, register: string): Promise<void> {
let result = await this.getRegisterDisplayValue(register);
if (result === undefined) {
StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.NothingInRegister, register));
} else {
result = result.replace(/\n/g, '\\n');
void vscode.window.showInformationMessage(`${register} ${result}`);
}
async execute(vimState: VimState): Promise<void> {
const quickPick = vscode.window.createQuickPick<RegisterDisplayItem>();

quickPick.items = await Promise.all(
this.registerKeys.map(async (r) => {
const register = await Register.get(r);
return new RegisterDisplayItem(r, register?.text);
}),
);

// The user clicked a QuickPick item
quickPick.onDidChangeSelection((items) => {
RegisterCommand.showRegisterContent(items);
quickPick.dispose();
});

quickPick.onDidTriggerItemButton(async (event) => {
await RegisterCommand.paste(vimState, event.item);
quickPick.dispose();
});

return new Promise<void>((resolve) => {
quickPick.onDidHide(resolve);
quickPick.show();
});
}

private regSortOrder(register: string): number {
const specials = ['-', '*', '+', '.', ':', '%', '#', '/', '='];
if (register === '"') {
return 0;
} else if (register >= '0' && register <= '9') {
return 10 + parseInt(register, 10);
} else if (register >= 'a' && register <= 'z') {
return 100 + (register.charCodeAt(0) - 'a'.charCodeAt(0));
} else if (specials.includes(register)) {
return 1000 + specials.indexOf(register);
} else {
throw new Error(`Unexpected register ${register}`);
private static showRegisterContent(items: readonly RegisterDisplayItem[]) {
if (items.length === 0) {
return;
}

const item = items[0];

const message = `${item.label} ${item.stringContent}`;
vscode.window.showInformationMessage(message);
}

async execute(vimState: VimState): Promise<void> {
if (this.registers.length === 1) {
await this.displayRegisterValue(vimState, this.registers[0]);
} else {
const currentRegisterKeys = Register.getKeys()
.filter(
(reg) => reg !== '_' && (this.registers.length === 0 || this.registers.includes(reg)),
)
.sort((reg1: string, reg2: string) => this.regSortOrder(reg1) - this.regSortOrder(reg2));
const registerKeyAndContent = new Array<vscode.QuickPickItem>();

for (const registerKey of currentRegisterKeys) {
const displayValue = await this.getRegisterDisplayValue(registerKey);
if (typeof displayValue === 'string') {
registerKeyAndContent.push({
label: registerKey,
description: displayValue,
});
}
}

void vscode.window.showQuickPick(registerKeyAndContent).then(async (val) => {
if (val) {
const result = val.description;
void vscode.window.showInformationMessage(`${val.label} ${result}`);
}
});
}
private static async paste(vimState: VimState, item: RegisterDisplayItem) {
const putCommand = new PutExCommand({
register: item.key,
bang: false,
});

await putCommand.execute(vimState);
}
}
59 changes: 46 additions & 13 deletions src/register/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ export interface IRegisterContent {
}

export class Register {
private static readonly readOnlyRegisters: readonly string[] = [
'.', // Last inserted text
':', // Most recently executed command
'%', // Current file path (relative to workspace root)
'#', // Previous file path (relative to workspace root)
'/', // Most recently executed search
];
private static readonly specialRegisters: readonly string[] = [
...this.readOnlyRegisters,
'"', // Unnamed (default)
'*', // Clipboard
'+', // Clipboard
'.', // Last inserted text
'-', // Last deleted text less than a line
'/', // Most recently executed search
':', // Most recently executed command
'%', // Current file path (relative to workspace root)
'#', // Previous file path (relative to workspace root)
'_', // Black hole (always empty)
'=', // Expression register
];
Expand Down Expand Up @@ -80,18 +83,18 @@ export class Register {

public static isValidRegister(register: string): boolean {
return (
Register.isValidLowercaseRegister(register) ||
Register.isValidUppercaseRegister(register) ||
/^[0-9]$/.test(register) ||
this.specialRegisters.includes(register)
this.isValidLowercaseRegister(register) ||
this.isValidUppercaseRegister(register) ||
this.isValidNumberedRegister(register) ||
this.isValidSpecialRegister(register)
);
}

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

private static isBlackHoleRegister(registerName: string): boolean {
public static isBlackHoleRegister(registerName: string): boolean {
return registerName === '_';
}

Expand All @@ -100,17 +103,25 @@ export class Register {
}

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

private static isValidLowercaseRegister(register: string): boolean {
public static isValidLowercaseRegister(register: string): boolean {
return /^[a-z]$/.test(register);
}

public static isValidUppercaseRegister(register: string): boolean {
return /^[A-Z]$/.test(register);
}

public static isValidNumberedRegister(register: string): boolean {
return /^[0-9]$/.test(register);
}

public static isValidSpecialRegister(register: string): boolean {
return this.specialRegisters.includes(register);
}

/**
* Puts the content at the specified index of the multicursor Register.
* If multicursorIndex === 0, the register will be completely overwritten. Otherwise, just that index will be.
Expand All @@ -125,7 +136,8 @@ export class Register {
Register.registers.set(register, []);
}

Register.registers.get(register)![multicursorIndex] = {
const registerContent = Register.registers.get(register);
registerContent![multicursorIndex] = {
registerMode: vimState.currentRegisterMode,
text: content,
};
Expand Down Expand Up @@ -310,6 +322,12 @@ export class Register {
return [...Register.registers.keys()];
}

public static getKeysSorted(): string[] {
return this.getKeys().sort(
(reg1: string, reg2: string) => this.sortIndex(reg1) - this.sortIndex(reg2),
);
}

public static clearAllRegisters(): void {
Register.registers.clear();
}
Expand Down Expand Up @@ -355,4 +373,19 @@ export class Register {
Register.registers = new Map();
}
}

private static sortIndex(register: string): number {
const specials = ['-', '*', '+', '.', ':', '%', '#', '/', '='];
if (register === '"') {
return 0;
} else if (register >= '0' && register <= '9') {
return 10 + parseInt(register, 10);
} else if (register >= 'a' && register <= 'z') {
return 100 + (register.charCodeAt(0) - 'a'.charCodeAt(0));
} else if (specials.includes(register)) {
return 1000 + specials.indexOf(register);
} else {
throw new Error(`Unexpected register ${register}`);
}
}
}