Add support for three or more chord keyboard shortcuts (#175253)

* Add support for three or more chords in shortcuts

* Add tests and fix bug that was uncovered

* Clean up TODO comments

* Make multi chords more explicit in quickInput.ts

* Address review comments

* keybindings: resolver: align terminology with rest of code

* Allow input of >2 chords in keybinding settings

* Keep the limit at 2 chords in the widget for now

* Revert "Keep the limit at 2 chords in the widget for now"

This reverts commit 55c32ae2e0.

* keybindings: UI: search widget: limit chords to 2 for now

* Fix single modifier chords

* Remove unused import

---------

Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Dima <alexdima@microsoft.com>
This commit is contained in:
Tilman Roeder 2023-03-20 10:59:07 +00:00 committed by GitHub
parent faaaa3cbb9
commit a0a30a5de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 179 additions and 120 deletions

View file

@ -93,13 +93,13 @@ export class KeybindingLabel {
this.clear();
if (this.keybinding) {
const [firstChord, secondChord] = this.keybinding.getChords();// TODO@chords
if (firstChord) {
this.renderChord(this.domNode, firstChord, this.matches ? this.matches.firstPart : null);
const chords = this.keybinding.getChords();
if (chords[0]) {
this.renderChord(this.domNode, chords[0], this.matches ? this.matches.firstPart : null);
}
if (secondChord) {
for (let i = 1; i < chords.length; i++) {
dom.append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
this.renderChord(this.domNode, secondChord, this.matches ? this.matches.chordPart : null);
this.renderChord(this.domNode, chords[i], this.matches ? this.matches.chordPart : null);
}
const title = (this.options.disableTitle ?? false) ? undefined : this.keybinding.getAriaLabel() || undefined;
if (title !== undefined) {

View file

@ -28,19 +28,27 @@ const enum BinaryKeybindingsMask {
KeyCode = 0x000000FF
}
export function decodeKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null {
if (keybinding === 0) {
return null;
export function decodeKeybinding(keybinding: number | number[], OS: OperatingSystem): Keybinding | null {
if (typeof keybinding === 'number') {
if (keybinding === 0) {
return null;
}
const firstChord = (keybinding & 0x0000FFFF) >>> 0;
const secondChord = (keybinding & 0xFFFF0000) >>> 16;
if (secondChord !== 0) {
return new Keybinding([
createSimpleKeybinding(firstChord, OS),
createSimpleKeybinding(secondChord, OS)
]);
}
return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
} else {
const chords = [];
for (let i = 0; i < keybinding.length; i++) {
chords.push(createSimpleKeybinding(keybinding[i], OS));
}
return new Keybinding(chords);
}
const firstChord = (keybinding & 0x0000FFFF) >>> 0;
const secondChord = (keybinding & 0xFFFF0000) >>> 16;
if (secondChord !== 0) {
return new Keybinding([
createSimpleKeybinding(firstChord, OS),
createSimpleKeybinding(secondChord, OS)
]);
}
return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
}
export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): KeyCodeChord {

View file

@ -36,7 +36,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
}
private _currentChord: CurrentChord | null;
private _currentChord: CurrentChord[] | null;
private _currentChordChecker: IntervalTimer;
private _currentChordStatusMessage: IDisposable | null;
private _ignoreSingleModifiers: KeybindingModifierSet;
@ -140,17 +140,12 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
}
const contextValue = this._contextKeyService.getContext(target);
const currentChord = this._currentChord ? this._currentChord.keypress : null;
const currentChord = this._currentChord ? this._currentChord.map((({ keypress }) => keypress)) : null;
return this._getResolver().resolve(contextValue, currentChord, firstChord);
}
private _enterMultiChordMode(firstChord: string, keypressLabel: string | null): void {
this._currentChord = {
keypress: firstChord,
label: keypressLabel
};
this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
const chordEnterTime = Date.now();
private _scheduleLeaveChordMode(): void {
const chordLastInteractedTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
if (!this._documentHasFocus()) {
@ -159,15 +154,35 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
return;
}
if (Date.now() - chordEnterTime > 5000) {
if (Date.now() - chordLastInteractedTime > 5000) {
// 5 seconds elapsed => leave chord mode
this._leaveChordMode();
}
}, 500);
}
private _enterMultiChordMode(firstChord: string, keypressLabel: string | null): void {
this._currentChord = [{
keypress: firstChord,
label: keypressLabel
}];
this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
this._scheduleLeaveChordMode();
IME.disable();
}
private _continueMultiChordMode(nextChord: string, keypressLabel: string | null): void {
this._currentChord = this._currentChord ? this._currentChord : [];
this._currentChord.push({
keypress: nextChord,
label: keypressLabel
});
const fullKeypressLabel = this._currentChord.map(({ label }) => label).join(', ');
this._currentChordStatusMessage = this._notificationService.status(nls.localize('next.chord', "({0}) was pressed. Waiting for next key of chord...", fullKeypressLabel));
this._scheduleLeaveChordMode();
}
private _leaveChordMode(): void {
if (this._currentChordStatusMessage) {
this._currentChordStatusMessage.dispose();
@ -255,15 +270,18 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
}
let firstChord: string | null = null; // the first keybinding i.e. Ctrl+K
let currentChord: string | null = null;// the "second" keybinding i.e. Ctrl+K "Ctrl+D"
let currentChord: string[] | null = null;// the "second" keybinding i.e. Ctrl+K "Ctrl+D"
if (isSingleModiferChord) {
// The keybinding is the second keypress of a single modifier chord, e.g. "shift shift".
// A single modifier can only occur when the same modifier is pressed in short sequence,
// hence we disregard `_currentChord` and use the same modifier instead.
const [dispatchKeyname,] = keybinding.getSingleModifierDispatchChords();
firstChord = dispatchKeyname;
currentChord = dispatchKeyname;
currentChord = dispatchKeyname ? [dispatchKeyname] : [];
} else {
[firstChord,] = keybinding.getDispatchChords();
currentChord = this._currentChord ? this._currentChord.keypress : null;
currentChord = this._currentChord ? this._currentChord.map(({ keypress }) => keypress) : null;
}
if (firstChord === null) {
@ -286,9 +304,15 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
}
if (this._currentChord) {
if (!resolveResult || !resolveResult.commandId) {
this._log(`+ Leaving chord mode: Nothing bound to "${this._currentChord.label} ${keypressLabel}".`);
this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
if (resolveResult && !resolveResult.leaveMultiChord) {
shouldPreventDefault = true;
this._continueMultiChordMode(firstChord, keypressLabel);
this._log(`+ Continuing chord mode...`);
return shouldPreventDefault;
} else if (!resolveResult || !resolveResult.commandId) {
const currentChordLabel = this._currentChord.map(({ label }) => label).join(', ');
this._log(`+ Leaving chord mode: Nothing bound to "${currentChordLabel}, ${keypressLabel}".`);
this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", currentChordLabel, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
shouldPreventDefault = true;
}
}

View file

@ -61,19 +61,17 @@ export class KeybindingResolver {
continue;
}
// TODO@chords
this._addKeyPress(k.chords[0], k);
}
}
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypressFirstPart: string | null, keypressChordPart: string | null, when: ContextKeyExpression | undefined): boolean {
// TODO@chords
if (keypressFirstPart && defaultKb.chords[0] !== keypressFirstPart) {
return false;
}
// TODO@chords
if (keypressChordPart && defaultKb.chords[1] !== keypressChordPart) {
return false;
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null, when: ContextKeyExpression | undefined): boolean {
if (keypress) {
for (let i = 0; i < keypress.length; i++) {
if (keypress[i] !== defaultKb.chords[i]) {
return false;
}
}
}
// `true` means always, as does `undefined`
@ -132,11 +130,8 @@ export class KeybindingResolver {
}
let isRemoved = false;
for (const commandRemoval of commandRemovals) {
// TODO@chords
const keypressFirstChord = commandRemoval.chords[0];
const keypressSecondChord = commandRemoval.chords[1];
const when = commandRemoval.when;
if (this._isTargetedForRemoval(rule, keypressFirstChord, keypressSecondChord, when)) {
if (this._isTargetedForRemoval(rule, commandRemoval.chords, when)) {
isRemoved = true;
break;
}
@ -167,12 +162,17 @@ export class KeybindingResolver {
continue;
}
const conflictHasMultipleChords = (conflict.chords.length > 1);
const itemHasMultipleChords = (item.chords.length > 1);
// TODO@chords
if (conflictHasMultipleChords && itemHasMultipleChords && conflict.chords[1] !== item.chords[1]) {
// The conflict only shares the first chord with this command
// Test if the shorter keybinding is a prefix of the longer one.
// If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.
let isShorterKbPrefix = true;
for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {
if (conflict.chords[i] !== item.chords[i]) {
// The ith step does not conflict
isShorterKbPrefix = false;
break;
}
}
if (!isShorterKbPrefix) {
continue;
}
@ -277,14 +277,13 @@ export class KeybindingResolver {
return items[items.length - 1];
}
public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null {
public resolve(context: IContext, currentChord: string[] | null, keypress: string): IResolveResult | null {
this._log(`| Resolving ${keypress}${currentChord ? ` chorded from ${currentChord}` : ``}`);
let lookupMap: ResolvedKeybindingItem[] | null = null;
if (currentChord !== null) {
// Fetch all chord bindings for `currentChord`
const candidates = this._map.get(currentChord);
const candidates = this._map.get(currentChord[0]);
if (typeof candidates === 'undefined') {
// No chords starting with `currentChord`
this._log(`\\ No keybinding entries.`);
@ -294,8 +293,18 @@ export class KeybindingResolver {
lookupMap = [];
for (let i = 0, len = candidates.length; i < len; i++) {
const candidate = candidates[i];
// TODO@chords
if (candidate.chords[1] === keypress) {
if (candidate.chords.length <= currentChord.length) {
continue;
}
let prefixMatches = true;
for (let i = 1; i < currentChord.length; i++) {
if (candidate.chords[i] !== currentChord[i]) {
prefixMatches = false;
break;
}
}
if (prefixMatches && candidate.chords[currentChord.length] === keypress) {
lookupMap.push(candidate);
}
}
@ -316,7 +325,6 @@ export class KeybindingResolver {
return null;
}
// TODO@chords
if (currentChord === null && result.chords.length > 1 && result.chords[1] !== null) {
this._log(`\\ From ${lookupMap.length} keybinding entries, matched chord, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
return {
@ -326,6 +334,15 @@ export class KeybindingResolver {
commandArgs: null,
bubble: false
};
} else if (currentChord !== null && currentChord.length + 1 < result.chords.length) {
this._log(`\\ From ${lookupMap.length} keybinding entries, continued chord, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
return {
enterMultiChord: false,
leaveMultiChord: false,
commandId: null,
commandArgs: null,
bubble: false
};
}
this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);

View file

@ -23,7 +23,7 @@ function createContext(ctx: any) {
suite('KeybindingResolver', () => {
function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean): ResolvedKeybindingItem {
function kbItem(keybinding: number | number[], command: string, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean): ResolvedKeybindingItem {
const resolvedKeybinding = createUSLayoutResolvedKeybinding(keybinding, OS);
return new ResolvedKeybindingItem(
resolvedKeybinding,
@ -304,7 +304,7 @@ suite('KeybindingResolver', () => {
test('resolve command', () => {
function _kbItem(keybinding: number, command: string, when: ContextKeyExpression | undefined): ResolvedKeybindingItem {
function _kbItem(keybinding: number | number[], command: string, when: ContextKeyExpression | undefined): ResolvedKeybindingItem {
return kbItem(keybinding, command, null, when, true);
}
@ -383,12 +383,17 @@ suite('KeybindingResolver', () => {
KeyMod.CtrlCmd | KeyCode.KeyG,
'eleven',
undefined
)
),
_kbItem(
[KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB],
'long multi chord',
undefined
),
];
const resolver = new KeybindingResolver(items, [], () => { });
const testKey = (commandId: string, expectedKeys: number[]) => {
const testKey = (commandId: string, expectedKeys: number[] | number[][]) => {
// Test lookup
const lookupResult = resolver.lookupKeybindings(commandId);
assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId);
@ -399,10 +404,10 @@ suite('KeybindingResolver', () => {
}
};
const testResolve = (ctx: IContext, _expectedKey: number, commandId: string) => {
const testResolve = (ctx: IContext, _expectedKey: number | number[], commandId: string) => {
const expectedKeybinding = decodeKeybinding(_expectedKey, OS)!;
let previousChord: (string | null) = null;
let previousChord: string[] | null = null;
for (let i = 0, len = expectedKeybinding.chords.length; i < len; i++) {
const chord = getDispatchStr(<KeyCodeChord>expectedKeybinding.chords[i]);
const result = resolver.resolve(ctx, previousChord, chord);
@ -412,14 +417,24 @@ suite('KeybindingResolver', () => {
assert.ok(result !== null, `Enters multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.commandId, commandId, `Enters multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.enterMultiChord, false, `Enters multi chord for ${commandId} at chord ${i}`);
} else if (i > 0) {
// if this is an intermediate chord, we should not find a valid command,
// and there should be an open chord we continue.
assert.ok(result !== null, `Continues multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.commandId, null, `Continues multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.enterMultiChord, false, `Is already in multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.leaveMultiChord, false, `Does not leave multi chord for ${commandId} at chord ${i}`);
} else {
// if it's not the final chord, then we should not find a valid command,
// and there should be a chord.
// if it's not the final chord and not an intermediate, then we should not
// find a valid command, and we should enter a chord.
assert.ok(result !== null, `Enters multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.commandId, null, `Enters multi chord for ${commandId} at chord ${i}`);
assert.strictEqual(result!.enterMultiChord, true, `Enters multi chord for ${commandId} at chord ${i}`);
}
previousChord = chord;
if (previousChord === null) {
previousChord = [];
}
previousChord.push(chord);
}
};
@ -452,5 +467,8 @@ suite('KeybindingResolver', () => {
testResolve(createContext({}), KeyMod.CtrlCmd | KeyCode.KeyG, 'eleven');
testKey('sixth', []);
testKey('long multi chord', [[KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB]]);
testResolve(createContext({}), [KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB], 'long multi chord');
});
});

View file

@ -7,7 +7,7 @@ import { decodeKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings
import { OperatingSystem } from 'vs/base/common/platform';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
export function createUSLayoutResolvedKeybinding(encodedKeybinding: number, OS: OperatingSystem): ResolvedKeybinding | undefined {
export function createUSLayoutResolvedKeybinding(encodedKeybinding: number | number[], OS: OperatingSystem): ResolvedKeybinding | undefined {
if (encodedKeybinding === 0) {
return undefined;
}

View file

@ -955,12 +955,12 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
// Select element when keys are pressed that signal it
const quickNavKeys = this._quickNavigate.keybindings;
const wasTriggerKeyPressed = quickNavKeys.some(k => {
const [firstChord, secondChord] = k.getChords();// TODO@chords
if (secondChord) {
const chords = k.getChords();
if (chords.length > 1) {
return false;
}
if (firstChord.shiftKey && keyCode === KeyCode.Shift) {
if (chords[0].shiftKey && keyCode === KeyCode.Shift) {
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
return false; // this is an optimistic check for the shift key being used to navigate back in quick input
}
@ -968,15 +968,15 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
return true;
}
if (firstChord.altKey && keyCode === KeyCode.Alt) {
if (chords[0].altKey && keyCode === KeyCode.Alt) {
return true;
}
if (firstChord.ctrlKey && keyCode === KeyCode.Ctrl) {
if (chords[0].ctrlKey && keyCode === KeyCode.Ctrl) {
return true;
}
if (firstChord.metaKey && keyCode === KeyCode.Meta) {
if (chords[0].metaKey && keyCode === KeyCode.Meta) {
return true;
}

View file

@ -35,14 +35,13 @@ export interface KeybindingsSearchOptions extends SearchOptions {
export class KeybindingsSearchWidget extends SearchWidget {
private _firstChord: ResolvedKeybinding | null;
private _secondChord: ResolvedKeybinding | null;
private _chords: ResolvedKeybinding[] | null;
private _inputValue: string;
private readonly recordDisposables = this._register(new DisposableStore());
private _onKeybinding = this._register(new Emitter<[ResolvedKeybinding | null, ResolvedKeybinding | null]>());
readonly onKeybinding: Event<[ResolvedKeybinding | null, ResolvedKeybinding | null]> = this._onKeybinding.event;
private _onKeybinding = this._register(new Emitter<ResolvedKeybinding[] | null>());
readonly onKeybinding: Event<ResolvedKeybinding[] | null> = this._onKeybinding.event;
private _onEnter = this._register(new Emitter<void>());
readonly onEnter: Event<void> = this._onEnter.event;
@ -61,8 +60,7 @@ export class KeybindingsSearchWidget extends SearchWidget {
) {
super(parent, options, contextViewService, instantiationService, contextKeyService, keybindingService);
this._register(toDisposable(() => this.stopRecordingKeys()));
this._firstChord = null;
this._secondChord = null;
this._chords = null;
this._inputValue = '';
this._reset();
@ -93,8 +91,7 @@ export class KeybindingsSearchWidget extends SearchWidget {
}
private _reset() {
this._firstChord = null;
this._secondChord = null;
this._chords = null;
}
private _onKeyDown(keyboardEvent: IKeyboardEvent): void {
@ -117,29 +114,26 @@ export class KeybindingsSearchWidget extends SearchWidget {
const info = `code: ${keyboardEvent.browserEvent.code}, keyCode: ${keyboardEvent.browserEvent.keyCode}, key: ${keyboardEvent.browserEvent.key} => UI: ${keybinding.getAriaLabel()}, user settings: ${keybinding.getUserSettingsLabel()}, dispatch: ${keybinding.getDispatchChords()[0]}`;
const options = this.options as KeybindingsSearchOptions;
const hasFirstChord = (this._firstChord && this._firstChord.getDispatchChords()[0] !== null);
const hasSecondChord = (this._secondChord && this._secondChord.getDispatchChords()[0] !== null);
if (hasFirstChord && hasSecondChord) {
// Reset
this._firstChord = keybinding;
this._secondChord = null;
} else if (!hasFirstChord) {
this._firstChord = keybinding;
} else {
this._secondChord = keybinding;
if (!this._chords) {
this._chords = [];
}
let value = '';
if (this._firstChord) {
value = (this._firstChord.getUserSettingsLabel() || '');
}
if (this._secondChord) {
value = value + ' ' + this._secondChord.getUserSettingsLabel();
// TODO: note that we allow a keybinding "shift shift", but this widget doesn't allow input "shift shift" because the first "shift" will be incomplete - this is _not_ a regression
const hasIncompleteChord = this._chords.length > 0 && this._chords[this._chords.length - 1].getDispatchChords()[0] === null;
if (hasIncompleteChord) {
this._chords[this._chords.length - 1] = keybinding;
} else {
if (this._chords.length === 2) { // TODO: limit chords # to 2 for now
this._chords = [];
}
this._chords.push(keybinding);
}
const value = this._chords.map((keybinding) => keybinding.getUserSettingsLabel() || '').join(' ');
this.setInputValue(options.quoteRecordedKeys ? `"${value}"` : value);
this.inputBox.inputElement.title = info;
this._onKeybinding.fire([this._firstChord, this._secondChord]);
this._onKeybinding.fire(this._chords);
}
}
@ -153,8 +147,7 @@ export class DefineKeybindingWidget extends Widget {
private _outputNode: HTMLElement;
private _showExistingKeybindingsNode: HTMLElement;
private _firstChord: ResolvedKeybinding | null = null;
private _secondChord: ResolvedKeybinding | null = null;
private _chords: ResolvedKeybinding[] | null = null;
private _isVisible: boolean = false;
private _onHide = this._register(new Emitter<void>());
@ -210,8 +203,7 @@ export class DefineKeybindingWidget extends Widget {
this._isVisible = true;
this._domNode.setDisplay('block');
this._firstChord = null;
this._secondChord = null;
this._chords = null;
this._keybindingInputWidget.setInputValue('');
dom.clearNode(this._outputNode);
dom.clearNode(this._showExistingKeybindingsNode);
@ -250,20 +242,24 @@ export class DefineKeybindingWidget extends Widget {
}
}
private onKeybinding(keybinding: [ResolvedKeybinding | null, ResolvedKeybinding | null]): void {
const [firstChord, secondChord] = keybinding;
this._firstChord = firstChord;
this._secondChord = secondChord;
private onKeybinding(keybinding: ResolvedKeybinding[] | null): void {
this._chords = keybinding;
dom.clearNode(this._outputNode);
dom.clearNode(this._showExistingKeybindingsNode);
const firstLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles);
firstLabel.set(withNullAsUndefined(this._firstChord));
if (this._secondChord) {
this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to")));
const chordLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles);
chordLabel.set(this._secondChord);
const firstLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles);
firstLabel.set(withNullAsUndefined(this._chords?.[0]));
if (this._chords) {
for (let i = 1; i < this._chords.length; i++) {
this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to")));
const chordLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles);
chordLabel.set(this._chords[i]);
}
}
const label = this.getUserSettingsLabel();
@ -274,18 +270,14 @@ export class DefineKeybindingWidget extends Widget {
private getUserSettingsLabel(): string | null {
let label: string | null = null;
if (this._firstChord) {
label = this._firstChord.getUserSettingsLabel();
if (this._secondChord) {
label = label + ' ' + this._secondChord.getUserSettingsLabel();
}
if (this._chords) {
label = this._chords.map(keybinding => keybinding.getUserSettingsLabel()).join(' ');
}
return label;
}
private onCancel(): void {
this._firstChord = null;
this._secondChord = null;
this._chords = null;
this.hide();
}