Path: blob/main/src/vs/platform/hover/browser/hoverService.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';6import { registerThemingParticipant } from '../../theme/common/themeService.js';7import { editorHoverBorder } from '../../theme/common/colorRegistry.js';8import { IHoverService } from './hover.js';9import { IContextMenuService } from '../../contextview/browser/contextView.js';10import { IInstantiationService } from '../../instantiation/common/instantiation.js';11import { HoverWidget } from './hoverWidget.js';12import { ContextView, ContextViewDOMPosition, IDelegate } from '../../../base/browser/ui/contextview/contextview.js';13import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';14import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow, isHTMLElement, isEditableElement } from '../../../base/browser/dom.js';15import { IKeybindingService } from '../../keybinding/common/keybinding.js';16import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js';17import { ResultKind } from '../../keybinding/common/keybindingResolver.js';18import { IAccessibilityService } from '../../accessibility/common/accessibility.js';19import { ILayoutService } from '../../layout/browser/layoutService.js';20import { mainWindow } from '../../../base/browser/window.js';21import { HoverStyle, isManagedHoverTooltipMarkdownString, type IHoverLifecycleOptions, type IHoverOptions, type IHoverTarget, type IHoverWidget, type IManagedHover, type IManagedHoverContentOrFactory, type IManagedHoverOptions } from '../../../base/browser/ui/hover/hover.js';22import type { IHoverDelegate, IHoverDelegateTarget } from '../../../base/browser/ui/hover/hoverDelegate.js';23import { ManagedHoverWidget } from './updatableHoverWidget.js';24import { timeout, TimeoutTimer } from '../../../base/common/async.js';25import { IConfigurationService } from '../../configuration/common/configuration.js';26import { isNumber, isString } from '../../../base/common/types.js';27import { KeyChord, KeyCode, KeyMod } from '../../../base/common/keyCodes.js';28import { KeybindingsRegistry, KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js';29import { IMarkdownString } from '../../../base/common/htmlContent.js';30import { stripIcons } from '../../../base/common/iconLabels.js';3132/**33* Maximum nesting depth for hovers. This prevents runaway nesting.34*/35const MAX_HOVER_NESTING_DEPTH = 3;3637/**38* An entry in the hover stack, representing a single hover and its associated state.39*/40interface IHoverStackEntry {41readonly hover: HoverWidget;42readonly options: IHoverOptions;43readonly contextView: ContextView;44readonly lastFocusedElementBeforeOpen: HTMLElement | undefined;45}4647/**48* Result of creating a hover, containing the hover widget and associated state.49*/50interface ICreateHoverResult {51readonly hover: HoverWidget;52readonly store: DisposableStore;53readonly lastFocusedElementBeforeOpen: HTMLElement | undefined;54}5556export class HoverService extends Disposable implements IHoverService {57declare readonly _serviceBrand: undefined;5859/**60* Stack of currently visible hovers. The last entry is the topmost hover.61* This enables nested hovers where hovering inside a hover can show another hover.62*/63private readonly _hoverStack: IHoverStackEntry[] = [];6465private _currentDelayedHover: HoverWidget | undefined;66private _currentDelayedHoverWasShown: boolean = false;67private _currentDelayedHoverGroupId: number | string | undefined;68private _lastHoverOptions: IHoverOptions | undefined;69private readonly _delayedHovers = new Map<HTMLElement, { show: (focus: boolean) => void }>();70private readonly _managedHovers = new Map<HTMLElement, IManagedHover>();7172/**73* Gets the current (topmost) hover from the stack, if any.74*/75private get _currentHover(): HoverWidget | undefined {76return this._hoverStack.at(-1)?.hover;77}7879/**80* Gets the current (topmost) hover options from the stack, if any.81*/82private get _currentHoverOptions(): IHoverOptions | undefined {83return this._hoverStack.at(-1)?.options;84}8586/**87* Returns whether the target element is inside any of the hovers in the stack.88* If it is, returns the index of the containing hover, otherwise returns -1.89*/90private _getContainingHoverIndex(target: HTMLElement | IHoverTarget): number {91const targetElements = isHTMLElement(target) ? [target] : target.targetElements;92// Search from top of stack to bottom (most recent hover first)93for (let i = this._hoverStack.length - 1; i >= 0; i--) {94for (const targetElement of targetElements) {95if (isAncestor(targetElement, this._hoverStack[i].hover.domNode)) {96return i;97}98}99}100return -1;101}102103constructor(104@IInstantiationService private readonly _instantiationService: IInstantiationService,105@IConfigurationService private readonly _configurationService: IConfigurationService,106@IContextMenuService contextMenuService: IContextMenuService,107@IKeybindingService private readonly _keybindingService: IKeybindingService,108@ILayoutService private readonly _layoutService: ILayoutService,109@IAccessibilityService private readonly _accessibilityService: IAccessibilityService110) {111super();112113this._register(contextMenuService.onDidShowContextMenu(() => this.hideHover()));114115this._register(KeybindingsRegistry.registerCommandAndKeybindingRule({116id: 'workbench.action.showHover',117weight: KeybindingWeight.EditorCore,118primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI),119handler: () => { this._showAndFocusHoverForActiveElement(); },120}));121}122123showInstantHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean, dontShow?: boolean): IHoverWidget | undefined {124const hover = this._createHover(options, skipLastFocusedUpdate);125if (!hover) {126return undefined;127}128this._showHover(hover, options, focus);129return hover.hover;130}131132showDelayedHover(133options: IHoverOptions,134lifecycleOptions: Pick<IHoverLifecycleOptions, 'groupId' | 'reducedDelay'>,135): IHoverWidget | undefined {136// Set `id` to default if it's undefined137if (options.id === undefined) {138options.id = getHoverIdFromContent(options.content);139}140141if (!this._currentDelayedHover || this._currentDelayedHoverWasShown) {142// Current hover is locked, reject143if (this._currentHover?.isLocked) {144return undefined;145}146147// Identity is the same, return current hover148if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {149return this._currentHover;150}151152// Check group identity, if it's the same skip the delay and show the hover immediately153if (this._currentHover && !this._currentHover.isDisposed && this._currentDelayedHoverGroupId !== undefined && this._currentDelayedHoverGroupId === lifecycleOptions?.groupId) {154return this.showInstantHover({155...options,156appearance: {157...options.appearance,158skipFadeInAnimation: true159}160});161}162} else if (this._currentDelayedHover && getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {163// If the hover is the same but timeout is not finished yet, return the current hover164return this._currentDelayedHover;165}166167const hover = this._createHover(options, undefined);168if (!hover) {169this._currentDelayedHover = undefined;170this._currentDelayedHoverWasShown = false;171this._currentDelayedHoverGroupId = undefined;172return undefined;173}174175this._currentDelayedHover = hover.hover;176this._currentDelayedHoverWasShown = false;177this._currentDelayedHoverGroupId = lifecycleOptions?.groupId;178179const delay = lifecycleOptions?.reducedDelay180? this._configurationService.getValue<number>('workbench.hover.reducedDelay')181: this._configurationService.getValue<number>('workbench.hover.delay');182timeout(delay).then(() => {183if (hover.hover && !hover.hover.isDisposed) {184this._currentDelayedHoverWasShown = true;185this._showHover(hover, options);186}187});188189return hover.hover;190}191192setupDelayedHover(193target: HTMLElement,194options: (() => Omit<IHoverOptions, 'target'>) | Omit<IHoverOptions, 'target'>,195lifecycleOptions?: IHoverLifecycleOptions,196): IDisposable {197const resolveHoverOptions = (e?: MouseEvent) => {198const resolved: IHoverOptions = {199...typeof options === 'function' ? options() : options,200target201};202if (resolved.style === HoverStyle.Mouse && e) {203resolved.target = resolveMouseStyleHoverTarget(target, e);204}205return resolved;206};207return this._setupDelayedHover(target, resolveHoverOptions, lifecycleOptions);208}209210setupDelayedHoverAtMouse(211target: HTMLElement,212options: (() => Omit<IHoverOptions, 'target' | 'position'>) | Omit<IHoverOptions, 'target' | 'position'>,213lifecycleOptions?: IHoverLifecycleOptions,214): IDisposable {215const resolveHoverOptions = (e?: MouseEvent) => ({216...typeof options === 'function' ? options() : options,217target: e ? resolveMouseStyleHoverTarget(target, e) : target218} satisfies IHoverOptions);219return this._setupDelayedHover(target, resolveHoverOptions, lifecycleOptions);220}221222private _setupDelayedHover(223target: HTMLElement,224resolveHoverOptions: ((e?: MouseEvent) => IHoverOptions),225lifecycleOptions?: IHoverLifecycleOptions,226) {227const store = new DisposableStore();228store.add(addDisposableListener(target, EventType.MOUSE_OVER, e => {229this.showDelayedHover(resolveHoverOptions(e), {230groupId: lifecycleOptions?.groupId,231reducedDelay: lifecycleOptions?.reducedDelay,232});233}));234if (lifecycleOptions?.setupKeyboardEvents) {235store.add(addDisposableListener(target, EventType.KEY_DOWN, e => {236const evt = new StandardKeyboardEvent(e);237if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) {238this.showInstantHover(resolveHoverOptions(), true);239}240}));241}242243this._delayedHovers.set(target, { show: (focus: boolean) => { this.showInstantHover(resolveHoverOptions(), focus); } });244store.add(toDisposable(() => this._delayedHovers.delete(target)));245246return store;247}248249private _createHover(options: IHoverOptions, skipLastFocusedUpdate?: boolean): ICreateHoverResult | undefined {250this._currentDelayedHover = undefined;251252if (options.content === '') {253return undefined;254}255256// Set `id` to default if it's undefined257if (options.id === undefined) {258options.id = getHoverIdFromContent(options.content);259}260261// Check if the target is inside an existing hover (nesting scenario)262const containingHoverIndex = this._getContainingHoverIndex(options.target);263const isNesting = containingHoverIndex >= 0;264265if (isNesting) {266// Check max nesting depth267if (this._hoverStack.length >= MAX_HOVER_NESTING_DEPTH) {268return undefined;269}270// When nesting, don't check if the parent is locked - we allow nested hovers inside locked parents271} else {272// Not nesting: check if current top-level hover is locked273if (this._currentHover?.isLocked) {274return undefined;275}276277// Check if identity is the same as current hover278if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {279return undefined;280}281}282283this._lastHoverOptions = options;284const trapFocus = options.trapFocus || this._accessibilityService.isScreenReaderOptimized();285const activeElement = getActiveElement();286let lastFocusedElementBeforeOpen: HTMLElement | undefined;287// HACK, remove this check when #189076 is fixed288if (!skipLastFocusedUpdate) {289if (trapFocus && activeElement) {290if (!activeElement.classList.contains('monaco-hover')) {291lastFocusedElementBeforeOpen = activeElement as HTMLElement;292}293}294}295296const hoverDisposables = new DisposableStore();297const hover = this._instantiationService.createInstance(HoverWidget, options);298if (options.persistence?.sticky) {299hover.isLocked = true;300}301302// Adjust target position when a mouse event is provided as the hover position303if (options.position?.hoverPosition && !isNumber(options.position.hoverPosition)) {304options.target = {305targetElements: isHTMLElement(options.target) ? [options.target] : options.target.targetElements,306x: options.position.hoverPosition.x + 10307};308}309310hover.onDispose(() => {311// Pop this hover from the stack if it's still there312const stackIndex = this._hoverStack.findIndex(entry => entry.hover === hover);313if (stackIndex >= 0) {314const entry = this._hoverStack[stackIndex];315// Restore focus if this hover was focused316const hoverWasFocused = isAncestorOfActiveElement(hover.domNode);317if (hoverWasFocused && entry.lastFocusedElementBeforeOpen) {318entry.lastFocusedElementBeforeOpen.focus();319}320// Also dispose all nested hovers (hovers at higher indices in the stack)321// Dispose from end to avoid index shifting issues322while (this._hoverStack.length > stackIndex + 1) {323const nestedEntry = this._hoverStack.pop()!;324nestedEntry.contextView.dispose();325nestedEntry.hover.dispose();326}327// Remove this hover from stack and dispose its context view328this._hoverStack.splice(stackIndex, 1);329entry.contextView.dispose();330}331hoverDisposables.dispose();332}, undefined, hoverDisposables);333334// Set the container explicitly to enable aux window support335if (!options.container) {336const targetElement = isHTMLElement(options.target) ? options.target : options.target.targetElements[0];337options.container = this._layoutService.getContainer(getWindow(targetElement));338}339340if (options.persistence?.sticky) {341hoverDisposables.add(addDisposableListener(getWindow(options.container).document, EventType.MOUSE_DOWN, e => {342if (!isAncestor(e.target as HTMLElement, hover.domNode)) {343this._hideHoverAndDescendants(hover);344}345}));346} else {347if ('targetElements' in options.target) {348for (const element of options.target.targetElements) {349hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this._hideHoverAndDescendants(hover)));350}351} else {352hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this._hideHoverAndDescendants(hover)));353}354const focusedElement = getActiveElement();355if (focusedElement) {356const focusedElementDocument = getWindow(focusedElement).document;357hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));358hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));359hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => this._keyUp(e, hover)));360hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_UP, e => this._keyUp(e, hover)));361}362}363364if ('IntersectionObserver' in mainWindow) {365const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 });366const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target;367observer.observe(firstTargetElement);368hoverDisposables.add(toDisposable(() => observer.disconnect()));369}370371return { hover, lastFocusedElementBeforeOpen, store: hoverDisposables };372}373374private _showHover(result: ICreateHoverResult, options: IHoverOptions, focus?: boolean) {375const { hover, lastFocusedElementBeforeOpen, store } = result;376377// Check if the target is inside an existing hover (nesting scenario)378const containingHoverIndex = this._getContainingHoverIndex(options.target);379const isNesting = containingHoverIndex >= 0;380381// If not nesting, close all existing hovers first382if (!isNesting) {383this._hideAllHovers();384} else {385// When nesting, close any sibling hovers (hovers at the same level or deeper386// than the containing hover). This ensures hovers within the same container387// are exclusive.388for (let i = this._hoverStack.length - 1; i > containingHoverIndex; i--) {389this._hoverStack[i].hover.dispose();390}391this._hoverStack.length = containingHoverIndex + 1;392}393394// When nesting, add the new hover's container to all parent hovers' mouse trackers.395// This makes the parent hovers treat the nested hover as part of themselves,396// so they won't close when the mouse moves into the nested hover.397if (isNesting) {398for (let i = 0; i <= containingHoverIndex; i++) {399store.add(this._hoverStack[i].hover.addMouseTrackingElement(hover.domNode));400}401}402403// Create a new ContextView for this hover with higher z-index for nested hovers404const container = options.container ?? this._layoutService.getContainer(getWindow(isHTMLElement(options.target) ? options.target : options.target.targetElements[0]));405const contextView = new ContextView(container, ContextViewDOMPosition.ABSOLUTE);406407// Push to stack408const stackEntry: IHoverStackEntry = {409hover,410options,411contextView,412lastFocusedElementBeforeOpen413};414this._hoverStack.push(stackEntry);415416// Show the hover in its context view417const delegate = new HoverContextViewDelegate(hover, focus, this._hoverStack.length);418contextView.show(delegate);419420// Set up layout handling421store.add(hover.onRequestLayout(() => contextView.layout()));422423options.onDidShow?.();424}425426/**427* Hides a specific hover and all hovers nested inside it.428*/429private _hideHoverAndDescendants(hover: HoverWidget): void {430const stackIndex = this._hoverStack.findIndex(entry => entry.hover === hover);431if (stackIndex < 0) {432return;433}434435// Dispose all hovers from this index onwards (including nested ones)436for (let i = this._hoverStack.length - 1; i >= stackIndex; i--) {437this._hoverStack[i].hover.dispose();438}439this._hoverStack.length = stackIndex;440}441442/**443* Hides all hovers in the stack.444*/445private _hideAllHovers(): void {446for (let i = this._hoverStack.length - 1; i >= 0; i--) {447this._hoverStack[i].hover.dispose();448}449this._hoverStack.length = 0;450}451452hideHover(force?: boolean): void {453if (this._hoverStack.length === 0) {454return;455}456457// If not forcing and the topmost hover is locked, don't hide458if (!force && this._currentHover?.isLocked) {459return;460}461462// Hide only the topmost hover (pop from stack)463this.doHideHover();464}465466private doHideHover(): void {467// Pop and dispose the topmost hover468const length = this._hoverStack.length;469this._hoverStack[length - 1]?.hover.dispose();470this._hoverStack.length = length - 1;471472// After popping a nested hover, unlock the parent if it was locked due to nesting473// (Note: the parent may have been explicitly locked via sticky, so we only unlock474// if there are remaining hovers and they're not sticky)475// For simplicity, we don't auto-unlock here - the parent remains in its current lock state476}477478private _intersectionChange(entries: IntersectionObserverEntry[], hover: IDisposable): void {479const entry = entries[entries.length - 1];480if (!entry.isIntersecting) {481hover.dispose();482}483}484485showAndFocusLastHover(): void {486if (!this._lastHoverOptions) {487return;488}489this.showInstantHover(this._lastHoverOptions, true, true);490}491492private _showAndFocusHoverForActiveElement(): void {493// TODO: if hover is visible, focus it to avoid flickering494495let activeElement = getActiveElement() as HTMLElement | null;496while (activeElement) {497const hover = this._delayedHovers.get(activeElement) ?? this._managedHovers.get(activeElement);498if (hover) {499hover.show(true);500return;501}502503activeElement = activeElement.parentElement;504}505}506507private _keyDown(e: KeyboardEvent, hover: HoverWidget, hideOnKeyDown: boolean) {508if (e.key === 'Alt') {509// Lock all hovers in the stack when Alt is pressed510for (const entry of this._hoverStack) {511entry.hover.isLocked = true;512}513return;514}515const event = new StandardKeyboardEvent(e);516const keybinding = this._keybindingService.resolveKeyboardEvent(event);517if (keybinding.getSingleModifierDispatchChords().some(value => !!value) || this._keybindingService.softDispatch(event, event.target).kind !== ResultKind.NoMatchingKb) {518return;519}520if (hideOnKeyDown && (!this._currentHoverOptions?.trapFocus || e.key !== 'Tab')) {521// Find the entry for this hover to get its lastFocusedElementBeforeOpen522const stackEntry = this._hoverStack.find(entry => entry.hover === hover);523this._hideHoverAndDescendants(hover);524stackEntry?.lastFocusedElementBeforeOpen?.focus();525}526}527528private _keyUp(e: KeyboardEvent, hover: HoverWidget) {529if (e.key === 'Alt') {530// Unlock all hovers in the stack when Alt is released531for (const entry of this._hoverStack) {532// Only unlock if not sticky533if (!entry.options.persistence?.sticky) {534entry.hover.isLocked = false;535}536}537// Hide all hovers if the mouse is not over any of them538const anyMouseIn = this._hoverStack.some(entry => entry.hover.isMouseIn);539if (!anyMouseIn) {540const topEntry = this._hoverStack[this._hoverStack.length - 1];541this._hideAllHovers();542topEntry?.lastFocusedElementBeforeOpen?.focus();543}544}545}546547// TODO: Investigate performance of this function. There seems to be a lot of content created548// and thrown away on start up549setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover {550if (hoverDelegate.showNativeHover) {551return setupNativeHover(targetElement, content);552}553554targetElement.setAttribute('custom-hover', 'true');555556if (targetElement.title !== '') {557console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.');558console.trace('Stack trace:', targetElement.title);559targetElement.title = '';560}561562let hoverPreparation: IDisposable | undefined;563let hoverWidget: ManagedHoverWidget | undefined;564565const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => {566const hadHover = hoverWidget !== undefined;567if (disposeWidget) {568hoverWidget?.dispose();569hoverWidget = undefined;570}571if (disposePreparation) {572hoverPreparation?.dispose();573hoverPreparation = undefined;574}575if (hadHover) {576hoverDelegate.onDidHideHover?.();577hoverWidget = undefined;578}579};580581const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => {582return new TimeoutTimer(async () => {583if (!hoverWidget || hoverWidget.isDisposed) {584hoverWidget = new ManagedHoverWidget(hoverDelegate, target || targetElement, delay > 0);585await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus });586}587}, delay);588};589590const store = new DisposableStore();591let isMouseDown = false;592store.add(addDisposableListener(targetElement, EventType.MOUSE_DOWN, () => {593isMouseDown = true;594hideHover(true, true);595}, true));596store.add(addDisposableListener(targetElement, EventType.MOUSE_UP, () => {597isMouseDown = false;598}, true));599store.add(addDisposableListener(targetElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => {600isMouseDown = false;601// HACK: `fromElement` is a non-standard property. Not sure what to replace it with,602// `relatedTarget` is NOT equivalent.603interface MouseEventWithFrom extends MouseEvent {604fromElement: Element | null;605}606hideHover(false, (e as MouseEventWithFrom).fromElement === targetElement);607}, true));608store.add(addDisposableListener(targetElement, EventType.MOUSE_OVER, (e: MouseEvent) => {609if (hoverPreparation) {610return;611}612613const mouseOverStore: DisposableStore = new DisposableStore();614615const target: IHoverDelegateTarget = {616targetElements: [targetElement],617dispose: () => { }618};619if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') {620// track the mouse position621const onMouseMove = (e: MouseEvent) => {622target.x = e.x + 10;623if (!eventIsRelatedToTarget(e, targetElement)) {624hideHover(true, true);625}626};627mouseOverStore.add(addDisposableListener(targetElement, EventType.MOUSE_MOVE, onMouseMove, true));628}629630hoverPreparation = mouseOverStore;631632if (!eventIsRelatedToTarget(e, targetElement)) {633return; // Do not show hover when the mouse is over another hover target634}635636mouseOverStore.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));637}, true));638639const onFocus = (e: FocusEvent) => {640if (isMouseDown || hoverPreparation) {641return;642}643if (!eventIsRelatedToTarget(e, targetElement)) {644return; // Do not show hover when the focus is on another hover target645}646647const target: IHoverDelegateTarget = {648targetElements: [targetElement],649dispose: () => { }650};651const toDispose: DisposableStore = new DisposableStore();652const onBlur = () => hideHover(true, true);653toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true));654toDispose.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));655hoverPreparation = toDispose;656};657658// Do not show hover when focusing an input or textarea659if (!isEditableElement(targetElement)) {660store.add(addDisposableListener(targetElement, EventType.FOCUS, onFocus, true));661}662663const hover: IManagedHover = {664show: focus => {665hideHover(false, true); // terminate a ongoing mouse over preparation666triggerShowHover(0, focus, undefined, focus); // show hover immediately667},668hide: () => {669hideHover(true, true);670},671update: async (newContent, hoverOptions) => {672content = newContent;673await hoverWidget?.update(content, undefined, hoverOptions);674},675dispose: () => {676this._managedHovers.delete(targetElement);677store.dispose();678hideHover(true, true);679}680};681this._managedHovers.set(targetElement, hover);682return hover;683}684685showManagedHover(target: HTMLElement): void {686const hover = this._managedHovers.get(target);687if (hover) {688hover.show(true);689}690}691692public override dispose(): void {693this._managedHovers.forEach(hover => hover.dispose());694super.dispose();695}696}697698function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined {699if (options === undefined) {700return undefined;701}702return options?.id ?? options;703}704705function getHoverIdFromContent(content: string | HTMLElement | IMarkdownString): string | undefined {706if (isHTMLElement(content)) {707return undefined;708}709if (typeof content === 'string') {710return content.toString();711}712return content.value;713}714715function getStringContent(contentOrFactory: IManagedHoverContentOrFactory): string | undefined {716const content = typeof contentOrFactory === 'function' ? contentOrFactory() : contentOrFactory;717if (isString(content)) {718// Icons don't render in the native hover so we strip them out719return stripIcons(content);720}721if (isManagedHoverTooltipMarkdownString(content)) {722return content.markdownNotSupportedFallback;723}724return undefined;725}726727function setupNativeHover(targetElement: HTMLElement, content: IManagedHoverContentOrFactory): IManagedHover {728function updateTitle(title: string | undefined) {729if (title) {730targetElement.setAttribute('title', title);731} else {732targetElement.removeAttribute('title');733}734}735736updateTitle(getStringContent(content));737return {738update: (content) => updateTitle(getStringContent(content)),739show: () => { },740hide: () => { },741dispose: () => updateTitle(undefined),742};743}744745class HoverContextViewDelegate implements IDelegate {746747// Render over all other context views, with higher layers for nested hovers748public readonly layer: number;749750get anchorPosition() {751return this._hover.anchor;752}753754constructor(755private readonly _hover: HoverWidget,756private readonly _focus: boolean = false,757stackDepth: number = 1758) {759// Base layer is 1, nested hovers get higher layers760this.layer = stackDepth;761}762763render(container: HTMLElement) {764this._hover.render(container);765if (this._focus) {766this._hover.focus();767}768return this._hover;769}770771getAnchor() {772return {773x: this._hover.x,774y: this._hover.y775};776}777778layout() {779this._hover.layout();780}781}782783function eventIsRelatedToTarget(event: UIEvent, target: HTMLElement): boolean {784return isHTMLElement(event.target) && getHoverTargetElement(event.target, target) === target;785}786787function getHoverTargetElement(element: HTMLElement, stopElement?: HTMLElement): HTMLElement {788stopElement = stopElement ?? getWindow(element).document.body;789while (!element.hasAttribute('custom-hover') && element !== stopElement) {790element = element.parentElement!;791}792return element;793}794795function resolveMouseStyleHoverTarget(target: HTMLElement, e: MouseEvent): IHoverTarget {796return {797targetElements: [target],798x: e.x + 10799};800}801802registerSingleton(IHoverService, HoverService, InstantiationType.Delayed);803804registerThemingParticipant((theme, collector) => {805const hoverBorder = theme.getColor(editorHoverBorder);806if (hoverBorder) {807collector.addRule(`.monaco-hover.workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);808collector.addRule(`.monaco-hover.workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);809}810});811812813