Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';
8
import { ContentWidgetPositionPreference, IContentWidget, IContentWidgetRenderedCoordinate } from '../../editorBrowser.js';
9
import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js';
10
import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';
11
import { ViewContext } from '../../../common/viewModel/viewContext.js';
12
import * as viewEvents from '../../../common/viewEvents.js';
13
import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
14
import { EditorOption } from '../../../common/config/editorOptions.js';
15
import { IDimension } from '../../../common/core/2d/dimension.js';
16
import { PositionAffinity } from '../../../common/model.js';
17
import { IPosition, Position } from '../../../common/core/position.js';
18
import { IViewModel } from '../../../common/viewModel.js';
19
20
/**
21
* This view part is responsible for rendering the content widgets, which are
22
* used for rendering elements that are associated to an editor position,
23
* such as suggestions or the parameter hints.
24
*/
25
export class ViewContentWidgets extends ViewPart {
26
27
private readonly _viewDomNode: FastDomNode<HTMLElement>;
28
private _widgets: { [key: string]: Widget };
29
30
public domNode: FastDomNode<HTMLElement>;
31
public overflowingContentWidgetsDomNode: FastDomNode<HTMLElement>;
32
33
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>) {
34
super(context);
35
this._viewDomNode = viewDomNode;
36
this._widgets = {};
37
38
this.domNode = createFastDomNode(document.createElement('div'));
39
PartFingerprints.write(this.domNode, PartFingerprint.ContentWidgets);
40
this.domNode.setClassName('contentWidgets');
41
this.domNode.setPosition('absolute');
42
this.domNode.setTop(0);
43
44
this.overflowingContentWidgetsDomNode = createFastDomNode(document.createElement('div'));
45
PartFingerprints.write(this.overflowingContentWidgetsDomNode, PartFingerprint.OverflowingContentWidgets);
46
this.overflowingContentWidgetsDomNode.setClassName('overflowingContentWidgets');
47
}
48
49
public override dispose(): void {
50
super.dispose();
51
this._widgets = {};
52
}
53
54
// --- begin event handlers
55
56
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
57
const keys = Object.keys(this._widgets);
58
for (const widgetId of keys) {
59
this._widgets[widgetId].onConfigurationChanged(e);
60
}
61
return true;
62
}
63
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
64
// true for inline decorations that can end up relayouting text
65
return true;
66
}
67
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
68
return true;
69
}
70
public override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean {
71
this._updateAnchorsViewPositions();
72
return true;
73
}
74
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
75
this._updateAnchorsViewPositions();
76
return true;
77
}
78
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
79
this._updateAnchorsViewPositions();
80
return true;
81
}
82
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
83
this._updateAnchorsViewPositions();
84
return true;
85
}
86
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
87
return true;
88
}
89
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
90
return true;
91
}
92
93
// ---- end view event handlers
94
95
private _updateAnchorsViewPositions(): void {
96
const keys = Object.keys(this._widgets);
97
for (const widgetId of keys) {
98
this._widgets[widgetId].updateAnchorViewPosition();
99
}
100
}
101
102
public addWidget(_widget: IContentWidget): void {
103
const myWidget = new Widget(this._context, this._viewDomNode, _widget);
104
this._widgets[myWidget.id] = myWidget;
105
106
if (myWidget.allowEditorOverflow) {
107
this.overflowingContentWidgetsDomNode.appendChild(myWidget.domNode);
108
} else {
109
this.domNode.appendChild(myWidget.domNode);
110
}
111
112
this.setShouldRender();
113
}
114
115
public setWidgetPosition(widget: IContentWidget, primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
116
const myWidget = this._widgets[widget.getId()];
117
myWidget.setPosition(primaryAnchor, secondaryAnchor, preference, affinity);
118
119
this.setShouldRender();
120
}
121
122
public removeWidget(widget: IContentWidget): void {
123
const widgetId = widget.getId();
124
if (this._widgets.hasOwnProperty(widgetId)) {
125
const myWidget = this._widgets[widgetId];
126
delete this._widgets[widgetId];
127
128
const domNode = myWidget.domNode.domNode;
129
domNode.remove();
130
domNode.removeAttribute('monaco-visible-content-widget');
131
132
this.setShouldRender();
133
}
134
}
135
136
public shouldSuppressMouseDownOnWidget(widgetId: string): boolean {
137
if (this._widgets.hasOwnProperty(widgetId)) {
138
return this._widgets[widgetId].suppressMouseDown;
139
}
140
return false;
141
}
142
143
public onBeforeRender(viewportData: ViewportData): void {
144
const keys = Object.keys(this._widgets);
145
for (const widgetId of keys) {
146
this._widgets[widgetId].onBeforeRender(viewportData);
147
}
148
}
149
150
public prepareRender(ctx: RenderingContext): void {
151
const keys = Object.keys(this._widgets);
152
for (const widgetId of keys) {
153
this._widgets[widgetId].prepareRender(ctx);
154
}
155
}
156
157
public render(ctx: RestrictedRenderingContext): void {
158
const keys = Object.keys(this._widgets);
159
for (const widgetId of keys) {
160
this._widgets[widgetId].render(ctx);
161
}
162
}
163
}
164
165
interface IBoxLayoutResult {
166
fitsAbove: boolean;
167
aboveTop: number;
168
169
fitsBelow: boolean;
170
belowTop: number;
171
172
left: number;
173
}
174
175
interface IOffViewportRenderData {
176
kind: 'offViewport';
177
preserveFocus: boolean;
178
}
179
180
interface IInViewportRenderData {
181
kind: 'inViewport';
182
coordinate: Coordinate;
183
position: ContentWidgetPositionPreference;
184
}
185
186
type IRenderData = IInViewportRenderData | IOffViewportRenderData;
187
188
class Widget {
189
private readonly _context: ViewContext;
190
private readonly _viewDomNode: FastDomNode<HTMLElement>;
191
private readonly _actual: IContentWidget;
192
193
public readonly domNode: FastDomNode<HTMLElement>;
194
public readonly id: string;
195
public readonly allowEditorOverflow: boolean;
196
public readonly suppressMouseDown: boolean;
197
198
private readonly _fixedOverflowWidgets: boolean;
199
private _contentWidth: number;
200
private _contentLeft: number;
201
202
private _primaryAnchor: PositionPair = new PositionPair(null, null);
203
private _secondaryAnchor: PositionPair = new PositionPair(null, null);
204
private _affinity: PositionAffinity | null;
205
private _preference: ContentWidgetPositionPreference[] | null;
206
private _cachedDomNodeOffsetWidth: number;
207
private _cachedDomNodeOffsetHeight: number;
208
private _maxWidth: number;
209
private _isVisible: boolean;
210
211
private _renderData: IRenderData | null;
212
213
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>, actual: IContentWidget) {
214
this._context = context;
215
this._viewDomNode = viewDomNode;
216
this._actual = actual;
217
218
const options = this._context.configuration.options;
219
const layoutInfo = options.get(EditorOption.layoutInfo);
220
const allowOverflow = options.get(EditorOption.allowOverflow);
221
222
this.domNode = createFastDomNode(this._actual.getDomNode());
223
this.id = this._actual.getId();
224
this.allowEditorOverflow = (this._actual.allowEditorOverflow || false) && allowOverflow;
225
this.suppressMouseDown = this._actual.suppressMouseDown || false;
226
227
this._fixedOverflowWidgets = options.get(EditorOption.fixedOverflowWidgets);
228
this._contentWidth = layoutInfo.contentWidth;
229
this._contentLeft = layoutInfo.contentLeft;
230
231
this._affinity = null;
232
this._preference = [];
233
this._cachedDomNodeOffsetWidth = -1;
234
this._cachedDomNodeOffsetHeight = -1;
235
this._maxWidth = this._getMaxWidth();
236
this._isVisible = false;
237
this._renderData = null;
238
239
this.domNode.setPosition((this._fixedOverflowWidgets && this.allowEditorOverflow) ? 'fixed' : 'absolute');
240
this.domNode.setDisplay('none');
241
this.domNode.setVisibility('hidden');
242
this.domNode.setAttribute('widgetId', this.id);
243
this.domNode.setMaxWidth(this._maxWidth);
244
}
245
246
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void {
247
const options = this._context.configuration.options;
248
if (e.hasChanged(EditorOption.layoutInfo)) {
249
const layoutInfo = options.get(EditorOption.layoutInfo);
250
this._contentLeft = layoutInfo.contentLeft;
251
this._contentWidth = layoutInfo.contentWidth;
252
this._maxWidth = this._getMaxWidth();
253
}
254
}
255
256
public updateAnchorViewPosition(): void {
257
this._setPosition(this._affinity, this._primaryAnchor.modelPosition, this._secondaryAnchor.modelPosition);
258
}
259
260
private _setPosition(affinity: PositionAffinity | null, primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null): void {
261
this._affinity = affinity;
262
this._primaryAnchor = getValidPositionPair(primaryAnchor, this._context.viewModel, this._affinity);
263
this._secondaryAnchor = getValidPositionPair(secondaryAnchor, this._context.viewModel, this._affinity);
264
265
function getValidPositionPair(position: IPosition | null, viewModel: IViewModel, affinity: PositionAffinity | null): PositionPair {
266
if (!position) {
267
return new PositionPair(null, null);
268
}
269
// Do not trust that widgets give a valid position
270
const validModelPosition = viewModel.model.validatePosition(position);
271
if (viewModel.coordinatesConverter.modelPositionIsVisible(validModelPosition)) {
272
const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(validModelPosition, affinity ?? undefined);
273
return new PositionPair(position, viewPosition);
274
}
275
return new PositionPair(position, null);
276
}
277
}
278
279
private _getMaxWidth(): number {
280
const elDocument = this.domNode.domNode.ownerDocument;
281
const elWindow = elDocument.defaultView;
282
return (
283
this.allowEditorOverflow
284
? elWindow?.innerWidth || elDocument.documentElement.offsetWidth || elDocument.body.offsetWidth
285
: this._contentWidth
286
);
287
}
288
289
public setPosition(primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void {
290
this._setPosition(affinity, primaryAnchor, secondaryAnchor);
291
this._preference = preference;
292
if (this._primaryAnchor.viewPosition && this._preference && this._preference.length > 0) {
293
// this content widget would like to be visible if possible
294
// we change it from `display:none` to `display:block` even if it
295
// might be outside the viewport such that we can measure its size
296
// in `prepareRender`
297
this.domNode.setDisplay('block');
298
} else {
299
this.domNode.setDisplay('none');
300
}
301
this._cachedDomNodeOffsetWidth = -1;
302
this._cachedDomNodeOffsetHeight = -1;
303
}
304
305
private _layoutBoxInViewport(anchor: AnchorCoordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
306
// Our visible box is split horizontally by the current line => 2 boxes
307
308
// a) the box above the line
309
const aboveLineTop = anchor.top;
310
const heightAvailableAboveLine = aboveLineTop;
311
312
// b) the box under the line
313
const underLineTop = anchor.top + anchor.height;
314
const heightAvailableUnderLine = ctx.viewportHeight - underLineTop;
315
316
const aboveTop = aboveLineTop - height;
317
const fitsAbove = (heightAvailableAboveLine >= height);
318
const belowTop = underLineTop;
319
const fitsBelow = (heightAvailableUnderLine >= height);
320
321
// And its left
322
let left = anchor.left;
323
if (left + width > ctx.scrollLeft + ctx.viewportWidth) {
324
left = ctx.scrollLeft + ctx.viewportWidth - width;
325
}
326
if (left < ctx.scrollLeft) {
327
left = ctx.scrollLeft;
328
}
329
330
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
331
}
332
333
private _layoutHorizontalSegmentInPage(windowSize: dom.Dimension, domNodePosition: dom.IDomNodePagePosition, left: number, width: number): [number, number] {
334
// Leave some clearance to the left/right
335
const LEFT_PADDING = 15;
336
const RIGHT_PADDING = 15;
337
338
// Initially, the limits are defined as the dom node limits
339
const MIN_LIMIT = Math.max(LEFT_PADDING, domNodePosition.left - width);
340
const MAX_LIMIT = Math.min(domNodePosition.left + domNodePosition.width + width, windowSize.width - RIGHT_PADDING);
341
342
const elDocument = this._viewDomNode.domNode.ownerDocument;
343
const elWindow = elDocument.defaultView;
344
let absoluteLeft = domNodePosition.left + left - (elWindow?.scrollX ?? 0);
345
346
if (absoluteLeft + width > MAX_LIMIT) {
347
const delta = absoluteLeft - (MAX_LIMIT - width);
348
absoluteLeft -= delta;
349
left -= delta;
350
}
351
352
if (absoluteLeft < MIN_LIMIT) {
353
const delta = absoluteLeft - MIN_LIMIT;
354
absoluteLeft -= delta;
355
left -= delta;
356
}
357
358
return [left, absoluteLeft];
359
}
360
361
private _layoutBoxInPage(anchor: AnchorCoordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult | null {
362
const aboveTop = anchor.top - height;
363
const belowTop = anchor.top + anchor.height;
364
365
const domNodePosition = dom.getDomNodePagePosition(this._viewDomNode.domNode);
366
const elDocument = this._viewDomNode.domNode.ownerDocument;
367
const elWindow = elDocument.defaultView;
368
const absoluteAboveTop = domNodePosition.top + aboveTop - (elWindow?.scrollY ?? 0);
369
const absoluteBelowTop = domNodePosition.top + belowTop - (elWindow?.scrollY ?? 0);
370
371
const windowSize = dom.getClientArea(elDocument.body);
372
const [left, absoluteAboveLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, anchor.left - ctx.scrollLeft + this._contentLeft, width);
373
374
// Leave some clearance to the top/bottom
375
const TOP_PADDING = 22;
376
const BOTTOM_PADDING = 22;
377
378
const fitsAbove = (absoluteAboveTop >= TOP_PADDING);
379
const fitsBelow = (absoluteBelowTop + height <= windowSize.height - BOTTOM_PADDING);
380
381
if (this._fixedOverflowWidgets) {
382
return {
383
fitsAbove,
384
aboveTop: Math.max(absoluteAboveTop, TOP_PADDING),
385
fitsBelow,
386
belowTop: absoluteBelowTop,
387
left: absoluteAboveLeft
388
};
389
}
390
391
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
392
}
393
394
private _prepareRenderWidgetAtExactPositionOverflowing(topLeft: Coordinate): Coordinate {
395
return new Coordinate(topLeft.top, topLeft.left + this._contentLeft);
396
}
397
398
/**
399
* Compute the coordinates above and below the primary and secondary anchors.
400
* The content widget *must* touch the primary anchor.
401
* The content widget should touch if possible the secondary anchor.
402
*/
403
private _getAnchorsCoordinates(ctx: RenderingContext): { primary: AnchorCoordinate | null; secondary: AnchorCoordinate | null } {
404
const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity);
405
const secondaryViewPosition = (this._secondaryAnchor.viewPosition?.lineNumber === this._primaryAnchor.viewPosition?.lineNumber ? this._secondaryAnchor.viewPosition : null);
406
const secondary = getCoordinates(secondaryViewPosition, this._affinity);
407
return { primary, secondary };
408
409
function getCoordinates(position: Position | null, affinity: PositionAffinity | null): AnchorCoordinate | null {
410
if (!position) {
411
return null;
412
}
413
414
const horizontalPosition = ctx.visibleRangeForPosition(position);
415
if (!horizontalPosition) {
416
return null;
417
}
418
419
// Left-align widgets that should appear :before content
420
const left = (position.column === 1 && affinity === PositionAffinity.LeftOfInjectedText ? 0 : horizontalPosition.left);
421
const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop;
422
const lineHeight = ctx.getLineHeightForLineNumber(position.lineNumber);
423
return new AnchorCoordinate(top, left, lineHeight);
424
}
425
}
426
427
private _reduceAnchorCoordinates(primary: AnchorCoordinate, secondary: AnchorCoordinate | null, width: number): AnchorCoordinate {
428
if (!secondary) {
429
return primary;
430
}
431
432
const fontInfo = this._context.configuration.options.get(EditorOption.fontInfo);
433
434
let left = secondary.left;
435
if (left < primary.left) {
436
left = Math.max(left, primary.left - width + fontInfo.typicalFullwidthCharacterWidth);
437
} else {
438
left = Math.min(left, primary.left + width - fontInfo.typicalFullwidthCharacterWidth);
439
}
440
return new AnchorCoordinate(primary.top, left, primary.height);
441
}
442
443
private _prepareRenderWidget(ctx: RenderingContext): IRenderData | null {
444
if (!this._preference || this._preference.length === 0) {
445
return null;
446
}
447
448
const { primary, secondary } = this._getAnchorsCoordinates(ctx);
449
if (!primary) {
450
return {
451
kind: 'offViewport',
452
preserveFocus: this.domNode.domNode.contains(this.domNode.domNode.ownerDocument.activeElement)
453
};
454
// return null;
455
}
456
457
if (this._cachedDomNodeOffsetWidth === -1 || this._cachedDomNodeOffsetHeight === -1) {
458
459
let preferredDimensions: IDimension | null = null;
460
if (typeof this._actual.beforeRender === 'function') {
461
preferredDimensions = safeInvoke(this._actual.beforeRender, this._actual);
462
}
463
if (preferredDimensions) {
464
this._cachedDomNodeOffsetWidth = preferredDimensions.width;
465
this._cachedDomNodeOffsetHeight = preferredDimensions.height;
466
} else {
467
const domNode = this.domNode.domNode;
468
const clientRect = domNode.getBoundingClientRect();
469
this._cachedDomNodeOffsetWidth = Math.round(clientRect.width);
470
this._cachedDomNodeOffsetHeight = Math.round(clientRect.height);
471
}
472
}
473
474
const anchor = this._reduceAnchorCoordinates(primary, secondary, this._cachedDomNodeOffsetWidth);
475
476
let placement: IBoxLayoutResult | null;
477
if (this.allowEditorOverflow) {
478
placement = this._layoutBoxInPage(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
479
} else {
480
placement = this._layoutBoxInViewport(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
481
}
482
483
// Do two passes, first for perfect fit, second picks first option
484
for (let pass = 1; pass <= 2; pass++) {
485
for (const pref of this._preference) {
486
// placement
487
if (pref === ContentWidgetPositionPreference.ABOVE) {
488
if (!placement) {
489
// Widget outside of viewport
490
return null;
491
}
492
if (pass === 2 || placement.fitsAbove) {
493
return {
494
kind: 'inViewport',
495
coordinate: new Coordinate(placement.aboveTop, placement.left),
496
position: ContentWidgetPositionPreference.ABOVE
497
};
498
}
499
} else if (pref === ContentWidgetPositionPreference.BELOW) {
500
if (!placement) {
501
// Widget outside of viewport
502
return null;
503
}
504
if (pass === 2 || placement.fitsBelow) {
505
return {
506
kind: 'inViewport',
507
coordinate: new Coordinate(placement.belowTop, placement.left),
508
position: ContentWidgetPositionPreference.BELOW
509
};
510
}
511
} else {
512
if (this.allowEditorOverflow) {
513
return {
514
kind: 'inViewport',
515
coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)),
516
position: ContentWidgetPositionPreference.EXACT
517
};
518
} else {
519
return {
520
kind: 'inViewport',
521
coordinate: new Coordinate(anchor.top, anchor.left),
522
position: ContentWidgetPositionPreference.EXACT
523
};
524
}
525
}
526
}
527
}
528
529
return null;
530
}
531
532
/**
533
* On this first pass, we ensure that the content widget (if it is in the viewport) has the max width set correctly.
534
*/
535
public onBeforeRender(viewportData: ViewportData): void {
536
if (!this._primaryAnchor.viewPosition || !this._preference) {
537
return;
538
}
539
540
if (this._primaryAnchor.viewPosition.lineNumber < viewportData.startLineNumber || this._primaryAnchor.viewPosition.lineNumber > viewportData.endLineNumber) {
541
// Outside of viewport
542
return;
543
}
544
545
this.domNode.setMaxWidth(this._maxWidth);
546
}
547
548
public prepareRender(ctx: RenderingContext): void {
549
this._renderData = this._prepareRenderWidget(ctx);
550
}
551
552
public render(ctx: RestrictedRenderingContext): void {
553
if (!this._renderData || this._renderData.kind === 'offViewport') {
554
// This widget should be invisible
555
if (this._isVisible) {
556
this.domNode.removeAttribute('monaco-visible-content-widget');
557
this._isVisible = false;
558
559
if (this._renderData?.kind === 'offViewport' && this._renderData.preserveFocus) {
560
// widget wants to be shown, but it is outside of the viewport and it
561
// has focus which we need to preserve
562
this.domNode.setTop(-1000);
563
} else {
564
this.domNode.setVisibility('hidden');
565
}
566
}
567
568
if (typeof this._actual.afterRender === 'function') {
569
safeInvoke(this._actual.afterRender, this._actual, null, null);
570
}
571
return;
572
}
573
574
// This widget should be visible
575
if (this.allowEditorOverflow) {
576
this.domNode.setTop(this._renderData.coordinate.top);
577
this.domNode.setLeft(this._renderData.coordinate.left);
578
} else {
579
this.domNode.setTop(this._renderData.coordinate.top + ctx.scrollTop - ctx.bigNumbersDelta);
580
this.domNode.setLeft(this._renderData.coordinate.left);
581
}
582
583
if (!this._isVisible) {
584
this.domNode.setVisibility('inherit');
585
this.domNode.setAttribute('monaco-visible-content-widget', 'true');
586
this._isVisible = true;
587
}
588
589
if (typeof this._actual.afterRender === 'function') {
590
safeInvoke(this._actual.afterRender, this._actual, this._renderData.position, this._renderData.coordinate);
591
}
592
}
593
}
594
595
class PositionPair {
596
constructor(
597
public readonly modelPosition: IPosition | null,
598
public readonly viewPosition: Position | null
599
) { }
600
}
601
602
class Coordinate implements IContentWidgetRenderedCoordinate {
603
_coordinateBrand: void = undefined;
604
605
constructor(
606
public readonly top: number,
607
public readonly left: number
608
) { }
609
}
610
611
class AnchorCoordinate {
612
_anchorCoordinateBrand: void = undefined;
613
614
constructor(
615
public readonly top: number,
616
public readonly left: number,
617
public readonly height: number
618
) { }
619
}
620
621
function safeInvoke<T extends (...args: any[]) => any>(fn: T, thisArg: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> | null {
622
try {
623
return fn.call(thisArg, ...args);
624
} catch {
625
// ignore
626
return null;
627
}
628
}
629
630