UNPKG

monaco-editor-core

Version:

A browser based code editor

284 lines (283 loc) • 12.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { coalesce, equals, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { illegalArgument, isCancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { Range } from '../../../common/core/range.js'; import { Selection } from '../../../common/core/selection.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { IModelService } from '../../../common/services/model.js'; import { TextModelCancellationTokenSource } from '../../editorState/browser/editorState.js'; import * as nls from '../../../../nls.js'; import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CodeActionItem, CodeActionKind, CodeActionTriggerSource, filtersAction, mayIncludeActionsOfKind } from '../common/types.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; export const codeActionCommandId = 'editor.action.codeAction'; export const quickFixCommandId = 'editor.action.quickFix'; export const autoFixCommandId = 'editor.action.autoFix'; export const refactorCommandId = 'editor.action.refactor'; export const sourceActionCommandId = 'editor.action.sourceAction'; export const organizeImportsCommandId = 'editor.action.organizeImports'; export const fixAllCommandId = 'editor.action.fixAll'; class ManagedCodeActionSet extends Disposable { static codeActionsPreferredComparator(a, b) { if (a.isPreferred && !b.isPreferred) { return -1; } else if (!a.isPreferred && b.isPreferred) { return 1; } else { return 0; } } static codeActionsComparator({ action: a }, { action: b }) { if (a.isAI && !b.isAI) { return 1; } else if (!a.isAI && b.isAI) { return -1; } if (isNonEmptyArray(a.diagnostics)) { return isNonEmptyArray(b.diagnostics) ? ManagedCodeActionSet.codeActionsPreferredComparator(a, b) : -1; } else if (isNonEmptyArray(b.diagnostics)) { return 1; } else { return ManagedCodeActionSet.codeActionsPreferredComparator(a, b); // both have no diagnostics } } constructor(actions, documentation, disposables) { super(); this.documentation = documentation; this._register(disposables); this.allActions = [...actions].sort(ManagedCodeActionSet.codeActionsComparator); this.validActions = this.allActions.filter(({ action }) => !action.disabled); } get hasAutoFix() { return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new HierarchicalKind(fix.kind)) && !!fix.isPreferred); } get hasAIFix() { return this.validActions.some(({ action: fix }) => !!fix.isAI); } get allAIFixes() { return this.validActions.every(({ action: fix }) => !!fix.isAI); } } const emptyCodeActionsResponse = { actions: [], documentation: undefined }; export async function getCodeActions(registry, model, rangeOrSelection, trigger, progress, token) { const filter = trigger.filter || {}; const notebookFilter = { ...filter, excludes: [...(filter.excludes || []), CodeActionKind.Notebook], }; const codeActionContext = { only: filter.include?.value, trigger: trigger.type, }; const cts = new TextModelCancellationTokenSource(model, token); // if the trigger is auto (autosave, lightbulb, etc), we should exclude notebook codeActions const excludeNotebookCodeActions = (trigger.type === 2 /* languages.CodeActionTriggerType.Auto */); const providers = getCodeActionProviders(registry, model, (excludeNotebookCodeActions) ? notebookFilter : filter); const disposables = new DisposableStore(); const promises = providers.map(async (provider) => { try { progress.report(provider); const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token); if (providedCodeActions) { disposables.add(providedCodeActions); } if (cts.token.isCancellationRequested) { return emptyCodeActionsResponse; } const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action)); const documentation = getDocumentationFromProvider(provider, filteredActions, filter.include); return { actions: filteredActions.map(action => new CodeActionItem(action, provider)), documentation }; } catch (err) { if (isCancellationError(err)) { throw err; } onUnexpectedExternalError(err); return emptyCodeActionsResponse; } }); const listener = registry.onDidChange(() => { const newProviders = registry.all(model); if (!equals(newProviders, providers)) { cts.cancel(); } }); try { const actions = await Promise.all(promises); const allActions = actions.map(x => x.actions).flat(); const allDocumentation = [ ...coalesce(actions.map(x => x.documentation)), ...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions) ]; return new ManagedCodeActionSet(allActions, allDocumentation, disposables); } finally { listener.dispose(); cts.dispose(); } } function getCodeActionProviders(registry, model, filter) { return registry.all(model) // Don't include providers that we know will not return code actions of interest .filter(provider => { if (!provider.providedCodeActionKinds) { // We don't know what type of actions this provider will return. return true; } return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new HierarchicalKind(kind))); }); } function* getAdditionalDocumentationForShowingActions(registry, model, trigger, actionsToShow) { if (model && actionsToShow.length) { for (const provider of registry.all(model)) { if (provider._getAdditionalMenuItems) { yield* provider._getAdditionalMenuItems?.({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action)); } } } } function getDocumentationFromProvider(provider, providedCodeActions, only) { if (!provider.documentation) { return undefined; } const documentation = provider.documentation.map(entry => ({ kind: new HierarchicalKind(entry.kind), command: entry.command })); if (only) { let currentBest; for (const entry of documentation) { if (entry.kind.contains(only)) { if (!currentBest) { currentBest = entry; } else { // Take best match if (currentBest.kind.contains(entry.kind)) { currentBest = entry; } } } } if (currentBest) { return currentBest?.command; } } // Otherwise, check to see if any of the provided actions match. for (const action of providedCodeActions) { if (!action.kind) { continue; } for (const entry of documentation) { if (entry.kind.contains(new HierarchicalKind(action.kind))) { return entry.command; } } } return undefined; } export var ApplyCodeActionReason; (function (ApplyCodeActionReason) { ApplyCodeActionReason["OnSave"] = "onSave"; ApplyCodeActionReason["FromProblemsView"] = "fromProblemsView"; ApplyCodeActionReason["FromCodeActions"] = "fromCodeActions"; ApplyCodeActionReason["FromAILightbulb"] = "fromAILightbulb"; // direct invocation when clicking on the AI lightbulb })(ApplyCodeActionReason || (ApplyCodeActionReason = {})); export async function applyCodeAction(accessor, item, codeActionReason, options, token = CancellationToken.None) { const bulkEditService = accessor.get(IBulkEditService); const commandService = accessor.get(ICommandService); const telemetryService = accessor.get(ITelemetryService); const notificationService = accessor.get(INotificationService); telemetryService.publicLog2('codeAction.applyCodeAction', { codeActionTitle: item.action.title, codeActionKind: item.action.kind, codeActionIsPreferred: !!item.action.isPreferred, reason: codeActionReason, }); await item.resolve(token); if (token.isCancellationRequested) { return; } if (item.action.edit?.edits.length) { const result = await bulkEditService.apply(item.action.edit, { editor: options?.editor, label: item.action.title, quotableLabel: item.action.title, code: 'undoredo.codeAction', respectAutoSaveConfig: codeActionReason !== ApplyCodeActionReason.OnSave, showPreview: options?.preview, }); if (!result.isApplied) { return; } } if (item.action.command) { try { await commandService.executeCommand(item.action.command.id, ...(item.action.command.arguments || [])); } catch (err) { const message = asMessage(err); notificationService.error(typeof message === 'string' ? message : nls.localize('applyCodeActionFailed', "An unknown error occurred while applying the code action")); } } } function asMessage(err) { if (typeof err === 'string') { return err; } else if (err instanceof Error && typeof err.message === 'string') { return err.message; } else { return undefined; } } CommandsRegistry.registerCommand('_executeCodeActionProvider', async function (accessor, resource, rangeOrSelection, kind, itemResolveCount) { if (!(resource instanceof URI)) { throw illegalArgument(); } const { codeActionProvider } = accessor.get(ILanguageFeaturesService); const model = accessor.get(IModelService).getModel(resource); if (!model) { throw illegalArgument(); } const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection) ? Selection.liftSelection(rangeOrSelection) : Range.isIRange(rangeOrSelection) ? model.validateRange(rangeOrSelection) : undefined; if (!validatedRangeOrSelection) { throw illegalArgument(); } const include = typeof kind === 'string' ? new HierarchicalKind(kind) : undefined; const codeActionSet = await getCodeActions(codeActionProvider, model, validatedRangeOrSelection, { type: 1 /* languages.CodeActionTriggerType.Invoke */, triggerAction: CodeActionTriggerSource.Default, filter: { includeSourceActions: true, include } }, Progress.None, CancellationToken.None); const resolving = []; const resolveCount = Math.min(codeActionSet.validActions.length, typeof itemResolveCount === 'number' ? itemResolveCount : 0); for (let i = 0; i < resolveCount; i++) { resolving.push(codeActionSet.validActions[i].resolve(CancellationToken.None)); } try { await Promise.all(resolving); return codeActionSet.validActions.map(item => item.action); } finally { setTimeout(() => codeActionSet.dispose(), 100); } });