From 8d41153ffc5558a7eb82a7b5c1866588b944d500 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 31 May 2021 09:55:16 +0200 Subject: [PATCH] support custom hover on status bar entries --- .../browser/ui/iconLabel/iconHoverProvider.ts | 131 ---------------- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 140 +++--------------- .../browser/ui/iconLabel/iconLabelHover.ts | 129 ++++++++++++++++ src/vs/base/common/htmlContent.ts | 10 ++ .../browser/parts/statusbar/statusbarPart.ts | 45 +++++- .../services/hover/browser/hoverWidget.ts | 3 +- .../services/statusbar/common/statusbar.ts | 4 +- 7 files changed, 206 insertions(+), 256 deletions(-) delete mode 100644 src/vs/base/browser/ui/iconLabel/iconHoverProvider.ts create mode 100644 src/vs/base/browser/ui/iconLabel/iconLabelHover.ts diff --git a/src/vs/base/browser/ui/iconLabel/iconHoverProvider.ts b/src/vs/base/browser/ui/iconLabel/iconHoverProvider.ts deleted file mode 100644 index df112a4ae60..00000000000 --- a/src/vs/base/browser/ui/iconLabel/iconHoverProvider.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - -import { isFunction, isString } from 'vs/base/common/types'; -import * as dom from 'vs/base/browser/dom'; -import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { domEvent } from 'vs/base/browser/event'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { localize } from 'vs/nls'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; - -export class CustomHoverProvider extends Disposable { - - private readonly customHovers: Map = new Map(); - - constructor(private hoverDelegate: IHoverDelegate) { - super(); - } - - public setupHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { - const previousCustomHover = this.customHovers.get(htmlElement); - if (previousCustomHover) { - previousCustomHover.dispose(); - this.customHovers.delete(htmlElement); - } - - if (tooltip) { - return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); - } - } - - private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { - if (isString(markdownTooltip)) { - return async () => markdownTooltip; - } else if (isFunction(markdownTooltip.markdown)) { - return markdownTooltip.markdown; - } else { - const markdown = markdownTooltip.markdown; - return async () => markdown; - } - } - - private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void { - let tooltip = this.getTooltipForCustom(markdownTooltip); - - let hoverOptions: IHoverDelegateOptions | undefined; - let mouseX: number | undefined; - let isHovering = false; - let tokenSource: CancellationTokenSource; - let hoverDisposable: IDisposable | undefined; - function mouseOver(this: HTMLElement, e: MouseEvent): void { - if (isHovering) { - return; - } - tokenSource = new CancellationTokenSource(); - function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void { - const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; - if (isMouseDown) { - hoverDisposable?.dispose(); - hoverDisposable = undefined; - } - if (isMouseDown || (e).fromElement === htmlElement) { - isHovering = false; - hoverOptions = undefined; - tokenSource.dispose(true); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); - } - } - const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); - const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); - isHovering = true; - - function mouseMove(this: HTMLElement, e: MouseEvent): void { - mouseX = e.x; - } - const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); - setTimeout(async () => { - if (isHovering && tooltip) { - // Re-use the already computed hover options if they exist. - if (!hoverOptions) { - const target: IHoverDelegateTarget = { - targetElements: [this], - dispose: () => { } - }; - hoverOptions = { - text: localize('iconLabel.loading', "Loading..."), - target, - hoverPosition: HoverPosition.BELOW - }; - hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - - const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); - if (resolvedTooltip) { - hoverOptions = { - text: resolvedTooltip, - target, - hoverPosition: HoverPosition.BELOW - }; - // awaiting the tooltip could take a while. Make sure we're still hovering. - hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - } else if (hoverDisposable) { - hoverDisposable.dispose(); - hoverDisposable = undefined; - } - } - - } - mouseMoveDisposable.dispose(); - }, hoverDelegate.delay); - } - const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); - this.customHovers.set(htmlElement, mouseOverDisposable); - } -} - -function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { - if (hoverOptions && isHovering) { - if (mouseX !== undefined) { - (hoverOptions.target).x = mouseX + 10; - } - return hoverDelegate.showHover(hoverOptions); - } - return undefined; -} diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 22ccd0073d2..d4103d7c8b2 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; import { equals } from 'vs/base/common/objects'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { isFunction, isString } from 'vs/base/common/types'; -import { domEvent } from 'vs/base/browser/event'; -import { localize } from 'vs/nls'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; @@ -91,17 +88,17 @@ class FastLabelNode { export class IconLabel extends Disposable { - private domNode: FastLabelNode; + private readonly domNode: FastLabelNode; - private nameNode: Label | LabelWithHighlights; + private readonly nameNode: Label | LabelWithHighlights; - private descriptionContainer: FastLabelNode; + private readonly descriptionContainer: FastLabelNode; private descriptionNode: FastLabelNode | HighlightedLabel | undefined; - private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; + private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; - private labelContainer: HTMLElement; + private readonly labelContainer: HTMLElement; - private hoverDelegate: IHoverDelegate | undefined = undefined; + private readonly hoverDelegate: IHoverDelegate | undefined; private readonly customHovers: Map = new Map(); constructor(container: HTMLElement, options?: IIconLabelCreationOptions) { @@ -126,9 +123,7 @@ export class IconLabel extends Disposable { this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } - if (options?.hoverDelegate) { - this.hoverDelegate = options.hoverDelegate; - } + this.hoverDelegate = options?.hoverDelegate; } get element(): HTMLElement { @@ -185,116 +180,21 @@ export class IconLabel extends Disposable { } if (!this.hoverDelegate) { - return this.setupNativeHover(htmlElement, tooltip); + setupNativeHover(htmlElement, tooltip); } else { - return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + if (hoverDisposable) { + this.customHovers.set(htmlElement, hoverDisposable); + } } } - private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { - if (hoverOptions && isHovering) { - if (mouseX !== undefined) { - (hoverOptions.target).x = mouseX + 10; - } - return hoverDelegate.showHover(hoverOptions); + public override dispose() { + super.dispose(); + for (const disposable of this.customHovers.values()) { + disposable.dispose(); } - return undefined; - } - - private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { - if (isString(markdownTooltip)) { - return async () => markdownTooltip; - } else if (isFunction(markdownTooltip.markdown)) { - return markdownTooltip.markdown; - } else { - const markdown = markdownTooltip.markdown; - return async () => markdown; - } - } - - private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void { - htmlElement.setAttribute('title', ''); - htmlElement.removeAttribute('title'); - let tooltip = this.getTooltipForCustom(markdownTooltip); - - let hoverOptions: IHoverDelegateOptions | undefined; - let mouseX: number | undefined; - let isHovering = false; - let tokenSource: CancellationTokenSource; - let hoverDisposable: IDisposable | undefined; - function mouseOver(this: HTMLElement, e: MouseEvent): void { - if (isHovering) { - return; - } - tokenSource = new CancellationTokenSource(); - function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void { - const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; - if (isMouseDown) { - hoverDisposable?.dispose(); - hoverDisposable = undefined; - } - if (isMouseDown || (e).fromElement === htmlElement) { - isHovering = false; - hoverOptions = undefined; - tokenSource.dispose(true); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); - } - } - const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); - const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); - isHovering = true; - - function mouseMove(this: HTMLElement, e: MouseEvent): void { - mouseX = e.x; - } - const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); - setTimeout(async () => { - if (isHovering && tooltip) { - // Re-use the already computed hover options if they exist. - if (!hoverOptions) { - const target: IHoverDelegateTarget = { - targetElements: [this], - dispose: () => { } - }; - hoverOptions = { - text: localize('iconLabel.loading', "Loading..."), - target, - hoverPosition: HoverPosition.BELOW - }; - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - - const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); - if (resolvedTooltip) { - hoverOptions = { - text: resolvedTooltip, - target, - hoverPosition: HoverPosition.BELOW - }; - // awaiting the tooltip could take a while. Make sure we're still hovering. - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - } else if (hoverDisposable) { - hoverDisposable.dispose(); - hoverDisposable = undefined; - } - } - - } - mouseMoveDisposable.dispose(); - }, hoverDelegate.delay); - } - const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); - this.customHovers.set(htmlElement, mouseOverDisposable); - } - - private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { - let stringTooltip: string = ''; - if (isString(tooltip)) { - stringTooltip = tooltip; - } else if (tooltip?.markdownNotSupportedFallback) { - stringTooltip = tooltip.markdownNotSupportedFallback; - } - htmlElement.title = stringTooltip; + this.customHovers.clear(); } } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts new file mode 100644 index 00000000000..cad53d127bf --- /dev/null +++ b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isFunction, isString } from 'vs/base/common/types'; +import * as dom from 'vs/base/browser/dom'; +import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { DomEmitter } from 'vs/base/browser/event'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { localize } from 'vs/nls'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; + + +export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { + if (isString(tooltip)) { + htmlElement.title = tooltip; + } else if (tooltip?.markdownNotSupportedFallback) { + htmlElement.title = tooltip.markdownNotSupportedFallback; + } else { + htmlElement.removeAttribute('title'); + } +} + +export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined { + if (!markdownTooltip) { + return undefined; + } + + const tooltip = getTooltipForCustom(markdownTooltip); + + let hoverOptions: IHoverDelegateOptions | undefined; + let mouseX: number | undefined; + let isHovering = false; + let tokenSource: CancellationTokenSource; + let hoverDisposable: IDisposable | undefined; + + const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true); + mouseOverDomEmitter.event((e: MouseEvent) => { + if (isHovering) { + return; + } + tokenSource = new CancellationTokenSource(); + function mouseLeaveOrDown(e: MouseEvent): void { + const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; + if (isMouseDown) { + hoverDisposable?.dispose(); + hoverDisposable = undefined; + } + if (isMouseDown || (e).fromElement === htmlElement) { + isHovering = false; + hoverOptions = undefined; + tokenSource.dispose(true); + mouseLeaveDomEmitter.dispose(); + mouseDownDomEmitter.dispose(); + } + } + const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true); + mouseLeaveDomEmitter.event(mouseLeaveOrDown); + const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true); + mouseDownDomEmitter.event(mouseLeaveOrDown); + isHovering = true; + + function mouseMove(e: MouseEvent): void { + mouseX = e.x; + } + const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true); + mouseMoveDomEmitter.event(mouseMove); + setTimeout(async () => { + if (isHovering && tooltip) { + // Re-use the already computed hover options if they exist. + if (!hoverOptions) { + const target: IHoverDelegateTarget = { + targetElements: [htmlElement], + dispose: () => { } + }; + hoverOptions = { + text: localize('iconLabel.loading', "Loading..."), + target, + hoverPosition: HoverPosition.BELOW + }; + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + + const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); + if (resolvedTooltip) { + hoverOptions = { + text: resolvedTooltip, + target, + hoverPosition: HoverPosition.BELOW + }; + // awaiting the tooltip could take a while. Make sure we're still hovering. + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + } else if (hoverDisposable) { + hoverDisposable.dispose(); + hoverDisposable = undefined; + } + } + + } + mouseMoveDomEmitter.dispose(); + }, hoverDelegate.delay); + }); + return mouseOverDomEmitter; +} + + +function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { + if (isString(markdownTooltip)) { + return async () => markdownTooltip; + } else if (isFunction(markdownTooltip.markdown)) { + return markdownTooltip.markdown; + } else { + const markdown = markdownTooltip.markdown; + return async () => markdown; + } +} + +function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { + if (hoverOptions && isHovering) { + if (mouseX !== undefined) { + (hoverOptions.target).x = mouseX + 10; + } + return hoverDelegate.showHover(hoverOptions); + } + return undefined; +} diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 055de69e55a..50f9a858c90 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -89,6 +89,16 @@ export function isMarkdownString(thing: any): thing is IMarkdownString { return false; } +export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.value === b.value && a.isTrusted === b.isTrusted && a.supportThemeIcons === b.supportThemeIcons; + } +} + export function escapeMarkdownSyntaxTokens(text: string): string { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index aa59b862c83..6603fe67e56 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -43,6 +43,11 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { hash } from 'vs/base/common/hash'; +import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; interface IStatusbarEntryPriority { @@ -426,6 +431,8 @@ export class StatusbarPart extends Part implements IStatusbarService { private leftItemsContainer: HTMLElement | undefined; private rightItemsContainer: HTMLElement | undefined; + private hoverDelegate: IHoverDelegate; + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -434,10 +441,17 @@ export class StatusbarPart extends Part implements IStatusbarService { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.registerListeners(); + + this.hoverDelegate = { + showHover: (options: IHoverDelegateOptions) => hoverService.showHover(options), + delay: configurationService.getValue('workbench.hover.delay') + }; } private registerListeners(): void { @@ -489,7 +503,7 @@ export class StatusbarPart extends Part implements IStatusbarService { // Create item const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); - const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry); + const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate); // Append to parent this.appendOneStatusbarEntry(itemContainer, alignment, priority); @@ -821,6 +835,8 @@ class StatusbarEntryItem extends Disposable { readonly labelContainer: HTMLElement; private readonly label: StatusBarCodiconLabel; + private customHover: IDisposable | undefined; + private entry: IStatusbarEntry | undefined = undefined; get name(): string { return assertIsDefined(this.entry).name; } @@ -833,6 +849,7 @@ class StatusbarEntryItem extends Disposable { constructor( private container: HTMLElement, entry: IStatusbarEntry, + private readonly customHoverDelegate: IHoverDelegate, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -881,8 +898,15 @@ class StatusbarEntryItem extends Disposable { } // Update: Tooltip (on the container, because label can be disabled) - if (!this.entry || entry.tooltip !== this.entry.tooltip) { - if (entry.tooltip) { + if (!this.entry || !isEqualTooltip(this.entry, entry)) { + if (this.customHover) { + this.customHover.dispose(); + this.customHover = undefined; + } + if (isMarkdownString(entry.tooltip)) { + this.container.removeAttribute('title'); + this.customHover = setupCustomHover(this.customHoverDelegate, this.container, { markdown: entry.tooltip, markdownNotSupportedFallback: undefined }); + } else if (entry.tooltip) { this.container.title = entry.tooltip; } else { this.container.title = ''; @@ -993,9 +1017,24 @@ class StatusbarEntryItem extends Disposable { dispose(this.backgroundListener); dispose(this.commandMouseListener); dispose(this.commandKeyboardListener); + if (this.customHover) { + this.customHover.dispose(); + } } } +function isEqualTooltip(e1: IStatusbarEntry, e2: IStatusbarEntry) { + const t1 = e1.tooltip; + const t2 = e2.tooltip; + if (t1 === undefined) { + return t2 === undefined; + } + if (isMarkdownString(t1)) { + return isMarkdownString(t2) && markdownStringEqual(t1, t2); + } + return t1 === t2; +} + registerThemingParticipant((theme, collector) => { if (theme.type !== ColorScheme.HIGH_CONTRAST) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index e1312fe85e5..bf357e33278 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -18,6 +18,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { isString } from 'vs/base/common/types'; const $ = dom.$; type TargetRect = { @@ -66,7 +67,7 @@ export class HoverWidget extends Widget { ) { super(); - this._linkHandler = options.linkHandler || this._openerService.open; + this._linkHandler = options.linkHandler || (url => this._openerService.open(url, { allowCommands: (!isString(options.text) && options.text.isTrusted) })); this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 7ef7ee73e64..56a5a4b1e20 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { Event } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export const IStatusbarService = createDecorator('statusbarService'); @@ -48,7 +49,7 @@ export interface IStatusbarEntry { /** * An optional tooltip text to show when you hover over the entry */ - readonly tooltip?: string; + readonly tooltip?: string | IMarkdownString; /** * An optional color to use for the entry @@ -74,6 +75,7 @@ export interface IStatusbarEntry { * Will enable a spinning icon in front of the text to indicate progress. */ readonly showProgress?: boolean; + } export interface IStatusbarService {