Skip to content

Commit 8e456bd

Browse files
authored
Added Icons and Header Separators to Code Action Widget (ref microsoft#132109)
* added disabled hover * code cleanup on disabled option hovers * removed comments * widget enabled by default * code cleanup and fix on build * clean up on css removed unused importants * small patch for css rules * minor refactor on codeactionitems * fix on disabled option click * fix on disabled option click * added some icons but just temp * added iconws and modified widget look * added beginning logic for menu groupings * looks pretty good for a menu wooo * added headers to menu + removed extra text from option labels * minor code cleanup on group filtering * Refactoring on code action kind * changed styling based on feedback * code cleanup * First couple of fixes on PR for code action kinds * modified icons and refactoring * removed extra push * removed parsing and added code action kind for surround
1 parent abc84e0 commit 8e456bd

File tree

4 files changed

+203
-51
lines changed

4 files changed

+203
-51
lines changed

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

Lines changed: 166 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
3030
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
3131
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
3232
import { IThemeService } from 'vs/platform/theme/common/themeService';
33+
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
34+
import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors
35+
import { Codicon } from 'vs/base/common/codicons';
3336

3437
export const Context = {
3538
Visible: new RawContextKey<boolean>('CodeActionMenuVisible', false, localize('CodeActionMenuVisible', "Whether the code action list widget is visible"))
@@ -67,6 +70,8 @@ export interface ICodeActionMenuItem {
6770
isSeparator: boolean;
6871
isEnabled: boolean;
6972
isDocumentation: boolean;
73+
isHeader: boolean;
74+
headerTitle: string;
7075
index: number;
7176
disposables?: IDisposable[];
7277
}
@@ -85,10 +90,12 @@ export interface ICodeActionMenuTemplateData {
8590
detail: HTMLElement;
8691
decoratorRight: HTMLElement;
8792
disposables: IDisposable[];
93+
icon: HTMLElement;
8894
}
8995

9096
const TEMPLATE_ID = 'codeActionWidget';
91-
const codeActionLineHeight = 26;
97+
const codeActionLineHeight = 24;
98+
const headerLineHeight = 26;
9299

93100
class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> {
94101

@@ -104,52 +111,86 @@ class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeAction
104111
data.disposables = [];
105112
data.root = container;
106113
data.text = document.createElement('span');
107-
// data.detail = document.createElement('');
114+
115+
const iconContainer = document.createElement('div');
116+
iconContainer.className = 'icon-container';
117+
118+
data.icon = document.createElement('div');
119+
120+
iconContainer.append(data.icon);
121+
container.append(iconContainer);
108122
container.append(data.text);
109-
// container.append(data.detail);
110123

111124
return data;
112125
}
113126
renderElement(element: ICodeActionMenuItem, index: number, templateData: ICodeActionMenuTemplateData): void {
114127
const data: ICodeActionMenuTemplateData = templateData;
115-
const text = element.action.label;
128+
116129
const isSeparator = element.isSeparator;
130+
const isHeader = element.isHeader;
117131

118-
element.isEnabled = element.action.enabled;
132+
if (isSeparator) {
133+
data.root.classList.add('separator');
134+
data.root.style.height = '10px';
135+
} else if (isHeader) {
136+
const text = element.headerTitle;
137+
data.text.textContent = text;
138+
element.isEnabled = false;
139+
data.root.classList.add('group-header');
140+
} else {
141+
const text = element.action.label;
142+
data.text.textContent = text;
143+
element.isEnabled = element.action.enabled;
119144

120-
if (element.action instanceof CodeActionAction) {
145+
if (element.action instanceof CodeActionAction) {
121146

122-
// Check documentation type
123-
element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
124-
if (!element.isDocumentation) {
147+
// Check documentation type
148+
element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
125149

126-
// Check if action has disabled reason
127-
if (element.action.action.disabled) {
128-
data.root.title = element.action.action.disabled;
150+
if (element.isDocumentation) {
151+
data.text.textContent = text;
152+
data.root.classList.add('documentation');
129153
} else {
130-
const updateLabel = () => {
131-
const [accept, preview] = this.acceptKeybindings;
132-
data.root.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Refactor, Shift+F2 to Preview"'] }, "{0} to Refactor, {1} to Preview", this.keybindingService.lookupKeybinding(accept)?.getLabel(), this.keybindingService.lookupKeybinding(preview)?.getLabel());
133-
};
134-
updateLabel();
154+
// Icons and Label modifaction based on group
155+
const group = element.action.action.kind;
156+
157+
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(group)))) {
158+
data.icon.className = Codicon.symbolArray.classNames;
159+
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(group)))) {
160+
data.icon.className = Codicon.wrench.classNames;
161+
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(group)))) {
162+
data.icon.className = Codicon.zap.classNames;
163+
data.icon.style.color = `var(--vscode-editorLightBulbAutoFix-foreground)`;
164+
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(group)))) {
165+
data.icon.className = Codicon.lightBulb.classNames;
166+
data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`;
167+
} else {
168+
data.icon.className = Codicon.lightBulb.classNames;
169+
data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`;
170+
}
171+
172+
// Check if action has disabled reason
173+
if (element.action.action.disabled) {
174+
data.root.title = element.action.action.disabled;
175+
} else {
176+
const updateLabel = () => {
177+
const [accept, preview] = this.acceptKeybindings;
178+
data.root.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Refactor, Shift+F2 to Preview"'] }, "{0} to Refactor, {1} to Preview", this.keybindingService.lookupKeybinding(accept)?.getLabel(), this.keybindingService.lookupKeybinding(preview)?.getLabel());
179+
};
180+
updateLabel();
181+
}
135182
}
136183
}
137-
}
138184

139-
data.text.textContent = text;
185+
}
140186

141187
if (!element.isEnabled) {
142188
data.root.classList.add('option-disabled');
143189
data.root.style.backgroundColor = 'transparent !important';
190+
data.icon.style.opacity = '0.4';
144191
} else {
145192
data.root.classList.remove('option-disabled');
146193
}
147-
148-
if (isSeparator) {
149-
data.root.classList.add('separator');
150-
data.root.style.height = '10px';
151-
}
152-
153194
}
154195
disposeTemplate(templateData: ICodeActionMenuTemplateData): void {
155196
templateData.disposables = dispose(templateData.disposables);
@@ -275,13 +316,19 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
275316
getHeight(element) {
276317
if (element.isSeparator) {
277318
return 10;
319+
} else if (element.isHeader) {
320+
return headerLineHeight;
278321
}
279322
return codeActionLineHeight;
280323
},
281324
getTemplateId(element) {
282325
return 'codeActionWidget';
283326
}
284-
}, [this.listRenderer], { keyboardSupport: false }
327+
}, [this.listRenderer],
328+
{
329+
keyboardSupport: false,
330+
331+
}
285332
);
286333

287334
const pointerBlockDiv = document.createElement('div');
@@ -305,28 +352,105 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
305352
renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e)));
306353
renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget()));
307354

308-
// Populating the list widget and tracking enabled options.
355+
// Filters and groups code actions by their group
356+
const menuEntries: IAction[][] = [];
357+
358+
// Code Action Groups
359+
const quickfixGroup: IAction[] = [];
360+
const extractGroup: IAction[] = [];
361+
const convertGroup: IAction[] = [];
362+
const surroundGroup: IAction[] = [];
363+
const sourceGroup: IAction[] = [];
364+
const separatorGroup: IAction[] = [];
365+
const documentationGroup: IAction[] = [];
366+
const otherGroup: IAction[] = [];
367+
309368
inputArray.forEach((item, index) => {
369+
if (item instanceof CodeActionAction) {
370+
const optionKind = item.action.kind;
371+
372+
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(optionKind)))) {
373+
surroundGroup.push(item);
374+
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(optionKind)))) {
375+
quickfixGroup.push(item);
376+
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(optionKind)))) {
377+
extractGroup.push(item);
378+
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(optionKind)))) {
379+
convertGroup.push(item);
380+
} else if (CodeActionKind.Source.contains(new CodeActionKind(String(optionKind)))) {
381+
sourceGroup.push(item);
382+
} else if (optionKind === CodeActionMenu.documentationID) {
383+
documentationGroup.push(item);
384+
} else {
385+
otherGroup.push(item);
386+
}
310387

311-
const currIsSeparator = item.class === 'separator';
388+
} else if (item.id === `vs.actions.separator`) {
389+
separatorGroup.push(item);
390+
}
391+
});
392+
393+
menuEntries.push(quickfixGroup, extractGroup, convertGroup, surroundGroup, sourceGroup, otherGroup, separatorGroup, documentationGroup);
312394

313-
if (currIsSeparator) {
314-
// set to true forever because there is a separator
315-
this.hasSeparator = true;
395+
const menuEntriesToPush = (menuID: string, entry: IAction[]) => {
396+
totalActionEntries.push(menuID);
397+
totalActionEntries.push(...entry);
398+
numHeaders++;
399+
};
400+
// Creates flat list of all menu entries with headers as separators
401+
let numHeaders = 0;
402+
const totalActionEntries: (IAction | string)[] = [];
403+
menuEntries.forEach(entry => {
404+
if (entry.length > 0 && entry[0] instanceof CodeActionAction) {
405+
const firstAction = entry[0].action.kind;
406+
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(firstAction)))) {
407+
menuEntriesToPush(localize('codeAction.widget.id.surround', 'Surround With ...'), entry);
408+
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(firstAction)))) {
409+
menuEntriesToPush(localize('codeAction.widget.id.quickfix', 'Quick Fix ...'), entry);
410+
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(firstAction)))) {
411+
menuEntriesToPush(localize('codeAction.widget.id.extract', 'Extract ...'), entry);
412+
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(firstAction)))) {
413+
menuEntriesToPush(localize('codeAction.widget.id.convert', 'Convert ...'), entry);
414+
} else if (CodeActionKind.Source.contains(new CodeActionKind(String(firstAction)))) {
415+
menuEntriesToPush(localize('codeAction.widget.id.source', 'Source Action ...'), entry);
416+
} else if (firstAction === CodeActionMenu.documentationID) {
417+
totalActionEntries.push(...entry);
418+
}
419+
} else {
420+
// case for separator - not a code action action
421+
totalActionEntries.push(...entry);
316422
}
317423

318-
const menuItem = <ICodeActionMenuItem>{ action: inputArray[index], isEnabled: item.enabled, isSeparator: currIsSeparator, index };
319-
if (item.enabled) {
320-
this.viewItems.push(menuItem);
424+
});
425+
426+
// Populating the list widget and tracking enabled options.
427+
totalActionEntries.forEach((item, index) => {
428+
if (typeof item === `string`) {
429+
const menuItem = <ICodeActionMenuItem>{ isEnabled: false, isSeparator: false, index, isHeader: true, headerTitle: item };
430+
this.options.push(menuItem);
431+
} else {
432+
const currIsSeparator = item.class === 'separator';
433+
434+
if (currIsSeparator) {
435+
// set to true forever because there is a separator
436+
this.hasSeparator = true;
437+
}
438+
439+
const menuItem = <ICodeActionMenuItem>{ action: item, isEnabled: item.enabled, isSeparator: currIsSeparator, index };
440+
if (item.enabled) {
441+
this.viewItems.push(menuItem);
442+
}
443+
this.options.push(menuItem);
321444
}
322-
this.options.push(menuItem);
323445
});
324446

325447
this.codeActionList.value.splice(0, this.codeActionList.value.length, this.options);
326448

327-
const height = this.hasSeparator ? (inputArray.length - 1) * codeActionLineHeight + 10 : inputArray.length * codeActionLineHeight;
328-
renderMenu.style.height = String(height) + 'px';
329-
this.codeActionList.value.layout(height);
449+
// Updating list height, depending on how many separators and headers there are.
450+
const height = this.hasSeparator ? (totalActionEntries.length - 1) * codeActionLineHeight + 10 : totalActionEntries.length * codeActionLineHeight;
451+
const heightWithHeaders = height + numHeaders * headerLineHeight - numHeaders * codeActionLineHeight;
452+
renderMenu.style.height = String(heightWithHeaders) + 'px';
453+
this.codeActionList.value.layout(heightWithHeaders);
330454

331455
// For finding width dynamically (not using resize observer)
332456
const arr: number[] = [];
@@ -341,9 +465,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
341465
// resize observer - can be used in the future since list widget supports dynamic height but not width
342466
const maxWidth = Math.max(...arr);
343467

344-
// 40 is the additional padding for the list widget (20 left, 20 right)
345-
renderMenu.style.width = maxWidth + 52 + 'px';
346-
this.codeActionList.value?.layout(height, maxWidth);
468+
// 52 is the additional padding for the list widget (26 left, 26 right)
469+
renderMenu.style.width = maxWidth + 52 + 5 + 'px';
470+
this.codeActionList.value?.layout(heightWithHeaders, maxWidth);
347471

348472
// List selection
349473
if (this.viewItems.length < 1 || this.viewItems.every(item => item.isDocumentation)) {
@@ -359,7 +483,6 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
359483
const focusTracker = dom.trackFocus(element);
360484
const blurListener = focusTracker.onDidBlur(() => {
361485
this.hideCodeActionWidget();
362-
// this._contextViewService.hideContextView({ source: this });
363486
});
364487
renderDisposables.add(blurListener);
365488
renderDisposables.add(focusTracker);
@@ -468,6 +591,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
468591
return;
469592
}
470593
const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
594+
471595
if (!actionsToShow.length) {
472596
this._visible = false;
473597
return;

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
.codeActionMenuWidget {
7-
padding: 8px 0px 8px 0px;
7+
padding: 0px 1px 3px 0px;
88
overflow: auto;
99
font-size: 13px;
10-
border-radius: 5px;
10+
border-radius: 0px;
1111
min-width: 160px;
1212
z-index: 40;
1313
display: block;
1414
/* flex-direction: column;
1515
flex: 0 1 auto; */
1616
width: 100%;
17-
border-width: 0px;
17+
border: 1px solid var(--vscode-menu-separatorBackground);
1818
border-color: none;
1919
background-color: var(--vscode-menu-background);
2020
color: var(--vscode-menu-foreground);
@@ -57,7 +57,7 @@
5757
display: flex;
5858
-mox-box-sizing: border-box;
5959
box-sizing: border-box;
60-
padding: 0px 26px 0px 26px;
60+
padding: 0px 26px 0px 10px;
6161
background-repeat: no-repeat;
6262
background-position: 2px 2px;
6363
white-space: nowrap;
@@ -89,6 +89,21 @@
8989
outline: 0px solid !important;
9090
}
9191

92+
.codeActionMenuWidget .monaco-list .monaco-list-row.group-header {
93+
padding: 3px 10px 0px 10px;
94+
}
95+
96+
.codeActionMenuWidget .monaco-list .monaco-list-row.documentation {
97+
padding: 0px 10px 0px 10px;
98+
}
99+
100+
101+
.codeActionMenuWidget .monaco-list .group-header.option-disabled {
102+
color: var(--vscode-textLink-activeForeground) !important;
103+
font-weight: bold;
104+
105+
}
106+
92107
.codeActionMenuWidget .monaco-list .separator {
93108
border-bottom: 1px solid var(--vscode-menu-separatorBackground);
94109
padding-top: 0px !important;
@@ -108,3 +123,13 @@
108123
cursor: pointer;
109124
touch-action: none;
110125
}
126+
127+
.codeActionMenuWidget .monaco-list-row:not(.group-header):not(.documentation) .icon-container {
128+
margin-top: 3px;
129+
margin-right: 10px;
130+
width: 13px;
131+
height: 13px;
132+
flex-shrink: 0;
133+
color: var(--vscode-editorLightBulb-foreground);
134+
}
135+

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ export class CodeActionKind {
1313
public static readonly Empty = new CodeActionKind('');
1414
public static readonly QuickFix = new CodeActionKind('quickfix');
1515
public static readonly Refactor = new CodeActionKind('refactor');
16+
public static readonly Extract = CodeActionKind.Refactor.append('extract');
17+
public static readonly Convert = CodeActionKind.Refactor.append('rewrite');
1618
public static readonly Source = new CodeActionKind('source');
1719
public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports');
1820
public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll');
21+
public static readonly SurroundWith = CodeActionKind.Refactor.append('surround');
1922

2023
constructor(
2124
public readonly value: string

0 commit comments

Comments
 (0)