diff --git a/src/vs/workbench/contrib/hover/browser/hover.contribution.ts b/src/vs/workbench/contrib/hover/browser/hover.contribution.ts new file mode 100644 index 0000000000000..372c4a9be8b98 --- /dev/null +++ b/src/vs/workbench/contrib/hover/browser/hover.contribution.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/hover'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { HoverService } from 'vs/workbench/contrib/hover/browser/hoverService'; +import { IHoverService } from 'vs/workbench/contrib/hover/browser/hover'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; + +registerSingleton(IHoverService, HoverService, true); + +registerThemingParticipant((theme, collector) => { + const hoverBackground = theme.getColor(editorHoverBackground); + if (hoverBackground) { + collector.addRule(`.monaco-workbench .workbench-hover { background-color: ${hoverBackground}; }`); + } + const hoverBorder = theme.getColor(editorHoverBorder); + if (hoverBorder) { + collector.addRule(`.monaco-workbench .workbench-hover { border: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-workbench .workbench-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.monaco-workbench .workbench-hover a { color: ${link}; }`); + } + const hoverForeground = theme.getColor(editorHoverForeground); + if (hoverForeground) { + collector.addRule(`.monaco-workbench .workbench-hover { color: ${hoverForeground}; }`); + } + const actionsBackground = theme.getColor(editorHoverStatusBarBackground); + if (actionsBackground) { + collector.addRule(`.monaco-workbench .workbench-hover .hover-row .actions { background-color: ${actionsBackground}; }`); + } + const codeBackground = theme.getColor(textCodeBlockBackground); + if (codeBackground) { + collector.addRule(`.monaco-workbench .workbench-hover code { background-color: ${codeBackground}; }`); + } +}); diff --git a/src/vs/workbench/contrib/hover/browser/hover.ts b/src/vs/workbench/contrib/hover/browser/hover.ts new file mode 100644 index 0000000000000..aeda610a76e7e --- /dev/null +++ b/src/vs/workbench/contrib/hover/browser/hover.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; + +export const IHoverService = createDecorator('hoverService'); + +/** + * Enables the convenient display of rich markdown-based hovers in the workbench. + */ +export interface IHoverService { + readonly _serviceBrand: undefined; + + /** + * Shows a hover. + * @param options A set of options defining the characteristics of the hover. + * @param focus Whether to focus the hover (useful for keyboard accessibility). + * + * **Example:** A simple usage with a single element target. + * + * ```typescript + * showHover({ + * text: new MarkdownString('Hello world'), + * target: someElement + * }); + * ``` + */ + showHover(options: IHoverOptions, focus?: boolean): void; + + /** + * Hides the hover if it was visible. + */ + hideHover(): void; +} + +export interface IHoverOptions { + /** + * The text to display in the primary section of the hover. + */ + text: IMarkdownString; + + /** + * The target for the hover. This determines the position of the hover and it will only be + * hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for + * simple cases and a IHoverTarget for more complex cases where multiple elements and/or a + * dispose method is required. + */ + target: IHoverTarget | HTMLElement; + + /** + * A set of actions for the hover's "status bar". + */ + actions?: IHoverAction[]; + + /** + * An optional array of classes to add to the hover element. + */ + additionalClasses?: string[]; + + /** + * An optional link handler for markdown links, if this is not provided the IOpenerService will + * be used to open the links using its default options. + */ + linkHandler?(url: string): void; +} + +export interface IHoverAction { + /** + * The label to use in the hover's status bar. + */ + label: string; + + /** + * The command ID of the action, this is used to resolve the keybinding to display after the + * action label. + */ + commandId: string; + + /** + * An optional class of an icon that will be displayed before the label. + */ + iconClass?: string; + + /** + * The callback to run the action. + * @param target The action element that was activated. + */ + run(target: HTMLElement): void; +} + +/** + * A target for a hover. + */ +export interface IHoverTarget extends IDisposable { + /** + * A set of target elements used to position the hover. If multiple elements are used the hover + * will try to not overlap any target element. An example use case for this is show a hover for + * wrapped text. + */ + readonly targetElements: readonly HTMLElement[]; +} diff --git a/src/vs/workbench/contrib/hover/browser/hoverService.ts b/src/vs/workbench/contrib/hover/browser/hoverService.ts new file mode 100644 index 0000000000000..590e3c2859d75 --- /dev/null +++ b/src/vs/workbench/contrib/hover/browser/hoverService.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IHoverService, IHoverOptions } from 'vs/workbench/contrib/hover/browser/hover'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { HoverWidget } from 'vs/workbench/contrib/hover/browser/hoverWidget'; +import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; + +export class HoverService implements IHoverService { + declare readonly _serviceBrand: undefined; + + private _currentHoverOptions: IHoverOptions | undefined; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextViewService private readonly _contextViewService: IContextViewService + ) { + } + + showHover(options: IHoverOptions, focus?: boolean): void { + if (this._currentHoverOptions === options) { + return; + } + this._currentHoverOptions = options; + + const hover = this._instantiationService.createInstance(HoverWidget, options); + const provider = this._contextViewService as IContextViewProvider; + const contextViewDelegate: IDelegate = { + render: container => { + hover.render(container); + hover.onDispose(() => this._currentHoverOptions = undefined); + if (focus) { + hover.focus(); + } + return hover; + }, + anchorPosition: hover.anchor, + getAnchor: () => ({ x: hover.x, y: hover.y }), + layout: () => hover.layout() + }; + provider.showContextView(contextViewDelegate); + hover.onRequestLayout(() => provider.layout()); + } + + hideHover(): void { + if (!this._currentHoverOptions) { + return; + } + this._currentHoverOptions = undefined; + this._contextViewService.hideContextView(); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/hoverWidget.ts b/src/vs/workbench/contrib/hover/browser/hoverWidget.ts similarity index 50% rename from src/vs/workbench/contrib/terminal/browser/widgets/hoverWidget.ts rename to src/vs/workbench/contrib/hover/browser/hoverWidget.ts index 5a5e1d5e307ce..e6926919f90fe 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/hoverWidget.ts +++ b/src/vs/workbench/contrib/hover/browser/hoverWidget.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { Event, Emitter } from 'vs/base/common/event'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorHoverHighlight, editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; import * as dom from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IHoverTarget, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; +import { IHoverTarget, IHoverOptions } from 'vs/workbench/contrib/hover/browser/hover'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { HoverWidget as BaseHoverWidget, renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; import { Widget } from 'vs/base/browser/ui/widget'; +import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -25,28 +24,44 @@ export class HoverWidget extends Widget { private readonly _mouseTracker: CompositeMouseTracker; private readonly _hover: BaseHoverWidget; + private readonly _target: IHoverTarget; + private readonly _linkHandler: (url: string) => any; private _isDisposed: boolean = false; + private _anchor: AnchorPosition = AnchorPosition.ABOVE; + private _x: number = 0; + private _y: number = 0; get isDisposed(): boolean { return this._isDisposed; } get domNode(): HTMLElement { return this._hover.containerDomNode; } - private readonly _onDispose = new Emitter(); + private readonly _onDispose = this._register(new Emitter()); get onDispose(): Event { return this._onDispose.event; } + private readonly _onRequestLayout = this._register(new Emitter()); + get onRequestLayout(): Event { return this._onRequestLayout.event; } + + get anchor(): AnchorPosition { return this._anchor; } + get x(): number { return this._x; } + get y(): number { return this._y; } constructor( - private _container: HTMLElement, - private _target: IHoverTarget, - private _text: IMarkdownString, - private _linkHandler: (url: string) => void, - private _actions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }[] | undefined, + options: IHoverOptions, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IOpenerService private readonly _openerService: IOpenerService ) { super(); + this._linkHandler = options.linkHandler || this._openerService.open; + + this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); + this._hover = this._register(new BaseHoverWidget()); - this._hover.containerDomNode.classList.add('terminal-hover-widget', 'fadeIn', 'xterm-hover'); + + this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn'); + if (options.additionalClasses) { + this._hover.containerDomNode.classList.add(...options.additionalClasses); + } // Don't allow mousedown out of the widget, otherwise preventDefault will call and text will // not be selected. @@ -61,7 +76,7 @@ export class HoverWidget extends Widget { const rowElement = $('div.hover-row.markdown-hover'); const contentsElement = $('div.hover-contents'); - const markdownElement = renderMarkdown(this._text, { + const markdownElement = renderMarkdown(options.text, { actionHandler: { callback: (content) => this._linkHandler(content), disposeables: this._messageListeners @@ -72,75 +87,70 @@ export class HoverWidget extends Widget { }, codeBlockRenderCallback: () => { contentsElement.classList.add('code-hover-contents'); - this.layout(); + // This changes the dimensions of the hover so trigger a layout + this._onRequestLayout.fire(); } }); contentsElement.appendChild(markdownElement); rowElement.appendChild(contentsElement); this._hover.contentsDomNode.appendChild(rowElement); - if (this._actions && this._actions.length > 0) { + if (options.actions && options.actions.length > 0) { const statusBarElement = $('div.hover-row.status-bar'); const actionsElement = $('div.actions'); - this._actions.forEach(action => { + options.actions.forEach(action => { const keybinding = this._keybindingService.lookupKeybinding(action.commandId); const keybindingLabel = keybinding ? keybinding.getLabel() : null; - renderHoverAction(actionsElement, action, keybindingLabel); + renderHoverAction(actionsElement, { + label: action.label, + commandId: action.commandId, + run: e => { + action.run(e); + this.dispose(); + }, + iconClass: action.iconClass + }, keybindingLabel); }); statusBarElement.appendChild(actionsElement); this._hover.containerDomNode.appendChild(statusBarElement); } - this._mouseTracker = new CompositeMouseTracker([this._hover.containerDomNode, ..._target.targetElements]); + this._mouseTracker = new CompositeMouseTracker([this._hover.containerDomNode, ...this._target.targetElements]); this._register(this._mouseTracker.onMouseOut(() => this.dispose())); this._register(this._mouseTracker); + } - this._container.appendChild(this._hover.containerDomNode); + public render(container?: HTMLElement): void { + if (this._hover.containerDomNode.parentElement !== container) { + container?.appendChild(this._hover.containerDomNode); + } this.layout(); } - public layout(): void { - const anchor = this._target.anchor; - + public layout() { this._hover.containerDomNode.classList.remove('right-aligned'); this._hover.contentsDomNode.style.maxHeight = ''; - if (anchor.horizontalAnchorSide === HorizontalAnchorSide.Left) { - if (anchor.x + this._hover.containerDomNode.clientWidth > document.documentElement.clientWidth) { - // Shift the hover to the left when part of it would get cut off - const width = Math.round(this._hover.containerDomNode.clientWidth); - this._hover.containerDomNode.style.width = `${width - 1}px`; - this._hover.containerDomNode.style.maxWidth = ''; - const left = document.documentElement.clientWidth - width - 1; - this._hover.containerDomNode.style.left = `${left}px`; - // Right align if the right edge is closer to the anchor than the left edge - if (left + width / 2 < anchor.x) { - this._hover.containerDomNode.classList.add('right-aligned'); - } - } else { - this._hover.containerDomNode.style.width = ''; - this._hover.containerDomNode.style.maxWidth = `${document.documentElement.clientWidth - anchor.x - 1}px`; - this._hover.containerDomNode.style.left = `${anchor.x}px`; - } + + // Get horizontal alignment and position + const targetBounds = this._target.targetElements.map(e => e.getBoundingClientRect()); + const targetLeft = Math.min(...targetBounds.map(e => e.left)); + if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) { + this._x = document.documentElement.clientWidth; + this._hover.containerDomNode.classList.add('right-aligned'); } else { - this._hover.containerDomNode.style.right = `${anchor.x}px`; + this._x = targetLeft; } - // Use fallback y value if there is not enough vertical space - if (anchor.verticalAnchorSide === VerticalAnchorSide.Bottom) { - if (anchor.y + this._hover.containerDomNode.clientHeight > document.documentElement.clientHeight) { - this._hover.containerDomNode.style.top = `${anchor.fallbackY}px`; - this._hover.contentsDomNode.style.maxHeight = `${document.documentElement.clientHeight - anchor.fallbackY}px`; - } else { - this._hover.containerDomNode.style.bottom = `${anchor.y}px`; - this._hover.containerDomNode.style.maxHeight = ''; - } + + // Get vertical alignment and position + const targetTop = Math.min(...targetBounds.map(e => e.top)); + if (targetTop - this._hover.containerDomNode.clientHeight < 0) { + this._anchor = AnchorPosition.BELOW; + this._y = Math.max(...targetBounds.map(e => e.bottom)) - 2; } else { - if (anchor.y + this._hover.containerDomNode.clientHeight > document.documentElement.clientHeight) { - this._hover.containerDomNode.style.bottom = `${anchor.fallbackY}px`; - } else { - this._hover.containerDomNode.style.top = `${anchor.y}px`; - } + this._y = targetTop; } + this._hover.onContentsChanged(); } @@ -210,40 +220,15 @@ class CompositeMouseTracker extends Widget { } } +class ElementHoverTarget implements IHoverTarget { + readonly targetElements: readonly HTMLElement[]; -registerThemingParticipant((theme, collector) => { - let editorHoverHighlightColor = theme.getColor(editorHoverHighlight); - if (editorHoverHighlightColor) { - if (editorHoverHighlightColor.isOpaque()) { - editorHoverHighlightColor = editorHoverHighlightColor.transparent(0.5); - } - collector.addRule(`.integrated-terminal .hoverHighlight { background-color: ${editorHoverHighlightColor}; }`); - } - const hoverBackground = theme.getColor(editorHoverBackground); - if (hoverBackground) { - collector.addRule(`.integrated-terminal .monaco-hover { background-color: ${hoverBackground}; }`); - } - const hoverBorder = theme.getColor(editorHoverBorder); - if (hoverBorder) { - collector.addRule(`.integrated-terminal .monaco-hover { border: 1px solid ${hoverBorder}; }`); - collector.addRule(`.integrated-terminal .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.integrated-terminal .monaco-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.integrated-terminal .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.integrated-terminal .monaco-hover a { color: ${link}; }`); - } - const hoverForeground = theme.getColor(editorHoverForeground); - if (hoverForeground) { - collector.addRule(`.integrated-terminal .monaco-hover { color: ${hoverForeground}; }`); - } - const actionsBackground = theme.getColor(editorHoverStatusBarBackground); - if (actionsBackground) { - collector.addRule(`.integrated-terminal .monaco-hover .hover-row .actions { background-color: ${actionsBackground}; }`); + constructor( + private _element: HTMLElement + ) { + this.targetElements = [this._element]; } - const codeBackground = theme.getColor(textCodeBlockBackground); - if (codeBackground) { - collector.addRule(`.integrated-terminal .monaco-hover code { background-color: ${codeBackground}; }`); + + dispose(): void { } -}); +} diff --git a/src/vs/workbench/contrib/hover/browser/media/hover.css b/src/vs/workbench/contrib/hover/browser/media/hover.css new file mode 100644 index 0000000000000..47d8ab484c639 --- /dev/null +++ b/src/vs/workbench/contrib/hover/browser/media/hover.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .workbench-hover { + position: relative; + font-size: 14px; + line-height: 19px; + animation: fadein 100ms linear; + /* Must be higher than sash's z-index and terminal canvases */ + z-index: 40; + overflow: hidden; + max-width: 700px; +} + +.monaco-workbench .workbench-hover a { + color: #3794ff; +} + +.monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions { + flex-direction: row-reverse; +} + +.monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions .action-container { + margin-right: 0; + margin-left: 16px; +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/widgets.css b/src/vs/workbench/contrib/terminal/browser/media/widgets.css index d33cfe12a546a..51a9ab8eff272 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/widgets.css +++ b/src/vs/workbench/contrib/terminal/browser/media/widgets.css @@ -12,29 +12,6 @@ overflow: visible; } -.monaco-workbench .terminal-hover-widget { - position: fixed; - font-size: 14px; - line-height: 19px; - animation: fadein 100ms linear; - /* Must be higher than sash's z-index and terminal canvases */ - z-index: 40; - overflow: hidden; -} - -.monaco-workbench .terminal-hover-widget a { - color: #3794ff; -} - -.monaco-workbench .terminal-hover-widget.right-aligned .hover-row.status-bar .actions { - flex-direction: row-reverse; -} - -.monaco-workbench .terminal-hover-widget.right-aligned .hover-row.status-bar .actions .action-container { - margin-right: 0; - margin-left: 16px; -} - .monaco-workbench .terminal-overlay-widget { position: absolute; left: 0; diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts index 974d0777202fa..be7027f563f40 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts @@ -6,28 +6,27 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { ITerminalWidget, IHoverTarget, IHoverAnchor, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { HoverWidget } from 'vs/workbench/contrib/terminal/browser/widgets/hoverWidget'; +import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import * as dom from 'vs/base/browser/dom'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IHoverService, IHoverOptions } from 'vs/workbench/contrib/hover/browser/hover'; export class EnvironmentVariableInfoWidget extends Widget implements ITerminalWidget { readonly id = 'env-var-info'; private _domNode: HTMLElement | undefined; private _container: HTMLElement | undefined; - private _hoverWidget: HoverWidget | undefined; private _mouseMoveListener: IDisposable | undefined; + private _hoverOptions: IHoverOptions | undefined; get requiresAction() { return this._info.requiresAction; } constructor( private _info: IEnvironmentVariableInfo, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IHoverService private readonly _hoverService: IHoverService ) { super(); } @@ -41,7 +40,6 @@ export class EnvironmentVariableInfoWidget extends Widget implements ITerminalWi } container.appendChild(this._domNode); - const timeout = this._configurationService.getValue('editor.hover.delay'); const scheduler: RunOnceScheduler = new RunOnceScheduler(() => this._showHover(), timeout); this._register(scheduler); @@ -74,42 +72,21 @@ export class EnvironmentVariableInfoWidget extends Widget implements ITerminalWi } focus() { - this._showHover(); - this._hoverWidget?.focus(); + this._showHover(true); } - private _showHover() { - if (!this._domNode || !this._container || this._hoverWidget) { + private _showHover(focus?: boolean) { + if (!this._domNode || !this._container) { return; } - const target = new ElementHoverTarget(this._domNode); - const actions = this._info.getActions ? this._info.getActions() : undefined; - this._hoverWidget = this._instantiationService.createInstance(HoverWidget, this._container, target, new MarkdownString(this._info.getInfo()), () => { }, actions); - this._register(this._hoverWidget); - this._register(this._hoverWidget.onDispose(() => this._hoverWidget = undefined)); - } -} - -class ElementHoverTarget implements IHoverTarget { - readonly targetElements: readonly HTMLElement[]; - - constructor( - private _element: HTMLElement - ) { - this.targetElements = [this._element]; - } - - get anchor(): IHoverAnchor { - const position = dom.getDomNodePagePosition(this._element); - return { - x: position.left, - horizontalAnchorSide: HorizontalAnchorSide.Left, - y: document.documentElement.clientHeight - position.top - 1, - verticalAnchorSide: VerticalAnchorSide.Bottom, - fallbackY: position.top + position.height - }; - } - - dispose(): void { + if (!this._hoverOptions) { + const actions = this._info.getActions ? this._info.getActions() : undefined; + this._hoverOptions = { + target: this._domNode, + text: new MarkdownString(this._info.getInfo()), + actions + }; + } + this._hoverService.showHover(this._hoverOptions, focus); } } diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index 438ae87c290ba..460f320761e0e 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -6,11 +6,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Widget } from 'vs/base/browser/ui/widget'; -import { ITerminalWidget, IHoverAnchor, IHoverTarget, HorizontalAnchorSide, VerticalAnchorSide } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { HoverWidget } from 'vs/workbench/contrib/terminal/browser/widgets/hoverWidget'; +import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; import * as dom from 'vs/base/browser/dom'; import { IViewportRange } from 'xterm'; +import { IHoverTarget, IHoverService } from 'vs/workbench/contrib/hover/browser/hover'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { editorHoverHighlight } from 'vs/platform/theme/common/colorRegistry'; const $ = dom.$; @@ -28,8 +29,8 @@ export class TerminalHover extends Disposable implements ITerminalWidget { constructor( private readonly _targetOptions: ILinkHoverTargetOptions, private readonly _text: IMarkdownString, - private readonly _linkHandler: (url: string) => void, - @IInstantiationService private readonly _instantiationService: IInstantiationService + private readonly _linkHandler: (url: string) => any, + @IHoverService private readonly _hoverService: IHoverService ) { super(); } @@ -40,92 +41,91 @@ export class TerminalHover extends Disposable implements ITerminalWidget { attach(container: HTMLElement): void { const target = new CellHoverTarget(container, this._targetOptions); - this._register(this._instantiationService.createInstance(HoverWidget, container, target, this._text, this._linkHandler, [])); + this._hoverService.showHover({ + target, + text: this._text, + linkHandler: this._linkHandler, + // .xterm-hover lets xterm know that the hover is part of a link + additionalClasses: ['xterm-hover'] + }); } } class CellHoverTarget extends Widget implements IHoverTarget { - private _domNode: HTMLElement; - private _isDisposed: boolean = false; + private _domNode: HTMLElement | undefined; + private readonly _targetElements: HTMLElement[] = []; - readonly targetElements: readonly HTMLElement[]; + get targetElements(): readonly HTMLElement[] { return this._targetElements; } constructor( - private readonly _container: HTMLElement, - o: ILinkHoverTargetOptions + container: HTMLElement, + private readonly _options: ILinkHoverTargetOptions ) { super(); - this._domNode = $('div.terminal-hover-targets'); - const targets: HTMLElement[] = []; - const rowCount = o.viewportRange.end.y - o.viewportRange.start.y + 1; + this._domNode = $('div.terminal-hover-targets.xterm-hover'); + const rowCount = this._options.viewportRange.end.y - this._options.viewportRange.start.y + 1; // Add top target row - const width = (o.viewportRange.end.y > o.viewportRange.start.y ? o.terminalDimensions.width - o.viewportRange.start.x : o.viewportRange.end.x - o.viewportRange.start.x + 1) * o.cellDimensions.width; + const width = (this._options.viewportRange.end.y > this._options.viewportRange.start.y ? this._options.terminalDimensions.width - this._options.viewportRange.start.x : this._options.viewportRange.end.x - this._options.viewportRange.start.x + 1) * this._options.cellDimensions.width; const topTarget = $('div.terminal-hover-target.hoverHighlight'); - topTarget.style.left = `${o.viewportRange.start.x * o.cellDimensions.width}px`; - topTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.start.y - 1) * o.cellDimensions.height}px`; + topTarget.style.left = `${this._options.viewportRange.start.x * this._options.cellDimensions.width}px`; + topTarget.style.bottom = `${(this._options.terminalDimensions.height - this._options.viewportRange.start.y - 1) * this._options.cellDimensions.height}px`; topTarget.style.width = `${width}px`; - topTarget.style.height = `${o.cellDimensions.height}px`; - targets.push(this._domNode.appendChild(topTarget)); + topTarget.style.height = `${this._options.cellDimensions.height}px`; + this._targetElements.push(this._domNode.appendChild(topTarget)); // Add middle target rows if (rowCount > 2) { const middleTarget = $('div.terminal-hover-target.hoverHighlight'); middleTarget.style.left = `0px`; - middleTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.start.y - 1 - (rowCount - 2)) * o.cellDimensions.height}px`; - middleTarget.style.width = `${o.terminalDimensions.width * o.cellDimensions.width}px`; - middleTarget.style.height = `${(rowCount - 2) * o.cellDimensions.height}px`; - targets.push(this._domNode.appendChild(middleTarget)); + middleTarget.style.bottom = `${(this._options.terminalDimensions.height - this._options.viewportRange.start.y - 1 - (rowCount - 2)) * this._options.cellDimensions.height}px`; + middleTarget.style.width = `${this._options.terminalDimensions.width * this._options.cellDimensions.width}px`; + middleTarget.style.height = `${(rowCount - 2) * this._options.cellDimensions.height}px`; + this._targetElements.push(this._domNode.appendChild(middleTarget)); } // Add bottom target row if (rowCount > 1) { const bottomTarget = $('div.terminal-hover-target.hoverHighlight'); bottomTarget.style.left = `0px`; - bottomTarget.style.bottom = `${(o.terminalDimensions.height - o.viewportRange.end.y - 1) * o.cellDimensions.height}px`; - bottomTarget.style.width = `${(o.viewportRange.end.x + 1) * o.cellDimensions.width}px`; - bottomTarget.style.height = `${o.cellDimensions.height}px`; - targets.push(this._domNode.appendChild(bottomTarget)); + bottomTarget.style.bottom = `${(this._options.terminalDimensions.height - this._options.viewportRange.end.y - 1) * this._options.cellDimensions.height}px`; + bottomTarget.style.width = `${(this._options.viewportRange.end.x + 1) * this._options.cellDimensions.width}px`; + bottomTarget.style.height = `${this._options.cellDimensions.height}px`; + this._targetElements.push(this._domNode.appendChild(bottomTarget)); } - this.targetElements = targets; - - if (o.modifierDownCallback && o.modifierUpCallback) { + if (this._options.modifierDownCallback && this._options.modifierUpCallback) { let down = false; this._register(dom.addDisposableListener(document, 'keydown', e => { if (e.ctrlKey && !down) { down = true; - o.modifierDownCallback!(); + this._options.modifierDownCallback!(); } })); this._register(dom.addDisposableListener(document, 'keyup', e => { if (!e.ctrlKey) { down = false; - o.modifierUpCallback!(); + this._options.modifierUpCallback!(); } })); } - this._container.appendChild(this._domNode); + container.appendChild(this._domNode); } dispose(): void { - if (!this._isDisposed) { - this._container.removeChild(this._domNode); - } - this._isDisposed = true; + this._domNode?.parentElement?.removeChild(this._domNode); super.dispose(); } +} - get anchor(): IHoverAnchor { - const firstPosition = dom.getDomNodePagePosition(this.targetElements[0]); - return { - x: firstPosition.left, - horizontalAnchorSide: HorizontalAnchorSide.Left, - y: document.documentElement.clientHeight - firstPosition.top - 1, - verticalAnchorSide: VerticalAnchorSide.Bottom, - fallbackY: firstPosition.top + firstPosition.height - 1 - }; +registerThemingParticipant((theme, collector) => { + let editorHoverHighlightColor = theme.getColor(editorHoverHighlight); + if (editorHoverHighlightColor) { + if (editorHoverHighlightColor.isOpaque()) { + editorHoverHighlightColor = editorHoverHighlightColor.transparent(0.5); + } + collector.addRule(`.integrated-terminal .hoverHighlight { background-color: ${editorHoverHighlightColor}; }`); } -} +}); diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts index d9071035e9713..50bc2ee7e5e28 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgets.ts @@ -12,32 +12,3 @@ export interface ITerminalWidget extends IDisposable { id: string; attach(container: HTMLElement): void; } - -export enum HorizontalAnchorSide { - Left, - Right -} - -export enum VerticalAnchorSide { - Top, - Bottom -} - -export interface IHoverAnchor { - x: number; - y: number; - horizontalAnchorSide: HorizontalAnchorSide; - verticalAnchorSide: VerticalAnchorSide; - /** - * Fallback Y value to try with opposite VerticalAlignment if the hover does not fit vertically. - */ - fallbackY: number; -} - -/** - * A target for a hover which can know about domain-specific locations. - */ -export interface IHoverTarget extends IDisposable { - readonly targetElements: readonly HTMLElement[]; - readonly anchor: IHoverAnchor; -} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index dfa6a353a3460..4663e8b4160f8 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -281,4 +281,7 @@ import 'vs/workbench/contrib/welcome/common/viewsWelcome.contribution'; // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; +// Hover +import 'vs/workbench/contrib/hover/browser/hover.contribution'; + //#endregion