Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/codeAction/browser/codeAction.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 { coalesce, equals, isNonEmptyArray } from '../../../../base/common/arrays.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { illegalArgument, isCancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js';
9
import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
10
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import * as nls from '../../../../nls.js';
13
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
14
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
15
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
16
import { INotificationService } from '../../../../platform/notification/common/notification.js';
17
import { IProgress, Progress } from '../../../../platform/progress/common/progress.js';
18
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
19
import { ICodeEditor } from '../../../browser/editorBrowser.js';
20
import { IBulkEditService } from '../../../browser/services/bulkEditService.js';
21
import { Range } from '../../../common/core/range.js';
22
import { Selection } from '../../../common/core/selection.js';
23
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
24
import * as languages from '../../../common/languages.js';
25
import { ITextModel } from '../../../common/model.js';
26
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
27
import { IModelService } from '../../../common/services/model.js';
28
import { EditSources } from '../../../common/textModelEditSource.js';
29
import { TextModelCancellationTokenSource } from '../../editorState/browser/editorState.js';
30
import { CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource, filtersAction, mayIncludeActionsOfKind } from '../common/types.js';
31
32
export const codeActionCommandId = 'editor.action.codeAction';
33
export const quickFixCommandId = 'editor.action.quickFix';
34
export const autoFixCommandId = 'editor.action.autoFix';
35
export const refactorCommandId = 'editor.action.refactor';
36
export const refactorPreviewCommandId = 'editor.action.refactor.preview';
37
export const sourceActionCommandId = 'editor.action.sourceAction';
38
export const organizeImportsCommandId = 'editor.action.organizeImports';
39
export const fixAllCommandId = 'editor.action.fixAll';
40
const CODE_ACTION_SOUND_APPLIED_DURATION = 1000;
41
42
class ManagedCodeActionSet extends Disposable implements CodeActionSet {
43
44
private static codeActionsPreferredComparator(a: languages.CodeAction, b: languages.CodeAction): number {
45
if (a.isPreferred && !b.isPreferred) {
46
return -1;
47
} else if (!a.isPreferred && b.isPreferred) {
48
return 1;
49
} else {
50
return 0;
51
}
52
}
53
54
private static codeActionsComparator({ action: a }: CodeActionItem, { action: b }: CodeActionItem): number {
55
if (a.isAI && !b.isAI) {
56
return 1;
57
} else if (!a.isAI && b.isAI) {
58
return -1;
59
}
60
if (isNonEmptyArray(a.diagnostics)) {
61
return isNonEmptyArray(b.diagnostics) ? ManagedCodeActionSet.codeActionsPreferredComparator(a, b) : -1;
62
} else if (isNonEmptyArray(b.diagnostics)) {
63
return 1;
64
} else {
65
return ManagedCodeActionSet.codeActionsPreferredComparator(a, b); // both have no diagnostics
66
}
67
}
68
69
public readonly validActions: readonly CodeActionItem[];
70
public readonly allActions: readonly CodeActionItem[];
71
72
public constructor(
73
actions: readonly CodeActionItem[],
74
public readonly documentation: readonly languages.Command[],
75
disposables: DisposableStore,
76
) {
77
super();
78
79
this._register(disposables);
80
81
this.allActions = [...actions].sort(ManagedCodeActionSet.codeActionsComparator);
82
this.validActions = this.allActions.filter(({ action }) => !action.disabled);
83
}
84
85
public get hasAutoFix() {
86
return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new HierarchicalKind(fix.kind)) && !!fix.isPreferred);
87
}
88
89
public get hasAIFix() {
90
return this.validActions.some(({ action: fix }) => !!fix.isAI);
91
}
92
93
public get allAIFixes() {
94
return this.validActions.every(({ action: fix }) => !!fix.isAI);
95
}
96
}
97
98
const emptyCodeActionsResponse = { actions: [] as CodeActionItem[], documentation: undefined };
99
100
export async function getCodeActions(
101
registry: LanguageFeatureRegistry<languages.CodeActionProvider>,
102
model: ITextModel,
103
rangeOrSelection: Range | Selection,
104
trigger: CodeActionTrigger,
105
progress: IProgress<languages.CodeActionProvider>,
106
token: CancellationToken,
107
): Promise<CodeActionSet> {
108
const filter = trigger.filter || {};
109
const notebookFilter: CodeActionFilter = {
110
...filter,
111
excludes: [...(filter.excludes || []), CodeActionKind.Notebook],
112
};
113
114
const codeActionContext: languages.CodeActionContext = {
115
only: filter.include?.value,
116
trigger: trigger.type,
117
};
118
119
const cts = new TextModelCancellationTokenSource(model, token);
120
// if the trigger is auto (autosave, lightbulb, etc), we should exclude notebook codeActions
121
const excludeNotebookCodeActions = (trigger.type === languages.CodeActionTriggerType.Auto);
122
const providers = getCodeActionProviders(registry, model, (excludeNotebookCodeActions) ? notebookFilter : filter);
123
124
const disposables = new DisposableStore();
125
const promises = providers.map(async provider => {
126
const handle = setTimeout(() => progress.report(provider), 1250);
127
try {
128
const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token);
129
if (cts.token.isCancellationRequested) {
130
providedCodeActions?.dispose();
131
return emptyCodeActionsResponse;
132
}
133
134
if (providedCodeActions) {
135
disposables.add(providedCodeActions);
136
}
137
138
const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action));
139
const documentation = getDocumentationFromProvider(provider, filteredActions, filter.include);
140
return {
141
actions: filteredActions.map(action => new CodeActionItem(action, provider)),
142
documentation
143
};
144
} catch (err) {
145
if (isCancellationError(err)) {
146
throw err;
147
}
148
onUnexpectedExternalError(err);
149
return emptyCodeActionsResponse;
150
} finally {
151
clearTimeout(handle);
152
}
153
});
154
155
const listener = registry.onDidChange(() => {
156
const newProviders = registry.all(model);
157
if (!equals(newProviders, providers)) {
158
cts.cancel();
159
}
160
});
161
162
try {
163
const actions = await Promise.all(promises);
164
const allActions = actions.map(x => x.actions).flat();
165
const allDocumentation = [
166
...coalesce(actions.map(x => x.documentation)),
167
...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions)
168
];
169
const managedCodeActionSet = new ManagedCodeActionSet(allActions, allDocumentation, disposables);
170
disposables.add(managedCodeActionSet);
171
return managedCodeActionSet;
172
} catch (err) {
173
disposables.dispose();
174
throw err;
175
} finally {
176
listener.dispose();
177
cts.dispose();
178
}
179
}
180
181
function getCodeActionProviders(
182
registry: LanguageFeatureRegistry<languages.CodeActionProvider>,
183
model: ITextModel,
184
filter: CodeActionFilter
185
) {
186
return registry.all(model)
187
// Don't include providers that we know will not return code actions of interest
188
.filter(provider => {
189
if (!provider.providedCodeActionKinds) {
190
// We don't know what type of actions this provider will return.
191
return true;
192
}
193
return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new HierarchicalKind(kind)));
194
});
195
}
196
197
function* getAdditionalDocumentationForShowingActions(
198
registry: LanguageFeatureRegistry<languages.CodeActionProvider>,
199
model: ITextModel,
200
trigger: CodeActionTrigger,
201
actionsToShow: readonly CodeActionItem[],
202
): Iterable<languages.Command> {
203
if (model && actionsToShow.length) {
204
for (const provider of registry.all(model)) {
205
if (provider._getAdditionalMenuItems) {
206
yield* provider._getAdditionalMenuItems?.({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action));
207
}
208
}
209
}
210
}
211
212
function getDocumentationFromProvider(
213
provider: languages.CodeActionProvider,
214
providedCodeActions: readonly languages.CodeAction[],
215
only?: HierarchicalKind
216
): languages.Command | undefined {
217
if (!provider.documentation) {
218
return undefined;
219
}
220
221
const documentation = provider.documentation.map(entry => ({ kind: new HierarchicalKind(entry.kind), command: entry.command }));
222
223
if (only) {
224
let currentBest: { readonly kind: HierarchicalKind; readonly command: languages.Command } | undefined;
225
for (const entry of documentation) {
226
if (entry.kind.contains(only)) {
227
if (!currentBest) {
228
currentBest = entry;
229
} else {
230
// Take best match
231
if (currentBest.kind.contains(entry.kind)) {
232
currentBest = entry;
233
}
234
}
235
}
236
}
237
if (currentBest) {
238
return currentBest?.command;
239
}
240
}
241
242
// Otherwise, check to see if any of the provided actions match.
243
for (const action of providedCodeActions) {
244
if (!action.kind) {
245
continue;
246
}
247
248
for (const entry of documentation) {
249
if (entry.kind.contains(new HierarchicalKind(action.kind))) {
250
return entry.command;
251
}
252
}
253
}
254
return undefined;
255
}
256
257
export enum ApplyCodeActionReason {
258
OnSave = 'onSave',
259
FromProblemsView = 'fromProblemsView',
260
FromCodeActions = 'fromCodeActions',
261
FromAILightbulb = 'fromAILightbulb', // direct invocation when clicking on the AI lightbulb
262
FromProblemsHover = 'fromProblemsHover'
263
}
264
265
export async function applyCodeAction(
266
accessor: ServicesAccessor,
267
item: CodeActionItem,
268
codeActionReason: ApplyCodeActionReason,
269
options?: { readonly preview?: boolean; readonly editor?: ICodeEditor },
270
token: CancellationToken = CancellationToken.None,
271
): Promise<void> {
272
const bulkEditService = accessor.get(IBulkEditService);
273
const commandService = accessor.get(ICommandService);
274
const telemetryService = accessor.get(ITelemetryService);
275
const notificationService = accessor.get(INotificationService);
276
const accessibilitySignalService = accessor.get(IAccessibilitySignalService);
277
278
type ApplyCodeActionEvent = {
279
codeActionTitle: string;
280
codeActionKind: string | undefined;
281
codeActionIsPreferred: boolean;
282
reason: ApplyCodeActionReason;
283
};
284
type ApplyCodeEventClassification = {
285
codeActionTitle: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The display label of the applied code action' };
286
codeActionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind (refactor, quickfix) of the applied code action' };
287
codeActionIsPreferred: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Was the code action marked as being a preferred action?' };
288
reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to trigger apply code action.' };
289
owner: 'justschen';
290
comment: 'Event used to gain insights into which code actions are being triggered';
291
};
292
293
telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', {
294
codeActionTitle: item.action.title,
295
codeActionKind: item.action.kind,
296
codeActionIsPreferred: !!item.action.isPreferred,
297
reason: codeActionReason,
298
});
299
accessibilitySignalService.playSignal(AccessibilitySignal.codeActionTriggered);
300
await item.resolve(token);
301
if (token.isCancellationRequested) {
302
return;
303
}
304
305
if (item.action.edit?.edits.length) {
306
const result = await bulkEditService.apply(item.action.edit, {
307
editor: options?.editor,
308
label: item.action.title,
309
quotableLabel: item.action.title,
310
code: 'undoredo.codeAction',
311
respectAutoSaveConfig: codeActionReason !== ApplyCodeActionReason.OnSave,
312
showPreview: options?.preview,
313
reason: EditSources.codeAction({ kind: item.action.kind, providerId: languages.ProviderId.fromExtensionId(item.provider?.extensionId) }),
314
});
315
316
if (!result.isApplied) {
317
return;
318
}
319
}
320
321
if (item.action.command) {
322
try {
323
await commandService.executeCommand(item.action.command.id, ...(item.action.command.arguments || []));
324
} catch (err) {
325
const message = asMessage(err);
326
notificationService.error(
327
typeof message === 'string'
328
? message
329
: nls.localize('applyCodeActionFailed', "An unknown error occurred while applying the code action"));
330
}
331
}
332
// ensure the start sound and end sound do not overlap
333
setTimeout(() => accessibilitySignalService.playSignal(AccessibilitySignal.codeActionApplied), CODE_ACTION_SOUND_APPLIED_DURATION);
334
}
335
336
function asMessage(err: any): string | undefined {
337
if (typeof err === 'string') {
338
return err;
339
} else if (err instanceof Error && typeof err.message === 'string') {
340
return err.message;
341
} else {
342
return undefined;
343
}
344
}
345
346
CommandsRegistry.registerCommand('_executeCodeActionProvider', async function (accessor, resource: URI, rangeOrSelection: Range | Selection, kind?: string, itemResolveCount?: number): Promise<ReadonlyArray<languages.CodeAction>> {
347
if (!(resource instanceof URI)) {
348
throw illegalArgument();
349
}
350
351
const { codeActionProvider } = accessor.get(ILanguageFeaturesService);
352
const model = accessor.get(IModelService).getModel(resource);
353
if (!model) {
354
throw illegalArgument();
355
}
356
357
const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection)
358
? Selection.liftSelection(rangeOrSelection)
359
: Range.isIRange(rangeOrSelection)
360
? model.validateRange(rangeOrSelection)
361
: undefined;
362
363
if (!validatedRangeOrSelection) {
364
throw illegalArgument();
365
}
366
367
const include = typeof kind === 'string' ? new HierarchicalKind(kind) : undefined;
368
const codeActionSet = await getCodeActions(
369
codeActionProvider,
370
model,
371
validatedRangeOrSelection,
372
{ type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default, filter: { includeSourceActions: true, include } },
373
Progress.None,
374
CancellationToken.None);
375
376
const resolving: Promise<any>[] = [];
377
const resolveCount = Math.min(codeActionSet.validActions.length, typeof itemResolveCount === 'number' ? itemResolveCount : 0);
378
for (let i = 0; i < resolveCount; i++) {
379
resolving.push(codeActionSet.validActions[i].resolve(CancellationToken.None));
380
}
381
382
try {
383
await Promise.all(resolving);
384
return codeActionSet.validActions.map(item => item.action);
385
} finally {
386
setTimeout(() => codeActionSet.dispose(), 100);
387
}
388
});
389
390