diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 4121550c..12a41702 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -115,16 +115,13 @@ "frontend": "lib/browser/theia/core/browser-menu-module", "frontendElectron": "lib/electron-browser/theia/core/electron-menu-module" }, - { - "frontend": "lib/browser/boards/quick-open/boards-quick-open-module" - }, { "electronMain": "lib/electron-main/arduino-electron-main-module" } ], "arduino": { "cli": { - "version": "20210315" + "version": "20210317" } } } 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 0b095f54..e3d41898 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -15,16 +15,10 @@ import { } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; import { naturalCompare } from '../../common/utils'; -import { compareAnything } from '../theia/monaco/comparers'; import { NotificationCenter } from '../notification-center'; import { CommandService } from '@theia/core'; import { ArduinoCommands } from '../arduino-commands'; -interface BoardMatch { - readonly board: BoardWithPackage; - readonly matches: monaco.filters.IMatch[] | undefined; -} - @injectable() export class BoardsServiceProvider implements FrontendApplicationContribution { @@ -205,38 +199,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { } } - async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise> { - const boards = await this.boardsService.allBoards({}); - const coresFilter = !!cores && cores.length - ? ((toFilter: BoardWithPackage) => cores.some(core => core === toFilter.packageName || core === toFilter.packageId)) - : () => true; - if (!query) { - return boards.filter(coresFilter).sort(Board.compare); - } - const toMatch = ((toFilter: BoardWithPackage) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) }))); - const compareEntries = (left: BoardMatch, right: BoardMatch, lookFor: string) => { - const leftMatches = left.matches || []; - const rightMatches = right.matches || []; - if (leftMatches.length && !rightMatches.length) { - return -1; - } - if (!leftMatches.length && rightMatches.length) { - return 1; - } - if (leftMatches.length === 0 && rightMatches.length === 0) { - return 0; - } - const leftLabel = left.board.name.replace(/\r?\n/g, ' '); - const rightLabel = right.board.name.replace(/\r?\n/g, ' '); - return compareAnything(leftLabel, rightLabel, lookFor); - } - const normalizedQuery = query.toLowerCase(); - return boards - .filter(coresFilter) - .map(toMatch) - .filter(({ matches }) => !!matches) - .sort((left, right) => compareEntries(left, right, normalizedQuery)) - .map(({ board }) => board); + async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise { + const boards = await this.boardsService.searchBoards({ query }); + return boards; } get boardsConfig(): BoardsConfig.Config { 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 deleted file mode 100644 index 1d1f6e07..00000000 --- a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 11182fed..00000000 --- a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts +++ /dev/null @@ -1,309 +0,0 @@ -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 { BoardsDataStore } from '../boards-data-store'; -import { BoardsServiceProvider, AvailableBoard } from '../boards-service-provider'; -import { NotificationCenter } from '../../notification-center'; - -@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(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(NotificationCenter) - protected notificationCenter: NotificationCenter; - - 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 data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY; - protected allBoards: Board.Detailed[] = [] - protected selectedBoard?: (AvailableBoard & { port: Port }); - - // `init` name is used by the `QuickOpenHandler`. - @postConstruct() - protected postConstruct(): void { - this.notificationCenter.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.data.configOptions.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.data.configOptions.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 [data, boards] = await Promise.all([ - selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY), - this.boardsService.allBoards({}) - ]); - this.allBoards = Board.decorateBoards(selectedBoard, boards) - .filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board))); - this.availableBoards = availableBoards; - this.data = data; - 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.boardsDataStore.selectConfigOption({ - 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/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 3373e20c..3e84a76d 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -210,7 +210,7 @@ PID: ${PID}`; } protected async installedBoards(): Promise { - const allBoards = await this.boardsService.allBoards({}); + const allBoards = await this.boardsService.searchBoards({}); return allBoards.filter(InstalledBoardWithPackage.is); } diff --git a/arduino-ide-extension/src/browser/theia/monaco/comparers.ts b/arduino-ide-extension/src/browser/theia/monaco/comparers.ts deleted file mode 100644 index 25c893ce..00000000 --- a/arduino-ide-extension/src/browser/theia/monaco/comparers.ts +++ /dev/null @@ -1,368 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// Copied from https://github.com/microsoft/vscode/blob/724c307bf35646ac549a8533a255c51b63fea5c7/src/vs/base/common/comparers.ts -// We cannot customize the monaco loader for Theia: https://github.com/eclipse-theia/theia/issues/8220 - -import { isWindows } from '@theia/core/lib/common/os'; - -const sep = (isWindows ? '\\' : '/'); -interface IDisposable { - dispose(): void; -} -interface IdleDeadline { - readonly didTimeout: boolean; - timeRemaining(): number; -} -let runWhenIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable; -declare function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number; -declare function cancelIdleCallback(handle: number): void; - -(function () { - if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') { - const dummyIdle: IdleDeadline = Object.freeze({ - didTimeout: true, - timeRemaining() { return 15; } - }); - runWhenIdle = (runner) => { - const handle = setTimeout(() => runner(dummyIdle)); - let disposed = false; - return { - dispose() { - if (disposed) { - return; - } - disposed = true; - clearTimeout(handle); - } - }; - }; - } else { - runWhenIdle = (runner, timeout?) => { - const handle: number = requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined); - let disposed = false; - return { - dispose() { - if (disposed) { - return; - } - disposed = true; - cancelIdleCallback(handle); - } - }; - }; - } -})(); - -/** - * An implementation of the "idle-until-urgent"-strategy as introduced - * here: https://philipwalton.com/articles/idle-until-urgent/ - */ -class IdleValue { - - private readonly _executor: () => void; - private readonly _handle: IDisposable; - - private _didRun: boolean = false; - private _value?: T; - private _error: any; - - constructor(executor: () => T) { - this._executor = () => { - try { - this._value = executor(); - } catch (err) { - this._error = err; - } finally { - this._didRun = true; - } - }; - this._handle = runWhenIdle(() => this._executor()); - } - - dispose(): void { - this._handle.dispose(); - } - - get value(): T { - if (!this._didRun) { - this._handle.dispose(); - this._executor(); - } - if (this._error) { - throw this._error; - } - return this._value!; - } -} - -// When comparing large numbers of strings, such as in sorting large arrays, is better for -// performance to create an Intl.Collator object and use the function provided by its compare -// property than it is to use String.prototype.localeCompare() - -// A collator with numeric sorting enabled, and no sensitivity to case or to accents -const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { - const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); - return { - collator: collator, - collatorIsNumeric: collator.resolvedOptions().numeric - }; -}); - -// A collator with numeric sorting enabled. -const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { - const collator = new Intl.Collator(undefined, { numeric: true }); - return { - collator: collator - }; -}); - -// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case. -const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { - const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); - return { - collator: collator - }; -}); - -export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { - const a = one || ''; - const b = other || ''; - const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b); - - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) { - return a < b ? -1 : 1; - } - - return result; -} - -/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ -export function compareFileNamesNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); - const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; - - // Check for name differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; - } - - // Disambiguate the extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; -} - -const FileNameMatch = /^(.*?)(\.([^.]*))?$/; - -export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { - if (!caseSensitive) { - one = one && one.toLowerCase(); - other = other && other.toLowerCase(); - } - - const [oneName, oneExtension] = extractNameAndExtension(one); - const [otherName, otherExtension] = extractNameAndExtension(other); - - if (oneName !== otherName) { - return oneName < otherName ? -1 : 1; - } - - if (oneExtension === otherExtension) { - return 0; - } - - return oneExtension < otherExtension ? -1 : 1; -} - -export function compareFileExtensions(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one); - const [otherName, otherExtension] = extractNameAndExtension(other); - - let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension); - - if (result === 0) { - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) { - return oneExtension < otherExtension ? -1 : 1; - } - - // Extensions are equal, compare filenames - result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName); - - if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) { - return oneName < otherName ? -1 : 1; - } - } - - return result; -} - -/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ -export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); - const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; - - // Check for extension differences, ignoring differences in case and comparing numbers numerically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; - } - - // Compare names. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Disambiguate extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; -} - -/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */ -function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] { - const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); - - let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; - - // if the dotfilesAsNames option is selected, treat an empty filename with an extension, - // or a filename that starts with a dot, as a dotfile name - if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) { - result = [result[0] + '.' + result[1], '']; - } - - return result; -} - -function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { - // Check for differences - let result = collator.compare(one, other); - if (result !== 0) { - return result; - } - - // In a numeric comparison, `foo1` and `foo01` will compare as equivalent. - // Disambiguate by sorting the shorter string first. - if (one.length !== other.length) { - return one.length < other.length ? -1 : 1; - } - - return 0; -} - -function comparePathComponents(one: string, other: string, caseSensitive = false): number { - if (!caseSensitive) { - one = one && one.toLowerCase(); - other = other && other.toLowerCase(); - } - - if (one === other) { - return 0; - } - - return one < other ? -1 : 1; -} - -export function comparePaths(one: string, other: string, caseSensitive = false): number { - const oneParts = one.split(sep); - const otherParts = other.split(sep); - - const lastOne = oneParts.length - 1; - const lastOther = otherParts.length - 1; - let endOne: boolean, endOther: boolean; - - for (let i = 0; ; i++) { - endOne = lastOne === i; - endOther = lastOther === i; - - if (endOne && endOther) { - return compareFileNames(oneParts[i], otherParts[i], caseSensitive); - } else if (endOne) { - return -1; - } else if (endOther) { - return 1; - } - - const result = comparePathComponents(oneParts[i], otherParts[i], caseSensitive); - - if (result !== 0) { - return result; - } - } -} - -export function compareAnything(one: string, other: string, lookFor: string): number { - const elementAName = one.toLowerCase(); - const elementBName = other.toLowerCase(); - - // Sort prefix matches over non prefix matches - const prefixCompare = compareByPrefix(one, other, lookFor); - if (prefixCompare) { - return prefixCompare; - } - - // Sort suffix matches over non suffix matches - const elementASuffixMatch = elementAName.endsWith(lookFor); - const elementBSuffixMatch = elementBName.endsWith(lookFor); - if (elementASuffixMatch !== elementBSuffixMatch) { - return elementASuffixMatch ? -1 : 1; - } - - // Understand file names - const r = compareFileNames(elementAName, elementBName); - if (r !== 0) { - return r; - } - - // Compare by name - return elementAName.localeCompare(elementBName); -} - -export function compareByPrefix(one: string, other: string, lookFor: string): number { - const elementAName = one.toLowerCase(); - const elementBName = other.toLowerCase(); - - // Sort prefix matches over non prefix matches - const elementAPrefixMatch = elementAName.startsWith(lookFor); - const elementBPrefixMatch = elementBName.startsWith(lookFor); - if (elementAPrefixMatch !== elementBPrefixMatch) { - return elementAPrefixMatch ? -1 : 1; - } - - // Same prefix: Sort shorter matches to the top to have those on top that match more precisely - // tslint:disable-next-line: one-line - else if (elementAPrefixMatch && elementBPrefixMatch) { - if (elementAName.length < elementBName.length) { - return -1; - } - - if (elementAName.length > elementBName.length) { - return 1; - } - } - - return 0; -} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 218b588b..8e61217a 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -121,9 +121,7 @@ export interface BoardsService extends Installable, Searchable; getBoardPackage(options: { id: string }): Promise; getContainerBoardPackage(options: { fqbn: string }): Promise; - // The CLI cannot do fuzzy search. This method provides all boards and we do the fuzzy search (with monaco) on the frontend. - // https://github.com/arduino/arduino-cli/issues/629 - allBoards(options: {}): Promise>; + searchBoards({ query }: { query?: string }): Promise; } export interface Port { diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index a7a8bbf2..87a546cf 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -10,11 +10,11 @@ import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp, PlatformUninstallResp, PlatformUninstallReq } from './cli-protocol/commands/core_pb'; +import { Platform } from './cli-protocol/commands/common_pb'; import { BoardDiscovery } from './board-discovery'; import { CoreClientAware } from './core-client-provider'; -import { BoardDetailsReq, BoardDetailsResp } from './cli-protocol/commands/board_pb'; +import { BoardDetailsReq, BoardDetailsResp, BoardSearchReq } from './cli-protocol/commands/board_pb'; import { ListProgrammersAvailableForUploadReq, ListProgrammersAvailableForUploadResp } from './cli-protocol/commands/upload_pb'; -import { Platform } from './cli-protocol/commands/common_pb'; @injectable() export class BoardsServiceImpl extends CoreClientAware implements BoardsService { @@ -145,10 +145,33 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService return packages.find(({ boards }) => boards.some(({ fqbn }) => fqbn === expectedFqbn)); } - async allBoards(options: {}): Promise> { - const results = await this.search(options); - return results.map(item => item.boards.map(board => ({ ...board, packageName: item.name, packageId: item.id }))) - .reduce((acc, curr) => acc.concat(curr), []); + async searchBoards({ query }: { query?: string }): Promise { + const { instance, client } = await this.coreClient(); + const req = new BoardSearchReq(); + req.setSearchArgs(query || ''); + req.setInstance(instance); + const boards = await new Promise((resolve, reject) => { + client.boardSearch(req, (error, resp) => { + if (error) { + reject(error); + return; + } + const boards: Array = []; + for (const board of resp.getBoardsList()) { + const platform = board.getPlatform(); + if (platform) { + boards.push({ + name: board.getName(), + fqbn: board.getFqbn(), + packageId: platform.getId(), + packageName: platform.getName() + }); + } + } + resolve(boards); + }) + }); + return boards; } async search(options: { query?: string }): Promise {