Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/hover/browser/hoverService.ts
4777 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 { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
7
import { registerThemingParticipant } from '../../theme/common/themeService.js';
8
import { editorHoverBorder } from '../../theme/common/colorRegistry.js';
9
import { IHoverService } from './hover.js';
10
import { IContextMenuService } from '../../contextview/browser/contextView.js';
11
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
12
import { HoverWidget } from './hoverWidget.js';
13
import { IContextViewProvider, IDelegate } from '../../../base/browser/ui/contextview/contextview.js';
14
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
15
import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow, isHTMLElement, isEditableElement } from '../../../base/browser/dom.js';
16
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
17
import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js';
18
import { ResultKind } from '../../keybinding/common/keybindingResolver.js';
19
import { IAccessibilityService } from '../../accessibility/common/accessibility.js';
20
import { ILayoutService } from '../../layout/browser/layoutService.js';
21
import { mainWindow } from '../../../base/browser/window.js';
22
import { ContextViewHandler } from '../../contextview/browser/contextViewService.js';
23
import { HoverStyle, isManagedHoverTooltipMarkdownString, type IHoverLifecycleOptions, type IHoverOptions, type IHoverTarget, type IHoverWidget, type IManagedHover, type IManagedHoverContentOrFactory, type IManagedHoverOptions } from '../../../base/browser/ui/hover/hover.js';
24
import type { IHoverDelegate, IHoverDelegateTarget } from '../../../base/browser/ui/hover/hoverDelegate.js';
25
import { ManagedHoverWidget } from './updatableHoverWidget.js';
26
import { timeout, TimeoutTimer } from '../../../base/common/async.js';
27
import { IConfigurationService } from '../../configuration/common/configuration.js';
28
import { isNumber, isString } from '../../../base/common/types.js';
29
import { KeyChord, KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
30
import { KeybindingsRegistry, KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js';
31
import { IMarkdownString } from '../../../base/common/htmlContent.js';
32
import { stripIcons } from '../../../base/common/iconLabels.js';
33
34
export class HoverService extends Disposable implements IHoverService {
35
declare readonly _serviceBrand: undefined;
36
37
private _contextViewHandler: IContextViewProvider;
38
private _currentHoverOptions: IHoverOptions | undefined;
39
private _currentHover: HoverWidget | undefined;
40
private _currentDelayedHover: HoverWidget | undefined;
41
private _currentDelayedHoverWasShown: boolean = false;
42
private _currentDelayedHoverGroupId: number | string | undefined;
43
private _lastHoverOptions: IHoverOptions | undefined;
44
45
private _lastFocusedElementBeforeOpen: HTMLElement | undefined;
46
47
private readonly _delayedHovers = new Map<HTMLElement, { show: (focus: boolean) => void }>();
48
private readonly _managedHovers = new Map<HTMLElement, IManagedHover>();
49
50
constructor(
51
@IInstantiationService private readonly _instantiationService: IInstantiationService,
52
@IConfigurationService private readonly _configurationService: IConfigurationService,
53
@IContextMenuService contextMenuService: IContextMenuService,
54
@IKeybindingService private readonly _keybindingService: IKeybindingService,
55
@ILayoutService private readonly _layoutService: ILayoutService,
56
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
57
) {
58
super();
59
60
this._register(contextMenuService.onDidShowContextMenu(() => this.hideHover()));
61
this._contextViewHandler = this._register(new ContextViewHandler(this._layoutService));
62
63
this._register(KeybindingsRegistry.registerCommandAndKeybindingRule({
64
id: 'workbench.action.showHover',
65
weight: KeybindingWeight.EditorCore,
66
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI),
67
handler: () => { this._showAndFocusHoverForActiveElement(); },
68
}));
69
}
70
71
showInstantHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean, dontShow?: boolean): IHoverWidget | undefined {
72
const hover = this._createHover(options, skipLastFocusedUpdate);
73
if (!hover) {
74
return undefined;
75
}
76
this._showHover(hover, options, focus);
77
return hover;
78
}
79
80
showDelayedHover(
81
options: IHoverOptions,
82
lifecycleOptions: Pick<IHoverLifecycleOptions, 'groupId'>,
83
): IHoverWidget | undefined {
84
// Set `id` to default if it's undefined
85
if (options.id === undefined) {
86
options.id = getHoverIdFromContent(options.content);
87
}
88
89
if (!this._currentDelayedHover || this._currentDelayedHoverWasShown) {
90
// Current hover is locked, reject
91
if (this._currentHover?.isLocked) {
92
return undefined;
93
}
94
95
// Identity is the same, return current hover
96
if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {
97
return this._currentHover;
98
}
99
100
// Check group identity, if it's the same skip the delay and show the hover immediately
101
if (this._currentHover && !this._currentHover.isDisposed && this._currentDelayedHoverGroupId !== undefined && this._currentDelayedHoverGroupId === lifecycleOptions?.groupId) {
102
return this.showInstantHover({
103
...options,
104
appearance: {
105
...options.appearance,
106
skipFadeInAnimation: true
107
}
108
});
109
}
110
} else if (this._currentDelayedHover && getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {
111
// If the hover is the same but timeout is not finished yet, return the current hover
112
return this._currentDelayedHover;
113
}
114
115
const hover = this._createHover(options, undefined);
116
if (!hover) {
117
this._currentDelayedHover = undefined;
118
this._currentDelayedHoverWasShown = false;
119
this._currentDelayedHoverGroupId = undefined;
120
return undefined;
121
}
122
123
this._currentDelayedHover = hover;
124
this._currentDelayedHoverWasShown = false;
125
this._currentDelayedHoverGroupId = lifecycleOptions?.groupId;
126
127
timeout(this._configurationService.getValue<number>('workbench.hover.delay')).then(() => {
128
if (hover && !hover.isDisposed) {
129
this._currentDelayedHoverWasShown = true;
130
this._showHover(hover, options);
131
}
132
});
133
134
return hover;
135
}
136
137
setupDelayedHover(
138
target: HTMLElement,
139
options: (() => Omit<IHoverOptions, 'target'>) | Omit<IHoverOptions, 'target'>,
140
lifecycleOptions?: IHoverLifecycleOptions,
141
): IDisposable {
142
const resolveHoverOptions = (e?: MouseEvent) => {
143
const resolved: IHoverOptions = {
144
...typeof options === 'function' ? options() : options,
145
target
146
};
147
if (resolved.style === HoverStyle.Mouse && e) {
148
resolved.target = resolveMouseStyleHoverTarget(target, e);
149
}
150
return resolved;
151
};
152
return this._setupDelayedHover(target, resolveHoverOptions, lifecycleOptions);
153
}
154
155
setupDelayedHoverAtMouse(
156
target: HTMLElement,
157
options: (() => Omit<IHoverOptions, 'target' | 'position'>) | Omit<IHoverOptions, 'target' | 'position'>,
158
lifecycleOptions?: IHoverLifecycleOptions,
159
): IDisposable {
160
const resolveHoverOptions = (e?: MouseEvent) => ({
161
...typeof options === 'function' ? options() : options,
162
target: e ? resolveMouseStyleHoverTarget(target, e) : target
163
} satisfies IHoverOptions);
164
return this._setupDelayedHover(target, resolveHoverOptions, lifecycleOptions);
165
}
166
167
private _setupDelayedHover(
168
target: HTMLElement,
169
resolveHoverOptions: ((e?: MouseEvent) => IHoverOptions),
170
lifecycleOptions?: IHoverLifecycleOptions,
171
) {
172
const store = new DisposableStore();
173
store.add(addDisposableListener(target, EventType.MOUSE_OVER, e => {
174
this.showDelayedHover(resolveHoverOptions(e), {
175
groupId: lifecycleOptions?.groupId
176
});
177
}));
178
if (lifecycleOptions?.setupKeyboardEvents) {
179
store.add(addDisposableListener(target, EventType.KEY_DOWN, e => {
180
const evt = new StandardKeyboardEvent(e);
181
if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) {
182
this.showInstantHover(resolveHoverOptions(), true);
183
}
184
}));
185
}
186
187
this._delayedHovers.set(target, { show: (focus: boolean) => { this.showInstantHover(resolveHoverOptions(), focus); } });
188
store.add(toDisposable(() => this._delayedHovers.delete(target)));
189
190
return store;
191
}
192
193
private _createHover(options: IHoverOptions, skipLastFocusedUpdate?: boolean): HoverWidget | undefined {
194
this._currentDelayedHover = undefined;
195
196
if (options.content === '') {
197
return undefined;
198
}
199
200
if (this._currentHover?.isLocked) {
201
return undefined;
202
}
203
204
// Set `id` to default if it's undefined
205
if (options.id === undefined) {
206
options.id = getHoverIdFromContent(options.content);
207
}
208
209
if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {
210
return undefined;
211
}
212
this._currentHoverOptions = options;
213
this._lastHoverOptions = options;
214
const trapFocus = options.trapFocus || this._accessibilityService.isScreenReaderOptimized();
215
const activeElement = getActiveElement();
216
// HACK, remove this check when #189076 is fixed
217
if (!skipLastFocusedUpdate) {
218
if (trapFocus && activeElement) {
219
if (!activeElement.classList.contains('monaco-hover')) {
220
this._lastFocusedElementBeforeOpen = activeElement as HTMLElement;
221
}
222
} else {
223
this._lastFocusedElementBeforeOpen = undefined;
224
}
225
}
226
227
const hoverDisposables = new DisposableStore();
228
const hover = this._instantiationService.createInstance(HoverWidget, options);
229
if (options.persistence?.sticky) {
230
hover.isLocked = true;
231
}
232
233
// Adjust target position when a mouse event is provided as the hover position
234
if (options.position?.hoverPosition && !isNumber(options.position.hoverPosition)) {
235
options.target = {
236
targetElements: isHTMLElement(options.target) ? [options.target] : options.target.targetElements,
237
x: options.position.hoverPosition.x + 10
238
};
239
}
240
241
hover.onDispose(() => {
242
const hoverWasFocused = this._currentHover?.domNode && isAncestorOfActiveElement(this._currentHover.domNode);
243
if (hoverWasFocused) {
244
// Required to handle cases such as closing the hover with the escape key
245
this._lastFocusedElementBeforeOpen?.focus();
246
}
247
248
// Only clear the current options if it's the current hover, the current options help
249
// reduce flickering when the same hover is shown multiple times
250
if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {
251
this.doHideHover();
252
}
253
hoverDisposables.dispose();
254
}, undefined, hoverDisposables);
255
// Set the container explicitly to enable aux window support
256
if (!options.container) {
257
const targetElement = isHTMLElement(options.target) ? options.target : options.target.targetElements[0];
258
options.container = this._layoutService.getContainer(getWindow(targetElement));
259
}
260
261
hover.onRequestLayout(() => this._contextViewHandler.layout(), undefined, hoverDisposables);
262
if (options.persistence?.sticky) {
263
hoverDisposables.add(addDisposableListener(getWindow(options.container).document, EventType.MOUSE_DOWN, e => {
264
if (!isAncestor(e.target as HTMLElement, hover.domNode)) {
265
this.doHideHover();
266
}
267
}));
268
} else {
269
if ('targetElements' in options.target) {
270
for (const element of options.target.targetElements) {
271
hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover()));
272
}
273
} else {
274
hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover()));
275
}
276
const focusedElement = getActiveElement();
277
if (focusedElement) {
278
const focusedElementDocument = getWindow(focusedElement).document;
279
hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));
280
hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));
281
hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => this._keyUp(e, hover)));
282
hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_UP, e => this._keyUp(e, hover)));
283
}
284
}
285
286
if ('IntersectionObserver' in mainWindow) {
287
const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 });
288
const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target;
289
observer.observe(firstTargetElement);
290
hoverDisposables.add(toDisposable(() => observer.disconnect()));
291
}
292
293
this._currentHover = hover;
294
295
return hover;
296
}
297
298
private _showHover(hover: HoverWidget, options: IHoverOptions, focus?: boolean) {
299
this._contextViewHandler.showContextView(
300
new HoverContextViewDelegate(hover, focus),
301
options.container
302
);
303
}
304
305
hideHover(force?: boolean): void {
306
if ((!force && this._currentHover?.isLocked) || !this._currentHoverOptions) {
307
return;
308
}
309
this.doHideHover();
310
}
311
312
private doHideHover(): void {
313
this._currentHover = undefined;
314
this._currentHoverOptions = undefined;
315
this._contextViewHandler.hideContextView();
316
}
317
318
private _intersectionChange(entries: IntersectionObserverEntry[], hover: IDisposable): void {
319
const entry = entries[entries.length - 1];
320
if (!entry.isIntersecting) {
321
hover.dispose();
322
}
323
}
324
325
showAndFocusLastHover(): void {
326
if (!this._lastHoverOptions) {
327
return;
328
}
329
this.showInstantHover(this._lastHoverOptions, true, true);
330
}
331
332
private _showAndFocusHoverForActiveElement(): void {
333
// TODO: if hover is visible, focus it to avoid flickering
334
335
let activeElement = getActiveElement() as HTMLElement | null;
336
while (activeElement) {
337
const hover = this._delayedHovers.get(activeElement) ?? this._managedHovers.get(activeElement);
338
if (hover) {
339
hover.show(true);
340
return;
341
}
342
343
activeElement = activeElement.parentElement;
344
}
345
}
346
347
private _keyDown(e: KeyboardEvent, hover: HoverWidget, hideOnKeyDown: boolean) {
348
if (e.key === 'Alt') {
349
hover.isLocked = true;
350
return;
351
}
352
const event = new StandardKeyboardEvent(e);
353
const keybinding = this._keybindingService.resolveKeyboardEvent(event);
354
if (keybinding.getSingleModifierDispatchChords().some(value => !!value) || this._keybindingService.softDispatch(event, event.target).kind !== ResultKind.NoMatchingKb) {
355
return;
356
}
357
if (hideOnKeyDown && (!this._currentHoverOptions?.trapFocus || e.key !== 'Tab')) {
358
this.hideHover();
359
this._lastFocusedElementBeforeOpen?.focus();
360
}
361
}
362
363
private _keyUp(e: KeyboardEvent, hover: HoverWidget) {
364
if (e.key === 'Alt') {
365
hover.isLocked = false;
366
// Hide if alt is released while the mouse is not over hover/target
367
if (!hover.isMouseIn) {
368
this.hideHover();
369
this._lastFocusedElementBeforeOpen?.focus();
370
}
371
}
372
}
373
374
// TODO: Investigate performance of this function. There seems to be a lot of content created
375
// and thrown away on start up
376
setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover {
377
if (hoverDelegate.showNativeHover) {
378
return setupNativeHover(targetElement, content);
379
}
380
381
targetElement.setAttribute('custom-hover', 'true');
382
383
if (targetElement.title !== '') {
384
console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.');
385
console.trace('Stack trace:', targetElement.title);
386
targetElement.title = '';
387
}
388
389
let hoverPreparation: IDisposable | undefined;
390
let hoverWidget: ManagedHoverWidget | undefined;
391
392
const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => {
393
const hadHover = hoverWidget !== undefined;
394
if (disposeWidget) {
395
hoverWidget?.dispose();
396
hoverWidget = undefined;
397
}
398
if (disposePreparation) {
399
hoverPreparation?.dispose();
400
hoverPreparation = undefined;
401
}
402
if (hadHover) {
403
hoverDelegate.onDidHideHover?.();
404
hoverWidget = undefined;
405
}
406
};
407
408
const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => {
409
return new TimeoutTimer(async () => {
410
if (!hoverWidget || hoverWidget.isDisposed) {
411
hoverWidget = new ManagedHoverWidget(hoverDelegate, target || targetElement, delay > 0);
412
await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus });
413
}
414
}, delay);
415
};
416
417
const store = new DisposableStore();
418
let isMouseDown = false;
419
store.add(addDisposableListener(targetElement, EventType.MOUSE_DOWN, () => {
420
isMouseDown = true;
421
hideHover(true, true);
422
}, true));
423
store.add(addDisposableListener(targetElement, EventType.MOUSE_UP, () => {
424
isMouseDown = false;
425
}, true));
426
store.add(addDisposableListener(targetElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => {
427
isMouseDown = false;
428
// HACK: `fromElement` is a non-standard property. Not sure what to replace it with,
429
// `relatedTarget` is NOT equivalent.
430
interface MouseEventWithFrom extends MouseEvent {
431
fromElement: Element | null;
432
}
433
hideHover(false, (e as MouseEventWithFrom).fromElement === targetElement);
434
}, true));
435
store.add(addDisposableListener(targetElement, EventType.MOUSE_OVER, (e: MouseEvent) => {
436
if (hoverPreparation) {
437
return;
438
}
439
440
const mouseOverStore: DisposableStore = new DisposableStore();
441
442
const target: IHoverDelegateTarget = {
443
targetElements: [targetElement],
444
dispose: () => { }
445
};
446
if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') {
447
// track the mouse position
448
const onMouseMove = (e: MouseEvent) => {
449
target.x = e.x + 10;
450
if (!eventIsRelatedToTarget(e, targetElement)) {
451
hideHover(true, true);
452
}
453
};
454
mouseOverStore.add(addDisposableListener(targetElement, EventType.MOUSE_MOVE, onMouseMove, true));
455
}
456
457
hoverPreparation = mouseOverStore;
458
459
if (!eventIsRelatedToTarget(e, targetElement)) {
460
return; // Do not show hover when the mouse is over another hover target
461
}
462
463
mouseOverStore.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));
464
}, true));
465
466
const onFocus = (e: FocusEvent) => {
467
if (isMouseDown || hoverPreparation) {
468
return;
469
}
470
if (!eventIsRelatedToTarget(e, targetElement)) {
471
return; // Do not show hover when the focus is on another hover target
472
}
473
474
const target: IHoverDelegateTarget = {
475
targetElements: [targetElement],
476
dispose: () => { }
477
};
478
const toDispose: DisposableStore = new DisposableStore();
479
const onBlur = () => hideHover(true, true);
480
toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true));
481
toDispose.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));
482
hoverPreparation = toDispose;
483
};
484
485
// Do not show hover when focusing an input or textarea
486
if (!isEditableElement(targetElement)) {
487
store.add(addDisposableListener(targetElement, EventType.FOCUS, onFocus, true));
488
}
489
490
const hover: IManagedHover = {
491
show: focus => {
492
hideHover(false, true); // terminate a ongoing mouse over preparation
493
triggerShowHover(0, focus, undefined, focus); // show hover immediately
494
},
495
hide: () => {
496
hideHover(true, true);
497
},
498
update: async (newContent, hoverOptions) => {
499
content = newContent;
500
await hoverWidget?.update(content, undefined, hoverOptions);
501
},
502
dispose: () => {
503
this._managedHovers.delete(targetElement);
504
store.dispose();
505
hideHover(true, true);
506
}
507
};
508
this._managedHovers.set(targetElement, hover);
509
return hover;
510
}
511
512
showManagedHover(target: HTMLElement): void {
513
const hover = this._managedHovers.get(target);
514
if (hover) {
515
hover.show(true);
516
}
517
}
518
519
public override dispose(): void {
520
this._managedHovers.forEach(hover => hover.dispose());
521
super.dispose();
522
}
523
}
524
525
function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined {
526
if (options === undefined) {
527
return undefined;
528
}
529
return options?.id ?? options;
530
}
531
532
function getHoverIdFromContent(content: string | HTMLElement | IMarkdownString): string | undefined {
533
if (isHTMLElement(content)) {
534
return undefined;
535
}
536
if (typeof content === 'string') {
537
return content.toString();
538
}
539
return content.value;
540
}
541
542
function getStringContent(contentOrFactory: IManagedHoverContentOrFactory): string | undefined {
543
const content = typeof contentOrFactory === 'function' ? contentOrFactory() : contentOrFactory;
544
if (isString(content)) {
545
// Icons don't render in the native hover so we strip them out
546
return stripIcons(content);
547
}
548
if (isManagedHoverTooltipMarkdownString(content)) {
549
return content.markdownNotSupportedFallback;
550
}
551
return undefined;
552
}
553
554
function setupNativeHover(targetElement: HTMLElement, content: IManagedHoverContentOrFactory): IManagedHover {
555
function updateTitle(title: string | undefined) {
556
if (title) {
557
targetElement.setAttribute('title', title);
558
} else {
559
targetElement.removeAttribute('title');
560
}
561
}
562
563
updateTitle(getStringContent(content));
564
return {
565
update: (content) => updateTitle(getStringContent(content)),
566
show: () => { },
567
hide: () => { },
568
dispose: () => updateTitle(undefined),
569
};
570
}
571
572
class HoverContextViewDelegate implements IDelegate {
573
574
// Render over all other context views
575
public readonly layer = 1;
576
577
get anchorPosition() {
578
return this._hover.anchor;
579
}
580
581
constructor(
582
private readonly _hover: HoverWidget,
583
private readonly _focus: boolean = false
584
) {
585
}
586
587
render(container: HTMLElement) {
588
this._hover.render(container);
589
if (this._focus) {
590
this._hover.focus();
591
}
592
return this._hover;
593
}
594
595
getAnchor() {
596
return {
597
x: this._hover.x,
598
y: this._hover.y
599
};
600
}
601
602
layout() {
603
this._hover.layout();
604
}
605
}
606
607
function eventIsRelatedToTarget(event: UIEvent, target: HTMLElement): boolean {
608
return isHTMLElement(event.target) && getHoverTargetElement(event.target, target) === target;
609
}
610
611
function getHoverTargetElement(element: HTMLElement, stopElement?: HTMLElement): HTMLElement {
612
stopElement = stopElement ?? getWindow(element).document.body;
613
while (!element.hasAttribute('custom-hover') && element !== stopElement) {
614
element = element.parentElement!;
615
}
616
return element;
617
}
618
619
function resolveMouseStyleHoverTarget(target: HTMLElement, e: MouseEvent): IHoverTarget {
620
return {
621
targetElements: [target],
622
x: e.x + 10
623
};
624
}
625
626
registerSingleton(IHoverService, HoverService, InstantiationType.Delayed);
627
628
registerThemingParticipant((theme, collector) => {
629
const hoverBorder = theme.getColor(editorHoverBorder);
630
if (hoverBorder) {
631
collector.addRule(`.monaco-hover.workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
632
collector.addRule(`.monaco-hover.workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
633
}
634
});
635
636