Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/codeAction/browser/codeActionController.ts
5236 views
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 { getDomNodePagePosition } from '../../../../base/browser/dom.js';
7
import * as aria from '../../../../base/browser/ui/aria/aria.js';
8
import { IAnchor } from '../../../../base/browser/ui/contextview/contextview.js';
9
import { IAction } from '../../../../base/common/actions.js';
10
import { CancellationToken } from '../../../../base/common/cancellation.js';
11
import { Color } from '../../../../base/common/color.js';
12
import { onUnexpectedError } from '../../../../base/common/errors.js';
13
import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
14
import { Lazy } from '../../../../base/common/lazy.js';
15
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
16
import { derived, IObservable } from '../../../../base/common/observable.js';
17
import { localize } from '../../../../nls.js';
18
import { IActionListDelegate } from '../../../../platform/actionWidget/browser/actionList.js';
19
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
20
import { ICommandService } from '../../../../platform/commands/common/commands.js';
21
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
22
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
23
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
24
import { IMarkerService } from '../../../../platform/markers/common/markers.js';
25
import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';
26
import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from '../../../../platform/theme/common/colorRegistry.js';
27
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
28
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
29
import { ICodeEditor } from '../../../browser/editorBrowser.js';
30
import { IPosition, Position } from '../../../common/core/position.js';
31
import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';
32
import { CodeActionTriggerType } from '../../../common/languages.js';
33
import { IModelDeltaDecoration } from '../../../common/model.js';
34
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
35
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
36
import { MessageController } from '../../message/browser/messageController.js';
37
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types.js';
38
import { ApplyCodeActionReason, applyCodeAction } from './codeAction.js';
39
import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver.js';
40
import { toMenuItems } from './codeActionMenu.js';
41
import { CodeActionModel, CodeActionsState } from './codeActionModel.js';
42
import { LightBulbInfo, LightBulbWidget } from './lightBulbWidget.js';
43
44
interface IActionShowOptions {
45
readonly includeDisabledActions?: boolean;
46
readonly fromLightbulb?: boolean;
47
}
48
49
50
const DECORATION_CLASS_NAME = 'quickfix-edit-highlight';
51
52
export class CodeActionController extends Disposable implements IEditorContribution {
53
54
public static readonly ID = 'editor.contrib.codeActionController';
55
56
public static get(editor: ICodeEditor): CodeActionController | null {
57
return editor.getContribution<CodeActionController>(CodeActionController.ID);
58
}
59
60
private readonly _editor: ICodeEditor;
61
private readonly _model: CodeActionModel;
62
63
private readonly _lightBulbWidget: Lazy<LightBulbWidget | null>;
64
private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>());
65
private _showDisabled = false;
66
67
private readonly _resolver: CodeActionKeybindingResolver;
68
69
private _disposed = false;
70
71
public readonly lightBulbState: IObservable<LightBulbInfo | undefined> = derived(this, reader => {
72
const widget = this._lightBulbWidget.rawValue;
73
if (!widget) {
74
return undefined;
75
}
76
return widget.lightBulbInfo.read(reader);
77
});
78
79
constructor(
80
editor: ICodeEditor,
81
@IMarkerService markerService: IMarkerService,
82
@IContextKeyService contextKeyService: IContextKeyService,
83
@IInstantiationService instantiationService: IInstantiationService,
84
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
85
@IEditorProgressService progressService: IEditorProgressService,
86
@ICommandService private readonly _commandService: ICommandService,
87
@IConfigurationService private readonly _configurationService: IConfigurationService,
88
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
89
@IInstantiationService private readonly _instantiationService: IInstantiationService,
90
@IEditorProgressService private readonly _progressService: IEditorProgressService,
91
) {
92
super();
93
94
this._editor = editor;
95
this._model = this._register(new CodeActionModel(this._editor, languageFeaturesService.codeActionProvider, markerService, contextKeyService, progressService, _configurationService));
96
this._register(this._model.onDidChangeState(newState => this.update(newState)));
97
98
this._lightBulbWidget = new Lazy(() => {
99
const widget = this._editor.getContribution<LightBulbWidget>(LightBulbWidget.ID);
100
if (widget) {
101
this._register(widget.onClick(e => this.showCodeActionsFromLightbulb(e.actions, e)));
102
}
103
return widget;
104
});
105
106
this._resolver = instantiationService.createInstance(CodeActionKeybindingResolver);
107
108
this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide()));
109
}
110
111
override dispose() {
112
this._disposed = true;
113
super.dispose();
114
}
115
116
private async showCodeActionsFromLightbulb(actions: CodeActionSet, at: IAnchor | IPosition): Promise<void> {
117
if (actions.allAIFixes && actions.validActions.length === 1) {
118
const actionItem = actions.validActions[0];
119
const command = actionItem.action.command;
120
if (command && command.id === 'inlineChat.start') {
121
if (command.arguments && command.arguments.length >= 1 && command.arguments[0]) {
122
command.arguments[0] = { ...command.arguments[0], autoSend: false };
123
}
124
}
125
await this.applyCodeAction(actionItem, false, false, ApplyCodeActionReason.FromAILightbulb);
126
return;
127
}
128
await this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: true });
129
}
130
131
public showCodeActions(_trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) {
132
return this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: false });
133
}
134
135
public hideCodeActions(): void {
136
this._actionWidgetService.hide();
137
}
138
139
public manualTriggerAtCurrentPosition(
140
notAvailableMessage: string,
141
triggerAction: CodeActionTriggerSource,
142
filter?: CodeActionFilter,
143
autoApply?: CodeActionAutoApply,
144
): void {
145
if (!this._editor.hasModel()) {
146
return;
147
}
148
149
MessageController.get(this._editor)?.closeMessage();
150
const triggerPosition = this._editor.getPosition();
151
this._trigger({ type: CodeActionTriggerType.Invoke, triggerAction, filter, autoApply, context: { notAvailableMessage, position: triggerPosition } });
152
}
153
154
private _trigger(trigger: CodeActionTrigger) {
155
return this._model.trigger(trigger);
156
}
157
158
async applyCodeAction(action: CodeActionItem, retrigger: boolean, preview: boolean, actionReason: ApplyCodeActionReason): Promise<void> {
159
const progress = this._progressService.show(true, 500);
160
try {
161
await this._instantiationService.invokeFunction(applyCodeAction, action, actionReason, { preview, editor: this._editor });
162
} finally {
163
if (retrigger) {
164
this._trigger({ type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.QuickFix, filter: {} });
165
}
166
progress.done();
167
}
168
}
169
170
public hideLightBulbWidget(): void {
171
this._lightBulbWidget.rawValue?.hide();
172
this._lightBulbWidget.rawValue?.gutterHide();
173
}
174
175
private async update(newState: CodeActionsState.State): Promise<void> {
176
if (newState.type !== CodeActionsState.Type.Triggered) {
177
this.hideLightBulbWidget();
178
return;
179
}
180
181
let actions: CodeActionSet;
182
try {
183
actions = await newState.actions;
184
} catch (e) {
185
onUnexpectedError(e);
186
return;
187
}
188
189
if (this._disposed) {
190
return;
191
}
192
193
194
const selection = this._editor.getSelection();
195
if (selection?.startLineNumber !== newState.position.lineNumber) {
196
return;
197
}
198
199
this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position);
200
201
if (newState.trigger.type === CodeActionTriggerType.Invoke) {
202
if (newState.trigger.filter?.include) { // Triggered for specific scope
203
// Check to see if we want to auto apply.
204
205
const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions);
206
if (validActionToApply) {
207
try {
208
this.hideLightBulbWidget();
209
await this.applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions);
210
} finally {
211
actions.dispose();
212
}
213
return;
214
}
215
216
// Check to see if there is an action that we would have applied were it not invalid
217
if (newState.trigger.context) {
218
const invalidAction = this.getInvalidActionThatWouldHaveBeenApplied(newState.trigger, actions);
219
if (invalidAction && invalidAction.action.disabled) {
220
MessageController.get(this._editor)?.showMessage(invalidAction.action.disabled, newState.trigger.context.position);
221
actions.dispose();
222
return;
223
}
224
}
225
}
226
227
const includeDisabledActions = !!newState.trigger.filter?.include;
228
if (newState.trigger.context) {
229
if (!actions.allActions.length || !includeDisabledActions && !actions.validActions.length) {
230
MessageController.get(this._editor)?.showMessage(newState.trigger.context.notAvailableMessage, newState.trigger.context.position);
231
this._activeCodeActions.value = actions;
232
actions.dispose();
233
return;
234
}
235
}
236
237
this._activeCodeActions.value = actions;
238
this.showCodeActionList(actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false });
239
} else {
240
// auto magically triggered
241
if (this._actionWidgetService.isVisible) {
242
// TODO: Figure out if we should update the showing menu?
243
actions.dispose();
244
} else {
245
this._activeCodeActions.value = actions;
246
}
247
}
248
}
249
250
private getInvalidActionThatWouldHaveBeenApplied(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {
251
if (!actions.allActions.length) {
252
return undefined;
253
}
254
255
if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length === 0)
256
|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.allActions.length === 1)
257
) {
258
return actions.allActions.find(({ action }) => action.disabled);
259
}
260
261
return undefined;
262
}
263
264
private tryGetValidActionToApply(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {
265
if (!actions.validActions.length) {
266
return undefined;
267
}
268
269
if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length > 0)
270
|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.validActions.length === 1)
271
) {
272
return actions.validActions[0];
273
}
274
275
return undefined;
276
}
277
278
private static readonly DECORATION = ModelDecorationOptions.register({
279
description: 'quickfix-highlight',
280
className: DECORATION_CLASS_NAME
281
});
282
283
public async showCodeActionList(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): Promise<void> {
284
285
const currentDecorations = this._editor.createDecorationsCollection();
286
287
const editorDom = this._editor.getDomNode();
288
if (!editorDom) {
289
return;
290
}
291
292
const actionsToShow = options.includeDisabledActions && (this._showDisabled || actions.validActions.length === 0) ? actions.allActions : actions.validActions;
293
if (!actionsToShow.length) {
294
return;
295
}
296
297
const anchor = Position.isIPosition(at) ? this.toCoords(at) : at;
298
299
const delegate: IActionListDelegate<CodeActionItem> = {
300
onSelect: async (action: CodeActionItem, preview?: boolean) => {
301
this.applyCodeAction(action, /* retrigger */ true, !!preview, options.fromLightbulb ? ApplyCodeActionReason.FromAILightbulb : ApplyCodeActionReason.FromCodeActions);
302
this._actionWidgetService.hide(false);
303
currentDecorations.clear();
304
},
305
onHide: (didCancel?) => {
306
this._editor?.focus();
307
currentDecorations.clear();
308
},
309
onHover: async (action: CodeActionItem, token: CancellationToken) => {
310
if (token.isCancellationRequested) {
311
return;
312
}
313
314
let canPreview = false;
315
const actionKind = action.action.kind;
316
317
if (actionKind) {
318
const hierarchicalKind = new HierarchicalKind(actionKind);
319
const refactorKinds = [
320
CodeActionKind.RefactorExtract,
321
CodeActionKind.RefactorInline,
322
CodeActionKind.RefactorRewrite,
323
CodeActionKind.RefactorMove,
324
CodeActionKind.Source
325
];
326
327
canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind));
328
}
329
330
return { canPreview: canPreview || !!action.action.edit?.edits.length };
331
},
332
onFocus: (action: CodeActionItem | undefined) => {
333
if (action && action.action) {
334
const ranges = action.action.ranges;
335
const diagnostics = action.action.diagnostics;
336
currentDecorations.clear();
337
if (ranges && ranges.length > 0) {
338
// Handles case for `fix all` where there are multiple diagnostics.
339
const decorations: IModelDeltaDecoration[] = (diagnostics && diagnostics?.length > 1)
340
? diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }))
341
: ranges.map(range => ({ range, options: CodeActionController.DECORATION }));
342
currentDecorations.set(decorations);
343
} else if (diagnostics && diagnostics.length > 0) {
344
const decorations: IModelDeltaDecoration[] = diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }));
345
currentDecorations.set(decorations);
346
const diagnostic = diagnostics[0];
347
if (diagnostic.startLineNumber && diagnostic.startColumn) {
348
const selectionText = this._editor.getModel()?.getWordAtPosition({ lineNumber: diagnostic.startLineNumber, column: diagnostic.startColumn })?.word;
349
aria.status(localize('editingNewSelection', "Context: {0} at line {1} and column {2}.", selectionText, diagnostic.startLineNumber, diagnostic.startColumn));
350
}
351
}
352
} else {
353
currentDecorations.clear();
354
}
355
}
356
};
357
358
this._actionWidgetService.show(
359
'codeActionWidget',
360
true,
361
toMenuItems(actionsToShow, this._shouldShowHeaders(), this._resolver.getResolver()),
362
delegate,
363
anchor,
364
editorDom,
365
this._getActionBarActions(actions, at, options));
366
}
367
368
private toCoords(position: IPosition): IAnchor {
369
if (!this._editor.hasModel()) {
370
return { x: 0, y: 0 };
371
}
372
373
this._editor.revealPosition(position, ScrollType.Immediate);
374
this._editor.render();
375
376
// Translate to absolute editor position
377
const cursorCoords = this._editor.getScrolledVisiblePosition(position);
378
const editorCoords = getDomNodePagePosition(this._editor.getDomNode());
379
const x = editorCoords.left + cursorCoords.left;
380
const y = editorCoords.top + cursorCoords.top + cursorCoords.height;
381
382
return { x, y };
383
}
384
385
private _shouldShowHeaders(): boolean {
386
const model = this._editor?.getModel();
387
return this._configurationService.getValue('editor.codeActionWidget.showHeaders', { resource: model?.uri });
388
}
389
390
private _getActionBarActions(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): IAction[] {
391
if (options.fromLightbulb) {
392
return [];
393
}
394
395
const resultActions = actions.documentation.map((command): IAction => ({
396
id: command.id,
397
label: command.title,
398
tooltip: command.tooltip ?? '',
399
class: undefined,
400
enabled: true,
401
run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])),
402
}));
403
404
if (options.includeDisabledActions && actions.validActions.length > 0 && actions.allActions.length !== actions.validActions.length) {
405
resultActions.push(this._showDisabled ? {
406
id: 'hideMoreActions',
407
label: localize('hideMoreActions', 'Hide Disabled'),
408
enabled: true,
409
tooltip: '',
410
class: undefined,
411
run: () => {
412
this._showDisabled = false;
413
return this.showCodeActionList(actions, at, options);
414
}
415
} : {
416
id: 'showMoreActions',
417
label: localize('showMoreActions', 'Show Disabled'),
418
enabled: true,
419
tooltip: '',
420
class: undefined,
421
run: () => {
422
this._showDisabled = true;
423
return this.showCodeActionList(actions, at, options);
424
}
425
});
426
}
427
428
return resultActions;
429
}
430
}
431
432
registerThemingParticipant((theme, collector) => {
433
const addBackgroundColorRule = (selector: string, color: Color | undefined): void => {
434
if (color) {
435
collector.addRule(`.monaco-editor ${selector} { background-color: ${color}; }`);
436
}
437
};
438
439
addBackgroundColorRule('.quickfix-edit-highlight', theme.getColor(editorFindMatchHighlight));
440
const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
441
442
if (findMatchHighlightBorder) {
443
collector.addRule(`.monaco-editor .quickfix-edit-highlight { border: 1px ${isHighContrast(theme.type) ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`);
444
}
445
});
446
447