diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 89e5008d3c429..ba3fc0686f6d5 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -9,6 +9,14 @@ height: 100%; } +.explorer-item-hover { + /* -- Must set important as hover overrides the cursor -- */ + cursor: pointer !important; + padding-left: 6px; + height: 22px; + font-size: 13px; +} + .explorer-folders-view .monaco-list-row { padding-left: 4px; /* align top level twistie with `Explorer` title label */ } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 65a9ad1461435..709f03e594836 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -48,7 +48,7 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isNumber } from 'vs/base/common/types'; +import { isNumber, isStringArray } from 'vs/base/common/types'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -62,6 +62,9 @@ import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { timeout } from 'vs/base/common/async'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -275,6 +278,64 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; + private readonly hoverDelegate = new class implements IHoverDelegate { + + readonly placement = 'element'; + + get delay() { + return this.configurationService.getValue('workbench.hover.delay'); + } + + constructor( + private readonly configurationService: IConfigurationService, + private readonly hoverService: IHoverService + ) { } + + showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { + let element: HTMLElement; + if (options.target instanceof HTMLElement) { + element = options.target; + } else { + element = options.target.targetElements[0]; + } + + const row = element.closest('.monaco-tl-row') as HTMLElement | undefined; + + const child = element.querySelector('div.monaco-icon-label-container') as Element | undefined; + const childOfChild = child?.querySelector('span.monaco-icon-name-container') as HTMLElement | undefined; + let overflowed = false; + if (childOfChild && child) { + const width = child.clientWidth; + const childWidth = childOfChild.offsetWidth; + // Check if element is overflowing its parent container + overflowed = width <= childWidth; + } + + const hasDecoration = element.classList.toString().includes('monaco-decoration-iconBadge'); + // If it's overflowing or has a decoration show the tooltip + overflowed = overflowed || hasDecoration; + + const indentGuideElement = row?.querySelector('.monaco-tl-indent') as HTMLElement | undefined; + if (!indentGuideElement) { + return; + } + + return overflowed ? this.hoverService.showHover({ + ...options, + target: indentGuideElement, + compact: true, + additionalClasses: ['explorer-item-hover'], + skipFadeInAnimation: true, + showPointer: false, + onClick: (e) => { + this.hoverService.hideHover(); + element.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); + }, + hoverPosition: HoverPosition.RIGHT, + }, focus) : undefined; + } + }(this.configurationService, this.hoverService); + constructor( container: HTMLElement, private labels: ResourceLabels, @@ -285,7 +346,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); @@ -317,8 +379,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { try { if (templateData.currentContext) { @@ -417,6 +478,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer('hoverService'); @@ -129,6 +128,11 @@ export interface IHoverOptions { trapFocus?: boolean; /** + * A callback which will be executed when the hover is clicked + */ + onClick?(e: MouseEvent): void; + + /* * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover * in. This is particularly useful for more natural tab focusing behavior, where the hover is * created as the next tab index after the element being hovered and/or to workaround the diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index c6fbd3a8d87c8..ab51a888eb3d2 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -64,6 +64,11 @@ export class HoverService implements IHoverService { options.container ); hover.onRequestLayout(() => provider.layout()); + if (options.onClick) { + hoverDisposables.add(addDisposableListener(hover.domNode, EventType.CLICK, e => { + options.onClick!(e); + })); + } if ('targetElements' in options.target) { for (const element of options.target.targetElements) { hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover()));