Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 461d12f

Browse files
author
Akos Kitta
committedSep 26, 2022
Link resolved for lib/boards manager.
Closes #1442 Signed-off-by: Akos Kitta <[email protected]>
1 parent 8783952 commit 461d12f

File tree

8 files changed

+368
-27
lines changed

8 files changed

+368
-27
lines changed
 

‎arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ import { OutputEditorFactory } from './theia/output/output-editor-factory';
338338
import { StartupTaskProvider } from '../electron-common/startup-task';
339339
import { DeleteSketch } from './contributions/delete-sketch';
340340
import { UserFields } from './contributions/user-fields';
341+
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
341342

342343
const registerArduinoThemes = () => {
343344
const themes: MonacoThemeJson[] = [
@@ -402,6 +403,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
402403
bind(FrontendApplicationContribution).toService(
403404
LibraryListWidgetFrontendContribution
404405
);
406+
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
405407

406408
// Sketch list service
407409
bind(SketchesService)
@@ -468,6 +470,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
468470
bind(FrontendApplicationContribution).toService(
469471
BoardsListWidgetFrontendContribution
470472
);
473+
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
471474

472475
// Board select dialog
473476
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();

‎arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { injectable } from '@theia/core/shared/inversify';
2-
import { BoardsListWidget } from './boards-list-widget';
3-
import type {
2+
import {
43
BoardSearch,
54
BoardsPackage,
65
} from '../../common/protocol/boards-service';
6+
import { URI } from '../contributions/contribution';
77
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
8+
import { BoardsListWidget } from './boards-list-widget';
89

910
@injectable()
1011
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
@@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
2425
});
2526
}
2627

27-
override async initializeLayout(): Promise<void> {
28-
this.openView();
28+
protected canParse(uri: URI): boolean {
29+
try {
30+
BoardSearch.UriParser.parse(uri);
31+
return true;
32+
} catch {
33+
return false;
34+
}
35+
}
36+
37+
protected parse(uri: URI): BoardSearch | undefined {
38+
return BoardSearch.UriParser.parse(uri);
2939
}
3040
}
Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import { nls } from '@theia/core/lib/common';
2+
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
13
import { injectable } from '@theia/core/shared/inversify';
2-
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
3-
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
4-
import { MenuModelRegistry } from '@theia/core';
5-
import { LibraryListWidget } from './library-list-widget';
4+
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
5+
import { URI } from '../contributions/contribution';
66
import { ArduinoMenus } from '../menu/arduino-menus';
7-
import { nls } from '@theia/core/lib/common';
7+
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
8+
import { LibraryListWidget } from './library-list-widget';
89

910
@injectable()
10-
export class LibraryListWidgetFrontendContribution
11-
extends AbstractViewContribution<LibraryListWidget>
12-
implements FrontendApplicationContribution
13-
{
11+
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
12+
LibraryPackage,
13+
LibrarySearch
14+
> {
1415
constructor() {
1516
super({
1617
widgetId: LibraryListWidget.WIDGET_ID,
@@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
2425
});
2526
}
2627

27-
async initializeLayout(): Promise<void> {
28-
this.openView();
29-
}
30-
3128
override registerMenus(menus: MenuModelRegistry): void {
3229
if (this.toggleCommand) {
3330
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
@@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
4037
});
4138
}
4239
}
40+
41+
protected canParse(uri: URI): boolean {
42+
try {
43+
LibrarySearch.UriParser.parse(uri);
44+
return true;
45+
} catch {
46+
return false;
47+
}
48+
}
49+
50+
protected parse(uri: URI): LibrarySearch | undefined {
51+
return LibrarySearch.UriParser.parse(uri);
52+
}
4353
}
Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,65 @@
1-
import { injectable } from '@theia/core/shared/inversify';
21
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
2+
import {
3+
OpenerOptions,
4+
OpenHandler,
5+
} from '@theia/core/lib/browser/opener-service';
36
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
7+
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
8+
import { URI } from '@theia/core/lib/common/uri';
9+
import { injectable } from '@theia/core/shared/inversify';
10+
import { Searchable } from '../../../common/protocol';
411
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
512
import { ListWidget } from './list-widget';
6-
import { Searchable } from '../../../common/protocol';
713

814
@injectable()
915
export abstract class ListWidgetFrontendContribution<
1016
T extends ArduinoComponent,
1117
S extends Searchable.Options
1218
>
1319
extends AbstractViewContribution<ListWidget<T, S>>
14-
implements FrontendApplicationContribution
20+
implements FrontendApplicationContribution, OpenHandler
1521
{
22+
readonly id: string = `http-opener-${this.viewId}`;
23+
1624
async initializeLayout(): Promise<void> {
17-
// TS requires at least one method from `FrontendApplicationContribution`.
18-
// Expected to be empty.
25+
this.openView();
1926
}
2027

21-
override registerMenus(): void {
28+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29+
override registerMenus(_: MenuModelRegistry): void {
2230
// NOOP
2331
}
32+
33+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
34+
canHandle(uri: URI, _?: OpenerOptions): number {
35+
// `500` is the default HTTP opener in Theia. IDE2 has higher priority.
36+
// https://github.com/eclipse-theia/theia/blob/b75b6144b0ffea06a549294903c374fa642135e4/packages/core/src/browser/http-open-handler.ts#L39
37+
return this.canParse(uri) ? 501 : 0;
38+
}
39+
40+
async open(
41+
uri: URI,
42+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
43+
_?: OpenerOptions | undefined
44+
): Promise<void> {
45+
const searchOptions = this.parse(uri);
46+
if (!searchOptions) {
47+
console.warn(
48+
`Failed to parse URI into a search options. URI: ${uri.toString()}`
49+
);
50+
return;
51+
}
52+
const widget = await this.openView({
53+
activate: true,
54+
reveal: true,
55+
});
56+
if (!widget) {
57+
console.warn(`Failed to open view for URI: ${uri.toString()}`);
58+
return;
59+
}
60+
widget.refresh(searchOptions);
61+
}
62+
63+
protected abstract canParse(uri: URI): boolean;
64+
protected abstract parse(uri: URI): S | undefined;
2465
}

‎arduino-ide-extension/src/common/protocol/boards-service.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { Searchable } from './searchable';
33
import { Installable } from './installable';
44
import { ArduinoComponent } from './arduino-component';
55
import { nls } from '@theia/core/lib/common/nls';
6-
import { All, Contributed, Partner, Type, Updatable } from '../nls';
6+
import {
7+
All,
8+
Contributed,
9+
Partner,
10+
Type as TypeLabel,
11+
Updatable,
12+
} from '../nls';
13+
import URI from '@theia/core/lib/common/uri';
714

815
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
916
export namespace AvailablePorts {
@@ -151,6 +158,7 @@ export interface BoardSearch extends Searchable.Options {
151158
readonly type?: BoardSearch.Type;
152159
}
153160
export namespace BoardSearch {
161+
export const Default: BoardSearch = { type: 'All' };
154162
export const TypeLiterals = [
155163
'All',
156164
'Updatable',
@@ -161,6 +169,11 @@ export namespace BoardSearch {
161169
'Arduino@Heart',
162170
] as const;
163171
export type Type = typeof TypeLiterals[number];
172+
export namespace Type {
173+
export function is(arg: unknown): arg is Type {
174+
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
175+
}
176+
}
164177
export const TypeLabels: Record<Type, string> = {
165178
All: All,
166179
Updatable: Updatable,
@@ -177,8 +190,41 @@ export namespace BoardSearch {
177190
keyof Omit<BoardSearch, 'query'>,
178191
string
179192
> = {
180-
type: Type,
193+
type: TypeLabel,
181194
};
195+
export namespace UriParser {
196+
export const authority = 'boardsmanager';
197+
export function parse(uri: URI): BoardSearch | undefined {
198+
if (uri.scheme !== 'http') {
199+
throw new Error(
200+
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
201+
);
202+
}
203+
if (uri.authority !== authority) {
204+
throw new Error(
205+
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
206+
);
207+
}
208+
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
209+
if (segments.length !== 1) {
210+
return undefined;
211+
}
212+
let searchOptions: BoardSearch | undefined = undefined;
213+
const [type] = segments;
214+
if (!type) {
215+
searchOptions = BoardSearch.Default;
216+
} else if (BoardSearch.Type.is(type)) {
217+
searchOptions = { type };
218+
}
219+
if (searchOptions) {
220+
return {
221+
...searchOptions,
222+
...Searchable.UriParser.parseQuery(uri),
223+
};
224+
}
225+
return undefined;
226+
}
227+
}
182228
}
183229

184230
export interface Port {

‎arduino-ide-extension/src/common/protocol/library-service.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
Partner,
99
Recommended,
1010
Retired,
11-
Type,
11+
Type as TypeLabel,
1212
Updatable,
1313
} from '../nls';
14+
import URI from '@theia/core/lib/common/uri';
1415

1516
export const LibraryServicePath = '/services/library-service';
1617
export const LibraryService = Symbol('LibraryService');
@@ -55,6 +56,7 @@ export interface LibrarySearch extends Searchable.Options {
5556
readonly topic?: LibrarySearch.Topic;
5657
}
5758
export namespace LibrarySearch {
59+
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
5860
export const TypeLiterals = [
5961
'All',
6062
'Updatable',
@@ -66,6 +68,11 @@ export namespace LibrarySearch {
6668
'Retired',
6769
] as const;
6870
export type Type = typeof TypeLiterals[number];
71+
export namespace Type {
72+
export function is(arg: unknown): arg is Type {
73+
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
74+
}
75+
}
6976
export const TypeLabels: Record<Type, string> = {
7077
All: All,
7178
Updatable: Updatable,
@@ -90,6 +97,11 @@ export namespace LibrarySearch {
9097
'Uncategorized',
9198
] as const;
9299
export type Topic = typeof TopicLiterals[number];
100+
export namespace Topic {
101+
export function is(arg: unknown): arg is Topic {
102+
return typeof arg === 'string' && TopicLiterals.includes(arg as Topic);
103+
}
104+
}
93105
export const TopicLabels: Record<Topic, string> = {
94106
All: All,
95107
Communication: nls.localize(
@@ -126,8 +138,60 @@ export namespace LibrarySearch {
126138
string
127139
> = {
128140
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
129-
type: Type,
141+
type: TypeLabel,
130142
};
143+
export namespace UriParser {
144+
export const authority = 'librarymanager';
145+
export function parse(uri: URI): LibrarySearch | undefined {
146+
if (uri.scheme !== 'http') {
147+
throw new Error(
148+
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
149+
);
150+
}
151+
if (uri.authority !== authority) {
152+
throw new Error(
153+
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
154+
);
155+
}
156+
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
157+
// Special magic handling for `Signal Input/Output`.
158+
// TODO: IDE2 deserves a better lib/boards URL spec.
159+
// https://github.com/arduino/arduino-ide/issues/1442#issuecomment-1252136377
160+
if (segments.length === 3) {
161+
const [type, topicHead, topicTail] = segments;
162+
const maybeTopic = `${topicHead}/${topicTail}`;
163+
if (
164+
LibrarySearch.Topic.is(maybeTopic) &&
165+
maybeTopic === 'Signal Input/Output' &&
166+
LibrarySearch.Type.is(type)
167+
) {
168+
return {
169+
type,
170+
topic: maybeTopic,
171+
...Searchable.UriParser.parseQuery(uri),
172+
};
173+
}
174+
}
175+
let searchOptions: LibrarySearch | undefined = undefined;
176+
const [type, topic] = segments;
177+
if (!type && !topic) {
178+
searchOptions = LibrarySearch.Default;
179+
} else if (LibrarySearch.Type.is(type)) {
180+
if (!topic) {
181+
searchOptions = { ...LibrarySearch.Default, type };
182+
} else if (LibrarySearch.Topic.is(topic)) {
183+
searchOptions = { type, topic };
184+
}
185+
}
186+
if (searchOptions) {
187+
return {
188+
...searchOptions,
189+
...Searchable.UriParser.parseQuery(uri),
190+
};
191+
}
192+
return undefined;
193+
}
194+
}
131195
}
132196

133197
export namespace LibraryService {

‎arduino-ide-extension/src/common/protocol/searchable.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import URI from '@theia/core/lib/common/uri';
2+
13
export interface Searchable<T, O extends Searchable.Options> {
24
search(options: O): Promise<T[]>;
35
}
@@ -8,4 +10,25 @@ export namespace Searchable {
810
*/
911
readonly query?: string;
1012
}
13+
export namespace UriParser {
14+
/**
15+
* Parses the `URI#fragment` into a query term. Replaces all underscore (`_`) with spaces (` `) to match the IDE 1.x search behavior.
16+
* See: https://github.com/arduino/arduino-cli/issues/1895.
17+
*/
18+
export function parseQuery(uri: URI): { query: string } {
19+
return { query: uri.fragment.replace(/_/g, ' ') };
20+
}
21+
/**
22+
* Splits the `URI#path#toString` on the `/` POSIX separator into decoded segments. The first, empty segment representing the root is omitted.
23+
* Examples:
24+
* - `/` -> `['']`
25+
* - `/All` -> `['All']`
26+
* - `/All/Device%20Control` -> `['All', 'Device Control']`
27+
* - `/All/Display` -> `['All', 'Display']`
28+
* - `/Updatable/Signal%20Input%2FOutput` -> `['Updatable', 'Signal Input', 'Output']` (**caveat**!)
29+
*/
30+
export function normalizedSegmentsOf(uri: URI): string[] {
31+
return uri.path.toString().split('/').slice(1).map(decodeURIComponent);
32+
}
33+
}
1134
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import URI from '@theia/core/lib/common/uri';
2+
import { expect } from 'chai';
3+
import { BoardSearch, LibrarySearch, Searchable } from '../../common/protocol';
4+
5+
interface Expectation<S extends Searchable.Options> {
6+
readonly uri: string;
7+
readonly expected: S | undefined | string;
8+
}
9+
10+
describe('searchable', () => {
11+
describe('parse', () => {
12+
describe(BoardSearch.UriParser.authority, () => {
13+
(
14+
[
15+
{
16+
uri: 'http://boardsmanager#SAMD',
17+
expected: { query: 'SAMD', type: 'All' },
18+
},
19+
{
20+
uri: 'http://boardsmanager/Arduino%40Heart#littleBits',
21+
expected: { query: 'littleBits', type: 'Arduino@Heart' },
22+
},
23+
{
24+
uri: 'http://boardsmanager/too/many/segments#invalidPath',
25+
expected: undefined,
26+
},
27+
{
28+
uri: 'http://boardsmanager/random#invalidPath',
29+
expected: undefined,
30+
},
31+
{
32+
uri: 'https://boardsmanager/#invalidScheme',
33+
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://boardsmanager/#invalidScheme.`,
34+
},
35+
{
36+
uri: 'http://librarymanager/#invalidAuthority',
37+
expected: `Invalid 'authority'. Expected: 'boardsmanager'. URI was: http://librarymanager/#invalidAuthority.`,
38+
},
39+
] as Expectation<BoardSearch>[]
40+
).map((expectation) => toIt(expectation, BoardSearch.UriParser.parse));
41+
});
42+
describe(LibrarySearch.UriParser.authority, () => {
43+
(
44+
[
45+
{
46+
uri: 'http://librarymanager#WiFiNINA',
47+
expected: { query: 'WiFiNINA', type: 'All', topic: 'All' },
48+
},
49+
{
50+
uri: 'http://librarymanager/All/Device%20Control#Servo',
51+
expected: {
52+
query: 'Servo',
53+
type: 'All',
54+
topic: 'Device Control',
55+
},
56+
},
57+
{
58+
uri: 'http://librarymanager/All/Display#SparkFun',
59+
expected: {
60+
query: 'SparkFun',
61+
type: 'All',
62+
topic: 'Display',
63+
},
64+
},
65+
{
66+
uri: 'http://librarymanager/Updatable/Display#SparkFun',
67+
expected: {
68+
query: 'SparkFun',
69+
type: 'Updatable',
70+
topic: 'Display',
71+
},
72+
},
73+
{
74+
uri: 'http://librarymanager/All/Signal%20Input%2FOutput#debouncer',
75+
expected: {
76+
query: 'debouncer',
77+
type: 'All',
78+
topic: 'Signal Input/Output',
79+
},
80+
},
81+
{
82+
uri: 'http://librarymanager/All#SparkFun_u-blox_GNSS',
83+
expected: {
84+
query: 'SparkFun u-blox GNSS',
85+
type: 'All',
86+
topic: 'All',
87+
},
88+
},
89+
{
90+
uri: 'http://librarymanager/too/many/segments#invalidPath',
91+
expected: undefined,
92+
},
93+
{
94+
uri: 'http://librarymanager/absent/invalid#invalidPath',
95+
expected: undefined,
96+
},
97+
{
98+
uri: 'https://librarymanager/#invalidScheme',
99+
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://librarymanager/#invalidScheme.`,
100+
},
101+
{
102+
uri: 'http://boardsmanager/#invalidAuthority',
103+
expected: `Invalid 'authority'. Expected: 'librarymanager'. URI was: http://boardsmanager/#invalidAuthority.`,
104+
},
105+
] as Expectation<LibrarySearch>[]
106+
).map((expectation) => toIt(expectation, LibrarySearch.UriParser.parse));
107+
});
108+
});
109+
});
110+
111+
function toIt<S extends Searchable.Options>(
112+
{ uri, expected }: Expectation<S>,
113+
run: (uri: URI) => Searchable.Options | undefined
114+
): Mocha.Test {
115+
return it(`should ${
116+
typeof expected === 'string'
117+
? `fail to parse '${uri}'`
118+
: !expected
119+
? `not parse '${uri}'`
120+
: `parse '${uri}' to ${JSON.stringify(expected)}`
121+
}`, () => {
122+
if (typeof expected === 'string') {
123+
try {
124+
run(new URI(uri));
125+
expect.fail(
126+
`Expected an error with message '${expected}' when parsing URI: ${uri}.`
127+
);
128+
} catch (err) {
129+
expect(err).to.be.instanceOf(Error);
130+
expect(err.message).to.be.equal(expected);
131+
}
132+
} else {
133+
const actual = run(new URI(uri));
134+
if (!expected) {
135+
expect(actual).to.be.undefined;
136+
} else {
137+
expect(actual).to.be.deep.equal(
138+
expected,
139+
`Was: ${JSON.stringify(actual)}`
140+
);
141+
}
142+
}
143+
});
144+
}

0 commit comments

Comments
 (0)
Please sign in to comment.