diff --git a/packages/core/src/common/message-service-protocol.ts b/packages/core/src/common/message-service-protocol.ts index eec1042fbd707..4486f49c53351 100644 --- a/packages/core/src/common/message-service-protocol.ts +++ b/packages/core/src/common/message-service-protocol.ts @@ -16,6 +16,7 @@ import { injectable, inject } from 'inversify'; import { ILogger } from './logger'; +import { Event } from '../common'; export const messageServicePath = '/services/messageService'; @@ -23,7 +24,8 @@ export enum MessageType { Error = 1, Warning = 2, Info = 3, - Log = 4 + Log = 4, + Progress = 5 } export interface Message { @@ -33,6 +35,11 @@ export interface Message { options?: MessageOptions; } +export interface ProgressMessageArguments { + text: string; + actions?: string[]; +} + export interface MessageOptions { timeout?: number; } @@ -53,6 +60,40 @@ export class MessageClient { this.logger.info(message.text); return Promise.resolve(undefined); } + + /** + * Show progress message with possible actions to user. + * + * To be implemented by an extension, e.g. by the messages extension. + */ + newProgress(message: ProgressMessageArguments): Promise { + return Promise.resolve(undefined); + } + + /** + * Hide progress message. + * + * To be implemented by an extension, e.g. by the messages extension. + */ + stopProgress(progress: ProgressToken): Promise { + return Promise.resolve(undefined); + } + + /** + * Update started progress message. + * + * To be implemented by an extension, e.g. by the messages extension. + */ + reportProgress(progress: ProgressToken, update: ProgressUpdate): Promise { + return Promise.resolve(undefined); + } + + /** + * Event that fires when a progress message is canceled. + * + * To be implemented by an extension, e.g. by the messages extension. + */ + onProgressCanceled: Event; } @injectable() @@ -67,3 +108,12 @@ export class DispatchingMessageClient extends MessageClient { } } + +export interface ProgressToken { + id: string; +} + +export interface ProgressUpdate { + value?: string; + increment?: number; +} diff --git a/packages/core/src/common/message-service.ts b/packages/core/src/common/message-service.ts index 891525f30c74a..e3879ff207ae0 100644 --- a/packages/core/src/common/message-service.ts +++ b/packages/core/src/common/message-service.ts @@ -15,7 +15,14 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { MessageClient, MessageType, MessageOptions } from './message-service-protocol'; +import { Event } from '../common'; +import { + MessageClient, + MessageType, + MessageOptions, + ProgressToken, + ProgressUpdate, ProgressMessageArguments +} from './message-service-protocol'; @injectable() export class MessageService { @@ -65,4 +72,17 @@ export class MessageService { return this.client.showMessage({ type, text }); } + newProgress(message: ProgressMessageArguments): Promise { + return this.client.newProgress(message); + } + + stopProgress(progress: ProgressToken): Promise { + return this.client.stopProgress(progress); + } + + reportProgress(progress: ProgressToken, update: ProgressUpdate): Promise { + return this.client.reportProgress(progress, update); + } + + onProgressCanceled: Event = this.client.onProgressCanceled; } diff --git a/packages/messages/src/browser/notifications-message-client.ts b/packages/messages/src/browser/notifications-message-client.ts index 2291dea498be1..09a7c8924a008 100644 --- a/packages/messages/src/browser/notifications-message-client.ts +++ b/packages/messages/src/browser/notifications-message-client.ts @@ -18,14 +18,20 @@ import { injectable, inject } from 'inversify'; import { MessageClient, MessageType, - Message + Message, + Emitter, + ProgressMessageArguments, + ProgressToken, + ProgressUpdate } from '@theia/core/lib/common'; -import { Notifications, NotificationAction } from './notifications'; +import { Notifications, NotificationAction, NotificationProperties, ProgressNotification} from './notifications'; import { NotificationPreferences } from './notification-preferences'; @injectable() export class NotificationsMessageClient extends MessageClient { + private readonly onProgressCanceledEmitter: Emitter = new Emitter(); + readonly onProgressCanceled = this.onProgressCanceledEmitter.event; protected notifications: Notifications = new Notifications(); @inject(NotificationPreferences) protected preferences: NotificationPreferences; @@ -33,7 +39,41 @@ export class NotificationsMessageClient extends MessageClient { return this.show(message); } + newProgress(message: ProgressMessageArguments): Promise { + const messageArguments = { type: MessageType.Progress, text: message.text, options: { timeout: 0 }, actions: message.actions }; + const key = this.getKey(messageArguments); + if (this.visibleProgressNotifications.has(key)) { + return Promise.resolve({ id: key }); + } + const progressNotification = this.notifications.create(this.getNotificationProperties( + messageArguments, + () => { + this.onProgressCanceledEmitter.fire(key); + this.visibleProgressNotifications.delete(key); + })); + this.visibleProgressNotifications.set(key, progressNotification); + progressNotification.show(); + return Promise.resolve({ id: key }); + } + + stopProgress(progress: ProgressToken): Promise { + const progressMessage = this.visibleProgressNotifications.get(progress.id); + if (progressMessage) { + progressMessage.close(); + } + return Promise.resolve(undefined); + } + + reportProgress(progress: ProgressToken, update: ProgressUpdate): Promise { + const notification = this.visibleProgressNotifications.get(progress.id); + if (notification) { + notification.update({ message: update.value, increment: update.increment }); + } + return Promise.resolve(undefined); + } + protected visibleMessages = new Set(); + protected visibleProgressNotifications = new Map(); protected show(message: Message): Promise { const key = this.getKey(message); if (this.visibleMessages.has(key)) { @@ -41,10 +81,10 @@ export class NotificationsMessageClient extends MessageClient { } this.visibleMessages.add(key); return new Promise(resolve => { - this.showToast(message, a => { + this.notifications.show(this.getNotificationProperties(message, a => { this.visibleMessages.delete(key); resolve(a); - }); + })); }); } @@ -52,7 +92,7 @@ export class NotificationsMessageClient extends MessageClient { return `${m.type}-${m.text}-${m.actions ? m.actions.join('|') : '|'}`; } - protected showToast(message: Message, onCloseFn: (action: string | undefined) => void): void { + protected getNotificationProperties(message: Message, onCloseFn: (action: string | undefined) => void): NotificationProperties { const icon = this.iconFor(message.type); const text = message.text; const actions = (message.actions || []).map(action => { @@ -69,22 +109,21 @@ export class NotificationsMessageClient extends MessageClient { label: 'Close', fn: element => onCloseFn(undefined) }); - this.notifications.show({ + return { icon, text, actions, timeout, onTimeout: () => onCloseFn(undefined) - }); + }; } protected iconFor(type: MessageType): string { - if (type === MessageType.Error) { - return 'error'; - } - if (type === MessageType.Warning) { - return 'warning'; + switch (type) { + case MessageType.Error: return 'error'; + case MessageType.Warning: return 'warning'; + case MessageType.Progress: return 'progress'; + default: return 'info'; } - return 'info'; } } diff --git a/packages/messages/src/browser/notifications.ts b/packages/messages/src/browser/notifications.ts index 016d3a3727fd9..798cd86b8fc20 100644 --- a/packages/messages/src/browser/notifications.ts +++ b/packages/messages/src/browser/notifications.ts @@ -13,6 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { Emitter, Event } from '@theia/core'; export const NOTIFICATIONS_CONTAINER = 'theia-NotificationsContainer'; export const NOTIFICATION = 'theia-Notification'; @@ -38,10 +39,20 @@ export interface Notification { element: Element; } +export interface ProgressNotification { + show(): void; + close(): void; + update(item: { message?: string, increment?: number }): void; + onCancel: Event; +} + export class Notifications { protected container: Element; + private readonly onCancelEmitter: Emitter = new Emitter(); + private readonly oncCancel: Event = this.onCancelEmitter.event; + constructor(protected parent?: Element) { this.parent = parent || document.body; this.container = this.createNotificationsContainer(this.parent); @@ -52,6 +63,10 @@ export class Notifications { this.container.appendChild(notificationElement); } + create(properties: NotificationProperties): ProgressNotification { + return new ProgressNotificationImpl(this.container, this.createNotificationElement(properties), this.oncCancel, properties); + } + protected createNotificationsContainer(parentContainer: Element): Element { const container = document.createElement('div'); container.classList.add(NOTIFICATIONS_CONTAINER); @@ -62,18 +77,34 @@ export class Notifications { const fragment = document.createDocumentFragment(); const element = fragment.appendChild(document.createElement('div')); element.classList.add(NOTIFICATION); + element.id = 'notification-container-' + properties.text; const iconContainer = element.appendChild(document.createElement('div')); iconContainer.classList.add(ICON); const icon = iconContainer.appendChild(document.createElement('i')); - icon.classList.add('fa', this.toIconClass(properties.icon), 'fa-fw', properties.icon); + icon.classList.add( + 'fa', + this.toIconClass(properties.icon), + ); + if (properties.icon === 'progress') { + icon.classList.add('fa-pulse'); + } + icon.classList.add( + 'fa-fw', + properties.icon + ); const textContainer = element.appendChild(document.createElement('div')); textContainer.classList.add(TEXT); const text = textContainer.appendChild(document.createElement('p')); + text.id = 'notification-text-' + properties.text; text.innerText = properties.text; + const handler = { element, properties }; const close = () => { element.remove(); + const actions = properties.actions; + if (actions) { + actions.filter(action => action.label === 'Close').forEach(action => action.fn({ element, properties })); + } }; - const handler = { element, properties }; const buttons = element.appendChild(document.createElement('div')); buttons.classList.add(BUTTONS); @@ -93,6 +124,7 @@ export class Notifications { } action.fn(handler); close(); + this.onCancelEmitter.fire(undefined); }); } } @@ -100,13 +132,77 @@ export class Notifications { } protected toIconClass(icon: string): string { - if (icon === 'error') { - return 'fa-times-circle'; + switch (icon) { + case 'error': return 'fa-times-circle'; + case 'warning': return 'fa-warning'; + case 'progress': return 'fa-spinner'; + default: return 'fa-info-circle'; + } + } + +} + +class ProgressNotificationImpl implements ProgressNotification { + private increment: number = 0; + private readonly node: Node; + private readonly container: Element; + private readonly properties: NotificationProperties; + + readonly onCancel: Event; + + constructor(container: Element, node: Node, oncCancel: Event, properties: NotificationProperties) { + this.node = node; + this.onCancel = oncCancel; + this.container = container; + this.properties = properties; + } + + close(): void { + const element = document.getElementById('notification-container-' + this.properties.text); + if (!element) { + return; } - if (icon === 'warning') { - return 'fa-warning'; + element.remove(); + const actions = this.properties.actions; + if (!actions) { + return; } - return 'fa-info-circle'; + actions.filter(action => action.label === 'Close') + .forEach(action => action.fn( + { + element, + properties: this.properties + }) + ); } + show(): void { + let container = document.getElementById('notification-container-' + this.properties.text); + if (!container) { + this.container.appendChild(this.node); + } + container = document.getElementById('notification-container-' + this.properties.text); + if (container) { + const progressContainer = container.appendChild(document.createElement('div')); + progressContainer.className = 'progress'; + const progress = progressContainer.appendChild(document.createElement('p')); + progress.id = 'notification-progress-' + this.properties.text; + } + } + + update(item: { message?: string, increment?: number }): void { + const textElement = document.getElementById('notification-text-' + this.properties.text); + if (textElement) { + if (item.increment) { + this.increment = this.increment + item.increment; + this.increment = this.increment > 100 ? 100 : this.increment; + + const progressElement = document.getElementById('notification-progress-' + this.properties.text); + if (progressElement) { + progressElement.innerText = this.increment + '%'; + } + } + textElement.innerText = this.properties.text + (item.message ? ': ' + item.message : ''); + } + } } diff --git a/packages/messages/src/browser/style/notifications.css b/packages/messages/src/browser/style/notifications.css index e8d4ec6faef14..5e1f4d39f05ab 100644 --- a/packages/messages/src/browser/style/notifications.css +++ b/packages/messages/src/browser/style/notifications.css @@ -77,8 +77,26 @@ color: var(--theia-warn-color0); } -.theia-Notification .text { +.theia-Notification .progress { order: 2; + width: 35px; + align-items: center; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + align-self: center; + height: 100%; +} + +.theia-Notification .progress > p { + margin: 0px; + font-size: var(--theia-ui-font-size1); + vertical-align: middle; +} + +.theia-Notification .text { + order: 3; display: flex; align-items: center; justify-items: left; @@ -104,7 +122,7 @@ .theia-Notification .buttons { display: flex; flex-direction: row; - order: 3; + order: 4; white-space: nowrap; align-self: flex-end; height: 40px; diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 7795735fbac42..d0421aa3de97c 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -39,6 +39,7 @@ import { DefinitionLink, DocumentLink } from './model'; +import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin'; export interface PluginInitData { plugins: PluginMetadata[]; @@ -374,6 +375,32 @@ export interface WindowStateExt { $onWindowStateChanged(focus: boolean): void; } +export interface NotificationExt { + withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + ): Thenable; + $onCancel(id: string): void; +} + +export interface NotificationMain { + $startProgress(message: string): Promise; + $stopProgress(id: string): void; + $updateProgress(message: string, item: { message?: string, increment?: number }): void; +} + +export interface StatusBarExt { + withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + ): Thenable; +} + +export interface StatusBarMain { + $setProgressMessage(message: string): Promise; + $removeProgressMessage(): Promise; +} + export enum EditorPosition { ONE = 0, TWO = 1, @@ -737,6 +764,8 @@ export const PLUGIN_RPC_CONTEXT = { DOCUMENTS_MAIN: createProxyIdentifier('DocumentsMain'), STATUS_BAR_MESSAGE_REGISTRY_MAIN: >createProxyIdentifier('StatusBarMessageRegistryMain'), ENV_MAIN: createProxyIdentifier('EnvMain'), + NOTIFICATION_MAIN: createProxyIdentifier('NotificationMain'), + STATUS_BAR_MAIN: createProxyIdentifier('StatusBarMain'), TERMINAL_MAIN: createProxyIdentifier('TerminalServiceMain'), TREE_VIEWS_MAIN: createProxyIdentifier('TreeViewsMain'), PREFERENCE_REGISTRY_MAIN: createProxyIdentifier('PreferenceRegistryMain'), @@ -749,6 +778,7 @@ export const MAIN_RPC_CONTEXT = { COMMAND_REGISTRY_EXT: createProxyIdentifier('CommandRegistryExt'), QUICK_OPEN_EXT: createProxyIdentifier('QuickOpenExt'), WINDOW_STATE_EXT: createProxyIdentifier('WindowStateExt'), + NOTIFICATION_EXT: createProxyIdentifier('NotificationExt'), WORKSPACE_EXT: createProxyIdentifier('WorkspaceExt'), TEXT_EDITORS_EXT: createProxyIdentifier('TextEditorsExt'), EDITORS_AND_DOCUMENTS_EXT: createProxyIdentifier('EditorsAndDocumentsExt'), diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index a54487c90f672..23f35f8cde08e 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -30,6 +30,8 @@ import { TerminalServiceMainImpl } from './terminal-main'; import { LanguagesMainImpl } from './languages-main'; import { DialogsMainImpl } from './dialogs-main'; import { TreeViewsMainImpl } from './view/tree-views-main'; +import { NotificationMainImpl } from './notification-main'; +import { StatusBarMainImpl } from './status-bar-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -61,6 +63,12 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const envMain = new EnvMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.ENV_MAIN, envMain); + const notificationMain = new NotificationMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN, notificationMain); + + const statusBarMain = new StatusBarMainImpl(container); + rpc.set(PLUGIN_RPC_CONTEXT.STATUS_BAR_MAIN, statusBarMain); + const terminalMain = new TerminalServiceMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.TERMINAL_MAIN, terminalMain); diff --git a/packages/plugin-ext/src/main/browser/notification-main.ts b/packages/plugin-ext/src/main/browser/notification-main.ts new file mode 100644 index 0000000000000..485bd702b3b60 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notification-main.ts @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { MAIN_RPC_CONTEXT, NotificationExt, NotificationMain } from '../../api/plugin-api'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { interfaces } from 'inversify'; +import { RPCProtocol } from '../../api/rpc-protocol'; + +export class NotificationMainImpl implements NotificationMain { + + private readonly proxy: NotificationExt; + private readonly messageService: MessageService; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTIFICATION_EXT); + this.messageService = container.get(MessageService); + + this.messageService.onProgressCanceled(id => { + this.proxy.$onCancel(id); + }); + } + + async $startProgress(message: string): Promise { + const progress = await this.messageService.newProgress({text: message}); + if (progress) { + return Promise.resolve(progress.id); + } else { + return Promise.resolve(undefined); + } + } + + $stopProgress(id: string): void { + this.messageService.stopProgress({ id }); + } + + $updateProgress(id: string, item: { message?: string, increment?: number }): void { + this.messageService.reportProgress({ id }, { value: item.message, increment: item.increment }); + } +} diff --git a/packages/plugin-ext/src/main/browser/status-bar-main.ts b/packages/plugin-ext/src/main/browser/status-bar-main.ts new file mode 100644 index 0000000000000..640a2c6b9ab1e --- /dev/null +++ b/packages/plugin-ext/src/main/browser/status-bar-main.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { StatusBarMain } from '../../api/plugin-api'; +import { interfaces } from 'inversify'; +import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; + +export class StatusBarMainImpl implements StatusBarMain { + + private readonly statusBar: StatusBar; + private static readonly ID: string = 'progress-status-message'; + + constructor(container: interfaces.Container) { + this.statusBar = container.get(StatusBar); + } + + $setProgressMessage(message: string): Promise { + return this.statusBar.setElement(StatusBarMainImpl.ID, { text: '$(refresh~spin)' + ` ${message}`, alignment: StatusBarAlignment.LEFT }); + } + + $removeProgressMessage(): Promise { + return this.statusBar.removeElement(StatusBarMainImpl.ID); + } +} diff --git a/packages/plugin-ext/src/plugin/notification.ts b/packages/plugin-ext/src/plugin/notification.ts new file mode 100644 index 0000000000000..4f7a567fbdec5 --- /dev/null +++ b/packages/plugin-ext/src/plugin/notification.ts @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { PLUGIN_RPC_CONTEXT, NotificationExt, NotificationMain } from '../api/plugin-api'; +import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin'; +import { Thenable } from 'es6-promise'; +import { RPCProtocol } from '../api/rpc-protocol'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; + +export class NotificationExtImpl implements NotificationExt { + private readonly proxy: NotificationMain; + + private readonly onCancelEmitter: Emitter = new Emitter(); + private readonly onCancel: Event = this.onCancelEmitter.event; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN); + } + + async withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + ): Promise { + const message = options.title ? options.title : ''; + const id = await this.proxy.$startProgress(message); + if (id) { + const token = new CancellationTokenImpl(id, this.onCancel); + const thenable = await task(new ProgressCallback(id, this.proxy), token); + this.proxy.$stopProgress(id); + token.dispose(); + return thenable; + } else { + throw new Error('Failed to create progress notification'); + } + } + + $onCancel(id: string): void { + this.onCancelEmitter.fire(id); + } +} + +class ProgressCallback implements Progress<{ message?: string, increment?: number }> { + + private readonly id: string | undefined; + private readonly proxy: NotificationMain; + + constructor(id: string | undefined, proxy: NotificationMain) { + this.id = id; + this.proxy = proxy; + } + report(item: { message?: string, increment?: number }) { + if (this.id) { + this.proxy.$updateProgress(this.id, item); + } + } +} + +class CancellationTokenImpl implements CancellationToken, Disposable { + + private readonly disposableCollection = new DisposableCollection(); + private readonly onCancellationRequestedEmitter: Emitter = new Emitter(); + + isCancellationRequested: boolean = false; + readonly onCancellationRequested: Event = this.onCancellationRequestedEmitter.event; + + constructor(id: string, onCancel: Event) { + this.disposableCollection.push(onCancel(cancelId => { + if (cancelId === id) { + this.onCancellationRequestedEmitter.fire(undefined); + this.isCancellationRequested = true; + this.dispose(); + } + })); + } + + dispose(): void { + this.disposableCollection.dispose(); + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 871803641b24d..d80d17d711642 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -55,6 +55,9 @@ import { DiagnosticSeverity, DiagnosticTag, Location, + Progress, + ProgressOptions, + ProgressLocation, ParameterInformation, SignatureInformation, SignatureHelp, @@ -82,6 +85,9 @@ import { TerminalServiceExtImpl } from './terminal-ext'; import { LanguagesExtImpl, score } from './languages'; import { fromDocumentSelector } from './type-converters'; import { DialogsExtImpl } from './dialogs'; +import { Thenable } from 'es6-promise'; +import { NotificationExtImpl } from './notification'; +import { StatusBarExtImpl } from './statusBar'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { MarkdownString } from './markdown-string'; import { TreeViewsExtImpl } from './tree/tree-views'; @@ -97,6 +103,8 @@ export function createAPIFactory( const dialogsExt = new DialogsExtImpl(rpc); const messageRegistryExt = new MessageRegistryExt(rpc); const windowStateExt = rpc.set(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT, new WindowStateExtImpl()); + const notificationExt = rpc.set(MAIN_RPC_CONTEXT.NOTIFICATION_EXT, new NotificationExtImpl(rpc)); + const statusBarExt = new StatusBarExtImpl(rpc); const editorsAndDocuments = rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, new EditorsAndDocumentsExtImpl(rpc)); const editors = rpc.set(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT, new TextEditorsExtImpl(rpc, editorsAndDocuments)); const documents = rpc.set(MAIN_RPC_CONTEXT.DOCUMENTS_EXT, new DocumentsExtImpl(rpc, editorsAndDocuments)); @@ -232,6 +240,18 @@ export function createAPIFactory( }, createTreeView(viewId: string, options: { treeDataProvider: theia.TreeDataProvider }): theia.TreeView { return treeViewsExt.createTreeView(viewId, options); + }, + withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken) => Thenable + ): Thenable { + switch (options.location) { + case ProgressLocation.Notification: return notificationExt.withProgress(options, task); + case ProgressLocation.Window: return statusBarExt.withProgress(options, task); + case ProgressLocation.SourceControl: return new Promise(() => { + console.error('Progress location \'SourceControl\' is not supported.'); + }); + } } }; @@ -424,6 +444,9 @@ export function createAPIFactory( Diagnostic, CompletionTriggerKind, TextEdit, + ProgressLocation, + ProgressOptions, + Progress, ParameterInformation, SignatureInformation, SignatureHelp, diff --git a/packages/plugin-ext/src/plugin/statusBar.ts b/packages/plugin-ext/src/plugin/statusBar.ts new file mode 100644 index 0000000000000..8cdf1f816f124 --- /dev/null +++ b/packages/plugin-ext/src/plugin/statusBar.ts @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { PLUGIN_RPC_CONTEXT, StatusBarExt, StatusBarMain } from '../api/plugin-api'; +import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin'; +import { Thenable } from 'es6-promise'; +import { RPCProtocol } from '../api/rpc-protocol'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; + +export class StatusBarExtImpl implements StatusBarExt { + private readonly proxy: StatusBarMain; + + private readonly onCancelEmitter: Emitter = new Emitter(); + private readonly onCancel: Event = this.onCancelEmitter.event; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.STATUS_BAR_MAIN); + } + + withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + ): Thenable { + const message = options.title ? options.title : ''; + const token = new CancellationTokenImpl(this.onCancel); + const thenable = task(new ProgressCallback(message, this.proxy), token); + this.proxy.$setProgressMessage(message).then(() => { + thenable.then((() => { + this.proxy.$removeProgressMessage().then(() => { + token.dispose(); + }); + })); + }); + return thenable; + } +} + +class ProgressCallback implements Progress<{ message?: string, increment?: number }> { + + private readonly message: string; + private readonly proxy: StatusBarMain; + + constructor(message: string, proxy: StatusBarMain) { + this.message = message; + this.proxy = proxy; + } + report(item: { message?: string, increment?: number }) { + this.proxy.$setProgressMessage(this.message + (item.message ? ': ' + ' ' + item.message : '')); + } +} + +class CancellationTokenImpl implements CancellationToken, Disposable { + + private readonly disposableCollection = new DisposableCollection(); + private readonly onCancellationRequestedEmitter: Emitter = new Emitter(); + + isCancellationRequested: boolean = false; + readonly onCancellationRequested: Event = this.onCancellationRequestedEmitter.event; + + constructor(oncCancel: Event) { + this.disposableCollection.push(oncCancel(() => { + this.onCancellationRequestedEmitter.fire(undefined); + this.isCancellationRequested = true; + this.dispose(); + })); + } + + dispose(): void { + this.disposableCollection.dispose(); + } +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 206c1ba2edfb0..86b70c1ffff74 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1068,3 +1068,49 @@ export class DocumentSymbol { DocumentSymbol.validate(this); } } + +export class ProgressOptions { + /** + * The location at which progress should show. + */ + location: ProgressLocation; + /** + * A human-readable string which will be used to describe the + * operation. + */ + title?: string; + /** + * Controls if a cancel button should show to allow the user to + * cancel the long running operation. Note that currently only + * `ProgressLocation.Notification` is supporting to show a cancel + * button. + */ + cancellable?: boolean; + constructor(location: ProgressLocation, title?: string, cancellable?: boolean) { + this.location = location; + } +} +export class Progress { + /** + * Report a progress update. + * @param value A progress item, like a message and/or an + * report on how much work finished + */ + report(value: T): void { + } +} +export enum ProgressLocation { + /** + * Show progress for the source control viewlet, as overlay for the icon and as progress bar + * inside the viewlet (when visible). Neither supports cancellation nor discrete progress. + */ + SourceControl = 1, + /** + * Show progress in the status bar of the editor. Neither supports cancellation nor discrete progress. + */ + Window = 10, + /** + * Show progress as notification with an optional cancel button. Supports to show infinite and discrete progress. + */ + Notification = 15 +} diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index aa31c3bf66f55..f80dad2b8d871 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2586,6 +2586,77 @@ declare module '@theia/plugin' { */ export function createTreeView(viewId: string, options: { treeDataProvider: TreeDataProvider }): TreeView; + /** + * Show progress in the editor. Progress is shown while running the given callback + * and while the promise it returned isn't resolved nor rejected. The location at which + * progress should show (and other details) is defined via the passed [`ProgressOptions`](#ProgressOptions). + * + * @param task A callback returning a promise. Progress state can be reported with + * the provided [progress](#Progress)-object. + * + * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with + * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of + * e.g. `10` accounts for `10%` of work done). + * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. + * + * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). + * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the + * long running operation. + * + * @return The thenable the task-callback returned. + */ + export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; + } + /** + * Value-object describing where and how progress should show. + */ + export interface ProgressOptions { + /** + * The location at which progress should show. + */ + location: ProgressLocation; + /** + * A human-readable string which will be used to describe the + * operation. + */ + title?: string; + /** + * Controls if a cancel button should show to allow the user to + * cancel the long running operation. Note that currently only + * `ProgressLocation.Notification` is supporting to show a cancel + * button. + */ + cancellable?: boolean; + } + /** + * A location in the editor at which progress information can be shown. It depends on the + * location how progress is visually represented. + */ + export enum ProgressLocation { + /** + * Show progress for the source control viewlet, as overlay for the icon and as progress bar + * inside the viewlet (when visible). Neither supports cancellation nor discrete progress. + */ + SourceControl = 1, + /** + * Show progress in the status bar of the editor. Neither supports cancellation nor discrete progress. + */ + Window = 10, + /** + * Show progress as notification with an optional cancel button. Supports to show infinite and discrete progress. + */ + Notification = 15 + } + /** + * Defines a generalized way of reporting progress updates. + */ + export interface Progress { + /** + * Report a progress update. + * @param value A progress item, like a message and/or an + * report on how much work finished + */ + report(value: T): void; } /**