/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import * as dom from '../../../../base/browser/dom.js';
import { List } from '../../../../base/browser/ui/list/listWidget.js';
import { Action, Separator } from '../../../../base/common/actions.js';
import { canceled } from '../../../../base/common/errors.js';
import { Lazy } from '../../../../base/common/lazy.js';
import { Disposable, dispose, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import './media/action.css';
import { Position } from '../../../common/core/position.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { codeActionCommandId, CodeActionItem, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from './codeAction.js';
import { CodeActionCommandArgs, CodeActionKind, CodeActionTriggerSource } from './types.js';
import { localize } from '../../../../nls.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
export const Context = {
    Visible: new RawContextKey('CodeActionMenuVisible', false, localize('CodeActionMenuVisible', "Whether the code action list widget is visible"))
};
class CodeActionAction extends Action {
    constructor(action, callback) {
        super(action.command ? action.command.id : action.title, stripNewlines(action.title), undefined, !action.disabled, callback);
        this.action = action;
    }
}
function stripNewlines(str) {
    return str.replace(/\r\n|\r|\n/g, ' ');
}
const TEMPLATE_ID = 'codeActionWidget';
const codeActionLineHeight = 26;
let CodeMenuRenderer = class CodeMenuRenderer {
    constructor(acceptKeybindings, keybindingService) {
        this.acceptKeybindings = acceptKeybindings;
        this.keybindingService = keybindingService;
    }
    get templateId() { return TEMPLATE_ID; }
    renderTemplate(container) {
        const data = Object.create(null);
        data.disposables = [];
        data.root = container;
        data.text = document.createElement('span');
        // data.detail = document.createElement('');
        container.append(data.text);
        // container.append(data.detail);
        return data;
    }
    renderElement(element, index, templateData) {
        const data = templateData;
        const text = element.action.label;
        const isSeparator = element.isSeparator;
        element.isEnabled = element.action.enabled;
        if (element.action instanceof CodeActionAction) {
            // Check documentation type
            element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
            if (!element.isDocumentation) {
                // Check if action has disabled reason
                if (element.action.action.disabled) {
                    data.root.title = element.action.action.disabled;
                }
                else {
                    const updateLabel = () => {
                        var _a, _b;
                        const [accept, preview] = this.acceptKeybindings;
                        data.root.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Refactor, Shift+F2 to Preview"'] }, "{0} to Refactor, {1} to Preview", (_a = this.keybindingService.lookupKeybinding(accept)) === null || _a === void 0 ? void 0 : _a.getLabel(), (_b = this.keybindingService.lookupKeybinding(preview)) === null || _b === void 0 ? void 0 : _b.getLabel());
                    };
                    updateLabel();
                }
            }
        }
        data.text.textContent = text;
        if (!element.isEnabled) {
            data.root.classList.add('option-disabled');
            data.root.style.backgroundColor = 'transparent !important';
        }
        else {
            data.root.classList.remove('option-disabled');
        }
        if (isSeparator) {
            data.root.classList.add('separator');
            data.root.style.height = '10px';
        }
    }
    disposeTemplate(templateData) {
        templateData.disposables = dispose(templateData.disposables);
    }
};
CodeMenuRenderer = __decorate([
    __param(1, IKeybindingService)
], CodeMenuRenderer);
let CodeActionMenu = class CodeActionMenu extends Disposable {
    constructor(_editor, _delegate, _contextMenuService, keybindingService, _languageFeaturesService, _telemetryService, _themeService, _configurationService, _contextViewService, _contextKeyService) {
        super();
        this._editor = _editor;
        this._delegate = _delegate;
        this._contextMenuService = _contextMenuService;
        this._languageFeaturesService = _languageFeaturesService;
        this._telemetryService = _telemetryService;
        this._configurationService = _configurationService;
        this._contextViewService = _contextViewService;
        this._contextKeyService = _contextKeyService;
        this._showingActions = this._register(new MutableDisposable());
        this.codeActionList = this._register(new MutableDisposable());
        this.options = [];
        this._visible = false;
        this.viewItems = [];
        this.hasSeparator = false;
        this._keybindingResolver = new CodeActionKeybindingResolver({
            getKeybindings: () => keybindingService.getKeybindings()
        });
        this._ctxMenuWidgetVisible = Context.Visible.bindTo(this._contextKeyService);
        this.listRenderer = new CodeMenuRenderer([`onEnterSelectCodeAction`, `onEnterSelectCodeActionWithPreview`], keybindingService);
    }
    get isVisible() {
        return this._visible;
    }
    isCodeActionWidgetEnabled(model) {
        return this._configurationService.getValue('editor.experimental.useCustomCodeActionMenu', {
            resource: model.uri
        });
    }
    _onListSelection(e) {
        if (e.elements.length) {
            e.elements.forEach(element => {
                if (element.isEnabled) {
                    element.action.run();
                    this.hideCodeActionWidget();
                }
            });
        }
    }
    _onListHover(e) {
        var _a, _b, _c, _d;
        if (!e.element) {
            this.currSelectedItem = undefined;
            (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.setFocus([]);
        }
        else {
            if ((_b = e.element) === null || _b === void 0 ? void 0 : _b.isEnabled) {
                (_c = this.codeActionList.value) === null || _c === void 0 ? void 0 : _c.setFocus([e.element.index]);
                this.focusedEnabledItem = this.viewItems.indexOf(e.element);
                this.currSelectedItem = e.element.index;
            }
            else {
                this.currSelectedItem = undefined;
                (_d = this.codeActionList.value) === null || _d === void 0 ? void 0 : _d.setFocus([e.element.index]);
            }
        }
    }
    _onListClick(e) {
        var _a;
        if (e.element) {
            if (!e.element.isEnabled) {
                this.currSelectedItem = undefined;
                (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.setFocus([]);
            }
        }
    }
    renderCodeActionMenuList(element, inputArray) {
        var _a;
        const renderDisposables = new DisposableStore();
        const renderMenu = document.createElement('div');
        // Render invisible div to block mouse interaction in the rest of the UI
        const menuBlock = document.createElement('div');
        this.block = element.appendChild(menuBlock);
        this.block.classList.add('context-view-block');
        this.block.style.position = 'fixed';
        this.block.style.cursor = 'initial';
        this.block.style.left = '0';
        this.block.style.top = '0';
        this.block.style.width = '100%';
        this.block.style.height = '100%';
        this.block.style.zIndex = '-1';
        renderDisposables.add(dom.addDisposableListener(this.block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));
        renderMenu.id = 'codeActionMenuWidget';
        renderMenu.classList.add('codeActionMenuWidget');
        element.appendChild(renderMenu);
        this.codeActionList.value = new List('codeActionWidget', renderMenu, {
            getHeight(element) {
                if (element.isSeparator) {
                    return 10;
                }
                return codeActionLineHeight;
            },
            getTemplateId(element) {
                return 'codeActionWidget';
            }
        }, [this.listRenderer], { keyboardSupport: false });
        renderDisposables.add(this.codeActionList.value.onMouseClick(e => this._onListClick(e)));
        renderDisposables.add(this.codeActionList.value.onMouseOver(e => this._onListHover(e)));
        renderDisposables.add(this.codeActionList.value.onDidChangeFocus(e => { var _a; return (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.domFocus(); }));
        renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e)));
        renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget()));
        // Populating the list widget and tracking enabled options.
        inputArray.forEach((item, index) => {
            const currIsSeparator = item.class === 'separator';
            if (currIsSeparator) {
                // set to true forever because there is a separator
                this.hasSeparator = true;
            }
            const menuItem = { action: inputArray[index], isEnabled: item.enabled, isSeparator: currIsSeparator, index };
            if (item.enabled) {
                this.viewItems.push(menuItem);
            }
            this.options.push(menuItem);
        });
        this.codeActionList.value.splice(0, this.codeActionList.value.length, this.options);
        const height = this.hasSeparator ? (inputArray.length - 1) * codeActionLineHeight + 10 : inputArray.length * codeActionLineHeight;
        renderMenu.style.height = String(height) + 'px';
        this.codeActionList.value.layout(height);
        // For finding width dynamically (not using resize observer)
        const arr = [];
        this.options.forEach((item, index) => {
            var _a, _b;
            if (!this.codeActionList.value) {
                return;
            }
            const element = (_b = document.getElementById((_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.getElementID(index))) === null || _b === void 0 ? void 0 : _b.getElementsByTagName('span')[0].offsetWidth;
            arr.push(Number(element));
        });
        // resize observer - can be used in the future since list widget supports dynamic height but not width
        const maxWidth = Math.max(...arr);
        // 40 is the additional padding for the list widget (20 left, 20 right)
        renderMenu.style.width = maxWidth + 52 + 'px';
        (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.layout(height, maxWidth);
        // List selection
        if (this.viewItems.length < 1 || this.viewItems.every(item => item.isDocumentation)) {
            this.currSelectedItem = undefined;
        }
        else {
            this.focusedEnabledItem = 0;
            this.currSelectedItem = this.viewItems[0].index;
            this.codeActionList.value.setFocus([this.currSelectedItem]);
        }
        // List Focus
        this.codeActionList.value.domFocus();
        const focusTracker = dom.trackFocus(element);
        const blurListener = focusTracker.onDidBlur(() => {
            this.hideCodeActionWidget();
            // this._contextViewService.hideContextView({ source: this });
        });
        renderDisposables.add(blurListener);
        renderDisposables.add(focusTracker);
        this._ctxMenuWidgetVisible.set(true);
        return renderDisposables;
    }
    focusPrevious() {
        var _a;
        if (typeof this.focusedEnabledItem === 'undefined') {
            this.focusedEnabledItem = this.viewItems[0].index;
        }
        else if (this.viewItems.length < 1) {
            return false;
        }
        const startIndex = this.focusedEnabledItem;
        let item;
        do {
            this.focusedEnabledItem = this.focusedEnabledItem - 1;
            if (this.focusedEnabledItem < 0) {
                this.focusedEnabledItem = this.viewItems.length - 1;
            }
            item = this.viewItems[this.focusedEnabledItem];
            (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.setFocus([item.index]);
            this.currSelectedItem = item.index;
        } while (this.focusedEnabledItem !== startIndex && ((!item.isEnabled) || item.action.id === Separator.ID));
        return true;
    }
    focusNext() {
        var _a;
        if (typeof this.focusedEnabledItem === 'undefined') {
            this.focusedEnabledItem = this.viewItems.length - 1;
        }
        else if (this.viewItems.length < 1) {
            return false;
        }
        const startIndex = this.focusedEnabledItem;
        let item;
        do {
            this.focusedEnabledItem = (this.focusedEnabledItem + 1) % this.viewItems.length;
            item = this.viewItems[this.focusedEnabledItem];
            (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.setFocus([item.index]);
            this.currSelectedItem = item.index;
        } while (this.focusedEnabledItem !== startIndex && ((!item.isEnabled) || item.action.id === Separator.ID));
        return true;
    }
    navigateListWithKeysUp() {
        this.focusPrevious();
    }
    navigateListWithKeysDown() {
        this.focusNext();
    }
    onEnterSet() {
        var _a;
        if (typeof this.currSelectedItem === 'number') {
            (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.setSelection([this.currSelectedItem]);
        }
    }
    dispose() {
        super.dispose();
    }
    hideCodeActionWidget() {
        this._ctxMenuWidgetVisible.reset();
        this.options = [];
        this.viewItems = [];
        this.focusedEnabledItem = 0;
        this.currSelectedItem = undefined;
        this.hasSeparator = false;
        this._contextViewService.hideContextView({ source: this });
    }
    codeActionTelemetry(openedFromString, didCancel, CodeActions) {
        this._telemetryService.publicLog2('codeAction.applyCodeAction', {
            codeActionFrom: openedFromString,
            validCodeActions: CodeActions.validActions.length,
            cancelled: didCancel,
        });
    }
    show(trigger, codeActions, at, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const model = this._editor.getModel();
            if (!model) {
                return;
            }
            const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
            if (!actionsToShow.length) {
                this._visible = false;
                return;
            }
            if (!this._editor.getDomNode()) {
                // cancel when editor went off-dom
                this._visible = false;
                throw canceled();
            }
            this._visible = true;
            this._showingActions.value = codeActions;
            const menuActions = this.getMenuActions(trigger, actionsToShow, codeActions.documentation);
            const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 };
            const resolver = this._keybindingResolver.getResolver();
            const useShadowDOM = this._editor.getOption(117 /* EditorOption.useShadowDOM */);
            if (this.isCodeActionWidgetEnabled(model)) {
                this._contextViewService.showContextView({
                    getAnchor: () => anchor,
                    render: (container) => this.renderCodeActionMenuList(container, menuActions),
                    onHide: (didCancel) => {
                        const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction;
                        this.codeActionTelemetry(openedFromString, didCancel, codeActions);
                        this._visible = false;
                        this._editor.focus();
                    },
                }, this._editor.getDomNode(), false);
            }
            else {
                this._contextMenuService.showContextMenu({
                    domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined,
                    getAnchor: () => anchor,
                    getActions: () => menuActions,
                    onHide: (didCancel) => {
                        const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction;
                        this.codeActionTelemetry(openedFromString, didCancel, codeActions);
                        this._visible = false;
                        this._editor.focus();
                    },
                    autoSelectFirstItem: true,
                    getKeyBinding: action => action instanceof CodeActionAction ? resolver(action.action) : undefined,
                });
            }
        });
    }
    getMenuActions(trigger, actionsToShow, documentation) {
        var _a, _b;
        const toCodeActionAction = (item) => new CodeActionAction(item.action, () => this._delegate.onSelectCodeAction(item, trigger));
        const result = actionsToShow
            .map(toCodeActionAction);
        const allDocumentation = [...documentation];
        const model = this._editor.getModel();
        if (model && result.length) {
            for (const provider of this._languageFeaturesService.codeActionProvider.all(model)) {
                if (provider._getAdditionalMenuItems) {
                    allDocumentation.push(...provider._getAdditionalMenuItems({ trigger: trigger.type, only: (_b = (_a = trigger.filter) === null || _a === void 0 ? void 0 : _a.include) === null || _b === void 0 ? void 0 : _b.value }, actionsToShow.map(item => item.action)));
                }
            }
        }
        if (allDocumentation.length) {
            result.push(new Separator(), ...allDocumentation.map(command => toCodeActionAction(new CodeActionItem({
                title: command.title,
                command: command,
                kind: CodeActionMenu.documentationID
            }, undefined))));
        }
        return result;
    }
    _toCoords(position) {
        if (!this._editor.hasModel()) {
            return { x: 0, y: 0 };
        }
        this._editor.revealPosition(position, 1 /* ScrollType.Immediate */);
        this._editor.render();
        // Translate to absolute editor position
        const cursorCoords = this._editor.getScrolledVisiblePosition(position);
        const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());
        const x = editorCoords.left + cursorCoords.left;
        const y = editorCoords.top + cursorCoords.top + cursorCoords.height;
        return { x, y };
    }
};
CodeActionMenu.documentationID = '_documentation';
CodeActionMenu = __decorate([
    __param(2, IContextMenuService),
    __param(3, IKeybindingService),
    __param(4, ILanguageFeaturesService),
    __param(5, ITelemetryService),
    __param(6, IThemeService),
    __param(7, IConfigurationService),
    __param(8, IContextViewService),
    __param(9, IContextKeyService)
], CodeActionMenu);
export { CodeActionMenu };
export class CodeActionKeybindingResolver {
    constructor(_keybindingProvider) {
        this._keybindingProvider = _keybindingProvider;
    }
    getResolver() {
        // Lazy since we may not actually ever read the value
        const allCodeActionBindings = new Lazy(() => this._keybindingProvider.getKeybindings()
            .filter(item => CodeActionKeybindingResolver.codeActionCommands.indexOf(item.command) >= 0)
            .filter(item => item.resolvedKeybinding)
            .map((item) => {
            // Special case these commands since they come built-in with VS Code and don't use 'commandArgs'
            let commandArgs = item.commandArgs;
            if (item.command === organizeImportsCommandId) {
                commandArgs = { kind: CodeActionKind.SourceOrganizeImports.value };
            }
            else if (item.command === fixAllCommandId) {
                commandArgs = { kind: CodeActionKind.SourceFixAll.value };
            }
            return Object.assign({ resolvedKeybinding: item.resolvedKeybinding }, CodeActionCommandArgs.fromUser(commandArgs, {
                kind: CodeActionKind.None,
                apply: "never" /* CodeActionAutoApply.Never */
            }));
        }));
        return (action) => {
            if (action.kind) {
                const binding = this.bestKeybindingForCodeAction(action, allCodeActionBindings.getValue());
                return binding === null || binding === void 0 ? void 0 : binding.resolvedKeybinding;
            }
            return undefined;
        };
    }
    bestKeybindingForCodeAction(action, candidates) {
        if (!action.kind) {
            return undefined;
        }
        const kind = new CodeActionKind(action.kind);
        return candidates
            .filter(candidate => candidate.kind.contains(kind))
            .filter(candidate => {
            if (candidate.preferred) {
                // If the candidate keybinding only applies to preferred actions, the this action must also be preferred
                return action.isPreferred;
            }
            return true;
        })
            .reduceRight((currentBest, candidate) => {
            if (!currentBest) {
                return candidate;
            }
            // Select the more specific binding
            return currentBest.kind.contains(candidate.kind) ? candidate : currentBest;
        }, undefined);
    }
}
CodeActionKeybindingResolver.codeActionCommands = [
    refactorCommandId,
    codeActionCommandId,
    sourceActionCommandId,
    organizeImportsCommandId,
    fixAllCommandId
];
