diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 4b7929cd8ed..b6bb7881850 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -219,6 +219,13 @@ class ElementPath { && path[1] === PartFingerprint.OverlayWidgets ); } + + public static isChildOfOverflowingOverlayWidgets(path: Uint8Array): boolean { + return ( + path.length >= 1 + && path[0] === PartFingerprint.OverflowingOverlayWidgets + ); + } } export class HitTestContext { @@ -490,7 +497,7 @@ export class MouseTargetFactory { } // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(path)) { + if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) { return true; } @@ -545,7 +552,7 @@ export class MouseTargetFactory { let result: IMouseTarget | null = null; - if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) { + if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) { // We only render dom nodes inside the overflow guard or in the overflowing content widgets result = result || request.fulfillUnknown(); } @@ -579,7 +586,7 @@ export class MouseTargetFactory { private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) { + if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) { const widgetId = ctx.findAttribute(request.target, 'widgetId'); if (widgetId) { return request.fulfillOverlayWidget(widgetId); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index c048cd07ba9..0b8fc09755a 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -225,6 +225,22 @@ export const enum OverlayWidgetPositionPreference { */ TOP_CENTER } + + +/** + * Represents editor-relative coordinates of an overlay widget. + */ +export interface IOverlayWidgetPositionCoordinates { + /** + * The top position for the overlay widget, relative to the editor. + */ + top: number; + /** + * The left position for the overlay widget, relative to the editor. + */ + left: number; +} + /** * A position for rendering overlay widgets. */ @@ -232,12 +248,16 @@ export interface IOverlayWidgetPosition { /** * The position preference for the overlay widget. */ - preference: OverlayWidgetPositionPreference | null; + preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Render this overlay widget in a location where it could overflow the editor's view dom node. + */ + allowEditorOverflow?: boolean; /** * Get a unique identifier of the overlay widget. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 6df5d5c6d6f..5bc11e49309 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -194,7 +194,7 @@ export class View extends ViewEventHandler { this._viewParts.push(this._viewCursors); // Overlay widgets - this._overlayWidgets = new ViewOverlayWidgets(this._context); + this._overlayWidgets = new ViewOverlayWidgets(this._context, this.domNode); this._viewParts.push(this._overlayWidgets); const rulers = new Rulers(this._context); @@ -231,8 +231,10 @@ export class View extends ViewEventHandler { if (overflowWidgetsDomNode) { overflowWidgetsDomNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode.domNode); + overflowWidgetsDomNode.appendChild(this._overlayWidgets.overflowingOverlayWidgetsDomNode.domNode); } else { this.domNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode); + this.domNode.appendChild(this._overlayWidgets.overflowingOverlayWidgetsDomNode); } this._applyLayout(); diff --git a/src/vs/editor/browser/view/viewPart.ts b/src/vs/editor/browser/view/viewPart.ts index ad88b9f9551..8ae1a965ecf 100644 --- a/src/vs/editor/browser/view/viewPart.ts +++ b/src/vs/editor/browser/view/viewPart.ts @@ -33,6 +33,7 @@ export const enum PartFingerprint { OverflowingContentWidgets, OverflowGuard, OverlayWidgets, + OverflowingOverlayWidgets, ScrollableElement, TextArea, ViewLines, diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0165d4eb63f..9bc7a585356 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,17 +5,18 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; - preference: OverlayWidgetPositionPreference | null; + preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; domNode: FastDomNode; } @@ -25,17 +26,20 @@ interface IWidgetMap { export class ViewOverlayWidgets extends ViewPart { + private readonly _viewDomNode: FastDomNode; private _widgets: IWidgetMap; + private _viewDomNodeRect: dom.IDomNodePagePosition; private readonly _domNode: FastDomNode; - + public readonly overflowingOverlayWidgetsDomNode: FastDomNode; private _verticalScrollbarWidth: number; private _minimapWidth: number; private _horizontalScrollbarHeight: number; private _editorHeight: number; private _editorWidth: number; - constructor(context: ViewContext) { + constructor(context: ViewContext, viewDomNode: FastDomNode) { super(context); + this._viewDomNode = viewDomNode; const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); @@ -46,10 +50,15 @@ export class ViewOverlayWidgets extends ViewPart { this._horizontalScrollbarHeight = layoutInfo.horizontalScrollbarHeight; this._editorHeight = layoutInfo.height; this._editorWidth = layoutInfo.width; + this._viewDomNodeRect = { top: 0, left: 0, width: 0, height: 0 }; this._domNode = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._domNode, PartFingerprint.OverlayWidgets); this._domNode.setClassName('overlayWidgets'); + + this.overflowingOverlayWidgetsDomNode = createFastDomNode(document.createElement('div')); + PartFingerprints.write(this.overflowingOverlayWidgetsDomNode, PartFingerprint.OverflowingOverlayWidgets); + this.overflowingOverlayWidgetsDomNode.setClassName('overflowingOverlayWidgets'); } public override dispose(): void { @@ -89,13 +98,18 @@ export class ViewOverlayWidgets extends ViewPart { // This is sync because a widget wants to be in the dom domNode.setPosition('absolute'); domNode.setAttribute('widgetId', widget.getId()); - this._domNode.appendChild(domNode); + + if (widget.allowEditorOverflow) { + this.overflowingOverlayWidgetsDomNode.appendChild(domNode); + } else { + this._domNode.appendChild(domNode); + } this.setShouldRender(); this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { const widgetData = this._widgets[widget.getId()]; if (widgetData.preference === preference) { this._updateMaxMinWidth(); @@ -116,7 +130,7 @@ export class ViewOverlayWidgets extends ViewPart { const domNode = widgetData.domNode.domNode; delete this._widgets[widgetId]; - domNode.parentNode!.removeChild(domNode); + domNode.remove(); this.setShouldRender(); this._updateMaxMinWidth(); } @@ -154,11 +168,26 @@ export class ViewOverlayWidgets extends ViewPart { } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { domNode.setTop(0); domNode.domNode.style.right = '50%'; + } else { + const { top, left } = widgetData.preference; + const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); + if (fixedOverflowWidgets && widgetData.widget.allowEditorOverflow) { + // top, left are computed relative to the editor and we need them relative to the page + const editorBoundingBox = this._viewDomNodeRect!; + domNode.setTop(top + editorBoundingBox.top); + domNode.setLeft(left + editorBoundingBox.left); + domNode.setPosition('fixed'); + + } else { + domNode.setTop(top); + domNode.setLeft(left); + domNode.setPosition('absolute'); + } } } public prepareRender(ctx: RenderingContext): void { - // Nothing to read + this._viewDomNodeRect = dom.getDomNodePagePosition(this._viewDomNode.domNode); } public render(ctx: RestrictedRenderingContext): void { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts index 2b5db3ba531..381cec7fbb2 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { ICodeEditor, IOverlayWidget } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ResizableHTMLElement } from 'vs/base/browser/ui/resizable/resizable'; import * as nls from 'vs/nls'; @@ -263,6 +263,8 @@ interface TopLeftPosition { export class SuggestDetailsOverlay implements IOverlayWidget { + readonly allowEditorOverflow = true; + private readonly _disposables = new DisposableStore(); private readonly _resizable: ResizableHTMLElement; @@ -341,14 +343,13 @@ export class SuggestDetailsOverlay implements IOverlayWidget { return this._resizable.domNode; } - getPosition(): null { - return null; + getPosition(): IOverlayWidgetPosition | null { + return this._topLeft ? { preference: this._topLeft } : null; } show(): void { if (!this._added) { this._editor.addOverlayWidget(this); - this.getDomNode().style.position = 'fixed'; this._added = true; } } @@ -442,8 +443,18 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } } - this._applyTopLeft({ left: placement.left, top: alignAtTop ? placement.top : bottom - height }); - this.getDomNode().style.position = 'fixed'; + let { top, left } = placement; + if (!alignAtTop) { + top = bottom - height; + } + const editorDomNode = this._editor.getDomNode(); + if (editorDomNode) { + // get bounding rectangle of the suggest widget relative to the editor + const editorBoundingBox = editorDomNode.getBoundingClientRect(); + top -= editorBoundingBox.top; + left -= editorBoundingBox.left; + } + this._applyTopLeft({ left, top }); this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); @@ -455,7 +466,6 @@ export class SuggestDetailsOverlay implements IOverlayWidget { private _applyTopLeft(topLeft: TopLeftPosition): void { this._topLeft = topLeft; - this.getDomNode().style.left = `${this._topLeft.left}px`; - this.getDomNode().style.top = `${this._topLeft.top}px`; + this._editor.layoutOverlayWidget(this); } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index eb224d668bf..d082987b0d3 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5263,6 +5263,20 @@ declare namespace monaco.editor { TOP_CENTER = 2 } + /** + * Represents editor-relative coordinates of an overlay widget. + */ + export interface IOverlayWidgetPositionCoordinates { + /** + * The top position for the overlay widget, relative to the editor. + */ + top: number; + /** + * The left position for the overlay widget, relative to the editor. + */ + left: number; + } + /** * A position for rendering overlay widgets. */ @@ -5270,13 +5284,17 @@ declare namespace monaco.editor { /** * The position preference for the overlay widget. */ - preference: OverlayWidgetPositionPreference | null; + preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Render this overlay widget in a location where it could overflow the editor's view dom node. + */ + allowEditorOverflow?: boolean; /** * Get a unique identifier of the overlay widget. */