mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
Improve hover positioning by introducing a secondary anchor
This commit is contained in:
@@ -132,10 +132,11 @@ export interface IContentWidgetPosition {
|
||||
*/
|
||||
position: IPosition | null;
|
||||
/**
|
||||
* Optionally, a range can be provided to further
|
||||
* define the position of the content widget.
|
||||
* Optionally, a secondary position can be provided to further
|
||||
* define the position of the content widget. The secondary position
|
||||
* must have the same line number as the primary position.
|
||||
*/
|
||||
range?: IRange | null;
|
||||
secondaryPosition?: IPosition | null;
|
||||
/**
|
||||
* Placement preference for position, in order of preference.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,6 @@ import { SelectionsOverlay } from 'vs/editor/browser/viewParts/selections/select
|
||||
import { ViewCursors } from 'vs/editor/browser/viewParts/viewCursors/viewCursors';
|
||||
import { ViewZones } from 'vs/editor/browser/viewParts/viewZones/viewZones';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
|
||||
import { RenderingContext } from 'vs/editor/browser/view/renderingContext';
|
||||
@@ -502,15 +501,13 @@ export class View extends ViewEventHandler {
|
||||
}
|
||||
|
||||
public layoutContentWidget(widgetData: IContentWidgetData): void {
|
||||
let newRange = widgetData.position ? widgetData.position.range || null : null;
|
||||
if (newRange === null) {
|
||||
const newPosition = widgetData.position ? widgetData.position.position : null;
|
||||
if (newPosition !== null) {
|
||||
newRange = new Range(newPosition.lineNumber, newPosition.column, newPosition.lineNumber, newPosition.column);
|
||||
}
|
||||
}
|
||||
const newPreference = widgetData.position ? widgetData.position.preference : null;
|
||||
this._contentWidgets.setWidgetPosition(widgetData.widget, newRange, newPreference, widgetData.position?.positionAffinity ?? null);
|
||||
this._contentWidgets.setWidgetPosition(
|
||||
widgetData.widget,
|
||||
widgetData.position?.position ?? null,
|
||||
widgetData.position?.secondaryPosition ?? null,
|
||||
widgetData.position?.preference ?? null,
|
||||
widgetData.position?.positionAffinity ?? null
|
||||
);
|
||||
this._scheduleRender();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,28 +7,15 @@ import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { ContentWidgetPositionPreference, IContentWidget } from 'vs/editor/browser/editorBrowser';
|
||||
import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { Constants } from 'vs/base/common/uint';
|
||||
import { HorizontalRange, LineVisibleRanges, RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
|
||||
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 { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IDimension } from 'vs/editor/common/core/dimension';
|
||||
import { PositionAffinity } from 'vs/editor/common/model';
|
||||
|
||||
|
||||
class Coordinate {
|
||||
_coordinateBrand: void = undefined;
|
||||
|
||||
public readonly top: number;
|
||||
public readonly left: number;
|
||||
|
||||
constructor(top: number, left: number) {
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
}
|
||||
}
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { IViewModel } from 'vs/editor/common/viewModel';
|
||||
|
||||
export class ViewContentWidgets extends ViewPart {
|
||||
|
||||
@@ -113,9 +100,9 @@ export class ViewContentWidgets extends ViewPart {
|
||||
this.setShouldRender();
|
||||
}
|
||||
|
||||
public setWidgetPosition(widget: IContentWidget, range: IRange | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
|
||||
public setWidgetPosition(widget: IContentWidget, primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
|
||||
const myWidget = this._widgets[widget.getId()];
|
||||
myWidget.setPosition(range, preference, affinity);
|
||||
myWidget.setPosition(primaryAnchor, secondaryAnchor, preference, affinity);
|
||||
|
||||
this.setShouldRender();
|
||||
}
|
||||
@@ -166,11 +153,11 @@ export class ViewContentWidgets extends ViewPart {
|
||||
interface IBoxLayoutResult {
|
||||
fitsAbove: boolean;
|
||||
aboveTop: number;
|
||||
aboveLeft: number;
|
||||
|
||||
fitsBelow: boolean;
|
||||
belowTop: number;
|
||||
belowLeft: number;
|
||||
|
||||
left: number;
|
||||
}
|
||||
|
||||
interface IRenderData {
|
||||
@@ -193,9 +180,9 @@ class Widget {
|
||||
private _contentLeft: number;
|
||||
private _lineHeight: number;
|
||||
|
||||
private _range: IRange | null;
|
||||
private _primaryAnchor: PositionPair = new PositionPair(null, null);
|
||||
private _secondaryAnchor: PositionPair = new PositionPair(null, null);
|
||||
private _affinity: PositionAffinity | null;
|
||||
private _viewRange: Range | null;
|
||||
private _preference: ContentWidgetPositionPreference[] | null;
|
||||
private _cachedDomNodeOffsetWidth: number;
|
||||
private _cachedDomNodeOffsetHeight: number;
|
||||
@@ -222,8 +209,6 @@ class Widget {
|
||||
this._contentLeft = layoutInfo.contentLeft;
|
||||
this._lineHeight = options.get(EditorOption.lineHeight);
|
||||
|
||||
this._range = null;
|
||||
this._viewRange = null;
|
||||
this._affinity = null;
|
||||
this._preference = [];
|
||||
this._cachedDomNodeOffsetWidth = -1;
|
||||
@@ -251,20 +236,25 @@ class Widget {
|
||||
}
|
||||
|
||||
public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): void {
|
||||
this._setPosition(this._range, this._affinity);
|
||||
this._setPosition(this._affinity, this._primaryAnchor.modelPosition, this._secondaryAnchor.modelPosition);
|
||||
}
|
||||
|
||||
private _setPosition(range: IRange | null, affinity: PositionAffinity | null): void {
|
||||
this._range = range;
|
||||
this._viewRange = null;
|
||||
private _setPosition(affinity: PositionAffinity | null, primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null): void {
|
||||
this._affinity = affinity;
|
||||
this._primaryAnchor = getValidPositionPair(primaryAnchor, this._context.viewModel, this._affinity);
|
||||
this._secondaryAnchor = getValidPositionPair(secondaryAnchor, this._context.viewModel, this._affinity);
|
||||
|
||||
if (this._range) {
|
||||
// Do not trust that widgets give a valid position
|
||||
const validModelRange = this._context.viewModel.model.validateRange(this._range);
|
||||
if (this._context.viewModel.coordinatesConverter.modelPositionIsVisible(validModelRange.getStartPosition()) || this._context.viewModel.coordinatesConverter.modelPositionIsVisible(validModelRange.getEndPosition())) {
|
||||
this._viewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(validModelRange, this._affinity ?? undefined);
|
||||
function getValidPositionPair(position: IPosition | null, viewModel: IViewModel, affinity: PositionAffinity | null): PositionPair {
|
||||
if (!position) {
|
||||
return new PositionPair(null, null);
|
||||
}
|
||||
// Do not trust that widgets give a valid position
|
||||
const validModelPosition = viewModel.model.validatePosition(position);
|
||||
if (viewModel.coordinatesConverter.modelPositionIsVisible(validModelPosition)) {
|
||||
const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(validModelPosition, affinity ?? undefined);
|
||||
return new PositionPair(position, viewPosition);
|
||||
}
|
||||
return new PositionPair(position, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +266,10 @@ class Widget {
|
||||
);
|
||||
}
|
||||
|
||||
public setPosition(range: IRange | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
|
||||
this._setPosition(range, affinity);
|
||||
public setPosition(primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
|
||||
this._setPosition(affinity, primaryAnchor, secondaryAnchor);
|
||||
this._preference = preference;
|
||||
if (this._viewRange && this._preference && this._preference.length > 0) {
|
||||
if (this._primaryAnchor.viewPosition && this._preference && this._preference.length > 0) {
|
||||
// this content widget would like to be visible if possible
|
||||
// we change it from `display:none` to `display:block` even if it
|
||||
// might be outside the viewport such that we can measure its size
|
||||
@@ -292,15 +282,15 @@ class Widget {
|
||||
this._cachedDomNodeOffsetHeight = -1;
|
||||
}
|
||||
|
||||
private _layoutBoxInViewport(aboveAnchor: Coordinate, belowAnchor: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
|
||||
private _layoutBoxInViewport(anchor: AnchorCoordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
|
||||
// Our visible box is split horizontally by the current line => 2 boxes
|
||||
|
||||
// a) the box above the line
|
||||
const aboveLineTop = aboveAnchor.top;
|
||||
const aboveLineTop = anchor.top;
|
||||
const heightAvailableAboveLine = aboveLineTop;
|
||||
|
||||
// b) the box under the line
|
||||
const underLineTop = belowAnchor.top;
|
||||
const underLineTop = anchor.top + anchor.height;
|
||||
const heightAvailableUnderLine = ctx.viewportHeight - underLineTop;
|
||||
|
||||
const aboveTop = aboveLineTop - height;
|
||||
@@ -309,30 +299,15 @@ class Widget {
|
||||
const fitsBelow = (heightAvailableUnderLine >= height);
|
||||
|
||||
// And its left
|
||||
let actualAboveLeft = aboveAnchor.left;
|
||||
let actualBelowLeft = belowAnchor.left;
|
||||
if (actualAboveLeft + width > ctx.scrollLeft + ctx.viewportWidth) {
|
||||
actualAboveLeft = ctx.scrollLeft + ctx.viewportWidth - width;
|
||||
let left = anchor.left;
|
||||
if (left + width > ctx.scrollLeft + ctx.viewportWidth) {
|
||||
left = ctx.scrollLeft + ctx.viewportWidth - width;
|
||||
}
|
||||
if (actualBelowLeft + width > ctx.scrollLeft + ctx.viewportWidth) {
|
||||
actualBelowLeft = ctx.scrollLeft + ctx.viewportWidth - width;
|
||||
}
|
||||
if (actualAboveLeft < ctx.scrollLeft) {
|
||||
actualAboveLeft = ctx.scrollLeft;
|
||||
}
|
||||
if (actualBelowLeft < ctx.scrollLeft) {
|
||||
actualBelowLeft = ctx.scrollLeft;
|
||||
if (left < ctx.scrollLeft) {
|
||||
left = ctx.scrollLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
fitsAbove: fitsAbove,
|
||||
aboveTop: aboveTop,
|
||||
aboveLeft: actualAboveLeft,
|
||||
|
||||
fitsBelow: fitsBelow,
|
||||
belowTop: belowTop,
|
||||
belowLeft: actualBelowLeft,
|
||||
};
|
||||
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
|
||||
}
|
||||
|
||||
private _layoutHorizontalSegmentInPage(windowSize: dom.Dimension, domNodePosition: dom.IDomNodePagePosition, left: number, width: number): [number, number] {
|
||||
@@ -357,17 +332,16 @@ class Widget {
|
||||
return [left, absoluteLeft];
|
||||
}
|
||||
|
||||
private _layoutBoxInPage(aboveAnchor: Coordinate, belowAnchor: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult | null {
|
||||
const aboveTop = aboveAnchor.top - height;
|
||||
const belowTop = belowAnchor.top;
|
||||
private _layoutBoxInPage(anchor: AnchorCoordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult | null {
|
||||
const aboveTop = anchor.top - height;
|
||||
const belowTop = anchor.top + anchor.height;
|
||||
|
||||
const domNodePosition = dom.getDomNodePagePosition(this._viewDomNode.domNode);
|
||||
const absoluteAboveTop = domNodePosition.top + aboveTop - window.scrollY;
|
||||
const absoluteBelowTop = domNodePosition.top + belowTop - window.scrollY;
|
||||
|
||||
const windowSize = dom.getClientArea(document.body);
|
||||
const [aboveLeft, absoluteAboveLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, aboveAnchor.left - ctx.scrollLeft + this._contentLeft, width);
|
||||
const [belowLeft, absoluteBelowLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, belowAnchor.left - ctx.scrollLeft + this._contentLeft, width);
|
||||
const [left, absoluteAboveLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, anchor.left - ctx.scrollLeft + this._contentLeft, width);
|
||||
|
||||
// Leave some clearance to the top/bottom
|
||||
const TOP_PADDING = 22;
|
||||
@@ -380,21 +354,13 @@ class Widget {
|
||||
return {
|
||||
fitsAbove,
|
||||
aboveTop: Math.max(absoluteAboveTop, TOP_PADDING),
|
||||
aboveLeft: absoluteAboveLeft,
|
||||
fitsBelow,
|
||||
belowTop: absoluteBelowTop,
|
||||
belowLeft: absoluteBelowLeft
|
||||
left: absoluteAboveLeft
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fitsAbove,
|
||||
aboveTop,
|
||||
aboveLeft,
|
||||
fitsBelow,
|
||||
belowTop,
|
||||
belowLeft
|
||||
};
|
||||
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
|
||||
}
|
||||
|
||||
private _prepareRenderWidgetAtExactPositionOverflowing(topLeft: Coordinate): Coordinate {
|
||||
@@ -402,43 +368,47 @@ class Widget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the above and below anchors
|
||||
* Compute the coordinates above and below the primary and secondary anchors.
|
||||
* The content widget *must* touch the primary anchor.
|
||||
* The content widget should touch if possible the secondary anchor.
|
||||
*/
|
||||
private _getAnchors(ctx: RenderingContext): [Coordinate | null, Coordinate | null] {
|
||||
if (!this._viewRange) {
|
||||
return [null, null];
|
||||
}
|
||||
private _getAnchorsCoordinates(ctx: RenderingContext): { primary: AnchorCoordinate | null; secondary: AnchorCoordinate | null } {
|
||||
const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity, this._lineHeight);
|
||||
const secondaryViewPosition = (this._secondaryAnchor.viewPosition?.lineNumber === this._primaryAnchor.viewPosition?.lineNumber ? this._secondaryAnchor.viewPosition : null);
|
||||
const secondary = getCoordinates(secondaryViewPosition, this._affinity, this._lineHeight);
|
||||
return { primary, secondary };
|
||||
|
||||
const visibleRangesForRange = ctx.linesVisibleRangesForRange(this._viewRange, false);
|
||||
const aboveAnchor = getCoordinate(LineVisibleRanges.firstLine(visibleRangesForRange), this._affinity, this._viewRange.startColumn, 0);
|
||||
const belowAnchor = getCoordinate(LineVisibleRanges.lastLine(visibleRangesForRange), this._affinity, this._viewRange.startColumn, this._lineHeight);
|
||||
return [aboveAnchor, belowAnchor];
|
||||
|
||||
function getCoordinate(line: LineVisibleRanges | null, affinity: PositionAffinity | null, startColumn: number, deltaTop: number): Coordinate | null {
|
||||
if (!line) {
|
||||
function getCoordinates(position: Position | null, affinity: PositionAffinity | null, lineHeight: number): AnchorCoordinate | null {
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
const left = getLeft(line.ranges, affinity, startColumn);
|
||||
const top = ctx.getVerticalOffsetForLineNumber(line.lineNumber) - ctx.scrollTop;
|
||||
return new Coordinate(top + deltaTop, left);
|
||||
|
||||
const horizontalPosition = ctx.visibleRangeForPosition(position);
|
||||
if (!horizontalPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Left-align widgets that should appear :before content
|
||||
const left = (position.column === 1 && affinity === PositionAffinity.LeftOfInjectedText ? 0 : horizontalPosition.left);
|
||||
const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop;
|
||||
return new AnchorCoordinate(top, left, lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private _reduceAnchorCoordinates(primary: AnchorCoordinate, secondary: AnchorCoordinate | null, width: number): AnchorCoordinate {
|
||||
if (!secondary) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the left-most position from `ranges`.
|
||||
*/
|
||||
function getLeft(ranges: HorizontalRange[], affinity: PositionAffinity | null, startColumn: number): number {
|
||||
// Left-align widgets that should appear :before content
|
||||
if (startColumn === 1 && affinity === PositionAffinity.LeftOfInjectedText) {
|
||||
return 0;
|
||||
}
|
||||
let result = Constants.MAX_SAFE_SMALL_INTEGER;
|
||||
for (const range of ranges) {
|
||||
if (range.left < result) {
|
||||
result = range.left;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
const fontInfo = this._context.configuration.options.get(EditorOption.fontInfo);
|
||||
|
||||
let left = secondary.left;
|
||||
if (left < primary.left) {
|
||||
left = Math.max(left, primary.left - width + fontInfo.typicalFullwidthCharacterWidth);
|
||||
} else {
|
||||
left = Math.min(left, primary.left + width - fontInfo.typicalFullwidthCharacterWidth);
|
||||
}
|
||||
return new AnchorCoordinate(primary.top, left, primary.height);
|
||||
}
|
||||
|
||||
private _prepareRenderWidget(ctx: RenderingContext): IRenderData | null {
|
||||
@@ -446,8 +416,8 @@ class Widget {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [aboveAnchor, belowAnchor] = this._getAnchors(ctx);
|
||||
if (!aboveAnchor || !belowAnchor) {
|
||||
const { primary, secondary } = this._getAnchorsCoordinates(ctx);
|
||||
if (!primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -468,11 +438,13 @@ class Widget {
|
||||
}
|
||||
}
|
||||
|
||||
const anchor = this._reduceAnchorCoordinates(primary, secondary, this._cachedDomNodeOffsetWidth);
|
||||
|
||||
let placement: IBoxLayoutResult | null;
|
||||
if (this.allowEditorOverflow) {
|
||||
placement = this._layoutBoxInPage(aboveAnchor, belowAnchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
|
||||
placement = this._layoutBoxInPage(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
|
||||
} else {
|
||||
placement = this._layoutBoxInViewport(aboveAnchor, belowAnchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
|
||||
placement = this._layoutBoxInViewport(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
|
||||
}
|
||||
|
||||
// Do two passes, first for perfect fit, second picks first option
|
||||
@@ -485,7 +457,7 @@ class Widget {
|
||||
return null;
|
||||
}
|
||||
if (pass === 2 || placement.fitsAbove) {
|
||||
return { coordinate: new Coordinate(placement.aboveTop, placement.aboveLeft), position: ContentWidgetPositionPreference.ABOVE };
|
||||
return { coordinate: new Coordinate(placement.aboveTop, placement.left), position: ContentWidgetPositionPreference.ABOVE };
|
||||
}
|
||||
} else if (pref === ContentWidgetPositionPreference.BELOW) {
|
||||
if (!placement) {
|
||||
@@ -493,13 +465,13 @@ class Widget {
|
||||
return null;
|
||||
}
|
||||
if (pass === 2 || placement.fitsBelow) {
|
||||
return { coordinate: new Coordinate(placement.belowTop, placement.belowLeft), position: ContentWidgetPositionPreference.BELOW };
|
||||
return { coordinate: new Coordinate(placement.belowTop, placement.left), position: ContentWidgetPositionPreference.BELOW };
|
||||
}
|
||||
} else {
|
||||
if (this.allowEditorOverflow) {
|
||||
return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(aboveAnchor), position: ContentWidgetPositionPreference.EXACT };
|
||||
return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), position: ContentWidgetPositionPreference.EXACT };
|
||||
} else {
|
||||
return { coordinate: aboveAnchor, position: ContentWidgetPositionPreference.EXACT };
|
||||
return { coordinate: new Coordinate(anchor.top, anchor.left), position: ContentWidgetPositionPreference.EXACT };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,11 +484,11 @@ class Widget {
|
||||
* On this first pass, we ensure that the content widget (if it is in the viewport) has the max width set correctly.
|
||||
*/
|
||||
public onBeforeRender(viewportData: ViewportData): void {
|
||||
if (!this._viewRange || !this._preference) {
|
||||
if (!this._primaryAnchor.viewPosition || !this._preference) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._viewRange.endLineNumber < viewportData.startLineNumber || this._viewRange.startLineNumber > viewportData.endLineNumber) {
|
||||
if (this._primaryAnchor.viewPosition.lineNumber < viewportData.startLineNumber || this._primaryAnchor.viewPosition.lineNumber > viewportData.endLineNumber) {
|
||||
// Outside of viewport
|
||||
return;
|
||||
}
|
||||
@@ -564,6 +536,32 @@ class Widget {
|
||||
}
|
||||
}
|
||||
|
||||
class PositionPair {
|
||||
constructor(
|
||||
public readonly modelPosition: IPosition | null,
|
||||
public readonly viewPosition: Position | null
|
||||
) { }
|
||||
}
|
||||
|
||||
class Coordinate {
|
||||
_coordinateBrand: void = undefined;
|
||||
|
||||
constructor(
|
||||
public readonly top: number,
|
||||
public readonly left: number
|
||||
) { }
|
||||
}
|
||||
|
||||
class AnchorCoordinate {
|
||||
_anchorCoordinateBrand: void = undefined;
|
||||
|
||||
constructor(
|
||||
public readonly top: number,
|
||||
public readonly left: number,
|
||||
public readonly height: number
|
||||
) { }
|
||||
}
|
||||
|
||||
function safeInvoke<T extends (...args: any[]) => any>(fn: T, thisArg: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> | null {
|
||||
try {
|
||||
return fn.call(thisArg, ...args);
|
||||
|
||||
@@ -24,7 +24,6 @@ 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 { Constants } from 'vs/base/common/uint';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -241,7 +240,7 @@ export class ContentHoverController extends Disposable {
|
||||
}
|
||||
|
||||
private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void {
|
||||
const { showAtPosition, showAtRange, highlightRange } = ContentHoverController.computeHoverRanges(this._editor, anchor.range, messages);
|
||||
const { showAtPosition, showAtSecondaryPosition, highlightRange } = ContentHoverController.computeHoverRanges(this._editor, anchor.range, messages);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService));
|
||||
@@ -284,7 +283,7 @@ export class ContentHoverController extends Disposable {
|
||||
this._widget.showAt(fragment, new ContentHoverVisibleData(
|
||||
colorPicker,
|
||||
showAtPosition,
|
||||
showAtRange,
|
||||
showAtSecondaryPosition,
|
||||
this._editor.getOption(EditorOption.hover).above,
|
||||
this._computer.shouldFocus,
|
||||
isBeforeContent,
|
||||
@@ -304,21 +303,17 @@ export class ContentHoverController extends Disposable {
|
||||
|
||||
public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, messages: IHoverPart[]) {
|
||||
let startColumnBoundary = 1;
|
||||
let endColumnBoundary = Constants.MAX_SAFE_SMALL_INTEGER;
|
||||
if (editor.hasModel()) {
|
||||
// Ensure the range is on the current view line
|
||||
const viewModel = editor._getViewModel();
|
||||
const coordinatesConverter = viewModel.coordinatesConverter;
|
||||
const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange);
|
||||
const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, viewModel.getLineMinColumn(anchorViewRange.startLineNumber));
|
||||
const anchorViewRangeEnd = new Position(anchorViewRange.endLineNumber, viewModel.getLineMaxColumn(anchorViewRange.endLineNumber));
|
||||
startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column;
|
||||
endColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeEnd).column;
|
||||
}
|
||||
// The anchor range is always on a single line
|
||||
const anchorLineNumber = anchorRange.startLineNumber;
|
||||
let renderStartColumn = anchorRange.startColumn;
|
||||
let renderEndColumn = anchorRange.endColumn;
|
||||
let highlightRange: Range = messages[0].range;
|
||||
let forceShowAtRange: Range | null = null;
|
||||
|
||||
@@ -327,7 +322,6 @@ export class ContentHoverController extends Disposable {
|
||||
if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) {
|
||||
// this message has a range that is completely sitting on the line of the anchor
|
||||
renderStartColumn = Math.max(Math.min(renderStartColumn, msg.range.startColumn), startColumnBoundary);
|
||||
renderEndColumn = Math.min(Math.max(renderEndColumn, msg.range.endColumn), endColumnBoundary);
|
||||
}
|
||||
if (msg.forceShowAtRange) {
|
||||
forceShowAtRange = msg.range;
|
||||
@@ -335,8 +329,8 @@ export class ContentHoverController extends Disposable {
|
||||
}
|
||||
|
||||
return {
|
||||
showAtPosition: forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorRange.startLineNumber, renderStartColumn),
|
||||
showAtRange: forceShowAtRange ? forceShowAtRange : new Range(anchorLineNumber, renderStartColumn, anchorLineNumber, renderEndColumn),
|
||||
showAtPosition: forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, anchorRange.startColumn),
|
||||
showAtSecondaryPosition: forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, renderStartColumn),
|
||||
highlightRange
|
||||
};
|
||||
}
|
||||
@@ -382,7 +376,7 @@ class ContentHoverVisibleData {
|
||||
constructor(
|
||||
public readonly colorPicker: IEditorHoverColorPickerWidget | null,
|
||||
public readonly showAtPosition: Position,
|
||||
public readonly showAtRange: Range,
|
||||
public readonly showAtSecondaryPosition: Position,
|
||||
public readonly preferAbove: boolean,
|
||||
public readonly stoleFocus: boolean,
|
||||
public readonly isBeforeContent: boolean,
|
||||
@@ -463,7 +457,7 @@ export class ContentHoverWidget extends Disposable implements IContentWidget {
|
||||
|
||||
return {
|
||||
position: this._visibleData.showAtPosition,
|
||||
range: this._visibleData.showAtRange,
|
||||
secondaryPosition: this._visibleData.showAtSecondaryPosition,
|
||||
preference: (
|
||||
preferAbove
|
||||
? [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]
|
||||
|
||||
7
src/vs/monaco.d.ts
vendored
7
src/vs/monaco.d.ts
vendored
@@ -4780,10 +4780,11 @@ declare namespace monaco.editor {
|
||||
*/
|
||||
position: IPosition | null;
|
||||
/**
|
||||
* Optionally, a range can be provided to further
|
||||
* define the position of the content widget.
|
||||
* Optionally, a secondary position can be provided to further
|
||||
* define the position of the content widget. The secondary position
|
||||
* must have the same line number as the primary position.
|
||||
*/
|
||||
range?: IRange | null;
|
||||
secondaryPosition?: IPosition | null;
|
||||
/**
|
||||
* Placement preference for position, in order of preference.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user