Fix showing keybindings in code action widget (#168254)

This also refactors how keybindings are passed in to remove the concept of a `IActionKeybindingResolver`
This commit is contained in:
Matt Bierner 2022-12-06 16:34:19 -08:00 committed by GitHub
parent a49a1db99e
commit 220f9387c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 52 additions and 45 deletions

View file

@ -8,7 +8,6 @@ import { Lazy } from 'vs/base/common/lazy';
import { CodeAction } from 'vs/editor/common/languages'; import { CodeAction } from 'vs/editor/common/languages';
import { codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction';
import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types';
import { IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
interface ResolveCodeActionKeybinding { interface ResolveCodeActionKeybinding {
@ -17,7 +16,7 @@ interface ResolveCodeActionKeybinding {
readonly resolvedKeybinding: ResolvedKeybinding; readonly resolvedKeybinding: ResolvedKeybinding;
} }
export class CodeActionKeybindingResolver implements IActionKeybindingResolver { export class CodeActionKeybindingResolver {
private static readonly codeActionCommands: readonly string[] = [ private static readonly codeActionCommands: readonly string[] = [
refactorCommandId, refactorCommandId,
codeActionCommandId, codeActionCommandId,
@ -27,7 +26,7 @@ export class CodeActionKeybindingResolver implements IActionKeybindingResolver {
]; ];
constructor( constructor(
private readonly keybindingService: IKeybindingService @IKeybindingService private readonly keybindingService: IKeybindingService
) { } ) { }
public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined { public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined {

View file

@ -5,12 +5,14 @@
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { CodeAction } from 'vs/editor/common/languages';
import { CodeActionItem, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import { CodeActionItem, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types';
import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { ActionListItemKind, IListMenuItem } from 'vs/platform/actionWidget/browser/actionList'; import { ActionListItemKind, IListMenuItem } from 'vs/platform/actionWidget/browser/actionList';
export interface ActionGroup { interface ActionGroup {
readonly kind: CodeActionKind; readonly kind: CodeActionKind;
readonly title: string; readonly title: string;
readonly icon?: { readonly codicon: Codicon; readonly color?: string }; readonly icon?: { readonly codicon: Codicon; readonly color?: string };
@ -29,7 +31,11 @@ const codeActionGroups = Object.freeze<ActionGroup[]>([
uncategorizedCodeActionGroup, uncategorizedCodeActionGroup,
]); ]);
export function toMenuItems(inputCodeActions: readonly CodeActionItem[], showHeaders: boolean): IListMenuItem<CodeActionItem>[] { export function toMenuItems(
inputCodeActions: readonly CodeActionItem[],
showHeaders: boolean,
keybindingResolver: (action: CodeAction) => ResolvedKeybinding | undefined
): IListMenuItem<CodeActionItem>[] {
if (!showHeaders) { if (!showHeaders) {
return inputCodeActions.map((action): IListMenuItem<CodeActionItem> => { return inputCodeActions.map((action): IListMenuItem<CodeActionItem> => {
return { return {
@ -60,7 +66,14 @@ export function toMenuItems(inputCodeActions: readonly CodeActionItem[], showHea
if (menuEntry.actions.length) { if (menuEntry.actions.length) {
allMenuItems.push({ kind: ActionListItemKind.Header, group: menuEntry.group }); allMenuItems.push({ kind: ActionListItemKind.Header, group: menuEntry.group });
for (const action of menuEntry.actions) { for (const action of menuEntry.actions) {
allMenuItems.push({ kind: ActionListItemKind.Action, item: action, group: menuEntry.group, label: action.action.title, disabled: !!action.action.disabled }); allMenuItems.push({
kind: ActionListItemKind.Action,
item: action,
group: menuEntry.group,
label: action.action.title,
disabled: !!action.action.disabled,
keybinding: keybindingResolver(action.action),
});
} }
} }
} }

View file

@ -13,7 +13,8 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IPosition, Position } from 'vs/editor/common/core/position'; import { IPosition, Position } from 'vs/editor/common/core/position';
import { ScrollType } from 'vs/editor/common/editorCommon'; import { ScrollType } from 'vs/editor/common/editorCommon';
import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { CodeActionTriggerType } from 'vs/editor/common/languages';
import { toMenuItems } from 'vs/editor/contrib/codeAction/browser/codeActionMenuItems'; import { CodeActionKeybindingResolver } from 'vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver';
import { toMenuItems } from 'vs/editor/contrib/codeAction/browser/codeActionMenu';
import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IActionWidgetService, IRenderDelegate } from 'vs/platform/actionWidget/browser/actionWidget'; import { IActionWidgetService, IRenderDelegate } from 'vs/platform/actionWidget/browser/actionWidget';
@ -30,9 +31,12 @@ export interface IActionShowOptions {
} }
export class CodeActionUi extends Disposable { export class CodeActionUi extends Disposable {
private readonly _lightBulbWidget: Lazy<LightBulbWidget>; private readonly _lightBulbWidget: Lazy<LightBulbWidget>;
private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>()); private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>());
private readonly _resolver: CodeActionKeybindingResolver;
#disposed = false; #disposed = false;
private _showDisabled = false; private _showDisabled = false;
@ -44,8 +48,8 @@ export class CodeActionUi extends Disposable {
private readonly delegate: { private readonly delegate: {
applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean, preview: boolean) => Promise<void>; applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean, preview: boolean) => Promise<void>;
}, },
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly _configurationService: IConfigurationService, @IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService readonly instantiationService: IInstantiationService,
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
@ICommandService private readonly _commandService: ICommandService, @ICommandService private readonly _commandService: ICommandService,
) { ) {
@ -57,6 +61,8 @@ export class CodeActionUi extends Disposable {
return widget; return widget;
}); });
this._resolver = instantiationService.createInstance(CodeActionKeybindingResolver);
this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide())); this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide()));
} }
@ -188,7 +194,7 @@ export class CodeActionUi extends Disposable {
this._actionWidgetService.show( this._actionWidgetService.show(
'codeActionWidget', 'codeActionWidget',
true, true,
toMenuItems(actionsToShow, this._shouldShowHeaders()), toMenuItems(actionsToShow, this._shouldShowHeaders(), this._resolver.getResolver()),
delegate, delegate,
anchor, anchor,
editorDom, editorDom,

View file

@ -193,8 +193,7 @@ export class CodeActionItem implements IActionItem {
constructor( constructor(
public readonly action: languages.CodeAction, public readonly action: languages.CodeAction,
public readonly provider: languages.CodeActionProvider | undefined, public readonly provider: languages.CodeActionProvider | undefined,
) { ) { }
}
async resolve(token: CancellationToken): Promise<this> { async resolve(token: CancellationToken): Promise<this> {
if (this.provider?.resolveCodeAction && !this.action.edit) { if (this.provider?.resolveCodeAction && !this.action.edit) {

View file

@ -8,11 +8,12 @@ import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLa
import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { List } from 'vs/base/browser/ui/list/listWidget'; import { List } from 'vs/base/browser/ui/list/listWidget';
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { OS } from 'vs/base/common/platform'; import { OS } from 'vs/base/common/platform';
import 'vs/css!./actionWidget'; import 'vs/css!./actionWidget';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IActionItem, IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget'; import { IActionItem } from 'vs/platform/actionWidget/common/actionWidget';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@ -25,12 +26,14 @@ export interface IRenderDelegate {
} }
export interface IListMenuItem<T extends IActionItem> { export interface IListMenuItem<T extends IActionItem> {
item?: T; readonly item?: T;
kind: ActionListItemKind; readonly kind: ActionListItemKind;
group?: { kind?: any; icon?: { codicon: Codicon; color?: string }; title: string }; readonly group?: { kind?: any; icon?: { codicon: Codicon; color?: string }; title: string };
disabled?: boolean; readonly disabled?: boolean;
label?: string; readonly label?: string;
description?: string; readonly description?: string;
readonly keybinding?: ResolvedKeybinding;
} }
interface IActionMenuTemplateData { interface IActionMenuTemplateData {
@ -64,10 +67,7 @@ class HeaderRenderer<T extends IListMenuItem<IActionItem>> implements IListRende
} }
renderElement(element: IListMenuItem<IActionItem>, _index: number, templateData: IHeaderTemplateData): void { renderElement(element: IListMenuItem<IActionItem>, _index: number, templateData: IHeaderTemplateData): void {
if (!element.group) { templateData.text.textContent = element.group?.title ?? '';
return;
}
templateData.text.textContent = element.group?.title;
} }
disposeTemplate(_templateData: IHeaderTemplateData): void { disposeTemplate(_templateData: IHeaderTemplateData): void {
@ -77,11 +77,10 @@ class HeaderRenderer<T extends IListMenuItem<IActionItem>> implements IListRende
class ActionItemRenderer<T extends IListMenuItem<IActionItem>> implements IListRenderer<T, IActionMenuTemplateData> { class ActionItemRenderer<T extends IListMenuItem<IActionItem>> implements IListRenderer<T, IActionMenuTemplateData> {
get templateId(): string { return 'action'; } get templateId(): string { return ActionListItemKind.Action; }
constructor( constructor(
private readonly _supportsPreview: boolean, private readonly _supportsPreview: boolean,
private readonly _keybindingResolver: IActionKeybindingResolver | undefined,
@IKeybindingService private readonly _keybindingService: IKeybindingService @IKeybindingService private readonly _keybindingService: IKeybindingService
) { } ) { }
@ -109,18 +108,17 @@ class ActionItemRenderer<T extends IListMenuItem<IActionItem>> implements IListR
data.icon.className = Codicon.lightBulb.classNames; data.icon.className = Codicon.lightBulb.classNames;
data.icon.style.color = 'var(--vscode-editorLightBulb-foreground)'; data.icon.style.color = 'var(--vscode-editorLightBulb-foreground)';
} }
if (!element.item || !element.label) { if (!element.item || !element.label) {
return; return;
} }
data.text.textContent = stripNewlines(element.label);
const binding = this._keybindingResolver?.getResolver()(element.item);
if (binding) {
data.keybinding.set(binding);
}
if (!binding) { data.text.textContent = stripNewlines(element.label);
if (!element.keybinding) {
dom.hide(data.keybinding.element); dom.hide(data.keybinding.element);
} else { } else {
data.keybinding.set(element.keybinding);
dom.show(data.keybinding.element); dom.show(data.keybinding.element);
} }
@ -138,6 +136,7 @@ class ActionItemRenderer<T extends IListMenuItem<IActionItem>> implements IListR
} else { } else {
data.container.title = ''; data.container.title = '';
} }
if (element.description) { if (element.description) {
const label = new HighlightedLabel(dom.append(data.container, dom.$('span.label-description'))); const label = new HighlightedLabel(dom.append(data.container, dom.$('span.label-description')));
label.element.classList.add('action-list-description'); label.element.classList.add('action-list-description');
@ -159,14 +158,13 @@ export class ActionList<T extends IActionItem> extends Disposable {
private readonly _actionLineHeight = 24; private readonly _actionLineHeight = 24;
private readonly _headerLineHeight = 26; private readonly _headerLineHeight = 26;
private readonly _allMenuItems: IListMenuItem<IActionItem>[]; private readonly _allMenuItems: readonly IListMenuItem<IActionItem>[];
constructor( constructor(
user: string, user: string,
preview: boolean, preview: boolean,
items: IListMenuItem<T>[], items: readonly IListMenuItem<T>[],
private readonly _delegate: IRenderDelegate, private readonly _delegate: IRenderDelegate,
resolver: IActionKeybindingResolver | undefined,
@IContextViewService private readonly _contextViewService: IContextViewService, @IContextViewService private readonly _contextViewService: IContextViewService,
@IKeybindingService private readonly _keybindingService: IKeybindingService @IKeybindingService private readonly _keybindingService: IKeybindingService
) { ) {
@ -178,7 +176,7 @@ export class ActionList<T extends IActionItem> extends Disposable {
getHeight: element => element.kind === ActionListItemKind.Header ? this._headerLineHeight : this._actionLineHeight, getHeight: element => element.kind === ActionListItemKind.Header ? this._headerLineHeight : this._actionLineHeight,
getTemplateId: element => element.kind getTemplateId: element => element.kind
}; };
this._list = this._register(new List(user, this.domNode, virtualDelegate, [new ActionItemRenderer<IListMenuItem<IActionItem>>(preview, resolver, this._keybindingService), new HeaderRenderer()], { this._list = this._register(new List(user, this.domNode, virtualDelegate, [new ActionItemRenderer<IListMenuItem<IActionItem>>(preview, this._keybindingService), new HeaderRenderer()], {
keyboardSupport: false, keyboardSupport: false,
accessibilityProvider: { accessibilityProvider: {
getAriaLabel: element => { getAriaLabel: element => {

View file

@ -87,9 +87,6 @@
cursor: default !important; cursor: default !important;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
background-color: transparent !important; background-color: transparent !important;
outline: 0 solid !important; outline: 0 solid !important;

View file

@ -12,7 +12,7 @@ import 'vs/css!./actionWidget';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { acceptSelectedActionCommand, ActionList, IListMenuItem, previewSelectedActionCommand } from 'vs/platform/actionWidget/browser/actionList'; import { acceptSelectedActionCommand, ActionList, IListMenuItem, previewSelectedActionCommand } from 'vs/platform/actionWidget/browser/actionList';
import { IActionItem, IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget'; import { IActionItem } from 'vs/platform/actionWidget/common/actionWidget';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
@ -58,10 +58,10 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
super(); super();
} }
async show(user: string, supportsPreview: boolean, items: IListMenuItem<IActionItem>[], delegate: IRenderDelegate<any>, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], resolver?: IActionKeybindingResolver): Promise<void> { async show(user: string, supportsPreview: boolean, items: IListMenuItem<IActionItem>[], delegate: IRenderDelegate<any>, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): Promise<void> {
const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService); const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService);
const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, resolver); const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate);
this.contextViewService.showContextView({ this.contextViewService.showContextView({
getAnchor: () => anchor, getAnchor: () => anchor,
render: (container: HTMLElement) => { render: (container: HTMLElement) => {

View file

@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
export interface ActionSet<T> extends IDisposable { export interface ActionSet<T> extends IDisposable {
@ -16,7 +15,3 @@ export interface IActionItem {
// TODO: Use generics // TODO: Use generics
action: any; action: any;
} }
export interface IActionKeybindingResolver {
getResolver(): (action: any) => ResolvedKeybinding | undefined;
}