/*---------------------------------------------------------------------------------------------
 *  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); }
};
import * as dom from '../../../../base/browser/dom.js';
import { HoverAction, HoverWidget } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
import { TokenizationRegistry } from '../../../common/languages.js';
import { HoverOperation } from './hoverOperation.js';
import { HoverParticipantRegistry, HoverRangeAnchor } from './hoverTypes.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { Context as SuggestContext } from '../../suggest/browser/suggest.js';
import { AsyncIterableObject } from '../../../../base/common/async.js';
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
const $ = dom.$;
let ContentHoverController = class ContentHoverController extends Disposable {
    constructor(_editor, _instantiationService, _keybindingService) {
        super();
        this._editor = _editor;
        this._instantiationService = _instantiationService;
        this._keybindingService = _keybindingService;
        this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));
        this._isChangingDecorations = false;
        this._messages = [];
        this._messagesAreComplete = false;
        // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order.
        this._participants = [];
        for (const participant of HoverParticipantRegistry.getAll()) {
            this._participants.push(this._instantiationService.createInstance(participant, this._editor));
        }
        this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal);
        this._computer = new ContentHoverComputer(this._editor, this._participants);
        this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer));
        this._register(this._hoverOperation.onResult((result) => {
            this._withResult(result.value, result.isComplete, result.hasLoadingMessage);
        }));
        this._register(this._editor.onDidChangeModelDecorations(() => {
            if (this._isChangingDecorations) {
                return;
            }
            this._onModelDecorationsChanged();
        }));
        this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => {
            if (e.equals(9 /* KeyCode.Escape */)) {
                this.hide();
            }
        }));
        this._register(TokenizationRegistry.onDidChange(() => {
            if (this._widget.position && this._computer.anchor && this._messages.length > 0) {
                this._widget.clear();
                this._renderMessages(this._computer.anchor, this._messages);
            }
        }));
    }
    _onModelDecorationsChanged() {
        if (this._widget.position) {
            // The decorations have changed and the hover is visible,
            // we need to recompute the displayed text
            this._hoverOperation.cancel();
            if (!this._widget.isColorPickerVisible) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place
                this._hoverOperation.start(0 /* HoverStartMode.Delayed */);
            }
        }
    }
    maybeShowAt(mouseEvent) {
        const anchorCandidates = [];
        for (const participant of this._participants) {
            if (participant.suggestHoverAnchor) {
                const anchor = participant.suggestHoverAnchor(mouseEvent);
                if (anchor) {
                    anchorCandidates.push(anchor);
                }
            }
        }
        const target = mouseEvent.target;
        if (target.type === 6 /* MouseTargetType.CONTENT_TEXT */) {
            anchorCandidates.push(new HoverRangeAnchor(0, target.range));
        }
        if (target.type === 7 /* MouseTargetType.CONTENT_EMPTY */) {
            const epsilon = this._editor.getOption(46 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth / 2;
            if (!target.detail.isAfterLines && typeof target.detail.horizontalDistanceToText === 'number' && target.detail.horizontalDistanceToText < epsilon) {
                // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
                anchorCandidates.push(new HoverRangeAnchor(0, target.range));
            }
        }
        if (anchorCandidates.length === 0) {
            return false;
        }
        anchorCandidates.sort((a, b) => b.priority - a.priority);
        this._startShowingAt(anchorCandidates[0], 0 /* HoverStartMode.Delayed */, false);
        return true;
    }
    startShowingAtRange(range, mode, focus) {
        this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus);
    }
    _startShowingAt(anchor, mode, focus) {
        if (this._computer.anchor && this._computer.anchor.equals(anchor)) {
            // We have to show the widget at the exact same range as before, so no work is needed
            return;
        }
        this._hoverOperation.cancel();
        if (this._widget.position) {
            // The range might have changed, but the hover is visible
            // Instead of hiding it completely, filter out messages that are still in the new range and
            // kick off a new computation
            if (!this._computer.anchor || !anchor.canAdoptVisibleHover(this._computer.anchor, this._widget.position)) {
                this.hide();
            }
            else {
                const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor));
                if (filteredMessages.length === 0) {
                    this.hide();
                }
                else if (filteredMessages.length === this._messages.length && this._messagesAreComplete) {
                    // no change
                    return;
                }
                else {
                    this._renderMessages(anchor, filteredMessages);
                }
            }
        }
        this._computer.anchor = anchor;
        this._computer.shouldFocus = focus;
        this._hoverOperation.start(mode);
    }
    hide() {
        this._computer.anchor = null;
        this._hoverOperation.cancel();
        this._widget.hide();
    }
    isColorPickerVisible() {
        return this._widget.isColorPickerVisible;
    }
    containsNode(node) {
        return this._widget.getDomNode().contains(node);
    }
    _addLoadingMessage(result) {
        if (this._computer.anchor) {
            for (const participant of this._participants) {
                if (participant.createLoadingMessage) {
                    const loadingMessage = participant.createLoadingMessage(this._computer.anchor);
                    if (loadingMessage) {
                        return result.slice(0).concat([loadingMessage]);
                    }
                }
            }
        }
        return result;
    }
    _withResult(result, isComplete, hasLoadingMessage) {
        this._messages = (hasLoadingMessage ? this._addLoadingMessage(result) : result);
        this._messagesAreComplete = isComplete;
        if (this._computer.anchor && this._messages.length > 0) {
            this._renderMessages(this._computer.anchor, this._messages);
        }
        else if (isComplete) {
            this.hide();
        }
    }
    _renderMessages(anchor, messages) {
        const { showAtPosition, showAtRange, highlightRange } = ContentHoverController.computeHoverRanges(anchor.range, messages);
        const disposables = new DisposableStore();
        const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService));
        const fragment = document.createDocumentFragment();
        let colorPicker = null;
        const context = {
            fragment,
            statusBar,
            setColorPicker: (widget) => colorPicker = widget,
            onContentsChanged: () => this._widget.onContentsChanged(),
            hide: () => this.hide()
        };
        for (const participant of this._participants) {
            const hoverParts = messages.filter(msg => msg.owner === participant);
            if (hoverParts.length > 0) {
                disposables.add(participant.renderHoverParts(context, hoverParts));
            }
        }
        if (statusBar.hasContent) {
            fragment.appendChild(statusBar.hoverElement);
        }
        if (fragment.hasChildNodes()) {
            if (highlightRange) {
                const highlightDecoration = this._editor.createDecorationsCollection();
                try {
                    this._isChangingDecorations = true;
                    highlightDecoration.set([{
                            range: highlightRange,
                            options: ContentHoverController._DECORATION_OPTIONS
                        }]);
                }
                finally {
                    this._isChangingDecorations = false;
                }
                disposables.add(toDisposable(() => {
                    try {
                        this._isChangingDecorations = true;
                        highlightDecoration.clear();
                    }
                    finally {
                        this._isChangingDecorations = false;
                    }
                }));
            }
            this._widget.showAt(fragment, new ContentHoverVisibleData(colorPicker, showAtPosition, showAtRange, this._editor.getOption(55 /* EditorOption.hover */).above, this._computer.shouldFocus, disposables));
        }
        else {
            disposables.dispose();
        }
    }
    static computeHoverRanges(anchorRange, messages) {
        // The anchor range is always on a single line
        const anchorLineNumber = anchorRange.startLineNumber;
        let renderStartColumn = anchorRange.startColumn;
        let renderEndColumn = anchorRange.endColumn;
        let highlightRange = messages[0].range;
        let forceShowAtRange = null;
        for (const msg of messages) {
            highlightRange = Range.plusRange(highlightRange, msg.range);
            if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) {
                // this message has a range that is completely sitting on the line of the anchor
                renderStartColumn = Math.min(renderStartColumn, msg.range.startColumn);
                renderEndColumn = Math.max(renderEndColumn, msg.range.endColumn);
            }
            if (msg.forceShowAtRange) {
                forceShowAtRange = msg.range;
            }
        }
        return {
            showAtPosition: forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorRange.startLineNumber, renderStartColumn),
            showAtRange: forceShowAtRange ? forceShowAtRange : new Range(anchorLineNumber, renderStartColumn, anchorLineNumber, renderEndColumn),
            highlightRange
        };
    }
};
ContentHoverController._DECORATION_OPTIONS = ModelDecorationOptions.register({
    description: 'content-hover-highlight',
    className: 'hoverHighlight'
});
ContentHoverController = __decorate([
    __param(1, IInstantiationService),
    __param(2, IKeybindingService)
], ContentHoverController);
export { ContentHoverController };
class ContentHoverVisibleData {
    constructor(colorPicker, showAtPosition, showAtRange, preferAbove, stoleFocus, disposables) {
        this.colorPicker = colorPicker;
        this.showAtPosition = showAtPosition;
        this.showAtRange = showAtRange;
        this.preferAbove = preferAbove;
        this.stoleFocus = stoleFocus;
        this.disposables = disposables;
    }
}
let ContentHoverWidget = class ContentHoverWidget extends Disposable {
    constructor(_editor, _contextKeyService) {
        super();
        this._editor = _editor;
        this._contextKeyService = _contextKeyService;
        this.allowEditorOverflow = true;
        this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(this._contextKeyService);
        this._hover = this._register(new HoverWidget());
        this._visibleData = null;
        this._register(this._editor.onDidLayoutChange(() => this._layout()));
        this._register(this._editor.onDidChangeConfiguration((e) => {
            if (e.hasChanged(46 /* EditorOption.fontInfo */)) {
                this._updateFont();
            }
        }));
        this._setVisibleData(null);
        this._layout();
        this._editor.addContentWidget(this);
    }
    /**
     * Returns `null` if the hover is not visible.
     */
    get position() {
        var _a, _b;
        return (_b = (_a = this._visibleData) === null || _a === void 0 ? void 0 : _a.showAtPosition) !== null && _b !== void 0 ? _b : null;
    }
    get isColorPickerVisible() {
        var _a;
        return Boolean((_a = this._visibleData) === null || _a === void 0 ? void 0 : _a.colorPicker);
    }
    dispose() {
        this._editor.removeContentWidget(this);
        if (this._visibleData) {
            this._visibleData.disposables.dispose();
        }
        super.dispose();
    }
    getId() {
        return ContentHoverWidget.ID;
    }
    getDomNode() {
        return this._hover.containerDomNode;
    }
    getPosition() {
        if (!this._visibleData) {
            return null;
        }
        let preferAbove = this._visibleData.preferAbove;
        if (!preferAbove && this._contextKeyService.getContextKeyValue(SuggestContext.Visible.key)) {
            // Prefer rendering above if the suggest widget is visible
            preferAbove = true;
        }
        return {
            position: this._visibleData.showAtPosition,
            range: this._visibleData.showAtRange,
            preference: (preferAbove
                ? [1 /* ContentWidgetPositionPreference.ABOVE */, 2 /* ContentWidgetPositionPreference.BELOW */]
                : [2 /* ContentWidgetPositionPreference.BELOW */, 1 /* ContentWidgetPositionPreference.ABOVE */]),
        };
    }
    _setVisibleData(visibleData) {
        if (this._visibleData) {
            this._visibleData.disposables.dispose();
        }
        this._visibleData = visibleData;
        this._hoverVisibleKey.set(!!this._visibleData);
        this._hover.containerDomNode.classList.toggle('hidden', !this._visibleData);
    }
    _layout() {
        const height = Math.max(this._editor.getLayoutInfo().height / 4, 250);
        const { fontSize, lineHeight } = this._editor.getOption(46 /* EditorOption.fontInfo */);
        this._hover.contentsDomNode.style.fontSize = `${fontSize}px`;
        this._hover.contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`;
        this._hover.contentsDomNode.style.maxHeight = `${height}px`;
        this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`;
    }
    _updateFont() {
        const codeClasses = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));
        codeClasses.forEach(node => this._editor.applyFontInfo(node));
    }
    showAt(node, visibleData) {
        var _a;
        this._setVisibleData(visibleData);
        this._hover.contentsDomNode.textContent = '';
        this._hover.contentsDomNode.appendChild(node);
        this._hover.contentsDomNode.style.paddingBottom = '';
        this._updateFont();
        this.onContentsChanged();
        // Simply force a synchronous render on the editor
        // such that the widget does not really render with left = '0px'
        this._editor.render();
        // See https://github.com/microsoft/vscode/issues/140339
        // TODO: Doing a second layout of the hover after force rendering the editor
        this.onContentsChanged();
        if (visibleData.stoleFocus) {
            this._hover.containerDomNode.focus();
        }
        (_a = visibleData.colorPicker) === null || _a === void 0 ? void 0 : _a.layout();
    }
    hide() {
        if (this._visibleData) {
            const stoleFocus = this._visibleData.stoleFocus;
            this._setVisibleData(null);
            this._editor.layoutContentWidget(this);
            if (stoleFocus) {
                this._editor.focus();
            }
        }
    }
    onContentsChanged() {
        this._editor.layoutContentWidget(this);
        this._hover.onContentsChanged();
        const scrollDimensions = this._hover.scrollbar.getScrollDimensions();
        const hasHorizontalScrollbar = (scrollDimensions.scrollWidth > scrollDimensions.width);
        if (hasHorizontalScrollbar) {
            // There is just a horizontal scrollbar
            const extraBottomPadding = `${this._hover.scrollbar.options.horizontalScrollbarSize}px`;
            if (this._hover.contentsDomNode.style.paddingBottom !== extraBottomPadding) {
                this._hover.contentsDomNode.style.paddingBottom = extraBottomPadding;
                this._editor.layoutContentWidget(this);
                this._hover.onContentsChanged();
            }
        }
    }
    clear() {
        this._hover.contentsDomNode.textContent = '';
    }
};
ContentHoverWidget.ID = 'editor.contrib.contentHoverWidget';
ContentHoverWidget = __decorate([
    __param(1, IContextKeyService)
], ContentHoverWidget);
export { ContentHoverWidget };
let EditorHoverStatusBar = class EditorHoverStatusBar extends Disposable {
    constructor(_keybindingService) {
        super();
        this._keybindingService = _keybindingService;
        this._hasContent = false;
        this.hoverElement = $('div.hover-row.status-bar');
        this.actionsElement = dom.append(this.hoverElement, $('div.actions'));
    }
    get hasContent() {
        return this._hasContent;
    }
    addAction(actionOptions) {
        const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId);
        const keybindingLabel = keybinding ? keybinding.getLabel() : null;
        this._hasContent = true;
        return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel));
    }
    append(element) {
        const result = dom.append(this.actionsElement, element);
        this._hasContent = true;
        return result;
    }
};
EditorHoverStatusBar = __decorate([
    __param(0, IKeybindingService)
], EditorHoverStatusBar);
class ContentHoverComputer {
    constructor(_editor, _participants) {
        this._editor = _editor;
        this._participants = _participants;
        this._anchor = null;
        this._shouldFocus = false;
    }
    get anchor() { return this._anchor; }
    set anchor(value) { this._anchor = value; }
    get shouldFocus() { return this._shouldFocus; }
    set shouldFocus(value) { this._shouldFocus = value; }
    static _getLineDecorations(editor, anchor) {
        if (anchor.type !== 1 /* HoverAnchorType.Range */) {
            return [];
        }
        const model = editor.getModel();
        const lineNumber = anchor.range.startLineNumber;
        if (lineNumber > model.getLineCount()) {
            // invalid line
            return [];
        }
        const maxColumn = model.getLineMaxColumn(lineNumber);
        return editor.getLineDecorations(lineNumber).filter((d) => {
            if (d.options.isWholeLine) {
                return true;
            }
            const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1;
            const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn;
            if (d.options.showIfCollapsed) {
                // Relax check around `showIfCollapsed` decorations to also include +/- 1 character
                if (startColumn > anchor.range.startColumn + 1 || anchor.range.endColumn - 1 > endColumn) {
                    return false;
                }
            }
            else {
                if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) {
                    return false;
                }
            }
            return true;
        });
    }
    computeAsync(token) {
        const anchor = this._anchor;
        if (!this._editor.hasModel() || !anchor) {
            return AsyncIterableObject.EMPTY;
        }
        const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, anchor);
        return AsyncIterableObject.merge(this._participants.map((participant) => {
            if (!participant.computeAsync) {
                return AsyncIterableObject.EMPTY;
            }
            return participant.computeAsync(anchor, lineDecorations, token);
        }));
    }
    computeSync() {
        if (!this._editor.hasModel() || !this._anchor) {
            return [];
        }
        const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, this._anchor);
        let result = [];
        for (const participant of this._participants) {
            result = result.concat(participant.computeSync(this._anchor, lineDecorations));
        }
        return coalesce(result);
    }
}
