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
163 changes: 103 additions & 60 deletions src/cmd_line/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,133 @@ 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 { IPutCommandArguments, 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;
}

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}`);
}
}

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}`);
if (registers.length > 0) {
this.registerKeys = this.registerKeys.filter((r) => registers.includes(r));
}
}

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

quickPick.onDidChangeSelection((items) => {
if (items.length === 0) {
return;
}

void vscode.window.showQuickPick(registerKeyAndContent).then(async (val) => {
if (val) {
const result = val.description;
void vscode.window.showInformationMessage(`${val.label} ${result}`);
RegisterCommand.showRegisterContent(vimState, items[0]);
quickPick.dispose();
});

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

quickPick.show();
}

private static showRegisterContent(vimState: VimState, item: RegisterDisplayItem) {
const paste: vscode.MessageItem = {
title: 'Paste',
isCloseAffordance: false,
};

void vscode.window
.showInformationMessage(`${item.label} ${item.stringContent}`, paste)
.then((action) => {
if (!action || action !== paste) {
return;
}

void RegisterCommand.paste(vimState, item);
});
}

private static async paste(vimState: VimState, item: RegisterDisplayItem) {
// TODO: Can I reuse PutCommand here?
Copy link
Author

Choose a reason for hiding this comment

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

This paste method feels wrong. I don't want to have to reinvent the wheel just to actually paste register content. I would like to reuse existing functionality, but I couldn't figure out the correct way to do that.

What I tried was to create an instance of PutCommand here and pass it the register key as part of its arguments, but somehow that didn't give me the desired result (I had to press ESC after clicking the paste button, for the text to show up).

Perhaps someone can give me some pointers for how to implement this properly.


const content = item.stringContent;
if (content === '') {
return;
}

const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}

vimState.recordedState.registerKey = item.key;

editor.edit((builder) => {
builder.insert(vimState.cursorStopPosition, content);
});
}
}
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}`);
}
}
}