From 3fcf5a6ac9728b8754d35bbb27d1a13eff079551 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 30 Jul 2019 16:45:04 +0200 Subject: [PATCH 01/10] generalized the boards and the libraries views. Signed-off-by: Akos Kitta --- .../src/browser/arduino-frontend-module.ts | 6 +- .../browser/boards/boards-item-renderer.tsx | 38 ++++++ .../src/browser/boards/boards-list-widget.ts | 27 +++++ .../src/browser/boards/boards-list-widget.tsx | 16 --- .../boards-widget-frontend-contribution.ts | 18 +-- .../src/browser/boards/list-widget.tsx | 76 ------------ .../component-list/component-list-item.tsx | 71 ++--------- .../component-list/component-list.tsx | 30 +++-- .../filterable-list-container.tsx | 73 ++++++------ .../component-list/list-item-renderer.tsx | 21 ++++ .../list-widget-frontend-contribution.ts | 12 ++ .../component-list/list-widget.tsx} | 53 ++++----- .../installation-progress-dialog.tsx | 8 +- .../library/library-component-list-item.tsx | 50 -------- .../library/library-component-list.tsx | 17 --- .../library-filterable-list-container.tsx | 110 ------------------ .../browser/library/library-item-renderer.tsx | 52 +++++++++ .../browser/library/library-list-widget.ts | 27 +++++ ...> library-widget-frontend-contribution.ts} | 0 .../src/common/protocol/boards-service.ts | 10 +- .../src/common/protocol/installable.ts | 3 + .../src/common/protocol/library-service.ts | 15 +-- .../src/common/protocol/searchable.ts | 11 ++ .../src/node/library-service-impl.ts | 2 +- 24 files changed, 303 insertions(+), 443 deletions(-) create mode 100644 arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx create mode 100644 arduino-ide-extension/src/browser/boards/boards-list-widget.ts delete mode 100644 arduino-ide-extension/src/browser/boards/boards-list-widget.tsx delete mode 100644 arduino-ide-extension/src/browser/boards/list-widget.tsx create mode 100644 arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx create mode 100644 arduino-ide-extension/src/browser/components/component-list/list-widget-frontend-contribution.ts rename arduino-ide-extension/src/browser/{library/library-list-widget.tsx => components/component-list/list-widget.tsx} (59%) delete mode 100644 arduino-ide-extension/src/browser/library/library-component-list-item.tsx delete mode 100644 arduino-ide-extension/src/browser/library/library-component-list.tsx delete mode 100644 arduino-ide-extension/src/browser/library/library-filterable-list-container.tsx create mode 100644 arduino-ide-extension/src/browser/library/library-item-renderer.tsx create mode 100644 arduino-ide-extension/src/browser/library/library-list-widget.ts rename arduino-ide-extension/src/browser/library/{list-widget-frontend-contribution.ts => library-widget-frontend-contribution.ts} (100%) create mode 100644 arduino-ide-extension/src/common/protocol/installable.ts create mode 100644 arduino-ide-extension/src/common/protocol/searchable.ts diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 8e361456..2ea69840 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -13,7 +13,6 @@ import { ArduinoLanguageGrammarContribution } from './language/arduino-language- import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; -import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution'; import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { BoardsListWidget } from './boards/boards-list-widget'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; @@ -52,6 +51,9 @@ import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution'; import { SilentScmContribution } from './customization/silent-scm-contribution'; import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'; import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution'; +import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution'; +import { LibraryItemRenderer } from './library/library-item-renderer'; +import { BoardItemRenderer } from './boards/boards-item-renderer'; const ElementQueries = require('css-element-queries/src/ElementQueries'); if (!ARDUINO_PRO_MODE) { @@ -87,6 +89,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un createWidget: () => context.container.get(LibraryListWidget) })); bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution); + bind(LibraryItemRenderer).toSelf().inSingletonScope(); // Sketch list service bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope(); @@ -113,6 +116,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un createWidget: () => context.container.get(BoardsListWidget) })); bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution); + bind(BoardItemRenderer).toSelf().inSingletonScope(); // Board select dialog bind(BoardsConfigDialogWidget).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx b/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx new file mode 100644 index 00000000..0ba76b85 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { injectable } from 'inversify'; +import { ListItemRenderer } from '../components/component-list/list-item-renderer'; +import { BoardPackage } from '../../common/protocol/boards-service'; + +@injectable() +export class BoardItemRenderer extends ListItemRenderer { + + renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise): React.ReactNode { + const name = {item.name}; + const author = {item.author}; + const installedVersion = !!item.installedVersion &&
+ Version {item.installedVersion} + INSTALLED +
; + + const summary =
{item.summary}
; + + const moreInfo = !!item.moreInfoLink && More info; + const installButton = item.installable && !item.installedVersion && + ; + + return
+
+ {name} by {author} + {installedVersion} +
+
+ {summary} +
+
+ {moreInfo} + {installButton} +
+
; + } + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts new file mode 100644 index 00000000..4f13c711 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts @@ -0,0 +1,27 @@ +import { inject, injectable } from 'inversify'; +import { BoardPackage, BoardsService } from '../../common/protocol/boards-service'; +import { ListWidget } from '../components/component-list/list-widget'; +import { BoardItemRenderer } from './boards-item-renderer'; + +@injectable() +export class BoardsListWidget extends ListWidget { + + static WIDGET_ID = 'boards-list-widget'; + static WIDGET_LABEL = 'Boards Manager'; + + constructor( + @inject(BoardsService) protected service: BoardsService, + @inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) { + + super({ + id: BoardsListWidget.WIDGET_ID, + label: BoardsListWidget.WIDGET_LABEL, + iconClass: 'fa fa-microchip', + searchable: service, + installable: service, + itemLabel: (item: BoardPackage) => item.name, + itemRenderer + }); + } + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx b/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx deleted file mode 100644 index 5ef50d07..00000000 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ListWidget } from './list-widget'; - -export class BoardsListWidget extends ListWidget { - - static WIDGET_ID = 'boards-list-widget'; - static WIDGET_LABEL = 'Boards Manager'; - - protected widgetProps(): ListWidget.Props { - return { - id: BoardsListWidget.WIDGET_ID, - title: BoardsListWidget.WIDGET_LABEL, - iconClass: 'fa fa-microchip' - } - } - -} diff --git a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts index f7211947..d46b3f39 100644 --- a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts @@ -1,22 +1,12 @@ import { injectable } from 'inversify'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; -import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; -import { ListWidget } from './list-widget'; -import { BoardsListWidget } from './boards-list-widget'; import { MenuModelRegistry } from '@theia/core'; +import { BoardsListWidget } from './boards-list-widget'; import { ArduinoMenus } from '../arduino-frontend-contribution'; +import { BoardPackage } from '../../common/protocol/boards-service'; +import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution'; @injectable() -export abstract class ListWidgetFrontendContribution extends AbstractViewContribution implements FrontendApplicationContribution { - - async initializeLayout(): Promise { - // await this.openView(); - } - -} - -@injectable() -export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution { +export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution { static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`; diff --git a/arduino-ide-extension/src/browser/boards/list-widget.tsx b/arduino-ide-extension/src/browser/boards/list-widget.tsx deleted file mode 100644 index a0ad85ca..00000000 --- a/arduino-ide-extension/src/browser/boards/list-widget.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from 'react'; -import { inject, injectable, postConstruct } from 'inversify'; -import { Message } from '@phosphor/messaging'; -import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { BoardsService } from '../../common/protocol/boards-service'; -import { FilterableListContainer } from '../components/component-list/filterable-list-container'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; - -@injectable() -export abstract class ListWidget extends ReactWidget { - - @inject(BoardsService) - protected readonly boardsService: BoardsService; - - @inject(WindowService) - protected readonly windowService: WindowService; - - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; - - constructor() { - super(); - const { id, title, iconClass } = this.widgetProps(); - this.id = id; - this.title.label = title; - this.title.caption = title; - this.title.iconClass = iconClass; - this.title.closable = true; - this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS); - this.node.tabIndex = 0; // To be able to set the focus on the widget. - } - - protected abstract widgetProps(): ListWidget.Props; - - @postConstruct() - protected init(): void { - this.update(); - } - - protected onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - this.node.focus(); - this.render(); - } - - protected onUpdateRequest(msg: Message): void { - super.onUpdateRequest(msg); - this.render(); - } - - render(): React.ReactNode { - return ; - } - -} - -export namespace ListWidget { - - /** - * Props for customizing the abstract list widget. - */ - export interface Props { - readonly id: string; - readonly title: string; - readonly iconClass: string; - } - - export namespace Styles { - export const LIST_WIDGET_CLASS = 'arduino-list-widget' - } - -} diff --git a/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx b/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx index 6ac4b723..ea92dfc1 100644 --- a/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx @@ -1,78 +1,25 @@ import * as React from 'react'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; +import { ListItemRenderer } from './list-item-renderer'; -export class ComponentListItem extends React.Component { +export class ComponentListItem extends React.Component> { - protected onClick = (event: React.SyntheticEvent) => { - const { target } = event.nativeEvent; - if (target instanceof HTMLAnchorElement) { - this.props.windowService.openNewWindow(target.href); - event.nativeEvent.preventDefault(); - } - } - - protected async install(item: ArduinoComponent): Promise { + protected async install(item: T): Promise { await this.props.install(item); } render(): React.ReactNode { - const { item } = this.props; - - const style = ComponentListItem.Styles; - const name = {item.name}; - const author = {item.author}; - const installedVersion = !!item.installedVersion &&
- Version {item.installedVersion} - INSTALLED -
; - - const summary =
{item.summary}
; - - const moreInfo = !!item.moreInfoLink && More info; - const install = this.props.install && item.installable && !item.installedVersion && - ; - - return
-
- {name} by {author} - {installedVersion} -
-
- {summary} -
-
- {moreInfo} - {install} -
-
; + const { item, itemRenderer, install } = this.props; + return itemRenderer.renderItem(item, install.bind(this)); } } export namespace ComponentListItem { - export interface Props { - readonly item: ArduinoComponent; - readonly windowService: WindowService; - readonly install: (comp: ArduinoComponent) => Promise; - } - - export namespace Styles { - export const LIST_ITEM_CLASS = 'component-list-item'; - export const HEADER_CLASS = 'header'; - export const VERSION_INFO_CLASS = 'version-info'; - export const CONTENT_CLASS = 'content'; - export const FOOTER_CLASS = 'footer'; - export const INSTALLED_CLASS = 'installed'; - export const NO_SELECT_CLASS = 'noselect'; - - export const NAME_CLASS = 'name'; - export const AUTHOR_CLASS = 'author'; - export const VERSION_CLASS = 'version'; - export const SUMMARY_CLASS = 'summary'; - export const DESCRIPTION_CLASS = 'description'; - export const INSTALL_BTN_CLASS = 'install'; + export interface Props { + readonly item: T; + readonly install: (item: T) => Promise; + readonly itemRenderer: ListItemRenderer; } } diff --git a/arduino-ide-extension/src/browser/components/component-list/component-list.tsx b/arduino-ide-extension/src/browser/components/component-list/component-list.tsx index 8a8c7574..0fe2b2c5 100644 --- a/arduino-ide-extension/src/browser/components/component-list/component-list.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/component-list.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { ComponentListItem } from './component-list-item'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; +import { ListItemRenderer } from './list-item-renderer'; -export class ComponentList extends React.Component { +export class ComponentList extends React.Component> { protected container?: HTMLElement; render(): React.ReactNode { return
this.container = element || undefined}> + ref={this.setRef}> {this.props.items.map(item => this.createItem(item))}
; } @@ -21,19 +20,28 @@ export class ComponentList extends React.Component { } } - protected createItem(item: ArduinoComponent): React.ReactNode { - return + protected setRef = (element: HTMLElement | null) => { + this.container = element || undefined; + } + + protected createItem(item: T): React.ReactNode { + return + key={this.props.itemLabel(item)} + item={item} + itemRenderer={this.props.itemRenderer} + install={this.props.install} /> } } export namespace ComponentList { - export interface Props { - readonly items: ArduinoComponent[]; - readonly windowService: WindowService; - readonly install: (comp: ArduinoComponent) => Promise; - readonly resolveContainer?: (element: HTMLElement) => void; + export interface Props { + readonly items: T[]; + readonly itemLabel: (item: T) => string; + readonly itemRenderer: ListItemRenderer; + readonly install: (item: T) => Promise; + readonly resolveContainer: (element: HTMLElement) => void; } } diff --git a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx index af1cbf78..b7764764 100644 --- a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx @@ -1,23 +1,24 @@ import * as React from 'react'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import debounce = require('lodash.debounce'); +import { Searchable } from '../../../common/protocol/searchable'; +import { Installable } from '../../../common/protocol/installable'; +import { InstallationProgressDialog } from '../installation-progress-dialog'; import { SearchBar } from './search-bar'; import { ComponentList } from './component-list'; -import { LibraryService } from '../../../common/protocol/library-service'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; -import { InstallationProgressDialog } from '../installation-progress-dialog'; +import { ListItemRenderer } from './list-item-renderer'; -export class FilterableListContainer extends React.Component { +export class FilterableListContainer extends React.Component, FilterableListContainer.State> { - constructor(props: Readonly) { + constructor(props: Readonly>) { super(props); this.state = { filterText: '', items: [] }; - this.handleFilterTextChange = this.handleFilterTextChange.bind(this); } componentWillMount(): void { + this.search = debounce(this.search, 500); this.handleFilterTextChange(''); } @@ -42,36 +43,43 @@ export class FilterableListContainer extends React.Component items={this.state.items} + itemLabel={itemLabel} + itemRenderer={itemRenderer} install={this.install.bind(this)} - windowService={this.props.windowService} - resolveContainer={this.props.resolveContainer} + resolveContainer={resolveContainer} /> } - private handleFilterTextChange(filterText: string): void { - const { props } = this.state; - this.props.service.search({ query: filterText, props }).then(result => { + protected handleFilterTextChange = (filterText: string) => { + this.setState({ filterText }); + this.search(filterText); + } + + protected search (query: string): void { + const { searchable } = this.props; + searchable.search({ query }).then(result => { const { items } = result; this.setState({ - filterText, items: this.sort(items) }); }); } - protected sort(items: ArduinoComponent[]): ArduinoComponent[] { - return items.sort((left, right) => left.name.localeCompare(right.name)); + protected sort(items: T[]): T[] { + const { itemLabel } = this.props; + return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right))); } - protected async install(comp: ArduinoComponent): Promise { - const dialog = new InstallationProgressDialog(comp.name); + protected async install(item: T): Promise { + const { installable, searchable, itemLabel } = this.props; + const dialog = new InstallationProgressDialog(itemLabel(item)); dialog.open(); try { - await this.props.service.install(comp); - const { props } = this.state; - const { items } = await this.props.service.search({ query: this.state.filterText, props }); + await installable.install(item); + const { items } = await searchable.search({ query: this.state.filterText }); this.setState({ items: this.sort(items) }); } finally { dialog.close(); @@ -82,23 +90,18 @@ export class FilterableListContainer extends React.Component void; - readonly resolveFocus?: (element: HTMLElement | undefined) => void; + export interface Props { + readonly installable: Installable; + readonly searchable: Searchable; + readonly itemLabel: (item: T) => string; + readonly itemRenderer: ListItemRenderer; + readonly resolveContainer: (element: HTMLElement) => void; + readonly resolveFocus: (element: HTMLElement | undefined) => void; } - export interface State { + export interface State { filterText: string; - items: ArduinoComponent[]; - props?: LibraryService.Search.Props; - } - - export interface ComponentSource { - search(req: { query: string, props?: LibraryService.Search.Props }): Promise<{ items: ArduinoComponent[] }> - install(board: ArduinoComponent): Promise; + items: T[]; } } - diff --git a/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx b/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx new file mode 100644 index 00000000..e683275c --- /dev/null +++ b/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { inject, injectable } from 'inversify'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; + +@injectable() +export abstract class ListItemRenderer { + + @inject(WindowService) + protected windowService: WindowService; + + protected onClick = (event: React.SyntheticEvent) => { + const { target } = event.nativeEvent; + if (target instanceof HTMLAnchorElement) { + this.windowService.openNewWindow(target.href); + event.nativeEvent.preventDefault(); + } + } + + abstract renderItem(item: T, install: (item: T) => Promise): React.ReactNode; + +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/components/component-list/list-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/components/component-list/list-widget-frontend-contribution.ts new file mode 100644 index 00000000..448f8070 --- /dev/null +++ b/arduino-ide-extension/src/browser/components/component-list/list-widget-frontend-contribution.ts @@ -0,0 +1,12 @@ +import { injectable } from 'inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; +import { ListWidget } from './list-widget'; + +@injectable() +export abstract class ListWidgetFrontendContribution extends AbstractViewContribution> implements FrontendApplicationContribution { + + async initializeLayout(): Promise { + } + +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.tsx b/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx similarity index 59% rename from arduino-ide-extension/src/browser/library/library-list-widget.tsx rename to arduino-ide-extension/src/browser/components/component-list/list-widget.tsx index 00b74e8e..897a76ff 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx @@ -1,24 +1,16 @@ import * as React from 'react'; -import { inject, injectable, postConstruct } from 'inversify'; +import { injectable, postConstruct } from 'inversify'; import { Message } from '@phosphor/messaging'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { LibraryFilterableListContainer } from './library-filterable-list-container'; -import { LibraryService } from '../../common/protocol/library-service'; +import { Installable } from '../../../common/protocol/installable'; +import { Searchable } from '../../../common/protocol/searchable'; +import { FilterableListContainer } from './filterable-list-container'; +import { ListItemRenderer } from './list-item-renderer'; @injectable() -export class LibraryListWidget extends ReactWidget { - - static WIDGET_ID = 'library-list-widget'; - static WIDGET_LABEL = 'Library Manager'; - - @inject(LibraryService) - protected readonly libraryService: LibraryService; - - @inject(WindowService) - protected readonly windowService: WindowService; +export abstract class ListWidget extends ReactWidget { /** * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. @@ -26,12 +18,13 @@ export class LibraryListWidget extends ReactWidget { protected focusNode: HTMLElement | undefined; protected readonly deferredContainer = new Deferred(); - constructor() { + constructor(protected options: ListWidget.Options) { super(); - this.id = LibraryListWidget.WIDGET_ID - this.title.label = LibraryListWidget.WIDGET_LABEL; - this.title.caption = LibraryListWidget.WIDGET_LABEL - this.title.iconClass = 'library-tab-icon'; + const { id, label, iconClass } = options; + this.id = id; + this.title.label = label; + this.title.caption = label; + this.title.iconClass = iconClass this.title.closable = true; this.addClass('arduino-list-widget'); this.node.tabIndex = 0; // To be able to set the focus on the widget. @@ -64,25 +57,25 @@ export class LibraryListWidget extends ReactWidget { } render(): React.ReactNode { - return resolveContainer={this.deferredContainer.resolve} resolveFocus={this.onFocusResolved} - service={this.libraryService} - windowService={this.windowService} - />; + searchable={this.options.searchable} + installable={this.options.installable} + itemLabel={this.options.itemLabel} + itemRenderer={this.options.itemRenderer} />; } } export namespace ListWidget { - - /** - * Props for customizing the abstract list widget. - */ - export interface Props { + export interface Options { readonly id: string; - readonly title: string; + readonly label: string; readonly iconClass: string; + readonly installable: Installable; + readonly searchable: Searchable; + readonly itemLabel: (item: T) => string; + readonly itemRenderer: ListItemRenderer; } - } diff --git a/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx b/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx index 8d0be2de..aefa3940 100644 --- a/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx +++ b/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx @@ -1,12 +1,12 @@ -import { AbstractDialog } from "@theia/core/lib/browser"; +import { AbstractDialog } from '@theia/core/lib/browser'; +export class InstallationProgressDialog extends AbstractDialog { -export class InstallationProgressDialog extends AbstractDialog { - readonly value: "does-not-matter"; + readonly value = undefined; constructor(componentName: string) { super({ title: 'Installation in progress' }); this.contentNode.textContent = `Installing ${componentName}. Please wait.`; } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/library/library-component-list-item.tsx b/arduino-ide-extension/src/browser/library/library-component-list-item.tsx deleted file mode 100644 index 988ddf72..00000000 --- a/arduino-ide-extension/src/browser/library/library-component-list-item.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import { ComponentListItem } from '../components/component-list/component-list-item'; - -export class LibraryComponentListItem extends ComponentListItem { - - render(): React.ReactNode { - const { item } = this.props; - - const name = {item.name}; - const author = by {item.author}; - const installedVersion = !!item.installedVersion &&
- Version {item.installedVersion} - INSTALLED -
; - - const summary =
{item.summary}
; - - const moreInfo = !!item.moreInfoLink && More info; - const install = this.props.install && item.installable && !item.installedVersion && - ; - const versions = (() => { - const { availableVersions } = item; - if (availableVersions.length === 0) { - return undefined; - } else if (availableVersions.length === 1) { - return - } else { - return ; - } - })(); - - return
-
- {name} {author} - {installedVersion} -
-
- {summary} -
-
- {moreInfo} -
-
- {install} - {versions} -
-
; - } - -} diff --git a/arduino-ide-extension/src/browser/library/library-component-list.tsx b/arduino-ide-extension/src/browser/library/library-component-list.tsx deleted file mode 100644 index 4cc67968..00000000 --- a/arduino-ide-extension/src/browser/library/library-component-list.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { ArduinoComponent } from '../../common/protocol/arduino-component'; -import { ComponentList } from '../components/component-list/component-list'; -import { LibraryComponentListItem } from './library-component-list-item'; - -export class LibraryComponentList extends ComponentList { - - createItem(item: ArduinoComponent): React.ReactNode { - return - } - -} diff --git a/arduino-ide-extension/src/browser/library/library-filterable-list-container.tsx b/arduino-ide-extension/src/browser/library/library-filterable-list-container.tsx deleted file mode 100644 index a532b45b..00000000 --- a/arduino-ide-extension/src/browser/library/library-filterable-list-container.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import * as React from 'react'; -import { FilterableListContainer } from '../components/component-list/filterable-list-container'; -import { LibraryComponentList } from './library-component-list'; - -export class LibraryFilterableListContainer extends FilterableListContainer { - - constructor(props: Readonly) { - super(props); - this.state = { - filterText: '', - items: [], - props: { - topic: this.topics[0], - type: this.types[0] - } - }; - } - - protected renderSearchFilter(): React.ReactNode { - const types = this.types.map(type => ); - let type = this.types[0]; - if (this.state.props) { - const currentType = this.types.find(t => t === this.state.props!.type) || this.types[0]; - if (currentType) { - type = currentType; - } - } - const topics = this.topics.map(topic => ); - let topic = this.topics[0]; - if (this.state.props) { - const currentTopic = this.topics.find(t => t === this.state.props!.topic) || this.topics[0]; - if (currentTopic) { - topic = currentTopic; - } - } - return
-
-
Type
{/** TODO: do `minWidth` better! */} - -
-
-
Topic
- -
-
- } - - protected onTypeChange = (event: React.ChangeEvent) => { - const type = event.target.value; - const props = { ...(this.state.props || {}), ...{ type } }; - this.setState({ - props - }); - } - - protected onTopicChange = (event: React.ChangeEvent) => { - const topic = event.target.value; - const props = { ...(this.state.props || {}), ...{ topic } }; - this.setState({ - props - }); - } - - protected renderComponentList(): React.ReactNode { - return - } - - private get topics(): string[] { - return [ - 'All', - 'Communication', - 'Data Processing', - 'Data Storage', - 'Device Control', - 'Display', - 'Other', - 'Sensor', - 'Signal Input/Output', - 'Timing', - 'Uncategorized' - ]; - } - - private get types(): string[] { - return [ - 'All', - 'Updatable', - 'Installed', - 'Arduino', - 'Partner', - 'Recommended', - 'Contributed', - 'Retired' - ]; - } - -} diff --git a/arduino-ide-extension/src/browser/library/library-item-renderer.tsx b/arduino-ide-extension/src/browser/library/library-item-renderer.tsx new file mode 100644 index 00000000..2e98480a --- /dev/null +++ b/arduino-ide-extension/src/browser/library/library-item-renderer.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { injectable } from 'inversify'; +import { Library } from '../../common/protocol/library-service'; +import { ListItemRenderer } from '../components/component-list/list-item-renderer'; + +@injectable() +export class LibraryItemRenderer extends ListItemRenderer { + + renderItem(item: Library, install: (item: Library) => Promise): React.ReactNode { + const name = {item.name}; + const author = by {item.author}; + const installedVersion = !!item.installedVersion &&
+ Version {item.installedVersion} + INSTALLED +
; + + const summary =
{item.summary}
; + + const moreInfo = !!item.moreInfoLink && More info; + const installButton = item.installable && !item.installedVersion && + ; + + const versions = (() => { + const { availableVersions } = item; + if (availableVersions.length === 0) { + return undefined; + } else if (availableVersions.length === 1) { + return + } else { + return ; + } + })(); + + return
+
+ {name} {author} + {installedVersion} +
+
+ {summary} +
+
+ {moreInfo} +
+
+ {installButton} + {versions} +
+
; + } + +} diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts new file mode 100644 index 00000000..83bd8bcc --- /dev/null +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -0,0 +1,27 @@ +import { inject, injectable } from 'inversify'; +import { Library, LibraryService } from '../../common/protocol/library-service'; +import { ListWidget } from '../components/component-list/list-widget'; +import { LibraryItemRenderer } from './library-item-renderer'; + +@injectable() +export class LibraryListWidget extends ListWidget { + + static WIDGET_ID = 'library-list-widget'; + static WIDGET_LABEL = 'Library Manager'; + + constructor( + @inject(LibraryService) protected service: LibraryService, + @inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) { + + super({ + id: LibraryListWidget.WIDGET_ID, + label: LibraryListWidget.WIDGET_LABEL, + iconClass: 'library-tab-icon', + searchable: service, + installable: service, + itemLabel: (item: Library) => item.name, + itemRenderer + }); + } + +} diff --git a/arduino-ide-extension/src/browser/library/list-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts similarity index 100% rename from arduino-ide-extension/src/browser/library/list-widget-frontend-contribution.ts rename to arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 5b781d81..edc2158a 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,5 +1,7 @@ -import { ArduinoComponent } from "./arduino-component"; -import { JsonRpcServer } from "@theia/core"; +import { JsonRpcServer } from '@theia/core'; +import { Searchable } from './searchable'; +import { Installable } from './installable'; +import { ArduinoComponent } from './arduino-component'; export interface AttachedBoardsChangeEvent { readonly oldState: Readonly<{ boards: Board[] }>; @@ -18,10 +20,8 @@ export interface BoardsServiceClient { export const BoardsServicePath = '/services/boards-service'; export const BoardsService = Symbol('BoardsService'); -export interface BoardsService extends JsonRpcServer { +export interface BoardsService extends Installable, Searchable, JsonRpcServer { getAttachedBoards(): Promise<{ boards: Board[] }>; - search(options: { query?: string }): Promise<{ items: BoardPackage[] }>; - install(item: BoardPackage): Promise; } export interface BoardPackage extends ArduinoComponent { diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts new file mode 100644 index 00000000..f9d1887f --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -0,0 +1,3 @@ +export interface Installable { + install(item: T): Promise; +} \ No newline at end of file diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index f8689d5a..67f463f7 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -1,20 +1,13 @@ -import { ArduinoComponent } from "./arduino-component"; +import { Searchable } from './searchable'; +import { Installable } from './installable'; +import { ArduinoComponent } from './arduino-component'; export const LibraryServicePath = '/services/library-service'; export const LibraryService = Symbol('LibraryService'); -export interface LibraryService { - search(options: { query?: string, props?: LibraryService.Search.Props }): Promise<{ items: Library[] }>; +export interface LibraryService extends Installable, Searchable { install(library: Library): Promise; } -export namespace LibraryService { - export namespace Search { - export interface Props { - [key: string]: string | undefined; - } - } -} - export interface Library extends ArduinoComponent { readonly builtIn?: boolean; } diff --git a/arduino-ide-extension/src/common/protocol/searchable.ts b/arduino-ide-extension/src/common/protocol/searchable.ts new file mode 100644 index 00000000..f4e996b9 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/searchable.ts @@ -0,0 +1,11 @@ +export interface Searchable { + search(options: Searchable.Options): Promise<{ items: T[] }>; +} +export namespace Searchable { + export interface Options { + /** + * Defaults to empty an empty string. + */ + readonly query?: string; + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index f149ae64..93a9cc42 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -14,7 +14,7 @@ export class LibraryServiceImpl implements LibraryService { @inject(ToolOutputServiceServer) protected readonly toolOutputService: ToolOutputServiceServer; - async search(options: { query?: string, props: LibraryService.Search.Props }): Promise<{ items: Library[] }> { + async search(options: { query?: string }): Promise<{ items: Library[] }> { const coreClient = await this.coreClientProvider.getClient(); if (!coreClient) { return { items: [] }; From 0dc45daf01e5bb8a5adcd0319fc6c8285babcde5 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 30 Jul 2019 17:40:10 +0200 Subject: [PATCH 02/10] aligned list view styles. Signed-off-by: Akos Kitta --- .../browser/boards/boards-item-renderer.tsx | 18 +++++++++++++++++- .../browser/library/library-item-renderer.tsx | 2 +- .../src/node/boards-service-impl.ts | 3 ++- .../src/node/library-service-impl.ts | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx b/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx index 0ba76b85..4b84360c 100644 --- a/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-item-renderer.tsx @@ -15,11 +15,23 @@ export class BoardItemRenderer extends ListItemRenderer { ; const summary =
{item.summary}
; + const description =
{item.description}
; const moreInfo = !!item.moreInfoLink && More info; const installButton = item.installable && !item.installedVersion && ; + const versions = (() => { + const { availableVersions } = item; + if (!!item.installedVersion || availableVersions.length === 0) { + return undefined; + } else if (availableVersions.length === 1) { + return + } else { + return ; + } + })(); + return
{name} by {author} @@ -27,10 +39,14 @@ export class BoardItemRenderer extends ListItemRenderer {
{summary} + {description} +
+
+ {moreInfo}
- {moreInfo} {installButton} + {versions}
; } diff --git a/arduino-ide-extension/src/browser/library/library-item-renderer.tsx b/arduino-ide-extension/src/browser/library/library-item-renderer.tsx index 2e98480a..cb504273 100644 --- a/arduino-ide-extension/src/browser/library/library-item-renderer.tsx +++ b/arduino-ide-extension/src/browser/library/library-item-renderer.tsx @@ -22,7 +22,7 @@ export class LibraryItemRenderer extends ListItemRenderer { const versions = (() => { const { availableVersions } = item; - if (availableVersions.length === 0) { + if (!!item.installedVersion || availableVersions.length === 0) { return undefined; } else if (availableVersions.length === 1) { return diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 7c41e875..843d0a87 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -172,12 +172,13 @@ export class BoardsServiceImpl implements BoardsService { id: item.getId(), name: item.getName(), author: item.getMaintainer(), - availableVersions: [item.getInstalled()], + availableVersions: [item.getLatest()], description: item.getBoardsList().map(b => b.getName()).join(", "), installable: true, summary: "Boards included in this package:", installedVersion, boards: item.getBoardsList().map(b => { name: b.getName(), fqbn: b.getFqbn() }), + moreInfoLink: item.getWebsite() } return result; }); diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index 93a9cc42..40c0dbdd 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -41,7 +41,7 @@ export class LibraryServiceImpl implements LibraryService { const resp = await new Promise((resolve, reject) => client.librarySearch(req, (err, resp) => !!err ? reject(err) : resolve(resp))); const items = resp.getLibrariesList() .filter(item => !!item.getLatest()) - .slice(0, 50) + // .slice(0, 50) .map(item => { let installedVersion: string | undefined; const installed = installedLibsIdx.get(item.getName()); From 66f429c47864bccea98c3134bec7e4b5a4ecf44c Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 31 Jul 2019 11:49:26 +0200 Subject: [PATCH 03/10] workaround for non-unique names. Fine tuned the port unnselection when attached boards change. This should make sure we do not have to `await` for the attached boards from the backend. Signed-off-by: Akos Kitta --- .../src/browser/boards/boards-config.tsx | 70 +++++++++++++------ .../src/browser/style/board-select-dialog.css | 15 +++- .../src/common/protocol/boards-service.ts | 19 +++-- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 61640d30..9509f46f 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { DisposableCollection } from '@theia/core'; -import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service'; +import { BoardsService, Board, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; import { BoardsServiceClientImpl } from './boards-service-client-impl'; export namespace BoardsConfig { @@ -18,7 +18,7 @@ export namespace BoardsConfig { } export interface State extends Config { - searchResults: Board[]; + searchResults: Array; knownPorts: string[]; } @@ -26,13 +26,15 @@ export namespace BoardsConfig { export abstract class Item extends React.Component<{ item: T, - name: string, + label: string, selected: boolean, onClick: (item: T) => void, - missing?: boolean }> { + missing?: boolean, + detail?: string +}> { render(): React.ReactNode { - const { selected, name, missing } = this.props; + const { selected, label, missing, detail } = this.props; const classNames = ['item']; if (selected) { classNames.push('selected'); @@ -40,9 +42,12 @@ export abstract class Item extends React.Component<{ if (missing === true) { classNames.push('missing') } - return
- {name} - {selected ? : ''} + return
+
+ {label} +
+ {!detail ? '' :
{detail}
} + {!selected ? '' :
}
; } @@ -72,7 +77,7 @@ export class BoardsConfig extends React.Component this.updatePorts(boards)); const { boardsServiceClient: client } = this.props; this.toDispose.pushAll([ - client.onBoardsChanged(event => this.updatePorts(event.newState.boards)), + client.onBoardsChanged(event => this.updatePorts(event.newState.boards, AttachedBoardsChangeEvent.diff(event).detached)), client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged()); }) @@ -96,23 +101,24 @@ export class BoardsConfig extends React.Component this.setState({ searchResults })); } - protected updatePorts = (boards: Board[] = []) => { + protected updatePorts = (boards: Board[] = [], detachedBoards: Board[] = []) => { this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => { let { selectedPort } = this.state; - if (!!selectedPort && knownPorts.indexOf(selectedPort) === -1) { + const removedPorts = detachedBoards.filter(AttachedSerialBoard.is).map(({ port }) => port); + if (!!selectedPort && removedPorts.indexOf(selectedPort) === -1) { selectedPort = undefined; } this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged()); }); } - protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Board[] }> => { + protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array }> => { const { boardsService } = this.props; const query = (options.query || '').toLocaleLowerCase(); - return new Promise<{ searchResults: Board[] }>(resolve => { + return new Promise<{ searchResults: Array }>(resolve => { boardsService.search(options) .then(({ items }) => items - .map(item => item.boards) + .map(item => item.boards.map(board => ({ ...board, packageName: item.name }))) .reduce((acc, curr) => acc.concat(curr), []) .filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1) .sort(Board.compare)) @@ -139,7 +145,7 @@ export class BoardsConfig extends React.Component this.fireConfigChanged()); } - protected selectBoard = (selectedBoard: Board | undefined) => { + protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => { this.setState({ selectedBoard }, () => this.fireConfigChanged()); } @@ -166,18 +172,40 @@ export class BoardsConfig extends React.Component(); + for (const { name } of searchResults) { + const counter = distinctBoardNames.get(name) || 0; + distinctBoardNames.set(name, counter + 1); + } + + // Due to the non-unique board names, we have to check the package name as well. + const selected = (board: Board & { packageName: string }) => { + if (!!selectedBoard) { + if (Board.equals(board, selectedBoard)) { + if ('packageName' in selectedBoard) { + return board.packageName === (selectedBoard as any).packageName; + } + return true; + } + } + return false; + } + return
- {this.state.searchResults.map((board, index) => - key={`${board.name}-${index}`} + {this.state.searchResults.map(board => + key={`${board.name}-${board.packageName}`} item={board} - name={board.name} - selected={!!selectedBoard && Board.equals(board, selectedBoard)} + label={board.name} + detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined} + selected={selected(board)} onClick={this.selectBoard} missing={!Board.installed(board)} />)} @@ -197,7 +225,7 @@ export class BoardsConfig extends React.Component key={port} item={port} - name={port} + label={port} selected={this.state.selectedPort === port} onClick={this.selectPort} />)} diff --git a/arduino-ide-extension/src/browser/style/board-select-dialog.css b/arduino-ide-extension/src/browser/style/board-select-dialog.css index 58dea0c5..15a6c05d 100644 --- a/arduino-ide-extension/src/browser/style/board-select-dialog.css +++ b/arduino-ide-extension/src/browser/style/board-select-dialog.css @@ -83,7 +83,20 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ #select-board-dialog .selectBoardContainer .body .list .item { padding: 10px 5px 10px 10px; display: flex; - justify-content: space-between; + justify-content: end; +} + +#select-board-dialog .selectBoardContainer .body .list .item .selected-icon { + margin-left: auto; +} + +#select-board-dialog .selectBoardContainer .body .list .item .detail { + font-size: var(--theia-ui-font-size1); + color: var(--theia-disabled-color0); + width: 155px; /* used heuristics for the calculation */ + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; } #select-board-dialog .selectBoardContainer .body .list .item.missing { diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index edc2158a..e59a00b4 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -7,6 +7,21 @@ export interface AttachedBoardsChangeEvent { readonly oldState: Readonly<{ boards: Board[] }>; readonly newState: Readonly<{ boards: Board[] }>; } +export namespace AttachedBoardsChangeEvent { + + export function diff(event: AttachedBoardsChangeEvent): Readonly<{ attached: Board[], detached: Board[] }> { + const diff = (left: T[], right: T[]) => { + return left.filter(item => right.indexOf(item) === -1); + } + const { boards: newBoards } = event.newState; + const { boards: oldBoards } = event.oldState; + return { + detached: diff(oldBoards, newBoards), + attached: diff(newBoards, oldBoards) + }; + } + +} export interface BoardInstalledEvent { readonly pkg: Readonly; @@ -34,10 +49,6 @@ export interface Board { fqbn?: string } -export interface Port { - port?: string; -} - export namespace Board { export function is(board: any): board is Board { From 502e9042ad07c22c529445487a0c618a4478ab50 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 31 Jul 2019 14:43:19 +0200 Subject: [PATCH 04/10] fine tuned `selectedPort` update based on the detached boards. Signed-off-by: Akos Kitta --- .../src/browser/boards/boards-service-client-impl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index f2b8c2f2..0d8822f0 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -30,11 +30,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { this.logger.info('Attached boards changed: ', JSON.stringify(event)); - const { boards } = event.newState; + const detachedBoards = AttachedBoardsChangeEvent.diff(event).detached.filter(AttachedSerialBoard.is).map(({ port }) => port); const { selectedPort, selectedBoard } = this.boardsConfig; this.onAttachedBoardsChangedEmitter.fire(event); - // Dynamically unset the port if there is not corresponding attached boards for it. - if (!!selectedPort && boards.filter(AttachedSerialBoard.is).map(({ port }) => port).indexOf(selectedPort) === -1) { + // Dynamically unset the port if the selected board was an attached one and we detached it. + if (!!selectedPort && detachedBoards.indexOf(selectedPort) === -1) { this.boardsConfig = { selectedBoard, selectedPort: undefined From ec50ea673ca0db922d991da6984412c57a2b91df Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 31 Jul 2019 15:24:19 +0200 Subject: [PATCH 05/10] do not `await` for attached boards. Signed-off-by: Akos Kitta --- arduino-ide-extension/src/browser/boards/boards-config.tsx | 2 +- .../src/browser/boards/boards-service-client-impl.ts | 2 +- arduino-ide-extension/src/node/boards-service-impl.ts | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 9509f46f..3e294eb0 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -105,7 +105,7 @@ export class BoardsConfig extends React.Component { let { selectedPort } = this.state; const removedPorts = detachedBoards.filter(AttachedSerialBoard.is).map(({ port }) => port); - if (!!selectedPort && removedPorts.indexOf(selectedPort) === -1) { + if (!!selectedPort && removedPorts.indexOf(selectedPort) !== -1) { selectedPort = undefined; } this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged()); diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index 0d8822f0..3ceb5d8f 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -34,7 +34,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { const { selectedPort, selectedBoard } = this.boardsConfig; this.onAttachedBoardsChangedEmitter.fire(event); // Dynamically unset the port if the selected board was an attached one and we detached it. - if (!!selectedPort && detachedBoards.indexOf(selectedPort) === -1) { + if (!!selectedPort && detachedBoards.indexOf(selectedPort) !== -1) { this.boardsConfig = { selectedBoard, selectedPort: undefined diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 843d0a87..8971a28c 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -6,7 +6,6 @@ import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInst import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; -import { Deferred } from '@theia/core/lib/common/promise-util'; @injectable() export class BoardsServiceImpl implements BoardsService { @@ -23,7 +22,6 @@ export class BoardsServiceImpl implements BoardsService { protected selectedBoard: Board | undefined; protected discoveryInitialized = false; - protected discoveryReady = new Deferred(); protected discoveryTimer: NodeJS.Timeout | undefined; /** * Poor man's serial discovery: @@ -41,7 +39,6 @@ export class BoardsServiceImpl implements BoardsService { this.doGetAttachedBoards().then(({ boards }) => { const update = (oldState: Board[], newState: Board[], message: string) => { this._attachedBoards = { boards: newState }; - this.discoveryReady.resolve(); this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newState)}`); if (this.client) { this.client.notifyAttachedBoardsChanged({ @@ -91,7 +88,6 @@ export class BoardsServiceImpl implements BoardsService { } async getAttachedBoards(): Promise<{ boards: Board[] }> { - await this.discoveryReady.promise; return this._attachedBoards; } From 8d79bb3ffb923781d02baeb855367d85b26175ad Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 31 Jul 2019 15:38:19 +0200 Subject: [PATCH 06/10] restored the the search limit for the lib manager. Signed-off-by: Akos Kitta --- arduino-ide-extension/src/node/library-service-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index 40c0dbdd..93a9cc42 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -41,7 +41,7 @@ export class LibraryServiceImpl implements LibraryService { const resp = await new Promise((resolve, reject) => client.librarySearch(req, (err, resp) => !!err ? reject(err) : resolve(resp))); const items = resp.getLibrariesList() .filter(item => !!item.getLatest()) - // .slice(0, 50) + .slice(0, 50) .map(item => { let installedVersion: string | undefined; const installed = installedLibsIdx.get(item.getName()); From 692c3f6e3f0f47436468f886b64663aa92dcb4a9 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 1 Aug 2019 08:22:29 +0200 Subject: [PATCH 07/10] Implemented serial-monitoring for the backend. Signed-off-by: Akos Kitta --- .../src/browser/arduino-commands.ts | 10 ++ .../browser/arduino-frontend-contribution.tsx | 86 ++++++++- .../src/browser/arduino-frontend-module.ts | 16 ++ .../monitor/monitor-service-client-impl.ts | 23 +++ .../src/common/protocol/monitor-service.ts | 43 +++++ .../src/node/arduino-backend-module.ts | 24 +++ .../node/monitor/monitor-client-provider.ts | 20 +++ .../src/node/monitor/monitor-service-impl.ts | 164 ++++++++++++++++++ 8 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts create mode 100644 arduino-ide-extension/src/common/protocol/monitor-service.ts create mode 100644 arduino-ide-extension/src/node/monitor/monitor-client-provider.ts create mode 100644 arduino-ide-extension/src/node/monitor/monitor-service-impl.ts diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts index e56f4e2d..0629e024 100644 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ b/arduino-ide-extension/src/browser/arduino-commands.ts @@ -43,4 +43,14 @@ export namespace ArduinoCommands { id: "arduino-toggle-pro-mode" } + export const CONNECT_TODO: Command = { + id: 'connect-to-attached-board', + label: 'Connect to Attached Board' + } + + export const SEND: Command = { + id: 'send', + label: 'Send a Message to the Connected Board' + } + } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 314956fb..e62c192e 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { MessageService } from '@theia/core/lib/common/message-service'; import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { BoardsService } from '../common/protocol/boards-service'; +import { BoardsService, AttachedSerialBoard } from '../common/protocol/boards-service'; import { ArduinoCommands } from './arduino-commands'; import { CoreService } from '../common/protocol/core-service'; import { WorkspaceServiceExt } from './workspace-service-ext'; @@ -19,7 +19,18 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service import { SketchFactory } from './sketch-factory'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; -import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer, StatusBarAlignment, LabelProvider } from '@theia/core/lib/browser'; +import { + ContextMenuRenderer, + OpenerService, + Widget, + StatusBar, + ShellLayoutRestorer, + StatusBarAlignment, + QuickOpenItem, + QuickOpenMode, + QuickOpenService, + LabelProvider +} from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { ArduinoToolbarContextMenu } from './arduino-file-menu'; @@ -34,6 +45,7 @@ import { MaybePromise } from '@theia/core/lib/common/types'; import { BoardsConfigDialog } from './boards/boards-config-dialog'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { BoardsConfig } from './boards/boards-config'; +import { MonitorService } from '../common/protocol/monitor-service'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -56,6 +68,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(CoreService) protected readonly coreService: CoreService; + @inject(MonitorService) + protected readonly monitorService: MonitorService; + + // TODO: make this better! + protected connectionId: string | undefined; + @inject(WorkspaceServiceExt) protected readonly workspaceServiceExt: WorkspaceServiceExt; @@ -115,6 +133,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; protected boardsToolbarItem: BoardsToolBarItem | null; protected wsSketchCount: number = 0; @@ -293,7 +314,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C this.boardsServiceClient.boardsConfig = boardsConfig; } } - }) + }); registry.registerCommand(ArduinoCommands.TOGGLE_PRO_MODE, { execute: () => { const oldModeState = ARDUINO_PRO_MODE; @@ -301,6 +322,65 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.executeCommand('reset.layout'); }, isToggled: () => ARDUINO_PRO_MODE + }); + registry.registerCommand(ArduinoCommands.CONNECT_TODO, { + execute: async () => { + const { boardsConfig } = this.boardsServiceClient; + const { selectedBoard, selectedPort } = boardsConfig; + if (!selectedBoard) { + this.messageService.warn('No boards selected.'); + return; + } + const { name } = selectedBoard; + if (!selectedPort) { + this.messageService.warn(`No ports selected for board: '${name}'.`); + return; + } + const attachedBoards = await this.boardsService.getAttachedBoards(); + const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board)); + if (!connectedBoard) { + this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`); + return; + } + if (this.connectionId) { + console.log('>>> Disposing existing monitor connection before establishing a new one...'); + const result = await this.monitorService.disconnect(this.connectionId); + if (!result) { + // TODO: better!!! + console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`); + } else { + console.log(`<<< Disposed ${this.connectionId} connection.`) + } + } + const { connectionId } = await this.monitorService.connect({ board: selectedBoard, port: selectedPort }); + this.connectionId = connectionId; + } + }); + registry.registerCommand(ArduinoCommands.SEND, { + isEnabled: () => !!this.connectionId, + execute: async () => { + const { monitorService, connectionId } = this; + const model = { + onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { + acceptor([ + new QuickOpenItem({ + label: "Type your message and press 'Enter' to send it to the board. Escape to cancel.", + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + monitorService.send(connectionId!, lookFor + '\n'); + return true; + } + }) + ]); + } + }; + const options = { + placeholder: "Your message. The message will be suffixed with a LF ['\\n'].", + }; + this.quickOpenService.open(model, options); + } }) } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 2ea69840..2cd6b476 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -54,6 +54,8 @@ import { SilentSearchInWorkspaceContribution } from './customization/silent-sear import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution'; import { LibraryItemRenderer } from './library/library-item-renderer'; import { BoardItemRenderer } from './boards/boards-item-renderer'; +import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl'; +import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service'; const ElementQueries = require('css-element-queries/src/ElementQueries'); if (!ARDUINO_PRO_MODE) { @@ -149,6 +151,20 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un return workspaceServiceExt; }); + // Frontend binding for the monitor service. + bind(MonitorService).toDynamicValue(context => { + const connection = context.container.get(WebSocketConnectionProvider); + const client = context.container.get(MonitorServiceClientImpl); + return connection.createProxy(MonitorServicePath, client); + }).inSingletonScope(); + // Monitor service client to receive and delegate notifications from the backend. + bind(MonitorServiceClientImpl).toSelf().inSingletonScope(); + bind(MonitorServiceClient).toDynamicValue(context => { + const client = context.container.get(MonitorServiceClientImpl); + WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client); + return client; + }).inSingletonScope(); + bind(AWorkspaceService).toSelf().inSingletonScope(); rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope(); bind(SketchFactory).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts new file mode 100644 index 00000000..28ea0b2c --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service'; + +@injectable() +export class MonitorServiceClientImpl implements MonitorServiceClient { + + protected readonly onReadEmitter = new Emitter(); + protected readonly onErrorEmitter = new Emitter(); + readonly onRead = this.onReadEmitter.event; + readonly onError = this.onErrorEmitter.event; + + notifyRead(event: MonitorReadEvent): void { + this.onReadEmitter.fire(event); + const { connectionId, data } = event; + console.log(`Received data from ${connectionId}: ${data}`); + } + + notifyError(error: MonitorError): void { + this.onErrorEmitter.fire(error); + } + +} diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts new file mode 100644 index 00000000..62b2893a --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -0,0 +1,43 @@ +import { JsonRpcServer } from '@theia/core'; +import { Board } from './boards-service'; + +export interface MonitorError { + readonly message: string; + readonly code: number +} + +export interface MonitorReadEvent { + readonly connectionId: string; + readonly data: string; +} + +export const MonitorServiceClient = Symbol('MonitorServiceClient'); +export interface MonitorServiceClient { + notifyRead(event: MonitorReadEvent): void; + notifyError(event: MonitorError): void; +} + +export const MonitorServicePath = '/services/serial-monitor'; +export const MonitorService = Symbol('MonitorService'); +export interface MonitorService extends JsonRpcServer { + connect(config: ConnectionConfig): Promise<{ connectionId: string }>; + disconnect(connectionId: string): Promise; + send(connectionId: string, data: string | Uint8Array): Promise; +} + +export interface ConnectionConfig { + readonly board: Board; + readonly port: string; + /** + * Defaults to [`SERIAL`](ConnectionType#SERIAL). + */ + readonly type?: ConnectionType; + /** + * Defaults to `9600`. + */ + readonly baudRate?: number; +} + +export enum ConnectionType { + SERIAL = 0 +} diff --git a/arduino-ide-extension/src/node/arduino-backend-module.ts b/arduino-ide-extension/src/node/arduino-backend-module.ts index 296835d8..3d50f866 100644 --- a/arduino-ide-extension/src/node/arduino-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-backend-module.ts @@ -19,6 +19,9 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext'; import { WorkspaceServer } from '@theia/workspace/lib/common'; import { SketchesServiceImpl } from './sketches-service-impl'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; +import { MonitorServiceImpl } from './monitor/monitor-service-impl'; +import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service'; +import { MonitorClientProvider } from './monitor/monitor-client-provider'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ArduinoDaemon).toSelf().inSingletonScope(); @@ -104,4 +107,25 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // If nothing was set previously. bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope(); rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt); + + // Shared monitor client provider service for the backend. + bind(MonitorClientProvider).toSelf().inSingletonScope(); + + // Connection scoped service for the serial monitor. + const monitorServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bind(MonitorServiceImpl).toSelf().inSingletonScope(); + bind(MonitorService).toService(MonitorServiceImpl); + bindBackendService(MonitorServicePath, MonitorService, (service, client) => { + service.setClient(client); + client.onDidCloseConnection(() => service.dispose()); + return service; + }); + }); + bind(ConnectionContainerModule).toConstantValue(monitorServiceConnectionModule); + + // Logger for the monitor service. + bind(ILogger).toDynamicValue(ctx => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child('monitor-service'); + }).inSingletonScope().whenTargetNamed('monitor-service'); }); diff --git a/arduino-ide-extension/src/node/monitor/monitor-client-provider.ts b/arduino-ide-extension/src/node/monitor/monitor-client-provider.ts new file mode 100644 index 00000000..609f0df9 --- /dev/null +++ b/arduino-ide-extension/src/node/monitor/monitor-client-provider.ts @@ -0,0 +1,20 @@ +import * as grpc from '@grpc/grpc-js'; +import { injectable, postConstruct } from 'inversify'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { MonitorClient } from '../cli-protocol/monitor/monitor_grpc_pb'; + +@injectable() +export class MonitorClientProvider { + + readonly deferred = new Deferred(); + + @postConstruct() + protected init(): void { + this.deferred.resolve(new MonitorClient('localhost:50051', grpc.credentials.createInsecure())); + } + + get client(): Promise { + return this.deferred.promise; + } + +} diff --git a/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts new file mode 100644 index 00000000..90eeeb0b --- /dev/null +++ b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts @@ -0,0 +1,164 @@ +import { v4 } from 'uuid'; +import * as grpc from '@grpc/grpc-js'; +import { TextDecoder, TextEncoder } from 'util'; +import { injectable, inject, named } from 'inversify'; +import { ILogger, Disposable, DisposableCollection } from '@theia/core'; +import { MonitorService, MonitorServiceClient, ConnectionConfig, ConnectionType } from '../../common/protocol/monitor-service'; +import { StreamingOpenReq, StreamingOpenResp, MonitorConfig } from '../cli-protocol/monitor/monitor_pb'; +import { MonitorClientProvider } from './monitor-client-provider'; + +export interface MonitorDuplex { + readonly toDispose: Disposable; + readonly duplex: grpc.ClientDuplexStream; +} + +type ErrorCode = { code: number }; +type MonitorError = Error & ErrorCode; +namespace MonitorError { + + export function is(error: Error & Partial): error is MonitorError { + return typeof error.code === 'number'; + } + + /** + * The frontend has refreshed the browser, for instance. + */ + export function isClientCancelledError(error: MonitorError): boolean { + return error.code === 1 && error.message === 'Cancelled on client'; + } + + /** + * When detaching a physical device when the duplex channel is still opened. + */ + export function isDeviceNotConfiguredError(error: MonitorError): boolean { + return error.code === 2 && error.message === 'device not configured'; + } + +} + +@injectable() +export class MonitorServiceImpl implements MonitorService { + + @inject(ILogger) + @named('monitor-service') + protected readonly logger: ILogger; + + @inject(MonitorClientProvider) + protected readonly monitorClientProvider: MonitorClientProvider; + + protected client?: MonitorServiceClient; + protected readonly connections = new Map(); + + setClient(client: MonitorServiceClient | undefined): void { + this.client = client; + } + + dispose(): void { + for (const [connectionId, duplex] of this.connections.entries()) { + this.doDisconnect(connectionId, duplex); + } + } + + async connect(config: ConnectionConfig): Promise<{ connectionId: string }> { + const client = await this.monitorClientProvider.client; + const duplex = client.streamingOpen(); + const connectionId = v4(); + const toDispose = new DisposableCollection( + Disposable.create(() => this.disconnect(connectionId)) + ); + + duplex.on('error', ((error: Error) => { + if (MonitorError.is(error) && ( + MonitorError.isClientCancelledError(error) + || MonitorError.isDeviceNotConfiguredError(error) + )) { + if (this.client) { + this.client.notifyError(error); + } + } + this.logger.error(`Error occurred for connection ${connectionId}.`, error); + toDispose.dispose(); + }).bind(this)); + + duplex.on('data', ((resp: StreamingOpenResp) => { + if (this.client) { + const raw = resp.getData(); + const data = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw); + this.client.notifyRead({ connectionId, data }); + } + }).bind(this)); + + const { type, port } = config; + const req = new StreamingOpenReq(); + const monitorConfig = new MonitorConfig(); + monitorConfig.setType(this.mapType(type)); + monitorConfig.setTarget(port); + if (config.baudRate !== undefined) { + monitorConfig.setAdditionalconfig({ 'BaudRate': config.baudRate }); + } + req.setMonitorconfig(monitorConfig); + + return new Promise<{ connectionId: string }>(resolve => { + duplex.write(req, () => { + this.connections.set(connectionId, { toDispose, duplex }); + resolve({ connectionId }); + }); + }); + } + + async disconnect(connectionId: string): Promise { + this.logger.info(`>>> Received disconnect request for connection: ${connectionId}`); + const disposable = this.connections.get(connectionId); + if (!disposable) { + this.logger.warn(`<<< No connection was found for ID: ${connectionId}`); + return false; + } + const result = await this.doDisconnect(connectionId, disposable); + if (result) { + this.logger.info(`<<< Successfully disconnected from ${connectionId}.`); + } else { + this.logger.info(`<<< Could not disconnected from ${connectionId}.`); + } + return result; + } + + protected async doDisconnect(connectionId: string, duplex: MonitorDuplex): Promise { + const { toDispose } = duplex; + this.logger.info(`>>> Disposing monitor connection: ${connectionId}...`); + try { + toDispose.dispose(); + this.logger.info(`<<< Connection disposed: ${connectionId}.`); + return true; + } catch (e) { + this.logger.error(`<<< Error occurred when disposing monitor connection: ${connectionId}. ${e}`); + return false; + } + } + + async send(connectionId: string, data: string): Promise { + const duplex = this.duplex(connectionId); + if (duplex) { + const req = new StreamingOpenReq(); + req.setData(new TextEncoder().encode(data)); + return new Promise(resolve => duplex.duplex.write(req, resolve)); + } else { + throw new Error(`No connection with ID: ${connectionId}.`); + } + } + + protected mapType(type?: ConnectionType): MonitorConfig.TargetType { + switch (type) { + case ConnectionType.SERIAL: return MonitorConfig.TargetType.SERIAL; + default: return MonitorConfig.TargetType.SERIAL; + } + } + + protected duplex(connectionId: string): MonitorDuplex | undefined { + const monitorClient = this.connections.get(connectionId); + if (!monitorClient) { + this.logger.warn(`Could not find monitor client for connection ID: ${connectionId}`); + } + return monitorClient; + } + +} From b6306c330f828e42ce4511ded258c49a8e311689 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 2 Aug 2019 13:35:25 +0200 Subject: [PATCH 08/10] Update/download the `library_index.json` at start Signed-off-by: Akos Kitta --- .../src/node/core-client-provider-impl.ts | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/arduino-ide-extension/src/node/core-client-provider-impl.ts b/arduino-ide-extension/src/node/core-client-provider-impl.ts index 19bd9c47..f259259b 100644 --- a/arduino-ide-extension/src/node/core-client-provider-impl.ts +++ b/arduino-ide-extension/src/node/core-client-provider-impl.ts @@ -6,7 +6,9 @@ import { InitReq, Configuration, UpdateIndexReq, - UpdateIndexResp + UpdateIndexResp, + UpdateLibrariesIndexReq, + UpdateLibrariesIndexResp } from './cli-protocol/commands/commands_pb'; import { WorkspaceServiceExt } from '../browser/workspace-service-ext'; import { FileSystem } from '@theia/filesystem/lib/common'; @@ -111,20 +113,34 @@ export class CoreClientProviderImpl implements CoreClientProvider { } // in a separate promise, try and update the index - let succeeded = true; + let indexUpdateSucceeded = true; for (let i = 0; i < 10; i++) { try { await this.updateIndex(client, instance); - succeeded = true; + indexUpdateSucceeded = true; break; } catch (e) { this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`); } } - if (!succeeded) { + if (!indexUpdateSucceeded) { this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`); } + let libIndexUpdateSucceeded = true; + for (let i = 0; i < 10; i++) { + try { + await this.updateLibraryIndex(client, instance); + libIndexUpdateSucceeded = true; + break; + } catch (e) { + this.toolOutputService.publishNewOutput("daemon", `Error while updating library index in attempt ${i}: ${e}`); + } + } + if (!libIndexUpdateSucceeded) { + this.toolOutputService.publishNewOutput("daemon", `Was unable to update the library index. Please restart to try again.`); + } + const result = { client, instance @@ -134,6 +150,38 @@ export class CoreClientProviderImpl implements CoreClientProvider { return result; } + protected async updateLibraryIndex(client: ArduinoCoreClient, instance: Instance): Promise { + const req = new UpdateLibrariesIndexReq(); + req.setInstance(instance); + const resp = client.updateLibrariesIndex(req); + let file: string | undefined; + resp.on('data', (data: UpdateLibrariesIndexResp) => { + const progress = data.getDownloadProgress(); + if (progress) { + if (!file && progress.getFile()) { + file = `${progress.getFile()}`; + } + if (progress.getCompleted()) { + if (file) { + if (/\s/.test(file)) { + this.toolOutputService.publishNewOutput("daemon", `${file} completed.\n`); + } else { + this.toolOutputService.publishNewOutput("daemon", `Download of '${file}' completed.\n'`); + } + } else { + this.toolOutputService.publishNewOutput("daemon", `The library index has been successfully updated.\n'`); + } + file = undefined; + } + } + }); + await new Promise((resolve, reject) => { + resp.on('error', reject); + resp.on('end', resolve); + }); + } + + protected async updateIndex(client: ArduinoCoreClient, instance: Instance): Promise { const updateReq = new UpdateIndexReq(); updateReq.setInstance(instance); @@ -165,4 +213,4 @@ export class CoreClientProviderImpl implements CoreClientProvider { }); } -} \ No newline at end of file +} From c5796677f8bf024c18b2cd8bf8a65150e683f7c5 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 12 Aug 2019 11:39:02 +0200 Subject: [PATCH 09/10] Ignore whitespaces when searhing for libs, cores. Signed-off-by: Akos Kitta --- .../components/component-list/filterable-list-container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx index b7764764..db93c9dd 100644 --- a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx @@ -60,7 +60,7 @@ export class FilterableListContainer extends React.Component { + searchable.search({ query: query.trim() }).then(result => { const { items } = result; this.setState({ items: this.sort(items) From 7c2a2956317776d3fcabb6ac69dca8bc270105be Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 12 Aug 2019 11:40:39 +0200 Subject: [PATCH 10/10] Fixed bug when checking if a core is installed `startsWith` was incorrect: `arduino:samd` Vs. `arduino:samd_beta` Signed-off-by: Akos Kitta --- arduino-ide-extension/src/node/boards-service-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 8971a28c..737b09eb 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -159,7 +159,7 @@ export class BoardsServiceImpl implements BoardsService { let items = resp.getSearchOutputList().map(item => { let installedVersion: string | undefined; - const matchingPlatform = installedPlatforms.find(ip => ip.getId().startsWith(`${item.getId()}`)); + const matchingPlatform = installedPlatforms.find(ip => ip.getId() === item.getId()); if (!!matchingPlatform) { installedVersion = matchingPlatform.getInstalled(); }