Skip to content

Commit 567d04c

Browse files
authored
Added action bar to Code Action Widget (microsoft#158399)
Action bar with button that shows/hides disabled code actions in the list.
1 parent feaf34b commit 567d04c

File tree

2 files changed

+141
-30
lines changed

2 files changed

+141
-30
lines changed

src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts

Lines changed: 121 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
3333
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
3434
import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors
3535
import { Codicon } from 'vs/base/common/codicons';
36+
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
3637

3738
export const Context = {
3839
Visible: new RawContextKey<boolean>('CodeActionMenuVisible', false, localize('CodeActionMenuVisible', "Whether the code action list widget is visible"))
@@ -74,6 +75,19 @@ export interface ICodeActionMenuItem {
7475
headerTitle: string;
7576
index: number;
7677
disposables?: IDisposable[];
78+
params: ICodeActionMenuParameters;
79+
}
80+
81+
export interface ICodeActionMenuParameters {
82+
options: CodeActionShowOptions;
83+
trigger: CodeActionTrigger;
84+
anchor: { x: number; y: number };
85+
menuActions: IAction[];
86+
codeActions: CodeActionSet;
87+
visible: boolean;
88+
showDisabled: boolean;
89+
menuObj: CodeActionMenu;
90+
7791
}
7892

7993
export interface ICodeMenuOptions {
@@ -97,8 +111,10 @@ const TEMPLATE_ID = 'codeActionWidget';
97111
const codeActionLineHeight = 24;
98112
const headerLineHeight = 26;
99113

100-
class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> {
114+
// TODO: Take a look at user storage for this so it is preserved across windows and on reload.
115+
let showDisabled = false;
101116

117+
class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> {
102118
constructor(
103119
private readonly acceptKeybindings: [string, string],
104120
@IKeybindingService private readonly keybindingService: IKeybindingService,
@@ -129,6 +145,7 @@ class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeAction
129145
const isSeparator = element.isSeparator;
130146
const isHeader = element.isHeader;
131147

148+
// Renders differently based on element type.
132149
if (isSeparator) {
133150
data.root.classList.add('separator');
134151
data.root.style.height = '10px';
@@ -139,21 +156,50 @@ class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeAction
139156
data.root.classList.add('group-header');
140157
} else {
141158
const text = element.action.label;
142-
data.text.textContent = text;
143159
element.isEnabled = element.action.enabled;
144160

145161
if (element.action instanceof CodeActionAction) {
162+
const openedFromString = (element.params?.options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : element.params?.trigger.triggerAction;
163+
146164

147165
// Check documentation type
148166
element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
149167

150168
if (element.isDocumentation) {
151-
data.text.textContent = text;
169+
element.isEnabled = false;
152170
data.root.classList.add('documentation');
171+
172+
const container = data.root;
173+
174+
const actionbarContainer = dom.append(container, dom.$('.codeActionWidget-action-bar'));
175+
176+
const reRenderAction = showDisabled ?
177+
<IAction>{
178+
id: 'hideMoreCodeActions',
179+
label: localize('hideMoreCodeActions', 'Hide Disabled'),
180+
enabled: true,
181+
run: () => CodeActionMenu.toggleDisabledOptions(element.params)
182+
} :
183+
<IAction>{
184+
id: 'showMoreCodeActions',
185+
label: localize('showMoreCodeActions', 'Show Disabled'),
186+
enabled: true,
187+
run: () => CodeActionMenu.toggleDisabledOptions(element.params)
188+
};
189+
190+
const actionbar = new ActionBar(actionbarContainer);
191+
data.disposables.push(actionbar);
192+
193+
if (openedFromString === CodeActionTriggerSource.Refactor && (element.params.codeActions.validActions.length > 0 || element.params.codeActions.allActions.length === element.params.codeActions.validActions.length)) {
194+
actionbar.push([element.action, reRenderAction], { icon: false, label: true });
195+
} else {
196+
actionbar.push([element.action], { icon: false, label: true });
197+
}
153198
} else {
199+
data.text.textContent = text;
200+
154201
// Icons and Label modifaction based on group
155202
const group = element.action.action.kind;
156-
157203
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(group)))) {
158204
data.icon.className = Codicon.symbolArray.classNames;
159205
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(group)))) {
@@ -249,6 +295,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
249295
return this._visible;
250296
}
251297

298+
/**
299+
* Checks if the settings have enabled the new code action widget.
300+
*/
252301
private isCodeActionWidgetEnabled(model: ITextModel): boolean {
253302
return this._configurationService.getValue('editor.experimental.useCustomCodeActionMenu', {
254303
resource: model.uri
@@ -291,7 +340,10 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
291340
}
292341
}
293342

294-
private renderCodeActionMenuList(element: HTMLElement, inputArray: IAction[]): IDisposable {
343+
/**
344+
* Renders the code action widget given the provided actions.
345+
*/
346+
private renderCodeActionMenuList(element: HTMLElement, inputArray: IAction[], params: ICodeActionMenuParameters): IDisposable {
295347
const renderDisposables = new DisposableStore();
296348
const renderMenu = document.createElement('div');
297349

@@ -332,10 +384,10 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
332384
accessibilityProvider: {
333385
getAriaLabel: element => {
334386
if (element.action instanceof CodeActionAction) {
335-
const label = element.action.label;
387+
let label = element.action.label;
336388
if (!element.action.enabled) {
337389
if (element.action instanceof CodeActionAction) {
338-
localize({ key: 'customCodeActionWidget.labels', comment: ['Code action labels for accessibility.'] }, "{0}, Disabled Reason: {1}", label, element.action.action.disabled);
390+
label = localize({ key: 'customCodeActionWidget.labels', comment: ['Code action labels for accessibility.'] }, "{0}, Disabled Reason: {1}", label, element.action.action.disabled);
339391
}
340392
}
341393
return label;
@@ -366,9 +418,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
366418

367419
renderDisposables.add(this.codeActionList.value.onMouseClick(e => this._onListClick(e)));
368420
renderDisposables.add(this.codeActionList.value.onMouseOver(e => this._onListHover(e)));
369-
renderDisposables.add(this.codeActionList.value.onDidChangeFocus(e => this.codeActionList.value?.domFocus()));
421+
renderDisposables.add(this.codeActionList.value.onDidChangeFocus(() => this.codeActionList.value?.domFocus()));
370422
renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e)));
371-
renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget()));
423+
renderDisposables.add(this._editor.onDidLayoutChange(() => this.hideCodeActionWidget()));
372424

373425
// Filters and groups code actions by their group
374426
const menuEntries: IAction[][] = [];
@@ -383,7 +435,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
383435
const documentationGroup: IAction[] = [];
384436
const otherGroup: IAction[] = [];
385437

386-
inputArray.forEach((item, index) => {
438+
inputArray.forEach((item) => {
387439
if (item instanceof CodeActionAction) {
388440
const optionKind = item.action.kind;
389441

@@ -440,7 +492,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
440492
menuEntriesToPush(localize('codeAction.widget.id.more', 'More Actions...'), entry);
441493
}
442494
} else {
443-
// case for separator - not a code action action
495+
// case for separator - separators are not codeActionAction typed
444496
totalActionEntries.push(...entry);
445497
}
446498

@@ -459,7 +511,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
459511
this.hasSeparator = true;
460512
}
461513

462-
const menuItem = <ICodeActionMenuItem>{ action: item, isEnabled: item.enabled, isSeparator: currIsSeparator, index };
514+
const menuItem = <ICodeActionMenuItem>{ action: item, isEnabled: item.enabled, isSeparator: currIsSeparator, index, params };
463515
if (item.enabled) {
464516
this.viewItems.push(menuItem);
465517
}
@@ -486,7 +538,12 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
486538
});
487539

488540
// resize observer - can be used in the future since list widget supports dynamic height but not width
489-
const maxWidth = Math.max(...arr);
541+
let maxWidth = Math.max(...arr);
542+
543+
// If there are no actions, the minimum width is the width of the list widget's action bar.
544+
if (params.trigger.triggerAction === CodeActionTriggerSource.Refactor && maxWidth < 230) {
545+
maxWidth = 230;
546+
}
490547

491548
// 52 is the additional padding for the list widget (26 left, 26 right)
492549
renderMenu.style.width = maxWidth + 52 + 5 + 'px';
@@ -514,6 +571,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
514571
return renderDisposables;
515572
}
516573

574+
/**
575+
* Focuses on the previous item in the list using the list widget.
576+
*/
517577
protected focusPrevious() {
518578
if (typeof this.focusedEnabledItem === 'undefined') {
519579
this.focusedEnabledItem = this.viewItems[0].index;
@@ -537,6 +597,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
537597
return true;
538598
}
539599

600+
/**
601+
* Focuses on the next item in the list using the list widget.
602+
*/
540603
protected focusNext() {
541604
if (typeof this.focusedEnabledItem === 'undefined') {
542605
this.focusedEnabledItem = this.viewItems.length - 1;
@@ -582,7 +645,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
582645
this.focusedEnabledItem = 0;
583646
this.currSelectedItem = undefined;
584647
this.hasSeparator = false;
585-
this._contextViewService.hideContextView({ source: this });
648+
this._contextViewService.hideContextView();
586649
}
587650

588651
codeActionTelemetry(openedFromString: CodeActionTriggerSource, didCancel: boolean, CodeActions: CodeActionSet) {
@@ -608,12 +671,52 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
608671
});
609672
}
610673

674+
/**
675+
* Helper function to create a context view item using code action `params`.
676+
*/
677+
private showContextViewHelper(params: ICodeActionMenuParameters, menuActions: IAction[]) {
678+
this._contextViewService.showContextView({
679+
getAnchor: () => params.anchor,
680+
render: (container: HTMLElement) => this.renderCodeActionMenuList(container, menuActions, params),
681+
onHide: (didCancel: boolean) => {
682+
const openedFromString = (params.options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : params.trigger.triggerAction;
683+
this.codeActionTelemetry(openedFromString, didCancel, params.codeActions);
684+
this._visible = false;
685+
this._editor.focus();
686+
},
687+
},
688+
this._editor.getDomNode()!, false,
689+
);
690+
691+
}
692+
693+
/**
694+
* Toggles whether the disabled actions in the code action widget are visible or not.
695+
*/
696+
public static toggleDisabledOptions(params: ICodeActionMenuParameters): void {
697+
params.menuObj.hideCodeActionWidget();
698+
699+
showDisabled = !showDisabled;
700+
701+
const actionsToShow = showDisabled ? params.codeActions.allActions : params.codeActions.validActions;
702+
703+
const menuActions = params.menuObj.getMenuActions(params.trigger, actionsToShow, params.codeActions.documentation);
704+
705+
params.menuObj.showContextViewHelper(params, menuActions);
706+
}
707+
611708
public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise<void> {
612709
const model = this._editor.getModel();
613710
if (!model) {
614711
return;
615712
}
616-
const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
713+
714+
let actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
715+
716+
// If there are no refactorings, we should still show the menu and only displayed disabled actions without `enable` button.
717+
if (trigger.triggerAction === CodeActionTriggerSource.Refactor && codeActions.validActions.length > 0) {
718+
actionsToShow = showDisabled ? codeActions.allActions : codeActions.validActions;
719+
}
617720

618721
if (!actionsToShow.length) {
619722
this._visible = false;
@@ -632,24 +735,15 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
632735
const menuActions = this.getMenuActions(trigger, actionsToShow, codeActions.documentation);
633736

634737
const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 };
738+
739+
const params = <ICodeActionMenuParameters>{ options, trigger, codeActions, anchor, menuActions, showDisabled: true, visible: this._visible, menuObj: this };
635740
const resolver = this._keybindingResolver.getResolver();
636741

637742
const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM);
638743

639744

640745
if (this.isCodeActionWidgetEnabled(model)) {
641-
this._contextViewService.showContextView({
642-
getAnchor: () => anchor,
643-
render: (container: HTMLElement) => this.renderCodeActionMenuList(container, menuActions),
644-
onHide: (didCancel) => {
645-
const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction;
646-
this.codeActionTelemetry(openedFromString, didCancel, codeActions);
647-
this._visible = false;
648-
this._editor.focus();
649-
},
650-
},
651-
this._editor.getDomNode()!, false,
652-
);
746+
this.showContextViewHelper(params, menuActions);
653747
} else {
654748
this._contextMenuService.showContextMenu({
655749
domForShadowRoot: useShadowDOM ? this._editor.getDomNode()! : undefined,

src/vs/editor/contrib/codeAction/browser/media/action.css

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
}
2020

2121
.codeActionMenuWidget {
22-
padding: 0px 1px 3px 0px;
22+
padding: 0px 0px 3px 0px;
2323
overflow: auto;
2424
font-size: 13px;
2525
border-radius: 0px;
@@ -69,8 +69,12 @@
6969

7070

7171
.codeActionMenuWidget .monaco-list .monaco-list-row:hover:not(.option-disabled),
72-
.codeActionMenuWidget .monaco-list .moncao-list-row.focused:not(.option-disabled) {
73-
color: var(--vscode-menu-selectionForeground) !important;
72+
.codeActionMenuWidget .monaco-list .monaco-list-row.focused:not(.option-disabled) {
73+
background-color: var(--vscode-list-hoverBackground) !important;
74+
}
75+
76+
.codeActionMenuWidget .monaco-list .monaco-list-row.focused:not(.option-disabled) {
77+
outline: 0px solid !important;
7478
background-color: var(--vscode-menu-selectionBackground) !important;
7579
}
7680

@@ -134,3 +138,16 @@
134138
color: var(--vscode-editorLightBulb-foreground);
135139
}
136140

141+
.codeActionWidget-action-bar .action-label {
142+
color: var(--vscode-textLink-activeForeground);
143+
pointer-events: all;
144+
}
145+
146+
.codeActionWidget-action-bar .action-item {
147+
margin-right: 10px;
148+
pointer-events: none;
149+
}
150+
151+
.codeActionWidget-action-bar .action-label:hover {
152+
background-color: transparent !important;
153+
}

0 commit comments

Comments
 (0)