Skip to content

Commit 9c9eb26

Browse files
authored
Show custom keybindings in code action widget (microsoft#160449)
* Show custom keybindings in code action widget Also cleans up some styling rules * Use full keybindings service
1 parent 4987750 commit 9c9eb26

File tree

3 files changed

+71
-50
lines changed

3 files changed

+71
-50
lines changed

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

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import * as dom from 'vs/base/browser/dom';
77
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
88
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
99
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
10+
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
1011
import { IListEvent, IListMouseEvent, IListRenderer } from 'vs/base/browser/ui/list/list';
1112
import { List } from 'vs/base/browser/ui/list/listWidget';
1213
import { IAction } from 'vs/base/common/actions';
1314
import { Codicon } from 'vs/base/common/codicons';
1415
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
1516
import { Lazy } from 'vs/base/common/lazy';
1617
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
18+
import { OS } from 'vs/base/common/platform';
1719
import 'vs/css!./media/action';
1820
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1921
import { IEditorContribution } from 'vs/editor/common/editorCommon';
@@ -28,7 +30,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
2830
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
2931
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
3032
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
31-
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
3233
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
3334

3435
export const Context = {
@@ -78,34 +79,36 @@ type ICodeActionMenuItem = CodeActionListItemCodeAction | CodeActionListItemHead
7879

7980
interface ICodeActionMenuTemplateData {
8081
readonly container: HTMLElement;
81-
readonly text: HTMLElement;
8282
readonly icon: HTMLElement;
83+
readonly text: HTMLElement;
84+
readonly keybinding: KeybindingLabel;
8385
}
8486

8587
class CodeActionItemRenderer implements IListRenderer<CodeActionListItemCodeAction, ICodeActionMenuTemplateData> {
8688
constructor(
89+
private readonly keybindingResolver: CodeActionKeybindingResolver,
8790
@IKeybindingService private readonly keybindingService: IKeybindingService,
8891
) { }
8992

9093
get templateId(): string { return CodeActionListItemKind.CodeAction; }
9194

9295
renderTemplate(container: HTMLElement): ICodeActionMenuTemplateData {
93-
const iconContainer = document.createElement('div');
94-
iconContainer.className = 'icon-container';
95-
container.append(iconContainer);
96+
container.classList.add('code-action');
9697

9798
const icon = document.createElement('div');
98-
iconContainer.append(icon);
99+
icon.className = 'icon';
100+
container.append(icon);
99101

100102
const text = document.createElement('span');
103+
text.className = 'title';
101104
container.append(text);
102105

103-
return { container, icon, text };
106+
const keybinding = new KeybindingLabel(container, OS);
107+
108+
return { container, icon, text, keybinding };
104109
}
105110

106111
renderElement(element: CodeActionListItemCodeAction, _index: number, data: ICodeActionMenuTemplateData): void {
107-
data.text.textContent = stripNewlines(element.action.action.title);
108-
109112
// Icons and Label modification based on group
110113
const kind = element.action.action.kind ? new CodeActionKind(element.action.action.kind) : CodeActionKind.None;
111114
if (CodeActionKind.SurroundWith.contains(kind)) {
@@ -123,6 +126,16 @@ class CodeActionItemRenderer implements IListRenderer<CodeActionListItemCodeActi
123126
data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`;
124127
}
125128

129+
data.text.textContent = stripNewlines(element.action.action.title);
130+
131+
const binding = this.keybindingResolver.getResolver()(element.action.action);
132+
data.keybinding.set(binding);
133+
if (!binding) {
134+
dom.hide(data.keybinding.element);
135+
} else {
136+
dom.show(data.keybinding.element);
137+
}
138+
126139
// Check if action has disabled reason
127140
if (element.action.action.disabled) {
128141
data.container.title = element.action.action.disabled;
@@ -157,7 +170,7 @@ class HeaderRenderer implements IListRenderer<CodeActionListItemHeader, HeaderTe
157170
get templateId(): string { return CodeActionListItemKind.Header; }
158171

159172
renderTemplate(container: HTMLElement): HeaderTemplateData {
160-
container.classList.add('group-header', 'option-disabled');
173+
container.classList.add('group-header');
161174

162175
const text = document.createElement('span');
163176
container.append(text);
@@ -203,10 +216,11 @@ class CodeActionList extends Disposable {
203216
getHeight: element => element.kind === CodeActionListItemKind.Header ? this.headerLineHeight : this.codeActionLineHeight,
204217
getTemplateId: element => element.kind,
205218
}, [
206-
new CodeActionItemRenderer(keybindingService),
219+
new CodeActionItemRenderer(new CodeActionKeybindingResolver(keybindingService), keybindingService),
207220
new HeaderRenderer(),
208221
], {
209222
keyboardSupport: false,
223+
mouseSupport: false,
210224
accessibilityProvider: {
211225
getAriaLabel: element => {
212226
if (element.kind === CodeActionListItemKind.CodeAction) {
@@ -251,9 +265,10 @@ class CodeActionList extends Disposable {
251265
const itemWidths: number[] = this.allMenuItems.map((_, index): number => {
252266
const element = document.getElementById(this.list.getElementID(index));
253267
if (element) {
254-
const textPadding = 10;
255-
const iconPadding = 10;
256-
return [...element.children].reduce((p, c) => p + c.clientWidth, 0) + (textPadding * 2) + iconPadding;
268+
element.style.width = 'auto';
269+
const width = element.getBoundingClientRect().width;
270+
element.style.width = '';
271+
return width;
257272
}
258273
return 0;
259274
});
@@ -672,15 +687,13 @@ export class CodeActionKeybindingResolver {
672687
];
673688

674689
constructor(
675-
private readonly _keybindingProvider: {
676-
getKeybindings(): readonly ResolvedKeybindingItem[];
677-
},
690+
private readonly keybindingService: IKeybindingService,
678691
) { }
679692

680693
public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined {
681694
// Lazy since we may not actually ever read the value
682695
const allCodeActionBindings = new Lazy<readonly ResolveCodeActionKeybinding[]>(() =>
683-
this._keybindingProvider.getKeybindings()
696+
this.keybindingService.getKeybindings()
684697
.filter(item => CodeActionKeybindingResolver.codeActionCommands.indexOf(item.command!) >= 0)
685698
.filter(item => item.resolvedKeybinding)
686699
.map((item): ResolveCodeActionKeybinding => {

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
font-size: 13px;
2323
border-radius: 0;
2424
min-width: 160px;
25+
max-width: 500px;
2526
z-index: 40;
2627
display: block;
2728
width: 100%;
@@ -48,27 +49,31 @@
4849
}
4950

5051
/** Styles for each row in the list element **/
51-
.codeActionWidget .monaco-list .monaco-list-row:not(.separator) {
52-
display: flex;
53-
box-sizing: border-box;
52+
.codeActionWidget .monaco-list .monaco-list-row {
5453
padding: 0 10px;
5554
white-space: nowrap;
5655
cursor: pointer;
5756
touch-action: none;
5857
width: 100%;
5958
}
6059

61-
.codeActionWidget .monaco-list .monaco-list-row:hover:not(.option-disabled),
62-
.codeActionWidget .monaco-list .monaco-list-row.focused:not(.option-disabled) {
60+
.codeActionWidget .monaco-list .monaco-list-row.code-action:hover:not(.option-disabled),
61+
.codeActionWidget .monaco-list .monaco-list-row.code-action.focused:not(.option-disabled) {
6362
background-color: var(--vscode-list-hoverBackground) !important;
6463
color: var(--vscode-list-activeSelectionForeground) !important;
6564
}
6665

67-
.codeActionWidget .monaco-list .monaco-list-row.focused:not(.option-disabled) {
66+
.codeActionWidget .monaco-list .monaco-list-row.code-action.focused:not(.option-disabled) {
6867
outline: 0 solid !important;
6968
background-color: var(--vscode-menu-selectionBackground) !important;
7069
}
7170

71+
.codeActionWidget .monaco-list-row.group-header {
72+
color: var(--vscode-textLink-activeForeground);
73+
font-weight: bold;
74+
}
75+
76+
.codeActionWidget .monaco-list .group-header,
7277
.codeActionWidget .monaco-list .option-disabled,
7378
.codeActionWidget .monaco-list .option-disabled:before,
7479
.codeActionWidget .monaco-list .option-disabled .focused,
@@ -81,24 +86,24 @@
8186
-ms-user-select: none;
8287
user-select: none;
8388
background-color: var(--vscode-menu-background) !important;
84-
color: var(--vscode-disabledForeground) !important;
8589
outline: 0 solid !important;
8690
}
8791

88-
.codeActionWidget .monaco-list .group-header.option-disabled {
89-
color: var(--vscode-textLink-activeForeground) !important;
90-
font-weight: bold;
92+
.codeActionWidget .monaco-list-row.code-action {
93+
display: flex;
94+
gap: 10px;
95+
align-items: center;
9196
}
9297

93-
.codeActionWidget .monaco-list-row:not(.group-header) .icon-container {
94-
margin-top: 3px;
95-
margin-right: 10px;
96-
width: 13px;
97-
height: 13px;
98-
flex-shrink: 0;
99-
color: var(--vscode-editorLightBulb-foreground);
98+
.codeActionWidget .monaco-list-row.code-action.option-disabled {
99+
color: var(--vscode-disabledForeground);
100100
}
101101

102+
.codeActionWidget .monaco-list-row.code-action .title {
103+
flex: 1;
104+
overflow: hidden;
105+
text-overflow: ellipsis;
106+
}
102107

103108
/* Action bar */
104109

src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CodeActionKeybindingResolver } from 'vs/editor/contrib/codeAction/brows
1212
import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types';
1313
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
1414
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
15+
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1516

1617
suite('CodeActionKeybindingResolver', () => {
1718
const refactorKeybinding = createCodeActionKeybinding(
@@ -30,11 +31,9 @@ suite('CodeActionKeybindingResolver', () => {
3031
undefined);
3132

3233
test('Should match refactor keybindings', async function () {
33-
const resolver = new CodeActionKeybindingResolver({
34-
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
35-
return [refactorKeybinding];
36-
},
37-
}).getResolver();
34+
const resolver = new CodeActionKeybindingResolver(
35+
createMockKeyBindingService([refactorKeybinding])
36+
).getResolver();
3837

3938
assert.strictEqual(
4039
resolver({ title: '' }),
@@ -54,11 +53,9 @@ suite('CodeActionKeybindingResolver', () => {
5453
});
5554

5655
test('Should prefer most specific keybinding', async function () {
57-
const resolver = new CodeActionKeybindingResolver({
58-
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
59-
return [refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding];
60-
},
61-
}).getResolver();
56+
const resolver = new CodeActionKeybindingResolver(
57+
createMockKeyBindingService([refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding])
58+
).getResolver();
6259

6360
assert.strictEqual(
6461
resolver({ title: '', kind: CodeActionKind.Refactor.value }),
@@ -70,18 +67,24 @@ suite('CodeActionKeybindingResolver', () => {
7067
});
7168

7269
test('Organize imports should still return a keybinding even though it does not have args', async function () {
73-
const resolver = new CodeActionKeybindingResolver({
74-
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
75-
return [refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding];
76-
},
77-
}).getResolver();
70+
const resolver = new CodeActionKeybindingResolver(
71+
createMockKeyBindingService([refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding])
72+
).getResolver();
7873

7974
assert.strictEqual(
8075
resolver({ title: '', kind: CodeActionKind.SourceOrganizeImports.value }),
8176
organizeImportsKeybinding.resolvedKeybinding);
8277
});
8378
});
8479

80+
function createMockKeyBindingService(items: ResolvedKeybindingItem[]): IKeybindingService {
81+
return <IKeybindingService>{
82+
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
83+
return items;
84+
},
85+
};
86+
}
87+
8588
function createCodeActionKeybinding(keycode: KeyCode, command: string, commandArgs: any) {
8689
return new ResolvedKeybindingItem(
8790
new USLayoutResolvedKeybinding(

0 commit comments

Comments
 (0)