Skip to content

Commit

Permalink
Merge pull request #170291 from microsoft/sandy081/fix116855
Browse files Browse the repository at this point in the history
use code editor for when input
  • Loading branch information
sandy081 committed Jan 5, 2023
2 parents 18820e4 + 8a9f56c commit c912bfb
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext }
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';

export interface SuggestResultsProvider {
/**
Expand Down Expand Up @@ -72,12 +73,22 @@ interface SuggestEnabledInputOptions {
* Defaults to the empty string.
*/
placeholderText?: string;

/**
* Initial value to be shown
*/
value?: string;

/**
* Context key tracking the focus state of this element
*/
focusContextKey?: IContextKey<boolean>;

/**
* Place overflow widgets inside an external DOM node.
* Defaults to an internal DOM node.
*/
overflowWidgetsDomNode?: HTMLElement;
}

export interface ISuggestEnabledInputStyleOverrides extends IStyleOverrides {
Expand Down Expand Up @@ -105,9 +116,6 @@ export class SuggestEnabledInput extends Widget implements IThemable {
private readonly _onShouldFocusResults = new Emitter<void>();
readonly onShouldFocusResults: Event<void> = this._onShouldFocusResults.event;

private readonly _onEnter = new Emitter<void>();
readonly onEnter: Event<void> = this._onEnter.event;

private readonly _onInputDidChange = new Emitter<string | undefined>();
readonly onInputDidChange: Event<string | undefined> = this._onInputDidChange.event;

Expand Down Expand Up @@ -141,9 +149,10 @@ export class SuggestEnabledInput extends Widget implements IThemable {
this.element = parent;
this.placeholderText = append(this.stylingContainer, $('.suggest-input-placeholder', undefined, options.placeholderText || ''));

const editorOptions: IEditorOptions = mixin(
const editorOptions: IEditorConstructionOptions = mixin(
getSimpleEditorOptions(),
getSuggestEnabledInputOptions(ariaLabel));
editorOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode;

const scopedContextKeyService = this.getScopedContextKeyService(contextKeyService);

Expand Down Expand Up @@ -184,7 +193,6 @@ export class SuggestEnabledInput extends Widget implements IThemable {
})));

const onKeyDownMonaco = Event.chain(this.inputWidget.onKeyDown);
this._register(onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => { e.preventDefault(); this._onEnter.fire(); }, this));
this._register(onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow && (isMacintosh ? e.metaKey : e.ctrlKey)).on(() => this._onShouldFocusResults.fire(), this));

let preexistingContent = this.getValue();
Expand Down
120 changes: 54 additions & 66 deletions src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
import { Delayer } from 'vs/base/common/async';
import * as DOM from 'vs/base/browser/dom';
import { isIOS, OS } from 'vs/base/common/platform';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ToggleActionViewItem } from 'vs/base/browser/ui/toggle/toggle';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
Expand All @@ -23,11 +23,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IKeybindingService, IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { DefineKeybindingWidget, KeybindingsSearchWidget } from 'vs/workbench/contrib/preferences/browser/keybindingWidgets';
import { CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, CONTEXT_WHEN_FOCUS } from 'vs/workbench/contrib/preferences/common/preferences';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeyCode } from 'vs/base/common/keyCodes';
import { badgeBackground, contrastBorder, badgeForeground, listActiveSelectionForeground, listInactiveSelectionForeground, listHoverForeground, listFocusForeground, editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground, listFocusBackground, listHoverBackground, registerColor, tableOddRowsBackgroundColor } from 'vs/platform/theme/common/colorRegistry';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
Expand All @@ -51,7 +51,8 @@ import { defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultToggleStyle
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { isString } from 'vs/base/common/types';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput';
import { CompletionItemKind } from 'vs/editor/common/languages';

const $ = DOM.$;

Expand All @@ -62,6 +63,12 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
private _onDefineWhenExpression: Emitter<IKeybindingItemEntry> = this._register(new Emitter<IKeybindingItemEntry>());
readonly onDefineWhenExpression: Event<IKeybindingItemEntry> = this._onDefineWhenExpression.event;

private _onRejectWhenExpression = this._register(new Emitter<IKeybindingItemEntry>());
readonly onRejectWhenExpression = this._onRejectWhenExpression.event;

private _onAcceptWhenExpression = this._register(new Emitter<IKeybindingItemEntry>());
readonly onAcceptWhenExpression = this._onAcceptWhenExpression.event;

private _onLayout: Emitter<void> = this._register(new Emitter<void>());
readonly onLayout: Event<void> = this._onLayout.event;

Expand Down Expand Up @@ -91,6 +98,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
private readonly recordKeysAction: Action;

private ariaLabelElement!: HTMLElement;
readonly overflowWidgetsDomNode: HTMLElement;

constructor(
@ITelemetryService telemetryService: ITelemetryService,
Expand Down Expand Up @@ -119,6 +127,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP

this.sortByPrecedenceAction = new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon));
this.sortByPrecedenceAction.checked = false;
this.overflowWidgetsDomNode = $('.keybindings-overflow-widgets-container.monaco-editor');
}

protected createEditor(parent: HTMLElement): void {
Expand Down Expand Up @@ -191,6 +200,14 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
}
}

rejectWhenExpression(keybindingEntry: IKeybindingItemEntry): void {
this._onRejectWhenExpression.fire(keybindingEntry);
}

acceptWhenExpression(keybindingEntry: IKeybindingItemEntry): void {
this._onAcceptWhenExpression.fire(keybindingEntry);
}

async updateKeybinding(keybindingEntry: IKeybindingItemEntry, key: string, when: string | undefined, add?: boolean): Promise<void> {
const currentKey = keybindingEntry.keybindingItem.keybinding ? keybindingEntry.keybindingItem.keybinding.getUserSettingsLabel() : '';
if (currentKey !== key || keybindingEntry.keybindingItem.when !== when) {
Expand Down Expand Up @@ -468,6 +485,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
multipleSelectionSupport: false,
setRowLineHeight: false,
openOnSingleClick: false,
transformOptimization: false // disable transform optimization as it causes the editor overflow widgets to be mispositioned
}
)) as WorkbenchTable<IKeybindingItemEntry>;

Expand All @@ -491,6 +509,8 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
this.defineKeybinding(activeKeybindingEntry, false);
}
}));

DOM.append(this.keybindingsTableContainer, this.overflowWidgetsDomNode);
}

private async render(preserveFocus: boolean): Promise<void> {
Expand Down Expand Up @@ -1023,8 +1043,7 @@ class SourceColumnRenderer implements ITableRenderer<IKeybindingItemEntry, ISour

class WhenInputWidget extends Disposable {

private readonly input: InputBox;
readonly el: HTMLElement;
private readonly input: SuggestEnabledInput;

private readonly _onDidAccept = this._register(new Emitter<string>());
readonly onDidAccept = this._onDidAccept.event;
Expand All @@ -1033,70 +1052,42 @@ class WhenInputWidget extends Disposable {
readonly onDidReject = this._onDidReject.event;

constructor(
parent: HTMLElement,
keybindingsEditor: KeybindingsEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService contextViewService: IContextViewService,
) {
super();

this.input = new InputBox(this.el = DOM.$('.when-input'), contextViewService, {
validationOptions: {
validation: (value) => {
try {
ContextKeyExpr.deserialize(value, true);
} catch (error) {
return {
content: error.message,
formatContent: true,
type: MessageType.ERROR
};
}
return null;
const focusContextKey = CONTEXT_WHEN_FOCUS.bindTo(contextKeyService);
this.input = this._register(instantiationService.createInstance(SuggestEnabledInput, 'keyboardshortcutseditor#wheninput', parent, {
provideResults: () => {
const result = [];
for (const contextKey of RawContextKey.all()) {
result.push({ label: contextKey.key, documentation: contextKey.description, detail: contextKey.type, kind: CompletionItemKind.Constant });
}
return result;
},
ariaLabel: localize('whenContextInputAriaLabel', "Type when context. Press Enter to confirm or Escape to cancel."),
inputBoxStyles: defaultInputBoxStyles
});

this._register(DOM.addStandardDisposableListener(this.input.inputElement, DOM.EventType.KEY_DOWN, e => {
let handled = false;
if (e.equals(KeyCode.Enter)) {
this._onDidAccept.fire(this.input.value);
handled = true;
} else if (e.equals(KeyCode.Escape)) {
this._onDidReject.fire();
handled = true;
}
if (handled) {
e.preventDefault();
e.stopPropagation();
}
}));
triggerCharacters: ['!'],
}, '', `keyboardshortcutseditor#wheninput`, { focusContextKey, overflowWidgetsDomNode: keybindingsEditor.overflowWidgetsDomNode }));

const whenFocusContextKey = CONTEXT_WHEN_FOCUS.bindTo(contextKeyService);
this._register((DOM.addDisposableListener(this.input.inputElement, DOM.EventType.FOCUS, () => whenFocusContextKey.set(true))));
this._register((DOM.addDisposableListener(this.input.inputElement, DOM.EventType.BLUR, () => {
whenFocusContextKey.set(false);
this._onDidReject.fire();
})));
this._register(attachSuggestEnabledInputBoxStyler(this.input, themeService, {}));
this._register((DOM.addDisposableListener(this.input.element, DOM.EventType.DBLCLICK, e => DOM.EventHelper.stop(e))));
this._register(toDisposable(() => focusContextKey.reset()));

// stop double click action on the input #148493
this._register((DOM.addDisposableListener(this.input.inputElement, DOM.EventType.DBLCLICK, e => DOM.EventHelper.stop(e))));
this._register(keybindingsEditor.onAcceptWhenExpression(() => this._onDidAccept.fire(this.input.getValue())));
this._register(Event.any(keybindingsEditor.onRejectWhenExpression, this.input.onDidBlur)(() => this._onDidReject.fire()));
}

layout(dimension: DOM.Dimension): void {
this.input.element.style.height = `${dimension.height}px`;
this.input.layout(dimension);
}

show(value: string): void {
DOM.show(this.el);
this.input.value = value;
this.input.focus();
this.input.select();
this.input.setValue(value);
this.input.focus(true);
}

hide(): void {
DOM.hide(this.el);
}
}

interface IWhenColumnTemplateData {
Expand All @@ -1112,14 +1103,11 @@ class WhenColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IWhenCo
static readonly TEMPLATE_ID = 'when';

readonly templateId: string = WhenColumnRenderer.TEMPLATE_ID;
private readonly whenInputWidget: WhenInputWidget;

constructor(
private readonly keybindingsEditor: KeybindingsEditor,
@IInstantiationService instantiationService: IInstantiationService,
) {
this.whenInputWidget = instantiationService.createInstance(WhenInputWidget);
}
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }

renderTemplate(container: HTMLElement): IWhenColumnTemplateData {
const element = DOM.append(container, $('.when'));
Expand All @@ -1144,25 +1132,25 @@ class WhenColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IWhenCo
templateData.disposables.add(this.keybindingsEditor.onDefineWhenExpression(e => {
if (keybindingItemEntry === e) {
templateData.element.classList.add('input-mode');
DOM.append(templateData.whenInputContainer, this.whenInputWidget.el);
this.whenInputWidget.show(keybindingItemEntry.keybindingItem.when || '');
this.whenInputWidget.layout(new DOM.Dimension(templateData.element.parentElement!.clientWidth, 24));

const inputWidget = whenInputDisposables.add(this.instantiationService.createInstance(WhenInputWidget, templateData.whenInputContainer, this.keybindingsEditor));
inputWidget.layout(new DOM.Dimension(templateData.element.parentElement!.clientWidth, 18));
inputWidget.show(keybindingItemEntry.keybindingItem.when || '');

const hideInputWidget = () => {
whenInputDisposables.clear();
templateData.element.classList.remove('input-mode');
templateData.element.parentElement!.style.paddingLeft = '10px';
DOM.clearNode(templateData.whenInputContainer);
this.whenInputWidget.hide();
};

whenInputDisposables.add(this.whenInputWidget.onDidAccept(value => {
whenInputDisposables.add(inputWidget.onDidAccept(value => {
hideInputWidget();
this.keybindingsEditor.updateKeybinding(keybindingItemEntry, keybindingItemEntry.keybindingItem.keybinding ? keybindingItemEntry.keybindingItem.keybinding.getUserSettingsLabel() || '' : '', value);
this.keybindingsEditor.selectKeybinding(keybindingItemEntry);
}));

whenInputDisposables.add(this.whenInputWidget.onDidReject(value => {
whenInputDisposables.add(inputWidget.onDidReject(() => {
hideInputWidget();
this.keybindingsEditor.selectKeybinding(keybindingItemEntry);
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
padding: 11px 0px 0px 27px;
}

.keybindings-overflow-widgets-container {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
overflow: visible;
z-index: 5000;
}

/* header styling */

.keybindings-editor > .keybindings-header {
Expand Down

0 comments on commit c912bfb

Please sign in to comment.