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