Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts
4779 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 { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition } from '../../../browser/editorBrowser.js';
8
import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';
9
import { HoverStartSource } from './hoverOperation.js';
10
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
11
import { ResizableContentWidget } from './resizableContentWidget.js';
12
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
13
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
15
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
16
import { getHoverAccessibleViewHint, HoverWidget } from '../../../../base/browser/ui/hover/hoverWidget.js';
17
import { PositionAffinity } from '../../../common/model.js';
18
import { Emitter } from '../../../../base/common/event.js';
19
import { RenderedContentHover } from './contentHoverRendered.js';
20
import { ScrollEvent } from '../../../../base/common/scrollable.js';
21
22
const HORIZONTAL_SCROLLING_BY = 30;
23
24
export class ContentHoverWidget extends ResizableContentWidget {
25
26
public static ID = 'editor.contrib.resizableContentHoverWidget';
27
private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0);
28
29
private _renderedHover: RenderedContentHover | undefined;
30
private _positionPreference: ContentWidgetPositionPreference | undefined;
31
private _minimumSize: dom.Dimension;
32
private _contentWidth: number | undefined;
33
34
private readonly _hover: HoverWidget = this._register(new HoverWidget(true));
35
private readonly _hoverVisibleKey: IContextKey<boolean>;
36
private readonly _hoverFocusedKey: IContextKey<boolean>;
37
38
private readonly _onDidResize = this._register(new Emitter<void>());
39
public readonly onDidResize = this._onDidResize.event;
40
41
private readonly _onDidScroll = this._register(new Emitter<ScrollEvent>());
42
public readonly onDidScroll = this._onDidScroll.event;
43
44
private readonly _onContentsChanged = this._register(new Emitter<void>());
45
public readonly onContentsChanged = this._onContentsChanged.event;
46
47
public get isVisibleFromKeyboard(): boolean {
48
return (this._renderedHover?.source === HoverStartSource.Keyboard);
49
}
50
51
public get isVisible(): boolean {
52
return this._hoverVisibleKey.get() ?? false;
53
}
54
55
public get isFocused(): boolean {
56
return this._hoverFocusedKey.get() ?? false;
57
}
58
59
constructor(
60
editor: ICodeEditor,
61
@IContextKeyService contextKeyService: IContextKeyService,
62
@IConfigurationService private readonly _configurationService: IConfigurationService,
63
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
64
@IKeybindingService private readonly _keybindingService: IKeybindingService
65
) {
66
const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8;
67
const minimumWidth = 150;
68
const minimumSize = new dom.Dimension(minimumWidth, minimumHeight);
69
super(editor, minimumSize);
70
71
this._minimumSize = minimumSize;
72
this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(contextKeyService);
73
this._hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(contextKeyService);
74
75
dom.append(this._resizableNode.domNode, this._hover.containerDomNode);
76
this._resizableNode.domNode.style.zIndex = '50';
77
this._resizableNode.domNode.className = 'monaco-resizable-hover';
78
79
this._register(this._editor.onDidLayoutChange(() => {
80
if (this.isVisible) {
81
this._updateMaxDimensions();
82
}
83
}));
84
this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
85
if (e.hasChanged(EditorOption.fontInfo)) {
86
this._updateFont();
87
}
88
}));
89
const focusTracker = this._register(dom.trackFocus(this._resizableNode.domNode));
90
this._register(focusTracker.onDidFocus(() => {
91
this._hoverFocusedKey.set(true);
92
}));
93
this._register(focusTracker.onDidBlur(() => {
94
this._hoverFocusedKey.set(false);
95
}));
96
this._register(this._hover.scrollbar.onScroll((e) => {
97
this._onDidScroll.fire(e);
98
}));
99
this._setRenderedHover(undefined);
100
this._editor.addContentWidget(this);
101
}
102
103
public override dispose(): void {
104
super.dispose();
105
this._renderedHover?.dispose();
106
this._editor.removeContentWidget(this);
107
}
108
109
public getId(): string {
110
return ContentHoverWidget.ID;
111
}
112
113
private static _applyDimensions(container: HTMLElement, width: number | string, height: number | string): void {
114
const transformedWidth = typeof width === 'number' ? `${width}px` : width;
115
const transformedHeight = typeof height === 'number' ? `${height}px` : height;
116
container.style.width = transformedWidth;
117
container.style.height = transformedHeight;
118
}
119
120
private _setContentsDomNodeDimensions(width: number | string, height: number | string): void {
121
const contentsDomNode = this._hover.contentsDomNode;
122
return ContentHoverWidget._applyDimensions(contentsDomNode, width, height);
123
}
124
125
private _setContainerDomNodeDimensions(width: number | string, height: number | string): void {
126
const containerDomNode = this._hover.containerDomNode;
127
return ContentHoverWidget._applyDimensions(containerDomNode, width, height);
128
}
129
130
private _setScrollableElementDimensions(width: number | string, height: number | string): void {
131
const scrollbarDomElement = this._hover.scrollbar.getDomNode();
132
return ContentHoverWidget._applyDimensions(scrollbarDomElement, width, height);
133
}
134
135
private _setHoverWidgetDimensions(width: number | string, height: number | string): void {
136
this._setContainerDomNodeDimensions(width, height);
137
this._setScrollableElementDimensions(width, height);
138
this._setContentsDomNodeDimensions(width, height);
139
this._layoutContentWidget();
140
}
141
142
private static _applyMaxDimensions(container: HTMLElement, width: number | string, height: number | string) {
143
const transformedWidth = typeof width === 'number' ? `${width}px` : width;
144
const transformedHeight = typeof height === 'number' ? `${height}px` : height;
145
container.style.maxWidth = transformedWidth;
146
container.style.maxHeight = transformedHeight;
147
}
148
149
private _setHoverWidgetMaxDimensions(width: number | string, height: number | string): void {
150
ContentHoverWidget._applyMaxDimensions(this._hover.contentsDomNode, width, height);
151
ContentHoverWidget._applyMaxDimensions(this._hover.scrollbar.getDomNode(), width, height);
152
ContentHoverWidget._applyMaxDimensions(this._hover.containerDomNode, width, height);
153
this._hover.containerDomNode.style.setProperty('--vscode-hover-maxWidth', typeof width === 'number' ? `${width}px` : width);
154
this._layoutContentWidget();
155
}
156
157
private _setAdjustedHoverWidgetDimensions(size: dom.Dimension): void {
158
this._setHoverWidgetMaxDimensions('none', 'none');
159
this._setHoverWidgetDimensions(size.width, size.height);
160
}
161
162
private _updateResizableNodeMaxDimensions(): void {
163
const maxRenderingWidth = this._findMaximumRenderingWidth() ?? Infinity;
164
const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity;
165
this._resizableNode.maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight);
166
this._setHoverWidgetMaxDimensions(maxRenderingWidth, maxRenderingHeight);
167
}
168
169
protected override _resize(size: dom.Dimension): void {
170
ContentHoverWidget._lastDimensions = new dom.Dimension(size.width, size.height);
171
this._setAdjustedHoverWidgetDimensions(size);
172
this._resizableNode.layout(size.height, size.width);
173
this._updateResizableNodeMaxDimensions();
174
this._hover.scrollbar.scanDomNode();
175
this._editor.layoutContentWidget(this);
176
this._onDidResize.fire();
177
}
178
179
private _findAvailableSpaceVertically(): number | undefined {
180
const position = this._renderedHover?.showAtPosition;
181
if (!position) {
182
return;
183
}
184
return this._positionPreference === ContentWidgetPositionPreference.ABOVE ?
185
this._availableVerticalSpaceAbove(position)
186
: this._availableVerticalSpaceBelow(position);
187
}
188
189
private _findMaximumRenderingHeight(): number | undefined {
190
const availableSpace = this._findAvailableSpaceVertically();
191
if (!availableSpace) {
192
return;
193
}
194
const children = this._hover.contentsDomNode.children;
195
let maximumHeight = children.length - 1;
196
Array.from(this._hover.contentsDomNode.children).forEach((hoverPart) => {
197
maximumHeight += hoverPart.clientHeight;
198
});
199
return Math.min(availableSpace, maximumHeight);
200
}
201
202
private _isHoverTextOverflowing(): boolean {
203
// To find out if the text is overflowing, we will disable wrapping, check the widths, and then re-enable wrapping
204
this._hover.containerDomNode.style.setProperty('--vscode-hover-whiteSpace', 'nowrap');
205
this._hover.containerDomNode.style.setProperty('--vscode-hover-sourceWhiteSpace', 'nowrap');
206
207
const overflowing = Array.from(this._hover.contentsDomNode.children).some((hoverElement) => {
208
return hoverElement.scrollWidth > hoverElement.clientWidth;
209
});
210
211
this._hover.containerDomNode.style.removeProperty('--vscode-hover-whiteSpace');
212
this._hover.containerDomNode.style.removeProperty('--vscode-hover-sourceWhiteSpace');
213
214
return overflowing;
215
}
216
217
private _findMaximumRenderingWidth(): number | undefined {
218
if (!this._editor || !this._editor.hasModel()) {
219
return;
220
}
221
222
const overflowing = this._isHoverTextOverflowing();
223
const initialWidth = (
224
typeof this._contentWidth === 'undefined'
225
? 0
226
: this._contentWidth
227
);
228
229
if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) {
230
const bodyBoxWidth = dom.getClientArea(this._hover.containerDomNode.ownerDocument.body).width;
231
const horizontalPadding = 14;
232
return bodyBoxWidth - horizontalPadding;
233
} else {
234
return this._hover.containerDomNode.clientWidth;
235
}
236
}
237
238
public isMouseGettingCloser(posx: number, posy: number): boolean {
239
240
if (!this._renderedHover) {
241
return false;
242
}
243
if (this._renderedHover.initialMousePosX === undefined || this._renderedHover.initialMousePosY === undefined) {
244
this._renderedHover.initialMousePosX = posx;
245
this._renderedHover.initialMousePosY = posy;
246
return false;
247
}
248
249
const widgetRect = dom.getDomNodePagePosition(this.getDomNode());
250
if (this._renderedHover.closestMouseDistance === undefined) {
251
this._renderedHover.closestMouseDistance = computeDistanceFromPointToRectangle(
252
this._renderedHover.initialMousePosX,
253
this._renderedHover.initialMousePosY,
254
widgetRect.left,
255
widgetRect.top,
256
widgetRect.width,
257
widgetRect.height
258
);
259
}
260
261
const distance = computeDistanceFromPointToRectangle(
262
posx,
263
posy,
264
widgetRect.left,
265
widgetRect.top,
266
widgetRect.width,
267
widgetRect.height
268
);
269
if (distance > this._renderedHover.closestMouseDistance + 4 /* tolerance of 4 pixels */) {
270
// The mouse is getting farther away
271
return false;
272
}
273
274
this._renderedHover.closestMouseDistance = Math.min(this._renderedHover.closestMouseDistance, distance);
275
return true;
276
}
277
278
private _setRenderedHover(renderedHover: RenderedContentHover | undefined): void {
279
this._renderedHover?.dispose();
280
this._renderedHover = renderedHover;
281
this._hoverVisibleKey.set(!!renderedHover);
282
this._hover.containerDomNode.classList.toggle('hidden', !renderedHover);
283
}
284
285
private _updateFont(): void {
286
const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo);
287
const contentsDomNode = this._hover.contentsDomNode;
288
contentsDomNode.style.fontSize = `${fontSize}px`;
289
contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`;
290
// eslint-disable-next-line no-restricted-syntax
291
const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));
292
codeClasses.forEach(node => this._editor.applyFontInfo(node));
293
}
294
295
private _updateContent(node: DocumentFragment): void {
296
const contentsDomNode = this._hover.contentsDomNode;
297
contentsDomNode.style.paddingBottom = '';
298
contentsDomNode.textContent = '';
299
contentsDomNode.appendChild(node);
300
}
301
302
private _layoutContentWidget(): void {
303
this._editor.layoutContentWidget(this);
304
this._hover.onContentsChanged();
305
}
306
307
private _updateMaxDimensions() {
308
const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height);
309
const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 750, ContentHoverWidget._lastDimensions.width);
310
this._resizableNode.maxSize = new dom.Dimension(width, height);
311
this._setHoverWidgetMaxDimensions(width, height);
312
}
313
314
private _render(renderedHover: RenderedContentHover) {
315
this._setRenderedHover(renderedHover);
316
this._updateFont();
317
this._updateContent(renderedHover.domNode);
318
this.handleContentsChanged();
319
// Simply force a synchronous render on the editor
320
// such that the widget does not really render with left = '0px'
321
this._editor.render();
322
}
323
324
override getPosition(): IContentWidgetPosition | null {
325
if (!this._renderedHover) {
326
return null;
327
}
328
return {
329
position: this._renderedHover.showAtPosition,
330
secondaryPosition: this._renderedHover.showAtSecondaryPosition,
331
positionAffinity: this._renderedHover.shouldAppearBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined,
332
preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE]
333
};
334
}
335
336
public show(renderedHover: RenderedContentHover): void {
337
if (!this._editor || !this._editor.hasModel()) {
338
return;
339
}
340
this._render(renderedHover);
341
const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode);
342
const widgetPosition = renderedHover.showAtPosition;
343
this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE;
344
345
// See https://github.com/microsoft/vscode/issues/140339
346
// TODO: Doing a second layout of the hover after force rendering the editor
347
this.handleContentsChanged();
348
if (renderedHover.shouldFocus) {
349
this._hover.containerDomNode.focus();
350
}
351
this._onDidResize.fire();
352
// The aria label overrides the label, so if we add to it, add the contents of the hover
353
const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode;
354
const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(
355
this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(),
356
this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel() ?? ''
357
);
358
359
if (accessibleViewHint) {
360
this._hover.contentsDomNode.ariaLabel = this._hover.contentsDomNode.textContent + ', ' + accessibleViewHint;
361
}
362
}
363
364
public hide(): void {
365
if (!this._renderedHover) {
366
return;
367
}
368
const hoverStoleFocus = this._renderedHover.shouldFocus || this._hoverFocusedKey.get();
369
this._setRenderedHover(undefined);
370
this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity);
371
this._resizableNode.clearSashHoverState();
372
this._hoverFocusedKey.set(false);
373
this._editor.layoutContentWidget(this);
374
if (hoverStoleFocus) {
375
this._editor.focus();
376
}
377
}
378
379
private _removeConstraintsRenderNormally(): void {
380
// Added because otherwise the initial size of the hover content is smaller than should be
381
const layoutInfo = this._editor.getLayoutInfo();
382
this._resizableNode.layout(layoutInfo.height, layoutInfo.width);
383
this._setHoverWidgetDimensions('auto', 'auto');
384
this._updateMaxDimensions();
385
}
386
387
public setMinimumDimensions(dimensions: dom.Dimension): void {
388
// We combine the new minimum dimensions with the previous ones
389
this._minimumSize = new dom.Dimension(
390
Math.max(this._minimumSize.width, dimensions.width),
391
Math.max(this._minimumSize.height, dimensions.height)
392
);
393
this._updateMinimumWidth();
394
}
395
396
private _updateMinimumWidth(): void {
397
const width = (
398
typeof this._contentWidth === 'undefined'
399
? this._minimumSize.width
400
: Math.min(this._contentWidth, this._minimumSize.width)
401
);
402
// We want to avoid that the hover is artificially large, so we use the content width as minimum width
403
this._resizableNode.minSize = new dom.Dimension(width, this._minimumSize.height);
404
}
405
406
public handleContentsChanged(): void {
407
this._removeConstraintsRenderNormally();
408
const contentsDomNode = this._hover.contentsDomNode;
409
410
let height = dom.getTotalHeight(contentsDomNode);
411
let width = dom.getTotalWidth(contentsDomNode) + 2;
412
this._resizableNode.layout(height, width);
413
414
this._setHoverWidgetDimensions(width, height);
415
416
height = dom.getTotalHeight(contentsDomNode);
417
width = dom.getTotalWidth(contentsDomNode);
418
this._contentWidth = width;
419
this._updateMinimumWidth();
420
this._resizableNode.layout(height, width);
421
422
if (this._renderedHover?.showAtPosition) {
423
const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode);
424
this._positionPreference = this._findPositionPreference(widgetHeight, this._renderedHover.showAtPosition);
425
}
426
this._layoutContentWidget();
427
this._onContentsChanged.fire();
428
}
429
430
public focus(): void {
431
this._hover.containerDomNode.focus();
432
}
433
434
public scrollUp(): void {
435
const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;
436
const fontInfo = this._editor.getOption(EditorOption.fontInfo);
437
this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - fontInfo.lineHeight });
438
}
439
440
public scrollDown(): void {
441
const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;
442
const fontInfo = this._editor.getOption(EditorOption.fontInfo);
443
this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + fontInfo.lineHeight });
444
}
445
446
public scrollLeft(): void {
447
const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft;
448
this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft - HORIZONTAL_SCROLLING_BY });
449
}
450
451
public scrollRight(): void {
452
const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft;
453
this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft + HORIZONTAL_SCROLLING_BY });
454
}
455
456
public pageUp(): void {
457
const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;
458
const scrollHeight = this._hover.scrollbar.getScrollDimensions().height;
459
this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - scrollHeight });
460
}
461
462
public pageDown(): void {
463
const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop;
464
const scrollHeight = this._hover.scrollbar.getScrollDimensions().height;
465
this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + scrollHeight });
466
}
467
468
public goToTop(): void {
469
this._hover.scrollbar.setScrollPosition({ scrollTop: 0 });
470
}
471
472
public goToBottom(): void {
473
this._hover.scrollbar.setScrollPosition({ scrollTop: this._hover.scrollbar.getScrollDimensions().scrollHeight });
474
}
475
}
476
477
function computeDistanceFromPointToRectangle(pointX: number, pointY: number, left: number, top: number, width: number, height: number): number {
478
const x = (left + width / 2); // x center of rectangle
479
const y = (top + height / 2); // y center of rectangle
480
const dx = Math.max(Math.abs(pointX - x) - width / 2, 0);
481
const dy = Math.max(Math.abs(pointY - y) - height / 2, 0);
482
return Math.sqrt(dx * dx + dy * dy);
483
}
484
485