Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rich tooltips for plugin trees and VSX extensions #10108

Merged
merged 8 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000/",
"webRoot": "${workspaceRoot}"
"webRoot": "${workspaceFolder}"
},
{
"type": "chrome",
Expand Down Expand Up @@ -185,8 +185,17 @@
"name": "Launch Electron Backend & Frontend",
"configurations": [
"Launch Electron Backend",
"Attach to Plugin Host",
"Attach to Electron Frontend"
]
},
{
"name": "Launch Browser Backend & Frontend",
"configurations": [
"Launch Browser Backend",
"Attach to Plugin Host",
"Launch Browser Frontend"
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thegecko should the changes be added to a separate commit, I believe they are overall unrelated to the pull-request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

}
]
}
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"perfect-scrollbar": "^1.3.0",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react-tooltip": "^4.2.21",
"react-virtualized": "^9.20.0",
"reconnecting-websocket": "^4.2.0",
"reflect-metadata": "^0.1.10",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
DefaultBreadcrumbRenderer,
} from './breadcrumbs';
import { RendererHost } from './widgets';
import { TooltipService, TooltipServiceImpl } from './tooltip-service';

export { bindResourceProvider, bindMessageService, bindPreferenceService };

Expand Down Expand Up @@ -186,6 +187,9 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(CommandOpenHandler).toSelf().inSingletonScope();
bind(OpenHandler).toService(CommandOpenHandler);

bind(TooltipServiceImpl).toSelf().inSingletonScope();
bind(TooltipService).toService(TooltipServiceImpl);

bindContributionProvider(bind, ApplicationShellLayoutMigration);
bind<ApplicationShellLayoutMigration>(ApplicationShellLayoutMigration).toConstantValue({
layoutVersion: 2.0,
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/browser/frontend-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { FrontendApplicationStateService } from './frontend-application-state';
import { preventNavigation, parseCssTime, animationFrame } from './browser';
import { CorePreferences } from './core-preferences';
import { WindowService } from './window/window-service';
import { TooltipService } from './tooltip-service';

/**
* Clients can implement to get a callback for contributing widgets to a shell on start.
Expand Down Expand Up @@ -100,6 +101,9 @@ export class FrontendApplication {
@inject(WindowService)
protected readonly windowsService: WindowService;

@inject(TooltipService)
protected readonly tooltipService: TooltipService;

constructor(
@inject(CommandRegistry) protected readonly commands: CommandRegistry,
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry,
Expand Down Expand Up @@ -130,6 +134,7 @@ export class FrontendApplication {

const host = await this.getHost();
this.attachShell(host);
this.attachTooltip(host);
await animationFrame();
this.stateService.state = 'attached_shell';

Expand Down Expand Up @@ -221,6 +226,13 @@ export class FrontendApplication {
Widget.attach(this.shell, host, ref);
}

/**
* Attach the tooltip container to the host element.
*/
protected attachTooltip(host: HTMLElement): void {
this.tooltipService.attachTo(host);
}

/**
* If a startup indicator is present, it is first hidden with the `theia-hidden` CSS class and then
* removed after a while. The delay until removal is taken from the CSS transition duration.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ export * from './diff-uris';
export * from './core-preferences';
export * from './view-container';
export * from './breadcrumbs';
export * from './tooltip-service';
1 change: 1 addition & 0 deletions packages/core/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,4 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
@import './quick-title-bar.css';
@import './progress-bar.css';
@import './breadcrumbs.css';
@import './tooltip.css';
28 changes: 28 additions & 0 deletions packages/core/src/browser/style/tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/********************************************************************************
* Copyright (C) 2021 Arm and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

.theia-tooltip {
color: var(--theia-list-hoverForeground) !important;
background: var(--theia-list-hoverBackground) !important;
border: 1px solid !important;
border-color: var(--theia-list-hoverForeground) !important;
thegecko marked this conversation as resolved.
Show resolved Hide resolved
}

/* Hide tooltip arrow */
.theia-tooltip::before,
.theia-tooltip::after {
border: none !important;
}
74 changes: 74 additions & 0 deletions packages/core/src/browser/tooltip-service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/********************************************************************************
* Copyright (C) 2021 Arm and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject, optional } from 'inversify';
import * as React from 'react';
import ReactTooltip from 'react-tooltip';
import { ReactRenderer, RendererHost } from './widgets/react-renderer';
import { v4 } from 'uuid';

export const TooltipService = Symbol('TooltipService');

export interface TooltipService {
attachTo(host: HTMLElement): void;
tooltipId: string;
update(): void;
}

/**
* Attributes to be added to an HTML element to enable
* rich HTML tooltip rendering
*/
export interface TooltipAttributes {
/**
* HTML to render in the tooltip.
*/
'data-tip': string;
/**
* The ID of the tooltip renderer. Should be TOOLTIP_ID.
*/
'data-for': string;
}

@injectable()
export class TooltipServiceImpl extends ReactRenderer implements TooltipService {
public readonly tooltipId: string;
protected rendered = false;

constructor(
@inject(RendererHost) @optional() host?: RendererHost
) {
super(host);
this.tooltipId = v4();
}

public attachTo(host: HTMLElement): void {
host.appendChild(this.host);
}

public update(): void {
if (!this.rendered) {
this.render();
this.rendered = true;
}

ReactTooltip.rebuild();
}

protected doRender(): React.ReactNode {
return <ReactTooltip id={this.tooltipId} className='theia-tooltip' html={true} delayShow={1000} />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use some type of TooltipProps where clients can more easily configure how they want their tooltips to behave, and possibly support other react-tooltip options? For example if they want to update delayShow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the props and didn't find much we'd want to expose. I'd also suggest the popup delay is kept consistent across the product

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delay in the vsx-registry felt a bit long, any reason you chose 1 second?

Suggested change
return <ReactTooltip id={this.tooltipId} className='theia-tooltip' html={true} delayShow={1000} />;
return <ReactTooltip id={this.tooltipId} className='theia-tooltip' html={true} delayShow={500} />;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I timed the vscode popup

Copy link
Member

@msujew msujew Oct 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Vince here. It seems like the delay is indeed a bit long (depending on which OS you're using). See this setting in the vscode codebase which controlls the delay of the tooltip popup.

I'd be in favor of adding a similar setting to the preferences in here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I'm on a mac hence the longer delay experienced!

}
}
2 changes: 2 additions & 0 deletions packages/plugin-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@theia/terminal": "1.18.0",
"@theia/timeline": "1.18.0",
"@theia/workspace": "1.18.0",
"@types/markdown-it": "*",
"@types/mime": "^2.0.1",
"decompress": "^4.2.1",
"escape-html": "^1.0.3",
Expand All @@ -34,6 +35,7 @@
"jsonc-parser": "^2.2.0",
"lodash.clonedeep": "^4.5.0",
"macaddress": "^0.2.9",
"markdown-it": "^8.4.0",
"mime": "^2.4.4",
"ps-tree": "^1.2.0",
"request": "^2.82.0",
Expand Down
46 changes: 37 additions & 9 deletions packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import {
TREE_NODE_SEGMENT_GROW_CLASS,
TREE_NODE_TAIL_CLASS,
TreeModelImpl,
TreeViewWelcomeWidget
TreeViewWelcomeWidget,
TooltipService,
TooltipAttributes
} from '@theia/core/lib/browser';
import { TreeViewItem, TreeViewItemCollapsibleState } from '../../../common/plugin-api-rpc';
import { MenuPath, MenuModelRegistry, ActionMenuNode } from '@theia/core/lib/common/menu';
Expand All @@ -42,6 +44,8 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import { View } from '../../../common/plugin-protocol';
import CoreURI from '@theia/core/lib/common/uri';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import * as markdownit from 'markdown-it';
import { isMarkdownString } from '../../../plugin/markdown-string';

export const TREE_NODE_HYPERLINK = 'theia-TreeNodeHyperlink';
export const VIEW_ITEM_CONTEXT_MENU: MenuPath = ['view-item-context-menu'];
Expand Down Expand Up @@ -245,6 +249,9 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

@inject(TooltipService)
protected readonly tooltipService: TooltipService;

protected readonly onDidChangeVisibilityEmitter = new Emitter<boolean>();
readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event;

Expand Down Expand Up @@ -274,13 +281,32 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
classes.push(TREE_NODE_SEGMENT_GROW_CLASS);
}
const className = classes.join(' ');
const title = node.tooltip ||
(node.resourceUri && this.labelProvider.getLongName(new CoreURI(node.resourceUri)))
|| this.toNodeName(node);
const attrs = this.decorateCaption(node, {
className, id: node.id,
title
});

let attrs: React.HTMLAttributes<HTMLElement> & Partial<TooltipAttributes> = {
...this.decorateCaption(node, {}),
className,
id: node.id
};

if (node.tooltip && isMarkdownString(node.tooltip)) {
// Render markdown in custom tooltip
const tooltip = markdownit().render(node.tooltip.value);

attrs = {
...attrs,
'data-tip': tooltip,
'data-for': this.tooltipService.tooltipId
};
} else {
const title = node.tooltip ||
(node.resourceUri && this.labelProvider.getLongName(new CoreURI(node.resourceUri)))
|| this.toNodeName(node);

attrs = {
...attrs,
title
};
}

const children: React.ReactNode[] = [];
const caption = this.toNodeName(node);
Expand Down Expand Up @@ -444,7 +470,9 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
}

protected render(): React.ReactNode {
return React.createElement('div', this.createContainerAttributes(), this.renderSearchInfo(), this.renderTree(this.model));
const node = React.createElement('div', this.createContainerAttributes(), this.renderSearchInfo(), this.renderTree(this.model));
this.tooltipService.update();
return node;
}

protected renderSearchInfo(): React.ReactNode {
Expand Down
2 changes: 2 additions & 0 deletions packages/vsx-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"@theia/plugin-ext-vscode": "1.18.0",
"@theia/preferences": "1.18.0",
"@theia/workspace": "1.18.0",
"@types/markdown-it": "*",
"@types/bent": "^7.0.1",
"@types/sanitize-html": "^2.3.1",
"@types/showdown": "^1.7.1",
"bent": "^7.1.0",
"markdown-it": "^8.4.0",
"p-debounce": "^2.1.0",
"requestretry": "^3.1.0",
"sanitize-html": "^2.3.2",
Expand Down
38 changes: 34 additions & 4 deletions packages/vsx-registry/src/browser/vsx-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as markdownit from 'markdown-it';
import * as React from '@theia/core/shared/react';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { injectable, inject } from '@theia/core/shared/inversify';
Expand All @@ -28,7 +29,7 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { MenuPath } from '@theia/core/lib/common';
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
import { codicon, ContextMenuRenderer, TooltipService } from '@theia/core/lib/browser';
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';

export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];
Expand Down Expand Up @@ -112,6 +113,9 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
@inject(VSXExtensionsSearchModel)
readonly search: VSXExtensionsSearchModel;

@inject(TooltipService)
readonly tooltipService: TooltipService;

protected readonly data: Partial<VSXExtensionData> = {};

get uri(): URI {
Expand Down Expand Up @@ -244,6 +248,29 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
return this.getData('publishedBy');
}

get tooltipId(): string {
return this.tooltipService.tooltipId;
}

get tooltip(): string {
const details = this.getData('readme') || this.description;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thegecko I believe it might be too much information to use the readme, it will lead to the following issue:

  1. search gitlens
  2. use the tooltip on the first result
vsx-tooltip.mp4

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readme removed

let md = `__${this.displayName}__ ${this.version}\n\n${details}\n_____\n\nPublisher: ${this.publisher}`;

if (this.license) {
md += ` \rLicense: ${this.license}`;
}

if (this.downloadCount) {
md += ` \rDownload count: ${downloadCompactFormatter.format(this.downloadCount)}`;
}

if (this.averageRating) {
md += ` \rAverage Rating: ${this.averageRating.toFixed(1)}`;
}

return markdownit().render(md);
}

protected _busy = 0;
get busy(): boolean {
return !!this._busy;
Expand Down Expand Up @@ -316,7 +343,9 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
}

render(): React.ReactNode {
return <VSXExtensionComponent extension={this} />;
const node = <VSXExtensionComponent extension={this} />;
this.tooltipService.update();
return node;
}
}

Expand Down Expand Up @@ -379,8 +408,9 @@ const downloadCompactFormatter = new Intl.NumberFormat(undefined, { notation: 'c

export class VSXExtensionComponent extends AbstractVSXExtensionComponent {
render(): React.ReactNode {
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating } = this.props.extension;
return <div className='theia-vsx-extension'>
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltipId, tooltip } = this.props.extension;

return <div className='theia-vsx-extension' data-for={tooltipId} data-tip={tooltip}>
{iconUrl ?
<img className='theia-vsx-extension-icon' src={iconUrl} /> :
<div className='theia-vsx-extension-icon placeholder' />}
Expand Down
Loading