diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 5cd824a3d7669..20c73b8332707 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -28,7 +28,6 @@ } .monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { - max-width: 500px; word-wrap: break-word; } diff --git a/src/vs/base/browser/ui/resizable/resizable.ts b/src/vs/base/browser/ui/resizable/resizable.ts index 95dfb06b8d007..9bd5f4ff54219 100644 --- a/src/vs/base/browser/ui/resizable/resizable.ts +++ b/src/vs/base/browser/ui/resizable/resizable.ts @@ -187,4 +187,20 @@ export class ResizableHTMLElement { get preferredSize() { return this._preferredSize; } + + get northSash() { + return this._northSash; + } + + get eastSash() { + return this._eastSash; + } + + get westSash() { + return this._westSash; + } + + get southSash() { + return this._southSash; + } } diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index b20c218516917..f0e87bb1f558b 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -247,7 +247,7 @@ const PointerEventsDisabledCssClass = 'pointer-events-disabled'; */ export class Sash extends Disposable { - private el: HTMLElement; + private _el: HTMLElement; private layoutProvider: ISashLayoutProvider; private orientation: Orientation; private size: number; @@ -269,6 +269,7 @@ export class Sash extends Disposable { private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore()); private _orthogonalEndDragHandle: HTMLElement | undefined; + get el(): HTMLElement { return this._el; } get state(): SashState { return this._state; } get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; } get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; } @@ -282,9 +283,9 @@ export class Sash extends Disposable { return; } - this.el.classList.toggle('disabled', state === SashState.Disabled); - this.el.classList.toggle('minimum', state === SashState.AtMinimum); - this.el.classList.toggle('maximum', state === SashState.AtMaximum); + this._el.classList.toggle('disabled', state === SashState.Disabled); + this._el.classList.toggle('minimum', state === SashState.AtMinimum); + this._el.classList.toggle('maximum', state === SashState.AtMaximum); this._state = state; this.onDidEnablementChange.fire(state); @@ -336,7 +337,7 @@ export class Sash extends Disposable { this.orthogonalStartDragHandleDisposables.clear(); if (state !== SashState.Disabled) { - this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start')); + this._orthogonalStartDragHandle = append(this._el, $('.orthogonal-drag-handle.start')); this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove())); this.orthogonalStartDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables); @@ -370,7 +371,7 @@ export class Sash extends Disposable { this.orthogonalEndDragHandleDisposables.clear(); if (state !== SashState.Disabled) { - this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end')); + this._orthogonalEndDragHandle = append(this._el, $('.orthogonal-drag-handle.end')); this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove())); this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables); @@ -406,30 +407,30 @@ export class Sash extends Disposable { constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) { super(); - this.el = append(container, $('.monaco-sash')); + this._el = append(container, $('.monaco-sash')); if (options.orthogonalEdge) { - this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`); + this._el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`); } if (isMacintosh) { - this.el.classList.add('mac'); + this._el.classList.add('mac'); } - const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event; + const onMouseDown = this._register(new DomEmitter(this._el, 'mousedown')).event; this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this)); - const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event; + const onMouseDoubleClick = this._register(new DomEmitter(this._el, 'dblclick')).event; this._register(onMouseDoubleClick(this.onPointerDoublePress, this)); - const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event; + const onMouseEnter = this._register(new DomEmitter(this._el, 'mouseenter')).event; this._register(onMouseEnter(() => Sash.onMouseEnter(this))); - const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event; + const onMouseLeave = this._register(new DomEmitter(this._el, 'mouseleave')).event; this._register(onMouseLeave(() => Sash.onMouseLeave(this))); - this._register(Gesture.addTarget(this.el)); + this._register(Gesture.addTarget(this._el)); - const onTouchStart = this._register(new DomEmitter(this.el, EventType.Start)).event; - this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this)); - const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event; + const onTouchStart = this._register(new DomEmitter(this._el, EventType.Start)).event; + this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this._el)), this)); + const onTap = this._register(new DomEmitter(this._el, EventType.Tap)).event; let doubleTapTimeout: any = undefined; this._register(onTap(event => { @@ -448,9 +449,9 @@ export class Sash extends Disposable { this.size = options.size; if (options.orientation === Orientation.VERTICAL) { - this.el.style.width = `${this.size}px`; + this._el.style.width = `${this.size - 2}px`; } else { - this.el.style.height = `${this.size}px`; + this._el.style.height = `${this.size - 2}px`; } } else { this.size = globalSize; @@ -470,14 +471,14 @@ export class Sash extends Disposable { this.orientation = options.orientation || Orientation.VERTICAL; if (this.orientation === Orientation.HORIZONTAL) { - this.el.classList.add('horizontal'); - this.el.classList.remove('vertical'); + this._el.classList.add('horizontal'); + this._el.classList.remove('vertical'); } else { - this.el.classList.remove('horizontal'); - this.el.classList.add('vertical'); + this._el.classList.remove('horizontal'); + this._el.classList.add('vertical'); } - this.el.classList.toggle('debug', DEBUG); + this._el.classList.toggle('debug', DEBUG); this.layout(); } @@ -516,11 +517,11 @@ export class Sash extends Disposable { const altKey = event.altKey; const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey }; - this.el.classList.add('active'); + this._el.classList.add('active'); this._onDidStart.fire(startEvent); // fix https://github.com/microsoft/vscode/issues/21675 - const style = createStyleSheet(this.el); + const style = createStyleSheet(this._el); const updateStyle = () => { let cursor = ''; @@ -565,9 +566,9 @@ export class Sash extends Disposable { const onPointerUp = (e: PointerEvent) => { EventHelper.stop(e, false); - this.el.removeChild(style); + this._el.removeChild(style); - this.el.classList.remove('active'); + this._el.classList.remove('active'); this._onDidEnd.fire(); disposables.dispose(); @@ -597,11 +598,11 @@ export class Sash extends Disposable { } private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void { - if (sash.el.classList.contains('active')) { + if (sash._el.classList.contains('active')) { sash.hoverDelayer.cancel(); - sash.el.classList.add('hover'); + sash._el.classList.add('hover'); } else { - sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { }); + sash.hoverDelayer.trigger(() => sash._el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { }); } if (!fromLinkedSash && sash.linkedSash) { @@ -611,7 +612,7 @@ export class Sash extends Disposable { private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void { sash.hoverDelayer.cancel(); - sash.el.classList.remove('hover'); + sash._el.classList.remove('hover'); if (!fromLinkedSash && sash.linkedSash) { Sash.onMouseLeave(sash.linkedSash, true); @@ -634,25 +635,25 @@ export class Sash extends Disposable { layout(): void { if (this.orientation === Orientation.VERTICAL) { const verticalProvider = (this.layoutProvider); - this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px'; + this._el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px'; if (verticalProvider.getVerticalSashTop) { - this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px'; + this._el.style.top = verticalProvider.getVerticalSashTop(this) + 'px'; } if (verticalProvider.getVerticalSashHeight) { - this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px'; + this._el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px'; } } else { const horizontalProvider = (this.layoutProvider); - this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px'; + this._el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px'; if (horizontalProvider.getHorizontalSashLeft) { - this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px'; + this._el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px'; } if (horizontalProvider.getHorizontalSashWidth) { - this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px'; + this._el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px'; } } } @@ -673,6 +674,6 @@ export class Sash extends Disposable { override dispose(): void { super.dispose(); - this.el.remove(); + this._el.remove(); } } diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 26741f8e03324..b88e2d04c60ff 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -9,9 +9,9 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration, PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -24,12 +24,19 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/suggest'; import { AsyncIterableObject } from 'vs/base/common/async'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { IResizeEvent, ResizableHTMLElement } from 'vs/base/browser/ui/resizable/resizable'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ResourceMap } from 'vs/base/common/map'; const $ = dom.$; +const SCROLLBAR_WIDTH = 10; +const SASH_WIDTH = 4; +const TOTAL_BORDER_WIDTH = 2; export class ContentHoverController extends Disposable { private readonly _participants: IEditorHoverParticipant[]; + private readonly _resizableOverlay = this._register(this._instantiationService.createInstance(ResizableHoverOverlay, this._editor)); private readonly _widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); private readonly _computer: ContentHoverComputer; private readonly _hoverOperation: HoverOperation; @@ -43,6 +50,7 @@ export class ContentHoverController extends Disposable { ) { super(); + this._resizableOverlay.hoverWidget = this._widget; // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. this._participants = []; for (const participant of HoverParticipantRegistry.getAll()) { @@ -72,12 +80,46 @@ export class ContentHoverController extends Disposable { this._setCurrentResult(this._currentResult); // render again } })); + this._register(this._resizableOverlay.onDidResize((e) => { + // When the resizable hover overlay changes, resize the widget + this._widget.resize(e.dimension); + // Update the left and top offset of the resizable element because the content widget may change its left and top offset as it is resized + this._repositionResizableOverlay(); + })); + this._register(this._editor.onDidLayoutChange(() => { + // Sometimes the hover does not disappear on changing the layout of the editor, in that case reposition the resizable hover + this._repositionResizableOverlay(); + })); + } + + private _repositionResizableOverlay(): void { + const resizableOverlayDomNode = this._resizableOverlay.getDomNode(); + const widgetDomNode = this._widget.getDomNode(); + const offsetTop = widgetDomNode.offsetTop; + const offsetLeft = widgetDomNode.offsetLeft; + + if (offsetLeft) { + resizableOverlayDomNode.style.left = offsetLeft - SASH_WIDTH + TOTAL_BORDER_WIDTH + 'px'; + } + if (offsetTop) { + resizableOverlayDomNode.style.top = offsetTop - SASH_WIDTH + TOTAL_BORDER_WIDTH + 'px'; + } + } + + get resizableOverlay() { + return this._resizableOverlay; } /** * Returns true if the hover shows now or will show. */ public maybeShowAt(mouseEvent: IEditorMouseEvent): boolean { + + // While the hover overlay is resizing, the hover is showing + if (this._resizableOverlay.isResizing()) { + return true; + } + const anchorCandidates: HoverAnchor[] = []; for (const participant of this._participants) { @@ -191,6 +233,7 @@ export class ContentHoverController extends Disposable { this._renderMessages(this._currentResult.anchor, this._currentResult.messages); } else { this._widget.hide(); + this._resizableOverlay.hide(); } } @@ -260,7 +303,75 @@ export class ContentHoverController extends Disposable { fragment, statusBar, setColorPicker: (widget) => colorPicker = widget, - onContentsChanged: () => this._widget.onContentsChanged(), + onContentsChanged: () => { + + const persistedSize = this._resizableOverlay.findPersistedSize(); + this._widget.onContentsChanged(persistedSize); + + // Needed in order to render correctly the content hover widget + this._editor.render(); + + // After the final rendering of the widget, retrieve its top and left offsets in order to set the size of the resizable element + const widgetDomNode = this._widget.getDomNode(); + const offsetTop = widgetDomNode.offsetTop; + const offsetLeft = widgetDomNode.offsetLeft; + const clientWidth = widgetDomNode.clientWidth; + const clientHeight = widgetDomNode.clientHeight; + + // Find if rendered above or below in the container dom node + const topLineNumber = anchor.initialMousePosY; + let renderingAbove: boolean = true; + + if (topLineNumber) { + if (offsetTop <= topLineNumber) { + renderingAbove = true; + } else { + renderingAbove = false; + } + } + + const contentWidgetPositionPreference = renderingAbove ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; + this._widget.renderingAbove = contentWidgetPositionPreference; + this._resizableOverlay.renderingAbove = contentWidgetPositionPreference; + + const resizableElement = this._resizableOverlay.resizableElement(); + resizableElement.layout(clientHeight + 2 * SASH_WIDTH - TOTAL_BORDER_WIDTH, clientWidth + 2 * SASH_WIDTH - TOTAL_BORDER_WIDTH); + + resizableElement.domNode.style.top = offsetTop - TOTAL_BORDER_WIDTH + 'px'; + resizableElement.domNode.style.left = offsetLeft - SASH_WIDTH + TOTAL_BORDER_WIDTH + 'px'; + const horizontalSashLeft = TOTAL_BORDER_WIDTH + 'px'; + resizableElement.northSash.el.style.left = horizontalSashLeft; + resizableElement.southSash.el.style.left = horizontalSashLeft; + const horizontalSashWidth = clientWidth + TOTAL_BORDER_WIDTH + 'px'; + resizableElement.northSash.el.style.width = horizontalSashWidth; + resizableElement.southSash.el.style.width = horizontalSashWidth; + const verticalSashHeight = clientHeight + TOTAL_BORDER_WIDTH + 'px'; + resizableElement.eastSash.el.style.height = verticalSashHeight; + resizableElement.westSash.el.style.height = verticalSashHeight; + + if (renderingAbove) { + this._resizableOverlay.resizableElement().enableSashes(true, true, false, false); + const verticalSashTop = SASH_WIDTH - TOTAL_BORDER_WIDTH + 'px'; + resizableElement.eastSash.el.style.top = verticalSashTop; + resizableElement.westSash.el.style.top = verticalSashTop; + } else { + this._resizableOverlay.resizableElement().enableSashes(false, true, true, false); + const verticalSashTop = TOTAL_BORDER_WIDTH + 'px'; + resizableElement.eastSash.el.style.top = verticalSashTop; + resizableElement.westSash.el.style.top = verticalSashTop; + } + + + const maxRenderingWidth = this._widget.findMaxRenderingWidth(); + const maxRenderingHeight = this._widget.findMaxRenderingHeight(this._widget.renderingAbove); + + if (!maxRenderingWidth || !maxRenderingHeight) { + return; + } + + this._resizableOverlay.resizableElement().maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight); + this._editor.layoutOverlayWidget(this._resizableOverlay); + }, hide: () => this.hide() }; @@ -289,6 +400,13 @@ export class ContentHoverController extends Disposable { })); } + // Save the position of the tooltip, where the content hover should appear + const tooltipPosition: IPosition = { lineNumber: showAtPosition.lineNumber, column: showAtPosition.column }; + // The tooltip position is saved in the resizable overlay + this._resizableOverlay.tooltipPosition = tooltipPosition; + const persistedSize = this._resizableOverlay.findPersistedSize(); + + // The persisted size is used in the content hover widget this._widget.showAt(fragment, new ContentHoverVisibleData( colorPicker, showAtPosition, @@ -300,7 +418,9 @@ export class ContentHoverController extends Disposable { anchor.initialMousePosX, anchor.initialMousePosY, disposables - )); + ), persistedSize); + + this._resizableOverlay.show(); } else { disposables.dispose(); } @@ -437,18 +557,225 @@ class ContentHoverVisibleData { ) { } } +export class ResizableHoverOverlay extends Disposable implements IOverlayWidget { + + static readonly ID = 'editor.contrib.resizableHoverOverlay'; + // Creating a new resizable HTML element + private readonly _resizableElement: ResizableHTMLElement = this._register(new ResizableHTMLElement()); + // Map which maps from a text model URI, to a map from the stringified version of [offset, left] to the dom dimension + private readonly _persistedHoverWidgetSizes = new ResourceMap>(); + // Boolean which is indicating whether we are currently resizing or not + private _resizing: boolean = false; + // The current size of the resizable element + private _size: dom.Dimension | null = null; + // The initial height of the content hover when it first appears + private _initialHeight: number = -1; + // The initial top of the content hover widget when it first appears + private _initialTop: number = -1; + // The maximum rendering height + private _maxRenderingHeight: number | undefined = this._editor.getLayoutInfo().height; + // The maximum rendering width + private _maxRenderingWidth: number | undefined = this._editor.getLayoutInfo().width; + // The position where to render the hover + private _tooltipPosition: IPosition | null = null; + // Boolean indicating whether we are rendering above or below + private _renderingAbove: ContentWidgetPositionPreference = this._editor.getOption(EditorOption.hover).above ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; + // The hover widget which appears above the resizable overlay + private _hoverWidget: ContentHoverWidget | null = null; + + // Event emitter which fires whenever the resizable hover is resized + private readonly _onDidResize = new Emitter(); + readonly onDidResize: Event = this._onDidResize.event; + + constructor(private readonly _editor: ICodeEditor) { + super(); + this._resizableElement.minSize = new dom.Dimension(10, 24); + this._register(this._editor.onDidChangeModelContent((e) => { + const uri = this._editor.getModel()?.uri; + if (!uri || !this._persistedHoverWidgetSizes.has(uri)) { + return; + } + const persistedSizesForUri = this._persistedHoverWidgetSizes.get(uri)!; + const updatedPersistedSizesForUri = new Map(); + for (const change of e.changes) { + const changeOffset = change.rangeOffset; + const rangeLength = change.rangeLength; + const endOffset = changeOffset + rangeLength; + const textLength = change.text.length; + for (const key of persistedSizesForUri.keys()) { + const parsedKey = JSON.parse(key); + const tokenOffset = parsedKey[0]; + const tokenLength = parsedKey[1]; + if (endOffset < tokenOffset) { + const oldSize = persistedSizesForUri.get(key)!; + const newKey: [number, number] = [tokenOffset - rangeLength + textLength, tokenLength]; + updatedPersistedSizesForUri.set(JSON.stringify(newKey), oldSize); + } else if (changeOffset >= tokenOffset + tokenLength) { + updatedPersistedSizesForUri.set(key, persistedSizesForUri.get(key)!); + } + } + } + this._persistedHoverWidgetSizes.set(uri, updatedPersistedSizesForUri); + })); + this._register(this._resizableElement.onDidWillResize(() => { + this._resizing = true; + this._initialHeight = this._resizableElement.domNode.clientHeight; + this._initialTop = this._resizableElement.domNode.offsetTop; + })); + this._register(this._resizableElement.onDidResize(e => { + + let height = e.dimension.height; + let width = e.dimension.width; + const maxWidth = this._resizableElement.maxSize.width; + const maxHeight = this._resizableElement.maxSize.height; + + width = Math.min(maxWidth, width); + height = Math.min(maxHeight, height); + if (!this._maxRenderingHeight) { + return; + } + this._size = new dom.Dimension(width, height); + this._resizableElement.layout(height, width); + + // Update the top parameters only when we decided to render above + if (this._renderingAbove === ContentWidgetPositionPreference.ABOVE) { + this._resizableElement.domNode.style.top = this._initialTop - (height - this._initialHeight) + 'px'; + } + const horizontalSashWidth = width - 2 * SASH_WIDTH + 2 * TOTAL_BORDER_WIDTH + 'px'; + this._resizableElement.northSash.el.style.width = horizontalSashWidth; + this._resizableElement.southSash.el.style.width = horizontalSashWidth; + const verticalSashWidth = height - 2 * SASH_WIDTH + 2 * TOTAL_BORDER_WIDTH + 'px'; + this._resizableElement.eastSash.el.style.height = verticalSashWidth; + this._resizableElement.westSash.el.style.height = verticalSashWidth; + this._resizableElement.eastSash.el.style.top = TOTAL_BORDER_WIDTH + 'px'; + + // Fire the current dimension + this._onDidResize.fire({ dimension: this._size, done: false }); + + this._maxRenderingWidth = this._hoverWidget!.findMaxRenderingWidth(); + this._maxRenderingHeight = this._hoverWidget!.findMaxRenderingHeight(this._renderingAbove); + + if (!this._maxRenderingHeight || !this._maxRenderingWidth) { + return; + } + + this._resizableElement.maxSize = new dom.Dimension(this._maxRenderingWidth, this._maxRenderingHeight); + + // Persist the height only when the resizing has stopped + if (e.done) { + if (!this._editor.hasModel()) { + return; + } + const uri = this._editor.getModel().uri; + if (!uri || !this._tooltipPosition) { + return; + } + const persistedSize = new dom.Dimension(width, height); + const wordPosition = this._editor.getModel().getWordAtPosition(this._tooltipPosition); + if (!wordPosition) { + return; + } + const offset = this._editor.getModel().getOffsetAt({ lineNumber: this._tooltipPosition.lineNumber, column: wordPosition.startColumn }); + const length = wordPosition.word.length; + + // Suppose that the uri does not exist in the persisted widget hover sizes, then create a map + if (!this._persistedHoverWidgetSizes.get(uri)) { + const persistedWidgetSizesForUri = new Map([]); + persistedWidgetSizesForUri.set(JSON.stringify([offset, length]), persistedSize); + this._persistedHoverWidgetSizes.set(uri, persistedWidgetSizesForUri); + } else { + const persistedWidgetSizesForUri = this._persistedHoverWidgetSizes.get(uri)!; + persistedWidgetSizesForUri.set(JSON.stringify([offset, length]), persistedSize); + } + this._resizing = false; + } + + this._editor.layoutOverlayWidget(this); + this._editor.render(); + })); + } + + public get renderingAbove(): ContentWidgetPositionPreference { + return this._renderingAbove; + } + + public set renderingAbove(renderingAbove: ContentWidgetPositionPreference) { + this._renderingAbove = renderingAbove; + } + + public set hoverWidget(hoverWidget: ContentHoverWidget) { + this._hoverWidget = hoverWidget; + } + + public findPersistedSize(): dom.Dimension | undefined { + if (!this._tooltipPosition || !this._editor.hasModel()) { + return; + } + const wordPosition = this._editor.getModel().getWordAtPosition(this._tooltipPosition); + if (!wordPosition) { + return; + } + const offset = this._editor.getModel().getOffsetAt({ lineNumber: this._tooltipPosition.lineNumber, column: wordPosition.startColumn }); + const length = wordPosition.word.length; + const uri = this._editor.getModel().uri; + const persistedSizesForUri = this._persistedHoverWidgetSizes.get(uri); + if (!persistedSizesForUri) { + return; + } + return persistedSizesForUri.get(JSON.stringify([offset, length])); + } + + public isResizing(): boolean { + return this._resizing; + } + + public hide(): void { + this._resizing = false; + this._resizableElement.maxSize = new dom.Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + this._resizableElement.clearSashHoverState(); + this._editor.removeOverlayWidget(this); + } + + public resizableElement(): ResizableHTMLElement { + return this._resizableElement; + } + + public getId(): string { + return ResizableHoverOverlay.ID; + } + + public getDomNode(): HTMLElement { + return this._resizableElement.domNode; + } + + public set tooltipPosition(tooltipPosition: IPosition) { + this._tooltipPosition = tooltipPosition; + } + + public getPosition(): IOverlayWidgetPosition | null { + return null; + } + + public show(): void { + this._editor.addOverlayWidget(this); + this._resizableElement.domNode.style.zIndex = '49'; + this._resizableElement.domNode.style.position = 'fixed'; + } +} + export class ContentHoverWidget extends Disposable implements IContentWidget { static readonly ID = 'editor.contrib.contentHoverWidget'; public readonly allowEditorOverflow = true; + public readonly _hover: HoverWidget = this._register(new HoverWidget()); private readonly _hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(this._contextKeyService); private readonly _hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(this._contextKeyService); - private readonly _hover: HoverWidget = this._register(new HoverWidget()); private readonly _focusTracker = this._register(dom.trackFocus(this.getDomNode())); private readonly _horizontalScrollingBy: number = 30; private _visibleData: ContentHoverVisibleData | null = null; + private _renderingAbove: ContentWidgetPositionPreference = this._editor.getOption(EditorOption.hover).above ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; /** * Returns `null` if the hover is not visible. @@ -469,6 +796,14 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { return this._hoverVisibleKey.get() ?? false; } + public get renderingAbove(): ContentWidgetPositionPreference { + return this._renderingAbove; + } + + public set renderingAbove(renderingAbove: ContentWidgetPositionPreference) { + this._renderingAbove = renderingAbove; + } + constructor( private readonly _editor: ICodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -494,6 +829,75 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { })); } + public resize(size: dom.Dimension) { + // Removing the max height and max width here - the max size is controlled by the resizable overlay + this._hover.contentsDomNode.style.maxHeight = 'none'; + this._hover.contentsDomNode.style.maxWidth = 'none'; + + const width = size.width - 2 * SASH_WIDTH + TOTAL_BORDER_WIDTH + 'px'; + this._hover.containerDomNode.style.width = width; + this._hover.contentsDomNode.style.width = width; + const height = size.height - 2 * SASH_WIDTH + TOTAL_BORDER_WIDTH + 'px'; + this._hover.containerDomNode.style.height = height; + this._hover.contentsDomNode.style.height = height; + + const scrollDimensions = this._hover.scrollbar.getScrollDimensions(); + const hasHorizontalScrollbar = (scrollDimensions.scrollWidth > scrollDimensions.width); + if (hasHorizontalScrollbar) { + // When there is a horizontal scroll-bar use a different height to make the scroll-bar visible + const extraBottomPadding = `${this._hover.scrollbar.options.horizontalScrollbarSize}px`; + if (this._hover.contentsDomNode.style.paddingBottom !== extraBottomPadding) { + this._hover.contentsDomNode.style.paddingBottom = extraBottomPadding; + } + this._hover.contentsDomNode.style.height = size.height - 2 * SASH_WIDTH + TOTAL_BORDER_WIDTH - SCROLLBAR_WIDTH + 'px'; + } + + this._hover.scrollbar.scanDomNode(); + this._editor.layoutContentWidget(this); + this._editor.render(); + } + + public findMaxRenderingHeight(rendering: ContentWidgetPositionPreference): number | undefined { + + if (!this._editor || !this._editor.hasModel() || !this._visibleData?.showAtPosition) { + return; + } + const editorBox = dom.getDomNodePagePosition(this._editor.getDomNode()); + const mouseBox = this._editor.getScrolledVisiblePosition(this._visibleData.showAtPosition); + const bodyBox = dom.getClientArea(document.body); + let availableSpace: number; + + if (rendering === ContentWidgetPositionPreference.ABOVE) { + availableSpace = editorBox.top + mouseBox.top - 30; + } else { + const mouseBottom = editorBox.top + mouseBox!.top + mouseBox!.height; + availableSpace = bodyBox.height - mouseBottom; + } + + let divMaxHeight = SASH_WIDTH; + for (const childHtmlElement of this._hover.contentsDomNode.children) { + divMaxHeight += childHtmlElement.clientHeight; + } + + if (this._hover.contentsDomNode.clientWidth < this._hover.contentsDomNode.scrollWidth) { + divMaxHeight += SCROLLBAR_WIDTH; + } + + return Math.min(availableSpace, divMaxHeight); + } + + public findMaxRenderingWidth(): number | undefined { + if (!this._editor || !this._editor.hasModel()) { + return; + } + const editorBox = dom.getDomNodePagePosition(this._editor.getDomNode()); + const widthOfEditor = editorBox.width; + const leftOfEditor = editorBox.left; + const glyphMarginWidth = this._editor.getLayoutInfo().glyphMarginWidth; + const leftOfContainer = this._hover.containerDomNode.offsetLeft; + return widthOfEditor + leftOfEditor - leftOfContainer - glyphMarginWidth; + } + public override dispose(): void { this._editor.removeContentWidget(this); if (this._visibleData) { @@ -510,6 +914,10 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { return this._hover.containerDomNode; } + public getContentsDomNode(): HTMLElement { + return this._hover.contentsDomNode; + } + public getPosition(): IContentWidgetPosition | null { if (!this._visibleData) { return null; @@ -526,11 +934,7 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { return { position: this._visibleData.showAtPosition, secondaryPosition: this._visibleData.showAtSecondaryPosition, - preference: ( - preferAbove - ? [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW] - : [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] - ), + preference: ([this._renderingAbove]), positionAffinity: affinity }; } @@ -582,7 +986,12 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { codeClasses.forEach(node => this._editor.applyFontInfo(node)); } - public showAt(node: DocumentFragment, visibleData: ContentHoverVisibleData): void { + public showAt(node: DocumentFragment, visibleData: ContentHoverVisibleData, persistedSize: dom.Dimension | undefined): void { + + if (!this._editor || !this._editor.hasModel()) { + return; + } + this._setVisibleData(visibleData); this._hover.contentsDomNode.textContent = ''; @@ -590,15 +999,66 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { this._hover.contentsDomNode.style.paddingBottom = ''; this._updateFont(); - this.onContentsChanged(); + const containerDomNode = this.getDomNode(); + let height; - // Simply force a synchronous render on the editor - // such that the widget does not really render with left = '0px' - this._editor.render(); + // If the persisted size has already been found then set a maximum height and width + if (!persistedSize) { + this._hover.contentsDomNode.style.maxHeight = `${Math.max(this._editor.getLayoutInfo().height / 4, 250)}px`; + this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; + this.onContentsChanged(); + + // Simply force a synchronous render on the editor + // such that the widget does not really render with left = '0px' + this._editor.render(); + height = containerDomNode.clientHeight; + } + // When there is a persisted size then do not use a maximum height or width + else { + this._hover.contentsDomNode.style.maxHeight = 'none'; + this._hover.contentsDomNode.style.maxWidth = 'none'; + height = persistedSize.height; + } + + // The dimensions of the document in which we are displaying the hover + const bodyBox = dom.getClientArea(document.body); + // Hard-coded in the hover.css file as 1.5em or 24px + const minHeight = 24; + // The full height is already passed in as a parameter + const fullHeight = height; + const editorBox = dom.getDomNodePagePosition(this._editor.getDomNode()); + const mouseBox = this._editor.getScrolledVisiblePosition(visibleData.showAtPosition); + // Position where the editor box starts + the top of the mouse box relatve to the editor + mouse box height + const mouseBottom = editorBox.top + mouseBox.top + mouseBox.height; + // Total height of the box minus the position of the bottom of the mouse, this is the maximum height below the mouse position + const availableSpaceBelow = bodyBox.height - mouseBottom; + // Max height below is the minimum of the available space below and the full height of the widget + const maxHeightBelow = Math.min(availableSpaceBelow, fullHeight); + // The available space above the mouse position is the height of the top of the editor plus the top of the mouse box relative to the editor + const availableSpaceAbove = editorBox.top + mouseBox.top - 30; + const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight); + // We find the maximum height of the widget possible on the top or on the bottom + const maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow), fullHeight); + + if (height < minHeight) { + height = minHeight; + } + if (height > maxHeight) { + height = maxHeight; + } + + // Determining whether we should render above or not ideally + if (this._editor.getOption(EditorOption.hover).above) { + this._renderingAbove = height <= maxHeightAbove ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; + } else { + this._renderingAbove = height <= maxHeightBelow ? ContentWidgetPositionPreference.BELOW : ContentWidgetPositionPreference.ABOVE; + } // See https://github.com/microsoft/vscode/issues/140339 // TODO: Doing a second layout of the hover after force rendering the editor - this.onContentsChanged(); + if (!persistedSize) { + this.onContentsChanged(); + } if (visibleData.stoleFocus) { this._hover.containerDomNode.focus(); @@ -617,7 +1077,32 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { } } - public onContentsChanged(): void { + public onContentsChanged(persistedSize?: dom.Dimension | undefined): void { + + const containerDomNode = this.getDomNode(); + const contentsDomNode = this.getContentsDomNode(); + + // Suppose a persisted size is defined + if (persistedSize) { + + const widthMinusSash = Math.min(this.findMaxRenderingWidth() ?? Infinity, persistedSize.width - SASH_WIDTH); + const heightMinusSash = Math.min(this.findMaxRenderingHeight(this._renderingAbove) ?? Infinity, persistedSize.height - SASH_WIDTH); + + // Already setting directly the height and width parameters + containerDomNode.style.width = widthMinusSash + 'px'; + containerDomNode.style.height = heightMinusSash + 'px'; + contentsDomNode.style.width = widthMinusSash + 'px'; + contentsDomNode.style.height = heightMinusSash + 'px'; + + } else { + + // Otherwise the height and width are set to auto + containerDomNode.style.width = 'auto'; + containerDomNode.style.height = 'auto'; + contentsDomNode.style.width = 'auto'; + contentsDomNode.style.height = 'auto'; + } + this._editor.layoutContentWidget(this); this._hover.onContentsChanged(); @@ -626,8 +1111,19 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { if (hasHorizontalScrollbar) { // There is just a horizontal scrollbar const extraBottomPadding = `${this._hover.scrollbar.options.horizontalScrollbarSize}px`; + let reposition = false; if (this._hover.contentsDomNode.style.paddingBottom !== extraBottomPadding) { this._hover.contentsDomNode.style.paddingBottom = extraBottomPadding; + reposition = true; + } + const maxRenderingHeight = this.findMaxRenderingHeight(this._renderingAbove); + // Need the following code since we are using an exact height when using the persisted size. If not used the horizontal scrollbar would just not be visible. + if (persistedSize && maxRenderingHeight) { + containerDomNode.style.height = Math.min(maxRenderingHeight, persistedSize.height - SASH_WIDTH) + 'px'; + contentsDomNode.style.height = Math.min(maxRenderingHeight, persistedSize.height - SASH_WIDTH - SCROLLBAR_WIDTH) + 'px'; + reposition = true; + } + if (reposition) { this._editor.layoutContentWidget(this); this._hover.onContentsChanged(); } diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts index 10df5c935132a..c66ef300f627e 100644 --- a/src/vs/editor/contrib/hover/browser/hover.ts +++ b/src/vs/editor/contrib/hover/browser/hover.ts @@ -15,7 +15,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { ContentHoverWidget, ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHover'; +import { ContentHoverWidget, ContentHoverController, ResizableHoverOverlay } from 'vs/editor/contrib/hover/browser/contentHover'; import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHover'; import * as nls from 'vs/nls'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -114,7 +114,12 @@ export class ModesHoverController implements IEditorContribution { } if (target.type === MouseTargetType.OVERLAY_WIDGET && target.detail === MarginHoverWidget.ID) { - // mouse down on top of overlay hover widget + // mouse down on top of overlay margin hover widget + return; + } + + if (target.type === MouseTargetType.OVERLAY_WIDGET && target.detail === ResizableHoverOverlay.ID) { + // mouse down on top of the overlay hover widget return; } @@ -221,7 +226,9 @@ export class ModesHoverController implements IEditorContribution { this._hoverClicked = false; this._glyphWidget?.hide(); - this._contentWidget?.hide(); + if (!this._contentWidget?.resizableOverlay.isResizing()) { + this._contentWidget?.hide(); + } } private _getOrCreateContentWidget(): ContentHoverController {