mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 03:46:33 +00:00
ATL-974: Use board search
command from the CLI
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
e90fa27ebf
commit
15b0564212
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Array<BoardWithPackage>> {
|
||||
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<BoardWithPackage[]> {
|
||||
const boards = await this.boardsService.searchBoards({ query });
|
||||
return boards;
|
||||
}
|
||||
|
||||
get boardsConfig(): BoardsConfig.Config {
|
||||
|
@ -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>(ILogger).child('boards-quick-open'))
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('boards-quick-open');
|
||||
});
|
@ -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<QuickOpenItemOptions>[], 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<QuickOpenItemOptions>[] = [];
|
||||
|
||||
// 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<void> {
|
||||
// `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<QuickOpenItemOptions> {
|
||||
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<QuickOpenGroupItemOptions>({ ...options, ...group });
|
||||
} else {
|
||||
return new QuickOpenItem<QuickOpenItemOptions>(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<void> {
|
||||
const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare);
|
||||
const toItem = (port: Port) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
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<void> {
|
||||
const toItem = (value: ConfigValue) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
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;
|
||||
}
|
@ -210,7 +210,7 @@ PID: ${PID}`;
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.allBoards({});
|
||||
const allBoards = await this.boardsService.searchBoards({});
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
|
||||
|
@ -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<T> {
|
||||
|
||||
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<string> : ([] as Array<string>);
|
||||
|
||||
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;
|
||||
}
|
@ -121,9 +121,7 @@ export interface BoardsService extends Installable<BoardsPackage>, Searchable<Bo
|
||||
getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined>;
|
||||
getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined>;
|
||||
getContainerBoardPackage(options: { fqbn: string }): Promise<BoardsPackage | undefined>;
|
||||
// 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<Array<BoardWithPackage>>;
|
||||
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
|
@ -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<Array<BoardWithPackage>> {
|
||||
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<BoardWithPackage[]> {
|
||||
const { instance, client } = await this.coreClient();
|
||||
const req = new BoardSearchReq();
|
||||
req.setSearchArgs(query || '');
|
||||
req.setInstance(instance);
|
||||
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
|
||||
client.boardSearch(req, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const boards: Array<BoardWithPackage> = [];
|
||||
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<BoardsPackage[]> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user