Skip to content

Commit

Permalink
Merge pull request #198730 from GenericTSDeveloper/suggestVSCode
Browse files Browse the repository at this point in the history
Fixed issue Misalignment of suggestion details widget (microsoft/monaco-editor#3373)
  • Loading branch information
jrieken authored Dec 14, 2023
2 parents 73d3045 + f3f0dcf commit 1ec656b
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 22 deletions.
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

0 comments on commit 1ec656b

Please sign in to comment.