mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-16 17:16:34 +00:00
Link resolved for lib/boards manager.
Closes #1442 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
019b2d5588
commit
0c49709f26
@ -334,6 +334,7 @@ import { DeleteSketch } from './contributions/delete-sketch';
|
||||
import { UserFields } from './contributions/user-fields';
|
||||
import { UpdateIndexes } from './contributions/update-indexes';
|
||||
import { InterfaceScale } from './contributions/interface-scale';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
const registerArduinoThemes = () => {
|
||||
const themes: MonacoThemeJson[] = [
|
||||
@ -398,6 +399,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@ -464,6 +466,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import type {
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
override async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
BoardSearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution
|
||||
extends AbstractViewContribution<LibraryListWidget>
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
LibrarySearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): LibrarySearch | undefined {
|
||||
return LibrarySearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
OpenerOptions,
|
||||
OpenHandler,
|
||||
} from '@theia/core/lib/browser/opener-service';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<
|
||||
@ -11,14 +17,49 @@ export abstract class ListWidgetFrontendContribution<
|
||||
S extends Searchable.Options
|
||||
>
|
||||
extends AbstractViewContribution<ListWidget<T, S>>
|
||||
implements FrontendApplicationContribution
|
||||
implements FrontendApplicationContribution, OpenHandler
|
||||
{
|
||||
readonly id: string = `http-opener-${this.viewId}`;
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
// TS requires at least one method from `FrontendApplicationContribution`.
|
||||
// Expected to be empty.
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override registerMenus(_: MenuModelRegistry): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
canHandle(uri: URI, _?: OpenerOptions): number {
|
||||
// `500` is the default HTTP opener in Theia. IDE2 has higher priority.
|
||||
// https://github.com/eclipse-theia/theia/blob/b75b6144b0ffea06a549294903c374fa642135e4/packages/core/src/browser/http-open-handler.ts#L39
|
||||
return this.canParse(uri) ? 501 : 0;
|
||||
}
|
||||
|
||||
async open(
|
||||
uri: URI,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_?: OpenerOptions | undefined
|
||||
): Promise<void> {
|
||||
const searchOptions = this.parse(uri);
|
||||
if (!searchOptions) {
|
||||
console.warn(
|
||||
`Failed to parse URI into a search options. URI: ${uri.toString()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const widget = await this.openView({
|
||||
activate: true,
|
||||
reveal: true,
|
||||
});
|
||||
if (!widget) {
|
||||
console.warn(`Failed to open view for URI: ${uri.toString()}`);
|
||||
return;
|
||||
}
|
||||
widget.refresh(searchOptions);
|
||||
}
|
||||
|
||||
protected abstract canParse(uri: URI): boolean;
|
||||
protected abstract parse(uri: URI): S | undefined;
|
||||
}
|
||||
|
@ -3,7 +3,14 @@ import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { All, Contributed, Partner, Type, Updatable } from '../nls';
|
||||
import {
|
||||
All,
|
||||
Contributed,
|
||||
Partner,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
|
||||
export namespace AvailablePorts {
|
||||
@ -151,6 +158,7 @@ export interface BoardSearch extends Searchable.Options {
|
||||
readonly type?: BoardSearch.Type;
|
||||
}
|
||||
export namespace BoardSearch {
|
||||
export const Default: BoardSearch = { type: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@ -161,6 +169,11 @@ export namespace BoardSearch {
|
||||
'Arduino@Heart',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@ -177,8 +190,41 @@ export namespace BoardSearch {
|
||||
keyof Omit<BoardSearch, 'query'>,
|
||||
string
|
||||
> = {
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'boardsmanager';
|
||||
export function parse(uri: URI): BoardSearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
if (segments.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
let searchOptions: BoardSearch | undefined = undefined;
|
||||
const [type] = segments;
|
||||
if (!type) {
|
||||
searchOptions = BoardSearch.Default;
|
||||
} else if (BoardSearch.Type.is(type)) {
|
||||
searchOptions = { type };
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
|
@ -8,9 +8,10 @@ import {
|
||||
Partner,
|
||||
Recommended,
|
||||
Retired,
|
||||
Type,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
@ -55,6 +56,7 @@ export interface LibrarySearch extends Searchable.Options {
|
||||
readonly topic?: LibrarySearch.Topic;
|
||||
}
|
||||
export namespace LibrarySearch {
|
||||
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@ -66,6 +68,11 @@ export namespace LibrarySearch {
|
||||
'Retired',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@ -90,6 +97,11 @@ export namespace LibrarySearch {
|
||||
'Uncategorized',
|
||||
] as const;
|
||||
export type Topic = typeof TopicLiterals[number];
|
||||
export namespace Topic {
|
||||
export function is(arg: unknown): arg is Topic {
|
||||
return typeof arg === 'string' && TopicLiterals.includes(arg as Topic);
|
||||
}
|
||||
}
|
||||
export const TopicLabels: Record<Topic, string> = {
|
||||
All: All,
|
||||
Communication: nls.localize(
|
||||
@ -126,8 +138,60 @@ export namespace LibrarySearch {
|
||||
string
|
||||
> = {
|
||||
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'librarymanager';
|
||||
export function parse(uri: URI): LibrarySearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
// Special magic handling for `Signal Input/Output`.
|
||||
// TODO: IDE2 deserves a better lib/boards URL spec.
|
||||
// https://github.com/arduino/arduino-ide/issues/1442#issuecomment-1252136377
|
||||
if (segments.length === 3) {
|
||||
const [type, topicHead, topicTail] = segments;
|
||||
const maybeTopic = `${topicHead}/${topicTail}`;
|
||||
if (
|
||||
LibrarySearch.Topic.is(maybeTopic) &&
|
||||
maybeTopic === 'Signal Input/Output' &&
|
||||
LibrarySearch.Type.is(type)
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
topic: maybeTopic,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
}
|
||||
let searchOptions: LibrarySearch | undefined = undefined;
|
||||
const [type, topic] = segments;
|
||||
if (!type && !topic) {
|
||||
searchOptions = LibrarySearch.Default;
|
||||
} else if (LibrarySearch.Type.is(type)) {
|
||||
if (!topic) {
|
||||
searchOptions = { ...LibrarySearch.Default, type };
|
||||
} else if (LibrarySearch.Topic.is(topic)) {
|
||||
searchOptions = { type, topic };
|
||||
}
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace LibraryService {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export interface Searchable<T, O extends Searchable.Options> {
|
||||
search(options: O): Promise<T[]>;
|
||||
}
|
||||
@ -8,4 +10,24 @@ export namespace Searchable {
|
||||
*/
|
||||
readonly query?: string;
|
||||
}
|
||||
export namespace UriParser {
|
||||
/**
|
||||
* Parses the `URI#fragment` into a query term.
|
||||
*/
|
||||
export function parseQuery(uri: URI): { query: string } {
|
||||
return { query: uri.fragment };
|
||||
}
|
||||
/**
|
||||
* Splits the `URI#path#toString` on the `/` POSIX separator into decoded segments. The first, empty segment representing the root is omitted.
|
||||
* Examples:
|
||||
* - `/` -> `['']`
|
||||
* - `/All` -> `['All']`
|
||||
* - `/All/Device%20Control` -> `['All', 'Device Control']`
|
||||
* - `/All/Display` -> `['All', 'Display']`
|
||||
* - `/Updatable/Signal%20Input%2FOutput` -> `['Updatable', 'Signal Input', 'Output']` (**caveat**!)
|
||||
*/
|
||||
export function normalizedSegmentsOf(uri: URI): string[] {
|
||||
return uri.path.toString().split('/').slice(1).map(decodeURIComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
136
arduino-ide-extension/src/test/common/searchable.test.ts
Normal file
136
arduino-ide-extension/src/test/common/searchable.test.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { expect } from 'chai';
|
||||
import { BoardSearch, LibrarySearch, Searchable } from '../../common/protocol';
|
||||
|
||||
interface Expectation<S extends Searchable.Options> {
|
||||
readonly uri: string;
|
||||
readonly expected: S | undefined | string;
|
||||
}
|
||||
|
||||
describe('searchable', () => {
|
||||
describe('parse', () => {
|
||||
describe(BoardSearch.UriParser.authority, () => {
|
||||
(
|
||||
[
|
||||
{
|
||||
uri: 'http://boardsmanager#SAMD',
|
||||
expected: { query: 'SAMD', type: 'All' },
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/Arduino%40Heart#littleBits',
|
||||
expected: { query: 'littleBits', type: 'Arduino@Heart' },
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/too/many/segments#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/random#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'https://boardsmanager/#invalidScheme',
|
||||
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://boardsmanager/#invalidScheme.`,
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/#invalidAuthority',
|
||||
expected: `Invalid 'authority'. Expected: 'boardsmanager'. URI was: http://librarymanager/#invalidAuthority.`,
|
||||
},
|
||||
] as Expectation<BoardSearch>[]
|
||||
).map((expectation) => toIt(expectation, BoardSearch.UriParser.parse));
|
||||
});
|
||||
describe(LibrarySearch.UriParser.authority, () => {
|
||||
(
|
||||
[
|
||||
{
|
||||
uri: 'http://librarymanager#WiFiNINA',
|
||||
expected: { query: 'WiFiNINA', type: 'All', topic: 'All' },
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Device%20Control#Servo',
|
||||
expected: {
|
||||
query: 'Servo',
|
||||
type: 'All',
|
||||
topic: 'Device Control',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Display#SparkFun',
|
||||
expected: {
|
||||
query: 'SparkFun',
|
||||
type: 'All',
|
||||
topic: 'Display',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/Updatable/Display#SparkFun',
|
||||
expected: {
|
||||
query: 'SparkFun',
|
||||
type: 'Updatable',
|
||||
topic: 'Display',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Signal%20Input%2FOutput#debouncer',
|
||||
expected: {
|
||||
query: 'debouncer',
|
||||
type: 'All',
|
||||
topic: 'Signal Input/Output',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/too/many/segments#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/absent/invalid#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'https://librarymanager/#invalidScheme',
|
||||
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://librarymanager/#invalidScheme.`,
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/#invalidAuthority',
|
||||
expected: `Invalid 'authority'. Expected: 'librarymanager'. URI was: http://boardsmanager/#invalidAuthority.`,
|
||||
},
|
||||
] as Expectation<LibrarySearch>[]
|
||||
).map((expectation) => toIt(expectation, LibrarySearch.UriParser.parse));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function toIt<S extends Searchable.Options>(
|
||||
{ uri, expected }: Expectation<S>,
|
||||
run: (uri: URI) => Searchable.Options | undefined
|
||||
): Mocha.Test {
|
||||
return it(`should ${
|
||||
typeof expected === 'string'
|
||||
? `fail to parse '${uri}'`
|
||||
: !expected
|
||||
? `not parse '${uri}'`
|
||||
: `parse '${uri}' to ${JSON.stringify(expected)}`
|
||||
}`, () => {
|
||||
if (typeof expected === 'string') {
|
||||
try {
|
||||
run(new URI(uri));
|
||||
expect.fail(
|
||||
`Expected an error with message '${expected}' when parsing URI: ${uri}.`
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(Error);
|
||||
expect(err.message).to.be.equal(expected);
|
||||
}
|
||||
} else {
|
||||
const actual = run(new URI(uri));
|
||||
if (!expected) {
|
||||
expect(actual).to.be.undefined;
|
||||
} else {
|
||||
expect(actual).to.be.deep.equal(
|
||||
expected,
|
||||
`Was: ${JSON.stringify(actual)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user