misc: add registers view

This commit is contained in:
Grégoire Geis 2022-03-03 00:03:52 +01:00
parent 042a044936
commit db2fbbfb60
7 changed files with 402 additions and 24 deletions

1
assets/dance-white.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 288 288" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><path id="D" d="M72,250.738l0,-213.476c0,0 144.788,-20.828 143.997,106.738c-0.791,127.566 -143.997,106.738 -143.997,106.738Z" style="fill:none;"/><clipPath id="_clip1"><path d="M72,250.738l0,-213.476c0,0 144.788,-20.828 143.997,106.738c-0.791,127.566 -143.997,106.738 -143.997,106.738Z"/></clipPath><g clip-path="url(#_clip1)"><g id="Lines"><g id="Black-lines" serif:id="Black lines"><g id="L6"><rect id="L1" x="72" y="218.4" width="144" height="25.2" style="fill:url(#_Linear2);"/></g><g id="L5"><rect id="L11" serif:id="L1" x="72" y="183.6" width="144" height="25.2" style="fill:url(#_Linear3);"/></g><g id="L4"><rect id="L12" serif:id="L1" x="72" y="148.8" width="144" height="25.2" style="fill:url(#_Linear4);"/></g><g id="L3"><rect id="L13" serif:id="L1" x="72" y="114" width="144" height="25.2" style="fill:url(#_Linear5);"/></g><g id="L2"><rect id="L14" serif:id="L1" x="72" y="79.2" width="144" height="25.2" style="fill:url(#_Linear6);"/></g><g id="L15" serif:id="L1"><rect id="L16" serif:id="L1" x="72" y="44.4" width="144" height="25.2" style="fill:url(#_Linear7);"/></g></g><g id="Gaps"><g id="L61" serif:id="L6"><rect x="127.2" y="217.2" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L51" serif:id="L5"><rect x="164.4" y="182.4" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L52" serif:id="L5"><rect x="100.8" y="182.4" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L41" serif:id="L4"><rect x="180" y="147.6" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L42" serif:id="L4"><rect x="112.8" y="147.6" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L21" serif:id="L2"><rect x="144" y="78" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L22" serif:id="L2"><rect x="93.6" y="78" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g><g id="L17" serif:id="L1"><rect x="120" y="43.2" width="9.6" height="27.6" style="fill:#f2f2f2;"/></g></g></g></g><g id="Highlight"><g id="L53" serif:id="L5"><g><rect x="158.4" y="181.2" width="6" height="30" style="fill:#fff;"/></g><g><rect x="110.4" y="183.6" width="48" height="25.2" style="fill:#fff;"/></g></g><g id="L31" serif:id="L3"><g><rect x="216" y="111.6" width="6" height="30" style="fill:#fff;"/></g><g><rect x="72" y="114" width="144" height="25.2" style="fill:#fff;"/></g></g></g></g><defs><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,238.414)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient><linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,203.614)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient><linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,168.814)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient><linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,134.014)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient><linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,99.2144)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient><linearGradient id="_Linear7" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(288,0,0,40.0288,72,64.4144)"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

View File

@ -666,6 +666,28 @@ export const pkg = (modules: Builder.ParsedModule[]) => ({
},
},
// Views.
// ========================================================================
viewsContainers: {
activitybar: [
{
id: "dance",
title: "Dance",
icon: "assets/dance-white.svg",
},
],
},
views: {
dance: [
{
id: "registers",
name: "Registers",
},
],
},
// Commands.
// ========================================================================

17
package.json generated
View File

@ -867,6 +867,23 @@
}
}
},
"viewsContainers": {
"activitybar": [
{
"id": "dance",
"title": "Dance",
"icon": "assets/dance-white.svg"
}
]
},
"views": {
"dance": [
{
"id": "registers",
"name": "Registers"
}
]
},
"commands": [
{
"command": "dance.dev.copyLastErrorMessage",

View File

@ -11,6 +11,7 @@ import { extensionName } from "../utils/constants";
import { AutoDisposable } from "../utils/disposables";
import { assert, CancellationError } from "../utils/errors";
import { SettingsValidator } from "../utils/settings-validator";
import { RegistersView } from "./registers-view";
// ===============================================================================================
// == EXTENSION ================================================================================
@ -167,6 +168,9 @@ export class Extension implements vscode.Disposable {
for (const descriptor of Object.values(commands)) {
this._subscriptions.push(descriptor.register(this));
}
// Render views.
this._subscriptions.push(new RegistersView(this.registers).register());
}
/**

244
src/state/registers-view.ts Normal file
View File

@ -0,0 +1,244 @@
import * as vscode from "vscode";
import { Register, Registers, RegisterSet } from "./registers";
/**
* A {@link vscode.TreeDataProvider} for Dance registers.
*/
export class RegistersView implements vscode.TreeDataProvider<TreeItem> {
private readonly _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
private readonly _registerToItemMap = new Map<Register, RegisterTreeItem>();
private readonly _documentToItemMap = new Map<vscode.TextDocument, RegisterSetTreeItem>();
private _globalDocumentItem?: RegisterSetTreeItem;
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
public get isActive() {
return this._globalDocumentItem !== undefined;
}
public constructor(
public readonly registers: Registers,
) {}
public getTreeItem(element: TreeItem) {
return element;
}
public async getChildren(element?: TreeItem): Promise<vscode.TreeItem[]> {
if (element === undefined) {
if (this._globalDocumentItem === undefined) {
this._globalDocumentItem = new RegisterSetTreeItem(
this.registers,
() => this._onDidChangeTreeData.fire(this._globalDocumentItem),
);
}
const document = vscode.window.activeTextEditor?.document;
return document === undefined
? [this._globalDocumentItem]
: [this._globalDocumentItem, this._itemForDocument(document)];
}
if (element instanceof ValueTreeItem) {
return [];
}
if (element instanceof RegisterTreeItem) {
return await element.values();
}
let registers = [...element.registers.registers]
.filter((r) => r.iconName !== undefined && r.canRead());
if (element !== this._globalDocumentItem) {
registers = registers.filter((r) => /^[a-zA-Z]|.{2,}$/.test(r.name));
}
const items = registers
.sort((a, b) => a.name.localeCompare(b.name))
.map((r) => this._itemForRegister(r as Register & Register.Readable, element));
const shouldShowItem = await Promise.all(items.map(async (item) => await item.shouldShow()));
return items.filter((_, i) => shouldShowItem[i]);
}
private _itemForDocument(document: vscode.TextDocument) {
const existing = this._documentToItemMap.get(document);
if (existing !== undefined) {
return existing;
}
const item: RegisterSetTreeItem = new RegisterSetTreeItem(
this.registers.forDocument(document),
() => this._onDidChangeTreeData.fire(item),
document,
);
this._documentToItemMap.set(document, item);
return item;
}
private _itemForRegister(register: Register & Register.Readable, documentItem: RegisterSetTreeItem) {
const existing = this._registerToItemMap.get(register);
if (existing !== undefined) {
return existing;
}
const item: RegisterTreeItem = new RegisterTreeItem(
register,
async (wasShown) => {
this._onDidChangeTreeData.fire(item);
if (await item.shouldShow() !== wasShown) {
this._onDidChangeTreeData.fire(documentItem);
}
},
);
this._registerToItemMap.set(register, item);
return item;
}
public register(): vscode.Disposable {
const treeDataProviderDisposable = vscode.window.createTreeView("registers", {
treeDataProvider: this,
showCollapseAll: true,
}),
editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(() =>
this.isActive && this._onDidChangeTreeData.fire(undefined),
),
documentChangeDisposable = vscode.workspace.onDidCloseTextDocument((e) => {
if (!this.isActive) {
return;
}
this._documentToItemMap.get(e)?.dispose();
this._documentToItemMap.delete(e);
for (const register of this.registers.forDocument(e).registers) {
this._registerToItemMap.get(register)?.dispose();
this._registerToItemMap.delete(register);
}
});
return {
dispose: () => {
for (const item of this._documentToItemMap.values()) {
item.dispose()
}
for (const item of this._registerToItemMap.values()) {
item.dispose();
}
this._globalDocumentItem?.dispose();
this._documentToItemMap.clear();
this._registerToItemMap.clear();
treeDataProviderDisposable.dispose();
editorChangeDisposable.dispose();
documentChangeDisposable.dispose();
},
};
}
}
class RegisterSetTreeItem extends vscode.TreeItem implements vscode.Disposable {
private readonly _disposable: vscode.Disposable;
public constructor(
public readonly registers: RegisterSet,
notifyChange: () => void,
document?: vscode.TextDocument,
) {
super(
document?.fileName ?? "Global",
vscode.TreeItemCollapsibleState.Expanded,
);
this.iconPath = new vscode.ThemeIcon(document === undefined ? "root-folder" : "folder-active");
this._disposable = registers.onRegisterChange(() => notifyChange());
}
public dispose() {
this._disposable.dispose();
}
}
class RegisterTreeItem extends vscode.TreeItem implements vscode.Disposable {
private readonly _disposable: vscode.Disposable;
private _shouldShow?: Thenable<boolean>;
private _values?: Thenable<ValueTreeItem[]>;
public constructor(
public readonly register: Register & Register.Readable,
notifyChange: (wasVisible: boolean) => void,
) {
super(
register.name,
/^(["/@^|.]|[a-zA-Z0-9]+)$/.test(register.name)
? vscode.TreeItemCollapsibleState.Expanded
: vscode.TreeItemCollapsibleState.Collapsed,
);
if (register.iconName !== undefined) {
this.iconPath = new vscode.ThemeIcon(register.iconName);
}
this._disposable = register.onChange(async () => {
const wasVisible = await this.shouldShow();
this._shouldShow = undefined;
this._values = undefined;
notifyChange(wasVisible);
});
}
private _shouldShowEmpty() {
return /^["/@^|.]$/.test(this.register.name);
}
public async shouldShow() {
if (this._shouldShowEmpty()) {
return true;
}
if (this._shouldShow === undefined) {
this._shouldShow = this.values().then((values) => values.length > 0);
}
return await this._shouldShow;
}
public async values() {
if (this._values === undefined) {
this._values = this.register.get()
.then((values) => values?.map((v) => new ValueTreeItem(v)) ?? []);
}
return await this._values;
}
public dispose() {
this._disposable.dispose();
}
}
class ValueTreeItem extends vscode.TreeItem {
public constructor(label: string) {
super(label, vscode.TreeItemCollapsibleState.None);
this.iconPath = new vscode.ThemeIcon("symbol-string");
}
}
type TreeItem = RegisterSetTreeItem | RegisterTreeItem | ValueTreeItem;

View File

@ -10,16 +10,34 @@ import type * as TrackedSelection from "../utils/tracked-selection";
* The base class for all registers.
*/
export abstract class Register {
private readonly _onChangeEvent = new vscode.EventEmitter<Register.ChangeKind>();
/**
* The name of the register.
*/
public readonly abstract name: string;
/**
* The name of the icon of the register, as seen in the [VS Code product icon
* reference](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing).
*
* `undefined` if the register shouldn't be displayed.
*/
public readonly abstract iconName: string | undefined;
/**
* The flags of the register, which define what a register can do.
*/
public readonly abstract flags: Register.Flags;
/**
* Event fired when the contents, selections or recording of the register
* change.
*/
public get onChange() {
return this._onChangeEvent.event;
}
/**
* Returns whether the register is readable.
*/
@ -146,6 +164,13 @@ export abstract class Register {
return this as any;
}
/**
* Notifies listeners that a {@link onChange change} occured to the register.
*/
protected notifyChange(kind: Register.ChangeKind) {
this._onChangeEvent.fire(kind);
}
}
export declare namespace Register {
@ -210,6 +235,12 @@ export declare namespace Register {
getRecording(): Recording | undefined;
setRecording(recording: Recording): void;
}
export const enum ChangeKind {
Contents,
Selections,
Recording,
}
}
/**
@ -232,12 +263,14 @@ class GeneralPurposeRegister extends Register implements Register.Readable,
public constructor(
public readonly name: string,
public readonly iconName: string,
) {
super();
}
public set(values: readonly string[]) {
this._values = values;
this.notifyChange(Register.ChangeKind.Contents);
return Promise.resolve();
}
@ -258,6 +291,7 @@ class GeneralPurposeRegister extends Register implements Register.Readable,
const previousSelectionSet = this._selections;
this._selections = trackedSelections;
this.notifyChange(Register.ChangeKind.Selections);
return previousSelectionSet;
}
@ -268,6 +302,7 @@ class GeneralPurposeRegister extends Register implements Register.Readable,
public setRecording(recording: Recording) {
this._recording = recording;
this.notifyChange(Register.ChangeKind.Recording);
}
}
@ -280,10 +315,20 @@ class SpecialRegister extends Register implements Register.Readable,
? Register.Flags.CanRead
: Register.Flags.CanRead | Register.Flags.CanWrite;
public override get onChange(): vscode.Event<Register.ChangeKind> {
if (this._listenToChanges === undefined) {
return super.onChange;
}
return (listener) => this._listenToChanges!(() => listener(Register.ChangeKind.Contents));
}
public constructor(
public readonly name: string,
public readonly iconName: string | undefined,
public readonly getter: () => Thenable<readonly string[]>,
public readonly setter?: (values: readonly string[]) => Thenable<void>,
private readonly _listenToChanges?: (fire: () => void) => vscode.Disposable,
) {
super();
}
@ -292,12 +337,14 @@ class SpecialRegister extends Register implements Register.Readable,
return this.getter();
}
public set(values: readonly string[]) {
public async set(values: readonly string[]) {
if (this.setter === undefined) {
throw new Error("cannot set read-only register");
}
return this.setter(values);
await this.setter(values);
this.notifyChange(Register.ChangeKind.Contents);
}
}
@ -310,6 +357,7 @@ class ClipboardRegister extends Register implements Register.Readable,
private _lastRawText?: string;
public readonly name = '"';
public readonly iconName = "clippy";
public readonly flags = Register.Flags.CanRead | Register.Flags.CanWrite;
public get() {
@ -327,6 +375,7 @@ class ClipboardRegister extends Register implements Register.Readable,
this._lastStrings = values;
this._lastRawText = values.join(newline);
this.notifyChange(Register.ChangeKind.Contents);
return vscode.env.clipboard.writeText(this._lastRawText);
}
@ -343,19 +392,43 @@ function activeEditor() {
/**
* A set of registers.
*/
export abstract class RegisterSet {
export abstract class RegisterSet implements vscode.Disposable {
private readonly _onRegisterChange
= new vscode.EventEmitter<{ register: Register; kind: "added" | "removed"; }>();
private readonly _onLastMatchesChange = new vscode.EventEmitter<void>();
private readonly _named = new Map<string, Register>();
private readonly _letters = Array.from(
{ length: 26 },
(_, i) => new GeneralPurposeRegister(String.fromCharCode(97 + i)) as Register,
(_, i) => new GeneralPurposeRegister(String.fromCharCode(97 + i), "symbol-text") as Register,
);
private readonly _digits = Array.from(
{ length: 10 },
(_, i) => new SpecialRegister((i + 1).toString(), () => Promise.resolve(this._lastMatches[i])),
{ length: 9 },
(_, i) => new SpecialRegister(
(i + 1).toString(),
"regex",
() => Promise.resolve(this._lastMatches[i]),
undefined,
(fire) => this._onLastMatchesChange.event(fire),
),
);
private _lastMatches: readonly (readonly string[])[] = [];
/**
* The set of registers.
*/
public get registers() {
return new Set(this._named.values());
}
/**
* Event fired when a change to the set occurs.
*/
public get onRegisterChange() {
return this._onRegisterChange.event;
}
/**
* The '"' (`dquote`) register, mapped to the system clipboard and default
* register for edit operations.
@ -365,29 +438,30 @@ export abstract class RegisterSet {
/**
* The "/" (`slash`) register, default register for search / regex operations.
*/
public readonly slash = new GeneralPurposeRegister("/");
public readonly slash = new GeneralPurposeRegister("/", "search-view-icon");
/**
* The "@" (`arobase`) register, default register for recordings (aka macros).
*/
public readonly arobase = new GeneralPurposeRegister("@");
public readonly arobase = new GeneralPurposeRegister("@", "record");
/**
* The "^" (`caret`) register, default register for saving selections.
*/
public readonly caret = new GeneralPurposeRegister("^");
public readonly caret = new GeneralPurposeRegister("^", "save");
/**
* The "|" (`pipe`) register, default register for outputs of external
* commands.
*/
public readonly pipe = new GeneralPurposeRegister("|");
public readonly pipe = new GeneralPurposeRegister("|", "console");
/**
* The "%" (`percent`) register, mapped to the name of the current document.
*/
public readonly percent = new SpecialRegister(
"%",
"file",
() => Promise.resolve([activeEditor().document.fileName]),
(values) => {
if (values.length !== 1) {
@ -396,6 +470,7 @@ export abstract class RegisterSet {
return vscode.workspace.openTextDocument(values[0]).then(() => {});
},
(fire) => vscode.window.onDidChangeActiveTextEditor(fire),
);
/**
@ -403,6 +478,7 @@ export abstract class RegisterSet {
*/
public readonly dot = new SpecialRegister(
".",
"selection",
() => {
const editor = activeEditor(),
document = editor.document,
@ -432,20 +508,25 @@ export abstract class RegisterSet {
}
}, noUndoStops).then((succeeded) => EditNotAppliedError.throwIfNotApplied(succeeded));
},
(fire) => vscode.window.onDidChangeTextEditorSelection(fire),
);
/**
* The read-only "#" (`hash`) register, mapped to the indices of the current
* selections.
*/
public readonly hash = new SpecialRegister("#", () =>
Promise.resolve(activeEditor().selections.map((_, i) => i.toString())),
public readonly hash = new SpecialRegister("#", "symbol-numeric",
() => Promise.resolve(activeEditor().selections.map((_, i) => i.toString())),
undefined,
(fire) => vscode.window.onDidChangeTextEditorSelection(fire),
);
/**
* The read-only "_" (`underscore`) register, mapped to an empty string.
*/
public readonly underscore = new SpecialRegister("_", () => Promise.resolve([""]));
public readonly underscore = new SpecialRegister("_", undefined, () =>
Promise.resolve([""]),
);
/**
* The ":" (`colon`) register.
@ -453,7 +534,7 @@ export abstract class RegisterSet {
* In Kakoune it is mapped to the last entered command, but since we don't
* have access to that information in Dance, we map it to a prompt.
*/
public readonly colon = new SpecialRegister(":", () =>
public readonly colon = new SpecialRegister(":", undefined, () =>
prompt({ prompt: ":" }).then((result) => [result]),
);
@ -463,6 +544,7 @@ export abstract class RegisterSet {
*/
public readonly null = new SpecialRegister(
"null",
undefined,
() => Promise.resolve([]),
() => Promise.resolve(),
);
@ -484,10 +566,25 @@ export abstract class RegisterSet {
this._named.set(longName, register);
}
for (let i = 0; i < this._digits.length; i++) {
this._named.set(`${i + 1}`, this._digits[i]);
}
for (let i = 0; i < this._letters.length; i++) {
const letter = this._letters[i];
this._named.set(String.fromCharCode(i + 65), letter);
this._named.set(String.fromCharCode(i + 97), letter);
}
this._named.set("", this.null);
this._named.set("null", this.null);
}
public dispose() {
this._onRegisterChange.dispose();
}
/**
* Returns the register with the given name or identified by the given key if
* the input is one-character long.
@ -520,14 +617,6 @@ export abstract class RegisterSet {
return this.colon;
default:
if (charCode >= 97 /* a */ && charCode <= 122 /* z */) {
return this._letters[charCode - 97];
}
if (charCode >= 65 /* A */ && charCode <= 90 /* Z */) {
return this._letters[charCode - 65];
}
if (charCode >= 49 /* 1 */ && charCode <= 57 /* 9 */) {
return this._digits[charCode - 49];
}
@ -536,10 +625,10 @@ export abstract class RegisterSet {
key = key.toLowerCase();
let register = this._named.get(key.toLowerCase());
let register = this._named.get(key);
if (register === undefined) {
this._named.set(key, register = new GeneralPurposeRegister(key));
this._named.set(key, register = new GeneralPurposeRegister(key, "symbol-text"));
}
return register;
@ -568,6 +657,7 @@ export abstract class RegisterSet {
}
this._lastMatches = transposed;
this._onLastMatchesChange.fire();
}
}