/*--------------------------------------------------------------------------------------------- * 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 { Widget } from '../../../base/browser/ui/widget.js'; import { coalesce } from '../../../base/common/arrays.js'; import { Disposable, DisposableStore } 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/modes.js'; import { ColorHoverParticipant } from './colorHoverParticipant.js'; import { HoverOperation } from './hoverOperation.js'; import { HoverRangeAnchor } from './hoverTypes.js'; import { MarkdownHoverParticipant } from './markdownHoverParticipant.js'; import { MarkerHoverParticipant } from './markerHoverParticipant.js'; import { InlineCompletionsHoverParticipant } from '../inlineCompletions/inlineCompletionsHoverParticipant.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/suggest.js'; import { UnicodeHighlighterHoverParticipant } from '../unicodeHighlighter/unicodeHighlighter.js'; import { AsyncIterableObject } from '../../../base/common/async.js'; const $ = dom.$; 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 ModesContentComputer { constructor(editor, _participants) { this._participants = _participants; this._editor = editor; this._result = []; this._anchor = null; } setAnchor(anchor) { this._anchor = anchor; this._result = []; } clearResult() { this._result = []; } static _getLineDecorations(editor, anchor) { if (anchor.type !== 1 /* Range */) { return []; } const model = editor.getModel(); const lineNumber = anchor.range.startLineNumber; 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 = ModesContentComputer._getLineDecorations(this._editor, anchor); return AsyncIterableObject.merge(this._participants.map(participant => this._computeAsync(participant, lineDecorations, anchor, token))); } _computeAsync(participant, lineDecorations, anchor, token) { if (!participant.computeAsync) { return AsyncIterableObject.EMPTY; } return participant.computeAsync(anchor, lineDecorations, token); } computeSync() { if (!this._editor.hasModel() || !this._anchor) { return []; } const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, this._anchor); let result = []; for (const participant of this._participants) { result = result.concat(participant.computeSync(this._anchor, lineDecorations)); } return coalesce(result); } onResult(result, isFromSynchronousComputation) { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { this._result = result.concat(this._result); } else { this._result = this._result.concat(result); } } getResult() { return this._result.slice(0); } getResultWithLoadingMessage() { if (this._anchor) { for (const participant of this._participants) { if (participant.createLoadingMessage) { const loadingMessage = participant.createLoadingMessage(this._anchor); if (loadingMessage) { return this._result.slice(0).concat([loadingMessage]); } } } } return this._result.slice(0); } } let ModesContentHoverWidget = class ModesContentHoverWidget extends Widget { constructor(editor, _hoverVisibleKey, instantiationService, _keybindingService, _contextKeyService) { super(); this._hoverVisibleKey = _hoverVisibleKey; this._keybindingService = _keybindingService; this._contextKeyService = _contextKeyService; // IContentWidget.allowEditorOverflow this.allowEditorOverflow = true; this._participants = [ instantiationService.createInstance(ColorHoverParticipant, editor, this), instantiationService.createInstance(MarkdownHoverParticipant, editor, this), instantiationService.createInstance(InlineCompletionsHoverParticipant, editor, this), instantiationService.createInstance(UnicodeHighlighterHoverParticipant, editor, this), instantiationService.createInstance(MarkerHoverParticipant, editor, this), ]; this._editor = editor; this._isVisible = false; this._stoleFocus = false; this._renderDisposable = null; this._hover = this._register(new HoverWidget()); this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); this.onkeydown(this._hover.containerDomNode, (e) => { if (e.equals(9 /* Escape */)) { this.hide(); } }); this._register(this._editor.onDidChangeConfiguration((e) => { if (e.hasChanged(43 /* fontInfo */)) { this._updateFont(); } })); this._editor.onDidLayoutChange(() => this.layout()); this.layout(); this._editor.addContentWidget(this); this._showAtPosition = null; this._showAtRange = null; this._stoleFocus = false; this._messages = []; this._messagesAreComplete = false; this._lastAnchor = null; this._computer = new ModesContentComputer(this._editor, this._participants); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; this._colorPicker = null; this._preferAbove = this._editor.getOption(52 /* hover */).above; this._hoverOperation = new HoverOperation(this._computer, result => this._withResult(result, true), null, result => this._withResult(result, false), this._editor.getOption(52 /* hover */).delay); this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => { if (this._colorPicker) { this.getDomNode().classList.add('colorpicker-hover'); } })); this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { this.getDomNode().classList.remove('colorpicker-hover'); })); this._register(editor.onDidChangeConfiguration(() => { this._hoverOperation.setHoverTime(this._editor.getOption(52 /* hover */).delay); this._preferAbove = this._editor.getOption(52 /* hover */).above; })); this._register(TokenizationRegistry.onDidChange(() => { if (this._isVisible && this._lastAnchor && this._messages.length > 0) { this._hover.contentsDomNode.textContent = ''; this._renderMessages(this._lastAnchor, this._messages); } })); } dispose() { this._hoverOperation.cancel(); this._editor.removeContentWidget(this); super.dispose(); } getId() { return ModesContentHoverWidget.ID; } getDomNode() { return this._hover.containerDomNode; } _shouldShowAt(mouseEvent) { const targetType = mouseEvent.target.type; if (targetType === 6 /* CONTENT_TEXT */) { return true; } if (targetType === 7 /* CONTENT_EMPTY */) { const epsilon = this._editor.getOption(43 /* fontInfo */).typicalHalfwidthCharacterWidth / 2; const data = mouseEvent.target.detail; if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.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 return true; } } return false; } maybeShowAt(mouseEvent) { var _a; const anchorCandidates = []; for (const participant of this._participants) { if (typeof participant.suggestHoverAnchor === 'function') { const anchor = participant.suggestHoverAnchor(mouseEvent); if (anchor) { anchorCandidates.push(anchor); } } } if (this._shouldShowAt(mouseEvent) && mouseEvent.target.range) { // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. // Check if mouse is hovering on color decorator const hoverOnColorDecorator = [...((_a = mouseEvent.target.element) === null || _a === void 0 ? void 0 : _a.classList.values()) || []].find(className => className.startsWith('ced-colorBox')) && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; const showAtRange = (hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) : mouseEvent.target.range); anchorCandidates.push(new HoverRangeAnchor(0, showAtRange)); } if (anchorCandidates.length === 0) { return false; } anchorCandidates.sort((a, b) => b.priority - a.priority); this._startShowingAt(anchorCandidates[0], 0 /* Delayed */, false); return true; } _showAt(position, range, focus) { // Position has changed this._showAtPosition = position; this._showAtRange = range; this._hoverVisibleKey.set(true); this._isVisible = true; this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); this._editor.layoutContentWidget(this); // Simply force a synchronous render on the editor // such that the widget does not really render with left = '0px' this._editor.render(); this._stoleFocus = focus; if (focus) { this._hover.containerDomNode.focus(); } } getPosition() { if (this._isVisible) { let preferAbove = this._preferAbove; if (!preferAbove && this._contextKeyService.getContextKeyValue(SuggestContext.Visible.key)) { // Prefer rendering above if the suggest widget is visible preferAbove = true; } return { position: this._showAtPosition, range: this._showAtRange, preference: preferAbove ? [ 1 /* ABOVE */, 2 /* BELOW */, ] : [ 2 /* BELOW */, 1 /* ABOVE */, ], }; } return null; } _updateFont() { const codeClasses = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); codeClasses.forEach(node => this._editor.applyFontInfo(node)); } _updateContents(node) { this._hover.contentsDomNode.textContent = ''; this._hover.contentsDomNode.appendChild(node); this._updateFont(); this._editor.layoutContentWidget(this); this._hover.onContentsChanged(); } layout() { const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); const { fontSize, lineHeight } = this._editor.getOption(43 /* 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`; } onModelDecorationsChanged() { if (this._isChangingDecorations) { return; } if (this._isVisible) { // The decorations have changed and the hover is visible, // we need to recompute the displayed text this._hoverOperation.cancel(); this._computer.clearResult(); if (!this._colorPicker) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place this._hoverOperation.start(0 /* Delayed */); } } } startShowingAtRange(range, mode, focus) { this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus); } _startShowingAt(anchor, mode, focus) { if (this._lastAnchor && this._lastAnchor.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._isVisible) { // 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._showAtPosition || !this._lastAnchor || !anchor.canAdoptVisibleHover(this._lastAnchor, this._showAtPosition)) { 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._lastAnchor = anchor; this._computer.setAnchor(anchor); this._shouldFocus = focus; this._hoverOperation.start(mode); } hide() { this._lastAnchor = null; this._hoverOperation.cancel(); if (this._isVisible) { setTimeout(() => { // Give commands a chance to see the key if (!this._isVisible) { this._hoverVisibleKey.set(false); } }, 0); this._isVisible = false; this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); this._editor.layoutContentWidget(this); if (this._stoleFocus) { this._editor.focus(); } } this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; if (this._renderDisposable) { this._renderDisposable.dispose(); this._renderDisposable = null; } this._colorPicker = null; } isColorPickerVisible() { return !!this._colorPicker; } setColorPicker(widget) { this._colorPicker = widget; } onContentsChanged() { this._hover.onContentsChanged(); } _withResult(result, complete) { this._messages = result; this._messagesAreComplete = complete; if (this._lastAnchor && this._messages.length > 0) { this._renderMessages(this._lastAnchor, this._messages); } else if (complete) { this.hide(); } } _renderMessages(anchor, messages) { if (this._renderDisposable) { this._renderDisposable.dispose(); this._renderDisposable = null; } this._colorPicker = null; // TODO: TypeScript thinks this is always null // update column from which to show let renderColumn = 1073741824 /* MAX_SAFE_SMALL_INTEGER */; let highlightRange = messages[0].range; let forceShowAtRange = null; let fragment = document.createDocumentFragment(); const disposables = new DisposableStore(); const hoverParts = new Map(); for (const msg of messages) { renderColumn = Math.min(renderColumn, msg.range.startColumn); highlightRange = Range.plusRange(highlightRange, msg.range); if (msg.forceShowAtRange) { forceShowAtRange = msg.range; } if (!hoverParts.has(msg.owner)) { hoverParts.set(msg.owner, []); } const dest = hoverParts.get(msg.owner); dest.push(msg); } const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); for (const participant of this._participants) { if (hoverParts.has(participant)) { const participantHoverParts = hoverParts.get(participant); disposables.add(participant.renderHoverParts(participantHoverParts, fragment, statusBar)); } } if (statusBar.hasContent) { fragment.appendChild(statusBar.hoverElement); } this._renderDisposable = disposables; // show if (fragment.hasChildNodes()) { if (forceShowAtRange) { this._showAt(forceShowAtRange.getStartPosition(), forceShowAtRange, this._shouldFocus); } else { this._showAt(new Position(anchor.range.startLineNumber, renderColumn), highlightRange, this._shouldFocus); } this._updateContents(fragment); } if (this._colorPicker) { this._colorPicker.layout(); } this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, highlightRange ? [{ range: highlightRange, options: ModesContentHoverWidget._DECORATION_OPTIONS }] : []); this._isChangingDecorations = false; } }; ModesContentHoverWidget.ID = 'editor.contrib.modesContentHoverWidget'; ModesContentHoverWidget._DECORATION_OPTIONS = ModelDecorationOptions.register({ description: 'content-hover-highlight', className: 'hoverHighlight' }); ModesContentHoverWidget = __decorate([ __param(2, IInstantiationService), __param(3, IKeybindingService), __param(4, IContextKeyService) ], ModesContentHoverWidget); export { ModesContentHoverWidget };