/*---------------------------------------------------------------------------------------------
 *  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 { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import '../../../../base/browser/ui/codicons/codiconStyles.js'; // The codicon symbol styles are defined here and must be loaded
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
import { List } from '../../../../base/browser/ui/list/listWidget.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { OS } from '../../../../base/common/platform.js';
import './codeActionWidget.css';
import { CodeActionKind, CodeActionTriggerSource } from './types.js';
import '../../symbolIcons/browser/symbolIcons.js'; // The codicon symbol colors are defined here and must be loaded to get colors
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver.js';
export const Context = {
    Visible: new RawContextKey('codeActionMenuVisible', false, localize('codeActionMenuVisible', "Whether the code action list widget is visible"))
};
export const acceptSelectedCodeActionCommand = 'acceptSelectedCodeAction';
export const previewSelectedCodeActionCommand = 'previewSelectedCodeAction';
var CodeActionListItemKind;
(function (CodeActionListItemKind) {
    CodeActionListItemKind["CodeAction"] = "action";
    CodeActionListItemKind["Header"] = "header";
})(CodeActionListItemKind || (CodeActionListItemKind = {}));
function stripNewlines(str) {
    return str.replace(/\r\n|\r|\n/g, ' ');
}
const uncategorizedCodeActionGroup = Object.freeze({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') });
const codeActionGroups = Object.freeze([
    { kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') },
    { kind: CodeActionKind.RefactorExtract, title: localize('codeAction.widget.id.extract', 'Extract...'), icon: { codicon: Codicon.wrench } },
    { kind: CodeActionKind.RefactorInline, title: localize('codeAction.widget.id.inline', 'Inline...'), icon: { codicon: Codicon.wrench } },
    { kind: CodeActionKind.RefactorRewrite, title: localize('codeAction.widget.id.convert', 'Rewrite...'), icon: { codicon: Codicon.wrench } },
    { kind: CodeActionKind.RefactorMove, title: localize('codeAction.widget.id.move', 'Move...'), icon: { codicon: Codicon.wrench } },
    { kind: CodeActionKind.SurroundWith, title: localize('codeAction.widget.id.surround', 'Surround With...'), icon: { codicon: Codicon.symbolSnippet } },
    { kind: CodeActionKind.Source, title: localize('codeAction.widget.id.source', 'Source Action...'), icon: { codicon: Codicon.symbolFile } },
    uncategorizedCodeActionGroup,
]);
let CodeActionItemRenderer = class CodeActionItemRenderer {
    constructor(keybindingResolver, keybindingService) {
        this.keybindingResolver = keybindingResolver;
        this.keybindingService = keybindingService;
    }
    get templateId() { return CodeActionListItemKind.CodeAction; }
    renderTemplate(container) {
        container.classList.add('code-action');
        const icon = document.createElement('div');
        icon.className = 'icon';
        container.append(icon);
        const text = document.createElement('span');
        text.className = 'title';
        container.append(text);
        const keybinding = new KeybindingLabel(container, OS);
        return { container, icon, text, keybinding };
    }
    renderElement(element, _index, data) {
        var _a, _b, _c;
        if (element.group.icon) {
            data.icon.className = element.group.icon.codicon.classNames;
            data.icon.style.color = (_a = element.group.icon.color) !== null && _a !== void 0 ? _a : '';
        }
        else {
            data.icon.className = Codicon.lightBulb.classNames;
            data.icon.style.color = 'var(--vscode-editorLightBulb-foreground)';
        }
        data.text.textContent = stripNewlines(element.action.action.title);
        const binding = this.keybindingResolver.getResolver()(element.action.action);
        data.keybinding.set(binding);
        if (!binding) {
            dom.hide(data.keybinding.element);
        }
        else {
            dom.show(data.keybinding.element);
        }
        if (element.action.action.disabled) {
            data.container.title = element.action.action.disabled;
            data.container.classList.add('option-disabled');
        }
        else {
            data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", (_b = this.keybindingService.lookupKeybinding(acceptSelectedCodeActionCommand)) === null || _b === void 0 ? void 0 : _b.getLabel(), (_c = this.keybindingService.lookupKeybinding(previewSelectedCodeActionCommand)) === null || _c === void 0 ? void 0 : _c.getLabel());
            data.container.classList.remove('option-disabled');
        }
    }
    disposeTemplate(_templateData) {
        // noop
    }
};
CodeActionItemRenderer = __decorate([
    __param(1, IKeybindingService)
], CodeActionItemRenderer);
class HeaderRenderer {
    get templateId() { return CodeActionListItemKind.Header; }
    renderTemplate(container) {
        container.classList.add('group-header');
        const text = document.createElement('span');
        container.append(text);
        return { container, text };
    }
    renderElement(element, _index, templateData) {
        templateData.text.textContent = element.group.title;
    }
    disposeTemplate(_templateData) {
        // noop
    }
}
const previewSelectedEventType = 'previewSelectedCodeAction';
let CodeActionList = class CodeActionList extends Disposable {
    constructor(codeActions, showHeaders, onDidSelect, keybindingService) {
        super();
        this.onDidSelect = onDidSelect;
        this.codeActionLineHeight = 24;
        this.headerLineHeight = 26;
        this.domNode = document.createElement('div');
        this.domNode.classList.add('codeActionList');
        this.list = this._register(new List('codeActionWidget', this.domNode, {
            getHeight: element => element.kind === CodeActionListItemKind.Header ? this.headerLineHeight : this.codeActionLineHeight,
            getTemplateId: element => element.kind,
        }, [
            new CodeActionItemRenderer(new CodeActionKeybindingResolver(keybindingService), keybindingService),
            new HeaderRenderer(),
        ], {
            keyboardSupport: false,
            accessibilityProvider: {
                getAriaLabel: element => {
                    if (element.kind === CodeActionListItemKind.CodeAction) {
                        let label = stripNewlines(element.action.action.title);
                        if (element.action.action.disabled) {
                            label = localize({ key: 'customCodeActionWidget.labels', comment: ['Code action labels for accessibility.'] }, "{0}, Disabled Reason: {1}", label, element.action.action.disabled);
                        }
                        return label;
                    }
                    return null;
                },
                getWidgetAriaLabel: () => localize({ key: 'customCodeActionWidget', comment: ['A Code Action Option'] }, "Code Action Widget"),
                getRole: () => 'option',
                getWidgetRole: () => 'code-action-widget'
            }
        }));
        this._register(this.list.onMouseClick(e => this.onListClick(e)));
        this._register(this.list.onMouseOver(e => this.onListHover(e)));
        this._register(this.list.onDidChangeFocus(() => this.list.domFocus()));
        this._register(this.list.onDidChangeSelection(e => this.onListSelection(e)));
        this.allMenuItems = this.toMenuItems(codeActions, showHeaders);
        this.list.splice(0, this.list.length, this.allMenuItems);
        this.focusNext();
    }
    layout(minWidth) {
        // Updating list height, depending on how many separators and headers there are.
        const numHeaders = this.allMenuItems.filter(item => item.kind === CodeActionListItemKind.Header).length;
        const height = this.allMenuItems.length * this.codeActionLineHeight;
        const heightWithHeaders = height + numHeaders * this.headerLineHeight - numHeaders * this.codeActionLineHeight;
        this.list.layout(heightWithHeaders);
        // For finding width dynamically (not using resize observer)
        const itemWidths = this.allMenuItems.map((_, index) => {
            const element = document.getElementById(this.list.getElementID(index));
            if (element) {
                element.style.width = 'auto';
                const width = element.getBoundingClientRect().width;
                element.style.width = '';
                return width;
            }
            return 0;
        });
        // resize observer - can be used in the future since list widget supports dynamic height but not width
        const width = Math.max(...itemWidths, minWidth);
        this.list.layout(heightWithHeaders, width);
        this.domNode.style.height = `${heightWithHeaders}px`;
        this.list.domFocus();
        return width;
    }
    focusPrevious() {
        this.list.focusPrevious(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled);
    }
    focusNext() {
        this.list.focusNext(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled);
    }
    acceptSelected(options) {
        const focused = this.list.getFocus();
        if (focused.length === 0) {
            return;
        }
        const focusIndex = focused[0];
        const element = this.list.element(focusIndex);
        if (element.kind !== CodeActionListItemKind.CodeAction || element.action.action.disabled) {
            return;
        }
        const event = new UIEvent((options === null || options === void 0 ? void 0 : options.preview) ? previewSelectedEventType : 'acceptSelectedCodeAction');
        this.list.setSelection([focusIndex], event);
    }
    onListSelection(e) {
        var _a;
        if (!e.elements.length) {
            return;
        }
        const element = e.elements[0];
        if (element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled) {
            this.onDidSelect(element.action, { preview: ((_a = e.browserEvent) === null || _a === void 0 ? void 0 : _a.type) === previewSelectedEventType });
        }
        else {
            this.list.setSelection([]);
        }
    }
    onListHover(e) {
        this.list.setFocus(typeof e.index === 'number' ? [e.index] : []);
    }
    onListClick(e) {
        if (e.element && e.element.kind === CodeActionListItemKind.CodeAction && e.element.action.action.disabled) {
            this.list.setFocus([]);
        }
    }
    toMenuItems(inputCodeActions, showHeaders) {
        if (!showHeaders) {
            return inputCodeActions.map((action) => ({ kind: CodeActionListItemKind.CodeAction, action, group: uncategorizedCodeActionGroup }));
        }
        // Group code actions
        const menuEntries = codeActionGroups.map(group => ({ group, actions: [] }));
        for (const action of inputCodeActions) {
            const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None;
            for (const menuEntry of menuEntries) {
                if (menuEntry.group.kind.contains(kind)) {
                    menuEntry.actions.push(action);
                    break;
                }
            }
        }
        const allMenuItems = [];
        for (const menuEntry of menuEntries) {
            if (menuEntry.actions.length) {
                allMenuItems.push({ kind: CodeActionListItemKind.Header, group: menuEntry.group });
                for (const action of menuEntry.actions) {
                    allMenuItems.push({ kind: CodeActionListItemKind.CodeAction, action, group: menuEntry.group });
                }
            }
        }
        return allMenuItems;
    }
};
CodeActionList = __decorate([
    __param(3, IKeybindingService)
], CodeActionList);
// TODO: Take a look at user storage for this so it is preserved across windows and on reload.
let showDisabled = false;
let CodeActionWidget = class CodeActionWidget extends Disposable {
    static get INSTANCE() { return this._instance; }
    static getOrCreateInstance(instantiationService) {
        if (!this._instance) {
            this._instance = instantiationService.createInstance(CodeActionWidget);
        }
        return this._instance;
    }
    constructor(_commandService, _contextViewService, _keybindingService, _telemetryService) {
        super();
        this._commandService = _commandService;
        this._contextViewService = _contextViewService;
        this._keybindingService = _keybindingService;
        this._telemetryService = _telemetryService;
        this.codeActionList = this._register(new MutableDisposable());
    }
    get isVisible() {
        return !!this.currentShowingContext;
    }
    show(trigger, codeActions, anchor, container, options, delegate, contextKeyService) {
        return __awaiter(this, void 0, void 0, function* () {
            this.currentShowingContext = undefined;
            const visibleContext = Context.Visible.bindTo(contextKeyService);
            const actionsToShow = options.includeDisabledActions && (showDisabled || codeActions.validActions.length === 0) ? codeActions.allActions : codeActions.validActions;
            if (!actionsToShow.length) {
                visibleContext.reset();
                return;
            }
            this.currentShowingContext = { trigger, codeActions, anchor, container, delegate, options, contextKeyService };
            this._contextViewService.showContextView({
                getAnchor: () => anchor,
                render: (container) => {
                    visibleContext.set(true);
                    return this.renderWidget(container, trigger, codeActions, options, actionsToShow, delegate);
                },
                onHide: (didCancel) => {
                    visibleContext.reset();
                    return this.onWidgetClosed(trigger, options, codeActions, didCancel, delegate);
                },
            }, container, false);
        });
    }
    focusPrevious() {
        var _a;
        (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.focusPrevious();
    }
    focusNext() {
        var _a;
        (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.focusNext();
    }
    acceptSelected(options) {
        var _a;
        (_a = this.codeActionList.value) === null || _a === void 0 ? void 0 : _a.acceptSelected(options);
    }
    hide() {
        this.codeActionList.clear();
        this._contextViewService.hideContextView();
    }
    renderWidget(element, trigger, codeActions, options, showingCodeActions, delegate) {
        var _a;
        const renderDisposables = new DisposableStore();
        const widget = document.createElement('div');
        widget.classList.add('codeActionWidget');
        element.appendChild(widget);
        this.codeActionList.value = new CodeActionList(showingCodeActions, (_a = options.showHeaders) !== null && _a !== void 0 ? _a : true, (action, options) => {
            this.hide();
            delegate.onSelectCodeAction(action, trigger, options);
        }, this._keybindingService);
        widget.appendChild(this.codeActionList.value.domNode);
        // Invisible div to block mouse interaction in the rest of the UI
        const menuBlock = document.createElement('div');
        const block = element.appendChild(menuBlock);
        block.classList.add('context-view-block');
        block.style.position = 'fixed';
        block.style.cursor = 'initial';
        block.style.left = '0';
        block.style.top = '0';
        block.style.width = '100%';
        block.style.height = '100%';
        block.style.zIndex = '-1';
        renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));
        // Invisible div to block mouse interaction with the menu
        const pointerBlockDiv = document.createElement('div');
        const pointerBlock = element.appendChild(pointerBlockDiv);
        pointerBlock.classList.add('context-view-pointerBlock');
        pointerBlock.style.position = 'fixed';
        pointerBlock.style.cursor = 'initial';
        pointerBlock.style.left = '0';
        pointerBlock.style.top = '0';
        pointerBlock.style.width = '100%';
        pointerBlock.style.height = '100%';
        pointerBlock.style.zIndex = '2';
        // Removes block on click INSIDE widget or ANY mouse movement
        renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.POINTER_MOVE, () => pointerBlock.remove()));
        renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.MOUSE_DOWN, () => pointerBlock.remove()));
        // Action bar
        let actionBarWidth = 0;
        if (!options.fromLightbulb) {
            const actionBar = this.createActionBar(codeActions, options);
            if (actionBar) {
                widget.appendChild(actionBar.getContainer().parentElement);
                renderDisposables.add(actionBar);
                actionBarWidth = actionBar.getContainer().offsetWidth;
            }
        }
        const width = this.codeActionList.value.layout(actionBarWidth);
        widget.style.width = `${width}px`;
        const focusTracker = renderDisposables.add(dom.trackFocus(element));
        renderDisposables.add(focusTracker.onDidBlur(() => this.hide()));
        return renderDisposables;
    }
    /**
     * Toggles whether the disabled actions in the code action widget are visible or not.
     */
    toggleShowDisabled(newShowDisabled) {
        const previousCtx = this.currentShowingContext;
        this.hide();
        showDisabled = newShowDisabled;
        if (previousCtx) {
            this.show(previousCtx.trigger, previousCtx.codeActions, previousCtx.anchor, previousCtx.container, previousCtx.options, previousCtx.delegate, previousCtx.contextKeyService);
        }
    }
    onWidgetClosed(trigger, options, codeActions, cancelled, delegate) {
        this._telemetryService.publicLog2('codeAction.applyCodeAction', {
            codeActionFrom: options.fromLightbulb ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction,
            validCodeActions: codeActions.validActions.length,
            cancelled: cancelled,
        });
        this.currentShowingContext = undefined;
        delegate.onHide(cancelled);
    }
    createActionBar(codeActions, options) {
        const actions = this.getActionBarActions(codeActions, options);
        if (!actions.length) {
            return undefined;
        }
        const container = dom.$('.codeActionWidget-action-bar');
        const actionBar = new ActionBar(container);
        actionBar.push(actions, { icon: false, label: true });
        return actionBar;
    }
    getActionBarActions(codeActions, options) {
        const actions = codeActions.documentation.map((command) => {
            var _a;
            return ({
                id: command.id,
                label: command.title,
                tooltip: (_a = command.tooltip) !== null && _a !== void 0 ? _a : '',
                class: undefined,
                enabled: true,
                run: () => { var _a; return this._commandService.executeCommand(command.id, ...((_a = command.arguments) !== null && _a !== void 0 ? _a : [])); },
            });
        });
        if (options.includeDisabledActions && codeActions.validActions.length > 0 && codeActions.allActions.length !== codeActions.validActions.length) {
            actions.push(showDisabled ? {
                id: 'hideMoreCodeActions',
                label: localize('hideMoreCodeActions', 'Hide Disabled'),
                enabled: true,
                tooltip: '',
                class: undefined,
                run: () => this.toggleShowDisabled(false)
            } : {
                id: 'showMoreCodeActions',
                label: localize('showMoreCodeActions', 'Show Disabled'),
                enabled: true,
                tooltip: '',
                class: undefined,
                run: () => this.toggleShowDisabled(true)
            });
        }
        return actions;
    }
};
CodeActionWidget = __decorate([
    __param(0, ICommandService),
    __param(1, IContextViewService),
    __param(2, IKeybindingService),
    __param(3, ITelemetryService)
], CodeActionWidget);
export { CodeActionWidget };
