Skip to content

Commit 7a13fb8

Browse files
Implement asynchronous tooltip provision (#16364)
1 parent f6c36e1 commit 7a13fb8

File tree

11 files changed

+189
-78
lines changed

11 files changed

+189
-78
lines changed

packages/core/src/browser/hover-service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export interface HoverRequest {
7575
* When true, the hover will register a click handler to allow interaction with elements in the hover area.
7676
*/
7777
interactive?: boolean;
78+
/**
79+
* If implemented, this method will be called when the hover is no longer shown or no longer scheduled to be shown.
80+
*/
81+
onHide?(): void;
7882
}
7983

8084
@injectable()
@@ -122,7 +126,10 @@ export class HoverService {
122126
protected async renderHover(request: HoverRequest): Promise<void> {
123127
const host = this.hoverHost;
124128
let firstChild: HTMLElement | undefined;
125-
const { target, content, position, cssClasses, interactive } = request;
129+
const { target, content, position, cssClasses, interactive, onHide } = request;
130+
if (onHide) {
131+
this.disposeOnHide.push({ dispose: onHide.bind(request) });
132+
}
126133
if (cssClasses) {
127134
host.classList.add(...cssClasses);
128135
}

packages/core/src/browser/status-bar/status-bar-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { MarkdownString } from '../../common/markdown-rendering/markdown-string';
1818
import { AccessibilityInformation } from '../../common/accessibility';
19+
import { CancellationToken, MaybePromise } from '../../common';
1920

2021
export interface StatusBarEntry {
2122
/**
@@ -40,7 +41,7 @@ export interface StatusBarEntry {
4041
color?: string;
4142
backgroundColor?: string;
4243
className?: string;
43-
tooltip?: string | MarkdownString | HTMLElement;
44+
tooltip?: string | MarkdownString | HTMLElement | ((token: CancellationToken) => MaybePromise<string | MarkdownString | HTMLElement | undefined | null>);
4445
command?: string;
4546
arguments?: unknown[];
4647
priority?: number;

packages/core/src/browser/status-bar/status-bar.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as React from 'react';
1818
import { injectable, inject } from 'inversify';
1919
import debounce = require('lodash.debounce');
20-
import { CommandService } from '../../common';
20+
import { CancellationTokenSource, CommandService, nls } from '../../common';
2121
import { ReactWidget } from '../widgets/react-widget';
2222
import { FrontendApplicationStateService } from '../frontend-application-state';
2323
import { LabelParser, LabelIcon } from '../label-parser';
@@ -119,11 +119,25 @@ export class StatusBarImpl extends ReactWidget implements StatusBar {
119119
}
120120

121121
protected requestHover(e: React.MouseEvent<HTMLElement, MouseEvent>, entry: StatusBarEntry): void {
122+
const target = e.currentTarget;
123+
if (typeof entry.tooltip === 'function') {
124+
const cancellationSource = new CancellationTokenSource();
125+
this.doRequestHover(target, nls.localizeByDefault('Loading...'), () => cancellationSource.dispose());
126+
Promise.resolve(entry.tooltip(cancellationSource.token))
127+
.catch(() => undefined)
128+
.then(res => res && !cancellationSource.token.isCancellationRequested && this.doRequestHover(target, res));
129+
} else {
130+
this.doRequestHover(target, entry.tooltip!);
131+
}
132+
}
133+
134+
protected doRequestHover(target: HTMLElement, content: string | HTMLElement | MarkdownString, onHide?: () => void): void {
122135
this.hoverService.requestHover({
123-
content: entry.tooltip!,
124-
target: e.currentTarget,
136+
content,
137+
target,
125138
position: 'top',
126-
interactive: entry.tooltip instanceof HTMLElement || MarkdownString.is(entry.tooltip),
139+
interactive: content instanceof HTMLElement || MarkdownString.is(content),
140+
onHide
127141
});
128142
}
129143

packages/plugin-ext/src/common/plugin-api-rpc.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/* eslint-disable @typescript-eslint/no-explicit-any */
1818

19-
import { createProxyIdentifier, ProxyIdentifier, RPCProtocol } from './rpc-protocol';
19+
import { createProxyIdentifier, RPCProtocol } from './rpc-protocol';
2020
import * as theia from '@theia/plugin';
2121
import { PluginLifecycle, PluginModel, PluginMetadata, PluginPackage, IconUrl, PluginJsonValidationContribution } from './plugin-protocol';
2222
import { QueryParameters } from './env';
@@ -513,13 +513,18 @@ export interface StatusBarMessageRegistryMain {
513513
alignment: theia.StatusBarAlignment,
514514
color: string | undefined,
515515
backgroundColor: string | undefined,
516-
tooltip: string | theia.MarkdownString | undefined,
516+
/** Value true indicates that the tooltip can be retrieved asynchronously */
517+
tooltip: string | theia.MarkdownString | true | undefined,
517518
command: string | undefined,
518519
accessibilityInformation: theia.AccessibilityInformation,
519520
args: any[] | undefined): PromiseLike<void>;
520521
$dispose(id: string): void;
521522
}
522523

524+
export interface StatusBarMessageRegistryExt {
525+
$getMessage(id: string, cancellation: CancellationToken): theia.ProviderResult<string | MarkdownString>;
526+
}
527+
523528
export interface QuickOpenExt {
524529
$onItemSelected(handle: number): void;
525530
$validateInput(input: string): Promise<string | { content: string; severity: Severity; } | null | undefined>;
@@ -2314,7 +2319,7 @@ export const PLUGIN_RPC_CONTEXT = {
23142319
QUICK_OPEN_MAIN: createProxyIdentifier<QuickOpenMain>('QuickOpenMain'),
23152320
DIALOGS_MAIN: createProxyIdentifier<DialogsMain>('DialogsMain'),
23162321
WORKSPACE_MAIN: createProxyIdentifier<WorkspaceMain>('WorkspaceMain'),
2317-
MESSAGE_REGISTRY_MAIN: <ProxyIdentifier<MessageRegistryMain>>createProxyIdentifier<MessageRegistryMain>('MessageRegistryMain'),
2322+
MESSAGE_REGISTRY_MAIN: createProxyIdentifier<MessageRegistryMain>('MessageRegistryMain'),
23182323
TEXT_EDITORS_MAIN: createProxyIdentifier<TextEditorsMain>('TextEditorsMain'),
23192324
DOCUMENTS_MAIN: createProxyIdentifier<DocumentsMain>('DocumentsMain'),
23202325
NOTEBOOKS_MAIN: createProxyIdentifier<NotebooksMain>('NotebooksMain'),
@@ -2323,13 +2328,13 @@ export const PLUGIN_RPC_CONTEXT = {
23232328
NOTEBOOK_DOCUMENTS_AND_EDITORS_MAIN: createProxyIdentifier<NotebookDocumentsAndEditorsMain>('NotebooksAndEditorsMain'),
23242329
NOTEBOOK_RENDERERS_MAIN: createProxyIdentifier<NotebookRenderersMain>('NotebookRenderersMain'),
23252330
NOTEBOOK_KERNELS_MAIN: createProxyIdentifier<NotebookKernelsMain>('NotebookKernelsMain'),
2326-
STATUS_BAR_MESSAGE_REGISTRY_MAIN: <ProxyIdentifier<StatusBarMessageRegistryMain>>createProxyIdentifier<StatusBarMessageRegistryMain>('StatusBarMessageRegistryMain'),
2331+
STATUS_BAR_MESSAGE_REGISTRY_MAIN: createProxyIdentifier<StatusBarMessageRegistryMain>('StatusBarMessageRegistryMain'),
23272332
ENV_MAIN: createProxyIdentifier<EnvMain>('EnvMain'),
23282333
NOTIFICATION_MAIN: createProxyIdentifier<NotificationMain>('NotificationMain'),
23292334
TERMINAL_MAIN: createProxyIdentifier<TerminalServiceMain>('TerminalServiceMain'),
23302335
TREE_VIEWS_MAIN: createProxyIdentifier<TreeViewsMain>('TreeViewsMain'),
23312336
PREFERENCE_REGISTRY_MAIN: createProxyIdentifier<PreferenceRegistryMain>('PreferenceRegistryMain'),
2332-
OUTPUT_CHANNEL_REGISTRY_MAIN: <ProxyIdentifier<OutputChannelRegistryMain>>createProxyIdentifier<OutputChannelRegistryMain>('OutputChannelRegistryMain'),
2337+
OUTPUT_CHANNEL_REGISTRY_MAIN: createProxyIdentifier<OutputChannelRegistryMain>('OutputChannelRegistryMain'),
23332338
LANGUAGES_MAIN: createProxyIdentifier<LanguagesMain>('LanguagesMain'),
23342339
CONNECTION_MAIN: createProxyIdentifier<ConnectionMain>('ConnectionMain'),
23352340
WEBVIEWS_MAIN: createProxyIdentifier<WebviewsMain>('WebviewsMain'),
@@ -2390,6 +2395,7 @@ export const MAIN_RPC_CONTEXT = {
23902395
SECRETS_EXT: createProxyIdentifier<SecretsExt>('SecretsExt'),
23912396
DECORATIONS_EXT: createProxyIdentifier<DecorationsExt>('DecorationsExt'),
23922397
LABEL_SERVICE_EXT: createProxyIdentifier<LabelServiceExt>('LabelServiceExt'),
2398+
STATUS_BAR_MESSAGE_REGISTRY_EXT: createProxyIdentifier<StatusBarMessageRegistryExt>('StatusBarMessageRegistryExt'),
23932399
TIMELINE_EXT: createProxyIdentifier<TimelineExt>('TimeLineExt'),
23942400
THEMING_EXT: createProxyIdentifier<ThemingExt>('ThemingExt'),
23952401
COMMENTS_EXT: createProxyIdentifier<CommentsExt>('CommentsExt'),

packages/plugin-ext/src/main/browser/main-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
123123
// start listening only after all clients are subscribed to events
124124
editorsAndDocuments.listen();
125125

126-
const statusBarMessageRegistryMain = new StatusBarMessageRegistryMainImpl(container);
126+
const statusBarMessageRegistryMain = new StatusBarMessageRegistryMainImpl(container, rpc);
127127
rpc.set(PLUGIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_MAIN, statusBarMessageRegistryMain);
128128

129129
const envMain = new EnvMainImpl(rpc, container);

packages/plugin-ext/src/main/browser/status-bar-message-registry-main.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@
1616
import { interfaces } from '@theia/core/shared/inversify';
1717
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
1818
import * as types from '../../plugin/types-impl';
19-
import { StatusBarMessageRegistryMain } from '../../common/plugin-api-rpc';
19+
import { StatusBarMessageRegistryMain, StatusBarMessageRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
2020
import { StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
2121
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
2222
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
23+
import { RPCProtocol } from '../../common/rpc-protocol';
24+
import { CancellationToken } from '@theia/core';
2325

2426
export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistryMain, Disposable {
2527
private readonly delegate: StatusBar;
2628
private readonly entries = new Map<string, StatusBarEntry>();
29+
private readonly proxy: StatusBarMessageRegistryExt;
2730
private readonly toDispose = new DisposableCollection(
2831
Disposable.create(() => { /* mark as not disposed */ })
2932
);
3033

3134
protected readonly colorRegistry: ColorRegistry;
3235

33-
constructor(container: interfaces.Container) {
36+
constructor(container: interfaces.Container, rpc: RPCProtocol) {
3437
this.delegate = container.get(StatusBar);
3538
this.colorRegistry = container.get(ColorRegistry);
39+
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_EXT);
3640
}
3741

3842
dispose(): void {
@@ -46,7 +50,7 @@ export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistr
4650
alignment: number,
4751
color: string | undefined,
4852
backgroundColor: string | undefined,
49-
tooltip: string | MarkdownString | undefined,
53+
tooltip: string | MarkdownString | true | undefined,
5054
command: string | undefined,
5155
accessibilityInformation: types.AccessibilityInformation,
5256
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -64,7 +68,8 @@ export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistr
6468
color: color && (this.colorRegistry.getCurrentColor(color) || color),
6569
// In contrast to color, the backgroundColor must be a theme color. Thus, do not hand in the plain string if it cannot be resolved.
6670
backgroundColor: backgroundColor && (this.colorRegistry.getCurrentColor(backgroundColor)),
67-
tooltip,
71+
// true is used as a serializable sentinel value to indicate that the tooltip can be retrieved asynchronously
72+
tooltip: tooltip === true ? (token: CancellationToken) => this.proxy.$getMessage(id, token) : tooltip,
6873
command,
6974
accessibilityInformation,
7075
args

packages/plugin-ext/src/plugin/plugin-context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
} from '../common/plugin-api-rpc';
3434
import { RPCProtocol } from '../common/rpc-protocol';
3535
import { MessageRegistryExt } from './message-registry';
36-
import { StatusBarMessageRegistryExt } from './status-bar-message-registry';
36+
import { StatusBarMessageRegistryExtImpl } from './status-bar-message-registry';
3737
import { WindowStateExtImpl } from './window-state';
3838
import { WorkspaceExtImpl } from './workspace';
3939
import { EnvExtImpl } from './env';
@@ -335,7 +335,7 @@ export function createAPIFactory(
335335
const notebookRenderers = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT, new NotebookRenderersExtImpl(rpc, notebooksExt));
336336
const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry, webviewExt, workspaceExt));
337337
const notebookDocuments = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT, new NotebookDocumentsExtImpl(notebooksExt));
338-
const statusBarMessageRegistryExt = new StatusBarMessageRegistryExt(rpc, commandRegistry);
338+
const statusBarMessageRegistryExt = rpc.set(MAIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_EXT, new StatusBarMessageRegistryExtImpl(rpc, commandRegistry));
339339
const terminalExt = rpc.set(MAIN_RPC_CONTEXT.TERMINAL_EXT, new TerminalServiceExtImpl(rpc));
340340
const outputChannelRegistryExt = rpc.set(MAIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_EXT, new OutputChannelRegistryExtImpl(rpc));
341341
const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry));

packages/plugin-ext/src/plugin/status-bar-message-registry.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,23 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import { Disposable, StatusBarAlignment } from './types-impl';
17-
import { StatusBarItem } from '@theia/plugin';
17+
import { CancellationToken, ProviderResult, StatusBarItem } from '@theia/plugin';
1818
import {
19-
PLUGIN_RPC_CONTEXT as Ext, StatusBarMessageRegistryMain
19+
PLUGIN_RPC_CONTEXT as Ext, StatusBarMessageRegistryMain,
20+
StatusBarMessageRegistryExt
2021
} from '../common/plugin-api-rpc';
2122
import { RPCProtocol } from '../common/rpc-protocol';
2223
import { StatusBarItemImpl } from './status-bar/status-bar-item';
2324
import { CommandRegistryImpl } from './command-registry';
25+
import { MarkdownString } from '../common/plugin-api-rpc-model';
2426

2527
/*---------------------------------------------------------------------------------------------
2628
* Copyright (c) Microsoft Corporation. All rights reserved.
2729
* Licensed under the MIT License. See License.txt in the project root for license information.
2830
*--------------------------------------------------------------------------------------------*/
2931

30-
export class StatusBarMessageRegistryExt {
32+
export class StatusBarMessageRegistryExtImpl implements StatusBarMessageRegistryExt {
33+
private readonly items = new Map<string, StatusBarItemImpl>();
3134

3235
proxy: StatusBarMessageRegistryMain;
3336

@@ -38,6 +41,15 @@ export class StatusBarMessageRegistryExt {
3841
this.statusMessage = new StatusBarMessage(this);
3942
}
4043

44+
$getMessage(id: string, cancellation: CancellationToken): ProviderResult<string | MarkdownString> {
45+
const item = this.items.get(id);
46+
if (!item) { return undefined; }
47+
if (typeof item.tooltip2 === 'function') {
48+
return item.tooltip2(cancellation);
49+
}
50+
return item.tooltip2 ?? item.tooltip;
51+
}
52+
4153
// copied from https://github.com/Microsoft/vscode/blob/6c8f02b41db9ae5c4d15df767d47755e5c73b9d5/src/vs/workbench/api/node/extHostStatusBar.ts#L174
4254
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4355
setStatusBarMessage(text: string, timeoutOrThenable?: number | PromiseLike<any>): Disposable {
@@ -59,7 +71,9 @@ export class StatusBarMessageRegistryExt {
5971
}
6072

6173
createStatusBarItem(alignment?: StatusBarAlignment, priority?: number, id?: string): StatusBarItem {
62-
return new StatusBarItemImpl(this.proxy, this.commandRegistry, alignment, priority, id);
74+
const item: StatusBarItemImpl = new StatusBarItemImpl(this.proxy, this.commandRegistry, alignment, priority, id, () => this.items.delete(item.id));
75+
this.items.set(item.id, item);
76+
return item;
6377
}
6478

6579
}
@@ -70,7 +84,7 @@ class StatusBarMessage {
7084
private _item: StatusBarItem;
7185
private _messages: { message: string }[] = [];
7286

73-
constructor(statusBar: StatusBarMessageRegistryExt) {
87+
constructor(statusBar: StatusBarMessageRegistryExtImpl) {
7488
this._item = statusBar.createStatusBarItem(StatusBarAlignment.Left, Number.MIN_VALUE);
7589
}
7690

0 commit comments

Comments
 (0)