From e184d3c149cb175b2dc924c3d14a77be30b930f4 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Mon, 24 Aug 2020 13:17:56 -0700 Subject: [PATCH 01/27] Support passing of "when" with key listener --- src/keyListener.ts | 14 +++++++++++--- src/menu/menu.ts | 11 +++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/keyListener.ts b/src/keyListener.ts index b912e2e..29503c7 100644 --- a/src/keyListener.ts +++ b/src/keyListener.ts @@ -1,12 +1,20 @@ import { EventEmitter } from "vscode"; +export interface KeybindingArgs { + key: string, + when?: string, +} + export default class KeyListener { - emitter: EventEmitter; + emitter: EventEmitter; constructor() { - this.emitter = new EventEmitter(); + this.emitter = new EventEmitter(); } - trigger(key: string) { + trigger(key: string | KeybindingArgs) { + if (typeof key === "string") { + key = { key }; + } this.emitter.fire(key); } diff --git a/src/menu/menu.ts b/src/menu/menu.ts index b3d839c..f3a1b62 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -26,6 +26,8 @@ export class WhichKeyMenu { // This is the currently entered value in delay mode // so we can display the chain of keys that's been entered private enteredValue = ''; + // This used to stored the last when condition from the key listener + private when?: string; constructor(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, delay: number, title?: string) { this.keyListener = keyListener; @@ -48,12 +50,13 @@ export class WhichKeyMenu { this.itemHistory = []; } - private onDidKeyPressed(value: string) { - this.quickPick.value += value; - this.onDidChangeValue(this.quickPick.value); + private onDidKeyPressed(value: KeybindingArgs) { + this.quickPick.value += value.key; + this.onDidChangeValue(this.quickPick.value, value.when); } - private async onDidChangeValue(value: string) { + private async onDidChangeValue(value: string, when?: string) { + this.when = when; if (this.timeoutId) { // When the menu is in the delay display mode if (value.startsWith(this.enteredValue)) { From 2ba3e9eb4c100fcc91f19b63cac20413a0eedc08 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Mon, 24 Aug 2020 16:21:48 -0700 Subject: [PATCH 02/27] Implement conditionals type --- src/bindingItem.ts | 25 +++++++++++++++++++++++ src/constants.ts | 3 ++- src/extension.ts | 1 + src/menu/menu.ts | 46 +++++++++++++++++++++++++++++++++--------- src/menu/menuItem.ts | 40 ++++++++++++++++++++++++++++++++++-- src/whichKeyCommand.ts | 16 +++------------ 6 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index 227b7e8..32ee739 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -1,8 +1,16 @@ +export const enum ConditionalActionType { + Command = "command", + Commands = "commands", + Bindings = "bindings", + Transient = "transient", +} + export const enum ActionType { Command = "command", Commands = "commands", Bindings = "bindings", Transient = "transient", + Conditionals = "conditionals", } export interface BindingItem { @@ -13,6 +21,7 @@ export interface BindingItem { commands?: string[], args?: any, bindings?: BindingItem[], + conditionals?: ConditionalBindingItem[], } export interface OverrideBindingItem { @@ -24,8 +33,24 @@ export interface OverrideBindingItem { commands?: string[], args?: any, bindings?: BindingItem[], + conditionals?: ConditionalBindingItem[], +} + +export interface ConditionalBindingItem { + type: ConditionalActionType, + command?: string, + commands?: string[], + args?: any, + bindings?: BindingItem[], + condition?: Condition, } +export type Condition = { + when?: string, + languageId?: string, +}; + + export function toBindingItem(o: any) { if (typeof o === "object") { const config = o as Partial; diff --git a/src/constants.ts b/src/constants.ts index b3bb12a..8eb1d8d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,7 +20,8 @@ export enum SortOrder { } export enum ContextKey { - Active = 'whichkeyActive' + Active = 'whichkeyActive', + Visible = 'whichkeyVisible' } export const whichKeyShow = `${contributePrefix}.${CommandKey.Show}`; export const whichKeyRegister = `${contributePrefix}.${CommandKey.Register}`; diff --git a/src/extension.ts b/src/extension.ts index 8d4a7e6..5d69eb5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,6 +41,7 @@ async function showWhichKey(args: any[]) { } function showWhichKeyCommand(args: any[]) { + window.activeTextEditor?.document.languageId // Not awaiting to fix the issue where executing show key which can freeze vim showWhichKey(args).catch(e => window.showErrorMessage(e.toString())); } diff --git a/src/menu/menu.ts b/src/menu/menu.ts index f3a1b62..e469922 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -3,6 +3,7 @@ import { ActionType } from "../bindingItem"; import KeyListener from "../keyListener"; import { setStatusBarMessage } from "../statusBar"; import MenuItem, { convertToMenuLabel } from "./menuItem"; +import { ContextKey, ConfigKey } from "../constants"; export class WhichKeyMenu { private keyListener: KeyListener; @@ -98,8 +99,9 @@ export class WhichKeyMenu { } } - private onDidHide() { + private async onDidHide() { this.clearDelay(); + await setContext(ContextKey.Visible, false); if (!this.isHiding) { // Dispose correctly when it is not manually hiding this.dispose(); @@ -132,6 +134,14 @@ export class WhichKeyMenu { } private async selectAction(item: MenuItem) { + if (item.type === ActionType.Conditionals) { + const languageId = window.activeTextEditor?.document.languageId; + const menu = item.getConditionalMenu(this.when, languageId); + if (menu) { + item = menu; + } + } + if (item.type === ActionType.Command && item.command) { await this.hide(); await executeCommand(item.command, item.args); @@ -145,7 +155,7 @@ export class WhichKeyMenu { } else if (item.type === ActionType.Bindings && item.items) { this.updateState(item.items, false, item.name); this.itemHistory.push(item); - this.show(); + await this.show(); } else if (item.type === ActionType.Transient && item.items) { await this.hide(); // optionally execute command/s before transient @@ -156,7 +166,7 @@ export class WhichKeyMenu { } this.updateState(item.items, true, item.name); this.itemHistory.push(item); - this.show(); + await this.show(); } else { const keyCombo = this.getHistoryString(item.key); throw new ActionError(item.type, keyCombo); @@ -166,6 +176,14 @@ export class WhichKeyMenu { private async selectActionTransient(item: MenuItem) { await this.hide(); + if (item.type === ActionType.Conditionals) { + const languageId = window.activeTextEditor?.document.languageId; + const menu = item.getConditionalMenu(this.when, languageId); + if (menu) { + item = menu; + } + } + if (item.type === ActionType.Command && item.command) { await executeCommand(item.command, item.args); } else if (item.type === ActionType.Commands && item.commands) { @@ -187,7 +205,7 @@ export class WhichKeyMenu { throw new ActionError(item.type, keyCombo); } - this.show(); + await this.show(); } @@ -204,7 +222,7 @@ export class WhichKeyMenu { } } - private show() { + private async show() { const updateQuickPick = () => { this.quickPick.busy = false; this.enteredValue = ''; @@ -226,6 +244,7 @@ export class WhichKeyMenu { updateQuickPick(); } + await setContext(ContextKey.Visible, true); this.quickPick.show(); } @@ -237,13 +256,22 @@ export class WhichKeyMenu { this.quickPick.dispose(); } - static show(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, delay: number, title?: string) { - const menu = new WhichKeyMenu(keyListener, items, isTransient, delay, title); - menu.show(); - return menu.promise; + static async show(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, delay: number, title?: string) { + try { + const menu = new WhichKeyMenu(keyListener, items, isTransient, delay, title); + await setContext(ContextKey.Active, true); + await menu.show(); + return await menu.promise; + } finally { + await setContext(ContextKey.Active, false); + } } } +function setContext(key: string, value: any) { + return commands.executeCommand("setContext", key, value); +} + function executeCommand(cmd: string, args: any) { if (Array.isArray(args)) { const arr = args as any[]; diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index c730494..75856ab 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,5 +1,5 @@ import { QuickPickItem } from 'vscode'; -import { ActionType, BindingItem, OverrideBindingItem } from "../bindingItem"; +import { ActionType, BindingItem, OverrideBindingItem, ConditionalBindingItem } from "../bindingItem"; import { SortOrder } from '../constants'; export default class MenuItem implements QuickPickItem { @@ -10,6 +10,7 @@ export default class MenuItem implements QuickPickItem { commands?: string[]; items?: MenuItem[]; args?: any; + private conditionals?: ConditionalBindingItem[]; private constructor(item: { name: string, @@ -18,8 +19,8 @@ export default class MenuItem implements QuickPickItem { bindings?: BindingItem[], command?: string, commands?: string[], - items?: MenuItem[], args?: any, + conditionals?: ConditionalBindingItem[], }) { this.name = item.name; this.key = item.key; @@ -27,6 +28,7 @@ export default class MenuItem implements QuickPickItem { this.command = item.command; this.commands = item.commands; this.args = item.args; + this.conditionals = item.conditionals; if (this.type === ActionType.Bindings && item.bindings) { this.items = MenuItem.createMenuItems(item.bindings); } else if (this.type === ActionType.Transient && item.bindings) { @@ -34,6 +36,39 @@ export default class MenuItem implements QuickPickItem { } } + getConditionalMenu(when?: string, languageId?: string) { + if (this.conditionals) { + let menu = this.conditionals.find(c => { + if (c.condition) { + let result = true; + if (c.condition.when) { + result = result && (c.condition.when === when); + } + if (c.condition.languageId) { + result = result && (c.condition.languageId === languageId); + } + return result; + } + return false; + }); + // Not match, find the first empty condition as else + menu = menu ?? this.conditionals.find(c => c.condition === undefined); + if (menu) { + // Lazy creation of conditional menu + return new MenuItem({ + name: this.name, + key: this.key, + type: menu.type as unknown as ActionType, + command: menu.command, + commands: menu.commands, + args: menu.args, + bindings: menu.bindings, + }); + } + } + return undefined; + } + get label() { return convertToMenuLabel(this.key); } @@ -140,6 +175,7 @@ export default class MenuItem implements QuickPickItem { commands: o.commands, args: o.args, bindings: o.bindings, + conditionals: o.conditionals }); } else { throw new Error('name or type of the override is undefined'); diff --git a/src/whichKeyCommand.ts b/src/whichKeyCommand.ts index 3df1efc..b0ca4fe 100644 --- a/src/whichKeyCommand.ts +++ b/src/whichKeyCommand.ts @@ -4,7 +4,7 @@ import { ConfigKey, ContextKey, contributePrefix, SortOrder } from "./constants" import KeyListener from "./keyListener"; import { WhichKeyMenu } from "./menu/menu"; import MenuItem from "./menu/menuItem"; -import { WhichKeyConfig } from "./whichKeyConfig"; +import { WhichKeyConfig, getFullSection } from "./whichKeyConfig"; export default class WhichKeyCommand { private keyListener: KeyListener; @@ -70,17 +70,7 @@ export default class WhichKeyCommand { } } -function setContext(key: string, value: any) { - return commands.executeCommand("setContext", key, value); -} - - async function showMenu(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, title?: string) { - try { - const delay = workspace.getConfiguration(contributePrefix).get(ConfigKey.Delay) ?? 0; - await setContext(ContextKey.Active, true); - await WhichKeyMenu.show(keyListener, items, isTransient, delay, title); - } finally { - await setContext(ContextKey.Active, false); - } + const delay = workspace.getConfiguration(contributePrefix).get(ConfigKey.Delay) ?? 0; + await WhichKeyMenu.show(keyListener, items, isTransient, delay, title); } \ No newline at end of file From ba48ad43256f94e50bf4bb7224bb88f54e12321b Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Mon, 24 Aug 2020 16:23:09 -0700 Subject: [PATCH 03/27] Add ConfigSections type --- src/whichKeyCommand.ts | 8 ++++---- src/whichKeyConfig.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/whichKeyCommand.ts b/src/whichKeyCommand.ts index b0ca4fe..d53795d 100644 --- a/src/whichKeyCommand.ts +++ b/src/whichKeyCommand.ts @@ -42,10 +42,10 @@ export default class WhichKeyCommand { this.onConfigChangeListener = workspace.onDidChangeConfiguration((e) => { if ( - e.affectsConfiguration(`${contributePrefix}.${ConfigKey.SortOrder}`) || - e.affectsConfiguration(`${config.bindings[0]}.${config.bindings[1]}`) || - (config.overrides && e.affectsConfiguration(`${config.overrides[0]}.${config.overrides[1]}`)) - ) { + e.affectsConfiguration(getFullSection([contributePrefix, ConfigKey.SortOrder])) || + e.affectsConfiguration(getFullSection(config.bindings)) || + (config.overrides && e.affectsConfiguration(getFullSection(config.overrides))) + ) { this.register(config); } }, this); diff --git a/src/whichKeyConfig.ts b/src/whichKeyConfig.ts index 52ba6ed..10933cd 100644 --- a/src/whichKeyConfig.ts +++ b/src/whichKeyConfig.ts @@ -1,11 +1,14 @@ import { ConfigKey, contributePrefix } from "./constants"; +type ConfigSections = [string, string]; + export interface WhichKeyConfig { - bindings: [string, string], - overrides?: [string, string], + bindings: ConfigSections, + overrides?: ConfigSections, title?: string, } + export function toWhichKeyConfig(o: any) { if (typeof o === "object") { const config = o as Partial; @@ -19,7 +22,7 @@ export function toWhichKeyConfig(o: any) { return undefined; } -export function getFullSection(sections: [string, string]) { +export function getFullSection(sections: ConfigSections) { return `${sections[0]}.${sections[1]}`; } From 0cd6fb66e869bdd3359210b03c0b264118e80619 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Mon, 24 Aug 2020 16:43:31 -0700 Subject: [PATCH 04/27] Add optional name in ConditionalBindingItem --- src/bindingItem.ts | 1 + src/menu/menuItem.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index 32ee739..c38b7f5 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -38,6 +38,7 @@ export interface OverrideBindingItem { export interface ConditionalBindingItem { type: ConditionalActionType, + name?: string, command?: string, commands?: string[], args?: any, diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 75856ab..8e2f09b 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -56,7 +56,7 @@ export default class MenuItem implements QuickPickItem { if (menu) { // Lazy creation of conditional menu return new MenuItem({ - name: this.name, + name: menu.name ?? this.name, key: this.key, type: menu.type as unknown as ActionType, command: menu.command, From be2d670f103182b10a1938abc4a67f1fead418f7 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 15:37:29 -0700 Subject: [PATCH 05/27] Refactoring to a inheritance model for menu items --- src/bindingItem.ts | 12 +- src/menu/menu.ts | 116 +++++------ src/menu/menuItem.ts | 449 +++++++++++++++++++++++++++-------------- src/whichKeyCommand.ts | 37 ++-- 4 files changed, 374 insertions(+), 240 deletions(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index c38b7f5..5288eaa 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -1,10 +1,3 @@ -export const enum ConditionalActionType { - Command = "command", - Commands = "commands", - Bindings = "bindings", - Transient = "transient", -} - export const enum ActionType { Command = "command", Commands = "commands", @@ -13,6 +6,11 @@ export const enum ActionType { Conditionals = "conditionals", } +export type ConditionalActionType = ActionType.Bindings + | ActionType.Command + | ActionType.Commands + | ActionType.Transient; + export interface BindingItem { key: string; name: string; diff --git a/src/menu/menu.ts b/src/menu/menu.ts index e469922..3e4a31b 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -1,20 +1,19 @@ import { commands, Disposable, QuickPick, window } from "vscode"; -import { ActionType } from "../bindingItem"; -import KeyListener from "../keyListener"; +import { ContextKey } from "../constants"; +import KeyListener, { KeybindingArgs } from "../keyListener"; import { setStatusBarMessage } from "../statusBar"; -import MenuItem, { convertToMenuLabel } from "./menuItem"; -import { ContextKey, ConfigKey } from "../constants"; +import { BaseMenuItem, convertToMenuLabel } from "./menuItem"; export class WhichKeyMenu { private keyListener: KeyListener; - private items: MenuItem[]; + private items: BaseMenuItem[]; private title?: string; private isTransient: boolean; - private quickPick: QuickPick; + private quickPick: QuickPick; private disposables: Disposable[]; private isHiding: boolean; - private itemHistory: MenuItem[]; + private itemHistory: BaseMenuItem[]; // Promise related properties for the promise returned by show() private promise: Promise; @@ -30,13 +29,13 @@ export class WhichKeyMenu { // This used to stored the last when condition from the key listener private when?: string; - constructor(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, delay: number, title?: string) { + constructor(keyListener: KeyListener, items: BaseMenuItem[], isTransient: boolean, delay: number, title?: string) { this.keyListener = keyListener; this.items = items; this.isTransient = isTransient; this.delay = delay; this.title = title; - this.quickPick = window.createQuickPick(); + this.quickPick = window.createQuickPick(); this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; @@ -51,6 +50,14 @@ export class WhichKeyMenu { this.itemHistory = []; } + private get condition() { + const languageId = window.activeTextEditor?.document.languageId; + return { + when: this.when, + languageId + }; + } + private onDidKeyPressed(value: KeybindingArgs) { this.quickPick.value += value.key; this.onDidChangeValue(this.quickPick.value, value.when); @@ -94,7 +101,7 @@ export class WhichKeyMenu { private onDidAccept() { if (this.quickPick.activeItems.length > 0) { - const chosenItems = this.quickPick.activeItems[0] as MenuItem; + const chosenItems = this.quickPick.activeItems[0] as BaseMenuItem; this.select(chosenItems); } } @@ -122,7 +129,7 @@ export class WhichKeyMenu { }); } - private async select(item: MenuItem) { + private async select(item: BaseMenuItem) { try { await ((this.isTransient) ? this.selectActionTransient(item) @@ -133,83 +140,60 @@ export class WhichKeyMenu { } } - private async selectAction(item: MenuItem) { - if (item.type === ActionType.Conditionals) { - const languageId = window.activeTextEditor?.document.languageId; - const menu = item.getConditionalMenu(this.when, languageId); - if (menu) { - item = menu; - } - } - - if (item.type === ActionType.Command && item.command) { + private async selectAction(item: BaseMenuItem) { + const result = item.select(this.condition); + if (result.commands && !result.items) { + // Commands only, hide, execute and dispose await this.hide(); - await executeCommand(item.command, item.args); + await executeCommands(result.commands, result.args); this.dispose(); this.resolve(); - } else if (item.type === ActionType.Commands && item.commands) { - await this.hide(); - await executeCommands(item.commands, item.args); - this.dispose(); - this.resolve(); - } else if (item.type === ActionType.Bindings && item.items) { - this.updateState(item.items, false, item.name); + } else if (!result.commands && result.items) { + // Bindings only, update and show + this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); await this.show(); - } else if (item.type === ActionType.Transient && item.items) { + } else if (result.commands && result.items) { + // Have both bindings and commands + // Hide execute, and await this.hide(); - // optionally execute command/s before transient - if (item.commands) { - await executeCommands(item.commands, item.args); - } else if (item.command) { - await executeCommand(item.command, item.args); - } - this.updateState(item.items, true, item.name); + await executeCommands(result.commands, result.args); + this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); await this.show(); } else { const keyCombo = this.getHistoryString(item.key); - throw new ActionError(item.type, keyCombo); + throw new ActionError(keyCombo); } } - private async selectActionTransient(item: MenuItem) { + private async selectActionTransient(item: BaseMenuItem) { await this.hide(); - if (item.type === ActionType.Conditionals) { - const languageId = window.activeTextEditor?.document.languageId; - const menu = item.getConditionalMenu(this.when, languageId); - if (menu) { - item = menu; - } - } - - if (item.type === ActionType.Command && item.command) { - await executeCommand(item.command, item.args); - } else if (item.type === ActionType.Commands && item.commands) { - await executeCommands(item.commands, item.args); - } else if (item.type === ActionType.Bindings && item.items) { - this.updateState(item.items, false, item.name); + const result = item.select(this.condition); + if (result.commands && !result.items) { + await executeCommands(result.commands, result.args); + } else if (!result.commands && result.items) { + // Bindings only, update and show + this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); - } else if (item.type === ActionType.Transient && item.items) { - // optionally execute command/s before transient - if (item.commands) { - await executeCommands(item.commands, item.args); - } else if (item.command) { - await executeCommand(item.command, item.args); - } - this.updateState(item.items, true, item.name); + } else if (result.commands && result.items) { + // Have both bindings and commands + // Hide execute, and + await this.hide(); + await executeCommands(result.commands, result.args); + this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); } else { const keyCombo = this.getHistoryString(item.key); - throw new ActionError(item.type, keyCombo); + throw new ActionError(keyCombo); } await this.show(); } - private updateState(items: MenuItem[], isTransient: boolean, title?: string) { + private updateState(items: BaseMenuItem[], isTransient: boolean, title?: string) { this.items = items; this.isTransient = isTransient; this.title = title; @@ -256,7 +240,7 @@ export class WhichKeyMenu { this.quickPick.dispose(); } - static async show(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, delay: number, title?: string) { + static async show(keyListener: KeyListener, items: BaseMenuItem[], isTransient: boolean, delay: number, title?: string) { try { const menu = new WhichKeyMenu(keyListener, items, isTransient, delay, title); await setContext(ContextKey.Active, true); @@ -294,7 +278,7 @@ async function executeCommands(cmds: string[], args: any) { } class ActionError extends Error { - constructor(itemType: string, keyCombo: string) { - super(`Incorrect properties for ${itemType} type with the key combination of ${keyCombo}`); + constructor(keyCombo: string) { + super(`Failed to select key with a combination of ${keyCombo}`); } } \ No newline at end of file diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 8e2f09b..99d19aa 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,188 +1,341 @@ -import { QuickPickItem } from 'vscode'; -import { ActionType, BindingItem, OverrideBindingItem, ConditionalBindingItem } from "../bindingItem"; -import { SortOrder } from '../constants'; +import { QuickPickItem } from "vscode"; +import { Condition, OverrideBindingItem, BindingItem, ActionType } from "../bindingItem"; +import { SortOrder } from "../constants"; -export default class MenuItem implements QuickPickItem { - name: string; +export interface MenuSelectionResult { + items?: BaseMenuItem[], + isTransient?: boolean, + commands?: string[], + args?: any, +} + +export abstract class BaseMenuItem implements QuickPickItem { key: string; - type: ActionType; - command?: string; - commands?: string[]; - items?: MenuItem[]; - args?: any; - private conditionals?: ConditionalBindingItem[]; - - private constructor(item: { - name: string, - key: string, - type: ActionType, - bindings?: BindingItem[], - command?: string, - commands?: string[], - args?: any, - conditionals?: ConditionalBindingItem[], - }) { - this.name = item.name; - this.key = item.key; - this.type = item.type; - this.command = item.command; - this.commands = item.commands; - this.args = item.args; - this.conditionals = item.conditionals; - if (this.type === ActionType.Bindings && item.bindings) { - this.items = MenuItem.createMenuItems(item.bindings); - } else if (this.type === ActionType.Transient && item.bindings) { - this.items = MenuItem.createMenuItems(item.bindings); + name: string; + constructor(key: string, name: string) { + this.key = key; + this.name = name; + } + + get label() { + return convertToMenuLabel(this.key); + } + + get description() { + // Add tab so the description is aligned + return `\t${this.name}`; + } + + abstract select(args?: Condition): MenuSelectionResult; + sortItems(_order: SortOrder): void { + // Nothing to sort + } + + get(_id: string): BaseMenuItem | undefined { + return undefined; + } + + overrideItem(_overrides: OverrideBindingItem) { + // Nothing to override by default + } +} + +abstract class BaseCollectionMenuItem extends BaseMenuItem { + protected items: BaseMenuItem[]; + + constructor(key: string, name: string, items: BaseMenuItem[]) { + super(key, name); + this.items = items; + } + + sortItems(order: SortOrder): void { + sortMenuItems(this.items, order); + for (const item of this.items) { + item.sortItems(order); } } - getConditionalMenu(when?: string, languageId?: string) { - if (this.conditionals) { - let menu = this.conditionals.find(c => { - if (c.condition) { - let result = true; - if (c.condition.when) { - result = result && (c.condition.when === when); - } - if (c.condition.languageId) { - result = result && (c.condition.languageId === languageId); - } - return result; + get(id: string) { + return this.items.find(i => i.key === id); + } + + overrideItem(o: OverrideBindingItem) { + const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; + const key = keys[keys.length - 1]; + const index = this.items.findIndex(i => i.key === key); + + if (o.position === undefined) { + const newItem = createMenuItemFromOverrides(o, key); + if (index !== -1) { + // replace the current item + this.items.splice(index, 1, newItem); + } else { + // append if there isn't an existing binding + this.items.push(newItem); + } + } else { + if (o.position < 0) { + // negative position, attempt to remove + if (index !== -1) { + this.items.splice(index, 1); } - return false; - }); - // Not match, find the first empty condition as else - menu = menu ?? this.conditionals.find(c => c.condition === undefined); - if (menu) { - // Lazy creation of conditional menu - return new MenuItem({ - name: menu.name ?? this.name, - key: this.key, - type: menu.type as unknown as ActionType, - command: menu.command, - commands: menu.commands, - args: menu.args, - bindings: menu.bindings, - }); + } else { + // Remove and replace + if (index !== -1) { + this.items.splice(index, 1); + } + const newItem = createMenuItemFromOverrides(o, key); + this.items.splice(o.position, 0, newItem); } } - return undefined; } +} - get label() { - return convertToMenuLabel(this.key); +class BindingsMenuItem extends BaseCollectionMenuItem { + constructor(item: BindingItem) { + if (!item.bindings) { + throw new Error("Property bindings is not defined for type bindings"); + } + const items = createMenuItems(item.bindings); + super(item.key, item.name, items); } - get description() { - // Add tab so the description is aligned - return `\t${this.name}`; + select(_args?: Condition): MenuSelectionResult { + return { + items: this.items, + isTransient: false, + }; } +} + +class CommandsMenuItem extends BaseMenuItem { + private commands: string[]; + private args?: any; + + constructor(item: BindingItem) { + if (!item.commands) { + throw new Error("Property commands is not defined for type commands"); + } - static createMenuItems(bindingItems: BindingItem[]) { - return bindingItems.map(i => new MenuItem(i)); + super(item.key, item.name); + this.commands = item.commands; + this.args = item.args; } - static overrideMenuItems( - items?: MenuItem[], - overrides?: OverrideBindingItem[]) { - overrides?.forEach(o => { + select(_args?: Condition): MenuSelectionResult { + return { + commands: this.commands, + args: this.args + }; + } +} + +type ConditionalMenuItem = { + condition: Condition | undefined, + item: BaseMenuItem, +}; + +function evalCondition(item: ConditionalMenuItem, condition?: Condition) { + if (condition && item.condition) { + let result = true; + if (item.condition.when) { + result = result && (item.condition.when === condition.when); + } + if (item.condition.languageId) { + result = result && (item.condition.languageId === condition.languageId); + } + return result; + } + return false; +} + +class ConditionalsMenuItem extends BaseMenuItem { + private conditionalItems: ConditionalMenuItem[]; + + constructor(item: BindingItem) { + super(item.key, item.name); + if (!item.conditionals) { + throw new Error("Property conditionals is not defined for type conditionals"); + } + + this.conditionalItems = item.conditionals.map(c => ({ + condition: c.condition, + item: createMenuItem({ + name: c.name ?? this.name, + key: this.key, + type: c.type as unknown as ActionType, + command: c.command, + commands: c.commands, + args: c.args, + bindings: c.bindings + }) + })); + } + + select(args?: Condition): MenuSelectionResult { + let match = this.conditionalItems.find(c => evalCondition(c, args)); + + // No matches, find the first empty condition as else + match = match ?? this.conditionalItems.find(c => c.condition === undefined); + if (match) { + return match.item.select(args); + } + + throw new Error("No conditions match!"); + } + + sortItems(order: SortOrder): void { + for (const i of this.conditionalItems) { + i.item.sortItems(order); + } + } +} + +class CommandMenuItem extends BaseMenuItem { + private command: string; + private args?: any; + + constructor(item: BindingItem) { + if (!item.command) { + throw new Error("Property command is not defined for type command"); + } + + super(item.key, item.name); + this.command = item.command; + this.args = item.args; + } + + select(_args?: Condition): MenuSelectionResult { + return { + commands: [this.command], + args: this.args ? [this.args] : this.args + }; + } +} + +class TransientMenuItem extends BaseCollectionMenuItem { + private commands?: string[]; + private args?: any; + + constructor(item: BindingItem) { + if (!item.bindings) { + throw new Error("Property bindings is not defined for type transient"); + } + const items = createMenuItems(item.bindings); + + super(item.key, item.name, items); + if (item.commands) { + this.commands = item.commands; + this.args = item.args; + } else if (item.command) { + this.commands = [item.command]; + this.args = [item.args]; + } + } + + select(_args?: Condition): MenuSelectionResult { + return { + items: this.items, + isTransient: true, + commands: this.commands, + args: this.args + }; + } +} + +export class RootMenuItem extends BaseCollectionMenuItem { + constructor(bindings: BindingItem[]) { + const items = createMenuItems(bindings); + super("", "", items); + } + + select(_args?: Condition): MenuSelectionResult { + return { + items: this.items, + isTransient: false, + }; + } + + override(overrides: OverrideBindingItem[]) { + for (const o of overrides) { try { const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; - - // Traverse to the last level - let menuItems = items; + let menuItem: BaseMenuItem | undefined = this; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; - const keyIndex = menuItems?.findIndex(i => i.key === key); - if (keyIndex === undefined || keyIndex === -1) { + menuItem = menuItem?.get(key); + if (menuItem === undefined) { console.warn(`Key ${key} of ${o.keys} not found`); break; } - menuItems = menuItems?.[keyIndex]?.items; - } - - if (menuItems !== undefined) { - const key = keys[keys.length - 1]; - const index = menuItems.findIndex(i => i.key === key); - - if (o.position === undefined) { - const newItem = MenuItem.createFromOverride(o, key); - if (index !== -1) { - // replace the current item - menuItems.splice(index, 1, newItem); - } else { - // append if there isn't an existing binding - menuItems.push(newItem); - } - } else { - if (o.position < 0) { - // negative position, attempt to remove - if (index !== -1) { - menuItems.splice(index, 1); - } - } else { - // Remove and replace - if (index !== -1) { - menuItems.splice(index, 1); - } - const newItem = MenuItem.createFromOverride(o, key); - menuItems.splice(o.position, 0, newItem); - } - } } + menuItem?.overrideItem(o); } catch (e) { console.error(e); } - }); - } - - static sortMenuItems(items: MenuItem[] | undefined, order: SortOrder) { - if (items && order !== SortOrder.None) { - - if (order === SortOrder.Alphabetically) { - items.sort((a, b) => a.key.localeCompare(b.key)); - } else if (order === SortOrder.NonNumberFirst) { - items.sort((a, b) => { - const regex = /^[0-9]/; - const aStartsWithNumber = regex.test(a.key); - const bStartsWithNumber = regex.test(b.key); - if (aStartsWithNumber !== bStartsWithNumber) { - // Sort non-number first - return aStartsWithNumber ? 1 : -1; - } else { - return a.key.localeCompare(b.key); - } - }); - } - - for (const item of items) { - MenuItem.sortMenuItems(item.items, order); - } } } +} - static createFromBinding(item: BindingItem) { - return new MenuItem(item); +function createMenuItem(bindingItem: BindingItem): BaseMenuItem { + switch (bindingItem.type) { + case ActionType.Command: + return new CommandMenuItem(bindingItem); + case ActionType.Commands: + return new CommandsMenuItem(bindingItem); + case ActionType.Bindings: + return new BindingsMenuItem(bindingItem); + case ActionType.Transient: + return new TransientMenuItem(bindingItem); + case ActionType.Conditionals: + return new ConditionalsMenuItem(bindingItem); + default: + throw new Error(`type ${bindingItem.type} not recognized`); } +} - static createFromOverride(o: OverrideBindingItem, key: string) { - if (o.name !== undefined && o.type !== undefined) { - return new MenuItem({ - key: key, - name: o.name, - type: o.type, - command: o.command, - commands: o.commands, - args: o.args, - bindings: o.bindings, - conditionals: o.conditionals +// sort menu item in-place +function sortMenuItems(items: BaseMenuItem[], order: SortOrder) { + if (order !== SortOrder.None) { + if (order === SortOrder.Alphabetically) { + items.sort((a, b) => a.key.localeCompare(b.key)); + } else if (order === SortOrder.NonNumberFirst) { + items.sort((a, b) => { + const regex = /^[0-9]/; + const aStartsWithNumber = regex.test(a.key); + const bStartsWithNumber = regex.test(b.key); + if (aStartsWithNumber !== bStartsWithNumber) { + // Sort non-number first + return aStartsWithNumber ? 1 : -1; + } else { + return a.key.localeCompare(b.key); + } }); - } else { - throw new Error('name or type of the override is undefined'); } } } +function createMenuItemFromOverrides(o: OverrideBindingItem, key: string) { + if (o.name !== undefined && o.type !== undefined) { + return createMenuItem({ + key: key, + name: o.name, + type: o.type, + command: o.command, + commands: o.commands, + args: o.args, + bindings: o.bindings, + conditionals: o.conditionals + }); + } else { + throw new Error('name or type of the override is undefined'); + } +} + +function createMenuItems(bindingItems: BindingItem[]) { + return bindingItems.map(createMenuItem); +} + export function convertToMenuLabel(s: string) { return s.replace(/ /g, '␣').replace(/\t/g, '↹'); } \ No newline at end of file diff --git a/src/whichKeyCommand.ts b/src/whichKeyCommand.ts index d53795d..c369d31 100644 --- a/src/whichKeyCommand.ts +++ b/src/whichKeyCommand.ts @@ -1,14 +1,14 @@ -import { commands, Disposable, workspace } from "vscode"; +import { Disposable, workspace } from "vscode"; import { BindingItem, OverrideBindingItem } from "./bindingItem"; -import { ConfigKey, ContextKey, contributePrefix, SortOrder } from "./constants"; +import { ConfigKey, contributePrefix, SortOrder } from "./constants"; import KeyListener from "./keyListener"; import { WhichKeyMenu } from "./menu/menu"; -import MenuItem from "./menu/menuItem"; -import { WhichKeyConfig, getFullSection } from "./whichKeyConfig"; +import { BaseMenuItem, RootMenuItem } from "./menu/menuItem"; +import { getFullSection, WhichKeyConfig } from "./whichKeyConfig"; export default class WhichKeyCommand { private keyListener: KeyListener; - private items?: MenuItem[]; + private root?: RootMenuItem; private config?: WhichKeyConfig; private onConfigChangeListener?: Disposable; constructor(keyListener: KeyListener) { @@ -23,54 +23,53 @@ export default class WhichKeyCommand { .getConfiguration(config.bindings[0]) .get(config.bindings[1]); if (bindings) { - this.items = MenuItem.createMenuItems(bindings); - } else { - this.items = undefined; + this.root = new RootMenuItem(bindings); } if (config.overrides) { const overrides = workspace .getConfiguration(config.overrides[0]) - .get(config.overrides[1]); - MenuItem.overrideMenuItems(this.items, overrides); + .get(config.overrides[1]) ?? []; + this.root?.override(overrides); } const sortOrder = workspace .getConfiguration(contributePrefix) .get(ConfigKey.SortOrder) ?? SortOrder.None; - MenuItem.sortMenuItems(this.items, sortOrder); + this.root?.sortItems(sortOrder); this.onConfigChangeListener = workspace.onDidChangeConfiguration((e) => { if ( e.affectsConfiguration(getFullSection([contributePrefix, ConfigKey.SortOrder])) || - e.affectsConfiguration(getFullSection(config.bindings)) || - (config.overrides && e.affectsConfiguration(getFullSection(config.overrides))) - ) { + e.affectsConfiguration(getFullSection(config.bindings)) || + (config.overrides && e.affectsConfiguration(getFullSection(config.overrides))) + ) { this.register(config); } }, this); } unregister() { - this.items = undefined; + this.root = undefined; this.onConfigChangeListener?.dispose(); } show() { - if (this.items) { - return showMenu(this.keyListener, this.items, false, this.config?.title); + const items = this.root?.select().items; + if (items) { + return showMenu(this.keyListener, items, false, this.config?.title); } else { throw new Error("No bindings is available"); } } static show(bindings: BindingItem[], keyWatcher: KeyListener) { - const items = MenuItem.createMenuItems(bindings); + const items = new RootMenuItem(bindings).select().items!; return showMenu(keyWatcher, items, false); } } -async function showMenu(keyListener: KeyListener, items: MenuItem[], isTransient: boolean, title?: string) { +async function showMenu(keyListener: KeyListener, items: BaseMenuItem[], isTransient: boolean, title?: string) { const delay = workspace.getConfiguration(contributePrefix).get(ConfigKey.Delay) ?? 0; await WhichKeyMenu.show(keyListener, items, isTransient, delay, title); } \ No newline at end of file From 610b20b7b09f6c3b39282de53d78b219fa92ffa2 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 18:12:07 -0700 Subject: [PATCH 06/27] Traverse through override bindings with keys --- src/menu/menuItem.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 99d19aa..36c9a20 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,6 +1,7 @@ import { QuickPickItem } from "vscode"; import { Condition, OverrideBindingItem, BindingItem, ActionType } from "../bindingItem"; import { SortOrder } from "../constants"; +import { URLSearchParams } from "url"; export interface MenuSelectionResult { items?: BaseMenuItem[], @@ -189,6 +190,59 @@ class ConditionalsMenuItem extends BaseMenuItem { i.item.sortItems(order); } } + + overrideItem(o: OverrideBindingItem) { + const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; + const key = keys[keys.length - 1]; + let condition: Condition | undefined; + if (key && key.length > 0) { + let params = new URLSearchParams(key); + condition = { + when: params.get("when") ?? undefined, + languageId: params.get("languageId") ?? undefined, + }; + } + + const index = this.conditionalItems.findIndex(i => evalCondition(i, condition)); + const createItem = () => ( + { + condition, + item: createMenuItem({ + name: o.name ?? this.name, + key: this.key, + type: o.type as unknown as ActionType, + command: o.command, + commands: o.commands, + args: o.args, + bindings: o.bindings + }) + } + ); + if (o.position === undefined) { + const newItem = createItem(); + if (index !== -1) { + // replace the current item + this.conditionalItems.splice(index, 1, newItem); + } else { + // append if there isn't an existing binding + this.conditionalItems.push(newItem); + } + } else { + if (o.position < 0) { + // negative position, attempt to remove + if (index !== -1) { + this.conditionalItems.splice(index, 1); + } + } else { + // Remove and replace + if (index !== -1) { + this.conditionalItems.splice(index, 1); + } + const newItem = createItem(); + this.conditionalItems.splice(o.position, 0, newItem); + } + } + } } class CommandMenuItem extends BaseMenuItem { From a6d6735ffc9c61653c5a7c1acd3cfab101efc40b Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 21:01:58 -0700 Subject: [PATCH 07/27] Remove useless statment --- src/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 5d69eb5..8d4a7e6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,7 +41,6 @@ async function showWhichKey(args: any[]) { } function showWhichKeyCommand(args: any[]) { - window.activeTextEditor?.document.languageId // Not awaiting to fix the issue where executing show key which can freeze vim showWhichKey(args).catch(e => window.showErrorMessage(e.toString())); } From 7eecab61b94a401fd3baca59370748cfe61d5af0 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 21:02:38 -0700 Subject: [PATCH 08/27] Implement query-string like key as condition --- src/bindingItem.ts | 14 +---------- src/menu/menuItem.ts | 60 +++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index 5288eaa..8d59d86 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -3,7 +3,7 @@ export const enum ActionType { Commands = "commands", Bindings = "bindings", Transient = "transient", - Conditionals = "conditionals", + Conditional = "conditional", } export type ConditionalActionType = ActionType.Bindings @@ -19,7 +19,6 @@ export interface BindingItem { commands?: string[], args?: any, bindings?: BindingItem[], - conditionals?: ConditionalBindingItem[], } export interface OverrideBindingItem { @@ -31,17 +30,6 @@ export interface OverrideBindingItem { commands?: string[], args?: any, bindings?: BindingItem[], - conditionals?: ConditionalBindingItem[], -} - -export interface ConditionalBindingItem { - type: ConditionalActionType, - name?: string, - command?: string, - commands?: string[], - args?: any, - bindings?: BindingItem[], - condition?: Condition, } export type Condition = { diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 36c9a20..57696b0 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,7 +1,6 @@ import { QuickPickItem } from "vscode"; -import { Condition, OverrideBindingItem, BindingItem, ActionType } from "../bindingItem"; +import { ActionType, BindingItem, Condition, OverrideBindingItem } from "../bindingItem"; import { SortOrder } from "../constants"; -import { URLSearchParams } from "url"; export interface MenuSelectionResult { items?: BaseMenuItem[], @@ -150,26 +149,35 @@ function evalCondition(item: ConditionalMenuItem, condition?: Condition) { return false; } +function getCondition(key: string): Condition | undefined { + if (key.length === 0) { + return undefined; + } + + const props = key.split(","); + const r = props.reduce((result, prop) => { + const [key, value] = prop.split(":"); + result[key] = value; + return result; + }, {} as Record); + return { + when: r["when"], + languageId: r["languageId"] + }; +} + class ConditionalsMenuItem extends BaseMenuItem { private conditionalItems: ConditionalMenuItem[]; constructor(item: BindingItem) { super(item.key, item.name); - if (!item.conditionals) { - throw new Error("Property conditionals is not defined for type conditionals"); + if (!item.bindings) { + throw new Error("Property bindings is not defined for type conditional"); } - this.conditionalItems = item.conditionals.map(c => ({ - condition: c.condition, - item: createMenuItem({ - name: c.name ?? this.name, - key: this.key, - type: c.type as unknown as ActionType, - command: c.command, - commands: c.commands, - args: c.args, - bindings: c.bindings - }) + this.conditionalItems = item.bindings.map(b => ({ + condition: getCondition(b.key), + item: createMenuItem(b) })); } @@ -194,28 +202,13 @@ class ConditionalsMenuItem extends BaseMenuItem { overrideItem(o: OverrideBindingItem) { const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; const key = keys[keys.length - 1]; - let condition: Condition | undefined; - if (key && key.length > 0) { - let params = new URLSearchParams(key); - condition = { - when: params.get("when") ?? undefined, - languageId: params.get("languageId") ?? undefined, - }; - } + let condition = getCondition(key); const index = this.conditionalItems.findIndex(i => evalCondition(i, condition)); const createItem = () => ( { condition, - item: createMenuItem({ - name: o.name ?? this.name, - key: this.key, - type: o.type as unknown as ActionType, - command: o.command, - commands: o.commands, - args: o.args, - bindings: o.bindings - }) + item: createMenuItemFromOverrides(o, key) } ); if (o.position === undefined) { @@ -341,7 +334,7 @@ function createMenuItem(bindingItem: BindingItem): BaseMenuItem { return new BindingsMenuItem(bindingItem); case ActionType.Transient: return new TransientMenuItem(bindingItem); - case ActionType.Conditionals: + case ActionType.Conditional: return new ConditionalsMenuItem(bindingItem); default: throw new Error(`type ${bindingItem.type} not recognized`); @@ -379,7 +372,6 @@ function createMenuItemFromOverrides(o: OverrideBindingItem, key: string) { commands: o.commands, args: o.args, bindings: o.bindings, - conditionals: o.conditionals }); } else { throw new Error('name or type of the override is undefined'); From 8899e231e957761fa34a8346a844475c1efdb459 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 23:22:26 -0700 Subject: [PATCH 09/27] Simplify menu selection code --- src/menu/menu.ts | 45 ++++++++++----------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/src/menu/menu.ts b/src/menu/menu.ts index 3e4a31b..61dcc34 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -142,28 +142,18 @@ export class WhichKeyMenu { private async selectAction(item: BaseMenuItem) { const result = item.select(this.condition); - if (result.commands && !result.items) { - // Commands only, hide, execute and dispose - await this.hide(); - await executeCommands(result.commands, result.args); - this.dispose(); - this.resolve(); - } else if (!result.commands && result.items) { - // Bindings only, update and show - this.updateState(result.items, !!result.isTransient, item.name); - this.itemHistory.push(item); - await this.show(); - } else if (result.commands && result.items) { - // Have both bindings and commands - // Hide execute, and + if (result.commands) { await this.hide(); await executeCommands(result.commands, result.args); + } + + if (result.items) { this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); await this.show(); } else { - const keyCombo = this.getHistoryString(item.key); - throw new ActionError(keyCombo); + this.dispose(); + this.resolve(); } } @@ -171,22 +161,13 @@ export class WhichKeyMenu { await this.hide(); const result = item.select(this.condition); - if (result.commands && !result.items) { - await executeCommands(result.commands, result.args); - } else if (!result.commands && result.items) { - // Bindings only, update and show - this.updateState(result.items, !!result.isTransient, item.name); - this.itemHistory.push(item); - } else if (result.commands && result.items) { - // Have both bindings and commands - // Hide execute, and - await this.hide(); + if (result.commands) { await executeCommands(result.commands, result.args); + } + + if (result.items) { this.updateState(result.items, !!result.isTransient, item.name); this.itemHistory.push(item); - } else { - const keyCombo = this.getHistoryString(item.key); - throw new ActionError(keyCombo); } await this.show(); @@ -275,10 +256,4 @@ async function executeCommands(cmds: string[], args: any) { const arg = args?.[i]; await executeCommand(cmd, arg); } -} - -class ActionError extends Error { - constructor(keyCombo: string) { - super(`Failed to select key with a combination of ${keyCombo}`); - } } \ No newline at end of file From c920f82cdb63f87ce0d5fd0cdb4677a631f87e34 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 25 Aug 2020 23:34:24 -0700 Subject: [PATCH 10/27] Add async to onDidAccept --- src/menu/menu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/menu.ts b/src/menu/menu.ts index 61dcc34..bdc8e60 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -99,10 +99,10 @@ export class WhichKeyMenu { return keyCombo.map(convertToMenuLabel).join(' '); } - private onDidAccept() { + private async onDidAccept() { if (this.quickPick.activeItems.length > 0) { const chosenItems = this.quickPick.activeItems[0] as BaseMenuItem; - this.select(chosenItems); + await this.select(chosenItems); } } From e08aae70107043fb7ea5ea6dbeaf1e9dbf511d0f Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Wed, 26 Aug 2020 00:35:05 -0700 Subject: [PATCH 11/27] Fix broken transient This is cause by the timing of setContext in onDidHide method. By the time setContext is done, the isHiding is turned back to true by the hide method; hence, disposed the QuickPick. --- src/menu/menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/menu.ts b/src/menu/menu.ts index bdc8e60..a51eed4 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -108,12 +108,12 @@ export class WhichKeyMenu { private async onDidHide() { this.clearDelay(); - await setContext(ContextKey.Visible, false); if (!this.isHiding) { // Dispose correctly when it is not manually hiding this.dispose(); this.resolve(); } + await setContext(ContextKey.Visible, false); } // Manually hide the menu From eeee24aab48bd9408bd29bc4fef00486d7f2e629 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Wed, 26 Aug 2020 00:38:55 -0700 Subject: [PATCH 12/27] Use vscode api for binding this for callbacks --- src/menu/menu.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/menu/menu.ts b/src/menu/menu.ts index a51eed4..ca3ca4d 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -41,10 +41,10 @@ export class WhichKeyMenu { this.reject = reject; }); this.disposables = [ - this.keyListener.onDidKeyPressed(this.onDidKeyPressed.bind(this)), - this.quickPick.onDidChangeValue(this.onDidChangeValue.bind(this)), - this.quickPick.onDidAccept(this.onDidAccept.bind(this)), - this.quickPick.onDidHide(this.onDidHide.bind(this)) + this.keyListener.onDidKeyPressed(this.onDidKeyPressed, this), + this.quickPick.onDidChangeValue(this.onDidChangeValue, this), + this.quickPick.onDidAccept(this.onDidAccept, this), + this.quickPick.onDidHide(this.onDidHide, this) ]; this.isHiding = false; this.itemHistory = []; From 84e8dea71828bcde331078f5f68c136cec73f635 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 15:07:25 -0700 Subject: [PATCH 13/27] Use ; instead , as separator of command --- src/menu/menuItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 57696b0..a95f55c 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -154,7 +154,7 @@ function getCondition(key: string): Condition | undefined { return undefined; } - const props = key.split(","); + const props = key.split(";"); const r = props.reduce((result, prop) => { const [key, value] = prop.split(":"); result[key] = value; From d56d1bf89935a0d7dc1fbaa9a31eb840bddfde53 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 17:05:42 -0700 Subject: [PATCH 14/27] Small refactoring for error rasing --- src/menu/menuItem.ts | 20 ++++++++++++++------ src/whichKeyCommand.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index a95f55c..3f60afe 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -94,7 +94,7 @@ abstract class BaseCollectionMenuItem extends BaseMenuItem { class BindingsMenuItem extends BaseCollectionMenuItem { constructor(item: BindingItem) { if (!item.bindings) { - throw new Error("Property bindings is not defined for type bindings"); + throw new MissingPropertyError("bindings", ActionType.Bindings); } const items = createMenuItems(item.bindings); super(item.key, item.name, items); @@ -114,7 +114,7 @@ class CommandsMenuItem extends BaseMenuItem { constructor(item: BindingItem) { if (!item.commands) { - throw new Error("Property commands is not defined for type commands"); + throw new MissingPropertyError("commands", ActionType.Commands); } super(item.key, item.name); @@ -172,7 +172,7 @@ class ConditionalsMenuItem extends BaseMenuItem { constructor(item: BindingItem) { super(item.key, item.name); if (!item.bindings) { - throw new Error("Property bindings is not defined for type conditional"); + throw new MissingPropertyError("bindings", ActionType.Conditional); } this.conditionalItems = item.bindings.map(b => ({ @@ -244,7 +244,7 @@ class CommandMenuItem extends BaseMenuItem { constructor(item: BindingItem) { if (!item.command) { - throw new Error("Property command is not defined for type command"); + throw new MissingPropertyError("command", ActionType.Command); } super(item.key, item.name); @@ -266,7 +266,7 @@ class TransientMenuItem extends BaseCollectionMenuItem { constructor(item: BindingItem) { if (!item.bindings) { - throw new Error("Property bindings is not defined for type transient"); + throw new MissingPropertyError("bindings", ActionType.Transient); } const items = createMenuItems(item.bindings); @@ -337,7 +337,7 @@ function createMenuItem(bindingItem: BindingItem): BaseMenuItem { case ActionType.Conditional: return new ConditionalsMenuItem(bindingItem); default: - throw new Error(`type ${bindingItem.type} not recognized`); + throw new Error(`type ${bindingItem.type} is not supported`); } } @@ -384,4 +384,12 @@ function createMenuItems(bindingItems: BindingItem[]) { export function convertToMenuLabel(s: string) { return s.replace(/ /g, '␣').replace(/\t/g, '↹'); +} + +class MissingPropertyError extends Error { + name: string; + constructor(propertyName: string, typeName: string) { + super(); + this.name = `Property ${propertyName} is not defined for type ${typeName}`; + } } \ No newline at end of file diff --git a/src/whichKeyCommand.ts b/src/whichKeyCommand.ts index c369d31..04991ba 100644 --- a/src/whichKeyCommand.ts +++ b/src/whichKeyCommand.ts @@ -59,7 +59,7 @@ export default class WhichKeyCommand { if (items) { return showMenu(this.keyListener, items, false, this.config?.title); } else { - throw new Error("No bindings is available"); + throw new Error("No bindings are available"); } } From 34fa407b11ab365af19c878610941634eaf7be49 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 17:07:02 -0700 Subject: [PATCH 15/27] Refactoring the ConditionalMenuItem - This reuses BaseCollectionMenuItem - This also fixes a bug where the equality check of overrides for conditional bindings were incorrect --- src/menu/menuItem.ts | 141 +++++++++++++++++-------------------------- 1 file changed, 57 insertions(+), 84 deletions(-) diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 3f60afe..1cdea89 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -31,7 +31,7 @@ export abstract class BaseMenuItem implements QuickPickItem { // Nothing to sort } - get(_id: string): BaseMenuItem | undefined { + get(_key: string): BaseMenuItem | undefined { return undefined; } @@ -55,14 +55,18 @@ abstract class BaseCollectionMenuItem extends BaseMenuItem { } } - get(id: string) { - return this.items.find(i => i.key === id); + get(key: string) { + return this.items.find(i => i.key === key); + } + + protected getIndex(key: string) { + return this.items.findIndex(i => i.key === key); } overrideItem(o: OverrideBindingItem) { const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; const key = keys[keys.length - 1]; - const index = this.items.findIndex(i => i.key === key); + const index = this.getIndex(key); if (o.position === undefined) { const newItem = createMenuItemFromOverrides(o, key); @@ -130,111 +134,80 @@ class CommandsMenuItem extends BaseMenuItem { } } -type ConditionalMenuItem = { - condition: Condition | undefined, - item: BaseMenuItem, -}; - -function evalCondition(item: ConditionalMenuItem, condition?: Condition) { - if (condition && item.condition) { +function evalCondition(stored?: Condition, evaluatee?: Condition) { + if (evaluatee && stored) { let result = true; - if (item.condition.when) { - result = result && (item.condition.when === condition.when); + if (stored.when) { + result = result && (stored.when === evaluatee.when); } - if (item.condition.languageId) { - result = result && (item.condition.languageId === condition.languageId); + if (stored.languageId) { + result = result && (stored.languageId === evaluatee.languageId); } return result; } return false; } -function getCondition(key: string): Condition | undefined { - if (key.length === 0) { - return undefined; +function isConditionEqual(condition1?: Condition, condition2?: Condition) { + if (condition1 && condition2) { + let result = true; + result = result && (condition1.when === condition2.when); + result = result && (condition1.languageId === condition2.languageId); + return result; } + // For if they are both undefined or null + return condition1 === condition2; +} - const props = key.split(";"); - const r = props.reduce((result, prop) => { - const [key, value] = prop.split(":"); - result[key] = value; - return result; - }, {} as Record); - return { - when: r["when"], - languageId: r["languageId"] - }; +function isConditionKeyEqual(key1?: string, key2?: string) { + return isConditionEqual(getCondition(key1), getCondition(key2)); } -class ConditionalsMenuItem extends BaseMenuItem { - private conditionalItems: ConditionalMenuItem[]; +function getCondition(key?: string): Condition | undefined { + if (key && key.length > 0) { + const props = key.split(";"); + const r = props.reduce((result, prop) => { + const [key, value] = prop.split(":"); + result[key] = value; + return result; + }, {} as Record); + return { + when: r["when"], + languageId: r["languageId"] + }; + } + return undefined; +} +class ConditionalsMenuItem extends BaseCollectionMenuItem { constructor(item: BindingItem) { - super(item.key, item.name); if (!item.bindings) { throw new MissingPropertyError("bindings", ActionType.Conditional); } - - this.conditionalItems = item.bindings.map(b => ({ - condition: getCondition(b.key), - item: createMenuItem(b) - })); + const items = createMenuItems(item.bindings); + super(item.key, item.name, items); } - select(args?: Condition): MenuSelectionResult { - let match = this.conditionalItems.find(c => evalCondition(c, args)); - - // No matches, find the first empty condition as else - match = match ?? this.conditionalItems.find(c => c.condition === undefined); - if (match) { - return match.item.select(args); - } - - throw new Error("No conditions match!"); + get(key: string) { + return this.items.find(i => isConditionKeyEqual(key, i.key)); } - sortItems(order: SortOrder): void { - for (const i of this.conditionalItems) { - i.item.sortItems(order); - } + protected getIndex(key: string) { + return this.items.findIndex(i => isConditionKeyEqual(key, i.key)); } - overrideItem(o: OverrideBindingItem) { - const keys = (typeof o.keys === 'string') ? o.keys.split('.') : o.keys; - const key = keys[keys.length - 1]; - let condition = getCondition(key); + eval(condition?: Condition) { + return this.items.find(i => evalCondition(getCondition(i.key), condition)); + } - const index = this.conditionalItems.findIndex(i => evalCondition(i, condition)); - const createItem = () => ( - { - condition, - item: createMenuItemFromOverrides(o, key) - } - ); - if (o.position === undefined) { - const newItem = createItem(); - if (index !== -1) { - // replace the current item - this.conditionalItems.splice(index, 1, newItem); - } else { - // append if there isn't an existing binding - this.conditionalItems.push(newItem); - } - } else { - if (o.position < 0) { - // negative position, attempt to remove - if (index !== -1) { - this.conditionalItems.splice(index, 1); - } - } else { - // Remove and replace - if (index !== -1) { - this.conditionalItems.splice(index, 1); - } - const newItem = createItem(); - this.conditionalItems.splice(o.position, 0, newItem); - } + select(args?: Condition): MenuSelectionResult { + // Search the condition first. If no matches, find the first empty condition as else + let match = this.eval(args) ?? this.eval(undefined); + if (match) { + return match.select(args); } + + throw new Error("No conditions match!"); } } From 203a36dd58a1cec04e5e709c1077fdf60cbd3b4b Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 21:03:35 -0700 Subject: [PATCH 16/27] Fix the bug where undefined key are not recognized --- package.json | 2 +- src/menu/menuItem.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 38a601d..02d6eaa 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "key": "tab", "command": "whichkey.triggerKey", "args": "\t", - "when": "whichkeyActive" + "when": "whichkeyVisible" } ], "commands": [ diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 1cdea89..1df09d2 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -145,7 +145,8 @@ function evalCondition(stored?: Condition, evaluatee?: Condition) { } return result; } - return false; + // For if they are both undefined or null + return stored === evaluatee; } function isConditionEqual(condition1?: Condition, condition2?: Condition) { @@ -171,10 +172,15 @@ function getCondition(key?: string): Condition | undefined { result[key] = value; return result; }, {} as Record); - return { - when: r["when"], - languageId: r["languageId"] - }; + + // Check to make sure at least the one property so we don't create + // { when: undefined, languagedId: undefined } + if ("when" in r || "languageId" in r) { + return { + when: r["when"], + languageId: r["languageId"] + }; + } } return undefined; } From ba32689b67dc4db223f18f239541fe6590fa0bd3 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 21:09:24 -0700 Subject: [PATCH 17/27] Show status bar msg when no conditionals matched --- src/bindingItem.ts | 1 - src/constants.ts | 2 ++ src/menu/menu.ts | 4 ++-- src/menu/menuItem.ts | 10 +++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index 8d59d86..99f4d03 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -37,7 +37,6 @@ export type Condition = { languageId?: string, }; - export function toBindingItem(o: any) { if (typeof o === "object") { const config = o as Partial; diff --git a/src/constants.ts b/src/constants.ts index 8eb1d8d..ce74c24 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,3 +26,5 @@ export enum ContextKey { export const whichKeyShow = `${contributePrefix}.${CommandKey.Show}`; export const whichKeyRegister = `${contributePrefix}.${CommandKey.Register}`; export const whichKeyTrigger = `${contributePrefix}.${CommandKey.Trigger}`; + +export const defaultStatusBarTimeout = 5000; \ No newline at end of file diff --git a/src/menu/menu.ts b/src/menu/menu.ts index ca3ca4d..91f4d8b 100644 --- a/src/menu/menu.ts +++ b/src/menu/menu.ts @@ -1,5 +1,5 @@ import { commands, Disposable, QuickPick, window } from "vscode"; -import { ContextKey } from "../constants"; +import { ContextKey, defaultStatusBarTimeout } from "../constants"; import KeyListener, { KeybindingArgs } from "../keyListener"; import { setStatusBarMessage } from "../statusBar"; import { BaseMenuItem, convertToMenuLabel } from "./menuItem"; @@ -85,7 +85,7 @@ export class WhichKeyMenu { } else { await this.hide(); const keyCombo = this.getHistoryString(value); - setStatusBarMessage(`${keyCombo} is undefined`, 5000, true); + setStatusBarMessage(`${keyCombo} is undefined`, defaultStatusBarTimeout, true); this.dispose(); this.resolve(); } diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index 1df09d2..a0b4923 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,6 +1,7 @@ -import { QuickPickItem } from "vscode"; +import { QuickPickItem, window } from "vscode"; import { ActionType, BindingItem, Condition, OverrideBindingItem } from "../bindingItem"; -import { SortOrder } from "../constants"; +import { defaultStatusBarTimeout, SortOrder } from "../constants"; +import { setStatusBarMessage } from "../statusBar"; export interface MenuSelectionResult { items?: BaseMenuItem[], @@ -213,7 +214,10 @@ class ConditionalsMenuItem extends BaseCollectionMenuItem { return match.select(args); } - throw new Error("No conditions match!"); + const msg = "No conditions matched"; + console.warn(`${msg};key=${this.key};name=${this.name}`); + setStatusBarMessage(msg, defaultStatusBarTimeout, true); + return {}; } } From d91efe806a5ba5c8e71b8189ab9bb9b7884eb80f Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Thu, 27 Aug 2020 21:11:39 -0700 Subject: [PATCH 18/27] Update doc for conditional bindings --- CHANGELOG.md | 1 + README.md | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4336eaa..5ee843a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Add color on the status bar message when key binding entered is not defined +- Add support for a new conditional type binding, which allows conditional binding execution. See README for more information on how to use it. ## [0.7.6] - 2020-08-03 ### Added diff --git a/README.md b/README.md index 6f5f7e3..8bbf16c 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,137 @@ Selected text can be hard to see when which-key menu is active. This could be du }, ``` +### Conditional bindings (experimental) + +> This is marked as experimental and the config is subjected to change. + +This allow conditional execution of bindings. Currently, it only supports condition on the `when` passed from shortcut and `languageId` of the active editor. It reuse the similar structure as the `bindings` type. The property `key` in a binding item is reused to present the condition. The condition can be thought of as a key-value pair serialized into a string. + +As an example, a condition in json like +```json +{ + "languageId": "javascript", + "when": "sideBarVisible" +} +``` +can be serialized into `languageId:javascript;when:sideBarVisible`. The string representation can then be use as the value of the binding key. + +A concrete example of a binding with that condition is as follow: +```json +{ + "whichkey.bindings": [ + { + "key": "m", + "name": "Major...", + "type": "conditional", + "bindings": [ + { + "key": "languageId:javascript;when:sideBarVisible", + "name": "Open file", + "type": "command", + "command": "workbench.action.files.openFileFolder" + }, + { + "key": "", + "name": "Buffers", + "type": "bindings", + "bindings": [ + { + "key": "b", + "name": "Show all buffers/editors", + "type": "command", + "command": "workbench.action.showAllEditors" + } + ] + }, + ] + } + ] +} +``` +In this example, when `m` click, it will find the first item that matches the current contrition. If no configured key matches the current condition, a default item showing a buffer menu will be use. Any item that has invalid key will be default item. + +#### Overrides +This is again similar with the `bindings` type. + +To override the condition binding item completely, the following config will overrides the `m` completely with the provided bindings. +```json +{ + "whichkey.bindingOverrides": [ + { + "keys": "m", + "name": "Major", + "type": "conditional", + "bindings": [ + { + "key": "languageId:javascript", + "name": "Go to", + "type": "command", + "command": "workbench.action.gotoLine", + } + ] + } + ] +} +``` +You also also choose to modify existing conditional bindings like adding and removal. The following will add a key of `languageId:javascript` to the existing conditional binding of `m`. +```json +{ + "whichkey.bindingOverrides": [ + { + "keys": ["m", "languageId:javascript"], + "name": "Go to", + "type": "command", + "command": "workbench.action.gotoLine", + } + ] +} +``` +Negative `position` property can also be used to remove conditional bindings. + +#### when +Since VSCode doesn't allow reading of the context, which is the condition of used in the `when` in shortcuts. In order to get around that, for every when condition, you will need to set up a shortcut to evaluate that specific condition. + +For example, the following keybindings will pass both `key` and `when` for which-key handle for key `t`. +`keybindings.json` +```json +{ + "key": "t", + "command": "whichkey.triggerKey", + "args": { + "key": "t", + "when": "sideBarVisible && explorerViewletVisible" + }, + "when": "whichkeyVisible && sideBarVisible && explorerViewletVisible" +} +``` + +You can then define the follow bindings that uses that specific `key` and `when`. +```json +{ + "key": "t", + "name": "Show tree/explorer view", + "type": "conditional", + "bindings": [ + { + "key": "", + "name": "default", + "type": "command", + "command": "workbench.view.explorer" + }, + { + "key": "when:sideBarVisible && explorerViewletVisible", + "name": "Hide explorer", + "type": "command", + "command": "workbench.action.toggleSidebarVisibility" + } + ] +} +``` + +#### languageId +This is language id of the active editor. The language id can be found in language selection menu inside the parenthesis next to the language name. + ## Release Notes See [CHANGELOG.md](CHANGELOG.md) From cd8398de6106a453cfde6ab8a3b4d27911236395 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Sun, 30 Aug 2020 23:38:52 -0700 Subject: [PATCH 19/27] Remove an extra layer of await function --- src/whichKeyCommand.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/whichKeyCommand.ts b/src/whichKeyCommand.ts index 04991ba..0c78741 100644 --- a/src/whichKeyCommand.ts +++ b/src/whichKeyCommand.ts @@ -69,7 +69,7 @@ export default class WhichKeyCommand { } } -async function showMenu(keyListener: KeyListener, items: BaseMenuItem[], isTransient: boolean, title?: string) { +function showMenu(keyListener: KeyListener, items: BaseMenuItem[], isTransient: boolean, title?: string) { const delay = workspace.getConfiguration(contributePrefix).get(ConfigKey.Delay) ?? 0; - await WhichKeyMenu.show(keyListener, items, isTransient, delay, title); + return WhichKeyMenu.show(keyListener, items, isTransient, delay, title); } \ No newline at end of file From 533959a5357b10e4895a043a46d99a0933efa90e Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Sun, 30 Aug 2020 23:44:13 -0700 Subject: [PATCH 20/27] Update wording --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8bbf16c..90793b6 100644 --- a/README.md +++ b/README.md @@ -267,9 +267,9 @@ Selected text can be hard to see when which-key menu is active. This could be du ### Conditional bindings (experimental) -> This is marked as experimental and the config is subjected to change. +> This is marked as experimental and the config is subject to change. -This allow conditional execution of bindings. Currently, it only supports condition on the `when` passed from shortcut and `languageId` of the active editor. It reuse the similar structure as the `bindings` type. The property `key` in a binding item is reused to present the condition. The condition can be thought of as a key-value pair serialized into a string. +This allows conditional execution of bindings. Currently, it only supports condition on the `when` passed from shortcut and `languageId` of the active editor. It reuses the similar structure to the `bindings` type. The property `key` in a binding item is reused to represent the condition. The condition can be thought of as a key-value pair serialized into a string. As an example, a condition in json like ```json @@ -313,12 +313,12 @@ A concrete example of a binding with that condition is as follow: ] } ``` -In this example, when `m` click, it will find the first item that matches the current contrition. If no configured key matches the current condition, a default item showing a buffer menu will be use. Any item that has invalid key will be default item. +In this example, when `m` click, it will find the first item that matches the current contrition. If no configured key matches the current condition, a default item showing a buffer menu will be use. Any item that has invalid key will used as default item. #### Overrides This is again similar with the `bindings` type. -To override the condition binding item completely, the following config will overrides the `m` completely with the provided bindings. +To override the condition binding item completely, the following config will overrides the `m` binding completely with the provided override. ```json { "whichkey.bindingOverrides": [ @@ -338,7 +338,7 @@ To override the condition binding item completely, the following config will ove ] } ``` -You also also choose to modify existing conditional bindings like adding and removal. The following will add a key of `languageId:javascript` to the existing conditional binding of `m`. +You also also choose to modify existing conditional bindings like adding and removal. The following will add a key of `languageId:javascript` to the conditional binding if `languageId:javascript` doesn't exist. ```json { "whichkey.bindingOverrides": [ From ebb84912077c5fcbe2c5c395267b8f4ecac7b6e2 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Mon, 31 Aug 2020 00:31:31 -0700 Subject: [PATCH 21/27] Code cleanup --- src/bindingItem.ts | 10 ---------- src/menu/menuItem.ts | 9 +++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/bindingItem.ts b/src/bindingItem.ts index 99f4d03..fc3a0fb 100644 --- a/src/bindingItem.ts +++ b/src/bindingItem.ts @@ -6,11 +6,6 @@ export const enum ActionType { Conditional = "conditional", } -export type ConditionalActionType = ActionType.Bindings - | ActionType.Command - | ActionType.Commands - | ActionType.Transient; - export interface BindingItem { key: string; name: string; @@ -32,11 +27,6 @@ export interface OverrideBindingItem { bindings?: BindingItem[], } -export type Condition = { - when?: string, - languageId?: string, -}; - export function toBindingItem(o: any) { if (typeof o === "object") { const config = o as Partial; diff --git a/src/menu/menuItem.ts b/src/menu/menuItem.ts index a0b4923..e450732 100644 --- a/src/menu/menuItem.ts +++ b/src/menu/menuItem.ts @@ -1,8 +1,13 @@ -import { QuickPickItem, window } from "vscode"; -import { ActionType, BindingItem, Condition, OverrideBindingItem } from "../bindingItem"; +import { QuickPickItem } from "vscode"; +import { ActionType, BindingItem, OverrideBindingItem } from "../bindingItem"; import { defaultStatusBarTimeout, SortOrder } from "../constants"; import { setStatusBarMessage } from "../statusBar"; +interface Condition { + when?: string, + languageId?: string, +} + export interface MenuSelectionResult { items?: BaseMenuItem[], isTransient?: boolean, From 62f22169c5c1672facac4d19b48178213eee36b4 Mon Sep 17 00:00:00 2001 From: MarcoIeni <11428655+MarcoIeni@users.noreply.github.com> Date: Tue, 1 Sep 2020 20:34:45 +0200 Subject: [PATCH 22/27] README: remove trailing whitespaces --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 90793b6..c129f44 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This extension can be used by itself or be called by other extension. ### Standalone -This extension comes with a default that didn't have any third-party dependencies. +This extension comes with a default that didn't have any third-party dependencies. #### Setup: I am using VSCode Vim If you want a better default behavior design for VSCode Vim, checkout [VSpaceCode](https://github.com/VSpaceCode/VSpaceCode). @@ -46,7 +46,6 @@ You can also bind a customize menu with Vim directly { "before": [""], "commands": ["whichkey.show"], - } ] ``` @@ -101,7 +100,7 @@ The following example will replace/append the whole ` g` menu with one bind "key": "s", "name": "Go to", "type": "command", - "command": "workbench.action.gotoLine", + "command": "workbench.action.gotoLine", } ] } @@ -331,7 +330,7 @@ To override the condition binding item completely, the following config will ove "key": "languageId:javascript", "name": "Go to", "type": "command", - "command": "workbench.action.gotoLine", + "command": "workbench.action.gotoLine", } ] } @@ -346,7 +345,7 @@ You also also choose to modify existing conditional bindings like adding and rem "keys": ["m", "languageId:javascript"], "name": "Go to", "type": "command", - "command": "workbench.action.gotoLine", + "command": "workbench.action.gotoLine", } ] } From 758a45eb1aafac8e3ccad8e51c2653b133ec6c63 Mon Sep 17 00:00:00 2001 From: MarcoIeni <11428655+MarcoIeni@users.noreply.github.com> Date: Tue, 1 Sep 2020 20:36:41 +0200 Subject: [PATCH 23/27] README: add some missing new lines --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index c129f44..2f96fe2 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,7 @@ This section config extra settings that pertain to both Standalone or With exten This section describes a way to use non-character keys in which-key menu like `` or `Control+D`. `` is supported out of the box. Follow the following instruction to add support for keys other than ``. Merge the following json to your `keybindings.json`. + ```json { "key": "ctrl+x", @@ -248,6 +249,7 @@ Merge the following json to your `keybindings.json`. "when": "whichkeyActive" } ``` + Once you've done that, you can use `C-x` in the `key` value of the which-key config. Effectively, the above keybinding will enter `C-x` in the QuickPick input box when `ctrl+x` is pressed when the which key is focused. ### Display menu with a delay @@ -258,6 +260,7 @@ You can set `whichkey.sortOrder` in `settings.json` to `alphabetically` to alway ### Unclear selection Selected text can be hard to see when which-key menu is active. This could be due to the `inactiveSelectionBackground` config of your current theme. You can selectively override that color in your `settings.json` like the following example. + ```json "workbench.colorCustomizations": { "editor.inactiveSelectionBackground": "color that works better", @@ -271,15 +274,18 @@ Selected text can be hard to see when which-key menu is active. This could be du This allows conditional execution of bindings. Currently, it only supports condition on the `when` passed from shortcut and `languageId` of the active editor. It reuses the similar structure to the `bindings` type. The property `key` in a binding item is reused to represent the condition. The condition can be thought of as a key-value pair serialized into a string. As an example, a condition in json like + ```json { "languageId": "javascript", "when": "sideBarVisible" } ``` + can be serialized into `languageId:javascript;when:sideBarVisible`. The string representation can then be use as the value of the binding key. A concrete example of a binding with that condition is as follow: + ```json { "whichkey.bindings": [ @@ -312,12 +318,15 @@ A concrete example of a binding with that condition is as follow: ] } ``` + In this example, when `m` click, it will find the first item that matches the current contrition. If no configured key matches the current condition, a default item showing a buffer menu will be use. Any item that has invalid key will used as default item. #### Overrides + This is again similar with the `bindings` type. To override the condition binding item completely, the following config will overrides the `m` binding completely with the provided override. + ```json { "whichkey.bindingOverrides": [ @@ -337,7 +346,9 @@ To override the condition binding item completely, the following config will ove ] } ``` + You also also choose to modify existing conditional bindings like adding and removal. The following will add a key of `languageId:javascript` to the conditional binding if `languageId:javascript` doesn't exist. + ```json { "whichkey.bindingOverrides": [ @@ -350,13 +361,16 @@ You also also choose to modify existing conditional bindings like adding and rem ] } ``` + Negative `position` property can also be used to remove conditional bindings. #### when + Since VSCode doesn't allow reading of the context, which is the condition of used in the `when` in shortcuts. In order to get around that, for every when condition, you will need to set up a shortcut to evaluate that specific condition. For example, the following keybindings will pass both `key` and `when` for which-key handle for key `t`. `keybindings.json` + ```json { "key": "t", @@ -370,6 +384,7 @@ For example, the following keybindings will pass both `key` and `when` for which ``` You can then define the follow bindings that uses that specific `key` and `when`. + ```json { "key": "t", @@ -393,6 +408,7 @@ You can then define the follow bindings that uses that specific `key` and `when` ``` #### languageId + This is language id of the active editor. The language id can be found in language selection menu inside the parenthesis next to the language name. ## Release Notes From 59a747d2125b088095e0063164413743cbafe913 Mon Sep 17 00:00:00 2001 From: MarcoIeni <11428655+MarcoIeni@users.noreply.github.com> Date: Tue, 1 Sep 2020 20:56:29 +0200 Subject: [PATCH 24/27] README: fix grammar, improve readability --- README.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2f96fe2..67f8852 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,11 @@ Selected text can be hard to see when which-key menu is active. This could be du > This is marked as experimental and the config is subject to change. -This allows conditional execution of bindings. Currently, it only supports condition on the `when` passed from shortcut and `languageId` of the active editor. It reuses the similar structure to the `bindings` type. The property `key` in a binding item is reused to represent the condition. The condition can be thought of as a key-value pair serialized into a string. +This allows conditional execution of bindings. Currently, it only supports conditions on the `when` passed from shortcut and `languageId` of the active editor. + +- It reuses the similar structure to the `bindings` type. +- The property `key` in a binding item is reused to represent the condition. +- The condition can be thought of as a key-value pair serialized into a string. As an example, a condition in json like @@ -282,7 +286,8 @@ As an example, a condition in json like } ``` -can be serialized into `languageId:javascript;when:sideBarVisible`. The string representation can then be use as the value of the binding key. +can be serialized into `languageId:javascript;when:sideBarVisible`. +The string representation can then be used as the value of the binding key. A concrete example of a binding with that condition is as follow: @@ -319,13 +324,18 @@ A concrete example of a binding with that condition is as follow: } ``` -In this example, when `m` click, it will find the first item that matches the current contrition. If no configured key matches the current condition, a default item showing a buffer menu will be use. Any item that has invalid key will used as default item. +In this example, when `m` is pressed, it will find the first binding that matches the current condition. +If no configured key matches the current condition, a default item showing a buffer menu will be used. +Any item that has an invalid key will be used as default item. + +Therefore, in this example, if the language is javascript and the sidebar is visible, `m` will open +the file browser, otherwise it will show the "buffers" menu. #### Overrides This is again similar with the `bindings` type. -To override the condition binding item completely, the following config will overrides the `m` binding completely with the provided override. +For example, the following config will override the `m` binding completely: ```json { @@ -347,7 +357,8 @@ To override the condition binding item completely, the following config will ove } ``` -You also also choose to modify existing conditional bindings like adding and removal. The following will add a key of `languageId:javascript` to the conditional binding if `languageId:javascript` doesn't exist. +You can also choose to add or remove conditions to existing conditional bindings. +For example, the following will add a key of `languageId:javascript` to the conditional binding if `languageId:javascript` doesn't already exist. ```json { @@ -366,7 +377,8 @@ Negative `position` property can also be used to remove conditional bindings. #### when -Since VSCode doesn't allow reading of the context, which is the condition of used in the `when` in shortcuts. In order to get around that, for every when condition, you will need to set up a shortcut to evaluate that specific condition. +Since VSCode doesn't allow reading of the context, which is the condition used in the `when` in shortcuts, +for every `when` condition, you will need to set up a shortcut to evaluate that specific condition. For example, the following keybindings will pass both `key` and `when` for which-key handle for key `t`. `keybindings.json` From a798b2bc542810a59e10c8ff1cfd46f3fdf53b19 Mon Sep 17 00:00:00 2001 From: Steven Guh Date: Tue, 1 Sep 2020 16:31:24 -0700 Subject: [PATCH 25/27] Clarify conditional section of the README --- README.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 67f8852..6e321b4 100644 --- a/README.md +++ b/README.md @@ -277,17 +277,7 @@ This allows conditional execution of bindings. Currently, it only supports condi - The property `key` in a binding item is reused to represent the condition. - The condition can be thought of as a key-value pair serialized into a string. -As an example, a condition in json like - -```json -{ - "languageId": "javascript", - "when": "sideBarVisible" -} -``` - -can be serialized into `languageId:javascript;when:sideBarVisible`. -The string representation can then be used as the value of the binding key. +`languageId:javascript;when:sideBarVisible` is an example condition serialized into a string for the `key` that checks if the lanauge id of the currently active editor is javascript and if the side bar is visible (See the `when` section for more detail) A concrete example of a binding with that condition is as follow: @@ -377,11 +367,9 @@ Negative `position` property can also be used to remove conditional bindings. #### when -Since VSCode doesn't allow reading of the context, which is the condition used in the `when` in shortcuts, -for every `when` condition, you will need to set up a shortcut to evaluate that specific condition. +Since VSCode doesn't allow reading of the context, which is the condition used in the `when` in shortcuts, you will need to you will need to set up a shortcut to evaluate that specific condition for every `when` condition used in conditional binding to get around until [vscode/#10471](https://github.com/microsoft/vscode/issues/10471) is implemented. -For example, the following keybindings will pass both `key` and `when` for which-key handle for key `t`. -`keybindings.json` +For example, the following shortcut in `keybindings.json` will pass both `key` and `when` in the `args` to `which-key`. The outer `when` is the [condition clause](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) for vscode to execute this key, and must contain `whichKeyVisible` which limits this shortcut to be only applicable when the which-key menu is visible. In this case, if a user press key `t` when which-key, sidebar and explorer viewlet is visible, it will execute `whichkey.triggerKey` command and send `which-key` the `args`, which contains the `key`, and `when`. ```json { @@ -395,7 +383,7 @@ For example, the following keybindings will pass both `key` and `when` for which } ``` -You can then define the follow bindings that uses that specific `key` and `when`. +The `args.key` and `args.when` that were sent to `which-key` are then used to find the a binding that matches the key `t` and any conditional binding that matches that condition. The following binding is example that contains a conditional binding that can match shortcut's `args.when`. ```json { @@ -419,6 +407,8 @@ You can then define the follow bindings that uses that specific `key` and `when` } ``` +Unfortunately, if you have another condition binding with a different `key` that want to match the same `when` condition as the `t` in the above example, you will need to setup another shortcut with that different `key`. + #### languageId This is language id of the active editor. The language id can be found in language selection menu inside the parenthesis next to the language name. From 1d6b6b4599ab04431fd2ae47268d0703735f78e4 Mon Sep 17 00:00:00 2001 From: Marco Ieni <11428655+MarcoIeni@users.noreply.github.com> Date: Wed, 2 Sep 2020 19:51:24 +0200 Subject: [PATCH 26/27] README: fix typo, simplify sentences --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6e321b4..efe13d9 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ This allows conditional execution of bindings. Currently, it only supports condi - The property `key` in a binding item is reused to represent the condition. - The condition can be thought of as a key-value pair serialized into a string. -`languageId:javascript;when:sideBarVisible` is an example condition serialized into a string for the `key` that checks if the lanauge id of the currently active editor is javascript and if the side bar is visible (See the `when` section for more detail) +`languageId:javascript;when:sideBarVisible` is an example condition serialized into a string for the `key` that checks if the language id of the currently active editor is javascript and if the side bar is visible (see the [when](#when) section for more details). A concrete example of a binding with that condition is as follow: @@ -367,9 +367,10 @@ Negative `position` property can also be used to remove conditional bindings. #### when -Since VSCode doesn't allow reading of the context, which is the condition used in the `when` in shortcuts, you will need to you will need to set up a shortcut to evaluate that specific condition for every `when` condition used in conditional binding to get around until [vscode/#10471](https://github.com/microsoft/vscode/issues/10471) is implemented. +Since VSCode doesn't allow reading of the context of a json field, we cannot read the condition used in the `when` in shortcuts. +For this reason, you will need to repeat every `when` condition used in conditional bindings, at least until [vscode/#10471](https://github.com/microsoft/vscode/issues/10471) is implemented. -For example, the following shortcut in `keybindings.json` will pass both `key` and `when` in the `args` to `which-key`. The outer `when` is the [condition clause](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) for vscode to execute this key, and must contain `whichKeyVisible` which limits this shortcut to be only applicable when the which-key menu is visible. In this case, if a user press key `t` when which-key, sidebar and explorer viewlet is visible, it will execute `whichkey.triggerKey` command and send `which-key` the `args`, which contains the `key`, and `when`. +For example, the following shortcut in `keybindings.json` will pass both `key` and `when` in the `args` to `which-key`. The outer `when` is the [condition clause](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) for vscode to execute this key, and must contain `whichKeyVisible` which limits this shortcut to be only applicable when the which-key menu is visible. In this case, if a user presses key `t` when which-key, sidebar and explorer viewlet are visible, it will execute `whichkey.triggerKey` command and send the `args` (`key` and `when`) to `which-key` ```json { @@ -383,7 +384,7 @@ For example, the following shortcut in `keybindings.json` will pass both `key` a } ``` -The `args.key` and `args.when` that were sent to `which-key` are then used to find the a binding that matches the key `t` and any conditional binding that matches that condition. The following binding is example that contains a conditional binding that can match shortcut's `args.when`. +The `args.key` and `args.when` that were sent to `which-key` are then used to find the a binding that matches the key `t` and any conditional binding that matches that condition. The following binding is an example that contains a conditional binding that matches the `args`. ```json { From 11a0c6292751d578ec70db8320dd48d3dbac4ca4 Mon Sep 17 00:00:00 2001 From: Marco Ieni <11428655+MarcoIeni@users.noreply.github.com> Date: Wed, 2 Sep 2020 20:01:50 +0200 Subject: [PATCH 27/27] README: hide experimental feature by default --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index efe13d9..0255d8f 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,8 @@ Selected text can be hard to see when which-key menu is active. This could be du ``` ### Conditional bindings (experimental) +
+ Click to expand! > This is marked as experimental and the config is subject to change. @@ -414,6 +416,8 @@ Unfortunately, if you have another condition binding with a different `key` that This is language id of the active editor. The language id can be found in language selection menu inside the parenthesis next to the language name. +
+ ## Release Notes See [CHANGELOG.md](CHANGELOG.md)