From 650e202faf6558d129fab649412e8de7d6b8e74b Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 1 Sep 2022 11:57:24 +0200 Subject: [PATCH 1/3] feat: simplify board and port handling Closes #1319 Signed-off-by: Akos Kitta --- arduino-ide-extension/package.json | 8 +- .../browser/arduino-ide-frontend-module.ts | 14 +- .../browser/boards/boards-auto-installer.ts | 412 +++--- .../boards/boards-config-component.tsx | 328 +++++ .../boards/boards-config-dialog-widget.tsx | 71 - .../browser/boards/boards-config-dialog.ts | 142 -- .../browser/boards/boards-config-dialog.tsx | 190 +++ .../src/browser/boards/boards-config.tsx | 432 ------ .../boards/boards-data-menu-updater.ts | 25 +- .../src/browser/boards/boards-data-store.ts | 108 +- .../browser/boards/boards-service-provider.ts | 1167 +++++++---------- .../browser/boards/boards-toolbar-item.tsx | 292 +++-- .../browser/contributions/board-selection.ts | 318 ++--- .../src/browser/contributions/debug.ts | 12 +- .../src/browser/contributions/examples.ts | 20 +- .../browser/contributions/include-library.ts | 6 +- .../src/browser/contributions/ino-language.ts | 19 +- .../contributions/open-boards-config.ts | 17 +- .../browser/contributions/selected-board.ts | 42 +- .../contributions/update-arduino-state.ts | 29 +- .../browser/contributions/upload-sketch.ts | 65 +- .../src/browser/contributions/user-fields.ts | 7 +- .../certificate-uploader-component.tsx | 72 +- .../certificate-uploader-dialog.tsx | 69 +- .../select-board-components.tsx | 80 +- .../firmware-uploader-component.tsx | 75 +- .../firmware-uploader-dialog.tsx | 34 +- .../monitor-manager-proxy-client-impl.ts | 98 +- .../src/browser/notification-center.ts | 18 +- .../browser/serial/monitor/monitor-widget.tsx | 1 - .../browser/style/boards-config-dialog.css | 35 +- .../src/browser/theia/dialogs/dialogs.tsx | 1 - arduino-ide-extension/src/common/nls.ts | 1 + .../src/common/protocol/board-list.ts | 387 ++++++ .../src/common/protocol/boards-service.ts | 642 +++++---- .../src/common/protocol/core-service.ts | 30 +- .../src/common/protocol/monitor-service.ts | 35 +- .../common/protocol/notification-service.ts | 8 +- arduino-ide-extension/src/common/types.ts | 4 + .../node/arduino-firmware-uploader-impl.ts | 2 +- .../src/node/board-discovery.ts | 209 ++- .../src/node/boards-service-impl.ts | 130 +- .../cli/commands/v1/commands_grpc_pb.d.ts | 17 + .../cli/commands/v1/commands_grpc_pb.js | 36 + .../arduino/cli/commands/v1/commands_pb.d.ts | 88 +- .../cc/arduino/cli/commands/v1/commands_pb.js | 648 +++++++-- .../cc/arduino/cli/commands/v1/lib_pb.d.ts | 4 + .../cc/arduino/cli/commands/v1/lib_pb.js | 32 +- .../cc/arduino/cli/commands/v1/upload_pb.d.ts | 51 + .../cc/arduino/cli/commands/v1/upload_pb.js | 304 ++++- .../cli/settings/v1/settings_grpc_pb.d.ts | 17 + .../cli/settings/v1/settings_grpc_pb.js | 34 + .../arduino/cli/settings/v1/settings_pb.d.ts | 38 + .../cc/arduino/cli/settings/v1/settings_pb.js | 275 ++++ .../src/node/core-service-impl.ts | 129 +- .../src/node/monitor-manager.ts | 58 +- .../src/node/notification-service-server.ts | 6 +- .../src/node/sketches-service-impl.ts | 6 +- .../browser/boards-auto-installer.test.ts | 247 ---- .../src/test/browser/fixtures/boards.ts | 50 +- .../src/test/common/board-list.test.ts | 130 ++ .../src/test/common/boards-service.test.ts | 136 +- .../node/core-client-provider.slow-test.ts | 58 +- .../src/test/node/core-service-impl.test.ts | 22 +- .../src/test/node/test-bindings.ts | 11 +- i18n/en.json | 12 +- 66 files changed, 4795 insertions(+), 3269 deletions(-) create mode 100644 arduino-ide-extension/src/browser/boards/boards-config-component.tsx delete mode 100644 arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx delete mode 100644 arduino-ide-extension/src/browser/boards/boards-config-dialog.ts create mode 100644 arduino-ide-extension/src/browser/boards/boards-config-dialog.tsx delete mode 100644 arduino-ide-extension/src/browser/boards/boards-config.tsx create mode 100644 arduino-ide-extension/src/common/protocol/board-list.ts delete mode 100644 arduino-ide-extension/src/test/browser/boards-auto-installer.test.ts create mode 100644 arduino-ide-extension/src/test/common/board-list.test.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index a928ffe09..fb0bf563d 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -171,10 +171,14 @@ ], "arduino": { "cli": { - "version": "0.33.1" + "version": { + "owner": "cmaglie", + "repo": "arduino-cli", + "commitish": "board_port_after_upload" + } }, "fwuploader": { - "version": "2.2.2" + "version": "2.3.0" }, "clangd": { "version": "14.0.0" 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 ea568d595..a10e6c1b3 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -27,7 +27,10 @@ import { SketchesServiceClientImpl } from './sketches-service-client-impl'; import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { BoardsListWidget } from './boards/boards-list-widget'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; -import { BoardsServiceProvider } from './boards/boards-service-provider'; +import { + BoardListDumper, + BoardsServiceProvider, +} from './boards/boards-service-provider'; import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from './theia/workspace/workspace-service'; import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; @@ -61,7 +64,6 @@ import { BoardsConfigDialog, BoardsConfigDialogProps, } from './boards/boards-config-dialog'; -import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget'; import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution'; import { ScmContribution } from './theia/scm/scm-contribution'; import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'; @@ -358,7 +360,7 @@ import { UpdateArduinoState } from './contributions/update-arduino-state'; import { TerminalWidgetImpl } from './theia/terminal/terminal-widget-impl'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalFrontendContribution } from './theia/terminal/terminal-frontend-contribution'; -import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution' +import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -447,6 +449,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BoardsServiceProvider).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(BoardsServiceProvider); bind(CommandContribution).toService(BoardsServiceProvider); + bind(BoardListDumper).toSelf().inSingletonScope(); // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board. bind(FrontendApplicationContribution) @@ -480,7 +483,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(OpenHandler).toService(BoardsListWidgetFrontendContribution); // Board select dialog - bind(BoardsConfigDialogWidget).toSelf().inSingletonScope(); bind(BoardsConfigDialog).toSelf().inSingletonScope(); bind(BoardsConfigDialogProps).toConstantValue({ title: nls.localize( @@ -1034,5 +1036,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Patch terminal issues. rebind(TerminalWidget).to(TerminalWidgetImpl).inTransientScope(); bind(TerminalFrontendContribution).toSelf().inSingletonScope(); - rebind(TheiaTerminalFrontendContribution).toService(TerminalFrontendContribution); + rebind(TheiaTerminalFrontendContribution).toService( + TerminalFrontendContribution + ); }); 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 6d95ea661..4b0f45e69 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -1,281 +1,229 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; -import { MessageService } from '@theia/core/lib/common/message-service'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { MessageType } from '@theia/core/lib/common/message-service-protocol'; +import { nls } from '@theia/core/lib/common/nls'; +import { notEmpty } from '@theia/core/lib/common/objects'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; +import { InstallManually } from '../../common/nls'; +import { Installable, ResponseServiceClient } from '../../common/protocol'; import { - BoardsService, + BoardIdentifier, BoardsPackage, - Board, - Port, + BoardsService, + createPlatformIdentifier, + isBoardIdentifierChangeEvent, + PlatformIdentifier, + platformIdentifierEquals, + serializePlatformIdentifier, } from '../../common/protocol/boards-service'; +import { NotificationCenter } from '../notification-center'; import { BoardsServiceProvider } from './boards-service-provider'; -import { Installable, ResponseServiceClient } from '../../common/protocol'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; -import { nls } from '@theia/core/lib/common'; -import { NotificationCenter } from '../notification-center'; -import { InstallManually } from '../../common/nls'; - -interface AutoInstallPromptAction { - // isAcceptance, whether or not the action indicates acceptance of auto-install proposal - isAcceptance?: boolean; - key: string; - handler: (...args: unknown[]) => unknown; -} - -type AutoInstallPromptActions = AutoInstallPromptAction[]; /** - * Listens on `BoardsConfig.Config` changes, if a board is selected which does not + * Listens on `BoardsConfigChangeEvent`s, if a board is selected which does not * have the corresponding core installed, it proposes the user to install the core. */ - -// * Cases in which we do not show the auto-install prompt: -// 1. When a related platform is already installed -// 2. When a prompt is already showing in the UI -// 3. When a board is unplugged @injectable() export class BoardsAutoInstaller implements FrontendApplicationContribution { @inject(NotificationCenter) private readonly notificationCenter: NotificationCenter; - @inject(MessageService) - protected readonly messageService: MessageService; - + private readonly messageService: MessageService; + @inject(NotificationManager) + private readonly notificationManager: NotificationManager; @inject(BoardsService) - protected readonly boardsService: BoardsService; - + private readonly boardsService: BoardsService; @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - + private readonly boardsServiceProvider: BoardsServiceProvider; @inject(ResponseServiceClient) - protected readonly responseService: ResponseServiceClient; - + private readonly responseService: ResponseServiceClient; @inject(BoardsListWidgetFrontendContribution) - protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; + private readonly boardsManagerWidgetContribution: BoardsListWidgetFrontendContribution; // Workaround for https://github.com/eclipse-theia/theia/issues/9349 - protected notifications: Board[] = []; - - // * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually") - // we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused" - // an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt - // showing again - private portSelectedOnLastRefusal: Port | undefined; - private lastRefusedPackageId: string | undefined; + private readonly installNotificationInfos: Readonly<{ + boardName: string; + platformId: string; + notificationId: string; + }>[] = []; + private readonly toDispose = new DisposableCollection(); onStart(): void { - const setEventListeners = () => { - this.boardsServiceClient.onBoardsConfigChanged((config) => { - const { selectedBoard, selectedPort } = config; - - const boardWasUnplugged = - !selectedPort && this.portSelectedOnLastRefusal; - - this.clearLastRefusedPromptInfo(); - - if ( - boardWasUnplugged || - !selectedBoard || - this.promptAlreadyShowingForBoard(selectedBoard) - ) { - return; - } - - this.ensureCoreExists(selectedBoard, selectedPort); - }); - - // we "clearRefusedPackageInfo" if a "refused" package is eventually - // installed, though this is not strictly necessary. It's more of a - // cleanup, to ensure the related variables are representative of - // current state. - this.notificationCenter.onPlatformDidInstall((installed) => { - if (this.lastRefusedPackageId === installed.item.id) { - this.clearLastRefusedPromptInfo(); + this.toDispose.pushAll([ + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.ensureCoreExists(event.selectedBoard); } - }); - }; - - // we should invoke this.ensureCoreExists only once we're sure - // everything has been reconciled - this.boardsServiceClient.reconciled.then(() => { - const { selectedBoard, selectedPort } = - this.boardsServiceClient.boardsConfig; - - if (selectedBoard) { - this.ensureCoreExists(selectedBoard, selectedPort); - } - - setEventListeners(); + }), + this.notificationCenter.onPlatformDidInstall((event) => + this.clearAllNotificationForPlatform(event.item.id) + ), + ]); + this.boardsServiceProvider.ready.then(() => { + const { selectedBoard } = this.boardsServiceProvider.boardsConfig; + this.ensureCoreExists(selectedBoard); }); } - private removeNotificationByBoard(selectedBoard: Board): void { - const index = this.notifications.findIndex((notification) => - Board.sameAs(notification, selectedBoard) - ); - if (index !== -1) { - this.notifications.splice(index, 1); + private async findPlatformToInstall( + selectedBoard: BoardIdentifier + ): Promise { + const platformId = await this.findPlatformIdToInstall(selectedBoard); + if (!platformId) { + return undefined; } + const id = serializePlatformIdentifier(platformId); + const platform = await this.boardsService.getBoardPackage({ id }); + if (!platform) { + console.warn(`Could not resolve platform for ID: ${id}`); + return undefined; + } + if (platform.installedVersion) { + return undefined; + } + return platform; } - private clearLastRefusedPromptInfo(): void { - this.lastRefusedPackageId = undefined; - this.portSelectedOnLastRefusal = undefined; - } - - private setLastRefusedPromptInfo( - packageId: string, - selectedPort?: Port - ): void { - this.lastRefusedPackageId = packageId; - this.portSelectedOnLastRefusal = selectedPort; - } - - private promptAlreadyShowingForBoard(board: Board): boolean { - return Boolean( - this.notifications.find((notification) => - Board.sameAs(notification, board) - ) - ); - } - - protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void { - this.notifications.push(selectedBoard); - this.boardsService.search({}).then((packages) => { - const candidate = this.getInstallCandidate(packages, selectedBoard); - - if (candidate) { - this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort); - } else { - this.removeNotificationByBoard(selectedBoard); + private async findPlatformIdToInstall( + selectedBoard: BoardIdentifier + ): Promise { + const selectedBoardPlatformId = createPlatformIdentifier(selectedBoard); + // The board is installed or the FQBN is available from the `board list watch` for Arduino boards. The latter might change! + if (selectedBoardPlatformId) { + const installedPlatforms = + await this.boardsService.getInstalledPlatforms(); + const installedPlatformIds = installedPlatforms + .map((platform) => createPlatformIdentifier(platform.id)) + .filter(notEmpty); + if ( + installedPlatformIds.every( + (installedPlatformId) => + !platformIdentifierEquals( + installedPlatformId, + selectedBoardPlatformId + ) + ) + ) { + return selectedBoardPlatformId; } - }); + } else { + // IDE2 knows that selected board is not installed. Look for board `name` match in not yet installed platforms. + // The order should be correct when there is a board name collision (e.g. Arduino Nano RP2040 from Arduino Mbed OS Nano Boards, [DEPRECATED] Arduino Mbed OS Nano Boards). The CLI boosts the platforms, so picking the first name match should be fine. + const platforms = await this.boardsService.search({}); + for (const platform of platforms) { + // Ignore installed platforms + if (platform.installedVersion) { + continue; + } + if ( + platform.boards.some((board) => board.name === selectedBoard.name) + ) { + const platformId = createPlatformIdentifier(platform.id); + if (platformId) { + return platformId; + } + } + } + } + return undefined; } - private getInstallCandidate( - packages: BoardsPackage[], - selectedBoard: Board - ): BoardsPackage | undefined { - // filter packagesForBoard selecting matches from the cli (installed packages) - // and matches based on the board name - // NOTE: this ensures the Deprecated & new packages are all in the array - // so that we can check if any of the valid packages is already installed - const packagesForBoard = packages.filter( - (pkg) => - BoardsPackage.contains(selectedBoard, pkg) || - pkg.boards.some((board) => board.name === selectedBoard.name) - ); - - // check if one of the packages for the board is already installed. if so, no hint - if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) { + private async ensureCoreExists( + selectedBoard: BoardIdentifier | undefined + ): Promise { + if (!selectedBoard) { return; } + const candidate = await this.findPlatformToInstall(selectedBoard); + if (!candidate) { + return; + } + const platformIdToInstall = candidate.id; + const selectedBoardName = selectedBoard.name; + if ( + this.installNotificationInfos.some( + ({ boardName, platformId }) => + platformIdToInstall === platformId && selectedBoardName === boardName + ) + ) { + // Already has a notification for the board with the same platform. Nothing to do. + return; + } + this.clearAllNotificationForPlatform(platformIdToInstall); - // filter the installable (not installed) packages, - // CLI returns the packages already sorted with the deprecated ones at the end of the list - // in order to ensure the new ones are preferred - const candidates = packagesForBoard.filter( - ({ installedVersion }) => !installedVersion - ); - - return candidates[0]; - } - - private showAutoInstallPrompt( - candidate: BoardsPackage, - selectedBoard: Board, - selectedPort?: Port - ): void { - const candidateName = candidate.name; const version = candidate.availableVersions[0] ? `[v ${candidate.availableVersions[0]}]` : ''; - - const info = this.generatePromptInfoText( - candidateName, - version, - selectedBoard.name - ); - - const actions = this.createPromptActions(candidate); - - const onRefuse = () => { - this.setLastRefusedPromptInfo(candidate.id, selectedPort); - }; - const handleAction = this.createOnAnswerHandler(actions, onRefuse); - - const onAnswer = (answer: string) => { - this.removeNotificationByBoard(selectedBoard); - - handleAction(answer); - }; - - this.messageService - .info(info, ...actions.map((action) => action.key)) - .then(onAnswer); - } - - private generatePromptInfoText( - candidateName: string, - version: string, - boardName: string - ): string { - return nls.localize( + const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); + const message = nls.localize( 'arduino/board/installNow', 'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?', - candidateName, + candidate.name, version, - boardName + selectedBoard.name ); + const notificationId = this.notificationId(message, InstallManually, yes); + this.installNotificationInfos.push({ + boardName: selectedBoardName, + platformId: platformIdToInstall, + notificationId, + }); + const answer = await this.messageService.info( + message, + InstallManually, + yes + ); + if (answer) { + const index = this.installNotificationInfos.findIndex( + ({ boardName, platformId }) => + platformIdToInstall === platformId && selectedBoardName === boardName + ); + if (index !== -1) { + this.installNotificationInfos.splice(index, 1); + } + if (answer === yes) { + await Installable.installWithProgress({ + installable: this.boardsService, + item: candidate, + messageService: this.messageService, + responseService: this.responseService, + version: candidate.availableVersions[0], + }); + return; + } + if (answer === InstallManually) { + this.boardsManagerWidgetContribution + .openView({ reveal: true }) + .then((widget) => + widget.refresh({ + query: candidate.name.toLocaleLowerCase(), + type: 'All', + }) + ); + } + } } - private createPromptActions( - candidate: BoardsPackage - ): AutoInstallPromptActions { - const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); - - const actions: AutoInstallPromptActions = [ - { - key: InstallManually, - handler: () => { - this.boardsManagerFrontendContribution - .openView({ reveal: true }) - .then((widget) => - widget.refresh({ - query: candidate.name.toLocaleLowerCase(), - type: 'All', - }) - ); - }, - }, - { - isAcceptance: true, - key: yes, - handler: () => { - return Installable.installWithProgress({ - installable: this.boardsService, - item: candidate, - messageService: this.messageService, - responseService: this.responseService, - version: candidate.availableVersions[0], - }); - }, - }, - ]; - - return actions; + private clearAllNotificationForPlatform(predicatePlatformId: string): void { + // Discard all install notifications for the same platform. + const notificationsLength = this.installNotificationInfos.length; + for (let i = notificationsLength - 1; i >= 0; i--) { + const { notificationId, platformId } = this.installNotificationInfos[i]; + if (platformId === predicatePlatformId) { + this.installNotificationInfos.splice(i, 1); + this.notificationManager.clear(notificationId); + } + } } - private createOnAnswerHandler( - actions: AutoInstallPromptActions, - onRefuse?: () => void - ): (answer: string) => void { - return (answer) => { - const actionToHandle = actions.find((action) => action.key === answer); - actionToHandle?.handler(); - - if (!actionToHandle?.isAcceptance && onRefuse) { - onRefuse(); - } - }; + private notificationId(message: string, ...actions: string[]): string { + return this.notificationManager['getMessageId']({ + text: message, + actions, + type: MessageType.Info, + }); } } diff --git a/arduino-ide-extension/src/browser/boards/boards-config-component.tsx b/arduino-ide-extension/src/browser/boards/boards-config-component.tsx new file mode 100644 index 000000000..7d925079e --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-config-component.tsx @@ -0,0 +1,328 @@ +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Event } from '@theia/core/lib/common/event'; +import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state'; +import { nls } from '@theia/core/lib/common/nls'; +import * as React from '@theia/core/shared/react'; +import { + Board, + BoardIdentifier, + BoardWithPackage, + DetectedPort, + findMatchingPortIndex, + Port, + PortIdentifier, +} from '../../common/protocol/boards-service'; +import { Defined } from '../../common/types'; +import { NotificationCenter } from '../notification-center'; +import { BoardsConfigDialogState } from './boards-config-dialog'; +import { EditBoardsConfigActionParams } from './boards-service-provider'; + +namespace BoardsConfigComponent { + export interface Props { + /** + * This is not the real config, it's only living in the dialog. Users can change it without update and can cancel any modifications. + */ + readonly boardsConfig: BoardsConfigDialogState; + readonly notificationCenter: NotificationCenter; + readonly appState: FrontendApplicationState; + readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; + readonly onFilteredTextDidChangeEvent: Event< + Defined + >; + readonly onAppStateDidChange: Event; + readonly onBoardSelected: (board: BoardIdentifier) => void; + readonly onPortSelected: (port: PortIdentifier) => void; + readonly searchBoards: (query?: { + query?: string; + }) => Promise; + readonly ports: ( + predicate?: (port: DetectedPort) => boolean + ) => readonly DetectedPort[]; + } + + export interface State { + searchResults: Array; + showAllPorts: boolean; + query: string; + } +} + +export abstract class Item extends React.Component<{ + item: T; + label: string; + selected: boolean; + onClick: (item: T) => void; + missing?: boolean; + details?: string; +}> { + override render(): React.ReactNode { + const { selected, label, missing, details } = this.props; + const classNames = ['item']; + if (selected) { + classNames.push('selected'); + } + if (missing === true) { + classNames.push('missing'); + } + return ( +
+
{label}
+ {!details ? '' :
{details}
} + {!selected ? ( + '' + ) : ( +
+ +
+ )} +
+ ); + } + + private readonly onClick = () => { + this.props.onClick(this.props.item); + }; +} + +export class BoardsConfigComponent extends React.Component< + BoardsConfigComponent.Props, + BoardsConfigComponent.State +> { + private readonly toDispose: DisposableCollection; + + constructor(props: BoardsConfigComponent.Props) { + super(props); + this.state = { + searchResults: [], + showAllPorts: false, + query: '', + }; + this.toDispose = new DisposableCollection(); + } + + override componentDidMount(): void { + this.toDispose.pushAll([ + this.props.onAppStateDidChange(async (state) => { + if (state === 'ready') { + const searchResults = await this.queryBoards({}); + this.setState({ searchResults }); + } + }), + this.props.notificationCenter.onPlatformDidInstall(() => + this.updateBoards(this.state.query) + ), + this.props.notificationCenter.onPlatformDidUninstall(() => + this.updateBoards(this.state.query) + ), + this.props.notificationCenter.onIndexUpdateDidComplete(() => + this.updateBoards(this.state.query) + ), + this.props.notificationCenter.onDaemonDidStart(() => + this.updateBoards(this.state.query) + ), + this.props.notificationCenter.onDaemonDidStop(() => + this.setState({ searchResults: [] }) + ), + this.props.onFilteredTextDidChangeEvent((query) => { + if (typeof query === 'string') { + this.setState({ query }, () => this.updateBoards(this.state.query)); + } else { + const action = query?.action; + if (action) { + const currentQuery = this.state.query; + if (!currentQuery && action === 'clear-if-not-empty') { + return; + } + this.setState({ query: '' }, () => + this.updateBoards(this.state.query) + ); + } + } + }), + ]); + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + private readonly updateBoards = ( + eventOrQuery: React.ChangeEvent | string = '' + ) => { + const query = + typeof eventOrQuery === 'string' + ? eventOrQuery + : eventOrQuery.target.value.toLowerCase(); + this.setState({ query }); + this.queryBoards({ query }).then((searchResults) => + this.setState({ searchResults }) + ); + }; + + private readonly queryBoards = ( + options: { query?: string } = {} + ): Promise> => { + return this.props.searchBoards(options); + }; + + private readonly toggleFilterPorts = () => { + this.setState({ showAllPorts: !this.state.showAllPorts }); + }; + + private readonly selectPort = (selectedPort: PortIdentifier) => { + this.props.onPortSelected(selectedPort); + }; + + private readonly selectBoard = (selectedBoard: BoardWithPackage) => { + this.props.onBoardSelected(selectedBoard); + }; + + private readonly focusNodeSet = (element: HTMLElement | null) => { + this.props.onFocusNodeSet(element || undefined); + }; + + override render(): React.ReactNode { + return ( + <> + {this.renderContainer( + nls.localize('arduino/board/boards', 'boards'), + this.renderBoards.bind(this) + )} + {this.renderContainer( + nls.localize('arduino/board/ports', 'ports'), + this.renderPorts.bind(this), + this.renderPortsFooter.bind(this) + )} + + ); + } + + private renderContainer( + title: string, + contentRenderer: () => React.ReactNode, + footerRenderer?: () => React.ReactNode + ): React.ReactNode { + return ( +
+
+
{title}
+ {contentRenderer()} +
{footerRenderer ? footerRenderer() : ''}
+
+
+ ); + } + + private renderBoards(): React.ReactNode { + const { boardsConfig } = this.props; + const { searchResults, query } = this.state; + // Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560 + // It is tricky when the core is not yet installed, no FQBNs are available. + const distinctBoards = new Map(); + const toKey = ({ name, packageName, fqbn }: Board.Detailed) => + !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`; + for (const board of Board.decorateBoards( + boardsConfig.selectedBoard, + searchResults + )) { + const key = toKey(board); + if (!distinctBoards.has(key)) { + distinctBoards.set(key, board); + } + } + + const boardsList = Array.from(distinctBoards.values()).map((board) => ( + + key={toKey(board)} + item={board} + label={board.name} + details={board.details} + selected={board.selected} + onClick={this.selectBoard} + missing={board.missing} + /> + )); + + return ( + +
+ + +
+ {boardsList.length > 0 ? ( +
{boardsList}
+ ) : ( +
+ {nls.localize( + 'arduino/board/noBoardsFound', + 'No boards found for "{0}"', + query + )} +
+ )} +
+ ); + } + + private renderPorts(): React.ReactNode { + const predicate = this.state.showAllPorts ? undefined : Port.isVisiblePort; + const detectedPorts = this.props.ports(predicate); + const matchingIndex = findMatchingPortIndex( + this.props.boardsConfig.selectedPort, + detectedPorts + ); + return !detectedPorts.length ? ( +
+ {nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')} +
+ ) : ( +
+ {detectedPorts.map((detectedPort, index) => ( + + key={`${Port.keyOf(detectedPort.port)}`} + item={detectedPort.port} + label={Port.toString(detectedPort.port)} + selected={index === matchingIndex} + onClick={this.selectPort} + /> + ))} +
+ ); + } + + private renderPortsFooter(): React.ReactNode { + return ( +
+ +
+ ); + } +} diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx b/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx deleted file mode 100644 index 7ad65697a..000000000 --- a/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from '@theia/core/shared/react'; -import { injectable, inject } from '@theia/core/shared/inversify'; -import { Emitter } from '@theia/core/lib/common/event'; -import { ReactWidget, Message } from '@theia/core/lib/browser'; -import { BoardsService } from '../../common/protocol/boards-service'; -import { BoardsConfig } from './boards-config'; -import { BoardsServiceProvider } from './boards-service-provider'; -import { NotificationCenter } from '../notification-center'; - -@injectable() -export class BoardsConfigDialogWidget extends ReactWidget { - @inject(BoardsService) - protected readonly boardsService: BoardsService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; - - protected readonly onFilterTextDidChangeEmitter = new Emitter(); - protected readonly onBoardConfigChangedEmitter = - new Emitter(); - readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event; - - protected focusNode: HTMLElement | undefined; - - constructor() { - super(); - this.id = 'select-board-dialog'; - this.toDispose.pushAll([ - this.onBoardConfigChangedEmitter, - this.onFilterTextDidChangeEmitter, - ]); - } - - search(query: string): void { - this.onFilterTextDidChangeEmitter.fire(query); - } - - protected fireConfigChanged = (config: BoardsConfig.Config) => { - this.onBoardConfigChangedEmitter.fire(config); - }; - - protected setFocusNode = (element: HTMLElement | undefined) => { - this.focusNode = element; - }; - - protected render(): React.ReactNode { - return ( -
- -
- ); - } - - protected override onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - if (this.focusNode instanceof HTMLInputElement) { - this.focusNode.select(); - } - (this.focusNode || this.node).focus(); - } -} diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts deleted file mode 100644 index b08c6de36..000000000 --- a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - injectable, - inject, - postConstruct, -} from '@theia/core/shared/inversify'; -import { Message } from '@theia/core/shared/@phosphor/messaging'; -import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser'; -import { AbstractDialog } from '../theia/dialogs/dialogs'; -import { BoardsConfig } from './boards-config'; -import { BoardsService } from '../../common/protocol/boards-service'; -import { BoardsServiceProvider } from './boards-service-provider'; -import { BoardsConfigDialogWidget } from './boards-config-dialog-widget'; -import { nls } from '@theia/core/lib/common'; - -@injectable() -export class BoardsConfigDialogProps extends DialogProps {} - -@injectable() -export class BoardsConfigDialog extends AbstractDialog { - @inject(BoardsConfigDialogWidget) - protected readonly widget: BoardsConfigDialogWidget; - - @inject(BoardsService) - protected readonly boardService: BoardsService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - protected config: BoardsConfig.Config = {}; - - constructor( - @inject(BoardsConfigDialogProps) - protected override readonly props: BoardsConfigDialogProps - ) { - super({ ...props, maxWidth: 500 }); - - this.node.id = 'select-board-dialog-container'; - this.contentNode.classList.add('select-board-dialog'); - this.contentNode.appendChild(this.createDescription()); - - this.appendCloseButton( - nls.localize('vscode/issueMainService/cancel', 'Cancel') - ); - this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK')); - } - - @postConstruct() - protected init(): void { - this.toDispose.push( - this.boardsServiceClient.onBoardsConfigChanged((config) => { - this.config = config; - this.update(); - }) - ); - } - - /** - * Pass in an empty string if you want to reset the search term. Using `undefined` has no effect. - */ - override async open( - query: string | undefined = undefined - ): Promise { - if (typeof query === 'string') { - this.widget.search(query); - } - return super.open(); - } - - protected createDescription(): HTMLElement { - const head = document.createElement('div'); - head.classList.add('head'); - - const text = document.createElement('div'); - text.classList.add('text'); - head.appendChild(text); - - for (const paragraph of [ - nls.localize( - 'arduino/board/configDialog1', - 'Select both a Board and a Port if you want to upload a sketch.' - ), - nls.localize( - 'arduino/board/configDialog2', - 'If you only select a Board you will be able to compile, but not to upload your sketch.' - ), - ]) { - const p = document.createElement('div'); - p.textContent = paragraph; - text.appendChild(p); - } - - return head; - } - - protected override onAfterAttach(msg: Message): void { - if (this.widget.isAttached) { - Widget.detach(this.widget); - } - Widget.attach(this.widget, this.contentNode); - this.toDisposeOnDetach.push( - this.widget.onBoardConfigChanged((config) => { - this.config = config; - this.update(); - }) - ); - super.onAfterAttach(msg); - this.update(); - } - - protected override onUpdateRequest(msg: Message): void { - super.onUpdateRequest(msg); - this.widget.update(); - } - - protected override onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - this.widget.activate(); - } - - protected override handleEnter(event: KeyboardEvent): boolean | void { - if (event.target instanceof HTMLTextAreaElement) { - return false; - } - } - - protected override isValid(value: BoardsConfig.Config): DialogError { - if (!value.selectedBoard) { - if (value.selectedPort) { - return nls.localize( - 'arduino/board/pleasePickBoard', - 'Please pick a board connected to the port you have selected.' - ); - } - return false; - } - return ''; - } - - get value(): BoardsConfig.Config { - return this.config; - } -} diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog.tsx b/arduino-ide-extension/src/browser/boards/boards-config-dialog.tsx new file mode 100644 index 000000000..0941550f0 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog.tsx @@ -0,0 +1,190 @@ +import { DialogError, DialogProps } from '@theia/core/lib/browser/dialogs'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { Emitter } from '@theia/core/lib/common/event'; +import { nls } from '@theia/core/lib/common/nls'; +import { deepClone } from '@theia/core/lib/common/objects'; +import type { Message } from '@theia/core/shared/@phosphor/messaging'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import React from '@theia/core/shared/react'; +import type { ReactNode } from '@theia/core/shared/react/index'; +import { + BoardsConfig, + BoardWithPackage, + DetectedPort, + emptyBoardsConfig, + PortIdentifier, +} from '../../common/protocol/boards-service'; +import { Defined } from '../../common/types'; +import { NotificationCenter } from '../notification-center'; +import { ReactDialog } from '../theia/dialogs/dialogs'; +import { BoardsConfigComponent } from './boards-config-component'; +import { + BoardsServiceProvider, + EditBoardsConfigActionParams, +} from './boards-service-provider'; + +@injectable() +export class BoardsConfigDialogProps extends DialogProps {} + +export type BoardsConfigDialogState = Omit & { + selectedBoard: BoardsConfig['selectedBoard'] | BoardWithPackage; +}; + +@injectable() +export class BoardsConfigDialog extends ReactDialog { + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + @inject(NotificationCenter) + private readonly notificationCenter: NotificationCenter; + @inject(FrontendApplicationStateService) + private readonly appStateService: FrontendApplicationStateService; + + private readonly onFilterTextDidChangeEmitter: Emitter< + Defined + >; + private readonly onBoardSelected = (board: BoardWithPackage): void => { + this._boardsConfig.selectedBoard = board; + this.update(); + }; + private readonly onPortSelected = (port: PortIdentifier): void => { + this._boardsConfig.selectedPort = port; + this.update(); + }; + private readonly setFocusNode = (element: HTMLElement | undefined): void => { + this.focusNode = element; + }; + private readonly searchBoards = (options: { + query?: string; + }): Promise => { + return this.boardsServiceProvider.searchBoards(options); + }; + private readonly ports = ( + predicate?: (port: DetectedPort) => boolean + ): readonly DetectedPort[] => { + return this.boardsServiceProvider.boardList.ports(predicate); + }; + private _boardsConfig: BoardsConfigDialogState; + private focusNode: HTMLElement | undefined; + + constructor( + @inject(BoardsConfigDialogProps) + protected override readonly props: BoardsConfigDialogProps + ) { + super({ ...props, maxWidth: 500 }); + this.node.id = 'select-board-dialog-container'; + this.contentNode.classList.add('select-board-dialog'); + this.appendCloseButton( + nls.localize('vscode/issueMainService/cancel', 'Cancel') + ); + this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK')); + this._boardsConfig = emptyBoardsConfig(); + this.onFilterTextDidChangeEmitter = new Emitter(); + } + + @postConstruct() + protected init(): void { + this.boardsServiceProvider.onBoardListDidChange(() => { + this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig); + this.update(); + }); // Do not add to `toDispose`! + this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig); + } + + override async open( + params?: EditBoardsConfigActionParams + ): Promise { + if (params) { + if (params.query || typeof params.query === 'string') { + this.onFilterTextDidChangeEmitter.fire(params.query); + } + if (params.selectedPort) { + this._boardsConfig.selectedPort = params.selectedPort; + } + if (params.selectedBoard) { + this._boardsConfig.selectedBoard = params.selectedBoard; + } + } + return super.open(); + } + + protected override onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.update(); + } + + protected override render(): ReactNode { + return ( + <> +
+
+
+ {nls.localize( + 'arduino/board/configDialog1', + 'Select both a Board and a Port if you want to upload a sketch.' + )} +
+
+ {nls.localize( + 'arduino/board/configDialog2', + 'If you only select a Board you will be able to compile, but not to upload your sketch.' + )} +
+
+
+
+
+ +
+
+ + ); + } + + protected override onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + if (this.focusNode instanceof HTMLInputElement) { + this.focusNode.select(); + } + (this.focusNode || this.node).focus(); + } + + protected override handleEnter(event: KeyboardEvent): boolean | void { + if (event.target instanceof HTMLTextAreaElement) { + return false; + } + } + + protected override isValid(value: BoardsConfig): DialogError { + if (!value.selectedBoard) { + if (value.selectedPort) { + return nls.localize( + 'arduino/board/pleasePickBoard', + 'Please pick a board connected to the port you have selected.' + ); + } + return false; + } + return ''; + } + + get value(): BoardsConfigDialogState { + return this._boardsConfig; + } +} diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx deleted file mode 100644 index 8781981e6..000000000 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import * as React from '@theia/core/shared/react'; -import { Event } from '@theia/core/lib/common/event'; -import { notEmpty } from '@theia/core/lib/common/objects'; -import { MaybePromise } from '@theia/core/lib/common/types'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { - Board, - Port, - BoardConfig as ProtocolBoardConfig, - BoardWithPackage, -} from '../../common/protocol/boards-service'; -import { NotificationCenter } from '../notification-center'; -import { - AvailableBoard, - BoardsServiceProvider, -} from './boards-service-provider'; -import { naturalCompare } from '../../common/utils'; -import { nls } from '@theia/core/lib/common'; -import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state'; - -export namespace BoardsConfig { - export type Config = ProtocolBoardConfig; - - export interface Props { - readonly boardsServiceProvider: BoardsServiceProvider; - readonly notificationCenter: NotificationCenter; - readonly onConfigChange: (config: Config) => void; - readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; - readonly onFilteredTextDidChangeEvent: Event; - readonly onAppStateDidChange: Event; - } - - export interface State extends Config { - searchResults: Array; - knownPorts: Port[]; - showAllPorts: boolean; - query: string; - } -} - -export abstract class Item extends React.Component<{ - item: T; - label: string; - selected: boolean; - onClick: (item: T) => void; - missing?: boolean; - details?: string; -}> { - override render(): React.ReactNode { - const { selected, label, missing, details } = this.props; - const classNames = ['item']; - if (selected) { - classNames.push('selected'); - } - if (missing === true) { - classNames.push('missing'); - } - return ( -
-
{label}
- {!details ? '' :
{details}
} - {!selected ? ( - '' - ) : ( -
- -
- )} -
- ); - } - - protected onClick = () => { - this.props.onClick(this.props.item); - }; -} - -export class BoardsConfig extends React.Component< - BoardsConfig.Props, - BoardsConfig.State -> { - protected toDispose = new DisposableCollection(); - - constructor(props: BoardsConfig.Props) { - super(props); - - const { boardsConfig } = props.boardsServiceProvider; - this.state = { - searchResults: [], - knownPorts: [], - showAllPorts: false, - query: '', - ...boardsConfig, - }; - } - - override componentDidMount(): void { - this.toDispose.pushAll([ - this.props.onAppStateDidChange((state) => { - if (state === 'ready') { - this.updateBoards(); - this.updatePorts( - this.props.boardsServiceProvider.availableBoards - .map(({ port }) => port) - .filter(notEmpty) - ); - } - }), - this.props.boardsServiceProvider.onAvailablePortsChanged( - ({ newState, oldState }) => { - const removedPorts = oldState.filter( - (oldPort) => - !newState.find((newPort) => Port.sameAs(newPort, oldPort)) - ); - this.updatePorts(newState, removedPorts); - } - ), - this.props.boardsServiceProvider.onBoardsConfigChanged( - ({ selectedBoard, selectedPort }) => { - this.setState({ selectedBoard, selectedPort }, () => - this.fireConfigChanged() - ); - } - ), - this.props.notificationCenter.onPlatformDidInstall(() => - this.updateBoards(this.state.query) - ), - this.props.notificationCenter.onPlatformDidUninstall(() => - this.updateBoards(this.state.query) - ), - this.props.notificationCenter.onIndexUpdateDidComplete(() => - this.updateBoards(this.state.query) - ), - this.props.notificationCenter.onDaemonDidStart(() => - this.updateBoards(this.state.query) - ), - this.props.notificationCenter.onDaemonDidStop(() => - this.setState({ searchResults: [] }) - ), - this.props.onFilteredTextDidChangeEvent((query) => - this.setState({ query }, () => this.updateBoards(this.state.query)) - ), - ]); - } - - override componentWillUnmount(): void { - this.toDispose.dispose(); - } - - protected fireConfigChanged(): void { - const { selectedBoard, selectedPort } = this.state; - this.props.onConfigChange({ selectedBoard, selectedPort }); - } - - protected updateBoards = ( - eventOrQuery: React.ChangeEvent | string = '' - ) => { - const query = - typeof eventOrQuery === 'string' - ? eventOrQuery - : eventOrQuery.target.value.toLowerCase(); - this.setState({ query }); - this.queryBoards({ query }).then((searchResults) => - this.setState({ searchResults }) - ); - }; - - protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => { - this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => { - let { selectedPort } = this.state; - // If the currently selected port is not available anymore, unset the selected port. - if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) { - selectedPort = undefined; - } - this.setState({ knownPorts, selectedPort }, () => - this.fireConfigChanged() - ); - }); - }; - - protected queryBoards = ( - options: { query?: string } = {} - ): Promise> => { - return this.props.boardsServiceProvider.searchBoards(options); - }; - - protected get availablePorts(): MaybePromise { - return this.props.boardsServiceProvider.availableBoards - .map(({ port }) => port) - .filter(notEmpty); - } - - protected get availableBoards(): AvailableBoard[] { - return this.props.boardsServiceProvider.availableBoards; - } - - protected queryPorts = async ( - availablePorts: MaybePromise = this.availablePorts - ) => { - // Available ports must be sorted in this order: - // 1. Serial with recognized boards - // 2. Serial with guessed boards - // 3. Serial with incomplete boards - // 4. Network with recognized boards - // 5. Other protocols with recognized boards - const ports = (await availablePorts).sort((left: Port, right: Port) => { - if (left.protocol === 'serial' && right.protocol !== 'serial') { - return -1; - } else if (left.protocol !== 'serial' && right.protocol === 'serial') { - return 1; - } else if (left.protocol === 'network' && right.protocol !== 'network') { - return -1; - } else if (left.protocol !== 'network' && right.protocol === 'network') { - return 1; - } else if (left.protocol === right.protocol) { - // We show ports, including those that have guessed - // or unrecognized boards, so we must sort those too. - const leftBoard = this.availableBoards.find( - (board) => board.port === left - ); - const rightBoard = this.availableBoards.find( - (board) => board.port === right - ); - if (leftBoard && !rightBoard) { - return -1; - } else if (!leftBoard && rightBoard) { - return 1; - } else if (leftBoard?.state! < rightBoard?.state!) { - return -1; - } else if (leftBoard?.state! > rightBoard?.state!) { - return 1; - } - } - return naturalCompare(left.address, right.address); - }); - return { knownPorts: ports }; - }; - - protected toggleFilterPorts = () => { - this.setState({ showAllPorts: !this.state.showAllPorts }); - }; - - protected selectPort = (selectedPort: Port | undefined) => { - this.setState({ selectedPort }, () => this.fireConfigChanged()); - }; - - protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => { - this.setState({ selectedBoard }, () => this.fireConfigChanged()); - }; - - protected focusNodeSet = (element: HTMLElement | null) => { - this.props.onFocusNodeSet(element || undefined); - }; - - override render(): React.ReactNode { - return ( - <> - {this.renderContainer( - nls.localize('arduino/board/boards', 'boards'), - this.renderBoards.bind(this) - )} - {this.renderContainer( - nls.localize('arduino/board/ports', 'ports'), - this.renderPorts.bind(this), - this.renderPortsFooter.bind(this) - )} - - ); - } - - protected renderContainer( - title: string, - contentRenderer: () => React.ReactNode, - footerRenderer?: () => React.ReactNode - ): React.ReactNode { - return ( -
-
-
{title}
- {contentRenderer()} -
{footerRenderer ? footerRenderer() : ''}
-
-
- ); - } - - protected renderBoards(): React.ReactNode { - const { selectedBoard, searchResults, query } = this.state; - // Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560 - // It is tricky when the core is not yet installed, no FQBNs are available. - const distinctBoards = new Map(); - const toKey = ({ name, packageName, fqbn }: Board.Detailed) => - !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`; - for (const board of Board.decorateBoards(selectedBoard, searchResults)) { - const key = toKey(board); - if (!distinctBoards.has(key)) { - distinctBoards.set(key, board); - } - } - - const boardsList = Array.from(distinctBoards.values()).map((board) => ( - - key={toKey(board)} - item={board} - label={board.name} - details={board.details} - selected={board.selected} - onClick={this.selectBoard} - missing={board.missing} - /> - )); - - return ( - -
- - -
- {boardsList.length > 0 ? ( -
{boardsList}
- ) : ( -
- {nls.localize( - 'arduino/board/noBoardsFound', - 'No boards found for "{0}"', - query - )} -
- )} -
- ); - } - - protected renderPorts(): React.ReactNode { - let ports = [] as Port[]; - if (this.state.showAllPorts) { - ports = this.state.knownPorts; - } else { - ports = this.state.knownPorts.filter( - Port.visiblePorts(this.availableBoards) - ); - } - return !ports.length ? ( -
- {nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')} -
- ) : ( -
- {ports.map((port) => ( - - key={`${Port.keyOf(port)}`} - item={port} - label={Port.toString(port)} - selected={Port.sameAs(this.state.selectedPort, port)} - onClick={this.selectPort} - /> - ))} -
- ); - } - - protected renderPortsFooter(): React.ReactNode { - return ( -
- -
- ); - } -} - -export namespace BoardsConfig { - export namespace Config { - export function sameAs(config: Config, other: Config | Board): boolean { - const { selectedBoard, selectedPort } = config; - if (Board.is(other)) { - return ( - !!selectedBoard && - Board.equals(other, selectedBoard) && - Port.sameAs(selectedPort, other.port) - ); - } - return sameAs(config, other); - } - - export function equals(left: Config, right: Config): boolean { - return ( - left.selectedBoard === right.selectedBoard && - left.selectedPort === right.selectedPort - ); - } - - export function toString( - config: Config, - options: { default: string } = { default: '' } - ): string { - const { selectedBoard, selectedPort: port } = config; - if (!selectedBoard) { - return options.default; - } - const { name } = selectedBoard; - return `${name}${port ? ` at ${port.address}` : ''}`; - } - } -} diff --git a/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts b/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts index f323621d3..067863d31 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts @@ -7,7 +7,12 @@ import { DisposableCollection, } from '@theia/core/lib/common/disposable'; import { BoardsServiceProvider } from './boards-service-provider'; -import { Board, ConfigOption, Programmer } from '../../common/protocol'; +import { + BoardIdentifier, + ConfigOption, + isBoardIdentifierChangeEvent, + Programmer, +} from '../../common/protocol'; import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { BoardsDataStore } from './boards-data-store'; import { MainMenuManager } from '../../common/main-menu-manager'; @@ -30,7 +35,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution { protected readonly boardsDataStore: BoardsDataStore; @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; + protected readonly boardsServiceProvider: BoardsServiceProvider; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; @@ -43,21 +48,21 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution { .reachedState('ready') .then(() => this.updateMenuActions( - this.boardsServiceClient.boardsConfig.selectedBoard + this.boardsServiceProvider.boardsConfig.selectedBoard ) ); this.boardsDataStore.onChanged(() => - this.updateMenuActions( - this.boardsServiceClient.boardsConfig.selectedBoard - ) - ); - this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => - this.updateMenuActions(selectedBoard) + this.updateMenuActions(this.boardsServiceProvider.boardsConfig.selectedBoard) ); + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.updateMenuActions(event.selectedBoard); + } + }); } protected async updateMenuActions( - selectedBoard: Board | undefined + selectedBoard: BoardIdentifier | undefined ): Promise { return this.queue.add(async () => { this.toDisposeOnBoardChange.dispose(); diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index cdf73d18f..52507a577 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -1,63 +1,64 @@ -import { injectable, inject, named } from '@theia/core/shared/inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Emitter, Event } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; import { deepClone } from '@theia/core/lib/common/objects'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { - FrontendApplicationContribution, - LocalStorageService, -} from '@theia/core/lib/browser'; -import { notEmpty } from '../../common/utils'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; import { + BoardDetails, BoardsService, ConfigOption, - BoardDetails, Programmer, } from '../../common/protocol'; +import { notEmpty } from '../../common/utils'; import { NotificationCenter } from '../notification-center'; @injectable() export class BoardsDataStore implements FrontendApplicationContribution { @inject(ILogger) @named('store') - protected readonly logger: ILogger; - + private readonly logger: ILogger; @inject(BoardsService) - protected readonly boardsService: BoardsService; - + private readonly boardsService: BoardsService; @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; - + private readonly notificationCenter: NotificationCenter; @inject(LocalStorageService) - protected readonly storageService: LocalStorageService; + private readonly storageService: LocalStorageService; - protected readonly onChangedEmitter = new Emitter(); + private readonly onChangedEmitter = new Emitter(); + private readonly toDispose = new DisposableCollection(this.onChangedEmitter); onStart(): void { - this.notificationCenter.onPlatformDidInstall(async ({ item }) => { - const dataDidChangePerFqbn: string[] = []; - for (const fqbn of item.boards - .map(({ fqbn }) => fqbn) - .filter(notEmpty) - .filter((fqbn) => !!fqbn)) { - const key = this.getStorageKey(fqbn); - let data = await this.storageService.getData< - ConfigOption[] | undefined - >(key); - if (!data || !data.length) { - const details = await this.getBoardDetailsSafe(fqbn); - if (details) { - data = details.configOptions; - if (data.length) { - await this.storageService.setData(key, data); - dataDidChangePerFqbn.push(fqbn); + this.toDispose.push( + this.notificationCenter.onPlatformDidInstall(async ({ item }) => { + const dataDidChangePerFqbn: string[] = []; + for (const fqbn of item.boards + .map(({ fqbn }) => fqbn) + .filter(notEmpty) + .filter((fqbn) => !!fqbn)) { + const key = this.getStorageKey(fqbn); + let data = await this.storageService.getData(key); + if (!data || !data.length) { + const details = await this.getBoardDetailsSafe(fqbn); + if (details) { + data = details.configOptions; + if (data.length) { + await this.storageService.setData(key, data); + dataDidChangePerFqbn.push(fqbn); + } } } } - } - if (dataDidChangePerFqbn.length) { - this.fireChanged(...dataDidChangePerFqbn); - } - }); + if (dataDidChangePerFqbn.length) { + this.fireChanged(...dataDidChangePerFqbn); + } + }) + ); + } + + onStop(): void { + this.toDispose.dispose(); } get onChanged(): Event { @@ -65,7 +66,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { } async appendConfigToFqbn( - fqbn: string | undefined, + fqbn: string | undefined ): Promise { if (!fqbn) { return undefined; @@ -100,12 +101,13 @@ export class BoardsDataStore implements FrontendApplicationContribution { return data; } - async selectProgrammer( - { - fqbn, - selectedProgrammer, - }: { fqbn: string; selectedProgrammer: Programmer }, - ): Promise { + async selectProgrammer({ + fqbn, + selectedProgrammer, + }: { + fqbn: string; + selectedProgrammer: Programmer; + }): Promise { const data = deepClone(await this.getData(fqbn)); const { programmers } = data; if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) { @@ -120,13 +122,15 @@ export class BoardsDataStore implements FrontendApplicationContribution { return true; } - async selectConfigOption( - { - fqbn, - option, - selectedValue, - }: { fqbn: string; option: string; selectedValue: string } - ): Promise { + async selectConfigOption({ + fqbn, + option, + selectedValue, + }: { + fqbn: string; + option: string; + selectedValue: string; + }): Promise { const data = deepClone(await this.getData(fqbn)); const { configOptions } = data; const configOption = configOptions.find((c) => c.option === option); diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 279a2e3fa..ac4142a35 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -1,37 +1,137 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; -import { Emitter } from '@theia/core/lib/common/event'; -import { ILogger } from '@theia/core/lib/common/logger'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { Command, CommandContribution, CommandRegistry, CommandService, } from '@theia/core/lib/common/command'; +import type { Disposable } from '@theia/core/lib/common/disposable'; +import { Emitter } from '@theia/core/lib/common/event'; +import { ILogger } from '@theia/core/lib/common/logger'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; -import { RecursiveRequired } from '../../common/types'; +import { nls } from '@theia/core/lib/common/nls'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import type { Mutable } from '@theia/core/lib/common/types'; +import { inject, injectable, optional } from '@theia/core/shared/inversify'; import { - Port, - Board, - BoardsService, + OutputChannel, + OutputChannelManager, +} from '@theia/output/lib/browser/output-channel'; +import { + BoardIdentifier, + boardIdentifierEquals, + BoardsConfig, + BoardsConfigChangeEvent, BoardsPackage, - AttachedBoardsChangeEvent, - BoardWithPackage, + BoardsService, BoardUserField, - AvailablePorts, + BoardWithPackage, + DetectedPorts, + emptyBoardsConfig, + isBoardIdentifier, + isPortIdentifier, + Port, + PortIdentifier, + portIdentifierEquals, + serializePlatformIdentifier, } from '../../common/protocol'; -import { BoardsConfig } from './boards-config'; -import { naturalCompare } from '../../common/utils'; -import { NotificationCenter } from '../notification-center'; -import { StorageWrapper } from '../storage-wrapper'; -import { nls } from '@theia/core/lib/common'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { Unknown } from '../../common/nls'; +import { + BoardList, + BoardListHistory, + createBoardList, + isBoardListHistory, +} from '../../common/protocol/board-list'; +import { Defined } from '../../common/types'; import { StartupTask, StartupTaskProvider, } from '../../electron-common/startup-task'; +import { NotificationCenter } from '../notification-center'; +import { StorageWrapper } from '../storage-wrapper'; +import { BoardsDataStore } from './boards-data-store'; + +const boardListHistoryStorageKey = 'arduino-ide:boardListHistory'; +const selectedPortStorageKey = 'arduino-ide:selectedPort'; +const selectedBoardStorageKey = 'arduino-ide:selectedBoard'; + +type UpdateBoardsConfigReason = + /** + * Restore previous state at IDE startup. + */ + | 'restore' + /** + * The board and the optional port were changed from the dialog. + */ + | 'dialog' + /** + * The board and the port were updated from the board select toolbar. + */ + | 'toolbar' + /** + * The board and port configuration was inherited from another window. + */ + | 'inherit'; + +interface RefreshBoardListParams { + detectedPorts?: DetectedPorts; + boardsConfig?: BoardsConfig; + boardListHistory?: BoardListHistory; +} + +export type SelectBoardsConfigActionParams = Defined; +export interface SelectBoardsConfigAction { + (params: SelectBoardsConfigActionParams): void; +} + +export interface EditBoardsConfigActionParams { + readonly selectedPort?: PortIdentifier; + readonly selectedBoard?: BoardIdentifier; + /** + * Query string to search for. Or `'clear-if-not-empty'` action when the `` should be cleared. It's a NOOP, if already empty. Use `'clear'` to force rerun the search. + */ + readonly query?: + | string + | Readonly<{ action: 'clear-if-not-empty' | 'clear' }>; +} +export interface EditBoardsConfigAction { + (params?: EditBoardsConfigActionParams): void; +} + +export interface BoardListUI extends BoardList { + /** + * Sets the frontend's port and board configuration according to the params. + */ + onSelect: SelectBoardsConfigAction; + /** + * Opens up the boards config dialog with the port and (optional) board to select in the dialog. + * Calling this function does not immediately change the frontend's port and board config, but + * preselects items in the dialog. + */ + onEdit: EditBoardsConfigAction; +} + +@injectable() +export class BoardListDumper implements Disposable { + @inject(OutputChannelManager) + private readonly outputChannelManager: OutputChannelManager; + + private outputChannel: OutputChannel | undefined; + + dump(boardList: BoardList): void { + if (!this.outputChannel) { + this.outputChannel = this.outputChannelManager.getChannel( + 'Developer (Arduino)' + ); + } + this.outputChannel.show({ preserveFocus: true }); + this.outputChannel.append(boardList.toString() + '\n'); + } + + dispose(): void { + this.outputChannel?.dispose(); + } +} @injectable() export class BoardsServiceProvider @@ -41,402 +141,328 @@ export class BoardsServiceProvider CommandContribution { @inject(ILogger) - protected logger: ILogger; - + private readonly logger: ILogger; @inject(MessageService) - protected messageService: MessageService; + private messageService: MessageService; @inject(BoardsService) - protected boardsService: BoardsService; - + private readonly boardsService: BoardsService; @inject(CommandService) - protected commandService: CommandService; - + private readonly commandService: CommandService; @inject(NotificationCenter) - protected notificationCenter: NotificationCenter; - + private readonly notificationCenter: NotificationCenter; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - - protected readonly onBoardsConfigChangedEmitter = - new Emitter(); - protected readonly onAvailableBoardsChangedEmitter = new Emitter< - AvailableBoard[] - >(); - protected readonly onAvailablePortsChangedEmitter = new Emitter<{ - newState: Port[]; - oldState: Port[]; - }>(); - private readonly inheritedConfig = new Deferred(); - - /** - * Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it. - * It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows, - * perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`. - * We have to listen on such changes and auto-reconnect the same board on another port. - * See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ - */ - protected latestValidBoardsConfig: - | RecursiveRequired - | undefined = undefined; - protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined; - protected _boardsConfig: BoardsConfig.Config = {}; - protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only. - protected _availablePorts: Port[] = []; - protected _availableBoards: AvailableBoard[] = []; - - private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined; - private lastAvailablePortsOnUpload: Port[] | undefined; - private boardConfigToAutoSelect: BoardsConfig.Config | undefined; - + @inject(BoardsDataStore) + private readonly boardsDataStore: BoardsDataStore; + @optional() + @inject(BoardListDumper) + private readonly boardListDumper?: BoardListDumper; + + private _boardsConfig = emptyBoardsConfig(); + private _detectedPorts: DetectedPorts = {}; + private _boardList = this.createBoardListUI(createBoardList({})); + private _boardListHistory: Mutable = {}; + private _ready = new Deferred(); + + private readonly boardsConfigDidChangeEmitter = + new Emitter(); + readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event; + + private readonly boardListDidChangeEmitter = new Emitter(); /** - * Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\ - * This event also fires, when the boards package was not available for the currently selected board, - * and the user installs the board package. Note: installing a board package will set the `fqbn` of the - * currently selected board. - * - * This event is also emitted when the board package for the currently selected board was uninstalled. + * Emits an event on board config (port or board) change, and when the discovery (`board list --watch`) detected any changes. */ - readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event; - readonly onAvailableBoardsChanged = - this.onAvailableBoardsChangedEmitter.event; - readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event; - - private readonly _reconciled = new Deferred(); + readonly onBoardListDidChange = this.boardListDidChangeEmitter.event; onStart(): void { - this.notificationCenter.onAttachedBoardsDidChange( - this.notifyAttachedBoardsChanged.bind(this) + this.notificationCenter.onDetectedPortsDidChange(({ detectedPorts }) => + this.refreshBoardList({ detectedPorts }) ); - this.notificationCenter.onPlatformDidInstall( - this.notifyPlatformInstalled.bind(this) + this.notificationCenter.onPlatformDidInstall((event) => + this.maybeUpdateSelectedBoard(event) ); - this.notificationCenter.onPlatformDidUninstall( - this.notifyPlatformUninstalled.bind(this) - ); - - this.appStateService.reachedState('ready').then(async () => { - const [state] = await Promise.all([ - this.boardsService.getState(), - this.loadState(), - ]); - const { boards: attachedBoards, ports: availablePorts } = - AvailablePorts.split(state); - this._attachedBoards = attachedBoards; - const oldState = this._availablePorts.slice(); - this._availablePorts = availablePorts; - this.onAvailablePortsChangedEmitter.fire({ - newState: this._availablePorts.slice(), - oldState, - }); + this.appStateService + .reachedState('ready') + .then(async () => { + const [detectedPorts, storedState] = await Promise.all([ + this.boardsService.getDetectedPorts(), + this.restoreState(), + ]); + const { selectedBoard, selectedPort, boardListHistory } = storedState; + const options: RefreshBoardListParams = { + boardListHistory, + detectedPorts, + }; + // If either the port or the board is set, restore it. Otherwise, do not restore nothing. + // It might override the inherited boards config from the other window on File > New Sketch + if (selectedBoard || selectedPort) { + options.boardsConfig = { selectedBoard, selectedPort }; + } + this.refreshBoardList(options); + this._ready.resolve(); + }) + .finally(() => this._ready.resolve()); + } - await this.reconcileAvailableBoards(); + private async maybeUpdateSelectedBoard(event: { + item: BoardsPackage; + }): Promise { + const { selectedBoard } = this._boardsConfig; + if ( + selectedBoard && + !selectedBoard.fqbn && + BoardWithPackage.is(selectedBoard) + ) { + const selectedBoardPlatformId = serializePlatformIdentifier( + selectedBoard.packageId + ); + if (selectedBoardPlatformId === event.item.id) { + const installedSelectedBoard = event.item.boards.find( + (board) => board.name === selectedBoard.name + ); + // if the board can be found by its name after the install event select it. otherwise unselect it + // historical hint: https://github.com/arduino/arduino-ide/blob/144df893d0dafec64a26565cf912a98f32572da9/arduino-ide-extension/src/browser/boards/boards-service-provider.ts#L289-L320 + this.updateBoard(installedSelectedBoard); + if (!installedSelectedBoard) { + const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); + const answer = await this.messageService.warn( + nls.localize( + 'arduino/board/couldNotFindPreviouslySelected', + "Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?", + selectedBoard.name, + event.item.name + ), + nls.localize('arduino/board/reselectLater', 'Reselect later'), + yes + ); + if (answer === yes) { + this.onBoardsConfigEdit({ + query: selectedBoard.name, + selectedPort: this._boardsConfig.selectedPort, + }); + } + } + } + } + } - this.tryReconnect(); - this._reconciled.resolve(); - }); + onStop(): void { + this.boardListDumper?.dispose(); } registerCommands(registry: CommandRegistry): void { registry.registerCommand(USE_INHERITED_CONFIG, { - execute: (inheritedConfig: BoardsConfig.Config) => - this.inheritedConfig.resolve(inheritedConfig), + execute: ( + boardsConfig: BoardsConfig, + boardListHistory: BoardListHistory + ) => { + if (boardListHistory) { + this._boardListHistory = boardListHistory; + } + this.update({ boardsConfig }, 'inherit'); + }, + }); + if (this.boardListDumper) { + registry.registerCommand(DUMP_BOARD_LIST, { + execute: () => this.boardListDumper?.dump(this._boardList), + }); + } + registry.registerCommand(CLEAR_BOARD_LIST_HISTORY, { + execute: () => { + this.refreshBoardList({ boardListHistory: {} }); + this.setData(boardListHistoryStorageKey, undefined); + }, + }); + registry.registerCommand(CLEAR_BOARDS_CONFIG, { + execute: () => { + this.refreshBoardList({ boardsConfig: emptyBoardsConfig() }); + Promise.all([ + this.setData(selectedPortStorageKey, undefined), + this.setData(selectedBoardStorageKey, undefined), + ]); + }, }); } - get reconciled(): Promise { - return this._reconciled.promise; + tasks(): StartupTask[] { + return [ + { + command: USE_INHERITED_CONFIG.id, + args: [this._boardsConfig, this._boardListHistory], + }, + ]; } - snapshotBoardDiscoveryOnUpload(): void { - this.lastBoardsConfigOnUpload = this._boardsConfig; - this.lastAvailablePortsOnUpload = this._availablePorts; + private refreshBoardList(params?: RefreshBoardListParams): void { + if (params?.detectedPorts) { + this._detectedPorts = params.detectedPorts; + } + if (params?.boardsConfig) { + this._boardsConfig = params.boardsConfig; + } + if (params?.boardListHistory) { + this._boardListHistory = params.boardListHistory; + } + const boardList = createBoardList( + this._detectedPorts, + this._boardsConfig, + this._boardListHistory + ); + this._boardList = this.createBoardListUI(boardList); + this.boardListDidChangeEmitter.fire(this._boardList); } - clearBoardDiscoverySnapshot(): void { - this.lastBoardsConfigOnUpload = undefined; - this.lastAvailablePortsOnUpload = undefined; + private createBoardListUI(boardList: BoardList): BoardListUI { + return Object.assign(boardList, { + onSelect: this.onBoardsConfigSelect.bind(this), + onEdit: this.onBoardsConfigEdit.bind(this), + }); } - attemptPostUploadAutoSelect(): void { - setTimeout(() => { - if (this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload) { - this.attemptAutoSelect({ - ports: this._availablePorts, - boards: this._availableBoards, - }); - } - }, 2000); // 2 second delay same as IDE 1.8 + private onBoardsConfigSelect(params: SelectBoardsConfigActionParams): void { + this.updateConfig(params, 'toolbar'); } - private attemptAutoSelect( - newState: AttachedBoardsChangeEvent['newState'] - ): void { - this.deriveBoardConfigToAutoSelect(newState); - this.tryReconnect(); + private async onBoardsConfigEdit( + params?: EditBoardsConfigActionParams + ): Promise { + const boardsConfig = await this.commandService.executeCommand< + BoardsConfig | undefined + >('arduino-open-boards-dialog', params); + if (boardsConfig) { + this.update({ boardsConfig }, 'dialog'); + } } - private deriveBoardConfigToAutoSelect( - newState: AttachedBoardsChangeEvent['newState'] + private update( + params: RefreshBoardListParams, + reason?: UpdateBoardsConfigReason ): void { - if (!this.lastBoardsConfigOnUpload || !this.lastAvailablePortsOnUpload) { - this.boardConfigToAutoSelect = undefined; + const { boardsConfig } = params; + if (!boardsConfig) { return; } - - const oldPorts = this.lastAvailablePortsOnUpload; - const { ports: newPorts, boards: newBoards } = newState; - - const appearedPorts = - oldPorts.length > 0 - ? newPorts.filter((newPort: Port) => - oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort)) - ) - : newPorts; - - for (const port of appearedPorts) { - const boardOnAppearedPort = newBoards.find((board: Board) => - Port.sameAs(board.port, port) + const { selectedBoard, selectedPort } = boardsConfig; + if (selectedBoard && selectedPort) { + this.updateConfig( + { + selectedBoard, + selectedPort, + }, + reason ); + } else if (selectedBoard) { + this.updateBoard(selectedBoard); + } else if (selectedPort) { + this.updatePort(selectedPort); + } + } - const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload; - - if (boardOnAppearedPort && lastBoardsConfigOnUpload.selectedBoard) { - const boardIsSameHardware = Board.hardwareIdEquals( - boardOnAppearedPort, - lastBoardsConfigOnUpload.selectedBoard - ); - - const boardIsSameFqbn = Board.sameAs( - boardOnAppearedPort, - lastBoardsConfigOnUpload.selectedBoard + updateConfig( + boardsConfig: Defined, + reason?: UpdateBoardsConfigReason + ): boolean { + const selectedBoard = boardsConfig.selectedBoard; + const previousSelectedBoard = this._boardsConfig.selectedBoard; + const selectedPort = boardsConfig.selectedPort; + const previousSelectedPort = this._boardsConfig.selectedPort; + + if (selectedBoard.fqbn && (reason === 'toolbar' || reason === 'inherit')) { + const [, , , ...rest] = selectedBoard.fqbn.split(':'); + if (rest && rest.length) { + console.log( + typeof this.boardsDataStore, + 'TODO: save update data store if reason is toolbar and the FQBN has options' ); - - if (!boardIsSameHardware && !boardIsSameFqbn) continue; - - let boardToAutoSelect = boardOnAppearedPort; - if (boardIsSameHardware && !boardIsSameFqbn) { - const { name, fqbn } = lastBoardsConfigOnUpload.selectedBoard; - - boardToAutoSelect = { - ...boardToAutoSelect, - name: - boardToAutoSelect.name === Unknown || !boardToAutoSelect.name - ? name - : boardToAutoSelect.name, - fqbn: boardToAutoSelect.fqbn || fqbn, - }; - } - - this.clearBoardDiscoverySnapshot(); - - this.boardConfigToAutoSelect = { - selectedBoard: boardToAutoSelect, - selectedPort: port, - }; - return; } } - } + if ( + previousSelectedBoard !== undefined && + boardIdentifierEquals(previousSelectedBoard, selectedBoard) + ) { + // the board did not change, fall back to port + return this.updatePort(selectedPort); + } - protected notifyAttachedBoardsChanged( - event: AttachedBoardsChangeEvent - ): void { - if (!AttachedBoardsChangeEvent.isEmpty(event)) { - this.logger.info('Attached boards and available ports changed:'); - this.logger.info(AttachedBoardsChangeEvent.toString(event)); - this.logger.info('------------------------------------------'); + if (selectedPort && selectedBoard) { + this._boardListHistory[Port.keyOf(selectedPort)] = selectedBoard; + } + if ( + previousSelectedPort !== undefined && + portIdentifierEquals(previousSelectedPort, selectedPort) + ) { + // the port did not change, fall back to board + return this.updateBoard(selectedBoard); } - this._attachedBoards = event.newState.boards; - const oldState = this._availablePorts.slice(); - this._availablePorts = event.newState.ports; - this.onAvailablePortsChangedEmitter.fire({ - newState: this._availablePorts.slice(), - oldState, - }); - this.reconcileAvailableBoards().then(() => { - const { uploadInProgress } = event; - // avoid attempting "auto-selection" while an - // upload is in progress - if (!uploadInProgress) { - this.attemptAutoSelect(event.newState); - } + this._boardsConfig.selectedBoard = selectedBoard; + this._boardsConfig.selectedPort = selectedPort; + this.boardsConfigDidChangeEmitter.fire({ + selectedBoard, + previousSelectedBoard, + previousSelectedPort, + selectedPort, }); + this.refreshBoardList(); + this.saveState(); + return true; } - protected notifyPlatformInstalled(event: { item: BoardsPackage }): void { - this.logger.info('Boards package installed: ', JSON.stringify(event)); - const { selectedBoard } = this.boardsConfig; - const { installedVersion, id } = event.item; - if (selectedBoard) { - const installedBoard = event.item.boards.find( - ({ name }) => name === selectedBoard.name - ); - if ( - installedBoard && - (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn) - ) { - this.logger.info( - `Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]` - ); - this.boardsConfig = { - ...this.boardsConfig, - selectedBoard: installedBoard, - }; - return; - } - // The board name can change after install. - // This logic handles it "gracefully" by unselecting the board, so that we can avoid no FQBN is set error. - // https://github.com/arduino/arduino-cli/issues/620 - // https://github.com/arduino/arduino-pro-ide/issues/374 - if ( - BoardWithPackage.is(selectedBoard) && - selectedBoard.packageId === event.item.id && - !installedBoard - ) { - const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); - this.messageService - .warn( - nls.localize( - 'arduino/board/couldNotFindPreviouslySelected', - "Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?", - selectedBoard.name, - event.item.name - ), - nls.localize('arduino/board/reselectLater', 'Reselect later'), - yes - ) - .then(async (answer) => { - if (answer === yes) { - this.commandService.executeCommand( - 'arduino-open-boards-dialog', - selectedBoard.name - ); - } - }); - this.boardsConfig = {}; - return; - } - // Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954 - // E.g: install `adafruit:avr`, then select `adafruit:avr:adafruit32u4` board, and finally install the required `arduino:avr` - this.boardsConfig = this.boardsConfig; + updateBoard(selectedBoard: BoardIdentifier | undefined): boolean { + const previousSelectedBoard = this._boardsConfig.selectedBoard; + if (boardIdentifierEquals(previousSelectedBoard, selectedBoard)) { + // NOOP if they're the same + return false; } + this._boardsConfig.selectedBoard = selectedBoard; + this.boardsConfigDidChangeEmitter.fire({ + previousSelectedBoard, + selectedBoard, + }); + this.refreshBoardList(); + this.saveState(); + return true; } - protected notifyPlatformUninstalled(event: { item: BoardsPackage }): void { - this.logger.info('Boards package uninstalled: ', JSON.stringify(event)); - const { selectedBoard } = this.boardsConfig; - if (selectedBoard && selectedBoard.fqbn) { - const uninstalledBoard = event.item.boards.find( - ({ name }) => name === selectedBoard.name - ); - if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) { - // We should not unset the FQBN, if the selected board is an attached, recognized board. - // Attach Uno and install AVR, select Uno. Uninstall the AVR core while Uno is selected. We do not want to discard the FQBN of the Uno board. - // Dev note: We cannot assume the `selectedBoard` is a type of `AvailableBoard`. - // When the user selects an `AvailableBoard` it works, but between app start/stops, - // it is just a FQBN, so we need to find the `selected` board among the `AvailableBoards` - const selectedAvailableBoard = AvailableBoard.is(selectedBoard) - ? selectedBoard - : this._availableBoards.find( - (availableBoard) => - Board.hardwareIdEquals(availableBoard, selectedBoard) || - Board.sameAs(availableBoard, selectedBoard) - ); - if ( - selectedAvailableBoard && - selectedAvailableBoard.selected && - selectedAvailableBoard.state === AvailableBoard.State.recognized - ) { - return; - } - this.logger.info( - `Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.` - ); - const selectedBoardWithoutFqbn = { - name: selectedBoard.name, - // No FQBN - }; - this.boardsConfig = { - ...this.boardsConfig, - selectedBoard: selectedBoardWithoutFqbn, - }; - } + updatePort(selectedPort: PortIdentifier | undefined): boolean { + const selectedBoard = this._boardsConfig.selectedBoard; + const previousSelectedPort = this._boardsConfig.selectedPort; + if (selectedPort && selectedBoard) { + this._boardListHistory[Port.keyOf(selectedPort)] = selectedBoard; } - } - - protected tryReconnect(): boolean { - if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) { - // ** Reconnect to a board unplugged from, and plugged back into the same port - for (const board of this.availableBoards.filter( - ({ state }) => state !== AvailableBoard.State.incomplete - )) { - if ( - Board.hardwareIdEquals( - this.latestValidBoardsConfig.selectedBoard, - board - ) - ) { - const { name, fqbn } = this.latestValidBoardsConfig.selectedBoard; - this.boardsConfig = { - selectedBoard: { - name: board.name === Unknown || !board.name ? name : board.name, - fqbn: board.fqbn || fqbn, - port: board.port, - }, - selectedPort: board.port, - }; - return true; - } - - if ( - this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn && - this.latestValidBoardsConfig.selectedBoard.name === board.name && - Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port) - ) { - this.boardsConfig = this.latestValidBoardsConfig; - return true; - } - } - // ** - - // ** Reconnect to a board whose port changed due to an upload - if (!this.boardConfigToAutoSelect) return false; - - this.boardsConfig = this.boardConfigToAutoSelect; - this.boardConfigToAutoSelect = undefined; - return true; - // ** + this._boardsConfig.selectedPort = selectedPort; + if (portIdentifierEquals(previousSelectedPort, selectedPort)) { + // NOOP if they're the same + return false; } - return false; + this.boardsConfigDidChangeEmitter.fire({ + previousSelectedPort, + selectedPort, + }); + this.refreshBoardList(); + this.saveState(); + return true; } - set boardsConfig(config: BoardsConfig.Config) { - this.setBoardsConfig(config); - this.saveState().finally(() => - this.reconcileAvailableBoards().finally(() => - this.onBoardsConfigChangedEmitter.fire(this._boardsConfig) - ) - ); + get ready(): Promise { + return this._ready.promise; } - get boardsConfig(): BoardsConfig.Config { + get boardsConfig(): BoardsConfig { return this._boardsConfig; } - protected setBoardsConfig(config: BoardsConfig.Config): void { - this.logger.debug('Board config changed: ', JSON.stringify(config)); - this._boardsConfig = config; - this.latestBoardsConfig = this._boardsConfig; - if (this.canUploadTo(this._boardsConfig)) { - this.latestValidBoardsConfig = this._boardsConfig; - } + get boardList(): BoardListUI { + return this._boardList; + } + + get detectedPorts(): DetectedPorts { + return this._detectedPorts; } async searchBoards({ query, - cores, }: { query?: string; cores?: string[]; @@ -459,293 +485,70 @@ export class BoardsServiceProvider return await this.boardsService.getBoardUserFields({ fqbn, protocol }); } - /** - * `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`. - */ - canVerify( - config: BoardsConfig.Config | undefined = this.boardsConfig, - options: { silent: boolean } = { silent: true } - ): config is BoardsConfig.Config & { selectedBoard: Board } { - if (!config) { - return false; - } - - if (!config.selectedBoard) { - if (!options.silent) { - this.messageService.warn( - nls.localize('arduino/board/noneSelected', 'No boards selected.'), - { - timeout: 3000, - } - ); - } - return false; - } - - return true; - } - - /** - * `true` if `canVerify`, the board has an FQBN and the `config.selectedPort` is also set, hence can upload to board. Otherwise, `false`. - */ - canUploadTo( - config: BoardsConfig.Config | undefined = this.boardsConfig, - options: { silent: boolean } = { silent: true } - ): config is RecursiveRequired { - if (!this.canVerify(config, options)) { - return false; - } - - const { name } = config.selectedBoard; - if (!config.selectedPort) { - if (!options.silent) { - this.messageService.warn( - nls.localize( - 'arduino/board/noPortsSelected', - "No ports selected for board: '{0}'.", - name - ), - { - timeout: 3000, - } - ); - } - return false; - } - - if (!config.selectedBoard.fqbn) { - if (!options.silent) { - this.messageService.warn( - nls.localize( - 'arduino/board/noFQBN', - 'The FQBN is not available for the selected board "{0}". Do you have the corresponding core installed?', - name - ), - { timeout: 3000 } - ); - } - return false; - } - - return true; - } - - get availableBoards(): AvailableBoard[] { - return this._availableBoards; - } - - /** - * @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload. - * - * See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236. - */ - // TODO: remove this API and fix the selected board config store/restore correctly. - get availablePorts(): Port[] { - return this._availablePorts.slice(); - } - - async waitUntilAvailable( - what: Board & { port: Port }, - timeout?: number - ): Promise { - const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) => - haystack.find( - (board) => - Board.equals(needle, board) && Port.sameAs(needle.port, board.port) - ); - const timeoutTask = - !!timeout && timeout > 0 - ? new Promise((_, reject) => - setTimeout( - () => reject(new Error(`Timeout after ${timeout} ms.`)), - timeout - ) - ) - : new Promise(() => { - /* never */ - }); - const waitUntilTask = new Promise((resolve) => { - let candidate = find(what, this.availableBoards); - if (candidate) { - resolve(); - return; - } - const disposable = this.onAvailableBoardsChanged((availableBoards) => { - candidate = find(what, availableBoards); - if (candidate) { - disposable.dispose(); - resolve(); - } - }); - }); - return await Promise.race([waitUntilTask, timeoutTask]); - } - - protected async reconcileAvailableBoards(): Promise { - const availablePorts = this._availablePorts; - // Unset the port on the user's config, if it is not available anymore. - if ( - this.boardsConfig.selectedPort && - !availablePorts.some((port) => - Port.sameAs(port, this.boardsConfig.selectedPort) - ) - ) { - this.setBoardsConfig({ - selectedBoard: this.boardsConfig.selectedBoard, - selectedPort: undefined, - }); - this.onBoardsConfigChangedEmitter.fire(this._boardsConfig); - } - const boardsConfig = this.boardsConfig; - const currentAvailableBoards = this._availableBoards; - const availableBoards: AvailableBoard[] = []; - const attachedBoards = this._attachedBoards.filter(({ port }) => !!port); - const availableBoardPorts = availablePorts.filter( - Port.visiblePorts(attachedBoards) - ); - - for (const boardPort of availableBoardPorts) { - const board = attachedBoards.find(({ port }) => - Port.sameAs(boardPort, port) - ); - // "board" will always be falsey for - // port that was originally mapped - // to unknown board and then selected - // manually by user - - const lastSelectedBoard = await this.getLastSelectedBoardOnPort( - boardPort - ); - - let availableBoard = {} as AvailableBoard; - if (board) { - availableBoard = { - ...board, - state: AvailableBoard.State.recognized, - selected: BoardsConfig.Config.sameAs(boardsConfig, board), - port: boardPort, - }; - } else if (lastSelectedBoard) { - // If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623 - // We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836 - availableBoard = { - ...lastSelectedBoard, - state: AvailableBoard.State.guessed, - selected: - BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard) && - Port.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection - port: boardPort, - }; - } else { - availableBoard = { - name: Unknown, - port: boardPort, - state: AvailableBoard.State.incomplete, - }; - } - availableBoards.push(availableBoard); - } - - if ( - boardsConfig.selectedBoard && - availableBoards.every(({ selected }) => !selected) - ) { - let port = boardsConfig.selectedPort; - // If the selected board has the same port of an unknown board - // that is already in availableBoards we might get a duplicate port. - // So we remove the one already in the array and add the selected one. - const found = availableBoards.findIndex( - (board) => board.port?.address === boardsConfig.selectedPort?.address - ); - if (found >= 0) { - // get the "Unknown board port" that we will substitute, - // then we can include it in the "availableBoard object" - // pushed below; to ensure addressLabel is included - port = availableBoards[found].port; - availableBoards.splice(found, 1); - } - availableBoards.push({ - ...boardsConfig.selectedBoard, - port, - selected: true, - state: AvailableBoard.State.incomplete, - }); - } - - availableBoards.sort(AvailableBoard.compare); - - let hasChanged = availableBoards.length !== currentAvailableBoards.length; - for (let i = 0; !hasChanged && i < availableBoards.length; i++) { - const [left, right] = [availableBoards[i], currentAvailableBoards[i]]; - hasChanged = - left.fqbn !== right.fqbn || - !!AvailableBoard.compare(left, right) || - left.selected !== right.selected; - } - if (hasChanged) { - this._availableBoards = availableBoards; - this.onAvailableBoardsChangedEmitter.fire(this._availableBoards); - } - } - - protected async getLastSelectedBoardOnPort( - port: Port - ): Promise { - const key = this.getLastSelectedBoardOnPortKey(port); - return this.getData(key); - } - - protected async saveState(): Promise { - // We save the port with the selected board name/FQBN, to be able to guess a better board name. - // Required when the attached board belongs to a 3rd party boards package, and neither the name, nor - // the FQBN can be retrieved with a `board list` command. - // https://github.com/arduino/arduino-cli/issues/623 + private async saveState(): Promise { const { selectedBoard, selectedPort } = this.boardsConfig; - if (selectedBoard && selectedPort) { - const key = this.getLastSelectedBoardOnPortKey(selectedPort); - await this.setData(key, selectedBoard); - } await Promise.all([ - this.setData('latest-valid-boards-config', this.latestValidBoardsConfig), - this.setData('latest-boards-config', this.latestBoardsConfig), + this.setData( + selectedBoardStorageKey, + selectedBoard + ? // to make sure no other properties of the board object are persisted + JSON.stringify({ + name: selectedBoard.name, + fqbn: selectedBoard.fqbn, + }) + : undefined + ), + this.setData( + selectedPortStorageKey, + selectedPort + ? // to make sure no other properties are persisted from the port object + JSON.stringify({ + protocol: selectedPort.protocol, + address: selectedPort.address, + }) + : undefined + ), + this.setData( + boardListHistoryStorageKey, + JSON.stringify(this._boardListHistory) + ), ]); } - protected getLastSelectedBoardOnPortKey(port: Port | string): string { - // TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`. - return `last-selected-board-on-port:${ - typeof port === 'string' ? port : port.address - }`; + private async restoreState(): Promise< + Readonly & { boardListHistory: BoardListHistory | undefined } + > { + const [maybeSelectedBoard, maybeSelectedPort, maybeBoardHistory] = + await Promise.all([ + this.getData(selectedBoardStorageKey), + this.getData(selectedPortStorageKey), + this.getData(boardListHistoryStorageKey), + ]); + const selectedBoard = this.tryParse(maybeSelectedBoard, isBoardIdentifier); + const selectedPort = this.tryParse(maybeSelectedPort, isPortIdentifier); + const boardListHistory = this.tryParse( + maybeBoardHistory, + isBoardListHistory + ); + return { selectedBoard, selectedPort, boardListHistory }; } - protected async loadState(): Promise { - const storedLatestValidBoardsConfig = await this.getData< - RecursiveRequired - >('latest-valid-boards-config'); - if (storedLatestValidBoardsConfig) { - this.latestValidBoardsConfig = storedLatestValidBoardsConfig; - if (this.canUploadTo(this.latestValidBoardsConfig)) { - this.boardsConfig = this.latestValidBoardsConfig; - } - } else { - // If we could not restore the latest valid config, try to restore something, the board at least. - let storedLatestBoardsConfig = await this.getData< - BoardsConfig.Config | undefined - >('latest-boards-config'); - // Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives. - if (!storedLatestBoardsConfig) { - storedLatestBoardsConfig = await Promise.race([ - this.inheritedConfig.promise, - new Promise((resolve) => - setTimeout(() => resolve(undefined), 2_000) - ), - ]); - } - if (storedLatestBoardsConfig) { - this.latestBoardsConfig = storedLatestBoardsConfig; - this.boardsConfig = this.latestBoardsConfig; + private tryParse( + raw: string | undefined, + typeGuard: (object: unknown) => object is T + ): T | undefined { + if (!raw) { + return undefined; + } + try { + const object = JSON.parse(raw); + if (typeGuard(object)) { + return object; } + } catch { + this.logger.error(`Failed to parse raw: '${raw}'`); } + return undefined; } private setData(key: string, value: T): Promise { @@ -762,15 +565,6 @@ export class BoardsServiceProvider key ); } - - tasks(): StartupTask[] { - return [ - { - command: USE_INHERITED_CONFIG.id, - args: [this.boardsConfig], - }, - ]; - } } /** @@ -787,77 +581,26 @@ const USE_INHERITED_CONFIG: Command = { id: 'arduino-use-inherited-boards-config', }; -/** - * Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI. - * An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`. - * If it has the selected board and a associated port, it can be used for `upload`. We render an available board for the user - * when it has the `port` set. - */ -export interface AvailableBoard extends Board { - readonly state: AvailableBoard.State; - readonly selected?: boolean; - readonly port?: Port; -} - -export namespace AvailableBoard { - export enum State { - /** - * Retrieved from the CLI via the `board list` command. - */ - 'recognized', - /** - * Guessed the name/FQBN of the board from the available board ports (3rd party). - */ - 'guessed', - /** - * We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet. - */ - 'incomplete', - } - - export function is(board: any): board is AvailableBoard { - return Board.is(board) && 'state' in board; - } +const DUMP_BOARD_LIST: Command = { + id: 'arduino-dump-board-list', + label: nls.localize('arduino/developer/dumpBoardList', 'Dump the Board List'), + category: 'Developer (Arduino)', +}; - export function hasPort( - board: AvailableBoard - ): board is AvailableBoard & { port: Port } { - return !!board.port; - } +const CLEAR_BOARD_LIST_HISTORY: Command = { + id: 'arduino-clear-board-list-history', + label: nls.localize( + 'arduino/developer/clearBoardList', + 'Clear the Board List History' + ), + category: 'Developer (Arduino)', +}; - // Available boards must be sorted in this order: - // 1. Serial with recognized boards - // 2. Serial with guessed boards - // 3. Serial with incomplete boards - // 4. Network with recognized boards - // 5. Other protocols with recognized boards - export const compare = (left: AvailableBoard, right: AvailableBoard) => { - if (left.port?.protocol === 'serial' && right.port?.protocol !== 'serial') { - return -1; - } else if ( - left.port?.protocol !== 'serial' && - right.port?.protocol === 'serial' - ) { - return 1; - } else if ( - left.port?.protocol === 'network' && - right.port?.protocol !== 'network' - ) { - return -1; - } else if ( - left.port?.protocol !== 'network' && - right.port?.protocol === 'network' - ) { - return 1; - } else if (left.port?.protocol === right.port?.protocol) { - // We show all ports, including those that have guessed - // or unrecognized boards, so we must sort those too. - if (left.state < right.state) { - return -1; - } else if (left.state > right.state) { - return 1; - } - } - return naturalCompare(left.port?.address!, right.port?.address!); - }; -} +const CLEAR_BOARDS_CONFIG: Command = { + id: 'arduino-clear-boards-config', + label: nls.localize( + 'arduino/developer/clearBoardsConfig', + 'Clear the Board and Port Selection' + ), + category: 'Developer (Arduino)', +}; diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx index 93a15a3c5..a49617f01 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -1,16 +1,27 @@ -import * as React from '@theia/core/shared/react'; -import * as ReactDOM from '@theia/core/shared/react-dom'; -import { CommandRegistry } from '@theia/core/lib/common/command'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { Port } from '../../common/protocol'; -import { OpenBoardsConfig } from '../contributions/open-boards-config'; +import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar'; +import { codicon } from '@theia/core/lib/browser/widgets/widget'; +import type { CommandRegistry } from '@theia/core/lib/common/command'; import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { nls } from '@theia/core/lib/common/nls'; +import React from '@theia/core/shared/react'; +import ReactDOM from '@theia/core/shared/react-dom'; +import classNames from 'classnames'; +import { unknownBoard } from '../../common/protocol'; +import { + BoardListItem, + getInferredBoardOrBoard, + InferredBoardListItem, + isInferredBoardListItem, +} from '../../common/protocol/board-list'; +import { + BoardListUI, BoardsServiceProvider, - AvailableBoard, + EditBoardsConfigAction, + SelectBoardsConfigAction, } from './boards-service-provider'; -import { nls } from '@theia/core/lib/common'; -import classNames from 'classnames'; -import { BoardsConfig } from './boards-config'; export interface BoardsDropDownListCoords { readonly top: number; @@ -22,18 +33,17 @@ export interface BoardsDropDownListCoords { export namespace BoardsDropDown { export interface Props { readonly coords: BoardsDropDownListCoords | 'hidden'; - readonly items: Array void; port: Port }>; + readonly boardList: BoardListUI; readonly openBoardsConfig: () => void; } } -export class BoardsDropDown extends React.Component { - protected dropdownElement: HTMLElement; +export class BoardListDropDown extends React.Component { + private dropdownElement: HTMLElement; private listRef: React.RefObject; constructor(props: BoardsDropDown.Props) { super(props); - this.listRef = React.createRef(); let list = document.getElementById('boards-dropdown-container'); if (!list) { @@ -51,11 +61,14 @@ export class BoardsDropDown extends React.Component { } override render(): React.ReactNode { - return ReactDOM.createPortal(this.renderNode(), this.dropdownElement); + return ReactDOM.createPortal( + this.renderBoardListItems(), + this.dropdownElement + ); } - protected renderNode(): React.ReactNode { - const { coords, items } = this.props; + private renderBoardListItems(): React.ReactNode { + const { coords, boardList } = this.props; if (coords === 'hidden') { return ''; } @@ -74,14 +87,14 @@ export class BoardsDropDown extends React.Component { tabIndex={0} >
- {items - .map(({ name, port, selected, onClick }) => ({ - boardLabel: name, - port, - selected, - onClick, - })) - .map(this.renderItem)} + {boardList.map((item, index) => + this.renderBoardListItem({ + item, + selected: index === boardList.selectedIndex, + onSelect: boardList.onSelect, + onEdit: boardList.onEdit, + }) + )}
{ ); } - protected renderItem({ - boardLabel, - port, + private renderBoardListItem({ + item, selected, - onClick, + onSelect, + onEdit, }: { - boardLabel: string; - port: Port; - selected?: boolean; - onClick: () => void; + item: BoardListItem; + selected: boolean; + onSelect: SelectBoardsConfigAction; + onEdit: EditBoardsConfigAction; }): React.ReactNode { - const protocolIcon = iconNameFromProtocol(port.protocol); + const port = item.port; + const board = getInferredBoardOrBoard(item); + const boardLabel = board?.name ?? unknownBoard; + const boardFqbn = board?.fqbn; + const onDefaultAction = () => { + if (board) { + onSelect({ selectedBoard: board, selectedPort: port }); + } else { + onEdit({ selectedPort: port, query: { action: 'clear-if-not-empty' } }); + } + }; const onKeyUp = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - onClick(); + onDefaultAction(); } }; - return (
@@ -127,21 +149,82 @@ export class BoardsDropDown extends React.Component { className={classNames( 'arduino-boards-dropdown-item--protocol', 'fa', - protocolIcon + iconNameFromProtocol(port.protocol) )} />
-
- {boardLabel} +
+
+ {boardLabel} +
{port.addressLabel}
- {selected ?
: ''} + {isInferredBoardListItem(item) && + this.renderActions(item, onSelect, onEdit)} +
+ ); + } + + private renderActions( + inferredItem: InferredBoardListItem, + onRevert: SelectBoardsConfigAction, + onEdit: EditBoardsConfigAction + ): React.ReactNode { + const { port } = inferredItem; + const edit = ( +
{ + event.preventDefault(); + event.stopPropagation(); + onEdit({ + query: inferredItem.inferredBoard.name, + selectedBoard: inferredItem.inferredBoard, + selectedPort: port, + }); + }} + /> + ); + const revert = + inferredItem.type === 'board-overridden' ? ( +
{ + event.preventDefault(); + event.stopPropagation(); + onRevert({ selectedBoard: inferredItem.board, selectedPort: port }); + }} + /> + ) : undefined; + return ( +
+
+ {edit} +
+
+ {revert} +
); } @@ -153,26 +236,27 @@ export class BoardsToolBarItem extends React.Component< > { static TOOLBAR_ID: 'boards-toolbar'; - protected readonly toDispose: DisposableCollection = - new DisposableCollection(); + private readonly toDispose: DisposableCollection; constructor(props: BoardsToolBarItem.Props) { super(props); - - const { availableBoards } = props.boardsServiceProvider; + const { boardList } = props.boardsServiceProvider; this.state = { - availableBoards, + boardList, coords: 'hidden', }; - - document.addEventListener('click', () => { - this.setState({ coords: 'hidden' }); - }); + const listener = () => this.setState({ coords: 'hidden' }); + document.addEventListener('click', listener); + this.toDispose = new DisposableCollection( + Disposable.create(() => document.removeEventListener('click', listener)) + ); } override componentDidMount(): void { - this.props.boardsServiceProvider.onAvailableBoardsChanged( - (availableBoards) => this.setState({ availableBoards }) + this.toDispose.push( + this.props.boardsServiceProvider.onBoardListDidChange((boardList) => + this.setState({ boardList }) + ) ); } @@ -180,7 +264,7 @@ export class BoardsToolBarItem extends React.Component< this.toDispose.dispose(); } - protected readonly show = (event: React.MouseEvent): void => { + private readonly show = (event: React.MouseEvent): void => { const { currentTarget: element } = event; if (element instanceof HTMLElement) { if (this.state.coords === 'hidden') { @@ -201,17 +285,31 @@ export class BoardsToolBarItem extends React.Component< event.nativeEvent.stopImmediatePropagation(); }; + private readonly hide = () => { + this.setState({ coords: 'hidden' }); + }; + override render(): React.ReactNode { - const { coords, availableBoards } = this.state; - const { selectedBoard, selectedPort } = - this.props.boardsServiceProvider.boardsConfig; + const { coords, boardList } = this.state; + const { selectedBoard, selectedPort } = boardList.boardsConfig; const boardLabel = selectedBoard?.name || nls.localize('arduino/board/selectBoard', 'Select Board'); - const selectedPortLabel = portLabel(selectedPort?.address); + const boardFqbn = selectedBoard?.fqbn; + const selectedItem: BoardListItem | undefined = + boardList[boardList.selectedIndex]; + let tooltip = `${boardLabel}${boardFqbn ? ` (${boardFqbn})` : ''}${ + selectedPort ? `\n${selectedPort.address}` : '' + }`; + if (selectedPort && !selectedItem) { + tooltip += ` ${nls.localize( + 'arduino/common/notConnected', + '[not connected]' + )}`; + } - const isConnected = Boolean(selectedBoard && selectedPort); + const isConnected = boardList.selectedIndex >= 0; const protocolIcon = isConnected ? iconNameFromProtocol(selectedPort?.protocol || '') : null; @@ -220,12 +318,21 @@ export class BoardsToolBarItem extends React.Component< 'fa', protocolIcon ); - + const originalOnSelect = boardList.onSelect; + boardList['onSelect'] = (params) => { + this.hide(); + return originalOnSelect.bind(boardList)(params); + }; + const originalOnEdit = boardList.onEdit; + boardList['onEdit'] = (params) => { + this.hide(); + return originalOnEdit.bind(boardList)(params); + }; return (
{protocolIcon &&
} @@ -241,50 +348,16 @@ export class BoardsToolBarItem extends React.Component<
- ({ - ...board, - onClick: () => { - if (!board.fqbn) { - const previousBoardConfig = - this.props.boardsServiceProvider.boardsConfig; - this.props.boardsServiceProvider.boardsConfig = { - selectedPort: board.port, - }; - this.openDialog(previousBoardConfig); - } else { - this.props.boardsServiceProvider.boardsConfig = { - selectedBoard: board, - selectedPort: board.port, - }; - } - this.setState({ coords: 'hidden' }); - }, - }))} - openBoardsConfig={this.openDialog} - > + boardList={boardList} + openBoardsConfig={() => + boardList.onEdit({ query: { action: 'clear-if-not-empty' } }) + } + /> ); } - - protected openDialog = async ( - previousBoardConfig?: BoardsConfig.Config - ): Promise => { - const selectedBoardConfig = - await this.props.commands.executeCommand( - OpenBoardsConfig.Commands.OPEN_DIALOG.id - ); - if ( - previousBoardConfig && - (!selectedBoardConfig?.selectedPort || - !selectedBoardConfig?.selectedBoard) - ) { - this.props.boardsServiceProvider.boardsConfig = previousBoardConfig; - } - }; } export namespace BoardsToolBarItem { export interface Props { @@ -293,7 +366,7 @@ export namespace BoardsToolBarItem { } export interface State { - availableBoards: AvailableBoard[]; + boardList: BoardListUI; coords: BoardsDropDownListCoords | 'hidden'; } } @@ -304,19 +377,10 @@ function iconNameFromProtocol(protocol: string): string { return 'fa-arduino-technology-usb'; case 'network': return 'fa-arduino-technology-connection'; - /* - Bluetooth ports are not listed yet from the CLI; - Not sure about the naming ('bluetooth'); make sure it's correct before uncommenting the following lines - */ - // case 'bluetooth': - // return 'fa-arduino-technology-bluetooth'; + // it is fine to assign dedicated icons to the protocols used by the official boards, + // but other than that it is best to avoid implementing any special handling + // for specific protocols in the IDE codebase. default: return 'fa-arduino-technology-3dimensionscube'; } } - -function portLabel(portName?: string): string { - return portName - ? nls.localize('arduino/board/portLabel', 'Port: {0}', portName) - : nls.localize('arduino/board/disconnected', 'Disconnected'); -} diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 3e7558c83..c90835c5c 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -1,57 +1,58 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; -import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { - DisposableCollection, Disposable, + DisposableCollection, } from '@theia/core/lib/common/disposable'; -import { BoardsConfig } from '../boards/boards-config'; +import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry'; +import type { MenuPath } from '@theia/core/lib/common/menu/menu-types'; +import { nls } from '@theia/core/lib/common/nls'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { MainMenuManager } from '../../common/main-menu-manager'; +import { + BoardsService, + BoardWithPackage, + createPlatformIdentifier, + getBoardInfo, + InstalledBoardWithPackage, + platformIdentifierEquals, + Port, + serializePlatformIdentifier, +} from '../../common/protocol'; +import type { BoardList } from '../../common/protocol/board-list'; import { BoardsListWidget } from '../boards/boards-list-widget'; -import { NotificationCenter } from '../notification-center'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu, } from '../menu/arduino-menus'; -import { - BoardsService, - InstalledBoardWithPackage, - AvailablePorts, - Port, - getBoardInfo, -} from '../../common/protocol'; -import { SketchContribution, Command, CommandRegistry } from './contribution'; -import { nls } from '@theia/core/lib/common'; +import { NotificationCenter } from '../notification-center'; +import { Command, CommandRegistry, SketchContribution } from './contribution'; @injectable() export class BoardSelection extends SketchContribution { @inject(CommandRegistry) - protected readonly commandRegistry: CommandRegistry; - + private readonly commandRegistry: CommandRegistry; @inject(MainMenuManager) - protected readonly mainMenuManager: MainMenuManager; - + private readonly mainMenuManager: MainMenuManager; @inject(MenuModelRegistry) - protected readonly menuModelRegistry: MenuModelRegistry; - + private readonly menuModelRegistry: MenuModelRegistry; @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; - + private readonly notificationCenter: NotificationCenter; @inject(BoardsService) - protected readonly boardsService: BoardsService; - + private readonly boardsService: BoardsService; @inject(BoardsServiceProvider) - protected readonly boardsServiceProvider: BoardsServiceProvider; + private readonly boardsServiceProvider: BoardsServiceProvider; - protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection(); + private readonly toDisposeBeforeMenuRebuild = new DisposableCollection(); + // do not query installed platforms on every change + private _installedBoards: Deferred | undefined; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, { execute: async () => { const boardInfo = await getBoardInfo( - this.boardsServiceProvider.boardsConfig.selectedPort, - this.boardsService.getState() + this.boardsServiceProvider.boardList ); if (typeof boardInfo === 'string') { this.messageService.info(boardInfo); @@ -76,34 +77,35 @@ SN: ${SN} } override onStart(): void { - this.notificationCenter.onPlatformDidInstall(() => this.updateMenus()); - this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus()); - this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus()); - this.boardsServiceProvider.onAvailableBoardsChanged(() => - this.updateMenus() - ); - this.boardsServiceProvider.onAvailablePortsChanged(() => - this.updateMenus() + this.notificationCenter.onPlatformDidInstall(() => this.updateMenus(true)); + this.notificationCenter.onPlatformDidUninstall(() => + this.updateMenus(true) ); + this.boardsServiceProvider.onBoardListDidChange(() => this.updateMenus()); } override async onReady(): Promise { this.updateMenus(); } - protected async updateMenus(): Promise { - const [installedBoards, availablePorts, config] = await Promise.all([ - this.installedBoards(), - this.boardsService.getState(), - this.boardsServiceProvider.boardsConfig, - ]); - this.rebuildMenus(installedBoards, availablePorts, config); + private async updateMenus(discardCache = false): Promise { + if (discardCache) { + this._installedBoards?.reject(); + this._installedBoards = undefined; + } + if (!this._installedBoards) { + this._installedBoards = new Deferred(); + this.installedBoards().then((installedBoards) => + this._installedBoards?.resolve(installedBoards) + ); + } + const installedBoards = await this._installedBoards.promise; + this.rebuildMenus(installedBoards, this.boardsServiceProvider.boardList); } - protected rebuildMenus( + private rebuildMenus( installedBoards: InstalledBoardWithPackage[], - availablePorts: AvailablePorts, - config: BoardsConfig.Config + boardList: BoardList ): void { this.toDisposeBeforeMenuRebuild.dispose(); @@ -112,7 +114,8 @@ SN: ${SN} ...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards', ]; - const boardsSubmenuLabel = config.selectedBoard?.name; + const { selectedBoard, selectedPort } = boardList.boardsConfig; + const boardsSubmenuLabel = selectedBoard?.name; // Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index. // The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order. this.menuModelRegistry.registerSubmenu( @@ -132,7 +135,7 @@ SN: ${SN} // Ports submenu const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU; - const portsSubmenuLabel = config.selectedPort?.address; + const portsSubmenuLabel = selectedPort?.address; this.menuModelRegistry.registerSubmenu( portsSubmenuPath, nls.localize( @@ -171,69 +174,116 @@ SN: ${SN} label: `${BoardsListWidget.WIDGET_LABEL}...`, }); - // Installed boards - installedBoards.forEach((board, index) => { - const { packageId, packageName, fqbn, name, manuallyInstalled } = board; + const selectedBoardPlatformId = selectedBoard + ? createPlatformIdentifier(selectedBoard) + : undefined; + + // Keys are the vendor IDs + type BoardsPerVendor = Record; + // Group boards by their platform names. The keys are the platform names as menu labels. + // If there is a platform name (menu label) collision, refine the menu label with the vendor ID. + const groupedBoards = new Map(); + for (const board of installedBoards) { + const { packageId, packageName } = board; + const { vendorId } = packageId; + let boardsPerPackageName = groupedBoards.get(packageName); + if (!boardsPerPackageName) { + boardsPerPackageName = {} as BoardsPerVendor; + groupedBoards.set(packageName, boardsPerPackageName); + } + let boardPerVendor: BoardWithPackage[] | undefined = + boardsPerPackageName[vendorId]; + if (!boardPerVendor) { + boardPerVendor = []; + boardsPerPackageName[vendorId] = boardPerVendor; + } + boardPerVendor.push(board); + } - const packageLabel = - packageName + - `${ - manuallyInstalled - ? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)') - : '' - }`; - // Platform submenu - const platformMenuPath = [...boardsPackagesGroup, packageId]; - // Note: Registering the same submenu twice is a noop. No need to group the boards per platform. - this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, { - order: packageName.toLowerCase(), - }); + // Installed boards + Array.from(groupedBoards.entries()).forEach( + ([packageName, boardsPerPackage]) => { + const useVendorSuffix = Object.keys(boardsPerPackage).length > 1; + Object.entries(boardsPerPackage).forEach(([vendorId, boards]) => { + let platformMenuPath: MenuPath | undefined = undefined; + boards.forEach((board, index) => { + const { packageId, fqbn, name, manuallyInstalled } = board; + // create the platform submenu once. + // creating and registering the same submenu twice in Theia is a noop, though. + if (!platformMenuPath) { + let packageLabel = + packageName + + `${ + manuallyInstalled + ? nls.localize( + 'arduino/board/inSketchbook', + ' (in Sketchbook)' + ) + : '' + }`; + if ( + selectedBoardPlatformId && + platformIdentifierEquals(packageId, selectedBoardPlatformId) + ) { + packageLabel = `✓ ${packageLabel}`; + } + if (useVendorSuffix) { + packageLabel += ` (${vendorId})`; + } + // Platform submenu + platformMenuPath = [ + ...boardsPackagesGroup, + serializePlatformIdentifier(packageId), + ]; + this.menuModelRegistry.registerSubmenu( + platformMenuPath, + packageLabel, + { + order: packageName.toLowerCase(), + } + ); + } - const id = `arduino-select-board--${fqbn}`; - const command = { id }; - const handler = { - execute: () => { - if ( - fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn - ) { - this.boardsServiceProvider.boardsConfig = { - selectedBoard: { - name, - fqbn, - port: this.boardsServiceProvider.boardsConfig.selectedBoard - ?.port, // TODO: verify! - }, - selectedPort: - this.boardsServiceProvider.boardsConfig.selectedPort, + const id = `arduino-select-board--${fqbn}`; + const command = { id }; + const handler = { + execute: () => + this.boardsServiceProvider.updateBoard({ + name: name, + fqbn: fqbn, + }), + isToggled: () => fqbn === selectedBoard?.fqbn, }; - } - }, - isToggled: () => - fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn, - }; - // Board menu - const menuAction = { - commandId: id, - label: name, - order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2 - }; - this.commandRegistry.registerCommand(command, handler); - this.toDisposeBeforeMenuRebuild.push( - Disposable.create(() => this.commandRegistry.unregisterCommand(command)) - ); - this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction); - // Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively. - }); + // Board menu + const menuAction = { + commandId: id, + label: name, + order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2 + }; + this.commandRegistry.registerCommand(command, handler); + this.toDisposeBeforeMenuRebuild.push( + Disposable.create(() => + this.commandRegistry.unregisterCommand(command) + ) + ); + this.menuModelRegistry.registerMenuAction( + platformMenuPath, + menuAction + ); + // Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively. + }); + }); + } + ); - // Installed ports + // Detected ports const registerPorts = ( protocol: string, - protocolOrder: number, - ports: AvailablePorts + ports: ReturnType, + protocolOrder: number ) => { - const portIDs = Object.keys(ports); - if (!portIDs.length) { + if (!ports.length) { return; } @@ -258,46 +308,26 @@ SN: ${SN} ) ); - // First we show addresses with recognized boards connected, - // then all the rest. - const sortedIDs = Object.keys(ports).sort( - (left: string, right: string): number => { - const [, leftBoards] = ports[left]; - const [, rightBoards] = ports[right]; - return rightBoards.length - leftBoards.length; - } - ); - - for (let i = 0; i < sortedIDs.length; i++) { - const portID = sortedIDs[i]; - const [port, boards] = ports[portID]; + for (let i = 0; i < ports.length; i++) { + const { port, boards } = ports[i]; + const portKey = Port.keyOf(port); let label = `${port.addressLabel}`; - if (boards.length) { + if (boards?.length) { const boardsList = boards.map((board) => board.name).join(', '); label = `${label} (${boardsList})`; } - const id = `arduino-select-port--${portID}`; + const id = `arduino-select-port--${portKey}`; const command = { id }; const handler = { execute: () => { - if ( - !Port.sameAs( - port, - this.boardsServiceProvider.boardsConfig.selectedPort - ) - ) { - this.boardsServiceProvider.boardsConfig = { - selectedBoard: - this.boardsServiceProvider.boardsConfig.selectedBoard, - selectedPort: port, - }; - } + this.boardsServiceProvider.updatePort({ + protocol: port.protocol, + address: port.address, + }); + }, + isToggled: () => { + return i === ports.matchingIndex; }, - isToggled: () => - Port.sameAs( - port, - this.boardsServiceProvider.boardsConfig.selectedPort - ), }; const menuAction = { commandId: id, @@ -314,22 +344,12 @@ SN: ${SN} } }; - const grouped = AvailablePorts.groupByProtocol(availablePorts); + const groupedPorts = boardList.portsGroupedByProtocol(); let protocolOrder = 100; - // We first show serial and network ports, then all the rest - ['serial', 'network'].forEach((protocol) => { - const ports = grouped.get(protocol); - if (ports) { - registerPorts(protocol, protocolOrder, ports); - grouped.delete(protocol); - protocolOrder = protocolOrder + 100; - } - }); - grouped.forEach((ports, protocol) => { - registerPorts(protocol, protocolOrder, ports); - protocolOrder = protocolOrder + 100; + Object.entries(groupedPorts).forEach(([protocol, ports]) => { + registerPorts(protocol, ports, protocolOrder); + protocolOrder += 100; }); - this.mainMenuManager.update(); } diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index f43f00426..ee07d7aa5 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -5,8 +5,10 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { NotificationCenter } from '../notification-center'; import { Board, + BoardIdentifier, BoardsService, ExecutableService, + isBoardIdentifierChangeEvent, Sketch, } from '../../common/protocol'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; @@ -88,9 +90,11 @@ export class Debug extends SketchContribution { : Debug.Commands.START_DEBUGGING.label }`) ); - this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => - this.refreshState(selectedBoard) - ); + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.refreshState(event.selectedBoard); + } + }); this.notificationCenter.onPlatformDidInstall(() => this.refreshState()); this.notificationCenter.onPlatformDidUninstall(() => this.refreshState()); } @@ -169,7 +173,7 @@ export class Debug extends SketchContribution { } private async startDebug( - board: Board | undefined = this.boardsServiceProvider.boardsConfig + board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard ): Promise { if (!board) { diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 16fc6a380..3796b7535 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -28,6 +28,8 @@ import { CoreService, SketchesService, Sketch, + isBoardIdentifierChangeEvent, + BoardIdentifier, } from '../../common/protocol'; import { nls } from '@theia/core/lib/common/nls'; import { unregisterSubmenu } from '../menu/arduino-menus'; @@ -108,7 +110,7 @@ export abstract class Examples extends SketchContribution { protected readonly coreService: CoreService; @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; + protected readonly boardsServiceProvider: BoardsServiceProvider; @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; @@ -117,12 +119,14 @@ export abstract class Examples extends SketchContribution { protected override init(): void { super.init(); - this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => - this.handleBoardChanged(selectedBoard) - ); + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.handleBoardChanged(event.selectedBoard); + } + }); this.notificationCenter.onDidReinitialize(() => this.update({ - board: this.boardsServiceClient.boardsConfig.selectedBoard, + board: this.boardsServiceProvider.boardsConfig.selectedBoard, // No force refresh. The core client was already refreshed. }) ); @@ -134,7 +138,7 @@ export abstract class Examples extends SketchContribution { } protected abstract update(options?: { - board?: Board | undefined; + board?: BoardIdentifier | undefined; forceRefresh?: boolean; }): void; @@ -225,7 +229,7 @@ export abstract class Examples extends SketchContribution { protected createHandler(uri: string): CommandHandler { const forceUpdate = () => this.update({ - board: this.boardsServiceClient.boardsConfig.selectedBoard, + board: this.boardsServiceProvider.boardsConfig.selectedBoard, forceRefresh: true, }); return { @@ -306,7 +310,7 @@ export class LibraryExamples extends Examples { protected override async update( options: { board?: Board; forceRefresh?: boolean } = { - board: this.boardsServiceClient.boardsConfig.selectedBoard, + board: this.boardsServiceProvider.boardsConfig.selectedBoard, } ): Promise { const { board, forceRefresh } = options; diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts index cb6479f18..5d77e9ec3 100644 --- a/arduino-ide-extension/src/browser/contributions/include-library.ts +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -37,7 +37,7 @@ export class IncludeLibrary extends SketchContribution { protected readonly notificationCenter: NotificationCenter; @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; + protected readonly boardsServiceProvider: BoardsServiceProvider; @inject(LibraryService) protected readonly libraryService: LibraryService; @@ -46,7 +46,7 @@ export class IncludeLibrary extends SketchContribution { protected readonly toDispose = new DisposableCollection(); override onStart(): void { - this.boardsServiceClient.onBoardsConfigChanged(() => + this.boardsServiceProvider.onBoardsConfigDidChange(() => this.updateMenuActions() ); this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions()); @@ -98,7 +98,7 @@ export class IncludeLibrary extends SketchContribution { this.toDispose.dispose(); this.mainMenuManager.update(); const libraries: LibraryPackage[] = []; - const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn; + const fqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; // Show all libraries, when no board is selected. // Otherwise, show libraries only for the selected board. libraries.push(...(await this.libraryService.list({ fqbn }))); diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 2577d5a73..6ab5845de 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -7,12 +7,13 @@ import { Mutex } from 'async-mutex'; import { ArduinoDaemon, assertSanitizedFqbn, + BoardIdentifier, BoardsService, ExecutableService, + isBoardIdentifierChangeEvent, sanitizeFqbn, } from '../../common/protocol'; import { CurrentSketch } from '../sketches-service-client-impl'; -import { BoardsConfig } from '../boards/boards-config'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { HostedPluginEvents } from '../hosted-plugin-events'; import { NotificationCenter } from '../notification-center'; @@ -48,7 +49,7 @@ export class InoLanguage extends SketchContribution { override onReady(): void { const start = ( - { selectedBoard }: BoardsConfig.Config, + selectedBoard: BoardIdentifier | undefined, forceStart = false ) => { if (selectedBoard) { @@ -59,12 +60,16 @@ export class InoLanguage extends SketchContribution { } }; const forceRestart = () => { - start(this.boardsServiceProvider.boardsConfig, true); + start(this.boardsServiceProvider.boardsConfig.selectedBoard, true); }; this.toDispose.pushAll([ - this.boardsServiceProvider.onBoardsConfigChanged(start), + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + start(event.selectedBoard); + } + }), this.hostedPluginEvents.onPluginsDidStart(() => - start(this.boardsServiceProvider.boardsConfig) + start(this.boardsServiceProvider.boardsConfig.selectedBoard) ), this.hostedPluginEvents.onPluginsWillUnload( () => (this.languageServerFqbn = undefined) @@ -101,12 +106,12 @@ export class InoLanguage extends SketchContribution { matchingFqbn && boardsConfig.selectedBoard?.fqbn === matchingFqbn ) { - start(boardsConfig); + start(boardsConfig.selectedBoard); } } }), ]); - start(this.boardsServiceProvider.boardsConfig); + start(this.boardsServiceProvider.boardsConfig.selectedBoard); } onStop(): void { diff --git a/arduino-ide-extension/src/browser/contributions/open-boards-config.ts b/arduino-ide-extension/src/browser/contributions/open-boards-config.ts index 8feffc14f..f7bb24c1a 100644 --- a/arduino-ide-extension/src/browser/contributions/open-boards-config.ts +++ b/arduino-ide-extension/src/browser/contributions/open-boards-config.ts @@ -1,25 +1,18 @@ -import { CommandRegistry } from '@theia/core'; +import type { Command, CommandRegistry } from '@theia/core/lib/common/command'; import { inject, injectable } from '@theia/core/shared/inversify'; import { BoardsConfigDialog } from '../boards/boards-config-dialog'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; -import { Contribution, Command } from './contribution'; +import type { EditBoardsConfigActionParams } from '../boards/boards-service-provider'; +import { Contribution } from './contribution'; @injectable() export class OpenBoardsConfig extends Contribution { - @inject(BoardsServiceProvider) - private readonly boardsServiceProvider: BoardsServiceProvider; - @inject(BoardsConfigDialog) private readonly boardsConfigDialog: BoardsConfigDialog; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, { - execute: async (query?: string | undefined) => { - const boardsConfig = await this.boardsConfigDialog.open(query); - if (boardsConfig) { - return (this.boardsServiceProvider.boardsConfig = boardsConfig); - } - }, + execute: async (params?: EditBoardsConfigActionParams) => + this.boardsConfigDialog.open(params), }); } } diff --git a/arduino-ide-extension/src/browser/contributions/selected-board.ts b/arduino-ide-extension/src/browser/contributions/selected-board.ts index bf8a84ae8..5afa763df 100644 --- a/arduino-ide-extension/src/browser/contributions/selected-board.ts +++ b/arduino-ide-extension/src/browser/contributions/selected-board.ts @@ -4,7 +4,10 @@ import { } from '@theia/core/lib/browser/status-bar/status-bar'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { BoardsConfig } from '../boards/boards-config'; +import type { + BoardList, + BoardListItem, +} from '../../common/protocol/board-list'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { Contribution } from './contribution'; @@ -12,21 +15,21 @@ import { Contribution } from './contribution'; export class SelectedBoard extends Contribution { @inject(StatusBar) private readonly statusBar: StatusBar; - @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; override onStart(): void { - this.boardsServiceProvider.onBoardsConfigChanged((config) => - this.update(config) + this.boardsServiceProvider.onBoardListDidChange(() => + this.update(this.boardsServiceProvider.boardList) ); } override onReady(): void { - this.update(this.boardsServiceProvider.boardsConfig); + this.update(this.boardsServiceProvider.boardList); } - private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void { + private update(boardList: BoardList): void { + const { selectedBoard, selectedPort } = boardList.boardsConfig; this.statusBar.setElement('arduino-selected-board', { alignment: StatusBarAlignment.RIGHT, text: selectedBoard @@ -38,17 +41,30 @@ export class SelectedBoard extends Contribution { className: 'arduino-selected-board', }); if (selectedBoard) { + const notConnectedLabel = nls.localize( + 'arduino/common/notConnected', + '[not connected]' + ); + let portLabel = notConnectedLabel; + if (selectedPort) { + portLabel = nls.localize( + 'arduino/common/selectedOn', + 'on {0}', + selectedPort.address + ); + const selectedItem: BoardListItem | undefined = + boardList[boardList.selectedIndex]; + if (!selectedItem) { + portLabel += ` ${notConnectedLabel}`; // append ` [not connected]` when the port is selected but it's not detected by the CLI + } + } this.statusBar.setElement('arduino-selected-port', { alignment: StatusBarAlignment.RIGHT, - text: selectedPort - ? nls.localize( - 'arduino/common/selectedOn', - 'on {0}', - selectedPort.address - ) - : nls.localize('arduino/common/notConnected', '[not connected]'), + text: portLabel, className: 'arduino-selected-port', }); + } else { + this.statusBar.removeElement('arduino-selected-port'); } } } diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts index a227a51a0..35b4c2ab7 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -6,15 +6,16 @@ import type { ArduinoState } from 'vscode-arduino-api'; import { BoardsService, CompileSummary, - Port, isCompileSummary, + BoardsConfig, + PortIdentifier, + resolveDetectedPort, } from '../../common/protocol'; import { toApiBoardDetails, toApiCompileSummary, toApiPort, } from '../../common/protocol/arduino-context-mapper'; -import type { BoardsConfig } from '../boards/boards-config'; import { BoardsDataStore } from '../boards/boards-data-store'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { CurrentSketch } from '../sketches-service-client-impl'; @@ -44,8 +45,8 @@ export class UpdateArduinoState extends SketchContribution { override onStart(): void { this.toDispose.pushAll([ - this.boardsServiceProvider.onBoardsConfigChanged((config) => - this.updateBoardsConfig(config) + this.boardsServiceProvider.onBoardsConfigDidChange(() => + this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig) ), this.sketchServiceClient.onCurrentSketchDidChange((sketch) => this.updateSketchPath(sketch) @@ -75,9 +76,7 @@ export class UpdateArduinoState extends SketchContribution { } override onReady(): void { - this.boardsServiceProvider.reconciled.then(() => { - this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig); - }); + this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig); // TODO: verify! this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch()); this.updateUserDirPath(this.configService.tryGetSketchDirUri()); this.updateDataDirPath(this.configService.tryGetDataDirUri()); @@ -106,9 +105,7 @@ export class UpdateArduinoState extends SketchContribution { }); } - private async updateBoardsConfig( - boardsConfig: BoardsConfig.Config - ): Promise { + private async updateBoardsConfig(boardsConfig: BoardsConfig): Promise { const fqbn = boardsConfig.selectedBoard?.fqbn; const port = boardsConfig.selectedPort; await this.updateFqbn(fqbn); @@ -146,8 +143,11 @@ export class UpdateArduinoState extends SketchContribution { }); } - private async updatePort(port: Port | undefined): Promise { - const apiPort = port && toApiPort(port); + private async updatePort(port: PortIdentifier | undefined): Promise { + const resolvedPort = + port && + resolveDetectedPort(port, this.boardsServiceProvider.detectedPorts); + const apiPort = resolvedPort && toApiPort(resolvedPort); return this.updateState({ key: 'port', value: apiPort }); } @@ -171,9 +171,6 @@ export class UpdateArduinoState extends SketchContribution { params: UpdateStateParams ): Promise { await this.hostedPluginSupport.didStart; - return this.commandService.executeCommand( - 'arduinoAPI.updateState', - params - ); + return this.commandService.executeCommand('arduinoAPI.updateState', params); } } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 034ea87d3..6f6e1efae 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -1,30 +1,34 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; -import { CoreService, Port, sanitizeFqbn } from '../../common/protocol'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + CoreService, + portIdentifierEquals, + sanitizeFqbn, +} from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; +import { CurrentSketch } from '../sketches-service-client-impl'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { Command, CommandRegistry, - MenuModelRegistry, + CoreServiceContribution, KeybindingRegistry, + MenuModelRegistry, TabBarToolbarRegistry, - CoreServiceContribution, } from './contribution'; -import { deepClone, nls } from '@theia/core/lib/common'; -import { CurrentSketch } from '../sketches-service-client-impl'; -import type { VerifySketchParams } from './verify-sketch'; import { UserFields } from './user-fields'; +import type { VerifySketchParams } from './verify-sketch'; @injectable() export class UploadSketch extends CoreServiceContribution { + @inject(UserFields) + private readonly userFields: UserFields; + private readonly onDidChangeEmitter = new Emitter(); private readonly onDidChange = this.onDidChangeEmitter.event; private uploadInProgress = false; - @inject(UserFields) - private readonly userFields: UserFields; - override registerCommands(registry: CommandRegistry): void { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { execute: async () => { @@ -107,7 +111,6 @@ export class UploadSketch extends CoreServiceContribution { // uploadInProgress will be set to false whether the upload fails or not this.uploadInProgress = true; this.menuManager.update(); - this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload(); this.onDidChangeEmitter.fire(); this.clearVisibleNotification(); @@ -135,13 +138,24 @@ export class UploadSketch extends CoreServiceContribution { return; } - await this.doWithProgress({ + const uploadResponse = await this.doWithProgress({ progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), task: (progressId, coreService) => coreService.upload({ ...uploadOptions, progressId }), keepOutput: true, }); + if ( + uploadResponse.portBeforeUpload && + !portIdentifierEquals( + uploadResponse.portBeforeUpload, + uploadResponse.portAfterUpload + ) + ) { + // https://github.com/arduino/arduino-cli/issues/2245 + this.boardsServiceProvider.updatePort(uploadResponse.portAfterUpload); + } + this.messageService.info( nls.localize('arduino/sketch/doneUploading', 'Done uploading.'), { timeout: 3000 } @@ -150,9 +164,10 @@ export class UploadSketch extends CoreServiceContribution { this.userFields.notifyFailedWithError(e); this.handleError(e); } finally { + // TODO: here comes the port change if happened during the upload + // https://github.com/arduino/arduino-cli/issues/2245 this.uploadInProgress = false; this.menuManager.update(); - this.boardsServiceProvider.attemptPostUploadAutoSelect(); this.onDidChangeEmitter.fire(); } } @@ -174,7 +189,7 @@ export class UploadSketch extends CoreServiceContribution { this.preferences.get('arduino.upload.verify'), this.preferences.get('arduino.upload.verbose'), ]); - const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort); + const port = boardsConfig.selectedPort; return { sketch, fqbn, @@ -185,28 +200,6 @@ export class UploadSketch extends CoreServiceContribution { userFields, }; } - - /** - * This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740) - * This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236). - * - * Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`. - * This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`. - */ - private maybeUpdatePortProperties(port: Port | undefined): Port | undefined { - if (port) { - const key = Port.keyOf(port); - for (const candidate of this.boardsServiceProvider.availablePorts) { - if (key === Port.keyOf(candidate) && candidate.properties) { - return { - ...port, - properties: deepClone(candidate.properties), - }; - } - } - } - return port; - } } export namespace UploadSketch { diff --git a/arduino-ide-extension/src/browser/contributions/user-fields.ts b/arduino-ide-extension/src/browser/contributions/user-fields.ts index 62bef9748..14a4e55a8 100644 --- a/arduino-ide-extension/src/browser/contributions/user-fields.ts +++ b/arduino-ide-extension/src/browser/contributions/user-fields.ts @@ -21,7 +21,7 @@ export class UserFields extends Contribution { protected override init(): void { super.init(); - this.boardsServiceProvider.onBoardsConfigChanged(async () => { + this.boardsServiceProvider.onBoardsConfigDidChange(async () => { const userFields = await this.boardsServiceProvider.selectedBoardUserFields(); this.boardRequiresUserFields = userFields.length > 0; @@ -43,10 +43,7 @@ export class UserFields extends Contribution { if (!fqbn) { return undefined; } - const address = - boardsConfig.selectedBoard?.port?.address || - boardsConfig.selectedPort?.address || - ''; + const address = boardsConfig.selectedPort?.address || ''; return fqbn + '|' + address; } diff --git a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-component.tsx b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-component.tsx index 528e7ec95..e12775b2d 100644 --- a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-component.tsx @@ -1,20 +1,30 @@ +import { nls } from '@theia/core/lib/common/nls'; import * as React from '@theia/core/shared/react'; import Tippy from '@tippyjs/react'; -import { AvailableBoard } from '../../boards/boards-service-provider'; -import { CertificateListComponent } from './certificate-list'; -import { SelectBoardComponent } from './select-board-components'; +import { + BoardList, + isInferredBoardListItem, +} from '../../../common/protocol/board-list'; +import { + boardIdentifierEquals, + portIdentifierEquals, +} from '../../../common/protocol/boards-service'; import { CertificateAddComponent } from './certificate-add-new'; -import { nls } from '@theia/core/lib/common'; +import { CertificateListComponent } from './certificate-list'; +import { + BoardOptionValue, + SelectBoardComponent, +} from './select-board-components'; export const CertificateUploaderComponent = ({ - availableBoards, + boardList, certificates, addCertificate, updatableFqbns, uploadCertificates, openContextMenu, }: { - availableBoards: AvailableBoard[]; + boardList: BoardList; certificates: string[]; addCertificate: (cert: string) => void; updatableFqbns: string[]; @@ -33,11 +43,17 @@ export const CertificateUploaderComponent = ({ const [selectedCerts, setSelectedCerts] = React.useState([]); - const [selectedBoard, setSelectedBoard] = - React.useState(null); + const [selectedItem, setSelectedItem] = + React.useState(null); const installCertificates = async () => { - if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) { + if (!selectedItem) { + return; + } + const board = isInferredBoardListItem(selectedItem) + ? selectedItem.inferredBoard + : selectedItem.board; + if (!board.fqbn) { return; } @@ -45,8 +61,8 @@ export const CertificateUploaderComponent = ({ try { await uploadCertificates( - selectedBoard.fqbn, - selectedBoard.port.address, + board.fqbn, + selectedItem.port.address, selectedCerts ); setInstallFeedback('ok'); @@ -55,17 +71,29 @@ export const CertificateUploaderComponent = ({ } }; - const onBoardSelect = React.useCallback( - (board: AvailableBoard) => { - const newFqbn = (board && board.fqbn) || null; - const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null; + const onItemSelect = React.useCallback( + (item: BoardOptionValue | null) => { + if (!item) { + return; + } + const board = isInferredBoardListItem(item) + ? item.inferredBoard + : item.board; + const selectedBoard = isInferredBoardListItem(selectedItem) + ? selectedItem.inferredBoard + : selectedItem?.board; + const port = item.port; + const selectedPort = selectedItem?.port; - if (newFqbn !== prevFqbn) { + if ( + !boardIdentifierEquals(board, selectedBoard) || + !portIdentifierEquals(port, selectedPort) + ) { setInstallFeedback(null); - setSelectedBoard(board); + setSelectedItem(item); } }, - [selectedBoard] + [selectedItem] ); return ( @@ -125,10 +153,10 @@ export const CertificateUploaderComponent = ({
@@ -167,7 +195,7 @@ export const CertificateUploaderComponent = ({ type="button" className="theia-button primary install-cert-btn" onClick={installCertificates} - disabled={selectedCerts.length === 0 || !selectedBoard} + disabled={selectedCerts.length === 0 || !selectedItem} > {nls.localize('arduino/certificate/upload', 'Upload')} diff --git a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-dialog.tsx index fa18fb6f2..c014e59c6 100644 --- a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/certificate-uploader-dialog.tsx @@ -1,62 +1,51 @@ -import * as React from '@theia/core/shared/react'; +import { DialogProps } from '@theia/core/lib/browser/dialogs'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { + PreferenceScope, + PreferenceService, +} from '@theia/core/lib/browser/preferences/preference-service'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { nls } from '@theia/core/lib/common/nls'; +import type { Message } from '@theia/core/shared/@phosphor/messaging'; +import { Widget } from '@theia/core/shared/@phosphor/widgets'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { DialogProps } from '@theia/core/lib/browser/dialogs'; +import * as React from '@theia/core/shared/react'; +import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader'; +import { createBoardList } from '../../../common/protocol/board-list'; +import { ArduinoPreferences } from '../../arduino-preferences'; +import { BoardsServiceProvider } from '../../boards/boards-service-provider'; import { AbstractDialog } from '../../theia/dialogs/dialogs'; -import { Widget } from '@theia/core/shared/@phosphor/widgets'; -import { Message } from '@theia/core/shared/@phosphor/messaging'; -import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; -import { - AvailableBoard, - BoardsServiceProvider, -} from '../../boards/boards-service-provider'; import { CertificateUploaderComponent } from './certificate-uploader-component'; -import { ArduinoPreferences } from '../../arduino-preferences'; -import { - PreferenceScope, - PreferenceService, -} from '@theia/core/lib/browser/preferences/preference-service'; -import { CommandRegistry } from '@theia/core/lib/common/command'; import { certificateList, sanifyCertString } from './utils'; -import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader'; -import { nls } from '@theia/core/lib/common'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @injectable() export class UploadCertificateDialogWidget extends ReactWidget { @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - + private readonly boardsServiceProvider: BoardsServiceProvider; @inject(ArduinoPreferences) - protected readonly arduinoPreferences: ArduinoPreferences; - + private readonly arduinoPreferences: ArduinoPreferences; @inject(PreferenceService) - protected readonly preferenceService: PreferenceService; - + private readonly preferenceService: PreferenceService; @inject(CommandRegistry) - protected readonly commandRegistry: CommandRegistry; - + private readonly commandRegistry: CommandRegistry; @inject(ArduinoFirmwareUploader) - protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; - + private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - protected certificates: string[] = []; - protected updatableFqbns: string[] = []; - protected availableBoards: AvailableBoard[] = []; + private certificates: string[] = []; + private updatableFqbns: string[] = []; + private boardList = createBoardList({}); - public busyCallback = (busy: boolean) => { + busyCallback = (busy: boolean) => { return; }; - constructor() { - super(); - } - @postConstruct() protected init(): void { this.arduinoPreferences.ready.then(() => { @@ -81,8 +70,8 @@ export class UploadCertificateDialogWidget extends ReactWidget { }) ); - this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => { - this.availableBoards = availableBoards; + this.boardsServiceProvider.onBoardListDidChange((boardList) => { + this.boardList = boardList; this.update(); }); } @@ -126,7 +115,7 @@ export class UploadCertificateDialogWidget extends ReactWidget { protected render(): React.ReactNode { return ( { @inject(UploadCertificateDialogWidget) - protected readonly widget: UploadCertificateDialogWidget; + private readonly widget: UploadCertificateDialogWidget; private busy = false; diff --git a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/select-board-components.tsx b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/select-board-components.tsx index 285f136c3..2316e8e1e 100644 --- a/arduino-ide-extension/src/browser/dialogs/certificate-uploader/select-board-components.tsx +++ b/arduino-ide-extension/src/browser/dialogs/certificate-uploader/select-board-components.tsx @@ -1,37 +1,38 @@ -import { nls } from '@theia/core/lib/common'; +import { nls } from '@theia/core/lib/common/nls'; import * as React from '@theia/core/shared/react'; -import { AvailableBoard } from '../../boards/boards-service-provider'; +import { + BoardList, + BoardListItemWithBoard, + InferredBoardListItem, + isInferredBoardListItem, +} from '../../../common/protocol/board-list'; import { ArduinoSelect } from '../../widgets/arduino-select'; -type BoardOption = { value: string; label: string }; +export type BoardOptionValue = BoardListItemWithBoard | InferredBoardListItem; +type BoardOption = { value: BoardOptionValue | undefined; label: string }; export const SelectBoardComponent = ({ - availableBoards, + boardList, updatableFqbns, - onBoardSelect, - selectedBoard, + onItemSelect, + selectedItem, busy, }: { - availableBoards: AvailableBoard[]; + boardList: BoardList; updatableFqbns: string[]; - onBoardSelect: (board: AvailableBoard | null) => void; - selectedBoard: AvailableBoard | null; + onItemSelect: (item: BoardOptionValue | null) => void; + selectedItem: BoardOptionValue | null; busy: boolean; }): React.ReactElement => { const [selectOptions, setSelectOptions] = React.useState([]); - const [selectBoardPlaceholder, setSelectBoardPlaceholder] = - React.useState(''); + const [selectItemPlaceholder, setSelectBoardPlaceholder] = React.useState(''); const selectOption = React.useCallback( - (boardOpt: BoardOption) => { - onBoardSelect( - (boardOpt && - availableBoards.find((board) => board.fqbn === boardOpt.value)) || - null - ); + (boardOpt: BoardOption | null) => { + onItemSelect(boardOpt?.value ?? null); }, - [availableBoards, onBoardSelect] + [onItemSelect] ); React.useEffect(() => { @@ -44,22 +45,29 @@ export const SelectBoardComponent = ({ 'arduino/certificate/selectBoard', 'Select a board...' ); + const updatableBoards = boardList.boards.filter((item) => { + const fqbn = ( + isInferredBoardListItem(item) ? item.inferredBoard : item.board + ).fqbn; + return fqbn && updatableFqbns.includes(fqbn); + }); let selBoard = -1; - const updatableBoards = availableBoards.filter( - (board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn) - ); - const boardsList: BoardOption[] = updatableBoards.map((board, i) => { - if (board.selected) { + + const boardsList: BoardOption[] = updatableBoards.map((item, i) => { + if (selectedItem === item) { selBoard = i; } + const board = isInferredBoardListItem(item) + ? item.inferredBoard + : item.board; return { label: nls.localize( 'arduino/certificate/boardAtPort', '{0} at {1}', board.name, - board.port?.address ?? '' + item.port?.address ?? '' ), - value: board.fqbn || '', + value: item, }; }); @@ -73,30 +81,30 @@ export const SelectBoardComponent = ({ setSelectBoardPlaceholder(placeholderTxt); setSelectOptions(boardsList); - if (selectedBoard) { - selBoard = boardsList - .map((boardOpt) => boardOpt.value) - .indexOf(selectedBoard.fqbn || ''); + if (selectedItem) { + selBoard = updatableBoards.indexOf(selectedItem); } selectOption(boardsList[selBoard] || null); - }, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]); - + }, [busy, boardList, selectOption, updatableFqbns, selectedItem]); return ( Promise; @@ -31,8 +39,8 @@ export const FirmwareUploaderComponent = ({ 'ok' | 'fail' | 'installing' | null >(null); - const [selectedBoard, setSelectedBoard] = - React.useState(null); + const [selectedItem, setSelectedItem] = + React.useState(null); const [availableFirmwares, setAvailableFirmwares] = React.useState< FirmwareInfo[] @@ -50,13 +58,16 @@ export const FirmwareUploaderComponent = ({ const fetchFirmwares = React.useCallback(async () => { setInstallFeedback(null); setFirmwaresFetching(true); - if (!selectedBoard) { + if (!selectedItem) { return; } // fetch the firmwares for the selected board + const board = isInferredBoardListItem(selectedItem) + ? selectedItem.inferredBoard + : selectedItem.board; const firmwaresForFqbn = await firmwareUploader.availableFirmwares( - selectedBoard.fqbn || '' + board.fqbn || '' ); setAvailableFirmwares(firmwaresForFqbn); @@ -69,7 +80,7 @@ export const FirmwareUploaderComponent = ({ if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]); setFirmwaresFetching(false); - }, [firmwareUploader, selectedBoard]); + }, [firmwareUploader, selectedItem]); const installFirmware = React.useCallback(async () => { setInstallFeedback('installing'); @@ -81,27 +92,39 @@ export const FirmwareUploaderComponent = ({ try { const installStatus = !!firmwareToFlash && - !!selectedBoard?.port && - (await flashFirmware(firmwareToFlash, selectedBoard?.port)); + !!selectedItem?.board && + (await flashFirmware(firmwareToFlash, selectedItem?.port)); setInstallFeedback((installStatus && 'ok') || 'fail'); } catch { setInstallFeedback('fail'); } - }, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]); + }, [selectedItem, selectedFirmware, availableFirmwares, flashFirmware]); - const onBoardSelect = React.useCallback( - (board: AvailableBoard) => { - const newFqbn = (board && board.fqbn) || null; - const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null; - - if (newFqbn !== prevFqbn) { + const onItemSelect = React.useCallback( + (item: BoardListItemWithBoard | null) => { + if (!item) { + return; + } + const board = isInferredBoardListItem(item) + ? item.inferredBoard + : item.board; + const selectedBoard = isInferredBoardListItem(selectedItem) + ? selectedItem.inferredBoard + : selectedItem?.board; + const port = item.port; + const selectedPort = selectedItem?.port; + + if ( + !boardIdentifierEquals(board, selectedBoard) || + !portIdentifierEquals(port, selectedPort) + ) { setInstallFeedback(null); setAvailableFirmwares([]); - setSelectedBoard(board); + setSelectedItem(item); } }, - [selectedBoard] + [selectedItem] ); return ( @@ -115,10 +138,10 @@ export const FirmwareUploaderComponent = ({
@@ -126,7 +149,7 @@ export const FirmwareUploaderComponent = ({ type="button" className="theia-button secondary" disabled={ - selectedBoard === null || + selectedItem === null || firmwaresFetching || installFeedback === 'installing' } @@ -150,7 +173,7 @@ export const FirmwareUploaderComponent = ({ id="firmware-select" menuPosition="fixed" isDisabled={ - !selectedBoard || + !selectedItem || firmwaresFetching || installFeedback === 'installing' } diff --git a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx index 20d97d4b2..980ae25cf 100644 --- a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx @@ -1,24 +1,21 @@ -import * as React from '@theia/core/shared/react'; +import { DialogProps } from '@theia/core/lib/browser/dialogs'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { Message } from '@theia/core/shared/@phosphor/messaging'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { DialogProps } from '@theia/core/lib/browser/dialogs'; -import { ReactDialog } from '../../theia/dialogs/dialogs'; -import { Message } from '@theia/core/shared/@phosphor/messaging'; -import { - AvailableBoard, - BoardsServiceProvider, -} from '../../boards/boards-service-provider'; +import * as React from '@theia/core/shared/react'; import { ArduinoFirmwareUploader, FirmwareInfo, } from '../../../common/protocol/arduino-firmware-uploader'; -import { FirmwareUploaderComponent } from './firmware-uploader-component'; +import { Port } from '../../../common/protocol/boards-service'; +import { BoardsServiceProvider } from '../../boards/boards-service-provider'; import { UploadFirmware } from '../../contributions/upload-firmware'; -import { Port } from '../../../common/protocol'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { ReactDialog } from '../../theia/dialogs/dialogs'; +import { FirmwareUploaderComponent } from './firmware-uploader-component'; @injectable() export class UploadFirmwareDialogProps extends DialogProps {} @@ -26,14 +23,13 @@ export class UploadFirmwareDialogProps extends DialogProps {} @injectable() export class UploadFirmwareDialog extends ReactDialog { @inject(BoardsServiceProvider) - private readonly boardsServiceClient: BoardsServiceProvider; + private readonly boardsServiceProvider: BoardsServiceProvider; @inject(ArduinoFirmwareUploader) private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; @inject(FrontendApplicationStateService) - private readonly appStatusService: FrontendApplicationStateService; + private readonly appStateService: FrontendApplicationStateService; private updatableFqbns: string[] = []; - private availableBoards: AvailableBoard[] = []; private isOpen = new Object(); private busy = false; @@ -49,16 +45,12 @@ export class UploadFirmwareDialog extends ReactDialog { @postConstruct() protected init(): void { - this.appStatusService.reachedState('ready').then(async () => { + this.appStateService.reachedState('ready').then(async () => { const fqbns = await this.arduinoFirmwareUploader.updatableBoards(); this.updatableFqbns = fqbns; this.update(); }); - - this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => { - this.availableBoards = availableBoards; - this.update(); - }); + this.boardsServiceProvider.onBoardListDidChange(() => this.update()); } get value(): void { @@ -70,7 +62,7 @@ export class UploadFirmwareDialog extends ReactDialog {
{ - await this.boardsServiceProvider.reconciled; - this.lastConnectedBoard = { - selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard, - selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort, - }; - - if (!this.onBoardsConfigChanged) { - this.onBoardsConfigChanged = - this.boardsServiceProvider.onBoardsConfigChanged( - async ({ selectedBoard, selectedPort }) => { - if ( - typeof selectedBoard === 'undefined' || - typeof selectedPort === 'undefined' - ) + const { boardList } = this.boardsServiceProvider; + this.lastConnectedBoard = boardList[boardList.selectedIndex]; + if (!this.onBoardListDidChange) { + this.onBoardListDidChange = + this.boardsServiceProvider.onBoardListDidChange( + async (newBoardList) => { + const currentConnectedBoard = + newBoardList[newBoardList.selectedIndex]; + if (!currentConnectedBoard) { return; + } - // a board is plugged and it's different from the old connected board if ( - selectedBoard?.fqbn !== - this.lastConnectedBoard?.selectedBoard?.fqbn || - Port.keyOf(selectedPort) !== - (this.lastConnectedBoard.selectedPort - ? Port.keyOf(this.lastConnectedBoard.selectedPort) - : undefined) + !this.lastConnectedBoard || + boardListItemEquals( + currentConnectedBoard, + this.lastConnectedBoard + ) ) { - this.lastConnectedBoard = { - selectedBoard: selectedBoard, - selectedPort: selectedPort, - }; - this.onMonitorShouldResetEmitter.fire(); - } else { // a board is plugged and it's the same as prev, rerun "this.startMonitor" to // recreate the listener callback this.startMonitor(); + } else { + // a board is plugged and it's different from the old connected board + this.lastConnectedBoard = currentConnectedBoard; + this.onMonitorShouldResetEmitter.fire(); } } ); } - const { selectedBoard, selectedPort } = - this.boardsServiceProvider.boardsConfig; - if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return; + if (!this.lastConnectedBoard) { + return; + } + + const board = getInferredBoardOrBoard(this.lastConnectedBoard); + if (!board) { + return; + } try { this.clearVisibleNotification(); - await this.server().startMonitor(selectedBoard, selectedPort, settings); + await this.server().startMonitor( + board, + this.lastConnectedBoard.port, + settings + ); } catch (err) { const message = ApplicationError.is(err) ? err.message : String(err); this.previousNotificationId = this.notificationId(message); @@ -186,7 +189,10 @@ export class MonitorManagerProxyClientImpl } } - getCurrentSettings(board: Board, port: Port): Promise { + getCurrentSettings( + board: BoardIdentifier, + port: PortIdentifier + ): Promise { return this.server().getCurrentSettings(board, port); } diff --git a/arduino-ide-extension/src/browser/notification-center.ts b/arduino-ide-extension/src/browser/notification-center.ts index 96c938d25..e7d5c6676 100644 --- a/arduino-ide-extension/src/browser/notification-center.ts +++ b/arduino-ide-extension/src/browser/notification-center.ts @@ -14,13 +14,13 @@ import { NotificationServiceClient, NotificationServiceServer, } from '../common/protocol/notification-service'; -import { - AttachedBoardsChangeEvent, +import type { BoardsPackage, LibraryPackage, ConfigState, Sketch, ProgressMessage, + DetectedPorts, } from '../common/protocol'; import { FrontendApplicationStateService, @@ -61,8 +61,9 @@ export class NotificationCenter private readonly libraryDidUninstallEmitter = new Emitter<{ item: LibraryPackage; }>(); - private readonly attachedBoardsDidChangeEmitter = - new Emitter(); + private readonly detectedPortsDidChangeEmitter = new Emitter<{ + detectedPorts: DetectedPorts; + }>(); private readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[]; }>(); @@ -82,7 +83,7 @@ export class NotificationCenter this.platformDidUninstallEmitter, this.libraryDidInstallEmitter, this.libraryDidUninstallEmitter, - this.attachedBoardsDidChangeEmitter + this.detectedPortsDidChangeEmitter ); readonly onDidReinitialize = this.didReinitializeEmitter.event; @@ -97,8 +98,7 @@ export class NotificationCenter readonly onPlatformDidUninstall = this.platformDidUninstallEmitter.event; readonly onLibraryDidInstall = this.libraryDidInstallEmitter.event; readonly onLibraryDidUninstall = this.libraryDidUninstallEmitter.event; - readonly onAttachedBoardsDidChange = - this.attachedBoardsDidChangeEmitter.event; + readonly onDetectedPortsDidChange = this.detectedPortsDidChangeEmitter.event; readonly onRecentSketchesDidChange = this.recentSketchesChangedEmitter.event; readonly onAppStateDidChange = this.onAppStateDidChangeEmitter.event; @@ -166,8 +166,8 @@ export class NotificationCenter this.libraryDidUninstallEmitter.fire(event); } - notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void { - this.attachedBoardsDidChangeEmitter.fire(event); + notifyDetectedPortsDidChange(event: { detectedPorts: DetectedPorts }): void { + this.detectedPortsDidChangeEmitter.fire(event); } notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index 7f2e6d02c..2ac378347 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -173,7 +173,6 @@ export class MonitorWidget extends ReactWidget { private async startMonitor(): Promise { await this.appStateService.reachedState('ready'); - await this.boardsServiceProvider.reconciled; await this.syncSettings(); await this.monitorManagerProxy.startMonitor(); } diff --git a/arduino-ide-extension/src/browser/style/boards-config-dialog.css b/arduino-ide-extension/src/browser/style/boards-config-dialog.css index 90938ca70..02ef735b8 100644 --- a/arduino-ide-extension/src/browser/style/boards-config-dialog.css +++ b/arduino-ide-extension/src/browser/style/boards-config-dialog.css @@ -196,7 +196,10 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { } .arduino-boards-toolbar-item--label-connected { + font-family: 'Open Sans Bold'; + font-style: normal; font-weight: 700; + font-size: 14px; } .arduino-boards-toolbar-item-container .caret { @@ -208,6 +211,10 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { margin: -1px; z-index: 1; border: 1px solid var(--theia-arduino-toolbar-dropdown-border); + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-size: 12px; } .arduino-boards-dropdown-list:focus { @@ -230,7 +237,6 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { cursor: default; display: flex; font-size: var(--theia-ui-font-size1); - gap: 10px; justify-content: space-between; padding: 10px; } @@ -240,10 +246,33 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { flex: 1; } +/* Redefine default codicon size https://github.com/microsoft/vscode/commit/38cd0a377b7abef34fb07fe770fc633e68819ba6 */ +.arduino-boards-dropdown-item .codicon[class*='codicon-'] { + font-size: 14px; +} + +.arduino-boards-dropdown-item .p-TabBar-toolbar { + padding: 0px; + margin: 0px; + flex-direction: column; +} + +.arduino-boards-dropdown-item .p-TabBar-toolbar .item { + margin: 0px; +} + +.arduino-boards-dropdown-item .p-TabBar-toolbar .item .action-label { + padding: 0px; +} + .arduino-boards-dropdown-item--board-label { font-size: 14px; } +.arduino-boards-dropdown-item .arduino-boards-dropdown-item--protocol { + margin-right: 10px; +} + .arduino-boards-dropdown-item--port-label { font-size: 12px; } @@ -267,10 +296,6 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { color: var(--theia-arduino-toolbar-dropdown-iconSelected); } -.arduino-boards-dropdown-item .fa-check { - align-self: center; -} - .arduino-board-dropdown-footer { color: var(--theia-secondaryButton-foreground); border-top: 1px solid var(--theia-dropdown-border); diff --git a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx index 57eef5639..20284b413 100644 --- a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx +++ b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx @@ -18,7 +18,6 @@ export abstract class AbstractDialog extends TheiaAbstractDialog { @inject(DialogProps) protected override readonly props: DialogProps ) { super(props); - this.closeCrossNode.classList.remove(...codiconArray('close')); this.closeCrossNode.classList.add('fa', 'fa-close'); } diff --git a/arduino-ide-extension/src/common/nls.ts b/arduino-ide-extension/src/common/nls.ts index 29ab5604f..06c8baee7 100644 --- a/arduino-ide-extension/src/common/nls.ts +++ b/arduino-ide-extension/src/common/nls.ts @@ -1,5 +1,6 @@ import { nls } from '@theia/core/lib/common/nls'; +// TODO: rename constants: `Unknown` should be `unknownLabel`, change `Later` to `laterLabel`, etc. export const Unknown = nls.localize('arduino/common/unknown', 'Unknown'); export const Later = nls.localize('arduino/common/later', 'Later'); export const Updatable = nls.localize('arduino/common/updateable', 'Updatable'); diff --git a/arduino-ide-extension/src/common/protocol/board-list.ts b/arduino-ide-extension/src/common/protocol/board-list.ts new file mode 100644 index 000000000..a06ec2f5b --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/board-list.ts @@ -0,0 +1,387 @@ +import { naturalCompare } from '../utils'; +import { + BoardIdentifier, + boardIdentifierComparator, + boardIdentifierEquals, + BoardsConfig, + DetectedPort, + DetectedPorts, + emptyBoardsConfig, + findMatchingPortIndex, + isBoardIdentifier, + isDefinedBoardsConfig, + Port, + portIdentifierEquals, + portProtocolComparator, +} from './boards-service'; + +export interface BoardListItem { + readonly port: Port; + readonly board?: BoardIdentifier; +} + +export function isBoardListItem(arg: unknown): arg is BoardListItem { + return ( + Boolean(arg) && + typeof arg === 'object' && + (arg).port !== undefined && + Port.is((arg).port) && + ((arg).board === undefined || + ((arg).board !== undefined && + isBoardIdentifier((arg).board))) + ); +} + +export function boardListItemEquals( + left: BoardListItem, + right: BoardListItem +): boolean { + if (portIdentifierEquals(left.port, right.port)) { + const leftBoard = getBoardOrInferredBoard(left); + const rightBoard = getBoardOrInferredBoard(right); + if (boardIdentifierEquals(leftBoard, rightBoard)) { + const leftInferredBoard = getInferredBoardOrBoard(left); + const rightInferredBoard = getInferredBoardOrBoard(right); + return boardIdentifierEquals(leftInferredBoard, rightInferredBoard); + } + } + return false; +} + +export interface BoardListItemWithBoard extends BoardListItem { + readonly board: BoardIdentifier; +} + +export function isBoardListItemWithBoard( + arg: unknown +): arg is BoardListItemWithBoard { + return isBoardListItem(arg) && Boolean(arg.board); +} + +export function getBoardOrInferredBoard( + item: BoardListItem +): BoardIdentifier | undefined { + let board: BoardIdentifier | undefined = undefined; + board = item.board; + if (!board && isInferredBoardListItem(item)) { + board = item.inferredBoard; + } + return board; +} + +export function getInferredBoardOrBoard( + item: BoardListItem +): BoardIdentifier | undefined { + if (isInferredBoardListItem(item)) { + return item.inferredBoard; + } + return item.board; +} + +interface BoardSelectedBoardListItem extends BoardListItem { + readonly inferredBoard: BoardIdentifier; + readonly type: Extract; +} + +interface BoardOverriddenBoardListItem extends BoardListItem { + readonly board: BoardIdentifier; + readonly inferredBoard: BoardIdentifier; + readonly type: Extract; +} + +export type InferredBoardListItem = + | BoardSelectedBoardListItem + | BoardOverriddenBoardListItem; + +export function isInferredBoardListItem( + arg: unknown +): arg is InferredBoardListItem { + return ( + isBoardListItem(arg) && + (arg).type !== undefined && + isInferenceType((arg).type) && + (arg).inferredBoard !== undefined && + isBoardIdentifier((arg).inferredBoard) + ); +} + +/** + * Stores historical info about boards manually specified for detected boards. The key are generated with `Port#keyOf`. + */ +export type BoardListHistory = Readonly>; + +export function isBoardListHistory(arg: unknown): arg is BoardListHistory { + return ( + Boolean(arg) && + typeof arg === 'object' && + Object.entries(arg).every(([, value]) => isBoardIdentifier(value)) + ); +} + +const inferenceTypeLiterals = [ + /** + * The user has manually selected the board (FQBN) for the detected port of a 3rd party board (no matching boards were detected by the CLI for the port) + */ + 'board-select', + /** + * The user has manually edited the detected FQBN of a recognized board from a detected port (there are matching boards for a detected port, but the user decided to use another FQBN) + */ + 'board-overridden', +] as const; +type InferenceType = (typeof inferenceTypeLiterals)[number]; +function isInferenceType(arg: unknown): arg is InferenceType { + return ( + typeof arg === 'string' && + inferenceTypeLiterals.includes(arg) + ); +} + +/** + * Compare precedence: + * 1. `BoardListItem#port#protocol`: `'serial'`, `'network'`, then natural compare of the `protocol` string. + * 1. `BoardListItem`s with a `board` comes before items without a `board`. + * 1. `BoardListItem#board`: + * 1. Items with `'arduino'` vendor ID in the `fqbn` come before other vendors. + * 1. Natural compare of the `name`. + * 1. If the `BoardListItem`s do not have a `board` property, `BoardListItem#port#address` natural compare is the fallback. + */ +function boardListItemComparator( + left: BoardListItem, + right: BoardListItem +): number { + // sort by port protocol + let result = portProtocolComparator(left.port, right.port); + if (result) { + return result; + } + + // compare by board + result = boardIdentifierComparator( + getBoardOrInferredBoard(left), + getBoardOrInferredBoard(right) + ); + if (result) { + return result; + } + + // fallback compare based on the address + return naturalCompare(left.port.address, right.port.address); +} + +/** + * A list of boards discovered by the Arduino CLI. With the `board list --watch` gRPC equivalent command, + * the CLI provides a `1..*` mapping between a port and the matching boards list. This type inverts the mapping + * and makes a `1..1` association between a board identifier and the port it belongs to. + */ +export type BoardList = + readonly T[] & { + /** + * A snapshot of the board and port configuration this board list has been initialized with. + */ + readonly boardsConfig: Readonly; + + /** + * Index of the board+port item that is currently "selected". A board list item is selected, if matches the board+port combination of `boardsConfig`. + */ + get selectedIndex(): number; + + /** + * Contains all boards recognized from the detected port, and an optional unrecognized one that is derived from the detected port and the `initParam#selectedBoard`. + */ + get boards(): readonly (BoardListItemWithBoard | InferredBoardListItem)[]; + + /** + * If `predicate` is not defined, no ports are filtered. + */ + ports( + predicate?: (detectedPort: DetectedPort) => boolean + ): readonly DetectedPort[] & Readonly<{ matchingIndex: number }>; + + portsGroupedByProtocol(): Readonly< + Record<'serial' | 'network' | string, ReturnType> + >; + + toString(): string; + }; + +export function createBoardList( + detectedPorts: DetectedPorts, + boardsConfig: Readonly = emptyBoardsConfig(), + boardListHistory: BoardListHistory = {} +): BoardList { + const items: BoardListItem[] = []; + for (const detectedPort of Object.values(detectedPorts)) { + const { port, boards } = detectedPort; + const portKey = Port.keyOf(port); + const inferredBoard = boardListHistory[portKey]; + if (!boards?.length) { + // Infer unrecognized boards from the history + if (inferredBoard) { + const item: InferredBoardListItem = { + port, + inferredBoard, + type: 'board-select', + }; + items.push(item); + } else { + items.push({ port }); + } + } else { + // Otherwise, include the port for each board. + for (const board of boards) { + const { fqbn, name } = board; + if ( + isBoardIdentifier(board) && + isBoardIdentifier(inferredBoard) && + !boardIdentifierEquals(board, inferredBoard) + ) { + const inferredItem: InferredBoardListItem = { + inferredBoard, + port, + type: 'board-overridden', + board, + }; + items.push(inferredItem); + } else { + items.push({ port, board: { fqbn, name } }); + } + } + } + } + items.sort(boardListItemComparator); + const length = items.length; + const findSelectedIndex = (): number => { + if (!isDefinedBoardsConfig(boardsConfig)) { + return -1; + } + const { selectedPort, selectedBoard } = boardsConfig; + const portKey = Port.keyOf(selectedPort); + // find the exact match of the board and port combination + for (let index = 0; index < length; index++) { + const item = items[index]; + const { board, port } = item; + if (!board) { + continue; + } + if ( + Port.keyOf(port) === portKey && + boardIdentifierEquals(board, selectedBoard) + ) { + return index; + } + } + // find match from inferred board + for (let index = 0; index < length; index++) { + const item = items[index]; + if (!isInferredBoardListItem(item)) { + continue; + } + const { inferredBoard, port } = item; + if ( + Port.keyOf(port) === portKey && + boardIdentifierEquals(inferredBoard, boardsConfig.selectedBoard) + ) { + return index; + } + } + return -1; + }; + + let _selectedIndex: number | undefined; + let _allPorts: DetectedPort[] | undefined; + const ports = ( + predicate: (detectedPort: DetectedPort) => boolean = () => true + ) => { + if (!_allPorts) { + _allPorts = []; + // to keep the order or the detected ports + const visitedPortKeys = new Set(); + for (let i = 0; i < length; i++) { + const { port } = items[i]; + const portKey = Port.keyOf(port); + if (!visitedPortKeys.has(portKey)) { + visitedPortKeys.add(portKey); + const detectedPort = detectedPorts[portKey]; + if (detectedPort) { + _allPorts.push(detectedPort); + } + } + } + } + const ports = _allPorts.filter(predicate); + const matchingIndex = findMatchingPortIndex( + boardsConfig.selectedPort, + ports + ); + return Object.assign(ports, { matchingIndex }); + }; + const selectedIndexMemoized = () => { + if (typeof _selectedIndex !== 'number') { + _selectedIndex = findSelectedIndex(); + } + return _selectedIndex; + }; + + let _boards: (BoardListItemWithBoard | InferredBoardListItem)[] | undefined; + const boardList: BoardList = Object.assign(items, { + boardsConfig, + get selectedIndex() { + return selectedIndexMemoized(); + }, + get boards() { + if (!_boards) { + _boards = []; + for (let i = 0; i < length; i++) { + const item = items[i]; + if (isInferredBoardListItem(item)) { + _boards.push(item); + } else if (item.board?.fqbn) { + _boards.push(>item); + } + } + } + return _boards; + }, + ports(predicate?: (detectedPort: DetectedPort) => boolean) { + return ports(predicate); + }, + portsGroupedByProtocol() { + const result: Record = + {}; + const allPorts = ports(); + for (const detectedPort of allPorts) { + const protocol = detectedPort.port.protocol; + if (!result[protocol]) { + result[protocol] = Object.assign([], { + matchingIndex: -1, + }); + } + const portsOnProtocol = result[protocol]; + portsOnProtocol.push(detectedPort); + } + const matchItem = allPorts[allPorts.matchingIndex]; + // match index is per all ports, IDE2 needs to adjust it per protocol + if (matchItem) { + const matchProtocol = matchItem.port.protocol; + const matchPorts = result[matchProtocol]; + matchPorts.matchingIndex = matchPorts.indexOf(matchItem); + } + return result; + }, + toString() { + const selectedIndex = selectedIndexMemoized(); + return JSON.stringify( + { + detectedPorts, + boardsConfig, + items, + selectedIndex, + boardListHistory, + }, + null, + 2 + ); + }, + }); + return boardList; +} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 7e2f77555..392703657 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,8 +1,6 @@ -import { naturalCompare } from './../utils'; -import { Searchable } from './searchable'; -import { Installable } from './installable'; -import { ArduinoComponent } from './arduino-component'; import { nls } from '@theia/core/lib/common/nls'; +import type { MaybePromise } from '@theia/core/lib/common/types'; +import URI from '@theia/core/lib/common/uri'; import { All, Contributed, @@ -10,131 +8,46 @@ import { Type as TypeLabel, Updatable, } from '../nls'; -import URI from '@theia/core/lib/common/uri'; -import { MaybePromise } from '@theia/core/lib/common/types'; - -export type AvailablePorts = Record]>; -export namespace AvailablePorts { - export function groupByProtocol( - availablePorts: AvailablePorts - ): Map { - const grouped = new Map(); - for (const portID of Object.keys(availablePorts)) { - const [port, boards] = availablePorts[portID]; - let ports = grouped.get(port.protocol); - if (!ports) { - ports = {} as AvailablePorts; - } - ports[portID] = [port, boards]; - grouped.set(port.protocol, ports); - } - return grouped; - } - export function split( - state: AvailablePorts - ): Readonly<{ boards: Board[]; ports: Port[] }> { - const availablePorts: Port[] = []; - const attachedBoards: Board[] = []; - for (const key of Object.keys(state)) { - const [port, boards] = state[key]; - availablePorts.push(port); - attachedBoards.push(...boards); - } - return { - boards: attachedBoards, - ports: availablePorts, - }; - } -} +import { Defined } from '../types'; +import { naturalCompare } from './../utils'; +import type { ArduinoComponent } from './arduino-component'; +import type { BoardList } from './board-list'; +import { Installable } from './installable'; +import { Searchable } from './searchable'; -export interface AttachedBoardsChangeEvent { - readonly oldState: Readonly<{ boards: Board[]; ports: Port[] }>; - readonly newState: Readonly<{ boards: Board[]; ports: Port[] }>; - readonly uploadInProgress: boolean; +export interface DetectedPort { + readonly port: Port; + readonly boards?: Pick[]; } -export namespace AttachedBoardsChangeEvent { - export function isEmpty(event: AttachedBoardsChangeEvent): boolean { - const { detached, attached } = diff(event); - return ( - !!detached.boards.length && - !!detached.ports.length && - !!attached.boards.length && - !!attached.ports.length - ); - } - export function toString(event: AttachedBoardsChangeEvent): string { - const rows: string[] = []; - if (!isEmpty(event)) { - const { attached, detached } = diff(event); - const visitedAttachedPorts: Port[] = []; - const visitedDetachedPorts: Port[] = []; - for (const board of attached.boards) { - const port = board.port ? ` on ${Port.toString(board.port)}` : ''; - rows.push(` - Attached board: ${Board.toString(board)}${port}`); - if (board.port) { - visitedAttachedPorts.push(board.port); - } - } - for (const board of detached.boards) { - const port = board.port ? ` from ${Port.toString(board.port)}` : ''; - rows.push(` - Detached board: ${Board.toString(board)}${port}`); - if (board.port) { - visitedDetachedPorts.push(board.port); - } - } - for (const port of attached.ports) { - if (!visitedAttachedPorts.find((p) => Port.sameAs(port, p))) { - rows.push(` - New port is available on ${Port.toString(port)}`); - } - } - for (const port of detached.ports) { - if (!visitedDetachedPorts.find((p) => Port.sameAs(port, p))) { - rows.push(` - Port is no longer available on ${Port.toString(port)}`); - } - } - } - return rows.length ? rows.join('\n') : 'No changes.'; +export function findMatchingPortIndex( + toFind: PortIdentifier | undefined, + ports: readonly DetectedPort[] | readonly Port[] +): number { + if (!toFind) { + return -1; } + const toFindPortKey = Port.keyOf(toFind); + return ports.findIndex((port) => Port.keyOf(port) === toFindPortKey); +} - export function diff(event: AttachedBoardsChangeEvent): Readonly<{ - attached: { - boards: Board[]; - ports: Port[]; - }; - detached: { - boards: Board[]; - ports: Port[]; - }; - }> { - // In `lefts` AND not in `rights`. - const diff = ( - lefts: T[], - rights: T[], - sameAs: (left: T, right: T) => boolean - ) => { - return lefts.filter( - (left) => rights.findIndex((right) => sameAs(left, right)) === -1 - ); - }; - const { boards: newBoards } = event.newState; - const { boards: oldBoards } = event.oldState; - const { ports: newPorts } = event.newState; - const { ports: oldPorts } = event.oldState; - const boardSameAs = (left: Board, right: Board) => - Board.sameAs(left, right); - const portSameAs = (left: Port, right: Port) => Port.sameAs(left, right); - return { - detached: { - boards: diff(oldBoards, newBoards, boardSameAs), - ports: diff(oldPorts, newPorts, portSameAs), - }, - attached: { - boards: diff(newBoards, oldBoards, boardSameAs), - ports: diff(newPorts, oldPorts, portSameAs), - }, - }; +/** + * The closest representation what the Arduino CLI detects with the `board list --watch` gRPC equivalent. + * The keys are unique identifiers generated from the port object (via `Port#keyOf`). + * The values are the detected ports with all their optional `properties` and matching board list. + */ +export type DetectedPorts = Readonly>; + +export function resolveDetectedPort( + port: PortIdentifier, + detectedPorts: DetectedPorts +): Port | undefined { + const portKey = Port.keyOf(port); + const detectedPort = detectedPorts[portKey]; + if (detectedPort) { + return detectedPort.port; } + return undefined; } export const BoardsServicePath = '/services/boards-service'; @@ -152,14 +65,17 @@ export interface BoardsService */ skipPostInstall?: boolean; }): Promise; - getState(): Promise; + getDetectedPorts(): Promise; getBoardDetails(options: { fqbn: string }): Promise; - getBoardPackage(options: { id: string }): Promise; + getBoardPackage(options: { + id: string /* TODO: change to PlatformIdentifier type? */; + }): Promise; getContainerBoardPackage(options: { fqbn: string; }): Promise; searchBoards({ query }: { query?: string }): Promise; getInstalledBoards(): Promise; + getInstalledPlatforms(): Promise; getBoardUserFields(options: { fqbn: string; protocol: string; @@ -180,7 +96,7 @@ export namespace BoardSearch { 'Partner', 'Arduino@Heart', ] as const; - export type Type = typeof TypeLiterals[number]; + export type Type = (typeof TypeLiterals)[number]; export namespace Type { export function is(arg: unknown): arg is Type { return typeof arg === 'string' && TypeLiterals.includes(arg as Type); @@ -252,9 +168,9 @@ export namespace Port { export namespace Properties { export function create( properties: [string, string][] | undefined - ): Properties { - if (!properties) { - return {}; + ): Properties | undefined { + if (!properties || !properties.length) { + return undefined; } return properties.reduce((acc, curr) => { const [key, value] = curr; @@ -282,10 +198,13 @@ export namespace Port { } /** - * Key is the combination of address and protocol formatted like `'${address}|${protocol}'` used to uniquely identify a port. + * Key is the combination of address and protocol formatted like `'arduino+${protocol}://${address}'` used to uniquely identify a port. */ - export function keyOf({ address, protocol }: Port): string { - return `${address}|${protocol}`; + export function keyOf(port: PortIdentifier | Port | DetectedPort): string { + if (isPortIdentifier(port)) { + return `arduino+${port.protocol}://${port.address}`; + } + return keyOf(port.port); } export function toString({ addressLabel, protocolLabel }: Port): string { @@ -297,16 +216,8 @@ export namespace Port { // 1. Serial // 2. Network // 3. Other protocols - if (left.protocol === 'serial' && right.protocol !== 'serial') { - return -1; - } else if (left.protocol !== 'serial' && right.protocol === 'serial') { - return 1; - } else if (left.protocol === 'network' && right.protocol !== 'network') { - return -1; - } else if (left.protocol !== 'network' && right.protocol === 'network') { - return 1; - } - return naturalCompare(left.address!, right.address!); + const priorityResult = portProtocolComparator(left, right); + return priorityResult || naturalCompare(left.address, right.address); } export function sameAs( @@ -324,35 +235,28 @@ export namespace Port { /** * All ports with `'serial'` or `'network'` `protocol`, or any other port `protocol` that has at least one recognized board connected to. */ - export function visiblePorts( - boardsHaystack: ReadonlyArray - ): (port: Port) => boolean { - return (port: Port) => { - if (port.protocol === 'serial' || port.protocol === 'network') { - // Allow all `serial` and `network` boards. - // IDE2 must support better label for unrecognized `network` boards: https://github.com/arduino/arduino-ide/issues/1331 - return true; - } - // All other ports with different protocol are - // only shown if there is a recognized board - // connected - for (const board of boardsHaystack) { - if (board.port?.address === port.address) { - return true; - } - } - return false; - }; + export function isVisiblePort(detectedPort: DetectedPort): boolean { + const protocol = detectedPort.port.protocol; + if (protocol === 'serial' || protocol === 'network') { + // Allow all `serial` and `network` boards. + // IDE2 must support better label for unrecognized `network` boards: https://github.com/arduino/arduino-ide/issues/1331 + return true; + } + // All other ports with different protocol are + // only shown if there is a recognized board + // connected + return Boolean(detectedPort?.boards?.length); } export namespace Protocols { + // IDE2 does not want to handle any other port protocols in a special way export const KnownProtocolLiterals = ['serial', 'network'] as const; - export type KnownProtocol = typeof KnownProtocolLiterals[number]; + export type KnownProtocol = (typeof KnownProtocolLiterals)[number]; export namespace KnownProtocol { export function is(protocol: unknown): protocol is KnownProtocol { return ( typeof protocol === 'string' && - KnownProtocolLiterals.indexOf(protocol as KnownProtocol) >= 0 + KnownProtocolLiterals.includes(protocol as KnownProtocol) ); } } @@ -377,29 +281,12 @@ export namespace BoardsPackage { export function equals(left: BoardsPackage, right: BoardsPackage): boolean { return left.id === right.id; } - - export function contains( - selectedBoard: Board, - { id, boards }: BoardsPackage - ): boolean { - if (boards.some((board) => Board.sameAs(board, selectedBoard))) { - return true; - } - if (selectedBoard.fqbn) { - const [platform, architecture] = selectedBoard.fqbn.split(':'); - if (platform && architecture) { - return `${platform}:${architecture}` === id; - } - } - return false; - } } -export interface Board { - readonly name: string; - readonly fqbn?: string; - readonly port?: Port; -} +/** + * @deprecated user `BoardIdentifier` instead. + */ +export type Board = BoardIdentifier; export interface BoardUserField { readonly toolId: string; @@ -411,14 +298,19 @@ export interface BoardUserField { export interface BoardWithPackage extends Board { readonly packageName: string; - readonly packageId: string; + readonly packageId: PlatformIdentifier; readonly manuallyInstalled: boolean; } export namespace BoardWithPackage { - export function is( - board: Board & Partial<{ packageName: string; packageId: string }> - ): board is BoardWithPackage { - return !!board.packageId && !!board.packageName; + export function is(arg: unknown): arg is BoardWithPackage { + return ( + isBoardIdentifier(arg) && + (arg).packageName !== undefined && + typeof (arg).packageName === 'string' && + isPlatformIdentifier((arg).packageId) && + (arg).manuallyInstalled !== undefined && + typeof (arg).manuallyInstalled === 'boolean' + ); } } @@ -456,18 +348,6 @@ export interface ConfigOption { readonly values: ConfigValue[]; } export namespace ConfigOption { - export function is(arg: any): arg is ConfigOption { - return ( - !!arg && - 'option' in arg && - 'label' in arg && - 'values' in arg && - typeof arg['option'] === 'string' && - typeof arg['label'] === 'string' && - Array.isArray(arg['values']) - ); - } - /** * Appends the configuration options to the `fqbn` argument. * Throws an error if the `fqbn` does not have the `segment(':'segment)*` format. @@ -555,24 +435,15 @@ export namespace Board { return left.name === right.name && left.fqbn === right.fqbn; } - export function hardwareIdEquals(left: Board, right: Board): boolean { - if (left.port && right.port) { - const { hardwareId: leftHardwareId } = left.port; - const { hardwareId: rightHardwareId } = right.port; - - if (leftHardwareId && rightHardwareId) { - return leftHardwareId === rightHardwareId; - } - } - - return false; - } - - export function sameAs(left: Board, right: string | Board): boolean { + export function sameAs( + left: BoardIdentifier, + right: string | BoardIdentifier + ): boolean { // How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200 // 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100 // 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ - const other = typeof right === 'string' ? { name: right } : right; + const other: BoardIdentifier = + typeof right === 'string' ? { name: right, fqbn: undefined } : right; if (left.fqbn && other.fqbn) { return left.fqbn === other.fqbn; } @@ -594,7 +465,7 @@ export namespace Board { } export function toString( - board: Board, + board: BoardIdentifier, options: { useFqbn: boolean } = { useFqbn: true } ): string { const fqbn = @@ -607,14 +478,15 @@ export namespace Board { selected: boolean; missing: boolean; packageName: string; - packageId: string; + packageId: PlatformIdentifier; details?: string; manuallyInstalled: boolean; }>; export function decorateBoards( - selectedBoard: Board | undefined, + selectedBoard: BoardIdentifier | BoardWithPackage | undefined, boards: Array ): Array { + let foundSelected = false; // Board names are not unique. We show the corresponding core name as a detail. // https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948 const distinctBoardNames = new Map(); @@ -622,21 +494,42 @@ export namespace Board { 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: BoardWithPackage) => { - if (!!selectedBoard) { - if (Board.equals(board, selectedBoard)) { - if ('packageName' in selectedBoard) { - return board.packageName === (selectedBoard as any).packageName; - } - if ('packageId' in selectedBoard) { - return board.packageId === (selectedBoard as any).packageId; + const selectedBoardPackageId = selectedBoard + ? createPlatformIdentifier(selectedBoard) + : undefined; + const selectedBoardFqbn = selectedBoard?.fqbn; + // Due to the non-unique board names, IDE2 has to check the package name when boards are not installed and the FQBN is absent. + const isSelected = (board: BoardWithPackage) => { + if (!selectedBoard) { + return false; + } + if (foundSelected) { + return false; + } + let selected = false; + if (board.fqbn && selectedBoardFqbn) { + if (boardIdentifierEquals(board, selectedBoard)) { + selected = true; + } + } + if (!selected) { + if (board.name === selectedBoard.name) { + if (selectedBoardPackageId) { + const boardPackageId = createPlatformIdentifier(board); + if (boardPackageId) { + if ( + platformIdentifierEquals(boardPackageId, selectedBoardPackageId) + ) { + selected = true; + } + } } - return true; } } - return false; + if (selected) { + foundSelected = true; + } + return selected; }; return boards.map((board) => ({ ...board, @@ -644,7 +537,7 @@ export namespace Board { (distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined, - selected: selected(board), + selected: isSelected(board), missing: !installed(board), })); } @@ -674,11 +567,255 @@ export function sanitizeFqbn(fqbn: string | undefined): string | undefined { return `${vendor}:${arch}:${id}`; } -export interface BoardConfig { - selectedBoard?: Board; - selectedPort?: Port; +export type PlatformIdentifier = Readonly<{ vendorId: string; arch: string }>; +export function createPlatformIdentifier( + board: BoardWithPackage +): PlatformIdentifier; +export function createPlatformIdentifier( + board: BoardIdentifier +): PlatformIdentifier | undefined; +export function createPlatformIdentifier( + fqbn: string +): PlatformIdentifier | undefined; +export function createPlatformIdentifier( + arg: BoardIdentifier | BoardWithPackage | string +): PlatformIdentifier | undefined { + if (BoardWithPackage.is(arg)) { + return arg.packageId; + } + const toSplit = typeof arg === 'string' ? arg : arg.fqbn; + if (toSplit) { + const [vendorId, arch] = toSplit.split(':'); + if (vendorId && arch) { + return { vendorId, arch }; + } + } + return undefined; +} + +export function isPlatformIdentifier(arg: unknown): arg is PlatformIdentifier { + return ( + Boolean(arg) && + typeof arg === 'object' && + (arg).vendorId !== undefined && + typeof (arg).vendorId === 'string' && + (arg).arch !== undefined && + typeof (arg).arch === 'string' + ); +} + +export function serializePlatformIdentifier({ + vendorId, + arch, +}: PlatformIdentifier): string { + return `${vendorId}:${arch}`; +} + +export function platformIdentifierEquals( + left: PlatformIdentifier, + right: PlatformIdentifier +) { + return left.vendorId === right.vendorId && left.arch === right.arch; +} + +/** + * Bare minimum information to identify port. + */ +export type PortIdentifier = Readonly>; + +export function portIdentifierEquals( + left: PortIdentifier | undefined, + right: PortIdentifier | undefined +): boolean { + if (!left) { + return !right; + } + if (!right) { + return !left; + } + return left.protocol === right.protocol && left.address === right.address; +} + +export function isPortIdentifier(arg: unknown): arg is PortIdentifier { + return ( + Boolean(arg) && + typeof arg === 'object' && + (arg).protocol !== undefined && + typeof (arg).protocol === 'string' && + (arg).address !== undefined && + typeof (arg).address === 'string' + ); +} + +// the smaller the number, the higher the priority +const portProtocolPriorities: Record = { + serial: 0, + network: 1, +} as const; + +/** + * See `boardListItemComparator`. + */ +export function portProtocolComparator( + left: PortIdentifier, + right: PortIdentifier +): number { + const leftPriority = + portProtocolPriorities[left.protocol] ?? Number.MAX_SAFE_INTEGER; + const rightPriority = + portProtocolPriorities[right.protocol] ?? Number.MAX_SAFE_INTEGER; + return leftPriority - rightPriority; +} + +/** + * Lightweight information to identify a board.\ + * \ + * Note: the `name` property of the board identifier must never participate in the board's identification. + * Hence, it should only be used as the final fallback for the UI when the board's platform is not installed and only the board's name is available. + */ +export interface BoardIdentifier { + /** + * The name of the board. It's only purpose is to provide a fallback for the UI. Preferably do not use this property for any sophisticated logic. When + */ + readonly name: string; + /** + * The FQBN might contain boards config options if selected from the discovered ports (see [arduino/arduino-ide#1588](https://github.com/arduino/arduino-ide/issues/1588)). + */ + // TODO: decide whether to persist the boards config if any + readonly fqbn: string | undefined; +} + +export function isBoardIdentifier(arg: unknown): arg is BoardIdentifier { + return ( + Boolean(arg) && + typeof arg === 'object' && + (arg).name !== undefined && + typeof (arg).name === 'string' && + ((arg).fqbn === undefined || + ((arg).fqbn !== undefined && + typeof (arg).fqbn === 'string')) + ); } +/** + * @param options if `looseFqbn` is `true`, FQBN config options are ignored. Hence, `{ name: 'x', fqbn: 'a:b:c:o1=v1 }` equals `{ name: 'y', fqbn: 'a:b:c' }`. It's `true` by default. + */ +export function boardIdentifierEquals( + left: BoardIdentifier | undefined, + right: BoardIdentifier | undefined, + options: { looseFqbn: boolean } = { looseFqbn: true } +): boolean { + if (!left) { + return !right; + } + if (!right) { + return !left; + } + if ((left.fqbn && !right.fqbn) || (!left.fqbn && right.fqbn)) { + // This can be very tricky when comparing boards + // the CLI's board search returns with falsy FQBN when the platform is not installed + // the CLI's board list returns with the full FQBN (for detected boards) even if the platform is not installed + // when there are multiple boards with the same name (Arduino Nano RP2040) from different platforms (Mbed Nano OS vs. the deprecated global Mbed OS) + // maybe add some 3rd party platform overhead (https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json) + // and it will get very tricky when comparing a board which has a FQBN and which does not. + return false; // TODO: This a strict now. Maybe compare name in the future. + } + if (left.fqbn && right.fqbn) { + const leftFqbn = options.looseFqbn ? sanitizeFqbn(left.fqbn) : left.fqbn; + const rightFqbn = options.looseFqbn ? sanitizeFqbn(right.fqbn) : right.fqbn; + return leftFqbn === rightFqbn; + } + // No more Genuino hack. + // https://github.com/arduino/arduino-ide/blob/f6a43254f5c416a2e4fa888875358336b42dd4d5/arduino-ide-extension/src/common/protocol/boards-service.ts#L572-L581 + return left.name === right.name; +} + +/** + * See `boardListItemComparator`. + */ +export function boardIdentifierComparator( + left: BoardIdentifier | undefined, + right: BoardIdentifier | undefined +): number { + if (!left) { + return right ? 1 : 0; + } + if (!right) { + return left ? -1 : 0; + } + let leftVendor: string | undefined = undefined; + let rightVendor: string | undefined = undefined; + if (left.fqbn) { + const [vendor] = left.fqbn.split(':'); + leftVendor = vendor; + } + if (right.fqbn) { + const [vendor] = right.fqbn.split(':'); + rightVendor = vendor; + } + if (leftVendor === 'arduino' && rightVendor !== 'arduino') { + return -1; + } + if (leftVendor !== 'arduino' && rightVendor === 'arduino') { + return 1; + } + return naturalCompare(left.name, right.name); +} + +export interface BoardsConfig { + selectedBoard: BoardIdentifier | undefined; + selectedPort: PortIdentifier | undefined; +} + +/** + * Creates a new board config object with `undefined` properties. + */ +export function emptyBoardsConfig(): BoardsConfig { + return { + selectedBoard: undefined, + selectedPort: undefined, + }; +} + +export function isDefinedBoardsConfig( + boardsConfig: BoardsConfig | undefined +): boardsConfig is Defined { + if (!boardsConfig) { + return false; + } + return ( + boardsConfig.selectedBoard !== undefined && + boardsConfig.selectedPort !== undefined + ); +} + +export interface BoardIdentifierChangeEvent { + readonly previousSelectedBoard: BoardIdentifier | undefined; + readonly selectedBoard: BoardIdentifier | undefined; +} + +export function isBoardIdentifierChangeEvent( + event: BoardsConfigChangeEvent +): event is BoardIdentifierChangeEvent { + return 'previousSelectedBoard' in event && 'selectedBoard' in event; +} + +export interface PortIdentifierChangeEvent { + readonly previousSelectedPort: PortIdentifier | undefined; + readonly selectedPort: PortIdentifier | undefined; +} + +export function isPortIdentifierChangeEvent( + event: BoardsConfigChangeEvent +): event is PortIdentifierChangeEvent { + return 'previousSelectedPort' in event && 'selectedPort' in event; +} + +export type BoardsConfigChangeEvent = + | BoardIdentifierChangeEvent + | PortIdentifierChangeEvent + | (BoardIdentifierChangeEvent & PortIdentifierChangeEvent); + export interface BoardInfo { /** * Board name. Could be `'Unknown board`'. @@ -719,45 +856,40 @@ export const unknownBoard = nls.localize( * The returned promise resolves to a `BoardInfo` if available to show in the UI or an info message explaining why showing the board info is not possible. */ export async function getBoardInfo( - selectedPort: Port | undefined, - availablePorts: MaybePromise + boardListProvider: MaybePromise ): Promise { - if (!selectedPort) { + const boardList = await boardListProvider; + const ports = boardList.ports(); + const detectedPort = ports[ports.matchingIndex]; + if (!detectedPort) { return selectPortForInfo; } + const { port: selectedPort, boards } = detectedPort; // IDE2 must show the board info based on the selected port. // https://github.com/arduino/arduino-ide/issues/1489 // IDE 1.x supports only serial port protocol if (selectedPort.protocol !== 'serial') { return nonSerialPort; } - const selectedPortKey = Port.keyOf(selectedPort); - const state = await availablePorts; - const boardListOnSelectedPort = Object.entries(state).filter( - ([portKey, [port]]) => - portKey === selectedPortKey && isNonNativeSerial(port) - ); - - if (!boardListOnSelectedPort.length) { + if (!isNonNativeSerial(selectedPort)) { return noNativeSerialPort; } - const [, [port, boards]] = boardListOnSelectedPort[0]; - if (boardListOnSelectedPort.length > 1 || boards.length > 1) { + if (boards && boards.length > 1) { console.warn( `Detected more than one available boards on the selected port : ${JSON.stringify( - selectedPort + detectedPort )}. Detected boards were: ${JSON.stringify( - boardListOnSelectedPort - )}. Using the first one: ${JSON.stringify([port, boards])}` + boards + )}. Using the first one: ${JSON.stringify(boards[0])}` ); } - const board = boards[0]; + const board = boards ? boards[0] : undefined; const BN = board?.name ?? unknownBoard; - const VID = readProperty('vid', port); - const PID = readProperty('pid', port); - const SN = readProperty('serialNumber', port); + const VID = readProperty('vid', selectedPort); + const PID = readProperty('pid', selectedPort); + const SN = readProperty('serialNumber', selectedPort); return { VID, PID, SN, BN }; } diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 2a683370d..b067aa5b1 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,15 +1,15 @@ -import { nls } from '@theia/core/lib/common/nls'; import { ApplicationError } from '@theia/core/lib/common/application-error'; +import { nls } from '@theia/core/lib/common/nls'; import type { Location, - Range, Position, + Range, } from '@theia/core/shared/vscode-languageserver-protocol'; -import type { BoardUserField, Port, Installable } from '../../common/protocol/'; -import type { Programmer } from './boards-service'; -import type { Sketch } from './sketches-service'; -import { IndexUpdateSummary } from './notification-service'; import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api'; +import type { BoardUserField, Installable } from '../../common/protocol/'; +import { isPortIdentifier, PortIdentifier, Programmer } from './boards-service'; +import type { IndexUpdateSummary } from './notification-service'; +import type { Sketch } from './sketches-service'; export const CompilerWarningLiterals = [ 'None', @@ -148,11 +148,25 @@ export function isCompileSummary(arg: unknown): arg is CompileSummary { ); } +export interface UploadResponse { + readonly portBeforeUpload?: PortIdentifier | undefined; + readonly portAfterUpload: PortIdentifier; +} +export function isUploadResponse(arg: unknown): arg is UploadResponse { + return ( + Boolean(arg) && + typeof arg === 'object' && + ((arg).portBeforeUpload === undefined || + isPortIdentifier((arg).portBeforeUpload)) && + isPortIdentifier((arg).portAfterUpload) + ); +} + export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); export interface CoreService { compile(options: CoreService.Options.Compile): Promise; - upload(options: CoreService.Options.Upload): Promise; + upload(options: CoreService.Options.Upload): Promise; burnBootloader(options: CoreService.Options.Bootloader): Promise; /** * Refreshes the underling core gRPC client for the Arduino CLI. @@ -198,7 +212,7 @@ export namespace CoreService { readonly sketch: Sketch; } export interface BoardBased { - readonly port?: Port; + readonly port?: PortIdentifier; readonly programmer?: Programmer | undefined; /** * For the _Verify after upload_ setting. diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 5eb793f5b..92fb7e4a6 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -1,5 +1,8 @@ -import { ApplicationError, Event, JsonRpcServer, nls } from '@theia/core'; -import { Board, Port } from './boards-service'; +import { ApplicationError } from '@theia/core/lib/common/application-error'; +import type { Event } from '@theia/core/lib/common/event'; +import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { nls } from '@theia/core/lib/common/nls'; +import type { BoardIdentifier, PortIdentifier } from './boards-service'; export type PluggableMonitorSettings = Record; export interface MonitorSettings { @@ -15,17 +18,20 @@ export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); export interface MonitorManagerProxy extends JsonRpcServer { startMonitor( - board: Board, - port: Port, + board: BoardIdentifier, + port: PortIdentifier, settings?: PluggableMonitorSettings ): Promise; changeMonitorSettings( - board: Board, - port: Port, + board: BoardIdentifier, + port: PortIdentifier, settings: MonitorSettings ): Promise; - stopMonitor(board: Board, port: Port): Promise; - getCurrentSettings(board: Board, port: Port): Promise; + stopMonitor(board: BoardIdentifier, port: PortIdentifier): Promise; + getCurrentSettings( + board: BoardIdentifier, + port: PortIdentifier + ): Promise; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); @@ -38,7 +44,10 @@ export interface MonitorManagerProxyClient { getWebSocketPort(): number | undefined; isWSConnected(): Promise; startMonitor(settings?: PluggableMonitorSettings): Promise; - getCurrentSettings(board: Board, port: Port): Promise; + getCurrentSettings( + board: BoardIdentifier, + port: PortIdentifier + ): Promise; send(message: string): void; changeSettings(settings: MonitorSettings): void; } @@ -95,7 +104,7 @@ export const MissingConfigurationError = declareMonitorError( ); export function createConnectionFailedError( - port: Port, + port: PortIdentifier, details?: string ): ApplicationError { const { protocol, address } = port; @@ -120,7 +129,7 @@ export function createConnectionFailedError( return ConnectionFailedError(message, { protocol, address }); } export function createNotConnectedError( - port: Port + port: PortIdentifier ): ApplicationError { const { protocol, address } = port; return NotConnectedError( @@ -134,7 +143,7 @@ export function createNotConnectedError( ); } export function createAlreadyConnectedError( - port: Port + port: PortIdentifier ): ApplicationError { const { protocol, address } = port; return AlreadyConnectedError( @@ -148,7 +157,7 @@ export function createAlreadyConnectedError( ); } export function createMissingConfigurationError( - port: Port + port: PortIdentifier ): ApplicationError { const { protocol, address } = port; return MissingConfigurationError( diff --git a/arduino-ide-extension/src/common/protocol/notification-service.ts b/arduino-ide-extension/src/common/protocol/notification-service.ts index eba8f798e..9ad5c202d 100644 --- a/arduino-ide-extension/src/common/protocol/notification-service.ts +++ b/arduino-ide-extension/src/common/protocol/notification-service.ts @@ -1,11 +1,11 @@ import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import type { - AttachedBoardsChangeEvent, BoardsPackage, ConfigState, + DetectedPorts, + IndexType, ProgressMessage, Sketch, - IndexType, } from '../protocol'; import type { LibraryPackage } from './library-service'; @@ -68,7 +68,9 @@ export interface NotificationServiceClient { notifyLibraryDidUninstall(event: { item: LibraryPackage }): void; // Boards discovery - notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void; + notifyDetectedPortsDidChange(event: { detectedPorts: DetectedPorts }): void; + + // Sketches notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void; } diff --git a/arduino-ide-extension/src/common/types.ts b/arduino-ide-extension/src/common/types.ts index 421a27534..c73987650 100644 --- a/arduino-ide-extension/src/common/types.ts +++ b/arduino-ide-extension/src/common/types.ts @@ -1,3 +1,7 @@ export type RecursiveRequired = { [P in keyof T]-?: RecursiveRequired; }; + +export type Defined = { + [P in keyof T]: NonNullable; +}; diff --git a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts index c8c4b3578..2ec810aca 100644 --- a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts +++ b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts @@ -65,7 +65,7 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader { ]); return output; } finally { - await this.monitorManager.notifyUploadFinished(board.fqbn, port); + await this.monitorManager.notifyUploadFinished(board.fqbn, port, port); // here the before and after ports are assumed to be always the same } } diff --git a/arduino-ide-extension/src/node/board-discovery.ts b/arduino-ide-extension/src/node/board-discovery.ts index 8699b7232..3ca946e0b 100644 --- a/arduino-ide-extension/src/node/board-discovery.ts +++ b/arduino-ide-extension/src/node/board-discovery.ts @@ -1,18 +1,22 @@ -import { ClientDuplexStream } from '@grpc/grpc-js'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import type { ClientDuplexStream } from '@grpc/grpc-js'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; import { deepClone } from '@theia/core/lib/common/objects'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { BackendApplicationContribution } from '@theia/core/lib/node'; +import type { Mutable } from '@theia/core/lib/common/types'; +import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { Disposable } from '@theia/core/lib/common/disposable'; +import { isDeepStrictEqual } from 'util'; import { v4 } from 'uuid'; import { Unknown } from '../common/nls'; import { - AttachedBoardsChangeEvent, - AvailablePorts, Board, + DetectedPort, + DetectedPorts, NotificationServiceServer, Port, } from '../common/protocol'; @@ -22,7 +26,7 @@ import { DetectedPort as RpcDetectedPort, } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; -import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; +import type { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { CoreClientAware } from './core-client-provider'; import { ServiceError } from './service-error'; @@ -57,23 +61,9 @@ export class BoardDiscovery private readonly onStreamDidCancelEmitter = new Emitter(); // when the watcher is canceled by the IDE2 private readonly toDisposeOnStopWatch = new DisposableCollection(); - private uploadInProgress = false; - - /** - * Keys are the `address` of the ports. - * - * The `protocol` is ignored because the board detach event does not carry the protocol information, - * just the address. - * ```json - * { - * "type": "remove", - * "address": "/dev/cu.usbmodem14101" - * } - * ``` - */ - private _availablePorts: AvailablePorts = {}; - get availablePorts(): AvailablePorts { - return this._availablePorts; + private _detectedPorts: DetectedPorts = {}; + get detectedPorts(): DetectedPorts { + return this._detectedPorts; } onStart(): void { @@ -120,10 +110,6 @@ export class BoardDiscovery }); } - setUploadInProgress(uploadInProgress: boolean): void { - this.uploadInProgress = uploadInProgress; - } - private createTimeout( after: number, onTimeout: (error: Error) => void @@ -202,18 +188,6 @@ export class BoardDiscovery return wrapper; } - private toJson(arg: BoardListWatchRequest | BoardListWatchResponse): string { - let object: Record | undefined = undefined; - if (arg instanceof BoardListWatchRequest) { - object = BoardListWatchRequest.toObject(false, arg); - } else if (arg instanceof BoardListWatchResponse) { - object = BoardListWatchResponse.toObject(false, arg); - } else { - throw new Error(`Unhandled object type: ${arg}`); - } - return JSON.stringify(object); - } - async start(): Promise { this.logger.info('start'); if (this.stopping) { @@ -240,9 +214,8 @@ export class BoardDiscovery this.logger.info('start resolved watching'); } - // XXX: make this `protected` and override for tests if IDE2 wants to mock events from the CLI. - private onBoardListWatchResponse(resp: BoardListWatchResponse): void { - this.logger.info(this.toJson(resp)); + protected onBoardListWatchResponse(resp: BoardListWatchResponse): void { + this.logger.info(JSON.stringify(resp.toObject(false))); const eventType = EventType.parse(resp.getEventType()); if (eventType === EventType.Quit) { @@ -251,69 +224,36 @@ export class BoardDiscovery return; } - const detectedPort = resp.getPort(); - if (detectedPort) { - const { port, boards } = this.fromRpc(detectedPort); - if (!port) { - if (!!boards.length) { - console.warn( - `Could not detect the port, but unexpectedly received discovered boards. This is most likely a bug! Response was: ${this.toJson( - resp - )}` - ); - } - return; - } - const oldState = deepClone(this._availablePorts); - const newState = deepClone(this._availablePorts); - const key = Port.keyOf(port); - - if (eventType === EventType.Add) { - if (newState[key]) { - const [, knownBoards] = newState[key]; - this.logger.warn( - `Port '${Port.toString( - port - )}' was already available. Known boards before override: ${JSON.stringify( - knownBoards - )}` - ); - } - newState[key] = [port, boards]; - } else if (eventType === EventType.Remove) { - if (!newState[key]) { - this.logger.warn( - `Port '${Port.toString(port)}' was not available. Skipping` - ); - return; - } - delete newState[key]; + const rpcDetectedPort = resp.getPort(); + if (rpcDetectedPort) { + const detectedPort = this.fromRpc(rpcDetectedPort); + if (detectedPort) { + this.fireSoon({ detectedPort, eventType }); + } else { + this.logger.warn( + `Could not extract the detected port from ${rpcDetectedPort.toObject( + false + )}` + ); } - - const event: AttachedBoardsChangeEvent = { - oldState: { - ...AvailablePorts.split(oldState), - }, - newState: { - ...AvailablePorts.split(newState), - }, - uploadInProgress: this.uploadInProgress, - }; - - this._availablePorts = newState; - this.notificationService.notifyAttachedBoardsDidChange(event); + } else if (resp.getError()) { + this.logger.error( + `Could not extract any detected 'port' from the board list watch response. An 'error' has occurred: ${resp.getError()}` + ); } } - private fromRpc(detectedPort: RpcDetectedPort): DetectedPort { + private fromRpc(detectedPort: RpcDetectedPort): DetectedPort | undefined { const rpcPort = detectedPort.getPort(); - const port = rpcPort && this.fromRpcPort(rpcPort); + if (!rpcPort) { + return undefined; + } + const port = createApiPort(rpcPort); const boards = detectedPort.getMatchingBoardsList().map( (board) => ({ - fqbn: board.getFqbn(), + fqbn: board.getFqbn() || undefined, // prefer undefined fqbn over empty string name: board.getName() || Unknown, - port, } as Board) ); return { @@ -322,15 +262,55 @@ export class BoardDiscovery }; } - private fromRpcPort(rpcPort: RpcPort): Port { - return { - address: rpcPort.getAddress(), - addressLabel: rpcPort.getLabel(), - protocol: rpcPort.getProtocol(), - protocolLabel: rpcPort.getProtocolLabel(), - properties: Port.Properties.create(rpcPort.getPropertiesMap().toObject()), - hardwareId: rpcPort.getHardwareId(), - }; + private fireSoonHandle: NodeJS.Timeout | undefined; + private readonly bufferedEvents: DetectedPortChangeEvent[] = []; + private fireSoon(event: DetectedPortChangeEvent): void { + this.bufferedEvents.push(event); + clearTimeout(this.fireSoonHandle); + this.fireSoonHandle = setTimeout(() => { + const current = deepClone(this.detectedPorts); + const newState = this.calculateNewState(this.bufferedEvents, current); + if (!isDeepStrictEqual(current, newState)) { + this._detectedPorts = newState; + this.notificationService.notifyDetectedPortsDidChange({ + detectedPorts: this._detectedPorts, + }); + } + this.bufferedEvents.length = 0; + }, 100); + } + + private calculateNewState( + events: DetectedPortChangeEvent[], + prevState: Mutable + ): DetectedPorts { + const newState = deepClone(prevState); + for (const { detectedPort, eventType } of events) { + const { port, boards } = detectedPort; + const key = Port.keyOf(port); + if (eventType === EventType.Add) { + const alreadyDetectedPort = newState[key]; + if (alreadyDetectedPort) { + console.warn( + `Detected a new port that has been already discovered. The old value will be overridden. Old value: ${JSON.stringify( + alreadyDetectedPort + )}, new value: ${JSON.stringify(detectedPort)}` + ); + } + newState[key] = { port, boards }; + } else if (eventType === EventType.Remove) { + const alreadyDetectedPort = newState[key]; + if (!alreadyDetectedPort) { + console.warn( + `Detected a port removal but it has not been discovered. This is most likely a bug! Detected port was: ${JSON.stringify( + detectedPort + )}` + ); + } + delete newState[key]; + } + } + return newState; } } @@ -357,7 +337,18 @@ namespace EventType { } } -interface DetectedPort { - port: Port | undefined; - boards: Board[]; +interface DetectedPortChangeEvent { + readonly detectedPort: DetectedPort; + readonly eventType: EventType.Add | EventType.Remove; +} + +export function createApiPort(rpcPort: RpcPort): Port { + return { + address: rpcPort.getAddress(), + addressLabel: rpcPort.getLabel(), + protocol: rpcPort.getProtocol(), + protocolLabel: rpcPort.getProtocolLabel(), + properties: Port.Properties.create(rpcPort.getPropertiesMap().toObject()), + hardwareId: rpcPort.getHardwareId() || undefined, // prefer undefined over empty string + }; } diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index a2db13193..80f1446e0 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -12,13 +12,15 @@ import { Programmer, ResponseService, NotificationServiceServer, - AvailablePorts, + DetectedPorts, BoardWithPackage, BoardUserField, BoardSearch, sortComponents, SortGroup, platformInstallFailed, + createPlatformIdentifier, + platformIdentifierEquals, } from '../common/protocol'; import { PlatformInstallRequest, @@ -65,8 +67,8 @@ export class BoardsServiceImpl @inject(BoardDiscovery) protected readonly boardDiscovery: BoardDiscovery; - async getState(): Promise { - return this.boardDiscovery.availablePorts; + async getDetectedPorts(): Promise { + return this.boardDiscovery.detectedPorts; } async getBoardDetails(options: { @@ -165,7 +167,7 @@ export class BoardsServiceImpl debuggingSupported, VID, PID, - buildProperties + buildProperties, }; } @@ -212,6 +214,28 @@ export class BoardsServiceImpl return this.handleListBoards(client.boardListAll.bind(client), req); } + async getInstalledPlatforms(): Promise { + const { instance, client } = await this.coreClient; + return new Promise((resolve, reject) => { + client.platformList( + new PlatformListRequest().setInstance(instance), + (err, response) => { + if (err) { + reject(err); + return; + } + resolve( + response + .getInstalledPlatformsList() + .map((platform, _, installedPlatforms) => + toBoardsPackage(platform, installedPlatforms) + ) + ); + } + ); + }); + } + private async handleListBoards( getBoards: ( request: BoardListAllRequest | BoardSearchRequest, @@ -232,10 +256,38 @@ export class BoardsServiceImpl for (const board of resp.getBoardsList()) { const platform = board.getPlatform(); if (platform) { + const platformId = platform.getId(); + const fqbn = board.getFqbn() || undefined; // prefer undefined over empty string + const parsedPlatformId = createPlatformIdentifier(platformId); + if (!parsedPlatformId) { + console.warn( + `Could not create platform identifier from platform ID input: ${platform.getId()}. Skipping` + ); + continue; + } + if (fqbn) { + const checkPlatformId = createPlatformIdentifier(board.getFqbn()); + if (!checkPlatformId) { + console.warn( + `Could not create platform identifier from FQBN input: ${board.getFqbn()}. Skipping` + ); + continue; + } + if ( + !platformIdentifierEquals(parsedPlatformId, checkPlatformId) + ) { + console.warn( + `Mismatching platform identifiers. Platform: ${JSON.stringify( + parsedPlatformId + )}, FQBN: ${JSON.stringify(checkPlatformId)}. Skipping` + ); + continue; + } + } boards.push({ name: board.getName(), fqbn: board.getFqbn(), - packageId: platform.getId(), + packageId: parsedPlatformId, packageName: platform.getName(), manuallyInstalled: platform.getManuallyInstalled(), }); @@ -316,38 +368,6 @@ export class BoardsServiceImpl } ); const packages = new Map(); - const toPackage = (platform: Platform) => { - let installedVersion: string | undefined; - const matchingPlatform = installedPlatforms.find( - (ip) => ip.getId() === platform.getId() - ); - if (!!matchingPlatform) { - installedVersion = matchingPlatform.getInstalled(); - } - return { - id: platform.getId(), - name: platform.getName(), - author: platform.getMaintainer(), - availableVersions: [platform.getLatest()], - description: platform - .getBoardsList() - .map((b) => b.getName()) - .join(', '), - installable: true, - types: platform.getTypeList(), - deprecated: platform.getDeprecated(), - summary: nls.localize( - 'arduino/component/boardsIncluded', - 'Boards included in this package:' - ), - installedVersion, - boards: platform - .getBoardsList() - .map((b) => { name: b.getName(), fqbn: b.getFqbn() }), - moreInfoLink: platform.getWebsite(), - }; - }; - // We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order. // Otherwise we lose the FQBN information. const groupedById: Map = new Map(); @@ -400,7 +420,7 @@ export class BoardsServiceImpl pkg.availableVersions.push(platform.getLatest()); pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse(); } else { - packages.set(id, toPackage(platform)); + packages.set(id, toBoardsPackage(platform, installedPlatforms)); } } } @@ -572,3 +592,37 @@ function boardsPackageSortGroup(boardsPackage: BoardsPackage): SortGroup { } return types.join('-') as SortGroup; } + +function toBoardsPackage( + platform: Platform, + installedPlatforms: Platform[] +): BoardsPackage { + let installedVersion: string | undefined; + const matchingPlatform = installedPlatforms.find( + (ip) => ip.getId() === platform.getId() + ); + if (!!matchingPlatform) { + installedVersion = matchingPlatform.getInstalled(); + } + return { + id: platform.getId(), + name: platform.getName(), + author: platform.getMaintainer(), + availableVersions: [platform.getLatest()], + description: platform + .getBoardsList() + .map((b) => b.getName()) + .join(', '), + types: platform.getTypeList(), + deprecated: platform.getDeprecated(), + summary: nls.localize( + 'arduino/component/boardsIncluded', + 'Boards included in this package:' + ), + installedVersion, + boards: platform + .getBoardsList() + .map((b) => { name: b.getName(), fqbn: b.getFqbn() }), + moreInfoLink: platform.getWebsite(), + }; +} diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts index b321b3c23..ff15d4c43 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts @@ -26,6 +26,7 @@ interface IArduinoCoreServiceService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface IArduinoCoreServiceService_ISetSketchDefaults extends grpc.MethodDefinition { + path: "/cc.arduino.cli.commands.v1.ArduinoCoreService/SetSketchDefaults"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IArduinoCoreServiceService_IBoardDetails extends grpc.MethodDefinition { path: "/cc.arduino.cli.commands.v1.ArduinoCoreService/BoardDetails"; requestStream: false; @@ -412,6 +422,7 @@ export interface IArduinoCoreServiceServer { newSketch: grpc.handleUnaryCall; loadSketch: grpc.handleUnaryCall; archiveSketch: grpc.handleUnaryCall; + setSketchDefaults: grpc.handleUnaryCall; boardDetails: grpc.handleUnaryCall; boardList: grpc.handleUnaryCall; boardListAll: grpc.handleUnaryCall; @@ -468,6 +479,9 @@ export interface IArduinoCoreServiceClient { archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; + setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; + setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; + setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; @@ -568,6 +582,9 @@ export class ArduinoCoreServiceClient extends grpc.Client implements IArduinoCor public archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; public archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; public archiveSketch(request: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.ArchiveSketchResponse) => void): grpc.ClientUnaryCall; + public setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; + public setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; + public setSketchDefaults(request: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse) => void): grpc.ClientUnaryCall; public boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; public boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; public boardDetails(request: cc_arduino_cli_commands_v1_board_pb.BoardDetailsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_board_pb.BoardDetailsResponse) => void): grpc.ClientUnaryCall; diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.js index 49ec3d019..8947c7570 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.js @@ -709,6 +709,28 @@ function deserialize_cc_arduino_cli_commands_v1_PlatformUpgradeResponse(buffer_a return cc_arduino_cli_commands_v1_core_pb.PlatformUpgradeResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_cc_arduino_cli_commands_v1_SetSketchDefaultsRequest(arg) { + if (!(arg instanceof cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest)) { + throw new Error('Expected argument of type cc.arduino.cli.commands.v1.SetSketchDefaultsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_cc_arduino_cli_commands_v1_SetSketchDefaultsRequest(buffer_arg) { + return cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_cc_arduino_cli_commands_v1_SetSketchDefaultsResponse(arg) { + if (!(arg instanceof cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse)) { + throw new Error('Expected argument of type cc.arduino.cli.commands.v1.SetSketchDefaultsResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_cc_arduino_cli_commands_v1_SetSketchDefaultsResponse(buffer_arg) { + return cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_cc_arduino_cli_commands_v1_SupportedUserFieldsRequest(arg) { if (!(arg instanceof cc_arduino_cli_commands_v1_upload_pb.SupportedUserFieldsRequest)) { throw new Error('Expected argument of type cc.arduino.cli.commands.v1.SupportedUserFieldsRequest'); @@ -975,6 +997,20 @@ archiveSketch: { responseSerialize: serialize_cc_arduino_cli_commands_v1_ArchiveSketchResponse, responseDeserialize: deserialize_cc_arduino_cli_commands_v1_ArchiveSketchResponse, }, + // Sets the sketch default FQBN and Port Address/Protocol in +// the sketch project file (sketch.yaml). These metadata can be retrieved +// using LoadSketch. +setSketchDefaults: { + path: '/cc.arduino.cli.commands.v1.ArduinoCoreService/SetSketchDefaults', + requestStream: false, + responseStream: false, + requestType: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsRequest, + responseType: cc_arduino_cli_commands_v1_commands_pb.SetSketchDefaultsResponse, + requestSerialize: serialize_cc_arduino_cli_commands_v1_SetSketchDefaultsRequest, + requestDeserialize: deserialize_cc_arduino_cli_commands_v1_SetSketchDefaultsRequest, + responseSerialize: serialize_cc_arduino_cli_commands_v1_SetSketchDefaultsResponse, + responseDeserialize: deserialize_cc_arduino_cli_commands_v1_SetSketchDefaultsResponse, + }, // BOARD COMMANDS // -------------- // diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts index fc1c8b525..2bb88967b 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts @@ -377,12 +377,6 @@ export namespace VersionResponse { } export class NewSketchRequest extends jspb.Message { - - hasInstance(): boolean; - clearInstance(): void; - getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined; - setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): NewSketchRequest; - getSketchName(): string; setSketchName(value: string): NewSketchRequest; @@ -405,7 +399,6 @@ export class NewSketchRequest extends jspb.Message { export namespace NewSketchRequest { export type AsObject = { - instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject, sketchName: string, sketchDir: string, overwrite: boolean, @@ -434,12 +427,6 @@ export namespace NewSketchResponse { } export class LoadSketchRequest extends jspb.Message { - - hasInstance(): boolean; - clearInstance(): void; - getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined; - setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): LoadSketchRequest; - getSketchPath(): string; setSketchPath(value: string): LoadSketchRequest; @@ -456,7 +443,6 @@ export class LoadSketchRequest extends jspb.Message { export namespace LoadSketchRequest { export type AsObject = { - instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject, sketchPath: string, } } @@ -483,6 +469,15 @@ export class LoadSketchResponse extends jspb.Message { setRootFolderFilesList(value: Array): LoadSketchResponse; addRootFolderFiles(value: string, index?: number): string; + getDefaultFqbn(): string; + setDefaultFqbn(value: string): LoadSketchResponse; + + getDefaultPort(): string; + setDefaultPort(value: string): LoadSketchResponse; + + getDefaultProtocol(): string; + setDefaultProtocol(value: string): LoadSketchResponse; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): LoadSketchResponse.AsObject; @@ -501,6 +496,9 @@ export namespace LoadSketchResponse { otherSketchFilesList: Array, additionalFilesList: Array, rootFolderFilesList: Array, + defaultFqbn: string, + defaultPort: string, + defaultProtocol: string, } } @@ -554,6 +552,68 @@ export namespace ArchiveSketchResponse { } } +export class SetSketchDefaultsRequest extends jspb.Message { + getSketchPath(): string; + setSketchPath(value: string): SetSketchDefaultsRequest; + + getDefaultFqbn(): string; + setDefaultFqbn(value: string): SetSketchDefaultsRequest; + + getDefaultPortAddress(): string; + setDefaultPortAddress(value: string): SetSketchDefaultsRequest; + + getDefaultPortProtocol(): string; + setDefaultPortProtocol(value: string): SetSketchDefaultsRequest; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SetSketchDefaultsRequest.AsObject; + static toObject(includeInstance: boolean, msg: SetSketchDefaultsRequest): SetSketchDefaultsRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SetSketchDefaultsRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SetSketchDefaultsRequest; + static deserializeBinaryFromReader(message: SetSketchDefaultsRequest, reader: jspb.BinaryReader): SetSketchDefaultsRequest; +} + +export namespace SetSketchDefaultsRequest { + export type AsObject = { + sketchPath: string, + defaultFqbn: string, + defaultPortAddress: string, + defaultPortProtocol: string, + } +} + +export class SetSketchDefaultsResponse extends jspb.Message { + getDefaultFqbn(): string; + setDefaultFqbn(value: string): SetSketchDefaultsResponse; + + getDefaultPortAddress(): string; + setDefaultPortAddress(value: string): SetSketchDefaultsResponse; + + getDefaultPortProtocol(): string; + setDefaultPortProtocol(value: string): SetSketchDefaultsResponse; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SetSketchDefaultsResponse.AsObject; + static toObject(includeInstance: boolean, msg: SetSketchDefaultsResponse): SetSketchDefaultsResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SetSketchDefaultsResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SetSketchDefaultsResponse; + static deserializeBinaryFromReader(message: SetSketchDefaultsResponse, reader: jspb.BinaryReader): SetSketchDefaultsResponse; +} + +export namespace SetSketchDefaultsResponse { + export type AsObject = { + defaultFqbn: string, + defaultPortAddress: string, + defaultPortProtocol: string, + } +} + export enum FailedInstanceInitReason { FAILED_INSTANCE_INIT_REASON_UNSPECIFIED = 0, FAILED_INSTANCE_INIT_REASON_INVALID_INDEX_URL = 1, diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js index ead2d3881..14f1dfc5e 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js @@ -53,6 +53,8 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LoadSketchRequest', null, gl goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LoadSketchResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.NewSketchRequest', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.NewSketchResponse', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UpdateIndexRequest', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UpdateIndexResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UpdateLibrariesIndexRequest', null, global); @@ -479,6 +481,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.cc.arduino.cli.commands.v1.ArchiveSketchResponse.displayName = 'proto.cc.arduino.cli.commands.v1.ArchiveSketchResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.displayName = 'proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.displayName = 'proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse'; +} @@ -2733,7 +2777,6 @@ proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.toObject = function( */ proto.cc.arduino.cli.commands.v1.NewSketchRequest.toObject = function(includeInstance, msg) { var f, obj = { - instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f), sketchName: jspb.Message.getFieldWithDefault(msg, 2, ""), sketchDir: jspb.Message.getFieldWithDefault(msg, 3, ""), overwrite: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) @@ -2773,11 +2816,6 @@ proto.cc.arduino.cli.commands.v1.NewSketchRequest.deserializeBinaryFromReader = } var field = reader.getFieldNumber(); switch (field) { - case 1: - var value = new cc_arduino_cli_commands_v1_common_pb.Instance; - reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader); - msg.setInstance(value); - break; case 2: var value = /** @type {string} */ (reader.readString()); msg.setSketchName(value); @@ -2819,14 +2857,6 @@ proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.serializeBinary = fu */ proto.cc.arduino.cli.commands.v1.NewSketchRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getInstance(); - if (f != null) { - writer.writeMessage( - 1, - f, - cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter - ); - } f = message.getSketchName(); if (f.length > 0) { writer.writeString( @@ -2851,43 +2881,6 @@ proto.cc.arduino.cli.commands.v1.NewSketchRequest.serializeBinaryToWriter = func }; -/** - * optional Instance instance = 1; - * @return {?proto.cc.arduino.cli.commands.v1.Instance} - */ -proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.getInstance = function() { - return /** @type{?proto.cc.arduino.cli.commands.v1.Instance} */ ( - jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_common_pb.Instance, 1)); -}; - - -/** - * @param {?proto.cc.arduino.cli.commands.v1.Instance|undefined} value - * @return {!proto.cc.arduino.cli.commands.v1.NewSketchRequest} returns this -*/ -proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.setInstance = function(value) { - return jspb.Message.setWrapperField(this, 1, value); -}; - - -/** - * Clears the message field making it undefined. - * @return {!proto.cc.arduino.cli.commands.v1.NewSketchRequest} returns this - */ -proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.clearInstance = function() { - return this.setInstance(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.cc.arduino.cli.commands.v1.NewSketchRequest.prototype.hasInstance = function() { - return jspb.Message.getField(this, 1) != null; -}; - - /** * optional string sketch_name = 2; * @return {string} @@ -3104,7 +3097,6 @@ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.toObject = function */ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.toObject = function(includeInstance, msg) { var f, obj = { - instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f), sketchPath: jspb.Message.getFieldWithDefault(msg, 2, "") }; @@ -3142,11 +3134,6 @@ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.deserializeBinaryFromReader = } var field = reader.getFieldNumber(); switch (field) { - case 1: - var value = new cc_arduino_cli_commands_v1_common_pb.Instance; - reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader); - msg.setInstance(value); - break; case 2: var value = /** @type {string} */ (reader.readString()); msg.setSketchPath(value); @@ -3180,14 +3167,6 @@ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.serializeBinary = f */ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getInstance(); - if (f != null) { - writer.writeMessage( - 1, - f, - cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter - ); - } f = message.getSketchPath(); if (f.length > 0) { writer.writeString( @@ -3198,43 +3177,6 @@ proto.cc.arduino.cli.commands.v1.LoadSketchRequest.serializeBinaryToWriter = fun }; -/** - * optional Instance instance = 1; - * @return {?proto.cc.arduino.cli.commands.v1.Instance} - */ -proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.getInstance = function() { - return /** @type{?proto.cc.arduino.cli.commands.v1.Instance} */ ( - jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_common_pb.Instance, 1)); -}; - - -/** - * @param {?proto.cc.arduino.cli.commands.v1.Instance|undefined} value - * @return {!proto.cc.arduino.cli.commands.v1.LoadSketchRequest} returns this -*/ -proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.setInstance = function(value) { - return jspb.Message.setWrapperField(this, 1, value); -}; - - -/** - * Clears the message field making it undefined. - * @return {!proto.cc.arduino.cli.commands.v1.LoadSketchRequest} returns this - */ -proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.clearInstance = function() { - return this.setInstance(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.cc.arduino.cli.commands.v1.LoadSketchRequest.prototype.hasInstance = function() { - return jspb.Message.getField(this, 1) != null; -}; - - /** * optional string sketch_path = 2; * @return {string} @@ -3296,7 +3238,10 @@ proto.cc.arduino.cli.commands.v1.LoadSketchResponse.toObject = function(includeI locationPath: jspb.Message.getFieldWithDefault(msg, 2, ""), otherSketchFilesList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f, additionalFilesList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f, - rootFolderFilesList: (f = jspb.Message.getRepeatedField(msg, 5)) == null ? undefined : f + rootFolderFilesList: (f = jspb.Message.getRepeatedField(msg, 5)) == null ? undefined : f, + defaultFqbn: jspb.Message.getFieldWithDefault(msg, 6, ""), + defaultPort: jspb.Message.getFieldWithDefault(msg, 7, ""), + defaultProtocol: jspb.Message.getFieldWithDefault(msg, 8, "") }; if (includeInstance) { @@ -3353,6 +3298,18 @@ proto.cc.arduino.cli.commands.v1.LoadSketchResponse.deserializeBinaryFromReader var value = /** @type {string} */ (reader.readString()); msg.addRootFolderFiles(value); break; + case 6: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultFqbn(value); + break; + case 7: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultPort(value); + break; + case 8: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultProtocol(value); + break; default: reader.skipField(); break; @@ -3417,6 +3374,27 @@ proto.cc.arduino.cli.commands.v1.LoadSketchResponse.serializeBinaryToWriter = fu f ); } + f = message.getDefaultFqbn(); + if (f.length > 0) { + writer.writeString( + 6, + f + ); + } + f = message.getDefaultPort(); + if (f.length > 0) { + writer.writeString( + 7, + f + ); + } + f = message.getDefaultProtocol(); + if (f.length > 0) { + writer.writeString( + 8, + f + ); + } }; @@ -3567,6 +3545,60 @@ proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.clearRootFolderFil }; +/** + * optional string default_fqbn = 6; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.getDefaultFqbn = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.LoadSketchResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.setDefaultFqbn = function(value) { + return jspb.Message.setProto3StringField(this, 6, value); +}; + + +/** + * optional string default_port = 7; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.getDefaultPort = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.LoadSketchResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.setDefaultPort = function(value) { + return jspb.Message.setProto3StringField(this, 7, value); +}; + + +/** + * optional string default_protocol = 8; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.getDefaultProtocol = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.LoadSketchResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.LoadSketchResponse.prototype.setDefaultProtocol = function(value) { + return jspb.Message.setProto3StringField(this, 8, value); +}; + + @@ -3888,6 +3920,416 @@ proto.cc.arduino.cli.commands.v1.ArchiveSketchResponse.serializeBinaryToWriter = }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + sketchPath: jspb.Message.getFieldWithDefault(msg, 1, ""), + defaultFqbn: jspb.Message.getFieldWithDefault(msg, 2, ""), + defaultPortAddress: jspb.Message.getFieldWithDefault(msg, 3, ""), + defaultPortProtocol: jspb.Message.getFieldWithDefault(msg, 4, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest; + return proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setSketchPath(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultFqbn(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultPortAddress(value); + break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultPortProtocol(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSketchPath(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getDefaultFqbn(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getDefaultPortAddress(); + if (f.length > 0) { + writer.writeString( + 3, + f + ); + } + f = message.getDefaultPortProtocol(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } +}; + + +/** + * optional string sketch_path = 1; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.getSketchPath = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.setSketchPath = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string default_fqbn = 2; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.getDefaultFqbn = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.setDefaultFqbn = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional string default_port_address = 3; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.getDefaultPortAddress = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.setDefaultPortAddress = function(value) { + return jspb.Message.setProto3StringField(this, 3, value); +}; + + +/** + * optional string default_port_protocol = 4; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.getDefaultPortProtocol = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsRequest.prototype.setDefaultPortProtocol = function(value) { + return jspb.Message.setProto3StringField(this, 4, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + defaultFqbn: jspb.Message.getFieldWithDefault(msg, 1, ""), + defaultPortAddress: jspb.Message.getFieldWithDefault(msg, 2, ""), + defaultPortProtocol: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse; + return proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultFqbn(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultPortAddress(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setDefaultPortProtocol(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getDefaultFqbn(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getDefaultPortAddress(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getDefaultPortProtocol(); + if (f.length > 0) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional string default_fqbn = 1; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.getDefaultFqbn = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.setDefaultFqbn = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string default_port_address = 2; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.getDefaultPortAddress = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.setDefaultPortAddress = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional string default_port_protocol = 3; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.getDefaultPortProtocol = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.SetSketchDefaultsResponse.prototype.setDefaultPortProtocol = function(value) { + return jspb.Message.setProto3StringField(this, 3, value); +}; + + /** * @enum {number} */ diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.d.ts index 27866d15a..4d71dd6e7 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.d.ts @@ -409,6 +409,9 @@ export class LibrarySearchRequest extends jspb.Message { getOmitReleasesDetails(): boolean; setOmitReleasesDetails(value: boolean): LibrarySearchRequest; + getSearchArgs(): string; + setSearchArgs(value: string): LibrarySearchRequest; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): LibrarySearchRequest.AsObject; @@ -425,6 +428,7 @@ export namespace LibrarySearchRequest { instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject, query: string, omitReleasesDetails: boolean, + searchArgs: string, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.js index fa7cc1365..7ec2e0bbf 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/lib_pb.js @@ -3209,7 +3209,8 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.toObject = function(includ var f, obj = { instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f), query: jspb.Message.getFieldWithDefault(msg, 2, ""), - omitReleasesDetails: jspb.Message.getBooleanFieldWithDefault(msg, 3, false) + omitReleasesDetails: jspb.Message.getBooleanFieldWithDefault(msg, 3, false), + searchArgs: jspb.Message.getFieldWithDefault(msg, 4, "") }; if (includeInstance) { @@ -3259,6 +3260,10 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.deserializeBinaryFromReade var value = /** @type {boolean} */ (reader.readBool()); msg.setOmitReleasesDetails(value); break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setSearchArgs(value); + break; default: reader.skipField(); break; @@ -3310,6 +3315,13 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.serializeBinaryToWriter = f ); } + f = message.getSearchArgs(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } }; @@ -3386,6 +3398,24 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.setOmitReleasesD }; +/** + * optional string search_args = 4; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.getSearchArgs = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.LibrarySearchRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.setSearchArgs = function(value) { + return jspb.Message.setProto3StringField(this, 4, value); +}; + + /** * List of repeated fields within this message type. diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.d.ts index 4427473a6..e2c5f4a37 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.d.ts @@ -78,17 +78,31 @@ export namespace UploadRequest { } export class UploadResponse extends jspb.Message { + + hasOutStream(): boolean; + clearOutStream(): void; getOutStream(): Uint8Array | string; getOutStream_asU8(): Uint8Array; getOutStream_asB64(): string; setOutStream(value: Uint8Array | string): UploadResponse; + + hasErrStream(): boolean; + clearErrStream(): void; getErrStream(): Uint8Array | string; getErrStream_asU8(): Uint8Array; getErrStream_asB64(): string; setErrStream(value: Uint8Array | string): UploadResponse; + hasResult(): boolean; + clearResult(): void; + getResult(): UploadResult | undefined; + setResult(value?: UploadResult): UploadResponse; + + + getMessageCase(): UploadResponse.MessageCase; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): UploadResponse.AsObject; static toObject(includeInstance: boolean, msg: UploadResponse): UploadResponse.AsObject; @@ -103,6 +117,43 @@ export namespace UploadResponse { export type AsObject = { outStream: Uint8Array | string, errStream: Uint8Array | string, + result?: UploadResult.AsObject, + } + + export enum MessageCase { + MESSAGE_NOT_SET = 0, + + OUT_STREAM = 1, + + ERR_STREAM = 2, + + RESULT = 3, + + } + +} + +export class UploadResult extends jspb.Message { + + hasUpdatedUploadPort(): boolean; + clearUpdatedUploadPort(): void; + getUpdatedUploadPort(): cc_arduino_cli_commands_v1_port_pb.Port | undefined; + setUpdatedUploadPort(value?: cc_arduino_cli_commands_v1_port_pb.Port): UploadResult; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): UploadResult.AsObject; + static toObject(includeInstance: boolean, msg: UploadResult): UploadResult.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: UploadResult, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): UploadResult; + static deserializeBinaryFromReader(message: UploadResult, reader: jspb.BinaryReader): UploadResult; +} + +export namespace UploadResult { + export type AsObject = { + updatedUploadPort?: cc_arduino_cli_commands_v1_port_pb.Port.AsObject, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.js index c38618ad6..937a819c4 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/upload_pb.js @@ -34,6 +34,8 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.SupportedUserFieldsRequest', goog.exportSymbol('proto.cc.arduino.cli.commands.v1.SupportedUserFieldsResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadRequest', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadResponse', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadResponse.MessageCase', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadResult', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadUsingProgrammerRequest', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UploadUsingProgrammerResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.UserField', null, global); @@ -69,7 +71,7 @@ if (goog.DEBUG && !COMPILED) { * @constructor */ proto.cc.arduino.cli.commands.v1.UploadResponse = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_); }; goog.inherits(proto.cc.arduino.cli.commands.v1.UploadResponse, jspb.Message); if (goog.DEBUG && !COMPILED) { @@ -79,6 +81,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.cc.arduino.cli.commands.v1.UploadResponse.displayName = 'proto.cc.arduino.cli.commands.v1.UploadResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.UploadResult = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.UploadResult, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.UploadResult.displayName = 'proto.cc.arduino.cli.commands.v1.UploadResult'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -765,6 +788,33 @@ proto.cc.arduino.cli.commands.v1.UploadRequest.prototype.clearUserFieldsMap = fu +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_ = [[1,2,3]]; + +/** + * @enum {number} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.MessageCase = { + MESSAGE_NOT_SET: 0, + OUT_STREAM: 1, + ERR_STREAM: 2, + RESULT: 3 +}; + +/** + * @return {proto.cc.arduino.cli.commands.v1.UploadResponse.MessageCase} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.getMessageCase = function() { + return /** @type {proto.cc.arduino.cli.commands.v1.UploadResponse.MessageCase} */(jspb.Message.computeOneofCase(this, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0])); +}; + if (jspb.Message.GENERATE_TO_OBJECT) { @@ -797,7 +847,8 @@ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.toObject = function(op proto.cc.arduino.cli.commands.v1.UploadResponse.toObject = function(includeInstance, msg) { var f, obj = { outStream: msg.getOutStream_asB64(), - errStream: msg.getErrStream_asB64() + errStream: msg.getErrStream_asB64(), + result: (f = msg.getResult()) && proto.cc.arduino.cli.commands.v1.UploadResult.toObject(includeInstance, f) }; if (includeInstance) { @@ -842,6 +893,11 @@ proto.cc.arduino.cli.commands.v1.UploadResponse.deserializeBinaryFromReader = fu var value = /** @type {!Uint8Array} */ (reader.readBytes()); msg.setErrStream(value); break; + case 3: + var value = new proto.cc.arduino.cli.commands.v1.UploadResult; + reader.readMessage(value,proto.cc.arduino.cli.commands.v1.UploadResult.deserializeBinaryFromReader); + msg.setResult(value); + break; default: reader.skipField(); break; @@ -871,20 +927,28 @@ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.serializeBinary = func */ proto.cc.arduino.cli.commands.v1.UploadResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getOutStream_asU8(); - if (f.length > 0) { + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 1)); + if (f != null) { writer.writeBytes( 1, f ); } - f = message.getErrStream_asU8(); - if (f.length > 0) { + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { writer.writeBytes( 2, f ); } + f = message.getResult(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.cc.arduino.cli.commands.v1.UploadResult.serializeBinaryToWriter + ); + } }; @@ -926,7 +990,25 @@ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.getOutStream_asU8 = fu * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this */ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.setOutStream = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setOneofField(this, 1, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.clearOutStream = function() { + return jspb.Message.setOneofField(this, 1, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.hasOutStream = function() { + return jspb.Message.getField(this, 1) != null; }; @@ -968,7 +1050,213 @@ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.getErrStream_asU8 = fu * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this */ proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.setErrStream = function(value) { - return jspb.Message.setProto3BytesField(this, 2, value); + return jspb.Message.setOneofField(this, 2, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.clearErrStream = function() { + return jspb.Message.setOneofField(this, 2, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.hasErrStream = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional UploadResult result = 3; + * @return {?proto.cc.arduino.cli.commands.v1.UploadResult} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.getResult = function() { + return /** @type{?proto.cc.arduino.cli.commands.v1.UploadResult} */ ( + jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.UploadResult, 3)); +}; + + +/** + * @param {?proto.cc.arduino.cli.commands.v1.UploadResult|undefined} value + * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this +*/ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.setResult = function(value) { + return jspb.Message.setOneofWrapperField(this, 3, proto.cc.arduino.cli.commands.v1.UploadResponse.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResponse} returns this + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.clearResult = function() { + return this.setResult(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.UploadResponse.prototype.hasResult = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.UploadResult.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.UploadResult} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.UploadResult.toObject = function(includeInstance, msg) { + var f, obj = { + updatedUploadPort: (f = msg.getUpdatedUploadPort()) && cc_arduino_cli_commands_v1_port_pb.Port.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResult} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.UploadResult; + return proto.cc.arduino.cli.commands.v1.UploadResult.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.UploadResult} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResult} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new cc_arduino_cli_commands_v1_port_pb.Port; + reader.readMessage(value,cc_arduino_cli_commands_v1_port_pb.Port.deserializeBinaryFromReader); + msg.setUpdatedUploadPort(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.UploadResult.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.UploadResult} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.UploadResult.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUpdatedUploadPort(); + if (f != null) { + writer.writeMessage( + 1, + f, + cc_arduino_cli_commands_v1_port_pb.Port.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Port updated_upload_port = 1; + * @return {?proto.cc.arduino.cli.commands.v1.Port} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.getUpdatedUploadPort = function() { + return /** @type{?proto.cc.arduino.cli.commands.v1.Port} */ ( + jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_port_pb.Port, 1)); +}; + + +/** + * @param {?proto.cc.arduino.cli.commands.v1.Port|undefined} value + * @return {!proto.cc.arduino.cli.commands.v1.UploadResult} returns this +*/ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.setUpdatedUploadPort = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.UploadResult} returns this + */ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.clearUpdatedUploadPort = function() { + return this.setUpdatedUploadPort(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.UploadResult.prototype.hasUpdatedUploadPort = function() { + return jspb.Message.getField(this, 1) != null; }; diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.d.ts index f08b64d21..823aa885e 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.d.ts @@ -14,6 +14,7 @@ interface ISettingsServiceService extends grpc.ServiceDefinition { @@ -61,6 +62,15 @@ interface ISettingsServiceService_IWrite extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } +interface ISettingsServiceService_IDelete extends grpc.MethodDefinition { + path: "/cc.arduino.cli.settings.v1.SettingsService/Delete"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} export const SettingsServiceService: ISettingsServiceService; @@ -70,6 +80,7 @@ export interface ISettingsServiceServer { getValue: grpc.handleUnaryCall; setValue: grpc.handleUnaryCall; write: grpc.handleUnaryCall; + delete: grpc.handleUnaryCall; } export interface ISettingsServiceClient { @@ -88,6 +99,9 @@ export interface ISettingsServiceClient { write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; + delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; + delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; + delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; } export class SettingsServiceClient extends grpc.Client implements ISettingsServiceClient { @@ -107,4 +121,7 @@ export class SettingsServiceClient extends grpc.Client implements ISettingsServi public write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; public write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; public write(request: cc_arduino_cli_settings_v1_settings_pb.WriteRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; + public delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; + public delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; + public delete(request: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse) => void): grpc.ClientUnaryCall; } diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.js index fd3549cdc..76c399866 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb.js @@ -19,6 +19,28 @@ 'use strict'; var cc_arduino_cli_settings_v1_settings_pb = require('../../../../../cc/arduino/cli/settings/v1/settings_pb.js'); +function serialize_cc_arduino_cli_settings_v1_DeleteRequest(arg) { + if (!(arg instanceof cc_arduino_cli_settings_v1_settings_pb.DeleteRequest)) { + throw new Error('Expected argument of type cc.arduino.cli.settings.v1.DeleteRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_cc_arduino_cli_settings_v1_DeleteRequest(buffer_arg) { + return cc_arduino_cli_settings_v1_settings_pb.DeleteRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_cc_arduino_cli_settings_v1_DeleteResponse(arg) { + if (!(arg instanceof cc_arduino_cli_settings_v1_settings_pb.DeleteResponse)) { + throw new Error('Expected argument of type cc.arduino.cli.settings.v1.DeleteResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_cc_arduino_cli_settings_v1_DeleteResponse(buffer_arg) { + return cc_arduino_cli_settings_v1_settings_pb.DeleteResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_cc_arduino_cli_settings_v1_GetAllRequest(arg) { if (!(arg instanceof cc_arduino_cli_settings_v1_settings_pb.GetAllRequest)) { throw new Error('Expected argument of type cc.arduino.cli.settings.v1.GetAllRequest'); @@ -193,5 +215,17 @@ write: { responseSerialize: serialize_cc_arduino_cli_settings_v1_WriteResponse, responseDeserialize: deserialize_cc_arduino_cli_settings_v1_WriteResponse, }, + // Deletes an entry and rewrites the file settings +delete: { + path: '/cc.arduino.cli.settings.v1.SettingsService/Delete', + requestStream: false, + responseStream: false, + requestType: cc_arduino_cli_settings_v1_settings_pb.DeleteRequest, + responseType: cc_arduino_cli_settings_v1_settings_pb.DeleteResponse, + requestSerialize: serialize_cc_arduino_cli_settings_v1_DeleteRequest, + requestDeserialize: deserialize_cc_arduino_cli_settings_v1_DeleteRequest, + responseSerialize: serialize_cc_arduino_cli_settings_v1_DeleteResponse, + responseDeserialize: deserialize_cc_arduino_cli_settings_v1_DeleteResponse, + }, }; diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.d.ts index 2453b6879..00d5860c1 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.d.ts @@ -207,3 +207,41 @@ export namespace WriteResponse { export type AsObject = { } } + +export class DeleteRequest extends jspb.Message { + getKey(): string; + setKey(value: string): DeleteRequest; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): DeleteRequest.AsObject; + static toObject(includeInstance: boolean, msg: DeleteRequest): DeleteRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: DeleteRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): DeleteRequest; + static deserializeBinaryFromReader(message: DeleteRequest, reader: jspb.BinaryReader): DeleteRequest; +} + +export namespace DeleteRequest { + export type AsObject = { + key: string, + } +} + +export class DeleteResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): DeleteResponse.AsObject; + static toObject(includeInstance: boolean, msg: DeleteResponse): DeleteResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: DeleteResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): DeleteResponse; + static deserializeBinaryFromReader(message: DeleteResponse, reader: jspb.BinaryReader): DeleteResponse; +} + +export namespace DeleteResponse { + export type AsObject = { + } +} diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.js index 585bd6473..a00c4ffe8 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/settings/v1/settings_pb.js @@ -21,6 +21,8 @@ var global = (function() { return Function('return this')(); }.call(null)); +goog.exportSymbol('proto.cc.arduino.cli.settings.v1.DeleteRequest', null, global); +goog.exportSymbol('proto.cc.arduino.cli.settings.v1.DeleteResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.settings.v1.GetAllRequest', null, global); goog.exportSymbol('proto.cc.arduino.cli.settings.v1.GetAllResponse', null, global); goog.exportSymbol('proto.cc.arduino.cli.settings.v1.GetValueRequest', null, global); @@ -241,6 +243,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.cc.arduino.cli.settings.v1.WriteResponse.displayName = 'proto.cc.arduino.cli.settings.v1.WriteResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.settings.v1.DeleteRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.settings.v1.DeleteRequest.displayName = 'proto.cc.arduino.cli.settings.v1.DeleteRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.settings.v1.DeleteResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.settings.v1.DeleteResponse.displayName = 'proto.cc.arduino.cli.settings.v1.DeleteResponse'; +} @@ -1485,4 +1529,235 @@ proto.cc.arduino.cli.settings.v1.WriteResponse.serializeBinaryToWriter = functio }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.settings.v1.DeleteRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.settings.v1.DeleteRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.toObject = function(includeInstance, msg) { + var f, obj = { + key: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.settings.v1.DeleteRequest} + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.settings.v1.DeleteRequest; + return proto.cc.arduino.cli.settings.v1.DeleteRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.settings.v1.DeleteRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.settings.v1.DeleteRequest} + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setKey(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.settings.v1.DeleteRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.settings.v1.DeleteRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getKey(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string key = 1; + * @return {string} + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.prototype.getKey = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.settings.v1.DeleteRequest} returns this + */ +proto.cc.arduino.cli.settings.v1.DeleteRequest.prototype.setKey = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.settings.v1.DeleteResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.settings.v1.DeleteResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.settings.v1.DeleteResponse} + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.settings.v1.DeleteResponse; + return proto.cc.arduino.cli.settings.v1.DeleteResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.settings.v1.DeleteResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.settings.v1.DeleteResponse} + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.settings.v1.DeleteResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.settings.v1.DeleteResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.settings.v1.DeleteResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + goog.object.extend(exports, proto.cc.arduino.cli.settings.v1); diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 253dcd383..42f7ee4b5 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -3,13 +3,14 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { relative } from 'node:path'; import * as jspb from 'google-protobuf'; import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; -import { ClientReadableStream } from '@grpc/grpc-js'; +import type { ClientReadableStream } from '@grpc/grpc-js'; import { CompilerWarnings, CoreService, CoreError, CompileSummary, isCompileSummary, + isUploadResponse, } from '../common/protocol/core-service'; import { CompileRequest, @@ -25,7 +26,13 @@ import { UploadUsingProgrammerResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; import { ResponseService } from '../common/protocol/response-service'; -import { OutputMessage, Port } from '../common/protocol'; +import { + resolveDetectedPort, + OutputMessage, + PortIdentifier, + Port, + UploadResponse as ApiUploadResponse, +} from '../common/protocol'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { ApplicationError, CommandService, Disposable, nls } from '@theia/core'; @@ -36,8 +43,8 @@ import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { firstToUpperCase, notEmpty } from '../common/utils'; import { ServiceError } from './service-error'; import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible'; -import { BoardDiscovery } from './board-discovery'; -import { Mutable } from '@theia/core/lib/common/types'; +import type { Mutable } from '@theia/core/lib/common/types'; +import { BoardDiscovery, createApiPort } from './board-discovery'; namespace Uploadable { export type Request = UploadRequest | UploadUsingProgrammerRequest; @@ -50,13 +57,10 @@ type CompileSummaryFragment = Partial>; export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(ResponseService) private readonly responseService: ResponseService; - @inject(MonitorManager) private readonly monitorManager: MonitorManager; - @inject(CommandService) private readonly commandService: CommandService; - @inject(BoardDiscovery) private readonly boardDiscovery: BoardDiscovery; @@ -172,7 +176,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - upload(options: CoreService.Options.Upload): Promise { + upload(options: CoreService.Options.Upload): Promise { const { usingProgrammer } = options; return this.doUpload( options, @@ -201,14 +205,48 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { ) => (request: REQ) => ClientReadableStream, errorCtor: ApplicationError.Constructor, task: string - ): Promise { + ): Promise { + const uploadResponseFragment: Mutable> = { + portBeforeUpload: options.port, + portAfterUpload: options.port, // assume no port changes during the upload + }; const coreClient = await this.coreClient; const { client, instance } = coreClient; const progressHandler = this.createProgressHandler(options); - const handler = this.createOnDataHandler(progressHandler); + // Track responses for port changes: + // - No port changes are expected when uploading using a programmer. + // - When the `updatedUploadPort` is missing, the port did not change. + // - The `updatedUploadPort` port can be the "same" as the port before the upload. The CLI populates this field if the port has been cycled during the upload. + // - IDE2 always provides the `portAfterUpload` + const updateUploadResponseFragmentHandler = (response: RESP) => { + if (response instanceof UploadResponse) { + // TODO: this instanceof should not be here but in `upload`. the upload and upload using programmer gRPC APIs are not symmetric + if (response.hasResult()) { + const port = response.getResult()?.getUpdatedUploadPort(); + if (port) { + uploadResponseFragment.portAfterUpload = createApiPort(port); + console.info( + `Detected a port change during the upload from the CLI [${ + options.port ? Port.keyOf(options.port) : '' + }, ${options.fqbn}, ${ + options.sketch.name + }]. Before port: ${JSON.stringify( + uploadResponseFragment.portBeforeUpload + )}, after port: ${JSON.stringify( + uploadResponseFragment.portAfterUpload + )}` + ); + } + } + } + }; + const handler = this.createOnDataHandler( + progressHandler, + updateUploadResponseFragmentHandler + ); const grpcCall = responseFactory(client); return this.notifyUploadWillStart(options).then(() => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { grpcCall(this.initUploadRequest(request, options, instance)) .on('data', handler.onData) .on('error', (error) => { @@ -234,10 +272,28 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { ); } }) - .on('end', resolve); + .on('end', () => { + if (isUploadResponse(uploadResponseFragment)) { + resolve(uploadResponseFragment); + } else { + reject( + new Error( + `Could not detect the port after the upload. Upload options were: ${JSON.stringify( + options + )}, upload response was: ${JSON.stringify( + uploadResponseFragment + )}` + ) + ); + } + }); }).finally(async () => { handler.dispose(); - await this.notifyUploadDidFinish(options); + await this.notifyUploadDidFinish( + Object.assign(options, { + afterPort: uploadResponseFragment.portAfterUpload, + }) + ); }) ); } @@ -302,7 +358,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { .on('end', resolve); }).finally(async () => { handler.dispose(); - await this.notifyUploadDidFinish(options); + await this.notifyUploadDidFinish( + Object.assign(options, { afterPort: options.port }) + ); }) ); } @@ -379,21 +437,25 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { port, }: { fqbn?: string | undefined; - port?: Port | undefined; + port?: PortIdentifier; }): Promise { - this.boardDiscovery.setUploadInProgress(true); - return this.monitorManager.notifyUploadStarted(fqbn, port); + if (fqbn && port) { + return this.monitorManager.notifyUploadStarted(fqbn, port); + } } private async notifyUploadDidFinish({ fqbn, port, + afterPort, }: { fqbn?: string | undefined; - port?: Port | undefined; + port?: PortIdentifier; + afterPort?: PortIdentifier; }): Promise { - this.boardDiscovery.setUploadInProgress(false); - return this.monitorManager.notifyUploadFinished(fqbn, port); + if (fqbn && port && afterPort) { + return this.monitorManager.notifyUploadFinished(fqbn, port, afterPort); + } } private mergeSourceOverrides( @@ -410,21 +472,28 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } } - private createPort(port: Port | undefined): RpcPort | undefined { + private createPort( + port: PortIdentifier | undefined, + resolve: (port: PortIdentifier) => Port | undefined = (port) => + resolveDetectedPort(port, this.boardDiscovery.detectedPorts) + ): RpcPort | undefined { if (!port) { return undefined; } + const resolvedPort = resolve(port); const rpcPort = new RpcPort(); - rpcPort.setAddress(port.address); - rpcPort.setLabel(port.addressLabel); rpcPort.setProtocol(port.protocol); - rpcPort.setProtocolLabel(port.protocolLabel); - if (port.hardwareId !== undefined) { - rpcPort.setHardwareId(port.hardwareId); - } - if (port.properties) { - for (const [key, value] of Object.entries(port.properties)) { - rpcPort.getPropertiesMap().set(key, value); + rpcPort.setAddress(port.address); + if (resolvedPort) { + rpcPort.setLabel(resolvedPort.addressLabel); + rpcPort.setProtocolLabel(resolvedPort.protocolLabel); + if (resolvedPort.hardwareId !== undefined) { + rpcPort.setHardwareId(resolvedPort.hardwareId); + } + if (resolvedPort.properties) { + for (const [key, value] of Object.entries(resolvedPort.properties)) { + rpcPort.getPropertiesMap().set(key, value); + } } } return rpcPort; diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 4eea5b46a..3a6161e39 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -7,6 +7,7 @@ import { MonitorSettings, PluggableMonitorSettings, Port, + PortIdentifier, } from '../common/protocol'; import { CoreClientAware } from './core-client-provider'; import { MonitorService } from './monitor-service'; @@ -180,13 +181,7 @@ export class MonitorManager extends CoreClientAware { * @param fqbn the FQBN of the board connected to port * @param port port to monitor */ - async notifyUploadStarted(fqbn?: string, port?: Port): Promise { - if (!fqbn || !port) { - // We have no way of knowing which monitor - // to retrieve if we don't have this information. - return; - } - + async notifyUploadStarted(fqbn: string, port: PortIdentifier): Promise { const monitorID = this.monitorID(fqbn, port); this.addToMonitorIDsByUploadState('uploadInProgress', monitorID); @@ -204,41 +199,40 @@ export class MonitorManager extends CoreClientAware { * Notifies the monitor service of that board/port combination * that an upload process started on that exact board/port combination. * @param fqbn the FQBN of the board connected to port - * @param port port to monitor + * @param beforePort port to monitor * @returns a Status object to know if the process has been * started or if there have been errors. */ async notifyUploadFinished( - fqbn?: string | undefined, - port?: Port + fqbn: string | undefined, + beforePort: PortIdentifier, + afterPort: PortIdentifier ): Promise { let portDidChangeOnUpload = false; + const beforeMonitorID = this.monitorID(fqbn, beforePort); + this.removeFromMonitorIDsByUploadState('uploadInProgress', beforeMonitorID); - // We have no way of knowing which monitor - // to retrieve if we don't have this information. - if (fqbn && port) { - const monitorID = this.monitorID(fqbn, port); - this.removeFromMonitorIDsByUploadState('uploadInProgress', monitorID); - - const monitor = this.monitorServices.get(monitorID); - if (monitor) { - await monitor.start(); - } + const monitor = this.monitorServices.get(beforeMonitorID); + if (monitor) { + await monitor.start(); + } - // this monitorID will only be present in "disposedForUpload" - // if the upload changed the board port - portDidChangeOnUpload = this.monitorIDIsInUploadState( + // this monitorID will only be present in "disposedForUpload" + // if the upload changed the board port + portDidChangeOnUpload = this.monitorIDIsInUploadState( + 'disposedForUpload', + beforeMonitorID + ); + if (portDidChangeOnUpload) { + this.removeFromMonitorIDsByUploadState( 'disposedForUpload', - monitorID + beforeMonitorID ); - if (portDidChangeOnUpload) { - this.removeFromMonitorIDsByUploadState('disposedForUpload', monitorID); - } - - // in case a service was paused but not disposed - this.removeFromMonitorIDsByUploadState('pausedForUpload', monitorID); } + // in case a service was paused but not disposed + this.removeFromMonitorIDsByUploadState('pausedForUpload', beforeMonitorID); + await this.startQueuedServices(portDidChangeOnUpload); } @@ -256,7 +250,7 @@ export class MonitorManager extends CoreClientAware { serviceStartParams: [, port], connectToClient, } of queued) { - const boardsState = await this.boardsService.getState(); + const boardsState = await this.boardsService.getDetectedPorts(); const boardIsStillOnPort = Object.keys(boardsState) .map((connection: string) => { const portAddress = connection.split('|')[0]; @@ -355,7 +349,7 @@ export class MonitorManager extends CoreClientAware { * @param port * @returns a unique monitor ID */ - private monitorID(fqbn: string | undefined, port: Port): MonitorID { + private monitorID(fqbn: string | undefined, port: PortIdentifier): MonitorID { const splitFqbn = fqbn?.split(':') || []; const shortenedFqbn = splitFqbn.slice(0, 3).join(':') || ''; return `${shortenedFqbn}-${port.address}-${port.protocol}`; diff --git a/arduino-ide-extension/src/node/notification-service-server.ts b/arduino-ide-extension/src/node/notification-service-server.ts index ce6a96304..cd3cac91e 100644 --- a/arduino-ide-extension/src/node/notification-service-server.ts +++ b/arduino-ide-extension/src/node/notification-service-server.ts @@ -2,7 +2,6 @@ import { injectable } from '@theia/core/shared/inversify'; import type { NotificationServiceServer, NotificationServiceClient, - AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, ConfigState, @@ -11,6 +10,7 @@ import type { IndexUpdateWillStartParams, IndexUpdateDidCompleteParams, IndexUpdateDidFailParams, + DetectedPorts, } from '../common/protocol'; @injectable() @@ -69,9 +69,9 @@ export class NotificationServiceServerImpl this.clients.forEach((client) => client.notifyLibraryDidUninstall(event)); } - notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void { + notifyDetectedPortsDidChange(event: { detectedPorts: DetectedPorts }): void { this.clients.forEach((client) => - client.notifyAttachedBoardsDidChange(event) + client.notifyDetectedPortsDidChange(event) ); } diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index d3ad0e58e..95493bb19 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import glob from 'glob'; import crypto from 'node:crypto'; import PQueue from 'p-queue'; -import { Mutable } from '@theia/core/lib/common/types'; +import type { Mutable } from '@theia/core/lib/common/types'; import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { FileUri } from '@theia/core/lib/node/file-uri'; @@ -128,11 +128,11 @@ export class SketchesServiceImpl uri: string, detectInvalidSketchNameError = true ): Promise { - const { client, instance } = await this.coreClient; + const { client } = await this.coreClient; const req = new LoadSketchRequest(); const requestSketchPath = FileUri.fsPath(uri); req.setSketchPath(requestSketchPath); - req.setInstance(instance); + // TODO: since the instance is not required on the request, can IDE2 do this faster or have a dedicated client for the sketch loading? const stat = new Deferred(); lstat(requestSketchPath, (err, result) => err ? stat.resolve(err) : stat.resolve(result) diff --git a/arduino-ide-extension/src/test/browser/boards-auto-installer.test.ts b/arduino-ide-extension/src/test/browser/boards-auto-installer.test.ts deleted file mode 100644 index 00570fff6..000000000 --- a/arduino-ide-extension/src/test/browser/boards-auto-installer.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -// import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -// const disableJSDOM = enableJSDOM(); - -// import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -// import { ApplicationProps } from '@theia/application-package/lib/application-props'; -// FrontendApplicationConfigProvider.set({ -// ...ApplicationProps.DEFAULT.frontend.config, -// }); - -// import { MessageService } from '@theia/core'; -// import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider'; -// import { BoardsListWidgetFrontendContribution } from '../../browser/boards/boards-widget-frontend-contribution'; -// import { -// Board, -// BoardsPackage, -// BoardsService, -// Port, -// ResponseServiceArduino, -// } from '../../common/protocol'; -// import { IMock, It, Mock, Times } from 'typemoq'; -// import { Container, ContainerModule } from '@theia/core/shared/inversify'; -// import { BoardsAutoInstaller } from '../../browser/boards/boards-auto-installer'; -// import { BoardsConfig } from '../../browser/boards/boards-config'; -// import { tick } from '../utils'; -// import { ListWidget } from '../../browser/widgets/component-list/list-widget'; - -// disableJSDOM(); - -// const aBoard: Board = { -// fqbn: 'some:board:fqbn', -// name: 'Some Arduino Board', -// port: { address: '/lol/port1234', protocol: 'serial' }, -// }; -// const aPort: Port = { -// address: aBoard.port!.address, -// protocol: aBoard.port!.protocol, -// }; -// const aBoardConfig: BoardsConfig.Config = { -// selectedBoard: aBoard, -// selectedPort: aPort, -// }; -// const aPackage: BoardsPackage = { -// author: 'someAuthor', -// availableVersions: ['some.ver.sion', 'some.other.version'], -// boards: [aBoard], -// deprecated: false, -// description: 'Some Arduino Board, Some Other Arduino Board', -// id: 'some:arduinoCoreId', -// installable: true, -// moreInfoLink: 'http://www.some-url.lol/', -// name: 'Some Arduino Package', -// summary: 'Boards included in this package:', -// }; - -// const anInstalledPackage: BoardsPackage = { -// ...aPackage, -// installedVersion: 'some.ver.sion', -// }; - -// describe('BoardsAutoInstaller', () => { -// let subject: BoardsAutoInstaller; -// let messageService: IMock; -// let boardsService: IMock; -// let boardsServiceClient: IMock; -// let responseService: IMock; -// let boardsManagerFrontendContribution: IMock; -// let boardsManagerWidget: IMock>; - -// let testContainer: Container; - -// beforeEach(() => { -// testContainer = new Container(); -// messageService = Mock.ofType(); -// boardsService = Mock.ofType(); -// boardsServiceClient = Mock.ofType(); -// responseService = Mock.ofType(); -// boardsManagerFrontendContribution = -// Mock.ofType(); -// boardsManagerWidget = Mock.ofType>(); - -// boardsManagerWidget.setup((b) => -// b.refresh(aPackage.name.toLocaleLowerCase()) -// ); - -// boardsManagerFrontendContribution -// .setup((b) => b.openView({ reveal: true })) -// .returns(async () => boardsManagerWidget.object); - -// messageService -// .setup((m) => m.showProgress(It.isAny(), It.isAny())) -// .returns(async () => ({ -// cancel: () => null, -// id: '', -// report: () => null, -// result: Promise.resolve(''), -// })); - -// responseService -// .setup((r) => r.onProgressDidChange(It.isAny())) -// .returns(() => ({ dispose: () => null })); - -// const module = new ContainerModule((bind) => { -// bind(BoardsAutoInstaller).toSelf(); -// bind(MessageService).toConstantValue(messageService.object); -// bind(BoardsService).toConstantValue(boardsService.object); -// bind(BoardsServiceProvider).toConstantValue(boardsServiceClient.object); -// bind(ResponseServiceArduino).toConstantValue(responseService.object); -// bind(BoardsListWidgetFrontendContribution).toConstantValue( -// boardsManagerFrontendContribution.object -// ); -// }); - -// testContainer.load(module); -// subject = testContainer.get(BoardsAutoInstaller); -// }); - -// context('when it starts', () => { -// it('should register to the BoardsServiceClient in order to check the packages every a new board is plugged in', () => { -// subject.onStart(); -// boardsServiceClient.verify( -// (b) => b.onBoardsConfigChanged(It.isAny()), -// Times.once() -// ); -// }); - -// context('and it checks the installable packages', () => { -// context(`and a port and a board a selected`, () => { -// beforeEach(() => { -// boardsServiceClient -// .setup((b) => b.boardsConfig) -// .returns(() => aBoardConfig); -// }); -// context('if no package for the board is already installed', () => { -// context('if a candidate package for the board is found', () => { -// beforeEach(() => { -// boardsService -// .setup((b) => b.search(It.isValue({}))) -// .returns(async () => [aPackage]); -// }); -// it('should show a notification suggesting to install that package', async () => { -// messageService -// .setup((m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()) -// ) -// .returns(() => Promise.resolve('Install Manually')); -// subject.onStart(); -// await tick(); -// messageService.verify( -// (m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()), -// Times.once() -// ); -// }); -// context(`if the answer to the message is 'Yes'`, () => { -// beforeEach(() => { -// messageService -// .setup((m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()) -// ) -// .returns(() => Promise.resolve('Yes')); -// }); -// it('should install the package', async () => { -// subject.onStart(); - -// await tick(); - -// messageService.verify( -// (m) => m.showProgress(It.isAny(), It.isAny()), -// Times.once() -// ); -// }); -// }); -// context( -// `if the answer to the message is 'Install Manually'`, -// () => { -// beforeEach(() => { -// messageService -// .setup((m) => -// m.info( -// It.isAnyString(), -// It.isAnyString(), -// It.isAnyString() -// ) -// ) -// .returns(() => Promise.resolve('Install Manually')); -// }); -// it('should open the boards manager widget', () => { -// subject.onStart(); -// }); -// } -// ); -// }); -// context('if a candidate package for the board is not found', () => { -// beforeEach(() => { -// boardsService -// .setup((b) => b.search(It.isValue({}))) -// .returns(async () => []); -// }); -// it('should do nothing', async () => { -// subject.onStart(); -// await tick(); -// messageService.verify( -// (m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()), -// Times.never() -// ); -// }); -// }); -// }); -// context( -// 'if one of the packages for the board is already installed', -// () => { -// beforeEach(() => { -// boardsService -// .setup((b) => b.search(It.isValue({}))) -// .returns(async () => [aPackage, anInstalledPackage]); -// messageService -// .setup((m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()) -// ) -// .returns(() => Promise.resolve('Yes')); -// }); -// it('should do nothing', async () => { -// subject.onStart(); -// await tick(); -// messageService.verify( -// (m) => -// m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()), -// Times.never() -// ); -// }); -// } -// ); -// }); -// context('and there is no selected board or port', () => { -// it('should do nothing', async () => { -// subject.onStart(); -// await tick(); -// messageService.verify( -// (m) => m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()), -// Times.never() -// ); -// }); -// }); -// }); -// }); -// }); diff --git a/arduino-ide-extension/src/test/browser/fixtures/boards.ts b/arduino-ide-extension/src/test/browser/fixtures/boards.ts index 16256f3ab..f42f9a757 100644 --- a/arduino-ide-extension/src/test/browser/fixtures/boards.ts +++ b/arduino-ide-extension/src/test/browser/fixtures/boards.ts @@ -1,44 +1,36 @@ -import { BoardsConfig } from '../../../browser/boards/boards-config'; -import { Board, BoardsPackage, Port } from '../../../common/protocol'; +import type { + Board, + BoardsConfig, + BoardsPackage, + Port, +} from '../../../common/protocol'; export const aBoard: Board = { fqbn: 'some:board:fqbn', name: 'Some Arduino Board', - port: { - address: '/lol/port1234', - addressLabel: '/lol/port1234', - protocol: 'serial', - protocolLabel: 'Serial Port (USB)', - }, -}; -export const aPort: Port = { - address: aBoard.port!.address, - addressLabel: aBoard.port!.addressLabel, - protocol: aBoard.port!.protocol, - protocolLabel: aBoard.port!.protocolLabel, -}; -export const aBoardConfig: BoardsConfig.Config = { - selectedBoard: aBoard, +}; +const aPort: Port = { + address: '/lol/port1234', + addressLabel: '/lol/port1234', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', +}; +export const aBoardsConfig: BoardsConfig = { + selectedBoard: { name: aBoard.name, fqbn: aBoard.fqbn }, selectedPort: aPort, }; export const anotherBoard: Board = { fqbn: 'another:board:fqbn', name: 'Another Arduino Board', - port: { - address: '/kek/port5678', - addressLabel: '/kek/port5678', - protocol: 'serial', - protocolLabel: 'Serial Port (USB)', - }, }; export const anotherPort: Port = { - address: anotherBoard.port!.address, - addressLabel: anotherBoard.port!.addressLabel, - protocol: anotherBoard.port!.protocol, - protocolLabel: anotherBoard.port!.protocolLabel, + address: '/kek/port5678', + addressLabel: '/kek/port5678', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', }; -export const anotherBoardConfig: BoardsConfig.Config = { - selectedBoard: anotherBoard, +export const anotherBoardsConfig: BoardsConfig = { + selectedBoard: { name: anotherBoard.name, fqbn: anotherBoard.fqbn }, selectedPort: anotherPort, }; diff --git a/arduino-ide-extension/src/test/common/board-list.test.ts b/arduino-ide-extension/src/test/common/board-list.test.ts new file mode 100644 index 000000000..3f7aaa248 --- /dev/null +++ b/arduino-ide-extension/src/test/common/board-list.test.ts @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { createBoardList } from '../../common/protocol/board-list'; +import { + BoardIdentifier, + DetectedPort, + DetectedPorts, + Port, +} from '../../common/protocol/boards-service'; + +const mkr1000: BoardIdentifier = { + name: 'Arduino MKR1000', + fqbn: 'arduino:samd:mkr1000', +}; +const uno: BoardIdentifier = { + name: 'Arduino Uno', + fqbn: 'arduino:avr:uno', +}; +const bluetoothSerialPort: Port = { + address: '/dev/cu.Bluetooth-Incoming-Port', + addressLabel: '/dev/cu.Bluetooth-Incoming-Port', + protocol: 'serial', + protocolLabel: 'Serial Port', + properties: {}, + hardwareId: '', +}; +const builtinSerialPort: Port = { + address: '/dev/cu.BLTH', + addressLabel: '/dev/cu.BLTH', + protocol: 'serial', + protocolLabel: 'Serial Port', + properties: {}, + hardwareId: '', +}; +const undiscoveredSerialPort: Port = { + address: '/dev/cu.usbserial-0001', + addressLabel: '/dev/cu.usbserial-0001', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', + properties: { + pid: '0xEA60', + serialNumber: '0001', + vid: '0x10C4', + }, + hardwareId: '0001', +}; +const mkr1000NetworkPort: Port = { + address: '192.168.0.104', + addressLabel: 'Arduino at 192.168.0.104', + protocol: 'network', + protocolLabel: 'Network Port', + properties: { + '.': 'mkr1000', + auth_upload: 'yes', + board: 'mkr1000', + hostname: 'Arduino.local.', + port: '65280', + ssh_upload: 'no', + tcp_check: 'no', + }, + hardwareId: '', +}; +const undiscoveredUsbToUARTSerialPort: Port = { + address: '/dev/cu.SLAB_USBtoUART', + addressLabel: '/dev/cu.SLAB_USBtoUART', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', + properties: { + pid: '0xEA60', + serialNumber: '0001', + vid: '0x10C4', + }, + hardwareId: '0001', +}; +const mkr1000SerialPort: Port = { + address: '/dev/cu.usbmodem14301', + addressLabel: '/dev/cu.usbmodem14301', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', + properties: { + pid: '0x804E', + serialNumber: '94A3397C5150435437202020FF150838', + vid: '0x2341', + }, + hardwareId: '94A3397C5150435437202020FF150838', +}; +const unoSerialPort: Port = { + address: '/dev/cu.usbmodem14201', + addressLabel: '/dev/cu.usbmodem14201', + protocol: 'serial', + protocolLabel: 'Serial Port (USB)', + properties: { + pid: '0x0043', + serialNumber: '75830303934351618212', + vid: '0x2341', + }, + hardwareId: '75830303934351618212', +}; + +function detectedPort( + port: Port, + ...boards: BoardIdentifier[] +): { [portKey: string]: DetectedPort } { + return { [Port.keyOf(port)]: boards.length ? { port, boards } : { port } }; +} + +const detectedPorts: DetectedPorts = { + ...detectedPort(builtinSerialPort), + ...detectedPort(bluetoothSerialPort), + ...detectedPort(unoSerialPort, uno), + ...detectedPort(mkr1000SerialPort, mkr1000), + ...detectedPort(mkr1000NetworkPort, mkr1000), + ...detectedPort(undiscoveredSerialPort), + ...detectedPort(undiscoveredUsbToUARTSerialPort), +}; + +describe('board-list', () => { + describe('createBoardList', () => { + it('should sort items deterministically', () => { + const actual = createBoardList(detectedPorts); + expect(actual[0].board).deep.equal(mkr1000); + expect(actual[1].board).deep.equal(uno); + expect(actual[2].port).deep.equal(builtinSerialPort); + expect(actual[3].port).deep.equal(bluetoothSerialPort); + expect(actual[4].port).deep.equal(undiscoveredUsbToUARTSerialPort); + expect(actual[5].port).deep.equal(undiscoveredSerialPort); + expect(actual[6].port.protocol).equal('network'); + expect(actual[6].board).deep.equal(mkr1000); + }); + }); +}); diff --git a/arduino-ide-extension/src/test/common/boards-service.test.ts b/arduino-ide-extension/src/test/common/boards-service.test.ts index d2cae5a53..693daa8b5 100644 --- a/arduino-ide-extension/src/test/common/boards-service.test.ts +++ b/arduino-ide-extension/src/test/common/boards-service.test.ts @@ -1,8 +1,6 @@ -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { Mutable } from '@theia/core/lib/common/types'; +import type { Mutable } from '@theia/core/lib/common/types'; import { expect } from 'chai'; import { - AttachedBoardsChangeEvent, BoardInfo, getBoardInfo, noNativeSerialPort, @@ -11,88 +9,10 @@ import { selectPortForInfo, unknownBoard, } from '../../common/protocol'; +import { createBoardList } from '../../common/protocol/board-list'; import { firstToUpperCase } from '../../common/utils'; describe('boards-service', () => { - describe('AttachedBoardsChangeEvent', () => { - it('should detect one attached port', () => { - const event = { - oldState: { - boards: [ - { - name: 'Arduino MKR1000', - fqbn: 'arduino:samd:mkr1000', - port: '/dev/cu.usbmodem14601', - }, - { - name: 'Arduino Uno', - fqbn: 'arduino:avr:uno', - port: '/dev/cu.usbmodem14501', - }, - ], - ports: [ - { - protocol: 'serial', - address: '/dev/cu.usbmodem14501', - }, - { - protocol: 'serial', - address: '/dev/cu.usbmodem14601', - }, - { - protocol: 'serial', - address: '/dev/cu.Bluetooth-Incoming-Port', - }, - { protocol: 'serial', address: '/dev/cu.MALS' }, - { protocol: 'serial', address: '/dev/cu.SOC' }, - ], - }, - newState: { - boards: [ - { - name: 'Arduino MKR1000', - fqbn: 'arduino:samd:mkr1000', - port: '/dev/cu.usbmodem1460', - }, - { - name: 'Arduino Uno', - fqbn: 'arduino:avr:uno', - port: '/dev/cu.usbmodem14501', - }, - ], - ports: [ - { - protocol: 'serial', - address: '/dev/cu.SLAB_USBtoUART', - }, - { - protocol: 'serial', - address: '/dev/cu.usbmodem14501', - }, - { - protocol: 'serial', - address: '/dev/cu.usbmodem14601', - }, - { - protocol: 'serial', - address: '/dev/cu.Bluetooth-Incoming-Port', - }, - { protocol: 'serial', address: '/dev/cu.MALS' }, - { protocol: 'serial', address: '/dev/cu.SOC' }, - ], - }, - }; - const diff = AttachedBoardsChangeEvent.diff(event); - expect(diff.attached.boards).to.be.empty; // tslint:disable-line:no-unused-expression - expect(diff.detached.boards).to.be.empty; // tslint:disable-line:no-unused-expression - expect(diff.detached.ports).to.be.empty; // tslint:disable-line:no-unused-expression - expect(diff.attached.ports.length).to.be.equal(1); - expect(diff.attached.ports[0].address).to.be.equal( - '/dev/cu.SLAB_USBtoUART' - ); - }); - }); - describe('getBoardInfo', () => { const vid = '0x0'; const pid = '0x1'; @@ -112,7 +32,7 @@ describe('boards-service', () => { }); it('should handle when no port is selected', async () => { - const info = await getBoardInfo(undefined, never()); + const info = await getBoardInfo(createBoardList({})); expect(info).to.be.equal(selectPortForInfo); }); @@ -125,7 +45,11 @@ describe('boards-service', () => { protocolLabel: firstToUpperCase(protocol), protocol, }; - const info = await getBoardInfo(selectedPort, never()); + const boardList = createBoardList( + { [Port.keyOf(selectedPort)]: { port: selectedPort } }, + { selectedPort, selectedBoard: undefined } + ); + const info = await getBoardInfo(boardList); expect(info).to.be.equal(nonSerialPort); }) ); @@ -140,18 +64,26 @@ describe('boards-service', () => { ]; for (const properties of insufficientProperties) { const port = selectedPort(properties); - const info = await getBoardInfo(port, { - [Port.keyOf(port)]: [port, []], - }); + const boardList = createBoardList( + { + [Port.keyOf(port)]: { port }, + }, + { selectedPort: port, selectedBoard: undefined } + ); + const info = await getBoardInfo(boardList); expect(info).to.be.equal(noNativeSerialPort); } }); it("should detect a port as non-native serial, if protocol is 'serial' and VID/PID are available", async () => { const port = selectedPort({ vid, pid }); - const info = await getBoardInfo(port, { - [Port.keyOf(port)]: [port, []], - }); + const boardList = createBoardList( + { + [Port.keyOf(port)]: { port }, + }, + { selectedPort: port, selectedBoard: undefined } + ); + const info = await getBoardInfo(boardList); expect(typeof info).to.be.equal('object'); const boardInfo = info; expect(boardInfo.VID).to.be.equal(vid); @@ -162,9 +94,13 @@ describe('boards-service', () => { it("should show the 'SN' even if no matching board was detected for the port", async () => { const port = selectedPort({ vid, pid, serialNumber }); - const info = await getBoardInfo(port, { - [Port.keyOf(port)]: [port, []], - }); + const boardList = createBoardList( + { + [Port.keyOf(port)]: { port }, + }, + { selectedPort: port, selectedBoard: undefined } + ); + const info = await getBoardInfo(boardList); expect(typeof info).to.be.equal('object'); const boardInfo = info; expect(boardInfo.VID).to.be.equal(vid); @@ -175,9 +111,13 @@ describe('boards-service', () => { it("should use the name of the matching board as 'BN' if available", async () => { const port = selectedPort({ vid, pid }); - const info = await getBoardInfo(port, { - [Port.keyOf(port)]: [port, [selectedBoard]], - }); + const boardList = createBoardList( + { + [Port.keyOf(port)]: { port, boards: [selectedBoard] }, + }, + { selectedPort: port, selectedBoard: undefined } + ); + const info = await getBoardInfo(boardList); expect(typeof info).to.be.equal('object'); const boardInfo = info; expect(boardInfo.VID).to.be.equal(vid); @@ -187,7 +127,3 @@ describe('boards-service', () => { }); }); }); - -function never(): Promise { - return new Deferred().promise; -} diff --git a/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts b/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts index 8ce8670be..533cca46e 100644 --- a/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts +++ b/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts @@ -3,6 +3,7 @@ import type { MaybePromise } from '@theia/core/lib/common/types'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { Container } from '@theia/core/shared/inversify'; import { expect } from 'chai'; +import { dump, load } from 'js-yaml'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import { sync as deleteSync } from 'rimraf'; @@ -66,16 +67,11 @@ describe('core-client-provider', () => { const configDirPath = await prepareTestConfigDir(); deleteSync(join(configDirPath, 'data')); - const now = new Date().toISOString(); const container = await startCli(configDirPath, toDispose); await assertFunctionalCli(container, ({ coreClientProvider }) => { const { indexUpdateSummaryBeforeInit } = coreClientProvider; - const libUpdateTimestamp = indexUpdateSummaryBeforeInit['library']; - expect(libUpdateTimestamp).to.be.not.empty; - expect(libUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0); - const platformUpdateTimestamp = indexUpdateSummaryBeforeInit['platform']; - expect(platformUpdateTimestamp).to.be.not.empty; - expect(platformUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0); + expect(indexUpdateSummaryBeforeInit).to.be.not.undefined; + expect(indexUpdateSummaryBeforeInit).to.be.empty; }); }); @@ -90,14 +86,11 @@ describe('core-client-provider', () => { ); deleteSync(primaryPackageIndexPath); - const now = new Date().toISOString(); const container = await startCli(configDirPath, toDispose); await assertFunctionalCli(container, ({ coreClientProvider }) => { const { indexUpdateSummaryBeforeInit } = coreClientProvider; - expect(indexUpdateSummaryBeforeInit['library']).to.be.undefined; - const platformUpdateTimestamp = indexUpdateSummaryBeforeInit['platform']; - expect(platformUpdateTimestamp).to.be.not.empty; - expect(platformUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0); + expect(indexUpdateSummaryBeforeInit).to.be.not.undefined; + expect(indexUpdateSummaryBeforeInit).to.be.empty; }); const rawJson = await fs.readFile(primaryPackageIndexPath, { encoding: 'utf8', @@ -149,14 +142,11 @@ describe('core-client-provider', () => { ); deleteSync(libraryPackageIndexPath); - const now = new Date().toISOString(); const container = await startCli(configDirPath, toDispose); await assertFunctionalCli(container, ({ coreClientProvider }) => { const { indexUpdateSummaryBeforeInit } = coreClientProvider; - const libUpdateTimestamp = indexUpdateSummaryBeforeInit['library']; - expect(libUpdateTimestamp).to.be.not.empty; - expect(libUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0); - expect(indexUpdateSummaryBeforeInit['platform']).to.be.undefined; + expect(indexUpdateSummaryBeforeInit).to.be.not.undefined; + expect(indexUpdateSummaryBeforeInit).to.be.empty; }); const rawJson = await fs.readFile(libraryPackageIndexPath, { encoding: 'utf8', @@ -191,20 +181,38 @@ describe('core-client-provider', () => { const container = await startCli(configDirPath, toDispose); await assertFunctionalCli( container, - async ({ coreClientProvider, boardsService, coreService }) => { + async ({ coreClientProvider, boardsService }) => { const { indexUpdateSummaryBeforeInit } = coreClientProvider; expect(indexUpdateSummaryBeforeInit).to.be.not.undefined; expect(indexUpdateSummaryBeforeInit).to.be.empty; - - // IDE2 cannot recover from a 3rd party package index issue. - // Only when the primary package or library index is corrupt. - // https://github.com/arduino/arduino-ide/issues/2021 - await coreService.updateIndex({ types: ['platform'] }); - await assertTeensyAvailable(boardsService); } ); }); + + it("should recover when invalid 3rd package URL is defined in the CLI config and the 'directories.data' folder is missing", async function () { + this.timeout(timeout); + const configDirPath = await prepareTestConfigDir(); + deleteSync(join(configDirPath, 'data')); + + // set an invalid URL so the CLI will try to download it + const cliConfigPath = join(configDirPath, 'arduino-cli.yaml'); + const rawYaml = await fs.readFile(cliConfigPath, { encoding: 'utf8' }); + const config: DefaultCliConfig = load(rawYaml); + expect(config.board_manager).to.be.undefined; + config.board_manager = { additional_urls: ['https://invalidUrl'] }; + expect(config.board_manager?.additional_urls?.[0]).to.be.equal( + 'https://invalidUrl' + ); + await fs.writeFile(cliConfigPath, dump(config)); + + const container = await startCli(configDirPath, toDispose); + await assertFunctionalCli(container, ({ coreClientProvider }) => { + const { indexUpdateSummaryBeforeInit } = coreClientProvider; + expect(indexUpdateSummaryBeforeInit).to.be.not.undefined; + expect(indexUpdateSummaryBeforeInit).to.be.empty; + }); + }); }); interface Services { @@ -277,7 +285,7 @@ async function prepareTestConfigDir( const params = { configDirPath: newTempConfigDirPath(), configOverrides }; const container = await createContainer(params); const daemon = container.get(ArduinoDaemonImpl); - const cliPath = await daemon.getExecPath(); + const cliPath = daemon.getExecPath(); const configDirUriProvider = container.get(ConfigDirUriProvider); const configDirPath = FileUri.fsPath(configDirUriProvider.configDirUri()); diff --git a/arduino-ide-extension/src/test/node/core-service-impl.test.ts b/arduino-ide-extension/src/test/node/core-service-impl.test.ts index abba88357..ac419dd2d 100644 --- a/arduino-ide-extension/src/test/node/core-service-impl.test.ts +++ b/arduino-ide-extension/src/test/node/core-service-impl.test.ts @@ -1,5 +1,10 @@ import { expect } from 'chai'; -import { Port } from '../../node/cli-protocol/cc/arduino/cli/commands/v1/port_pb'; +import { + PortIdentifier, + portIdentifierEquals, + Port, +} from '../../common/protocol/boards-service'; +import { Port as RpcPort } from '../../node/cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { CoreServiceImpl } from '../../node/core-service-impl'; describe('core-service-impl', () => { @@ -22,9 +27,18 @@ describe('core-service-impl', () => { protocolLabel: 'serial port', properties, } as const; - const actual = new CoreServiceImpl()['createPort'](port); + const resolve = (toResolve: PortIdentifier): Port | undefined => { + if (portIdentifierEquals(toResolve, port)) { + return port; + } + return undefined; + }; + const actual = new CoreServiceImpl()['createPort']( + { protocol: port.protocol, address: port.address }, + resolve + ); expect(actual).to.be.not.undefined; - const expected = new Port() + const expected = new RpcPort() .setAddress(port.address) .setHardwareId(port.hardwareId) .setLabel(port.addressLabel) @@ -33,7 +47,7 @@ describe('core-service-impl', () => { Object.entries(properties).forEach(([key, value]) => expected.getPropertiesMap().set(key, value) ); - expect((actual).toObject(false)).to.be.deep.equal( + expect((actual).toObject(false)).to.be.deep.equal( expected.toObject(false) ); }); diff --git a/arduino-ide-extension/src/test/node/test-bindings.ts b/arduino-ide-extension/src/test/node/test-bindings.ts index 30d8513a9..ecf07dbc8 100644 --- a/arduino-ide-extension/src/test/node/test-bindings.ts +++ b/arduino-ide-extension/src/test/node/test-bindings.ts @@ -29,13 +29,12 @@ import { join } from 'node:path'; import { path as tempPath, track } from 'temp'; import { ArduinoDaemon, - AttachedBoardsChangeEvent, - AvailablePorts, BoardsPackage, BoardsService, ConfigService, ConfigState, CoreService, + DetectedPorts, IndexUpdateDidCompleteParams, IndexUpdateDidFailParams, IndexUpdateParams, @@ -160,7 +159,7 @@ class SilentArduinoDaemon extends ArduinoDaemonImpl { @injectable() class TestBoardDiscovery extends BoardDiscovery { - mutableAvailablePorts: AvailablePorts = {}; + mutableDetectedPorts: DetectedPorts = {}; override async start(): Promise { // NOOP @@ -168,8 +167,8 @@ class TestBoardDiscovery extends BoardDiscovery { override async stop(): Promise { // NOOP } - override get availablePorts(): AvailablePorts { - return this.mutableAvailablePorts; + override get detectedPorts(): DetectedPorts { + return this.mutableDetectedPorts; } } @@ -221,7 +220,7 @@ class TestNotificationServiceServer implements NotificationServiceServer { notifyLibraryDidUninstall(event: { item: LibraryPackage }): void { this.events.push(`notifyLibraryDidUninstall:${JSON.stringify(event)}`); } - notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void { + notifyDetectedPortsDidChange(event: { detectedPorts: DetectedPorts }): void { this.events.push(`notifyAttachedBoardsDidChange:${JSON.stringify(event)}`); } notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { diff --git a/i18n/en.json b/i18n/en.json index 9a5b38eda..c0a294770 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -18,24 +18,21 @@ "configDialog1": "Select both a Board and a Port if you want to upload a sketch.", "configDialog2": "If you only select a Board you will be able to compile, but not to upload your sketch.", "couldNotFindPreviouslySelected": "Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?", - "disconnected": "Disconnected", + "editBoardsConfig": "Edit Board and Port...", "getBoardInfo": "Get Board Info", "inSketchbook": " (in Sketchbook)", "installNow": "The \"{0} {1}\" core has to be installed for the currently selected \"{2}\" board. Do you want to install it now?", "noBoardsFound": "No boards found for \"{0}\"", - "noFQBN": "The FQBN is not available for the selected board \"{0}\". Do you have the corresponding core installed?", "noNativeSerialPort": "Native serial port, can't obtain info.", "noPortsDiscovered": "No ports discovered", - "noPortsSelected": "No ports selected for board: '{0}'.", "nonSerialPort": "Non-serial port, can't obtain info.", - "noneSelected": "No boards selected.", "openBoardsConfig": "Select other board and port…", "pleasePickBoard": "Please pick a board connected to the port you have selected.", "port": "Port{0}", - "portLabel": "Port: {0}", "ports": "ports", "programmer": "Programmer", "reselectLater": "Reselect later", + "revertBoardsConfig": "Revert the selected '{0}' board to '{1}' detected on '{2}'", "searchBoard": "Search board", "selectBoard": "Select Board", "selectPortForInfo": "Please select a port to obtain board info.", @@ -215,6 +212,11 @@ "optimizeForDebugging": "Optimize for Debugging", "sketchIsNotCompiled": "Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?" }, + "developer": { + "clearBoardList": "Clear the Board List History", + "clearBoardsConfig": "Clear the Board and Port Selection", + "dumpBoardList": "Dump the Board List" + }, "dialog": { "dontAskAgain": "Don't ask again" }, From cf3a07fd4fcfa50a0f3d27842bd39bf9b2c8f0e4 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 3 Aug 2023 11:45:56 +0200 Subject: [PATCH 2/3] fix(build): made protoc optional it's unavailable on macOS arm64 (YePpHa/node-protoc#9) and the API generation is a manual process: 0942ef6450ddbd335c128cef217ead3433449fc9 Signed-off-by: Akos Kitta --- arduino-ide-extension/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index fb0bf563d..a210347f5 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -123,13 +123,13 @@ "mockdate": "^3.0.5", "moment": "^2.24.0", "ncp": "^2.0.0", - "protoc": "^1.0.4", "shelljs": "^0.8.3", "uuid": "^3.2.1", "yargs": "^11.1.0" }, "optionalDependencies": { - "grpc-tools": "^1.9.0" + "grpc-tools": "^1.9.0", + "protoc": "^1.0.4" }, "mocha": { "require": [ From 610e46433c3089a89d3acfeb85fe15b4da783ade Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 3 Aug 2023 15:15:36 +0200 Subject: [PATCH 3/3] fix: do not let the React dialogs dispose on close To workaround the React-based widget lifecycle issue: eclipse-theia/theia#12093. The dialog constructor is called once, hence the default `toDispose` listener will execute only once per app lifecycle. The default dialog close behavior (from Theia) will dispose the widget. On dialog reopen, the constructor won't be called again, as the dialog is injected as a singleton, but the React component has been unmounted on dialog dispose. Hence, they will be recreated. The `componentDidMount` method will be called, but the `componentWillUnmount` is not called anymore. The dialog is already disposed. It leads to a resource leak. Signed-off-by: Akos Kitta --- .../src/browser/theia/dialogs/dialogs.tsx | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx index 20284b413..c354bfd77 100644 --- a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx +++ b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx @@ -3,14 +3,9 @@ import { DialogProps, } from '@theia/core/lib/browser/dialogs'; import { ReactDialog as TheiaReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog'; -import { codiconArray, Message } from '@theia/core/lib/browser/widgets/widget'; -import { - Disposable, - DisposableCollection, -} from '@theia/core/lib/common/disposable'; +import { codiconArray } from '@theia/core/lib/browser/widgets/widget'; +import type { Message } from '@theia/core/shared/@phosphor/messaging'; import { inject, injectable } from '@theia/core/shared/inversify'; -import * as React from '@theia/core/shared/react'; -import { createRoot } from '@theia/core/shared/react-dom/client'; @injectable() export abstract class AbstractDialog extends TheiaAbstractDialog { @@ -25,38 +20,26 @@ export abstract class AbstractDialog extends TheiaAbstractDialog { @injectable() export abstract class ReactDialog extends TheiaReactDialog { - protected override onUpdateRequest(msg: Message): void { - // This is tricky to bypass the default Theia code. - // Otherwise, there is a warning when opening the dialog for the second time. - // You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it. - const disposables = new DisposableCollection(); - if (!this.isMounted) { - // toggle the `isMounted` logic for the time being of the super call so that the `createRoot` does not run - this.isMounted = true; - disposables.push(Disposable.create(() => (this.isMounted = false))); - } + private _isOnCloseRequestInProgress = false; - // Always unset the `contentNodeRoot` so there is no double update when calling super. - const restoreContentNodeRoot = this.contentNodeRoot; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.contentNodeRoot as any) = undefined; - disposables.push( - Disposable.create(() => (this.contentNodeRoot = restoreContentNodeRoot)) - ); + override dispose(): void { + // There is a bug in Theia, and the React component's `componentWillUnmount` will not be called, as the Theia widget is already disposed when closing and reopening a dialog. + // Widget lifecycle issue in Theia: https://github.com/eclipse-theia/theia/issues/12093 + // Bogus react widget lifecycle management PR: https://github.com/eclipse-theia/theia/pull/11687 + // Do not call super. Do not let the Phosphor widget to be disposed on dialog close. + if (this._isOnCloseRequestInProgress) { + // Do not let the widget dispose on close. + return; + } + super.dispose(); + } + protected override onCloseRequest(message: Message): void { + this._isOnCloseRequestInProgress = true; try { - super.onUpdateRequest(msg); + super.onCloseRequest(message); } finally { - disposables.dispose(); - } - - // Use the patched rendering. - if (!this.isMounted) { - this.contentNodeRoot = createRoot(this.contentNode); - // Resetting the prop is missing from the Theia code. - // https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/browser/dialogs/react-dialog.tsx#L41-L47 - this.isMounted = true; + this._isOnCloseRequestInProgress = false; } - this.contentNodeRoot?.render(<>{this.render()}); } }