Path: blob/main/src/vs/editor/contrib/codeAction/browser/codeAction.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { coalesce, equals, isNonEmptyArray } from '../../../../base/common/arrays.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { illegalArgument, isCancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js';8import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';9import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';10import { URI } from '../../../../base/common/uri.js';11import * as nls from '../../../../nls.js';12import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';13import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';14import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';15import { INotificationService } from '../../../../platform/notification/common/notification.js';16import { IProgress, Progress } from '../../../../platform/progress/common/progress.js';17import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';18import { ICodeEditor } from '../../../browser/editorBrowser.js';19import { IBulkEditService } from '../../../browser/services/bulkEditService.js';20import { Range } from '../../../common/core/range.js';21import { Selection } from '../../../common/core/selection.js';22import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';23import * as languages from '../../../common/languages.js';24import { ITextModel } from '../../../common/model.js';25import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';26import { IModelService } from '../../../common/services/model.js';27import { EditSources } from '../../../common/textModelEditSource.js';28import { TextModelCancellationTokenSource } from '../../editorState/browser/editorState.js';29import { CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource, filtersAction, mayIncludeActionsOfKind } from '../common/types.js';3031export const codeActionCommandId = 'editor.action.codeAction';32export const quickFixCommandId = 'editor.action.quickFix';33export const autoFixCommandId = 'editor.action.autoFix';34export const refactorCommandId = 'editor.action.refactor';35export const refactorPreviewCommandId = 'editor.action.refactor.preview';36export const sourceActionCommandId = 'editor.action.sourceAction';37export const organizeImportsCommandId = 'editor.action.organizeImports';38export const fixAllCommandId = 'editor.action.fixAll';39const CODE_ACTION_SOUND_APPLIED_DURATION = 1000;4041class ManagedCodeActionSet extends Disposable implements CodeActionSet {4243private static codeActionsPreferredComparator(a: languages.CodeAction, b: languages.CodeAction): number {44if (a.isPreferred && !b.isPreferred) {45return -1;46} else if (!a.isPreferred && b.isPreferred) {47return 1;48} else {49return 0;50}51}5253private static codeActionsComparator({ action: a }: CodeActionItem, { action: b }: CodeActionItem): number {54if (a.isAI && !b.isAI) {55return 1;56} else if (!a.isAI && b.isAI) {57return -1;58}59if (isNonEmptyArray(a.diagnostics)) {60return isNonEmptyArray(b.diagnostics) ? ManagedCodeActionSet.codeActionsPreferredComparator(a, b) : -1;61} else if (isNonEmptyArray(b.diagnostics)) {62return 1;63} else {64return ManagedCodeActionSet.codeActionsPreferredComparator(a, b); // both have no diagnostics65}66}6768public readonly validActions: readonly CodeActionItem[];69public readonly allActions: readonly CodeActionItem[];7071public constructor(72actions: readonly CodeActionItem[],73public readonly documentation: readonly languages.Command[],74disposables: DisposableStore,75) {76super();7778this._register(disposables);7980this.allActions = [...actions].sort(ManagedCodeActionSet.codeActionsComparator);81this.validActions = this.allActions.filter(({ action }) => !action.disabled);82}8384public get hasAutoFix() {85return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new HierarchicalKind(fix.kind)) && !!fix.isPreferred);86}8788public get hasAIFix() {89return this.validActions.some(({ action: fix }) => !!fix.isAI);90}9192public get allAIFixes() {93return this.validActions.every(({ action: fix }) => !!fix.isAI);94}95}9697const emptyCodeActionsResponse = { actions: [] as CodeActionItem[], documentation: undefined };9899export async function getCodeActions(100registry: LanguageFeatureRegistry<languages.CodeActionProvider>,101model: ITextModel,102rangeOrSelection: Range | Selection,103trigger: CodeActionTrigger,104progress: IProgress<languages.CodeActionProvider>,105token: CancellationToken,106): Promise<CodeActionSet> {107const filter = trigger.filter || {};108const notebookFilter: CodeActionFilter = {109...filter,110excludes: [...(filter.excludes || []), CodeActionKind.Notebook],111};112113const codeActionContext: languages.CodeActionContext = {114only: filter.include?.value,115trigger: trigger.type,116};117118const cts = new TextModelCancellationTokenSource(model, token);119// if the trigger is auto (autosave, lightbulb, etc), we should exclude notebook codeActions120const excludeNotebookCodeActions = (trigger.type === languages.CodeActionTriggerType.Auto);121const providers = getCodeActionProviders(registry, model, (excludeNotebookCodeActions) ? notebookFilter : filter);122123const disposables = new DisposableStore();124const promises = providers.map(async provider => {125const handle = setTimeout(() => progress.report(provider), 1250);126try {127const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token);128if (cts.token.isCancellationRequested) {129providedCodeActions?.dispose();130return emptyCodeActionsResponse;131}132133if (providedCodeActions) {134disposables.add(providedCodeActions);135}136137const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action));138const documentation = getDocumentationFromProvider(provider, filteredActions, filter.include);139return {140actions: filteredActions.map(action => new CodeActionItem(action, provider)),141documentation142};143} catch (err) {144if (isCancellationError(err)) {145throw err;146}147onUnexpectedExternalError(err);148return emptyCodeActionsResponse;149} finally {150clearTimeout(handle);151}152});153154const listener = registry.onDidChange(() => {155const newProviders = registry.all(model);156if (!equals(newProviders, providers)) {157cts.cancel();158}159});160161try {162const actions = await Promise.all(promises);163const allActions = actions.map(x => x.actions).flat();164const allDocumentation = [165...coalesce(actions.map(x => x.documentation)),166...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions)167];168const managedCodeActionSet = new ManagedCodeActionSet(allActions, allDocumentation, disposables);169disposables.add(managedCodeActionSet);170return managedCodeActionSet;171} catch (err) {172disposables.dispose();173throw err;174} finally {175listener.dispose();176cts.dispose();177}178}179180function getCodeActionProviders(181registry: LanguageFeatureRegistry<languages.CodeActionProvider>,182model: ITextModel,183filter: CodeActionFilter184) {185return registry.all(model)186// Don't include providers that we know will not return code actions of interest187.filter(provider => {188if (!provider.providedCodeActionKinds) {189// We don't know what type of actions this provider will return.190return true;191}192return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new HierarchicalKind(kind)));193});194}195196function* getAdditionalDocumentationForShowingActions(197registry: LanguageFeatureRegistry<languages.CodeActionProvider>,198model: ITextModel,199trigger: CodeActionTrigger,200actionsToShow: readonly CodeActionItem[],201): Iterable<languages.Command> {202if (model && actionsToShow.length) {203for (const provider of registry.all(model)) {204if (provider._getAdditionalMenuItems) {205yield* provider._getAdditionalMenuItems?.({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action));206}207}208}209}210211function getDocumentationFromProvider(212provider: languages.CodeActionProvider,213providedCodeActions: readonly languages.CodeAction[],214only?: HierarchicalKind215): languages.Command | undefined {216if (!provider.documentation) {217return undefined;218}219220const documentation = provider.documentation.map(entry => ({ kind: new HierarchicalKind(entry.kind), command: entry.command }));221222if (only) {223let currentBest: { readonly kind: HierarchicalKind; readonly command: languages.Command } | undefined;224for (const entry of documentation) {225if (entry.kind.contains(only)) {226if (!currentBest) {227currentBest = entry;228} else {229// Take best match230if (currentBest.kind.contains(entry.kind)) {231currentBest = entry;232}233}234}235}236if (currentBest) {237return currentBest?.command;238}239}240241// Otherwise, check to see if any of the provided actions match.242for (const action of providedCodeActions) {243if (!action.kind) {244continue;245}246247for (const entry of documentation) {248if (entry.kind.contains(new HierarchicalKind(action.kind))) {249return entry.command;250}251}252}253return undefined;254}255256export enum ApplyCodeActionReason {257OnSave = 'onSave',258FromProblemsView = 'fromProblemsView',259FromCodeActions = 'fromCodeActions',260FromAILightbulb = 'fromAILightbulb', // direct invocation when clicking on the AI lightbulb261FromProblemsHover = 'fromProblemsHover'262}263264export async function applyCodeAction(265accessor: ServicesAccessor,266item: CodeActionItem,267codeActionReason: ApplyCodeActionReason,268options?: { readonly preview?: boolean; readonly editor?: ICodeEditor },269token: CancellationToken = CancellationToken.None,270): Promise<void> {271const bulkEditService = accessor.get(IBulkEditService);272const commandService = accessor.get(ICommandService);273const telemetryService = accessor.get(ITelemetryService);274const notificationService = accessor.get(INotificationService);275const accessibilitySignalService = accessor.get(IAccessibilitySignalService);276277type ApplyCodeActionEvent = {278codeActionTitle: string;279codeActionKind: string | undefined;280codeActionIsPreferred: boolean;281reason: ApplyCodeActionReason;282};283type ApplyCodeEventClassification = {284codeActionTitle: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The display label of the applied code action' };285codeActionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind (refactor, quickfix) of the applied code action' };286codeActionIsPreferred: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Was the code action marked as being a preferred action?' };287reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to trigger apply code action.' };288owner: 'justschen';289comment: 'Event used to gain insights into which code actions are being triggered';290};291292telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', {293codeActionTitle: item.action.title,294codeActionKind: item.action.kind,295codeActionIsPreferred: !!item.action.isPreferred,296reason: codeActionReason,297});298accessibilitySignalService.playSignal(AccessibilitySignal.codeActionTriggered);299await item.resolve(token);300if (token.isCancellationRequested) {301return;302}303304if (item.action.edit?.edits.length) {305const result = await bulkEditService.apply(item.action.edit, {306editor: options?.editor,307label: item.action.title,308quotableLabel: item.action.title,309code: 'undoredo.codeAction',310respectAutoSaveConfig: codeActionReason !== ApplyCodeActionReason.OnSave,311showPreview: options?.preview,312reason: EditSources.codeAction({ kind: item.action.kind, providerId: languages.ProviderId.fromExtensionId(item.provider?.extensionId) }),313});314315if (!result.isApplied) {316return;317}318}319320if (item.action.command) {321try {322await commandService.executeCommand(item.action.command.id, ...(item.action.command.arguments || []));323} catch (err) {324const message = asMessage(err);325notificationService.error(326typeof message === 'string'327? message328: nls.localize('applyCodeActionFailed', "An unknown error occurred while applying the code action"));329}330}331// ensure the start sound and end sound do not overlap332setTimeout(() => accessibilitySignalService.playSignal(AccessibilitySignal.codeActionApplied), CODE_ACTION_SOUND_APPLIED_DURATION);333}334335function asMessage(err: any): string | undefined {336if (typeof err === 'string') {337return err;338} else if (err instanceof Error && typeof err.message === 'string') {339return err.message;340} else {341return undefined;342}343}344345CommandsRegistry.registerCommand('_executeCodeActionProvider', async function (accessor, resource: URI, rangeOrSelection: Range | Selection, kind?: string, itemResolveCount?: number): Promise<ReadonlyArray<languages.CodeAction>> {346if (!(resource instanceof URI)) {347throw illegalArgument();348}349350const { codeActionProvider } = accessor.get(ILanguageFeaturesService);351const model = accessor.get(IModelService).getModel(resource);352if (!model) {353throw illegalArgument();354}355356const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection)357? Selection.liftSelection(rangeOrSelection)358: Range.isIRange(rangeOrSelection)359? model.validateRange(rangeOrSelection)360: undefined;361362if (!validatedRangeOrSelection) {363throw illegalArgument();364}365366const include = typeof kind === 'string' ? new HierarchicalKind(kind) : undefined;367const codeActionSet = await getCodeActions(368codeActionProvider,369model,370validatedRangeOrSelection,371{ type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default, filter: { includeSourceActions: true, include } },372Progress.None,373CancellationToken.None);374375const resolving: Promise<any>[] = [];376const resolveCount = Math.min(codeActionSet.validActions.length, typeof itemResolveCount === 'number' ? itemResolveCount : 0);377for (let i = 0; i < resolveCount; i++) {378resolving.push(codeActionSet.validActions[i].resolve(CancellationToken.None));379}380381try {382await Promise.all(resolving);383return codeActionSet.validActions.map(item => item.action);384} finally {385setTimeout(() => codeActionSet.dispose(), 100);386}387});388389390