Path: blob/main/src/vs/editor/contrib/codeAction/browser/codeActionController.ts
5236 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 { getDomNodePagePosition } from '../../../../base/browser/dom.js';6import * as aria from '../../../../base/browser/ui/aria/aria.js';7import { IAnchor } from '../../../../base/browser/ui/contextview/contextview.js';8import { IAction } from '../../../../base/common/actions.js';9import { CancellationToken } from '../../../../base/common/cancellation.js';10import { Color } from '../../../../base/common/color.js';11import { onUnexpectedError } from '../../../../base/common/errors.js';12import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';13import { Lazy } from '../../../../base/common/lazy.js';14import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';15import { derived, IObservable } from '../../../../base/common/observable.js';16import { localize } from '../../../../nls.js';17import { IActionListDelegate } from '../../../../platform/actionWidget/browser/actionList.js';18import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';19import { ICommandService } from '../../../../platform/commands/common/commands.js';20import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';21import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';22import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';23import { IMarkerService } from '../../../../platform/markers/common/markers.js';24import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';25import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from '../../../../platform/theme/common/colorRegistry.js';26import { isHighContrast } from '../../../../platform/theme/common/theme.js';27import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';28import { ICodeEditor } from '../../../browser/editorBrowser.js';29import { IPosition, Position } from '../../../common/core/position.js';30import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';31import { CodeActionTriggerType } from '../../../common/languages.js';32import { IModelDeltaDecoration } from '../../../common/model.js';33import { ModelDecorationOptions } from '../../../common/model/textModel.js';34import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';35import { MessageController } from '../../message/browser/messageController.js';36import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types.js';37import { ApplyCodeActionReason, applyCodeAction } from './codeAction.js';38import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver.js';39import { toMenuItems } from './codeActionMenu.js';40import { CodeActionModel, CodeActionsState } from './codeActionModel.js';41import { LightBulbInfo, LightBulbWidget } from './lightBulbWidget.js';4243interface IActionShowOptions {44readonly includeDisabledActions?: boolean;45readonly fromLightbulb?: boolean;46}474849const DECORATION_CLASS_NAME = 'quickfix-edit-highlight';5051export class CodeActionController extends Disposable implements IEditorContribution {5253public static readonly ID = 'editor.contrib.codeActionController';5455public static get(editor: ICodeEditor): CodeActionController | null {56return editor.getContribution<CodeActionController>(CodeActionController.ID);57}5859private readonly _editor: ICodeEditor;60private readonly _model: CodeActionModel;6162private readonly _lightBulbWidget: Lazy<LightBulbWidget | null>;63private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>());64private _showDisabled = false;6566private readonly _resolver: CodeActionKeybindingResolver;6768private _disposed = false;6970public readonly lightBulbState: IObservable<LightBulbInfo | undefined> = derived(this, reader => {71const widget = this._lightBulbWidget.rawValue;72if (!widget) {73return undefined;74}75return widget.lightBulbInfo.read(reader);76});7778constructor(79editor: ICodeEditor,80@IMarkerService markerService: IMarkerService,81@IContextKeyService contextKeyService: IContextKeyService,82@IInstantiationService instantiationService: IInstantiationService,83@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,84@IEditorProgressService progressService: IEditorProgressService,85@ICommandService private readonly _commandService: ICommandService,86@IConfigurationService private readonly _configurationService: IConfigurationService,87@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,88@IInstantiationService private readonly _instantiationService: IInstantiationService,89@IEditorProgressService private readonly _progressService: IEditorProgressService,90) {91super();9293this._editor = editor;94this._model = this._register(new CodeActionModel(this._editor, languageFeaturesService.codeActionProvider, markerService, contextKeyService, progressService, _configurationService));95this._register(this._model.onDidChangeState(newState => this.update(newState)));9697this._lightBulbWidget = new Lazy(() => {98const widget = this._editor.getContribution<LightBulbWidget>(LightBulbWidget.ID);99if (widget) {100this._register(widget.onClick(e => this.showCodeActionsFromLightbulb(e.actions, e)));101}102return widget;103});104105this._resolver = instantiationService.createInstance(CodeActionKeybindingResolver);106107this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide()));108}109110override dispose() {111this._disposed = true;112super.dispose();113}114115private async showCodeActionsFromLightbulb(actions: CodeActionSet, at: IAnchor | IPosition): Promise<void> {116if (actions.allAIFixes && actions.validActions.length === 1) {117const actionItem = actions.validActions[0];118const command = actionItem.action.command;119if (command && command.id === 'inlineChat.start') {120if (command.arguments && command.arguments.length >= 1 && command.arguments[0]) {121command.arguments[0] = { ...command.arguments[0], autoSend: false };122}123}124await this.applyCodeAction(actionItem, false, false, ApplyCodeActionReason.FromAILightbulb);125return;126}127await this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: true });128}129130public showCodeActions(_trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) {131return this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: false });132}133134public hideCodeActions(): void {135this._actionWidgetService.hide();136}137138public manualTriggerAtCurrentPosition(139notAvailableMessage: string,140triggerAction: CodeActionTriggerSource,141filter?: CodeActionFilter,142autoApply?: CodeActionAutoApply,143): void {144if (!this._editor.hasModel()) {145return;146}147148MessageController.get(this._editor)?.closeMessage();149const triggerPosition = this._editor.getPosition();150this._trigger({ type: CodeActionTriggerType.Invoke, triggerAction, filter, autoApply, context: { notAvailableMessage, position: triggerPosition } });151}152153private _trigger(trigger: CodeActionTrigger) {154return this._model.trigger(trigger);155}156157async applyCodeAction(action: CodeActionItem, retrigger: boolean, preview: boolean, actionReason: ApplyCodeActionReason): Promise<void> {158const progress = this._progressService.show(true, 500);159try {160await this._instantiationService.invokeFunction(applyCodeAction, action, actionReason, { preview, editor: this._editor });161} finally {162if (retrigger) {163this._trigger({ type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.QuickFix, filter: {} });164}165progress.done();166}167}168169public hideLightBulbWidget(): void {170this._lightBulbWidget.rawValue?.hide();171this._lightBulbWidget.rawValue?.gutterHide();172}173174private async update(newState: CodeActionsState.State): Promise<void> {175if (newState.type !== CodeActionsState.Type.Triggered) {176this.hideLightBulbWidget();177return;178}179180let actions: CodeActionSet;181try {182actions = await newState.actions;183} catch (e) {184onUnexpectedError(e);185return;186}187188if (this._disposed) {189return;190}191192193const selection = this._editor.getSelection();194if (selection?.startLineNumber !== newState.position.lineNumber) {195return;196}197198this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position);199200if (newState.trigger.type === CodeActionTriggerType.Invoke) {201if (newState.trigger.filter?.include) { // Triggered for specific scope202// Check to see if we want to auto apply.203204const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions);205if (validActionToApply) {206try {207this.hideLightBulbWidget();208await this.applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions);209} finally {210actions.dispose();211}212return;213}214215// Check to see if there is an action that we would have applied were it not invalid216if (newState.trigger.context) {217const invalidAction = this.getInvalidActionThatWouldHaveBeenApplied(newState.trigger, actions);218if (invalidAction && invalidAction.action.disabled) {219MessageController.get(this._editor)?.showMessage(invalidAction.action.disabled, newState.trigger.context.position);220actions.dispose();221return;222}223}224}225226const includeDisabledActions = !!newState.trigger.filter?.include;227if (newState.trigger.context) {228if (!actions.allActions.length || !includeDisabledActions && !actions.validActions.length) {229MessageController.get(this._editor)?.showMessage(newState.trigger.context.notAvailableMessage, newState.trigger.context.position);230this._activeCodeActions.value = actions;231actions.dispose();232return;233}234}235236this._activeCodeActions.value = actions;237this.showCodeActionList(actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false });238} else {239// auto magically triggered240if (this._actionWidgetService.isVisible) {241// TODO: Figure out if we should update the showing menu?242actions.dispose();243} else {244this._activeCodeActions.value = actions;245}246}247}248249private getInvalidActionThatWouldHaveBeenApplied(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {250if (!actions.allActions.length) {251return undefined;252}253254if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length === 0)255|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.allActions.length === 1)256) {257return actions.allActions.find(({ action }) => action.disabled);258}259260return undefined;261}262263private tryGetValidActionToApply(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {264if (!actions.validActions.length) {265return undefined;266}267268if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length > 0)269|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.validActions.length === 1)270) {271return actions.validActions[0];272}273274return undefined;275}276277private static readonly DECORATION = ModelDecorationOptions.register({278description: 'quickfix-highlight',279className: DECORATION_CLASS_NAME280});281282public async showCodeActionList(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): Promise<void> {283284const currentDecorations = this._editor.createDecorationsCollection();285286const editorDom = this._editor.getDomNode();287if (!editorDom) {288return;289}290291const actionsToShow = options.includeDisabledActions && (this._showDisabled || actions.validActions.length === 0) ? actions.allActions : actions.validActions;292if (!actionsToShow.length) {293return;294}295296const anchor = Position.isIPosition(at) ? this.toCoords(at) : at;297298const delegate: IActionListDelegate<CodeActionItem> = {299onSelect: async (action: CodeActionItem, preview?: boolean) => {300this.applyCodeAction(action, /* retrigger */ true, !!preview, options.fromLightbulb ? ApplyCodeActionReason.FromAILightbulb : ApplyCodeActionReason.FromCodeActions);301this._actionWidgetService.hide(false);302currentDecorations.clear();303},304onHide: (didCancel?) => {305this._editor?.focus();306currentDecorations.clear();307},308onHover: async (action: CodeActionItem, token: CancellationToken) => {309if (token.isCancellationRequested) {310return;311}312313let canPreview = false;314const actionKind = action.action.kind;315316if (actionKind) {317const hierarchicalKind = new HierarchicalKind(actionKind);318const refactorKinds = [319CodeActionKind.RefactorExtract,320CodeActionKind.RefactorInline,321CodeActionKind.RefactorRewrite,322CodeActionKind.RefactorMove,323CodeActionKind.Source324];325326canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind));327}328329return { canPreview: canPreview || !!action.action.edit?.edits.length };330},331onFocus: (action: CodeActionItem | undefined) => {332if (action && action.action) {333const ranges = action.action.ranges;334const diagnostics = action.action.diagnostics;335currentDecorations.clear();336if (ranges && ranges.length > 0) {337// Handles case for `fix all` where there are multiple diagnostics.338const decorations: IModelDeltaDecoration[] = (diagnostics && diagnostics?.length > 1)339? diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }))340: ranges.map(range => ({ range, options: CodeActionController.DECORATION }));341currentDecorations.set(decorations);342} else if (diagnostics && diagnostics.length > 0) {343const decorations: IModelDeltaDecoration[] = diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }));344currentDecorations.set(decorations);345const diagnostic = diagnostics[0];346if (diagnostic.startLineNumber && diagnostic.startColumn) {347const selectionText = this._editor.getModel()?.getWordAtPosition({ lineNumber: diagnostic.startLineNumber, column: diagnostic.startColumn })?.word;348aria.status(localize('editingNewSelection', "Context: {0} at line {1} and column {2}.", selectionText, diagnostic.startLineNumber, diagnostic.startColumn));349}350}351} else {352currentDecorations.clear();353}354}355};356357this._actionWidgetService.show(358'codeActionWidget',359true,360toMenuItems(actionsToShow, this._shouldShowHeaders(), this._resolver.getResolver()),361delegate,362anchor,363editorDom,364this._getActionBarActions(actions, at, options));365}366367private toCoords(position: IPosition): IAnchor {368if (!this._editor.hasModel()) {369return { x: 0, y: 0 };370}371372this._editor.revealPosition(position, ScrollType.Immediate);373this._editor.render();374375// Translate to absolute editor position376const cursorCoords = this._editor.getScrolledVisiblePosition(position);377const editorCoords = getDomNodePagePosition(this._editor.getDomNode());378const x = editorCoords.left + cursorCoords.left;379const y = editorCoords.top + cursorCoords.top + cursorCoords.height;380381return { x, y };382}383384private _shouldShowHeaders(): boolean {385const model = this._editor?.getModel();386return this._configurationService.getValue('editor.codeActionWidget.showHeaders', { resource: model?.uri });387}388389private _getActionBarActions(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): IAction[] {390if (options.fromLightbulb) {391return [];392}393394const resultActions = actions.documentation.map((command): IAction => ({395id: command.id,396label: command.title,397tooltip: command.tooltip ?? '',398class: undefined,399enabled: true,400run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])),401}));402403if (options.includeDisabledActions && actions.validActions.length > 0 && actions.allActions.length !== actions.validActions.length) {404resultActions.push(this._showDisabled ? {405id: 'hideMoreActions',406label: localize('hideMoreActions', 'Hide Disabled'),407enabled: true,408tooltip: '',409class: undefined,410run: () => {411this._showDisabled = false;412return this.showCodeActionList(actions, at, options);413}414} : {415id: 'showMoreActions',416label: localize('showMoreActions', 'Show Disabled'),417enabled: true,418tooltip: '',419class: undefined,420run: () => {421this._showDisabled = true;422return this.showCodeActionList(actions, at, options);423}424});425}426427return resultActions;428}429}430431registerThemingParticipant((theme, collector) => {432const addBackgroundColorRule = (selector: string, color: Color | undefined): void => {433if (color) {434collector.addRule(`.monaco-editor ${selector} { background-color: ${color}; }`);435}436};437438addBackgroundColorRule('.quickfix-edit-highlight', theme.getColor(editorFindMatchHighlight));439const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);440441if (findMatchHighlightBorder) {442collector.addRule(`.monaco-editor .quickfix-edit-highlight { border: 1px ${isHighContrast(theme.type) ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`);443}444});445446447