Path: blob/main/src/vs/editor/contrib/codeAction/browser/codeActionController.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 { 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 { localize } from '../../../../nls.js';16import { IActionListDelegate } from '../../../../platform/actionWidget/browser/actionList.js';17import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';18import { ICommandService } from '../../../../platform/commands/common/commands.js';19import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';20import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { IMarkerService } from '../../../../platform/markers/common/markers.js';23import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';24import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from '../../../../platform/theme/common/colorRegistry.js';25import { isHighContrast } from '../../../../platform/theme/common/theme.js';26import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';27import { ICodeEditor } from '../../../browser/editorBrowser.js';28import { IPosition, Position } from '../../../common/core/position.js';29import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';30import { CodeActionTriggerType } from '../../../common/languages.js';31import { IModelDeltaDecoration } from '../../../common/model.js';32import { ModelDecorationOptions } from '../../../common/model/textModel.js';33import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';34import { MessageController } from '../../message/browser/messageController.js';35import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types.js';36import { ApplyCodeActionReason, applyCodeAction } from './codeAction.js';37import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver.js';38import { toMenuItems } from './codeActionMenu.js';39import { CodeActionModel, CodeActionsState } from './codeActionModel.js';40import { LightBulbWidget } from './lightBulbWidget.js';4142interface IActionShowOptions {43readonly includeDisabledActions?: boolean;44readonly fromLightbulb?: boolean;45}464748const DECORATION_CLASS_NAME = 'quickfix-edit-highlight';4950export class CodeActionController extends Disposable implements IEditorContribution {5152public static readonly ID = 'editor.contrib.codeActionController';5354public static get(editor: ICodeEditor): CodeActionController | null {55return editor.getContribution<CodeActionController>(CodeActionController.ID);56}5758private readonly _editor: ICodeEditor;59private readonly _model: CodeActionModel;6061private readonly _lightBulbWidget: Lazy<LightBulbWidget | null>;62private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>());63private _showDisabled = false;6465private readonly _resolver: CodeActionKeybindingResolver;6667private _disposed = false;6869constructor(70editor: ICodeEditor,71@IMarkerService markerService: IMarkerService,72@IContextKeyService contextKeyService: IContextKeyService,73@IInstantiationService instantiationService: IInstantiationService,74@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,75@IEditorProgressService progressService: IEditorProgressService,76@ICommandService private readonly _commandService: ICommandService,77@IConfigurationService private readonly _configurationService: IConfigurationService,78@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,79@IInstantiationService private readonly _instantiationService: IInstantiationService,80@IEditorProgressService private readonly _progressService: IEditorProgressService,81) {82super();8384this._editor = editor;85this._model = this._register(new CodeActionModel(this._editor, languageFeaturesService.codeActionProvider, markerService, contextKeyService, progressService, _configurationService));86this._register(this._model.onDidChangeState(newState => this.update(newState)));8788this._lightBulbWidget = new Lazy(() => {89const widget = this._editor.getContribution<LightBulbWidget>(LightBulbWidget.ID);90if (widget) {91this._register(widget.onClick(e => this.showCodeActionsFromLightbulb(e.actions, e)));92}93return widget;94});9596this._resolver = instantiationService.createInstance(CodeActionKeybindingResolver);9798this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide()));99}100101override dispose() {102this._disposed = true;103super.dispose();104}105106private async showCodeActionsFromLightbulb(actions: CodeActionSet, at: IAnchor | IPosition): Promise<void> {107if (actions.allAIFixes && actions.validActions.length === 1) {108const actionItem = actions.validActions[0];109const command = actionItem.action.command;110if (command && command.id === 'inlineChat.start') {111if (command.arguments && command.arguments.length >= 1) {112command.arguments[0] = { ...command.arguments[0], autoSend: false };113}114}115await this.applyCodeAction(actionItem, false, false, ApplyCodeActionReason.FromAILightbulb);116return;117}118await this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: true });119}120121public showCodeActions(_trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) {122return this.showCodeActionList(actions, at, { includeDisabledActions: false, fromLightbulb: false });123}124125public hideCodeActions(): void {126this._actionWidgetService.hide();127}128129public manualTriggerAtCurrentPosition(130notAvailableMessage: string,131triggerAction: CodeActionTriggerSource,132filter?: CodeActionFilter,133autoApply?: CodeActionAutoApply,134): void {135if (!this._editor.hasModel()) {136return;137}138139MessageController.get(this._editor)?.closeMessage();140const triggerPosition = this._editor.getPosition();141this._trigger({ type: CodeActionTriggerType.Invoke, triggerAction, filter, autoApply, context: { notAvailableMessage, position: triggerPosition } });142}143144private _trigger(trigger: CodeActionTrigger) {145return this._model.trigger(trigger);146}147148async applyCodeAction(action: CodeActionItem, retrigger: boolean, preview: boolean, actionReason: ApplyCodeActionReason): Promise<void> {149const progress = this._progressService.show(true, 500);150try {151await this._instantiationService.invokeFunction(applyCodeAction, action, actionReason, { preview, editor: this._editor });152} finally {153if (retrigger) {154this._trigger({ type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.QuickFix, filter: {} });155}156progress.done();157}158}159160public hideLightBulbWidget(): void {161this._lightBulbWidget.rawValue?.hide();162this._lightBulbWidget.rawValue?.gutterHide();163}164165private async update(newState: CodeActionsState.State): Promise<void> {166if (newState.type !== CodeActionsState.Type.Triggered) {167this.hideLightBulbWidget();168return;169}170171let actions: CodeActionSet;172try {173actions = await newState.actions;174} catch (e) {175onUnexpectedError(e);176return;177}178179if (this._disposed) {180return;181}182183184const selection = this._editor.getSelection();185if (selection?.startLineNumber !== newState.position.lineNumber) {186return;187}188189this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position);190191if (newState.trigger.type === CodeActionTriggerType.Invoke) {192if (newState.trigger.filter?.include) { // Triggered for specific scope193// Check to see if we want to auto apply.194195const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions);196if (validActionToApply) {197try {198this.hideLightBulbWidget();199await this.applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions);200} finally {201actions.dispose();202}203return;204}205206// Check to see if there is an action that we would have applied were it not invalid207if (newState.trigger.context) {208const invalidAction = this.getInvalidActionThatWouldHaveBeenApplied(newState.trigger, actions);209if (invalidAction && invalidAction.action.disabled) {210MessageController.get(this._editor)?.showMessage(invalidAction.action.disabled, newState.trigger.context.position);211actions.dispose();212return;213}214}215}216217const includeDisabledActions = !!newState.trigger.filter?.include;218if (newState.trigger.context) {219if (!actions.allActions.length || !includeDisabledActions && !actions.validActions.length) {220MessageController.get(this._editor)?.showMessage(newState.trigger.context.notAvailableMessage, newState.trigger.context.position);221this._activeCodeActions.value = actions;222actions.dispose();223return;224}225}226227this._activeCodeActions.value = actions;228this.showCodeActionList(actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false });229} else {230// auto magically triggered231if (this._actionWidgetService.isVisible) {232// TODO: Figure out if we should update the showing menu?233actions.dispose();234} else {235this._activeCodeActions.value = actions;236}237}238}239240private getInvalidActionThatWouldHaveBeenApplied(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {241if (!actions.allActions.length) {242return undefined;243}244245if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length === 0)246|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.allActions.length === 1)247) {248return actions.allActions.find(({ action }) => action.disabled);249}250251return undefined;252}253254private tryGetValidActionToApply(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {255if (!actions.validActions.length) {256return undefined;257}258259if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length > 0)260|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.validActions.length === 1)261) {262return actions.validActions[0];263}264265return undefined;266}267268private static readonly DECORATION = ModelDecorationOptions.register({269description: 'quickfix-highlight',270className: DECORATION_CLASS_NAME271});272273public async showCodeActionList(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): Promise<void> {274275const currentDecorations = this._editor.createDecorationsCollection();276277const editorDom = this._editor.getDomNode();278if (!editorDom) {279return;280}281282const actionsToShow = options.includeDisabledActions && (this._showDisabled || actions.validActions.length === 0) ? actions.allActions : actions.validActions;283if (!actionsToShow.length) {284return;285}286287const anchor = Position.isIPosition(at) ? this.toCoords(at) : at;288289const delegate: IActionListDelegate<CodeActionItem> = {290onSelect: async (action: CodeActionItem, preview?: boolean) => {291this.applyCodeAction(action, /* retrigger */ true, !!preview, options.fromLightbulb ? ApplyCodeActionReason.FromAILightbulb : ApplyCodeActionReason.FromCodeActions);292this._actionWidgetService.hide(false);293currentDecorations.clear();294},295onHide: (didCancel?) => {296this._editor?.focus();297currentDecorations.clear();298},299onHover: async (action: CodeActionItem, token: CancellationToken) => {300if (token.isCancellationRequested) {301return;302}303304let canPreview = false;305const actionKind = action.action.kind;306307if (actionKind) {308const hierarchicalKind = new HierarchicalKind(actionKind);309const refactorKinds = [310CodeActionKind.RefactorExtract,311CodeActionKind.RefactorInline,312CodeActionKind.RefactorRewrite,313CodeActionKind.RefactorMove,314CodeActionKind.Source315];316317canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind));318}319320return { canPreview: canPreview || !!action.action.edit?.edits.length };321},322onFocus: (action: CodeActionItem | undefined) => {323if (action && action.action) {324const ranges = action.action.ranges;325const diagnostics = action.action.diagnostics;326currentDecorations.clear();327if (ranges && ranges.length > 0) {328// Handles case for `fix all` where there are multiple diagnostics.329const decorations: IModelDeltaDecoration[] = (diagnostics && diagnostics?.length > 1)330? diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }))331: ranges.map(range => ({ range, options: CodeActionController.DECORATION }));332currentDecorations.set(decorations);333} else if (diagnostics && diagnostics.length > 0) {334const decorations: IModelDeltaDecoration[] = diagnostics.map(diagnostic => ({ range: diagnostic, options: CodeActionController.DECORATION }));335currentDecorations.set(decorations);336const diagnostic = diagnostics[0];337if (diagnostic.startLineNumber && diagnostic.startColumn) {338const selectionText = this._editor.getModel()?.getWordAtPosition({ lineNumber: diagnostic.startLineNumber, column: diagnostic.startColumn })?.word;339aria.status(localize('editingNewSelection', "Context: {0} at line {1} and column {2}.", selectionText, diagnostic.startLineNumber, diagnostic.startColumn));340}341}342} else {343currentDecorations.clear();344}345}346};347348this._actionWidgetService.show(349'codeActionWidget',350true,351toMenuItems(actionsToShow, this._shouldShowHeaders(), this._resolver.getResolver()),352delegate,353anchor,354editorDom,355this._getActionBarActions(actions, at, options));356}357358private toCoords(position: IPosition): IAnchor {359if (!this._editor.hasModel()) {360return { x: 0, y: 0 };361}362363this._editor.revealPosition(position, ScrollType.Immediate);364this._editor.render();365366// Translate to absolute editor position367const cursorCoords = this._editor.getScrolledVisiblePosition(position);368const editorCoords = getDomNodePagePosition(this._editor.getDomNode());369const x = editorCoords.left + cursorCoords.left;370const y = editorCoords.top + cursorCoords.top + cursorCoords.height;371372return { x, y };373}374375private _shouldShowHeaders(): boolean {376const model = this._editor?.getModel();377return this._configurationService.getValue('editor.codeActionWidget.showHeaders', { resource: model?.uri });378}379380private _getActionBarActions(actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): IAction[] {381if (options.fromLightbulb) {382return [];383}384385const resultActions = actions.documentation.map((command): IAction => ({386id: command.id,387label: command.title,388tooltip: command.tooltip ?? '',389class: undefined,390enabled: true,391run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])),392}));393394if (options.includeDisabledActions && actions.validActions.length > 0 && actions.allActions.length !== actions.validActions.length) {395resultActions.push(this._showDisabled ? {396id: 'hideMoreActions',397label: localize('hideMoreActions', 'Hide Disabled'),398enabled: true,399tooltip: '',400class: undefined,401run: () => {402this._showDisabled = false;403return this.showCodeActionList(actions, at, options);404}405} : {406id: 'showMoreActions',407label: localize('showMoreActions', 'Show Disabled'),408enabled: true,409tooltip: '',410class: undefined,411run: () => {412this._showDisabled = true;413return this.showCodeActionList(actions, at, options);414}415});416}417418return resultActions;419}420}421422registerThemingParticipant((theme, collector) => {423const addBackgroundColorRule = (selector: string, color: Color | undefined): void => {424if (color) {425collector.addRule(`.monaco-editor ${selector} { background-color: ${color}; }`);426}427};428429addBackgroundColorRule('.quickfix-edit-highlight', theme.getColor(editorFindMatchHighlight));430const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);431432if (findMatchHighlightBorder) {433collector.addRule(`.monaco-editor .quickfix-edit-highlight { border: 1px ${isHighContrast(theme.type) ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`);434}435});436437438