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