diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 73d60d977..c90835c5c 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -3,12 +3,14 @@ import { DisposableCollection, } from '@theia/core/lib/common/disposable'; 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, @@ -176,54 +178,104 @@ SN: ${SN} ? createPlatformIdentifier(selectedBoard) : undefined; - // Installed boards - installedBoards.forEach((board, index) => { - const { packageId, packageName, fqbn, name, manuallyInstalled } = board; - - let packageLabel = - packageName + - `${ - manuallyInstalled - ? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)') - : '' - }`; - if ( - selectedBoardPlatformId && - platformIdentifierEquals(packageId, selectedBoardPlatformId) - ) { - packageLabel = `✓ ${packageLabel}`; + // 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); } - // Platform submenu - const platformMenuPath = [ - ...boardsPackagesGroup, - serializePlatformIdentifier(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(), - }); + let boardPerVendor: BoardWithPackage[] | undefined = + boardsPerPackageName[vendorId]; + if (!boardPerVendor) { + boardPerVendor = []; + boardsPerPackageName[vendorId] = boardPerVendor; + } + boardPerVendor.push(board); + } - const id = `arduino-select-board--${fqbn}`; - const command = { id }; - const handler = { - execute: () => - this.boardsServiceProvider.updateBoard({ name: name, fqbn: fqbn }), - isToggled: () => fqbn === selectedBoard?.fqbn, - }; + // 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(), + } + ); + } - // 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. - }); + const id = `arduino-select-board--${fqbn}`; + const command = { id }; + const handler = { + execute: () => + this.boardsServiceProvider.updateBoard({ + name: name, + fqbn: fqbn, + }), + isToggled: () => fqbn === 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. + }); + }); + } + ); // Detected ports const registerPorts = (