Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts
4779 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 * as dom from '../../../../base/browser/dom.js';6import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition } from '../../../browser/editorBrowser.js';7import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';8import { HoverStartSource } from './hoverOperation.js';9import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';10import { ResizableContentWidget } from './resizableContentWidget.js';11import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';12import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';14import { EditorContextKeys } from '../../../common/editorContextKeys.js';15import { getHoverAccessibleViewHint, HoverWidget } from '../../../../base/browser/ui/hover/hoverWidget.js';16import { PositionAffinity } from '../../../common/model.js';17import { Emitter } from '../../../../base/common/event.js';18import { RenderedContentHover } from './contentHoverRendered.js';19import { ScrollEvent } from '../../../../base/common/scrollable.js';2021const HORIZONTAL_SCROLLING_BY = 30;2223export class ContentHoverWidget extends ResizableContentWidget {2425public static ID = 'editor.contrib.resizableContentHoverWidget';26private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0);2728private _renderedHover: RenderedContentHover | undefined;29private _positionPreference: ContentWidgetPositionPreference | undefined;30private _minimumSize: dom.Dimension;31private _contentWidth: number | undefined;3233private readonly _hover: HoverWidget = this._register(new HoverWidget(true));34private readonly _hoverVisibleKey: IContextKey<boolean>;35private readonly _hoverFocusedKey: IContextKey<boolean>;3637private readonly _onDidResize = this._register(new Emitter<void>());38public readonly onDidResize = this._onDidResize.event;3940private readonly _onDidScroll = this._register(new Emitter<ScrollEvent>());41public readonly onDidScroll = this._onDidScroll.event;4243private readonly _onContentsChanged = this._register(new Emitter<void>());44public readonly onContentsChanged = this._onContentsChanged.event;4546public get isVisibleFromKeyboard(): boolean {47return (this._renderedHover?.source === HoverStartSource.Keyboard);48}4950public get isVisible(): boolean {51return this._hoverVisibleKey.get() ?? false;52}5354public get isFocused(): boolean {55return this._hoverFocusedKey.get() ?? false;56}5758constructor(59editor: ICodeEditor,60@IContextKeyService contextKeyService: IContextKeyService,61@IConfigurationService private readonly _configurationService: IConfigurationService,62@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,63@IKeybindingService private readonly _keybindingService: IKeybindingService64) {65const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8;66const minimumWidth = 150;67const minimumSize = new dom.Dimension(minimumWidth, minimumHeight);68super(editor, minimumSize);6970this._minimumSize = minimumSize;71this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(contextKeyService);72this._hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(contextKeyService);7374dom.append(this._resizableNode.domNode, this._hover.containerDomNode);75this._resizableNode.domNode.style.zIndex = '50';76this._resizableNode.domNode.className = 'monaco-resizable-hover';7778this._register(this._editor.onDidLayoutChange(() => {79if (this.isVisible) {80this._updateMaxDimensions();81}82}));83this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {84if (e.hasChanged(EditorOption.fontInfo)) {85this._updateFont();86}87}));88const focusTracker = this._register(dom.trackFocus(this._resizableNode.domNode));89this._register(focusTracker.onDidFocus(() => {90this._hoverFocusedKey.set(true);91}));92this._register(focusTracker.onDidBlur(() => {93this._hoverFocusedKey.set(false);94}));95this._register(this._hover.scrollbar.onScroll((e) => {96this._onDidScroll.fire(e);97}));98this._setRenderedHover(undefined);99this._editor.addContentWidget(this);100}101102public override dispose(): void {103super.dispose();104this._renderedHover?.dispose();105this._editor.removeContentWidget(this);106}107108public getId(): string {109return ContentHoverWidget.ID;110}111112private static _applyDimensions(container: HTMLElement, width: number | string, height: number | string): void {113const transformedWidth = typeof width === 'number' ? `${width}px` : width;114const transformedHeight = typeof height === 'number' ? `${height}px` : height;115container.style.width = transformedWidth;116container.style.height = transformedHeight;117}118119private _setContentsDomNodeDimensions(width: number | string, height: number | string): void {120const contentsDomNode = this._hover.contentsDomNode;121return ContentHoverWidget._applyDimensions(contentsDomNode, width, height);122}123124private _setContainerDomNodeDimensions(width: number | string, height: number | string): void {125const containerDomNode = this._hover.containerDomNode;126return ContentHoverWidget._applyDimensions(containerDomNode, width, height);127}128129private _setScrollableElementDimensions(width: number | string, height: number | string): void {130const scrollbarDomElement = this._hover.scrollbar.getDomNode();131return ContentHoverWidget._applyDimensions(scrollbarDomElement, width, height);132}133134private _setHoverWidgetDimensions(width: number | string, height: number | string): void {135this._setContainerDomNodeDimensions(width, height);136this._setScrollableElementDimensions(width, height);137this._setContentsDomNodeDimensions(width, height);138this._layoutContentWidget();139}140141private static _applyMaxDimensions(container: HTMLElement, width: number | string, height: number | string) {142const transformedWidth = typeof width === 'number' ? `${width}px` : width;143const transformedHeight = typeof height === 'number' ? `${height}px` : height;144container.style.maxWidth = transformedWidth;145container.style.maxHeight = transformedHeight;146}147148private _setHoverWidgetMaxDimensions(width: number | string, height: number | string): void {149ContentHoverWidget._applyMaxDimensions(this._hover.contentsDomNode, width, height);150ContentHoverWidget._applyMaxDimensions(this._hover.scrollbar.getDomNode(), width, height);151ContentHoverWidget._applyMaxDimensions(this._hover.containerDomNode, width, height);152this._hover.containerDomNode.style.setProperty('--vscode-hover-maxWidth', typeof width === 'number' ? `${width}px` : width);153this._layoutContentWidget();154}155156private _setAdjustedHoverWidgetDimensions(size: dom.Dimension): void {157this._setHoverWidgetMaxDimensions('none', 'none');158this._setHoverWidgetDimensions(size.width, size.height);159}160161private _updateResizableNodeMaxDimensions(): void {162const maxRenderingWidth = this._findMaximumRenderingWidth() ?? Infinity;163const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity;164this._resizableNode.maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight);165this._setHoverWidgetMaxDimensions(maxRenderingWidth, maxRenderingHeight);166}167168protected override _resize(size: dom.Dimension): void {169ContentHoverWidget._lastDimensions = new dom.Dimension(size.width, size.height);170this._setAdjustedHoverWidgetDimensions(size);171this._resizableNode.layout(size.height, size.width);172this._updateResizableNodeMaxDimensions();173this._hover.scrollbar.scanDomNode();174this._editor.layoutContentWidget(this);175this._onDidResize.fire();176}177178private _findAvailableSpaceVertically(): number | undefined {179const position = this._renderedHover?.showAtPosition;180if (!position) {181return;182}183return this._positionPreference === ContentWidgetPositionPreference.ABOVE ?184this._availableVerticalSpaceAbove(position)185: this._availableVerticalSpaceBelow(position);186}187188private _findMaximumRenderingHeight(): number | undefined {189const availableSpace = this._findAvailableSpaceVertically();190if (!availableSpace) {191return;192}193const children = this._hover.contentsDomNode.children;194let maximumHeight = children.length - 1;195Array.from(this._hover.contentsDomNode.children).forEach((hoverPart) => {196maximumHeight += hoverPart.clientHeight;197});198return Math.min(availableSpace, maximumHeight);199}200201private _isHoverTextOverflowing(): boolean {202// To find out if the text is overflowing, we will disable wrapping, check the widths, and then re-enable wrapping203this._hover.containerDomNode.style.setProperty('--vscode-hover-whiteSpace', 'nowrap');204this._hover.containerDomNode.style.setProperty('--vscode-hover-sourceWhiteSpace', 'nowrap');205206const overflowing = Array.from(this._hover.contentsDomNode.children).some((hoverElement) => {207return hoverElement.scrollWidth > hoverElement.clientWidth;208});209210this._hover.containerDomNode.style.removeProperty('--vscode-hover-whiteSpace');211this._hover.containerDomNode.style.removeProperty('--vscode-hover-sourceWhiteSpace');212213return overflowing;214}215216private _findMaximumRenderingWidth(): number | undefined {217if (!this._editor || !this._editor.hasModel()) {218return;219}220221const overflowing = this._isHoverTextOverflowing();222const initialWidth = (223typeof this._contentWidth === 'undefined'224? 0225: this._contentWidth226);227228if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) {229const bodyBoxWidth = dom.getClientArea(this._hover.containerDomNode.ownerDocument.body).width;230const horizontalPadding = 14;231return bodyBoxWidth - horizontalPadding;232} else {233return this._hover.containerDomNode.clientWidth;234}235}236237public isMouseGettingCloser(posx: number, posy: number): boolean {238239if (!this._renderedHover) {240return false;241}242if (this._renderedHover.initialMousePosX === undefined || this._renderedHover.initialMousePosY === undefined) {243this._renderedHover.initialMousePosX = posx;244this._renderedHover.initialMousePosY = posy;245return false;246}247248const widgetRect = dom.getDomNodePagePosition(this.getDomNode());249if (this._renderedHover.closestMouseDistance === undefined) {250this._renderedHover.closestMouseDistance = computeDistanceFromPointToRectangle(251this._renderedHover.initialMousePosX,252this._renderedHover.initialMousePosY,253widgetRect.left,254widgetRect.top,255widgetRect.width,256widgetRect.height257);258}259260const distance = computeDistanceFromPointToRectangle(261posx,262posy,263widgetRect.left,264widgetRect.top,265widgetRect.width,266widgetRect.height267);268if (distance > this._renderedHover.closestMouseDistance + 4 /* tolerance of 4 pixels */) {269// The mouse is getting farther away270return false;271}272273this._renderedHover.closestMouseDistance = Math.min(this._renderedHover.closestMouseDistance, distance);274return true;275}276277private _setRenderedHover(renderedHover: RenderedContentHover | undefined): void {278this._renderedHover?.dispose();279this._renderedHover = renderedHover;280this._hoverVisibleKey.set(!!renderedHover);281this._hover.containerDomNode.classList.toggle('hidden', !renderedHover);282}283284private _updateFont(): void {285const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo);286const contentsDomNode = this._hover.contentsDomNode;287contentsDomNode.style.fontSize = `${fontSize}px`;288contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`;289// eslint-disable-next-line no-restricted-syntax290const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));291codeClasses.forEach(node => this._editor.applyFontInfo(node));292}293294private _updateContent(node: DocumentFragment): void {295const contentsDomNode = this._hover.contentsDomNode;296contentsDomNode.style.paddingBottom = '';297contentsDomNode.textContent = '';298contentsDomNode.appendChild(node);299}300301private _layoutContentWidget(): void {302this._editor.layoutContentWidget(this);303this._hover.onContentsChanged();304}305306private _updateMaxDimensions() {307const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height);308const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 750, ContentHoverWidget._lastDimensions.width);309this._resizableNode.maxSize = new dom.Dimension(width, height);310this._setHoverWidgetMaxDimensions(width, height);311}312313private _render(renderedHover: RenderedContentHover) {314this._setRenderedHover(renderedHover);315this._updateFont();316this._updateContent(renderedHover.domNode);317this.handleContentsChanged();318// Simply force a synchronous render on the editor319// such that the widget does not really render with left = '0px'320this._editor.render();321}322323override getPosition(): IContentWidgetPosition | null {324if (!this._renderedHover) {325return null;326}327return {328position: this._renderedHover.showAtPosition,329secondaryPosition: this._renderedHover.showAtSecondaryPosition,330positionAffinity: this._renderedHover.shouldAppearBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined,331preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE]332};333}334335public show(renderedHover: RenderedContentHover): void {336if (!this._editor || !this._editor.hasModel()) {337return;338}339this._render(renderedHover);340const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode);341const widgetPosition = renderedHover.showAtPosition;342this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE;343344// See https://github.com/microsoft/vscode/issues/140339345// TODO: Doing a second layout of the hover after force rendering the editor346this.handleContentsChanged();347if (renderedHover.shouldFocus) {348this._hover.containerDomNode.focus();349}350this._onDidResize.fire();351// The aria label overrides the label, so if we add to it, add the contents of the hover352const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode;353const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(354this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(),355this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel() ?? ''356);357358if (accessibleViewHint) {359this._hover.contentsDomNode.ariaLabel = this._hover.contentsDomNode.textContent + ', ' + accessibleViewHint;360}361}362363public hide(): void {364if (!this._renderedHover) {365return;366}367const hoverStoleFocus = this._renderedHover.shouldFocus || this._hoverFocusedKey.get();368this._setRenderedHover(undefined);369this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity);370this._resizableNode.clearSashHoverState();371this._hoverFocusedKey.set(false);372this._editor.layoutContentWidget(this);373if (hoverStoleFocus) {374this._editor.focus();375}376}377378private _removeConstraintsRenderNormally(): void {379// Added because otherwise the initial size of the hover content is smaller than should be380const layoutInfo = this._editor.getLayoutInfo();381this._resizableNode.layout(layoutInfo.height, layoutInfo.width);382this._setHoverWidgetDimensions('auto', 'auto');383this._updateMaxDimensions();384}385386public setMinimumDimensions(dimensions: dom.Dimension): void {387// We combine the new minimum dimensions with the previous ones388this._minimumSize = new dom.Dimension(389Math.max(this._minimumSize.width, dimensions.width),390Math.max(this._minimumSize.height, dimensions.height)391);392this._updateMinimumWidth();393}394395private _updateMinimumWidth(): void {396const width = (397typeof this._contentWidth === 'undefined'398? this._minimumSize.width399: Math.min(this._contentWidth, this._minimumSize.width)400);401// We want to avoid that the hover is artificially large, so we use the content width as minimum width402this._resizableNode.minSize = new dom.Dimension(width, this._minimumSize.height);403}404405public handleContentsChanged(): void {406this._removeConstraintsRenderNormally();407const contentsDomNode = this._hover.contentsDomNode;408409let height = dom.getTotalHeight(contentsDomNode);410let width = dom.getTotalWidth(contentsDomNode) + 2;411this._resizableNode.layout(height, width);412413this._setHoverWidgetDimensions(width, height);414415height = dom.getTotalHeight(contentsDomNode);416width = dom.getTotalWidth(contentsDomNode);417this._contentWidth = width;418this._updateMinimumWidth();419this._resizableNode.layout(height, width);420421if (this._renderedHover?.showAtPosition) {422const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode);423this._positionPreference = this._findPositionPreference(widgetHeight, this._renderedHover.showAtPosition);424}425this._layoutContentWidget();426this._onContentsChanged.fire();427}428429public focus(): void {430this._hover.containerDomNode.focus();431}432433public scrollUp(): void {434const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;435const fontInfo = this._editor.getOption(EditorOption.fontInfo);436this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - fontInfo.lineHeight });437}438439public scrollDown(): void {440const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;441const fontInfo = this._editor.getOption(EditorOption.fontInfo);442this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + fontInfo.lineHeight });443}444445public scrollLeft(): void {446const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft;447this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft - HORIZONTAL_SCROLLING_BY });448}449450public scrollRight(): void {451const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft;452this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft + HORIZONTAL_SCROLLING_BY });453}454455public pageUp(): void {456const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;457const scrollHeight = this._hover.scrollbar.getScrollDimensions().height;458this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - scrollHeight });459}460461public pageDown(): void {462const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;463const scrollHeight = this._hover.scrollbar.getScrollDimensions().height;464this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + scrollHeight });465}466467public goToTop(): void {468this._hover.scrollbar.setScrollPosition({ scrollTop: 0 });469}470471public goToBottom(): void {472this._hover.scrollbar.setScrollPosition({ scrollTop: this._hover.scrollbar.getScrollDimensions().scrollHeight });473}474}475476function computeDistanceFromPointToRectangle(pointX: number, pointY: number, left: number, top: number, width: number, height: number): number {477const x = (left + width / 2); // x center of rectangle478const y = (top + height / 2); // y center of rectangle479const dx = Math.max(Math.abs(pointX - x) - width / 2, 0);480const dy = Math.max(Math.abs(pointY - y) - height / 2, 0);481return Math.sqrt(dx * dx + dy * dy);482}483484485