Skip to content

Commit f614d0b

Browse files
authored
add ActionWidgetService (microsoft#164096)
1 parent 44441de commit f614d0b

File tree

13 files changed

+905
-760
lines changed

13 files changed

+905
-760
lines changed

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

-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ export const sourceActionCommandId = 'editor.action.sourceAction';
3333
export const organizeImportsCommandId = 'editor.action.organizeImports';
3434
export const fixAllCommandId = 'editor.action.fixAll';
3535

36-
export const acceptSelectedCodeActionCommand = 'acceptSelectedCodeAction';
37-
export const previewSelectedCodeActionCommand = 'previewSelectedCodeAction';
38-
3936
class ManagedCodeActionSet extends Disposable implements CodeActionSet {
4037

4138
private static codeActionsPreferredComparator(a: languages.CodeAction, b: languages.CodeAction): number {

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

+2-118
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon';
1616
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
1717
import { CodeActionTriggerType } from 'vs/editor/common/languages';
1818
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
19-
import { acceptSelectedCodeActionCommand, applyCodeAction, ApplyCodeActionReason, codeActionCommandId, fixAllCommandId, organizeImportsCommandId, previewSelectedCodeActionCommand, refactorCommandId, refactorPreviewCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction';
19+
import { applyCodeAction, ApplyCodeActionReason, codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, refactorPreviewCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction';
2020
import { CodeActionUi } from 'vs/editor/contrib/codeAction/browser/codeActionUi';
21-
import { CodeActionWidget, Context } from 'vs/editor/contrib/codeAction/browser/codeActionWidget';
2221
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
2322
import * as nls from 'vs/nls';
24-
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
2523
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2624
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
2725
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
@@ -102,7 +100,7 @@ export class CodeActionController extends Disposable implements IEditorContribut
102100
@IContextKeyService contextKeyService: IContextKeyService,
103101
@IEditorProgressService progressService: IEditorProgressService,
104102
@IInstantiationService private readonly _instantiationService: IInstantiationService,
105-
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
103+
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService
106104
) {
107105
super();
108106

@@ -416,117 +414,3 @@ export class AutoFixAction extends EditorAction {
416414
CodeActionAutoApply.IfSingle, undefined, CodeActionTriggerSource.AutoFix);
417415
}
418416
}
419-
420-
const weight = KeybindingWeight.EditorContrib + 1000;
421-
422-
registerAction2(class extends Action2 {
423-
constructor() {
424-
super({
425-
id: 'hideCodeActionWidget',
426-
title: {
427-
value: nls.localize('hideCodeActionWidget.title', "Hide code action widget"),
428-
original: 'Hide code action widget'
429-
},
430-
precondition: Context.Visible,
431-
keybinding: {
432-
weight,
433-
primary: KeyCode.Escape,
434-
secondary: [KeyMod.Shift | KeyCode.Escape]
435-
},
436-
});
437-
}
438-
439-
run(): void {
440-
CodeActionWidget.INSTANCE?.hide();
441-
}
442-
});
443-
444-
registerAction2(class extends Action2 {
445-
constructor() {
446-
super({
447-
id: 'selectPrevCodeAction',
448-
title: {
449-
value: nls.localize('selectPrevCodeAction.title', "Select previous code action"),
450-
original: 'Select previous code action'
451-
},
452-
precondition: Context.Visible,
453-
keybinding: {
454-
weight,
455-
primary: KeyCode.UpArrow,
456-
secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],
457-
mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] },
458-
}
459-
});
460-
}
461-
462-
run(): void {
463-
CodeActionWidget.INSTANCE?.focusPrevious();
464-
}
465-
});
466-
467-
registerAction2(class extends Action2 {
468-
constructor() {
469-
super({
470-
id: 'selectNextCodeAction',
471-
title: {
472-
value: nls.localize('selectNextCodeAction.title', "Select next code action"),
473-
original: 'Select next code action'
474-
},
475-
precondition: Context.Visible,
476-
keybinding: {
477-
weight,
478-
primary: KeyCode.DownArrow,
479-
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],
480-
mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] }
481-
}
482-
});
483-
}
484-
485-
run(): void {
486-
CodeActionWidget.INSTANCE?.focusNext();
487-
}
488-
});
489-
490-
registerAction2(class extends Action2 {
491-
constructor() {
492-
super({
493-
id: acceptSelectedCodeActionCommand,
494-
title: {
495-
value: nls.localize('acceptSelected.title', "Accept selected code action"),
496-
original: 'Accept selected code action'
497-
},
498-
precondition: Context.Visible,
499-
keybinding: {
500-
weight,
501-
primary: KeyCode.Enter,
502-
secondary: [KeyMod.CtrlCmd | KeyCode.Period],
503-
}
504-
});
505-
}
506-
507-
run(): void {
508-
CodeActionWidget.INSTANCE?.acceptSelected();
509-
}
510-
});
511-
512-
registerAction2(class extends Action2 {
513-
constructor() {
514-
super({
515-
id: previewSelectedCodeActionCommand,
516-
title: {
517-
value: nls.localize('previewSelected.title', "Preview selected code action"),
518-
original: 'Preview selected code action'
519-
},
520-
precondition: Context.Visible,
521-
keybinding: {
522-
weight,
523-
primary: KeyMod.CtrlCmd | KeyCode.Enter,
524-
}
525-
});
526-
}
527-
528-
run(): void {
529-
CodeActionWidget.INSTANCE?.acceptSelected({ preview: true });
530-
}
531-
});
532-

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Lazy } from 'vs/base/common/lazy';
88
import { CodeAction } from 'vs/editor/common/languages';
99
import { codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction';
1010
import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types';
11+
import { IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget';
1112
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1213

1314
interface ResolveCodeActionKeybinding {
@@ -16,7 +17,7 @@ interface ResolveCodeActionKeybinding {
1617
readonly resolvedKeybinding: ResolvedKeybinding;
1718
}
1819

19-
export class CodeActionKeybindingResolver {
20+
export class CodeActionKeybindingResolver implements IActionKeybindingResolver {
2021
private static readonly codeActionCommands: readonly string[] = [
2122
refactorCommandId,
2223
codeActionCommandId,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
7+
import { Codicon } from 'vs/base/common/codicons';
8+
import { ActionListItemKind, IListMenuItem } from 'vs/platform/actionWidget/browser/actionWidget';
9+
import { CodeActionItem, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types';
10+
import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors
11+
import { localize } from 'vs/nls';
12+
13+
export interface ActionGroup {
14+
readonly kind: CodeActionKind;
15+
readonly title: string;
16+
readonly icon?: { readonly codicon: Codicon; readonly color?: string };
17+
}
18+
19+
const uncategorizedCodeActionGroup = Object.freeze<ActionGroup>({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') });
20+
21+
const codeActionGroups = Object.freeze<ActionGroup[]>([
22+
{ kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') },
23+
{ kind: CodeActionKind.RefactorExtract, title: localize('codeAction.widget.id.extract', 'Extract...'), icon: { codicon: Codicon.wrench } },
24+
{ kind: CodeActionKind.RefactorInline, title: localize('codeAction.widget.id.inline', 'Inline...'), icon: { codicon: Codicon.wrench } },
25+
{ kind: CodeActionKind.RefactorRewrite, title: localize('codeAction.widget.id.convert', 'Rewrite...'), icon: { codicon: Codicon.wrench } },
26+
{ kind: CodeActionKind.RefactorMove, title: localize('codeAction.widget.id.move', 'Move...'), icon: { codicon: Codicon.wrench } },
27+
{ kind: CodeActionKind.SurroundWith, title: localize('codeAction.widget.id.surround', 'Surround With...'), icon: { codicon: Codicon.symbolSnippet } },
28+
{ kind: CodeActionKind.Source, title: localize('codeAction.widget.id.source', 'Source Action...'), icon: { codicon: Codicon.symbolFile } },
29+
uncategorizedCodeActionGroup,
30+
]);
31+
32+
export function toMenuItems(inputCodeActions: readonly CodeActionItem[], showHeaders: boolean): IListMenuItem<CodeActionItem>[] {
33+
if (!showHeaders) {
34+
return inputCodeActions.map((action): IListMenuItem<CodeActionItem> => {
35+
return {
36+
kind: ActionListItemKind.Action,
37+
item: action,
38+
group: uncategorizedCodeActionGroup,
39+
disabled: !!action.action.disabled,
40+
label: action.action.disabled || action.action.title
41+
};
42+
});
43+
}
44+
45+
// Group code actions
46+
const menuEntries = codeActionGroups.map(group => ({ group, actions: [] as CodeActionItem[] }));
47+
48+
for (const action of inputCodeActions) {
49+
const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None;
50+
for (const menuEntry of menuEntries) {
51+
if (menuEntry.group.kind.contains(kind)) {
52+
menuEntry.actions.push(action);
53+
break;
54+
}
55+
}
56+
}
57+
58+
const allMenuItems: IListMenuItem<CodeActionItem>[] = [];
59+
for (const menuEntry of menuEntries) {
60+
if (menuEntry.actions.length) {
61+
allMenuItems.push({ kind: ActionListItemKind.Header, group: menuEntry.group });
62+
for (const action of menuEntry.actions) {
63+
allMenuItems.push({ kind: ActionListItemKind.Action, item: action, group: menuEntry.group, label: action.action.title, disabled: !!action.action.disabled });
64+
}
65+
}
66+
}
67+
return allMenuItems;
68+
}

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

+22-13
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1212
import { IPosition, Position } from 'vs/editor/common/core/position';
1313
import { ScrollType } from 'vs/editor/common/editorCommon';
1414
import { CodeActionTriggerType } from 'vs/editor/common/languages';
15+
import { IActionShowOptions, IActionWidgetService, IRenderDelegate } from 'vs/platform/actionWidget/browser/actionWidget';
1516
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
1617
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
17-
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1818
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1919
import { CodeActionAutoApply, CodeActionItem, CodeActionSet, CodeActionTrigger } from '../common/types';
2020
import { CodeActionsState } from './codeActionModel';
21-
import { CodeActionShowOptions, CodeActionWidget } from './codeActionWidget';
2221
import { LightBulbWidget } from './lightBulbWidget';
22+
import { toMenuItems } from 'vs/editor/contrib/codeAction/browser/codeActionMenuItems';
2323

2424
export class CodeActionUi extends Disposable {
2525
private readonly _lightBulbWidget: Lazy<LightBulbWidget>;
@@ -35,19 +35,19 @@ export class CodeActionUi extends Disposable {
3535
applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean, preview: boolean) => Promise<void>;
3636
},
3737
@IConfigurationService private readonly _configurationService: IConfigurationService,
38-
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
39-
@IInstantiationService private readonly _instantiationService: IInstantiationService,
38+
@IInstantiationService readonly instantiationService: IInstantiationService,
39+
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService
4040
) {
4141
super();
4242

4343

4444
this._lightBulbWidget = new Lazy(() => {
45-
const widget = this._register(_instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId));
45+
const widget = this._register(instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId));
4646
this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false, fromLightbulb: true, showHeaders: this.shouldShowHeaders() })));
4747
return widget;
4848
});
4949

50-
this._register(this._editor.onDidLayoutChange(() => CodeActionWidget.INSTANCE?.hide()));
50+
this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide()));
5151
}
5252

5353
override dispose() {
@@ -115,7 +115,7 @@ export class CodeActionUi extends Disposable {
115115
this.showCodeActionList(newState.trigger, actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false, showHeaders: this.shouldShowHeaders() });
116116
} else {
117117
// auto magically triggered
118-
if (CodeActionWidget.INSTANCE?.isVisible) {
118+
if (this._actionWidgetService.isVisible) {
119119
// TODO: Figure out if we should update the showing menu?
120120
actions.dispose();
121121
} else {
@@ -152,22 +152,31 @@ export class CodeActionUi extends Disposable {
152152
return undefined;
153153
}
154154

155-
public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise<void> {
155+
public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): Promise<void> {
156156
const editorDom = this._editor.getDomNode();
157157
if (!editorDom) {
158158
return;
159159
}
160160

161161
const anchor = Position.isIPosition(at) ? this.toCoords(at) : at;
162162

163-
CodeActionWidget.getOrCreateInstance(this._instantiationService).show(trigger, actions, anchor, editorDom, { ...options, showHeaders: this.shouldShowHeaders() }, {
164-
onSelectCodeAction: async (action, trigger, options) => {
165-
this.delegate.applyCodeAction(action, /* retrigger */ true, Boolean(options.preview || trigger.preview));
163+
const delegate: IRenderDelegate<CodeActionItem> = {
164+
onSelect: async (action: CodeActionItem, preview?: boolean) => {
165+
this.delegate.applyCodeAction(action, /* retrigger */ true, !!preview ? preview : false);
166+
this._actionWidgetService.hide();
166167
},
167168
onHide: () => {
168169
this._editor?.focus();
169-
},
170-
}, this._contextKeyService);
170+
}
171+
};
172+
this._actionWidgetService.show(
173+
'codeActionWidget',
174+
toMenuItems,
175+
delegate,
176+
actions,
177+
anchor,
178+
editorDom,
179+
{ ...options, showHeaders: this.shouldShowHeaders() });
171180
}
172181

173182
private toCoords(position: IPosition): IAnchor {

0 commit comments

Comments
 (0)