Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed issue Misalignment of suggestion details widget (https://github.com/microsoft/monaco-editor/issues/3373) #198730

Merged
merged 8 commits into from
Dec 14, 2023
13 changes: 10 additions & 3 deletions src/vs/editor/browser/controller/mouseTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 21 additions & 1 deletion src/vs/editor/browser/editorBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,19 +225,39 @@ 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.
*/
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.
*/
Expand Down
4 changes: 3 additions & 1 deletion src/vs/editor/browser/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/browser/view/viewPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const enum PartFingerprint {
OverflowingContentWidgets,
OverflowGuard,
OverlayWidgets,
OverflowingOverlayWidgets,
ScrollableElement,
TextArea,
ViewLines,
Expand Down
45 changes: 37 additions & 8 deletions src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>;
}

Expand All @@ -25,17 +26,20 @@ interface IWidgetMap {

export class ViewOverlayWidgets extends ViewPart {

private readonly _viewDomNode: FastDomNode<HTMLElement>;
private _widgets: IWidgetMap;
private _viewDomNodeRect: dom.IDomNodePagePosition;
private readonly _domNode: FastDomNode<HTMLElement>;

public readonly overflowingOverlayWidgetsDomNode: FastDomNode<HTMLElement>;
private _verticalScrollbarWidth: number;
private _minimapWidth: number;
private _horizontalScrollbarHeight: number;
private _editorHeight: number;
private _editorWidth: number;

constructor(context: ViewContext) {
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>) {
super(context);
this._viewDomNode = viewDomNode;

const options = this._context.configuration.options;
const layoutInfo = options.get(EditorOption.layoutInfo);
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 {
Expand Down
26 changes: 18 additions & 8 deletions src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -263,6 +263,8 @@ interface TopLeftPosition {

export class SuggestDetailsOverlay implements IOverlayWidget {

readonly allowEditorOverflow = true;

private readonly _disposables = new DisposableStore();
private readonly _resizable: ResizableHTMLElement;

Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);

Expand All @@ -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);
}
}
20 changes: 19 additions & 1 deletion src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5263,20 +5263,38 @@ 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.
*/
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.
*/
Expand Down
Loading