diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 82c1028fc..db49c33d9 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -44,7 +44,7 @@ import { WorkspaceService } from './theia/workspace/workspace-service'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { OutputService } from '../common/protocol/output-service'; +import { ResponseService } from '../common/protocol/response-service'; import { ArduinoPreferences } from './arduino-preferences'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl'; import { SaveAsSketch } from './contributions/save-as-sketch'; @@ -151,8 +151,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut @inject(ExecutableService) protected executableService: ExecutableService; - @inject(OutputService) - protected readonly outputService: OutputService; + @inject(ResponseService) + protected readonly responseService: ResponseService; @inject(ArduinoPreferences) protected readonly arduinoPreferences: ArduinoPreferences; diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 1346acc9f..d4bfcbe73 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -120,8 +120,8 @@ import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, Ou import { ExecutableService, ExecutableServicePath } from '../common/protocol'; import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service'; -import { OutputServiceImpl } from './output-service-impl'; -import { OutputServicePath, OutputService } from '../common/protocol/output-service'; +import { ResponseServiceImpl } from './response-service-impl'; +import { ResponseServicePath, ResponseService } from '../common/protocol/response-service'; import { NotificationCenter } from './notification-center'; import { NotificationServicePath, NotificationServiceServer } from '../common/protocol'; import { About } from './contributions/about'; @@ -159,6 +159,10 @@ import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco import { DebugEditorModel } from './theia/debug/debug-editor-model'; import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model'; import { StorageWrapper } from './storage-wrapper'; +import { NotificationManager } from './theia/messages/notifications-manager'; +import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager'; +import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer'; +import { NotificationsRenderer } from './theia/messages/notifications-renderer'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -383,11 +387,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, ArchiveSketch); Contribution.configure(bind, AddZipLibrary); - bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => { - WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService); - return outputService; + bind(ResponseServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, responseService) => { + WebSocketConnectionProvider.createProxy(container, ResponseServicePath, responseService); + return responseService; }); - bind(OutputService).toService(OutputServiceImpl); + bind(ResponseService).toService(ResponseServiceImpl); bind(NotificationCenter).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(NotificationCenter); @@ -439,4 +443,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(StorageWrapper).toSelf().inSingletonScope(); bind(CommandContribution).toService(StorageWrapper); + + bind(NotificationManager).toSelf().inSingletonScope(); + rebind(TheiaNotificationManager).toService(NotificationManager); + bind(NotificationsRenderer).toSelf().inSingletonScope(); + rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer); }); diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts index 94393f4e5..201873a16 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -4,8 +4,9 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service'; import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; -import { InstallationProgressDialog } from '../widgets/progress-dialog'; import { BoardsConfig } from './boards-config'; +import { Installable } from '../../common/protocol'; +import { ResponseServiceImpl } from '../response-service-impl'; /** * Listens on `BoardsConfig.Config` changes, if a board is selected which does not @@ -23,6 +24,9 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClient: BoardsServiceProvider; + @inject(ResponseServiceImpl) + protected readonly responseService: ResponseServiceImpl; + @inject(BoardsListWidgetFrontendContribution) protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; @@ -42,13 +46,13 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { // tslint:disable-next-line:max-line-length this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => { if (answer === 'Yes') { - const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]); - dialog.open(); - try { - await this.boardsService.install({ item: candidate }); - } finally { - dialog.close(); - } + await Installable.installWithProgress({ + installable: this.boardsService, + item: candidate, + messageService: this.messageService, + responseService: this.responseService, + version: candidate.availableVersions[0] + }); } if (answer) { this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase())); diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts index e5ded8d9c..8df9f0ee1 100644 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts @@ -33,9 +33,14 @@ export class BoardsListWidget extends ListWidget { ]); } - async install({ item, version }: { item: BoardsPackage; version: string; }): Promise { - await super.install({ item, version }); - this.messageService.info(`Successfully installed platform ${item.name}:${version}.`, { timeout: 3000 }); + protected async install({ item, progressId, version }: { item: BoardsPackage, progressId: string, version: string; }): Promise { + await super.install({ item, progressId, version }); + this.messageService.info(`Successfully installed platform ${item.name}:${version}`, { timeout: 3000 }); + } + + protected async uninstall({ item, progressId }: { item: BoardsPackage, progressId: string }): Promise { + await super.uninstall({ item, progressId }); + this.messageService.info(`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, { timeout: 3000 }); } } diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 0a12e979d..d08456295 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -1,12 +1,12 @@ import { inject, injectable } from 'inversify'; import { remote } from 'electron'; +import URI from '@theia/core/lib/common/uri'; +import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { ArduinoMenus } from '../menu/arduino-menus'; +import { ResponseServiceImpl } from '../response-service-impl'; +import { Installable, LibraryService } from '../../common/protocol'; import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import URI from '@theia/core/lib/common/uri'; -import { InstallationProgressDialog } from '../widgets/progress-dialog'; -import { LibraryService } from '../../common/protocol'; -import { ConfirmDialog } from '@theia/core/lib/browser'; @injectable() export class AddZipLibrary extends SketchContribution { @@ -14,6 +14,9 @@ export class AddZipLibrary extends SketchContribution { @inject(EnvVariablesServer) protected readonly envVariableServer: EnvVariablesServer; + @inject(ResponseServiceImpl) + protected readonly responseService: ResponseServiceImpl; + @inject(LibraryService) protected readonly libraryService: LibraryService; @@ -69,11 +72,14 @@ export class AddZipLibrary extends SketchContribution { } private async doInstall(zipUri: string, overwrite?: boolean): Promise { - const dialog = new InstallationProgressDialog('Installing library', 'zip'); try { - this.outputChannelManager.getChannel('Arduino').clear(); - dialog.open(); - await this.libraryService.installZip({ zipUri, overwrite }); + await Installable.doWithProgress({ + messageService: this.messageService, + progressText: `Processing ${new URI(zipUri).path.base}`, + responseService: this.responseService, + run: () => this.libraryService.installZip({ zipUri, overwrite }) + }); + this.messageService.info(`Successfully installed library from ${new URI(zipUri).path.base} archive`, { timeout: 3000 }); } catch (error) { if (error instanceof Error) { const match = error.message.match(/library (.*?) already installed/); @@ -88,8 +94,6 @@ export class AddZipLibrary extends SketchContribution { } this.messageService.error(error.toString()); throw error; - } finally { - dialog.close(); } } diff --git a/arduino-ide-extension/src/browser/data/arduino.color-theme.json b/arduino-ide-extension/src/browser/data/arduino.color-theme.json index 355b2e694..00ea41b48 100644 --- a/arduino-ide-extension/src/browser/data/arduino.color-theme.json +++ b/arduino-ide-extension/src/browser/data/arduino.color-theme.json @@ -82,6 +82,7 @@ "colors": { "list.highlightForeground": "#005c5f", "list.activeSelectionBackground": "#005c5f", + "progressBar.background": "#005c5f", "editor.background": "#ffffff", "editorCursor.foreground": "#434f54", "editor.foreground": "#434f54", diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index b9514b871..a0c70eb3e 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -37,7 +37,7 @@ export class LibraryListWidget extends ListWidget { ]); } - protected async install({ item, version }: { item: LibraryPackage, version: Installable.Version }): Promise { + protected async install({ item, progressId, version }: { item: LibraryPackage, progressId: string, version: Installable.Version }): Promise { const dependencies = await this.service.listDependencies({ item, version, filterSelf: true }); let installDependencies: boolean | undefined = undefined; if (dependencies.length) { @@ -84,11 +84,16 @@ export class LibraryListWidget extends ListWidget { } if (typeof installDependencies === 'boolean') { - await this.service.install({ item, version, installDependencies }); - this.messageService.info(`Successfully installed library ${item.name}:${version}.`, { timeout: 3000 }); + await this.service.install({ item, version, progressId, installDependencies }); + this.messageService.info(`Successfully installed library ${item.name}:${version}`, { timeout: 3000 }); } } + protected async uninstall({ item, progressId }: { item: LibraryPackage, progressId: string }): Promise { + await super.uninstall({ item, progressId }); + this.messageService.info(`Successfully uninstalled library ${item.name}:${item.installedVersion}`, { timeout: 3000 }); + } + } class MessageBoxDialog extends AbstractDialog { diff --git a/arduino-ide-extension/src/browser/output-service-impl.ts b/arduino-ide-extension/src/browser/output-service-impl.ts deleted file mode 100644 index 60c5a94bd..000000000 --- a/arduino-ide-extension/src/browser/output-service-impl.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; -import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; -import { OutputService, OutputMessage } from '../common/protocol/output-service'; - -@injectable() -export class OutputServiceImpl implements OutputService { - - @inject(OutputContribution) - protected outputContribution: OutputContribution; - - @inject(OutputChannelManager) - protected outputChannelManager: OutputChannelManager; - - append(message: OutputMessage): void { - const { chunk } = message; - const channel = this.outputChannelManager.getChannel('Arduino'); - channel.show({ preserveFocus: true }); - channel.append(chunk); - } - -} diff --git a/arduino-ide-extension/src/browser/response-service-impl.ts b/arduino-ide-extension/src/browser/response-service-impl.ts new file mode 100644 index 000000000..d1fc6ce6f --- /dev/null +++ b/arduino-ide-extension/src/browser/response-service-impl.ts @@ -0,0 +1,34 @@ +import { inject, injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; +import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; +import { ResponseService, OutputMessage, ProgressMessage } from '../common/protocol/response-service'; + +@injectable() +export class ResponseServiceImpl implements ResponseService { + + @inject(OutputContribution) + protected outputContribution: OutputContribution; + + @inject(OutputChannelManager) + protected outputChannelManager: OutputChannelManager; + + protected readonly progressDidChangeEmitter = new Emitter(); + readonly onProgressDidChange = this.progressDidChangeEmitter.event; + + appendToOutput(message: OutputMessage): void { + const { chunk } = message; + const channel = this.outputChannelManager.getChannel('Arduino'); + channel.show({ preserveFocus: true }); + channel.append(chunk); + } + + clearArduinoChannel(): void { + this.outputChannelManager.getChannel('Arduino').clear(); + } + + reportProgress(progress: ProgressMessage): void { + this.progressDidChangeEmitter.fire(progress); + } + +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 44dab9409..3169a6242 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -67,3 +67,20 @@ button.theia-button.secondary { button.theia-button.main { color: var(--theia-button-foreground); } + +/* To make the progress-bar slightly thicker, and use the color from the status bar */ +.theia-progress-bar-container { + width: 100%; + height: 4px; +} + +.theia-progress-bar { + height: 4px; + width: 3%; + animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1); +} + +.theia-notification-item-progressbar { + height: 4px; + width: 66%; +} diff --git a/arduino-ide-extension/src/browser/theia/messages/notification-center-component.tsx b/arduino-ide-extension/src/browser/theia/messages/notification-center-component.tsx new file mode 100644 index 000000000..50d5c81ad --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/messages/notification-center-component.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { NotificationComponent } from './notification-component'; +import { NotificationCenterComponent as TheiaNotificationCenterComponent } from '@theia/messages/lib/browser/notification-center-component' + +const PerfectScrollbar = require('react-perfect-scrollbar'); + +export class NotificationCenterComponent extends TheiaNotificationCenterComponent { + + render(): React.ReactNode { + const empty = this.state.notifications.length === 0; + const title = empty ? 'NO NEW NOTIFICATIONS' : 'NOTIFICATIONS'; + return ( +
+
+
{title}
+
+
    +
  • +
  • +
+
+
+ +
+ {this.state.notifications.map(notification => + + )} +
+
+
+ ); + } + +} diff --git a/arduino-ide-extension/src/browser/theia/messages/notification-component.tsx b/arduino-ide-extension/src/browser/theia/messages/notification-component.tsx new file mode 100644 index 000000000..3daa37d36 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/messages/notification-component.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component' + +export class NotificationComponent extends TheiaNotificationComponent { + + render(): React.ReactNode { + const { messageId, message, type, collapsed, expandable, source, actions } = this.props.notification; + return (
+
+
+
+
+ +
+
    + {expandable && ( +
  • + )} + {!this.isProgress && (
  • )} +
+
+ {(source || !!actions.length) && ( +
+
+ {source && ({source})} +
+
+ {actions && actions.map((action, index) => ( + + ))} +
+
+ )} +
+ {this.renderProgressBar()} +
); + } + + private renderProgressBar(): React.ReactNode { + if (!this.isProgress) { + return undefined; + } + if (!Number.isNaN(this.props.notification.progress)) { + return
+
+
; + } + return
+
+
+ } + + private get isProgress(): boolean { + return typeof this.props.notification.progress === 'number'; + } + +} diff --git a/arduino-ide-extension/src/browser/theia/messages/notification-toasts-component.tsx b/arduino-ide-extension/src/browser/theia/messages/notification-toasts-component.tsx new file mode 100644 index 000000000..f441283c2 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/messages/notification-toasts-component.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { NotificationComponent } from './notification-component'; +import { NotificationToastsComponent as TheiaNotificationToastsComponent } from '@theia/messages/lib/browser/notification-toasts-component' + +export class NotificationToastsComponent extends TheiaNotificationToastsComponent { + + render(): React.ReactNode { + return ( +
+
+ {this.state.toasts.map(notification => )} +
+
+ ); + } + +} diff --git a/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts b/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts new file mode 100644 index 000000000..99b646f0d --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts @@ -0,0 +1,37 @@ +import { injectable } from 'inversify'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { ProgressMessage, ProgressUpdate } from '@theia/core/lib/common/message-service-protocol'; +import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager'; + +@injectable() +export class NotificationManager extends TheiaNotificationManager { + + async reportProgress(messageId: string, update: ProgressUpdate, originalMessage: ProgressMessage, cancellationToken: CancellationToken): Promise { + const notification = this.find(messageId); + if (!notification) { + return; + } + if (cancellationToken.isCancellationRequested) { + this.clear(messageId); + } else { + notification.message = originalMessage.text && update.message ? `${originalMessage.text}: ${update.message}` : + originalMessage.text || update?.message || notification.message; + + // Unlike in Theia, we allow resetting the progress monitor to NaN to enforce unknown progress. + const candidate = this.toPlainProgress(update); + notification.progress = typeof candidate === 'number' ? candidate : notification.progress; + } + this.fireUpdatedEvent(); + } + + protected toPlainProgress(update: ProgressUpdate): number | undefined { + if (!update.work) { + return undefined; + } + if (Number.isNaN(update.work.done) || Number.isNaN(update.work.total)) { + return Number.NaN; // This should trigger the unknown monitor. + } + return Math.min(update.work.done / update.work.total * 100, 100); + } + +} diff --git a/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx b/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx new file mode 100644 index 000000000..e7d4b4451 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { injectable } from 'inversify'; +import { NotificationCenterComponent } from './notification-center-component'; +import { NotificationToastsComponent } from './notification-toasts-component'; +import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer'; + +@injectable() +export class NotificationsRenderer extends TheiaNotificationsRenderer { + + protected render(): void { + ReactDOM.render(
+ + +
, this.container); + } + +} diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 35b943096..66163a0a3 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -3,16 +3,15 @@ import debounce = require('lodash.debounce'); import { Event } from '@theia/core/lib/common/event'; import { CommandService } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { OutputCommands } from '@theia/output/lib/browser/output-commands'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { Searchable } from '../../../common/protocol/searchable'; import { Installable } from '../../../common/protocol/installable'; import { ArduinoComponent } from '../../../common/protocol/arduino-component'; -import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog'; import { SearchBar } from './search-bar'; import { ListWidget } from './list-widget'; import { ComponentList } from './component-list'; import { ListItemRenderer } from './list-item-renderer'; +import { ResponseServiceImpl } from '../../response-service-impl'; export class FilterableListContainer extends React.Component, FilterableListContainer.State> { @@ -84,20 +83,14 @@ export class FilterableListContainer extends React.C } protected async install(item: T, version: Installable.Version): Promise { - const { install, searchable, itemLabel } = this.props; - const dialog = new InstallationProgressDialog(itemLabel(item), version); - try { - dialog.open(); - await this.clearArduinoChannel(); - await install({ item, version }); - const items = await searchable.search({ query: this.state.filterText }); - this.setState({ items: this.sort(items) }); - } catch (error) { - this.props.messageService.error(error instanceof Error ? error.message : String(error)); - throw error; - } finally { - dialog.close(); - } + const { install, searchable } = this.props; + await Installable.doWithProgress({ + ...this.props, + progressText: `Processing ${item.name}:${version}`, + run: ({ progressId }) => install({ item, progressId, version }) + }); + const items = await searchable.search({ query: this.state.filterText }); + this.setState({ items: this.sort(items) }); } protected async uninstall(item: T): Promise { @@ -110,21 +103,14 @@ export class FilterableListContainer extends React.C if (!ok) { return; } - const { uninstall, searchable, itemLabel } = this.props; - const dialog = new UninstallationProgressDialog(itemLabel(item)); - try { - await this.clearArduinoChannel(); - dialog.open(); - await uninstall({ item }); - const items = await searchable.search({ query: this.state.filterText }); - this.setState({ items: this.sort(items) }); - } finally { - dialog.close(); - } - } - - private async clearArduinoChannel(): Promise { - return this.props.commandService.executeCommand(OutputCommands.CLEAR.id, { name: 'Arduino' }); + const { uninstall, searchable } = this.props; + await Installable.doWithProgress({ + ...this.props, + progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`, + run: ({ progressId }) => uninstall({ item, progressId }) + }); + const items = await searchable.search({ query: this.state.filterText }); + this.setState({ items: this.sort(items) }); } } @@ -139,9 +125,10 @@ export namespace FilterableListContainer { readonly resolveContainer: (element: HTMLElement) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly filterTextChangeEvent: Event; - readonly install: ({ item, version }: { item: T, version: Installable.Version }) => Promise; - readonly uninstall: ({ item }: { item: T }) => Promise; readonly messageService: MessageService; + readonly responseService: ResponseServiceImpl; + readonly install: ({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }) => Promise; + readonly uninstall: ({ item, progressId }: { item: T, progressId: string }) => Promise; readonly commandService: CommandService; } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index 1c4baabe9..376f12089 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { injectable, postConstruct, inject } from 'inversify'; +import { Widget } from '@phosphor/widgets'; import { Message } from '@phosphor/messaging'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { Emitter } from '@theia/core/lib/common/event'; @@ -7,12 +8,11 @@ import { MaybePromise } from '@theia/core/lib/common/types'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { CommandService } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { Installable } from '../../../common/protocol/installable'; -import { Searchable } from '../../../common/protocol/searchable'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; +import { Installable, Searchable, ArduinoComponent } from '../../../common/protocol'; import { FilterableListContainer } from './filterable-list-container'; import { ListItemRenderer } from './list-item-renderer'; import { NotificationCenter } from '../../notification-center'; +import { ResponseServiceImpl } from '../../response-service-impl'; @injectable() export abstract class ListWidget extends ReactWidget { @@ -23,6 +23,9 @@ export abstract class ListWidget extends ReactWidget @inject(CommandService) protected readonly commandService: CommandService; + @inject(ResponseServiceImpl) + protected readonly responseService: ResponseServiceImpl; + @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; @@ -63,26 +66,31 @@ export abstract class ListWidget extends ReactWidget return this.deferredContainer.promise; } - protected onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); + protected onActivateRequest(message: Message): void { + super.onActivateRequest(message); (this.focusNode || this.node).focus(); } - protected onUpdateRequest(msg: Message): void { - super.onUpdateRequest(msg); + protected onUpdateRequest(message: Message): void { + super.onUpdateRequest(message); this.render(); } + protected onResize(message: Widget.ResizeMessage): void { + super.onResize(message); + this.updateScrollBar(); + } + protected onFocusResolved = (element: HTMLElement | undefined) => { this.focusNode = element; }; - protected async install({ item, version }: { item: T, version: Installable.Version }): Promise { - return this.options.installable.install({ item, version }); + protected async install({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }): Promise { + return this.options.installable.install({ item, progressId, version }); } - protected async uninstall({ item }: { item: T }): Promise { - return this.options.installable.uninstall({ item }); + protected async uninstall({ item, progressId }: { item: T, progressId: string }): Promise { + return this.options.installable.uninstall({ item, progressId }); } render(): React.ReactNode { @@ -97,7 +105,8 @@ export abstract class ListWidget extends ReactWidget itemRenderer={this.options.itemRenderer} filterTextChangeEvent={this.filterTextChangeEmitter.event} messageService={this.messageService} - commandService={this.commandService} />; + commandService={this.commandService} + responseService={this.responseService} />; } /** diff --git a/arduino-ide-extension/src/browser/widgets/progress-dialog.tsx b/arduino-ide-extension/src/browser/widgets/progress-dialog.tsx deleted file mode 100644 index bfe564d13..000000000 --- a/arduino-ide-extension/src/browser/widgets/progress-dialog.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { AbstractDialog } from '@theia/core/lib/browser'; - -export class InstallationProgressDialog extends AbstractDialog { - - readonly value = undefined; - - constructor(componentName: string, version: string) { - super({ title: 'Installation in progress' }); - this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`; - } - -} - -export class UninstallationProgressDialog extends AbstractDialog { - - readonly value = undefined; - - constructor(componentName: string) { - super({ title: 'Uninstallation in progress' }); - this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`; - } - -} diff --git a/arduino-ide-extension/src/common/protocol/index.ts b/arduino-ide-extension/src/common/protocol/index.ts index 78219adf1..de17ea88b 100644 --- a/arduino-ide-extension/src/common/protocol/index.ts +++ b/arduino-ide-extension/src/common/protocol/index.ts @@ -11,5 +11,5 @@ export * from './searchable'; export * from './sketches-service'; export * from './examples-service'; export * from './executable-service'; -export * from './output-service'; +export * from './response-service'; export * from './notification-service'; diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index 1160f2b12..c90ebc164 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,20 +1,26 @@ import * as semver from 'semver'; +import { Progress } from '@theia/core/lib/common/message-service-protocol'; +import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { naturalCompare } from './../utils'; import { ArduinoComponent } from './arduino-component'; +import { MessageService } from '@theia/core'; +import { ResponseServiceImpl } from '../../browser/response-service-impl'; export interface Installable { /** * If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`. */ - install(options: { item: T, version?: Installable.Version }): Promise; + install(options: { item: T, progressId?: string, version?: Installable.Version }): Promise; /** * Uninstalls the given component. It is a NOOP if not installed. */ - uninstall(options: { item: T }): Promise; + uninstall(options: { item: T, progressId?: string }): Promise; } export namespace Installable { + export type Version = string; + export namespace Version { /** * Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.) @@ -26,4 +32,70 @@ export namespace Installable { return naturalCompare(left, right); }; } + + export async function installWithProgress(options: { + installable: Installable, + messageService: MessageService, + responseService: ResponseServiceImpl, + item: T, + version: Installable.Version + }): Promise { + + const { item, version } = options; + return doWithProgress({ + ...options, + progressText: `Processing ${item.name}:${version}`, + run: ({ progressId }) => options.installable.install({ ...options, progressId }) + }); + } + + export async function uninstallWithProgress(options: { + installable: Installable, + messageService: MessageService, + responseService: ResponseServiceImpl, + item: T + }): Promise { + + const { item } = options; + return doWithProgress({ + ...options, + progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`, + run: ({ progressId }) => options.installable.uninstall({ ...options, progressId }) + }); + } + + export async function doWithProgress(options: { + run: ({ progressId }: { progressId: string }) => Promise, + messageService: MessageService, + responseService: ResponseServiceImpl, + progressText: string + }): Promise { + + return withProgress(options.progressText, options.messageService, async (progress, _) => { + const progressId = progress.id; + const toDispose = options.responseService.onProgressDidChange(progressMessage => { + if (progressId === progressMessage.progressId) { + const { message, work } = progressMessage; + progress.report({ message, work }); + } + }); + try { + options.responseService.clearArduinoChannel(); + await options.run({ ...options, progressId }); + } finally { + toDispose.dispose(); + } + }); + } + + async function withProgress(text: string, messageService: MessageService, cb: (progress: Progress, token: CancellationToken) => Promise): Promise { + const cancellationSource = new CancellationTokenSource(); + const { token } = cancellationSource; + const progress = await messageService.showProgress({ text, options: { cancelable: false } }, () => cancellationSource.cancel()); + try { + await cb(progress, token); + } finally { + progress.cancel(); + } + } } diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index 7369e84f9..4e3c4536d 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -9,8 +9,8 @@ export interface LibraryService extends Installable, Searchable< /** * When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`. */ - install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise; - installZip(options: { zipUri: string, overwrite?: boolean }): Promise; + install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise; + installZip(options: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise; /** * Set `filterSelf` to `true` if you want to avoid having `item` in the result set. * Note: as of today (22.02.2021), the CLI works like this: `./arduino-cli lib deps Adaino@0.1.0 ✕ Adaino 0.1.0 must be installed.`. diff --git a/arduino-ide-extension/src/common/protocol/output-service.ts b/arduino-ide-extension/src/common/protocol/output-service.ts deleted file mode 100644 index 84509f679..000000000 --- a/arduino-ide-extension/src/common/protocol/output-service.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface OutputMessage { - readonly chunk: string; - readonly severity?: 'error' | 'warning' | 'info'; // Currently not used! -} - -export const OutputServicePath = '/services/output-service'; -export const OutputService = Symbol('OutputService'); -export interface OutputService { - append(message: OutputMessage): void; -} diff --git a/arduino-ide-extension/src/common/protocol/response-service.ts b/arduino-ide-extension/src/common/protocol/response-service.ts new file mode 100644 index 000000000..7e263757c --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/response-service.ts @@ -0,0 +1,23 @@ +export interface OutputMessage { + readonly chunk: string; + readonly severity?: 'error' | 'warning' | 'info'; // Currently not used! +} + +export interface ProgressMessage { + readonly progressId: string; + readonly message: string; + readonly work?: ProgressMessage.Work; +} +export namespace ProgressMessage { + export interface Work { + readonly done: number; + readonly total: number; + } +} + +export const ResponseServicePath = '/services/response-service'; +export const ResponseService = Symbol('ResponseService'); +export interface ResponseService { + appendToOutput(message: OutputMessage): void; + reportProgress(message: ProgressMessage): void; +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index e31778c1c..3e98610bd 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -29,7 +29,7 @@ import { ExamplesServiceImpl } from './examples-service-impl'; import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service'; import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service'; import { ExecutableServiceImpl } from './executable-service-impl'; -import { OutputServicePath, OutputService } from '../common/protocol/output-service'; +import { ResponseServicePath, ResponseService } from '../common/protocol/response-service'; import { NotificationServiceServerImpl } from './notification-service-server'; import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol'; import { BackendApplication } from './theia/core/backend-application'; @@ -127,7 +127,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Output service per connection. bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => { - bindFrontendService(OutputServicePath, OutputService); + bindFrontendService(ResponseServicePath, ResponseService); })); // Notify all connected frontend instances diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 772bd7953..5f5f1e26b 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -4,17 +4,18 @@ import { notEmpty } from '@theia/core/lib/common/objects'; import { BoardsService, Installable, - BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, OutputService, NotificationServiceServer, AvailablePorts, BoardWithPackage + BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage } from '../common/protocol'; import { - PlatformInstallRequest, PlatformInstallResponse, PlatformListRequest, PlatformListResponse, PlatformSearchRequest, - PlatformSearchResponse, PlatformUninstallRequest, PlatformUninstallResponse + PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest, + PlatformSearchResponse, PlatformUninstallRequest } from './cli-protocol/cc/arduino/cli/commands/v1/core_pb'; import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { BoardDiscovery } from './board-discovery'; import { CoreClientAware } from './core-client-provider'; import { BoardDetailsRequest, BoardDetailsResponse, BoardSearchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb'; import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; +import { InstallWithProgress } from './grpc-installable'; @injectable() export class BoardsServiceImpl extends CoreClientAware implements BoardsService { @@ -26,8 +27,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService @named('discovery') protected discoveryLogger: ILogger; - @inject(OutputService) - protected readonly outputService: OutputService; + @inject(ResponseService) + protected readonly responseService: ResponseService; @inject(NotificationServiceServer) protected readonly notificationService: NotificationServiceServer; @@ -254,7 +255,7 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService return [...packages.values()]; } - async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise { + async install(options: { item: BoardsPackage, progressId?: string, version?: Installable.Version }): Promise { const item = options.item; const version = !!options.version ? options.version : item.availableVersions[0]; const coreClient = await this.coreClient(); @@ -270,17 +271,12 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService console.info('>>> Starting boards package installation...', item); const resp = client.platformInstall(req); - resp.on('data', (r: PlatformInstallResponse) => { - const prog = r.getProgress(); - if (prog && prog.getFile()) { - this.outputService.append({ chunk: `downloading ${prog.getFile()}\n` }); - } - }); + resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', error => { - this.outputService.append({ chunk: `Failed to install platform: ${item.id}.\n` }); - this.outputService.append({ chunk: error.toString() }); + this.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` }); + this.responseService.appendToOutput({ chunk: error.toString() }); reject(error); }); }); @@ -291,8 +287,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService console.info('<<< Boards package installation done.', item); } - async uninstall(options: { item: BoardsPackage }): Promise { - const item = options.item; + async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise { + const { item, progressId } = options; const coreClient = await this.coreClient(); const { client, instance } = coreClient; @@ -304,14 +300,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService req.setPlatformPackage(platform); console.info('>>> Starting boards package uninstallation...', item); - let logged = false; const resp = client.platformUninstall(req); - resp.on('data', (_: PlatformUninstallResponse) => { - if (!logged) { - this.outputService.append({ chunk: `uninstalling ${item.id}\n` }); - logged = true; - } - }) + resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject); diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 07d5dd811..3084dc619 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -2,22 +2,22 @@ import { FileUri } from '@theia/core/lib/node/file-uri'; import { inject, injectable } from 'inversify'; import { relative } from 'path'; import * as jspb from 'google-protobuf'; +import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; +import { ClientReadableStream } from '@grpc/grpc-js'; import { CompilerWarnings, CoreService } from '../common/protocol/core-service'; import { CompileRequest, CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; import { CoreClientAware } from './core-client-provider'; import { BurnBootloaderRequest, BurnBootloaderResponse, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, UploadUsingProgrammerResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; -import { OutputService } from '../common/protocol/output-service'; +import { ResponseService } from '../common/protocol/response-service'; import { NotificationServiceServer } from '../common/protocol'; -import { ClientReadableStream } from '@grpc/grpc-js'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; import { firstToUpperCase, firstToLowerCase } from '../common/utils'; -import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; @injectable() export class CoreServiceImpl extends CoreClientAware implements CoreService { - @inject(OutputService) - protected readonly outputService: OutputService; + @inject(ResponseService) + protected readonly responseService: ResponseService; @inject(NotificationServiceServer) protected readonly notificationService: NotificationServiceServer; @@ -53,15 +53,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (cr: CompileResponse) => { - this.outputService.append({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() }); - this.outputService.append({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); - this.outputService.append({ chunk: '\n--------------------------\nCompilation complete.\n' }); + this.responseService.appendToOutput({ chunk: '\n--------------------------\nCompilation complete.\n' }); } catch (e) { - this.outputService.append({ chunk: `Compilation error: ${e}\n`, severity: 'error' }); + this.responseService.appendToOutput({ chunk: `Compilation error: ${e}\n`, severity: 'error' }); throw e; } } @@ -107,15 +107,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (resp: UploadResponse) => { - this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); - this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); - this.outputService.append({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' }); + this.responseService.appendToOutput({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' }); } catch (e) { - this.outputService.append({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' }); + this.responseService.appendToOutput({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' }); throw e; } } @@ -141,14 +141,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (resp: BurnBootloaderResponse) => { - this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); - this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); + this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); } catch (e) { - this.outputService.append({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' }); + this.responseService.appendToOutput({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' }); throw e; } } diff --git a/arduino-ide-extension/src/node/grpc-client-provider.ts b/arduino-ide-extension/src/node/grpc-client-provider.ts index 49ba5a9de..845e416f0 100644 --- a/arduino-ide-extension/src/node/grpc-client-provider.ts +++ b/arduino-ide-extension/src/node/grpc-client-provider.ts @@ -24,7 +24,7 @@ export abstract class GrpcClientProvider { const updateClient = () => { const cliConfig = this.configService.cliConfiguration; this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined); - } + }; this.configService.onConfigChange(updateClient); this.daemon.ready.then(updateClient); this.daemon.onDaemonStopped(() => { @@ -33,7 +33,7 @@ export abstract class GrpcClientProvider { } this._client = undefined; this._port = undefined; - }) + }); } async client(): Promise { diff --git a/arduino-ide-extension/src/node/grpc-installable.ts b/arduino-ide-extension/src/node/grpc-installable.ts new file mode 100644 index 000000000..b03b6150c --- /dev/null +++ b/arduino-ide-extension/src/node/grpc-installable.ts @@ -0,0 +1,72 @@ +import { ProgressMessage, ResponseService } from '../common/protocol/response-service'; +import { DownloadProgress, TaskProgress } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; + +export interface InstallResponse { + getProgress?(): DownloadProgress | undefined; + getTaskProgress(): TaskProgress | undefined; +} + +export namespace InstallWithProgress { + + export interface Options { + /** + * _unknown_ progress if falsy. + */ + readonly progressId?: string; + readonly responseService: ResponseService; + } + + export function createDataCallback({ responseService, progressId }: InstallWithProgress.Options): (response: InstallResponse) => void { + let localFile = ''; + let localTotalSize = Number.NaN; + return (response: InstallResponse) => { + const download = response.getProgress ? response.getProgress() : undefined; + const task = response.getTaskProgress(); + if (!download && !task) { + throw new Error("Implementation error. Neither 'download' nor 'task' is available."); + } + if (task && download) { + throw new Error("Implementation error. Both 'download' and 'task' are available."); + } + if (task) { + const message = task.getName() || task.getMessage(); + if (message) { + if (progressId) { + responseService.reportProgress({ progressId, message, work: { done: Number.NaN, total: Number.NaN } }); + } + responseService.appendToOutput({ chunk: `${message}\n` }); + } + } else if (download) { + if (download.getFile() && !localFile) { + localFile = download.getFile(); + } + if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) { + localTotalSize = download.getTotalSize(); + } + + // This happens only once per file download. + if (download.getTotalSize() && localFile) { + responseService.appendToOutput({ chunk: `${localFile}\n` }); + } + + if (progressId && localFile) { + let work: ProgressMessage.Work | undefined = undefined; + if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) { + work = { total: localTotalSize, done: download.getDownloaded() }; + } + responseService.reportProgress({ progressId, message: `Downloading ${localFile}`, work }); + } + if (download.getCompleted()) { + // Discard local state. + if (progressId && !Number.isNaN(localTotalSize)) { + responseService.reportProgress({ progressId, message: '', work: { done: Number.NaN, total: Number.NaN } }); + } + localFile = ''; + localTotalSize = Number.NaN; + } + } + }; + } + +} + diff --git a/arduino-ide-extension/src/node/library-service-server-impl.ts b/arduino-ide-extension/src/node/library-service-server-impl.ts index 89ff74af4..7b65d4f8b 100644 --- a/arduino-ide-extension/src/node/library-service-server-impl.ts +++ b/arduino-ide-extension/src/node/library-service-server-impl.ts @@ -2,14 +2,15 @@ import { injectable, inject } from 'inversify'; import { LibraryDependency, LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol/library-service'; import { CoreClientAware } from './core-client-provider'; import { - InstalledLibrary, Library, LibraryInstallRequest, LibraryInstallResponse, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease, - LibraryResolveDependenciesRequest, LibraryUninstallRequest, LibraryUninstallResponse, ZipLibraryInstallRequest, ZipLibraryInstallResponse, LibrarySearchRequest, + InstalledLibrary, Library, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease, + LibraryResolveDependenciesRequest, LibraryUninstallRequest, ZipLibraryInstallRequest, LibrarySearchRequest, LibrarySearchResponse } from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb'; import { Installable } from '../common/protocol/installable'; import { ILogger, notEmpty } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; -import { OutputService, NotificationServiceServer } from '../common/protocol'; +import { ResponseService, NotificationServiceServer } from '../common/protocol'; +import { InstallWithProgress } from './grpc-installable'; @injectable() export class LibraryServiceImpl extends CoreClientAware implements LibraryService { @@ -17,8 +18,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic @inject(ILogger) protected logger: ILogger; - @inject(OutputService) - protected readonly outputService: OutputService; + @inject(ResponseService) + protected readonly responseService: ResponseService; @inject(NotificationServiceServer) protected readonly notificationServer: NotificationServiceServer; @@ -157,7 +158,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies; } - async install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise { + async install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise { const item = options.item; const version = !!options.version ? options.version : item.availableVersions[0]; const coreClient = await this.coreClient(); @@ -173,17 +174,12 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic console.info('>>> Starting library package installation...', item); const resp = client.libraryInstall(req); - resp.on('data', (r: LibraryInstallResponse) => { - const prog = r.getProgress(); - if (prog) { - this.outputService.append({ chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` }); - } - }); + resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', error => { - this.outputService.append({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` }); - this.outputService.append({ chunk: error.toString() }); + this.responseService.appendToOutput({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` }); + this.responseService.appendToOutput({ chunk: error.toString() }); reject(error); }); }); @@ -194,7 +190,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic console.info('<<< Library package installation done.', item); } - async installZip({ zipUri, overwrite }: { zipUri: string, overwrite?: boolean }): Promise { + async installZip({ zipUri, progressId, overwrite }: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; const req = new ZipLibraryInstallRequest(); @@ -204,20 +200,15 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic req.setOverwrite(overwrite); } const resp = client.zipLibraryInstall(req); - resp.on('data', (r: ZipLibraryInstallResponse) => { - const task = r.getTaskProgress(); - if (task && task.getMessage()) { - this.outputService.append({ chunk: task.getMessage() }); - } - }); + resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject); }); } - async uninstall(options: { item: LibraryPackage }): Promise { - const item = options.item; + async uninstall(options: { item: LibraryPackage, progressId?: string }): Promise { + const { item, progressId } = options; const coreClient = await this.coreClient(); const { client, instance } = coreClient; @@ -227,14 +218,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic req.setVersion(item.installedVersion!); console.info('>>> Starting library package uninstallation...', item); - let logged = false; const resp = client.libraryUninstall(req); - resp.on('data', (_: LibraryUninstallResponse) => { - if (!logged) { - this.outputService.append({ chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` }); - logged = true; - } - }); + resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject);