diff --git a/src/cmd_line/commands/register.ts b/src/cmd_line/commands/register.ts index 2ccaa36b47a..e2c66d0eeff 100644 --- a/src/cmd_line/commands/register.ts +++ b/src/cmd_line/commands/register.ts @@ -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 = 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 { - 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 { - 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 { - 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(); - - 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(); + + 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? + + 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); + }); } } diff --git a/src/register/register.ts b/src/register/register.ts index 6ae5f9c0447..0f44d6455de 100644 --- a/src/register/register.ts +++ b/src/register/register.ts @@ -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 ]; @@ -80,10 +83,10 @@ 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) ); } @@ -91,7 +94,7 @@ export class Register { return /^[a-zA-Z0-9:]$/.test(register); } - private static isBlackHoleRegister(registerName: string): boolean { + public static isBlackHoleRegister(registerName: string): boolean { return registerName === '_'; } @@ -100,10 +103,10 @@ 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); } @@ -111,6 +114,14 @@ export class Register { 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. @@ -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, }; @@ -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(); } @@ -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}`); + } + } }