diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index c73d4223..8eae39c0 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -36,9 +36,9 @@ "@theia/workspace": "next", "@types/dateformat": "^3.0.1", "@types/deepmerge": "^2.2.0", - "@types/js-yaml": "^3.12.2", "@types/glob": "^5.0.35", "@types/google-protobuf": "^3.7.1", + "@types/js-yaml": "^3.12.2", "@types/lodash.debounce": "^4.0.6", "@types/ps-tree": "^1.1.0", "@types/react-select": "^3.0.0", @@ -47,6 +47,7 @@ "css-element-queries": "^1.2.0", "dateformat": "^3.0.3", "deepmerge": "^4.2.2", + "fuzzy": "^0.1.3", "glob": "^7.1.6", "google-protobuf": "^3.11.0", "lodash.debounce": "^4.0.8", @@ -105,6 +106,9 @@ { "frontend": "lib/browser/menu/browser-arduino-menu-module", "frontendElectron": "lib/electron-browser/menu/electron-arduino-menu-module" + }, + { + "frontend": "lib/browser/boards/quick-open/boards-quick-open-module" } ] } diff --git a/arduino-ide-extension/src/browser/boards/boards-config-quick-open-service.ts b/arduino-ide-extension/src/browser/boards/boards-config-quick-open-service.ts deleted file mode 100644 index 3220174b..00000000 --- a/arduino-ide-extension/src/browser/boards/boards-config-quick-open-service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { QuickOpenItem, QuickOpenModel } from '@theia/core/lib/common/quick-open-model'; -import { QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/quick-open-service'; -import { BoardsService, BoardsServiceClient } from '../../common/protocol'; - -@injectable() -export class BoardsConfigQuickOpenService { - - @inject(QuickOpenService) - protected readonly quickOpenService: QuickOpenService; - - @inject(BoardsService) - protected readonly boardsService: BoardsService; - - @inject(BoardsServiceClient) - protected readonly boardsServiceClient: BoardsServiceClient; - - async selectBoard(): Promise { - - } - - protected open(items: QuickOpenItem | QuickOpenItem[], placeholder: string): void { - this.quickOpenService.open(this.getModel(Array.isArray(items) ? items : [items]), this.getOptions(placeholder)); - } - - protected getOptions(placeholder: string, fuzzyMatchLabel: boolean = true, onClose: (canceled: boolean) => void = () => { }): QuickOpenOptions { - return QuickOpenOptions.resolve({ - placeholder, - fuzzyMatchLabel, - fuzzySort: false, - onClose - }); - } - - protected getModel(items: QuickOpenItem | QuickOpenItem[]): QuickOpenModel { - return { - onType(_: string, acceptor: (items: QuickOpenItem[]) => void): void { - acceptor(Array.isArray(items) ? items : [items]); - } - }; - } - -} diff --git a/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts b/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts index 8fcaa66b..c9df896c 100644 --- a/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts +++ b/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts @@ -45,24 +45,25 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution const boardsConfigMenuPath = [...ArduinoMenus.TOOLS, 'z_boardsConfig']; // `z_` is for ordering. for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) { const menuPath = [...boardsConfigMenuPath, `${option}`]; - const commands = new Map() + const commands = new Map() for (const value of values) { const id = `${fqbn}-${option}--${value.value}`; - const command = { id, label: value.label }; + const command = { id }; const selectedValue = value.value; const handler = { execute: () => this.boardsConfigStore.setSelected({ fqbn, option, selectedValue }), isToggled: () => value.selected }; - commands.set(id, this.commandRegistry.registerCommand(command, handler)); + commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label })); } this.menuRegistry.registerSubmenu(menuPath, label); this.toDisposeOnBoardChange.pushAll([ ...commands.values(), Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299 ...Array.from(commands.keys()).map((commandId, index) => { - this.menuRegistry.registerMenuAction(menuPath, { commandId, order: String(index) }) - return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId)) + const { label } = commands.get(commandId)!; + this.menuRegistry.registerMenuAction(menuPath, { commandId, order: String(index), label }); + return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId)); }) ]); } diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index b9970c06..aa18b5ea 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -7,6 +7,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten import { RecursiveRequired } from '../../common/types'; import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; +import { naturalCompare } from '../../common/utils'; @injectable() export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution { @@ -262,10 +263,10 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp }); } - const sortedAvailableBoards = availableBoards.sort(AvailableBoard.COMPARATOR); + const sortedAvailableBoards = availableBoards.sort(AvailableBoard.compare); let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length; for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) { - hasChanged = AvailableBoard.COMPARATOR(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0; + hasChanged = AvailableBoard.compare(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0; } if (hasChanged) { this._availableBoards = sortedAvailableBoards; @@ -312,9 +313,10 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp } /** - * Representation of a ready-to-use board, configured by the user. Not all of the available boards are - * 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`. + * 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; @@ -339,17 +341,27 @@ export namespace AvailableBoard { 'incomplete' } - export function isWithPort(board: AvailableBoard): board is AvailableBoard & { port: Port } { + export function is(board: any): board is AvailableBoard { + return Board.is(board) && 'state' in board; + } + + export function hasPort(board: AvailableBoard): board is AvailableBoard & { port: Port } { return !!board.port; } - export const COMPARATOR = (left: AvailableBoard, right: AvailableBoard) => { - let result = left.name.localeCompare(right.name); + export const compare = (left: AvailableBoard, right: AvailableBoard) => { + if (left.selected && !right.selected) { + return -1; + } + if (right.selected && !left.selected) { + return 1; + } + let result = naturalCompare(left.name, right.name); if (result !== 0) { return result; } if (left.fqbn && right.fqbn) { - result = left.name.localeCompare(right.name); + result = naturalCompare(left.fqbn, right.fqbn); if (result !== 0) { return result; } 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 a24c7882..5e34e1fb 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -151,7 +151,7 @@ export class BoardsToolBarItem extends React.Component ({ + items={availableBoards.filter(AvailableBoard.hasPort).map(board => ({ ...board, onClick: () => { if (board.state === AvailableBoard.State.incomplete) { diff --git a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts new file mode 100644 index 00000000..1d1f6e07 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts @@ -0,0 +1,16 @@ +import { ContainerModule } from 'inversify'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { CommandContribution } from '@theia/core/lib/common/command'; +import { QuickOpenContribution } from '@theia/core/lib/browser/quick-open'; +import { KeybindingContribution } from '@theia/core/lib/browser/keybinding'; +import { BoardsQuickOpenService } from './boards-quick-open-service'; + +export default new ContainerModule(bind => { + bind(BoardsQuickOpenService).toSelf().inSingletonScope(); + bind(CommandContribution).toService(BoardsQuickOpenService); + bind(KeybindingContribution).toService(BoardsQuickOpenService); + bind(QuickOpenContribution).toService(BoardsQuickOpenService); + bind(ILogger).toDynamicValue(({ container }) => container.get(ILogger).child('boards-quick-open')) + .inSingletonScope() + .whenTargetNamed('boards-quick-open'); +}); diff --git a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts new file mode 100644 index 00000000..27a21819 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts @@ -0,0 +1,309 @@ +import * as fuzzy from 'fuzzy'; +import { inject, injectable, postConstruct, named } from 'inversify'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command'; +import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { QuickOpenItem, QuickOpenModel, QuickOpenMode, QuickOpenGroupItem } from '@theia/core/lib/common/quick-open-model'; +import { + QuickOpenService, + QuickOpenHandler, + QuickOpenOptions, + QuickOpenItemOptions, + QuickOpenContribution, + QuickOpenActionProvider, + QuickOpenHandlerRegistry, + QuickOpenGroupItemOptions +} from '@theia/core/lib/browser/quick-open'; +import { naturalCompare } from '../../../common/utils'; +import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol'; +import { CoreServiceClientImpl } from '../../core-service-client-impl'; +import { BoardsConfigStore } from '../boards-config-store'; +import { BoardsServiceClientImpl, AvailableBoard } from '../boards-service-client-impl'; + +@injectable() +export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command { + + readonly id = 'arduino-boards-quick-open'; + readonly prefix = '|'; + readonly description = 'Configure Available Boards'; + readonly label: 'Configure Available Boards'; + + @inject(ILogger) + @named('boards-quick-open') + protected readonly logger: ILogger; + + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; + + @inject(BoardsService) + protected readonly boardsService: BoardsService; + + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + + @inject(BoardsConfigStore) + protected readonly configStore: BoardsConfigStore; + + @inject(CoreServiceClientImpl) + protected coreServiceClient: CoreServiceClientImpl; + + protected isOpen: boolean = false; + protected currentQuery: string = ''; + // Attached boards plus the user's config. + protected availableBoards: AvailableBoard[] = []; + // Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional. + protected boardConfigs: ConfigOption[] = []; + protected allBoards: Board.Detailed[] = [] + protected selectedBoard?: (AvailableBoard & { port: Port }); + + // `init` name is used by the `QuickOpenHandler`. + @postConstruct() + protected postConstruct(): void { + this.coreServiceClient.onIndexUpdated(() => this.update(this.availableBoards)); + this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards)); + this.update(this.boardsServiceClient.availableBoards); + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(this, { execute: () => this.open() }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ command: this.id, keybinding: 'ctrlCmd+k ctrlCmd+b' }); + } + + registerQuickOpenHandlers(registry: QuickOpenHandlerRegistry): void { + registry.registerHandler(this); + } + + getModel(): QuickOpenModel { + return this; + } + + getOptions(): QuickOpenOptions { + let placeholder = ''; + if (!this.selectedBoard) { + placeholder += 'No board selected.'; + } + placeholder += 'Type to filter boards'; + if (this.boardConfigs.length) { + placeholder += ' or use the ↓↑ keys to adjust the board settings...'; + } else { + placeholder += '...'; + } + return { + placeholder, + fuzzyMatchLabel: true, + onClose: () => this.isOpen = false + }; + } + + open(): void { + this.isOpen = true; + this.quickOpenService.open(this, this.getOptions()); + } + + onType( + lookFor: string, + acceptor: (items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider) => void): void { + + this.currentQuery = lookFor; + const fuzzyFilter = this.fuzzyFilter(lookFor); + const availableBoards = this.availableBoards.filter(AvailableBoard.hasPort).filter(({ name }) => fuzzyFilter(name)); + const toAccept: QuickOpenItem[] = []; + + // Show the selected attached in a different group. + if (this.selectedBoard && fuzzyFilter(this.selectedBoard.name)) { + toAccept.push(this.toQuickItem(this.selectedBoard, { groupLabel: 'Selected Board' })); + } + + // Filter the selected from the attached ones. + toAccept.push(...availableBoards.filter(board => board !== this.selectedBoard).map((board, i) => { + let group: QuickOpenGroupItemOptions | undefined = undefined; + if (i === 0) { + // If no `selectedBoard`, then this item is the top one, no borders required. + group = { groupLabel: 'Attached Boards', showBorder: !!this.selectedBoard }; + } + return this.toQuickItem(board, group); + })); + + // Show the config only if the `input` is empty. + if (!lookFor.trim().length) { + toAccept.push(...this.boardConfigs.map((config, i) => { + let group: QuickOpenGroupItemOptions | undefined = undefined; + if (i === 0) { + group = { groupLabel: 'Board Settings', showBorder: true }; + } + return this.toQuickItem(config, group); + })); + } else { + toAccept.push(...this.allBoards.filter(({ name }) => fuzzyFilter(name)).map((board, i) => { + let group: QuickOpenGroupItemOptions | undefined = undefined; + if (i === 0) { + group = { groupLabel: 'Boards', showBorder: true }; + } + return this.toQuickItem(board, group); + })); + } + + acceptor(toAccept); + } + + private fuzzyFilter(lookFor: string): (inputString: string) => boolean { + const shouldFilter = !!lookFor.trim().length; + return (inputString: string) => shouldFilter ? fuzzy.test(lookFor.toLocaleLowerCase(), inputString.toLocaleLowerCase()) : true; + } + + protected async update(availableBoards: AvailableBoard[]): Promise { + // `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!) + const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected); + const [configs, boards] = await Promise.all([ + selectedBoard && selectedBoard.fqbn ? this.configStore.getConfig(selectedBoard.fqbn) : Promise.resolve([]), + this.boardsService.searchBoards({}) + ]); + this.allBoards = Board.decorateBoards(selectedBoard, boards) + .filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board))); + this.availableBoards = availableBoards; + this.boardConfigs = configs; + this.selectedBoard = selectedBoard; + + if (this.isOpen) { + // Hack, to update the state without closing and reopening the quick open widget. + (this.quickOpenService as any).onType(this.currentQuery); + } + } + + protected toQuickItem(item: BoardsQuickOpenService.Item, group?: QuickOpenGroupItemOptions): QuickOpenItem { + let options: QuickOpenItemOptions; + if (AvailableBoard.is(item)) { + const description = `on ${Port.toString(item.port)}` + options = { + label: `${item.name}`, + description, + descriptionHighlights: [ + { + start: 0, + end: description.length + } + ], + run: this.toRun(() => this.boardsServiceClient.boardsConfig = ({ selectedBoard: item, selectedPort: item.port })) + }; + } else if (ConfigOption.is(item)) { + const selected = item.values.find(({ selected }) => selected); + const value = selected ? selected.label : 'Not set'; + const label = `${item.label}: ${value}`; + options = { + label, + // Intended to match the value part of a board setting. + // NOTE: this does not work, as `fuzzyMatchLabel: true` is set. Manual highlighting is ignored, apparently. + labelHighlights: [ + { + start: label.length - value.length, + end: label.length + } + ], + run: (mode) => { + if (mode === QuickOpenMode.OPEN) { + this.setConfig(item); + return false; + } + return true; + } + }; + if (!selected) { + options.description = 'Not set'; + }; + } else { + options = { + label: `${item.name}`, + description: `${item.missing ? '' : `[installed with '${item.packageName}']`}`, + run: (mode) => { + if (mode === QuickOpenMode.OPEN) { + this.selectBoard(item); + return false; + } + return true; + } + }; + } + if (group) { + return new QuickOpenGroupItem({ ...options, ...group }); + } else { + return new QuickOpenItem(options); + } + } + + protected toRun(run: (() => void)): ((mode: QuickOpenMode) => boolean) { + return (mode) => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + run(); + return true; + }; + } + + protected async selectBoard(board: Board): Promise { + const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare); + const toItem = (port: Port) => new QuickOpenItem({ + label: Port.toString(port, { useLabel: true }), + run: this.toRun(() => { + this.boardsServiceClient.boardsConfig = { + selectedBoard: board, + selectedPort: port + }; + }) + }); + const options = { + placeholder: `Select a port for '${board.name}'. Press 'Enter' to confirm or 'Escape' to cancel.`, + fuzzyMatchLabel: true + } + this.quickOpenService.open({ + onType: (lookFor, acceptor) => { + const fuzzyFilter = this.fuzzyFilter(lookFor); + acceptor(allPorts.filter(({ address }) => fuzzyFilter(address)).map(toItem)); + } + }, options); + } + + protected async setConfig(config: ConfigOption): Promise { + const toItem = (value: ConfigValue) => new QuickOpenItem({ + label: value.label, + iconClass: value.selected ? 'fa fa-check' : '', + run: this.toRun(() => { + if (!this.selectedBoard) { + this.logger.warn(`Could not alter the boards settings. No board selected. ${JSON.stringify(config)}`); + return; + } + if (!this.selectedBoard.fqbn) { + this.logger.warn(`Could not alter the boards settings. The selected board does not have a FQBN. ${JSON.stringify(this.selectedBoard)}`); + return; + } + const { fqbn } = this.selectedBoard; + this.configStore.setSelected({ + fqbn, + option: config.option, + selectedValue: value.value + }); + }) + }); + const options = { + placeholder: `Configure '${config.label}'. Press 'Enter' to confirm or 'Escape' to cancel.`, + fuzzyMatchLabel: true + } + this.quickOpenService.open({ + onType: (lookFor, acceptor) => { + const fuzzyFilter = this.fuzzyFilter(lookFor); + acceptor(config.values + .filter(({ label }) => fuzzyFilter(label)) + .sort((left, right) => naturalCompare(left.label, right.label)) + .map(toItem)); + } + }, options); + } + +} + +export namespace BoardsQuickOpenService { + export type Item = AvailableBoard & { port: Port } | Board.Detailed | ConfigOption; +} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 03638b05..df660801 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,9 +1,9 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { naturalCompare } from './../utils'; import { Searchable } from './searchable'; import { Installable } from './installable'; import { ArduinoComponent } from './arduino-component'; -const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive; export interface AttachedBoardsChangeEvent { readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>; @@ -109,7 +109,7 @@ export namespace Port { if (!isBoardPort(left) && isBoardPort(right)) { return 1; } - let result = left.protocol.toLocaleLowerCase().localeCompare(right.protocol.toLocaleLowerCase()); + let result = naturalCompare(left.protocol.toLocaleLowerCase(), right.protocol.toLocaleLowerCase()); if (result !== 0) { return result; } @@ -117,7 +117,7 @@ export namespace Port { if (result !== 0) { return result; } - return (left.label || '').localeCompare(right.label || ''); + return naturalCompare(left.label || '', right.label || ''); } export function equals(left: Port | undefined, right: Port | undefined): boolean { @@ -214,6 +214,11 @@ export interface ConfigOption { } 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. @@ -266,7 +271,7 @@ export namespace ConfigOption { } } - export const LABEL_COMPARATOR = (left: ConfigOption, right: ConfigOption) => left.label.toLocaleLowerCase().localeCompare(right.label.toLocaleLowerCase()); + export const LABEL_COMPARATOR = (left: ConfigOption, right: ConfigOption) => naturalCompare(left.label.toLocaleLowerCase(), right.label.toLocaleLowerCase()); } @@ -298,9 +303,9 @@ export namespace Board { } export function compare(left: Board, right: Board): number { - let result = left.name.localeCompare(right.name); + let result = naturalCompare(left.name, right.name); if (result === 0) { - result = (left.fqbn || '').localeCompare(right.fqbn || ''); + result = naturalCompare(left.fqbn || '', right.fqbn || ''); } return result; } @@ -314,13 +319,14 @@ export namespace Board { return `${board.name}${fqbn}`; } + export type Detailed = Board & Readonly<{ selected: boolean, missing: boolean, packageName: string, details?: string }>; export function decorateBoards( selectedBoard: Board | undefined, - searchResults: Array): Array { + boards: Array): Array { // 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(); - for (const { name } of searchResults) { + for (const { name } of boards) { const counter = distinctBoardNames.get(name) || 0; distinctBoardNames.set(name, counter + 1); } @@ -337,7 +343,7 @@ export namespace Board { } return false; } - return searchResults.map(board => ({ + return boards.map(board => ({ ...board, details: (distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined, selected: selected(board), diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index ae7334d2..4bf90759 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,4 +1,4 @@ -const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive; +import { naturalCompare } from './../utils'; import { ArduinoComponent } from './arduino-component'; export interface Installable { diff --git a/arduino-ide-extension/src/common/utils.ts b/arduino-ide-extension/src/common/utils.ts new file mode 100644 index 00000000..77dbaf54 --- /dev/null +++ b/arduino-ide-extension/src/common/utils.ts @@ -0,0 +1 @@ +export const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;