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 c8388a7

Browse files
author
Akos Kitta
committedSep 27, 2022
Refresh menus when opening example/recent fails.
Closes #53 Signed-off-by: Akos Kitta <[email protected]>
1 parent 96cf09d commit c8388a7

13 files changed

+295
-344
lines changed
 

‎arduino-ide-extension/src/browser/contributions/examples.ts

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,33 @@ import {
2121
MenuModelRegistry,
2222
} from './contribution';
2323
import { NotificationCenter } from '../notification-center';
24-
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
24+
import {
25+
Board,
26+
SketchRef,
27+
SketchContainer,
28+
SketchesError,
29+
Sketch,
30+
CoreService,
31+
} from '../../common/protocol';
2532
import { nls } from '@theia/core/lib/common';
2633

2734
@injectable()
2835
export abstract class Examples extends SketchContribution {
2936
@inject(CommandRegistry)
30-
protected readonly commandRegistry: CommandRegistry;
37+
private readonly commandRegistry: CommandRegistry;
3138

3239
@inject(MenuModelRegistry)
33-
protected readonly menuRegistry: MenuModelRegistry;
40+
private readonly menuRegistry: MenuModelRegistry;
3441

3542
@inject(MainMenuManager)
3643
protected readonly menuManager: MainMenuManager;
3744

3845
@inject(ExamplesService)
3946
protected readonly examplesService: ExamplesService;
4047

48+
@inject(CoreService)
49+
protected readonly coreService: CoreService;
50+
4151
@inject(BoardsServiceProvider)
4252
protected readonly boardsServiceClient: BoardsServiceProvider;
4353

@@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
5060
);
5161
}
5262

63+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
5364
protected handleBoardChanged(board: Board | undefined): void {
5465
// NOOP
5566
}
5667

68+
protected abstract update(options?: {
69+
board?: Board | undefined;
70+
forceRefresh?: boolean;
71+
}): void;
72+
5773
override registerMenus(registry: MenuModelRegistry): void {
5874
try {
5975
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
@@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
149165
protected createHandler(uri: string): CommandHandler {
150166
return {
151167
execute: async () => {
152-
const sketch = await this.sketchService.cloneExample(uri);
153-
return this.commandService.executeCommand(
154-
OpenSketch.Commands.OPEN_SKETCH.id,
155-
sketch
156-
);
168+
const sketch = await this.clone(uri);
169+
if (sketch) {
170+
try {
171+
return this.commandService.executeCommand(
172+
OpenSketch.Commands.OPEN_SKETCH.id,
173+
sketch
174+
);
175+
} catch (err) {
176+
if (SketchesError.NotFound.is(err)) {
177+
// Do not toast the error message. It's handled by the `Open Sketch` command.
178+
this.update({
179+
board: this.boardsServiceClient.boardsConfig.selectedBoard,
180+
forceRefresh: true,
181+
});
182+
} else {
183+
throw err;
184+
}
185+
}
186+
}
157187
},
158188
};
159189
}
190+
191+
private async clone(uri: string): Promise<Sketch | undefined> {
192+
try {
193+
const sketch = await this.sketchService.cloneExample(uri);
194+
return sketch;
195+
} catch (err) {
196+
if (SketchesError.NotFound.is(err)) {
197+
this.messageService.error(err.message);
198+
this.update({
199+
board: this.boardsServiceClient.boardsConfig.selectedBoard,
200+
forceRefresh: true,
201+
});
202+
} else {
203+
throw err;
204+
}
205+
}
206+
}
160207
}
161208

162209
@injectable()
163210
export class BuiltInExamples extends Examples {
164211
override async onReady(): Promise<void> {
165-
this.register(); // no `await`
212+
this.update(); // no `await`
166213
}
167214

168-
protected async register(): Promise<void> {
215+
protected override async update(): Promise<void> {
169216
let sketchContainers: SketchContainer[] | undefined;
170217
try {
171218
sketchContainers = await this.examplesService.builtIns();
@@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
197244
@injectable()
198245
export class LibraryExamples extends Examples {
199246
@inject(NotificationCenter)
200-
protected readonly notificationCenter: NotificationCenter;
247+
private readonly notificationCenter: NotificationCenter;
201248

202-
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
249+
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
203250

204251
override onStart(): void {
205-
this.notificationCenter.onLibraryDidInstall(() => this.register());
206-
this.notificationCenter.onLibraryDidUninstall(() => this.register());
252+
this.notificationCenter.onLibraryDidInstall(() => this.update());
253+
this.notificationCenter.onLibraryDidUninstall(() => this.update());
207254
}
208255

209256
override async onReady(): Promise<void> {
210-
this.register(); // no `await`
257+
this.update(); // no `await`
211258
}
212259

213260
protected override handleBoardChanged(board: Board | undefined): void {
214-
this.register(board);
261+
this.update({ board });
215262
}
216263

217-
protected async register(
218-
board: Board | undefined = this.boardsServiceClient.boardsConfig
219-
.selectedBoard
264+
protected override async update(
265+
options: { board?: Board; forceRefresh?: boolean } = {
266+
board: this.boardsServiceClient.boardsConfig.selectedBoard,
267+
}
220268
): Promise<void> {
269+
const { board, forceRefresh } = options;
221270
return this.queue.add(async () => {
222271
this.toDispose.dispose();
272+
if (forceRefresh) {
273+
await this.coreService.refresh();
274+
}
223275
const fqbn = board?.fqbn;
224276
const name = board?.name;
225277
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.

‎arduino-ide-extension/src/browser/contributions/new-sketch.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { nls } from '@theia/core/lib/common';
22
import { injectable } from '@theia/core/shared/inversify';
33
import { ArduinoMenus } from '../menu/arduino-menus';
4-
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
54
import {
65
SketchContribution,
76
URI,
@@ -17,11 +16,6 @@ export class NewSketch extends SketchContribution {
1716
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
1817
execute: () => this.newSketch(),
1918
});
20-
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
21-
isVisible: (widget) =>
22-
ArduinoToolbar.is(widget) && widget.side === 'left',
23-
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
24-
});
2519
}
2620

2721
override registerMenus(registry: MenuModelRegistry): void {
@@ -54,8 +48,5 @@ export namespace NewSketch {
5448
export const NEW_SKETCH: Command = {
5549
id: 'arduino-new-sketch',
5650
};
57-
export const NEW_SKETCH__TOOLBAR: Command = {
58-
id: 'arduino-new-sketch--toolbar',
59-
};
6051
}
6152
}

‎arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
1515
import { OpenSketch } from './open-sketch';
1616
import { NotificationCenter } from '../notification-center';
1717
import { nls } from '@theia/core/lib/common';
18+
import { SketchesError } from '../../common/protocol';
1819

1920
@injectable()
2021
export class OpenRecentSketch extends SketchContribution {
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
3334
@inject(NotificationCenter)
3435
protected readonly notificationCenter: NotificationCenter;
3536

36-
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
37+
protected toDispose = new DisposableCollection();
3738

3839
override onStart(): void {
3940
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
4243
}
4344

4445
override async onReady(): Promise<void> {
46+
this.update();
47+
}
48+
49+
private update(forceUpdate?: boolean): void {
4550
this.sketchService
46-
.recentlyOpenedSketches()
51+
.recentlyOpenedSketches(forceUpdate)
4752
.then((sketches) => this.refreshMenu(sketches));
4853
}
4954

@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
6267

6368
protected register(sketches: Sketch[]): void {
6469
const order = 0;
70+
this.toDispose.dispose();
6571
for (const sketch of sketches) {
6672
const { uri } = sketch;
67-
const toDispose = this.toDisposeBeforeRegister.get(uri);
68-
if (toDispose) {
69-
toDispose.dispose();
70-
}
7173
const command = { id: `arduino-open-recent--${uri}` };
7274
const handler = {
73-
execute: () =>
74-
this.commandRegistry.executeCommand(
75-
OpenSketch.Commands.OPEN_SKETCH.id,
76-
sketch
77-
),
75+
execute: async () => {
76+
try {
77+
await this.commandRegistry.executeCommand(
78+
OpenSketch.Commands.OPEN_SKETCH.id,
79+
sketch
80+
);
81+
} catch (err) {
82+
if (SketchesError.NotFound.is(err)) {
83+
this.update(true);
84+
} else {
85+
throw err;
86+
}
87+
}
88+
},
7889
};
7990
this.commandRegistry.registerCommand(command, handler);
8091
this.menuRegistry.registerMenuAction(
@@ -85,17 +96,16 @@ export class OpenRecentSketch extends SketchContribution {
8596
order: String(order),
8697
}
8798
);
88-
this.toDisposeBeforeRegister.set(
89-
sketch.uri,
99+
this.toDispose.pushAll([
90100
new DisposableCollection(
91101
Disposable.create(() =>
92102
this.commandRegistry.unregisterCommand(command)
93103
),
94104
Disposable.create(() =>
95105
this.menuRegistry.unregisterMenuAction(command)
96106
)
97-
)
98-
);
107+
),
108+
]);
99109
}
100110
}
101111
}
Lines changed: 42 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,44 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
21
import * as remote from '@theia/core/electron-shared/@electron/remote';
3-
import { MaybePromise } from '@theia/core/lib/common/types';
4-
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
5-
import {
6-
Disposable,
7-
DisposableCollection,
8-
} from '@theia/core/lib/common/disposable';
2+
import { nls } from '@theia/core/lib/common/nls';
3+
import { injectable } from '@theia/core/shared/inversify';
4+
import { SketchesError, SketchRef } from '../../common/protocol';
95
import { ArduinoMenus } from '../menu/arduino-menus';
10-
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
116
import {
12-
SketchContribution,
13-
Sketch,
14-
URI,
157
Command,
168
CommandRegistry,
17-
MenuModelRegistry,
189
KeybindingRegistry,
10+
MenuModelRegistry,
11+
Sketch,
12+
SketchContribution,
13+
URI,
1914
} from './contribution';
20-
import { ExamplesService } from '../../common/protocol/examples-service';
21-
import { BuiltInExamples } from './examples';
22-
import { Sketchbook } from './sketchbook';
23-
import { SketchContainer } from '../../common/protocol';
24-
import { nls } from '@theia/core/lib/common';
15+
16+
export type SketchLocation = string | URI | SketchRef;
17+
export namespace SketchLocation {
18+
export function toUri(location: SketchLocation): URI {
19+
if (typeof location === 'string') {
20+
return new URI(location);
21+
} else if (SketchRef.is(location)) {
22+
return toUri(location.uri);
23+
} else {
24+
return location;
25+
}
26+
}
27+
export function is(arg: unknown): arg is SketchLocation {
28+
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
29+
}
30+
}
2531

2632
@injectable()
2733
export class OpenSketch extends SketchContribution {
28-
@inject(MenuModelRegistry)
29-
private readonly menuRegistry: MenuModelRegistry;
30-
31-
@inject(ContextMenuRenderer)
32-
private readonly contextMenuRenderer: ContextMenuRenderer;
33-
34-
@inject(BuiltInExamples)
35-
private readonly builtInExamples: BuiltInExamples;
36-
37-
@inject(ExamplesService)
38-
private readonly examplesService: ExamplesService;
39-
40-
@inject(Sketchbook)
41-
private readonly sketchbook: Sketchbook;
42-
43-
private readonly toDispose = new DisposableCollection();
44-
4534
override registerCommands(registry: CommandRegistry): void {
4635
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
47-
execute: (arg) =>
48-
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
49-
});
50-
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
51-
isVisible: (widget) =>
52-
ArduinoToolbar.is(widget) && widget.side === 'left',
53-
execute: async (_: Widget, target: EventTarget) => {
54-
const container = await this.sketchService.getSketches({
55-
exclude: ['**/hardware/**'],
56-
});
57-
if (SketchContainer.isEmpty(container)) {
58-
this.openSketch();
59-
} else {
60-
this.toDispose.dispose();
61-
if (!(target instanceof HTMLElement)) {
62-
return;
63-
}
64-
const { parentElement } = target;
65-
if (!parentElement) {
66-
return;
67-
}
68-
69-
this.menuRegistry.registerMenuAction(
70-
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
71-
{
72-
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
73-
label: nls.localize(
74-
'vscode/workspaceActions/openFileFolder',
75-
'Open...'
76-
),
77-
}
78-
);
79-
this.toDispose.push(
80-
Disposable.create(() =>
81-
this.menuRegistry.unregisterMenuAction(
82-
OpenSketch.Commands.OPEN_SKETCH
83-
)
84-
)
85-
);
86-
this.sketchbook.registerRecursively(
87-
[...container.children, ...container.sketches],
88-
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
89-
this.toDispose
90-
);
91-
try {
92-
const containers = await this.examplesService.builtIns();
93-
for (const container of containers) {
94-
this.builtInExamples.registerRecursively(
95-
container,
96-
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
97-
this.toDispose
98-
);
99-
}
100-
} catch (e) {
101-
console.error('Error when collecting built-in examples.', e);
102-
}
103-
const options = {
104-
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
105-
anchor: {
106-
x: parentElement.getBoundingClientRect().left,
107-
y:
108-
parentElement.getBoundingClientRect().top +
109-
parentElement.offsetHeight,
110-
},
111-
};
112-
this.contextMenuRenderer.render(options);
36+
execute: async (arg) => {
37+
const toOpen = !SketchLocation.is(arg)
38+
? await this.selectSketch()
39+
: arg;
40+
if (toOpen) {
41+
return this.openSketch(toOpen);
11342
}
11443
},
11544
});
@@ -130,13 +59,20 @@ export class OpenSketch extends SketchContribution {
13059
});
13160
}
13261

133-
private async openSketch(
134-
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
135-
): Promise<void> {
136-
const sketch = await toOpen;
137-
if (sketch) {
138-
this.workspaceService.open(new URI(sketch.uri));
62+
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
63+
if (!toOpen) {
64+
return;
13965
}
66+
const uri = SketchLocation.toUri(toOpen);
67+
try {
68+
await this.sketchService.loadSketch(uri.toString());
69+
} catch (err) {
70+
if (SketchesError.NotFound.is(err)) {
71+
this.messageService.error(err.message);
72+
}
73+
throw err;
74+
}
75+
this.workspaceService.open(uri);
14076
}
14177

14278
private async selectSketch(): Promise<Sketch | undefined> {
@@ -220,8 +156,5 @@ export namespace OpenSketch {
220156
export const OPEN_SKETCH: Command = {
221157
id: 'arduino-open-sketch',
222158
};
223-
export const OPEN_SKETCH__TOOLBAR: Command = {
224-
id: 'arduino-open-sketch--toolbar',
225-
};
226159
}
227160
}

‎arduino-ide-extension/src/browser/contributions/save-sketch.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { injectable } from '@theia/core/shared/inversify';
22
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
33
import { ArduinoMenus } from '../menu/arduino-menus';
4-
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
54
import { SaveAsSketch } from './save-as-sketch';
65
import {
76
SketchContribution,
@@ -19,12 +18,6 @@ export class SaveSketch extends SketchContribution {
1918
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
2019
execute: () => this.saveSketch(),
2120
});
22-
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
23-
isVisible: (widget) =>
24-
ArduinoToolbar.is(widget) && widget.side === 'left',
25-
execute: () =>
26-
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
27-
});
2821
}
2922

3023
override registerMenus(registry: MenuModelRegistry): void {
@@ -68,8 +61,5 @@ export namespace SaveSketch {
6861
export const SAVE_SKETCH: Command = {
6962
id: 'arduino-save-sketch',
7063
};
71-
export const SAVE_SKETCH__TOOLBAR: Command = {
72-
id: 'arduino-save-sketch--toolbar',
73-
};
7464
}
7565
}

‎arduino-ide-extension/src/browser/contributions/sketchbook.ts

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
1+
import { injectable } from '@theia/core/shared/inversify';
22
import { CommandHandler } from '@theia/core/lib/common/command';
3-
import { CommandRegistry, MenuModelRegistry } from './contribution';
3+
import { MenuModelRegistry } from './contribution';
44
import { ArduinoMenus } from '../menu/arduino-menus';
5-
import { MainMenuManager } from '../../common/main-menu-manager';
6-
import { NotificationCenter } from '../notification-center';
75
import { Examples } from './examples';
8-
import {
9-
SketchContainer,
10-
SketchesError,
11-
SketchRef,
12-
} from '../../common/protocol';
6+
import { SketchContainer, SketchesError } from '../../common/protocol';
137
import { OpenSketch } from './open-sketch';
14-
import { nls } from '@theia/core/lib/common';
8+
import { nls } from '@theia/core/lib/common/nls';
159

1610
@injectable()
1711
export class Sketchbook extends Examples {
18-
@inject(CommandRegistry)
19-
protected override readonly commandRegistry: CommandRegistry;
20-
21-
@inject(MenuModelRegistry)
22-
protected override readonly menuRegistry: MenuModelRegistry;
23-
24-
@inject(MainMenuManager)
25-
protected readonly mainMenuManager: MainMenuManager;
26-
27-
@inject(NotificationCenter)
28-
protected readonly notificationCenter: NotificationCenter;
29-
3012
override onStart(): void {
3113
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
3214
}
@@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
3517
this.update();
3618
}
3719

38-
private update() {
20+
protected override update(): void {
3921
this.sketchService.getSketches({}).then((container) => {
4022
this.register(container);
41-
this.mainMenuManager.update();
23+
this.menuManager.update();
4224
});
4325
}
4426

@@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
5032
);
5133
}
5234

53-
protected register(container: SketchContainer): void {
35+
private register(container: SketchContainer): void {
5436
this.toDispose.dispose();
5537
this.registerRecursively(
5638
[...container.children, ...container.sketches],
@@ -62,24 +44,20 @@ export class Sketchbook extends Examples {
6244
protected override createHandler(uri: string): CommandHandler {
6345
return {
6446
execute: async () => {
65-
let sketch: SketchRef | undefined = undefined;
6647
try {
67-
sketch = await this.sketchService.loadSketch(uri);
48+
await this.commandService.executeCommand(
49+
OpenSketch.Commands.OPEN_SKETCH.id,
50+
uri
51+
);
52+
this.sketchService.markAsRecentlyOpened(uri); // no `await`.
6853
} catch (err) {
6954
if (SketchesError.NotFound.is(err)) {
70-
// To handle the following:
71-
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
72-
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
73-
this.messageService.error(err.message);
55+
// Force update the menu items to remove the absent sketch.
7456
this.update();
57+
} else {
58+
throw err;
7559
}
7660
}
77-
if (sketch) {
78-
await this.commandService.executeCommand(
79-
OpenSketch.Commands.OPEN_SKETCH.id,
80-
sketch
81-
);
82-
}
8361
},
8462
};
8563
}

‎arduino-ide-extension/src/browser/theia/core/about-dialog.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
2-
import { duration } from '../../../common/decorators';
32

43
export class AboutDialog extends TheiaAboutDialog {
5-
@duration({ name: 'theia-about#init' })
64
protected override async init(): Promise<void> {
75
// NOOP
86
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ export interface CoreService {
108108
compile(options: CoreService.Options.Compile): Promise<void>;
109109
upload(options: CoreService.Options.Upload): Promise<void>;
110110
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
111+
/**
112+
* Refreshes the underling core gRPC client for the Arduino CLI.
113+
*/
114+
refresh(): Promise<void>;
111115
}
112116

113117
export namespace CoreService {

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

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,9 @@ export const SketchesService = Symbol('SketchesService');
2121
export interface SketchesService {
2222
/**
2323
* Resolves to a sketch container representing the hierarchical structure of the sketches.
24-
* If `uri` is not given, `directories.user` will be user instead. Specify `exclude` global patterns to filter folders from the sketch container.
25-
* If `exclude` is not set `['**\/libraries\/**', '**\/hardware\/**']` will be used instead.
24+
* If `uri` is not given, `directories.user` will be user instead.
2625
*/
27-
getSketches({
28-
uri,
29-
exclude,
30-
}: {
31-
uri?: string;
32-
exclude?: string[];
33-
}): Promise<SketchContainer>;
26+
getSketches({ uri }: { uri?: string }): Promise<SketchContainer>;
3427

3528
/**
3629
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
@@ -82,8 +75,10 @@ export interface SketchesService {
8275

8376
/**
8477
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
78+
* If `forceUpdate` is `true`, the array of recently opened sketches will be recalculated.
79+
* Invalid and missing sketches will be removed from the list. It's `false` by default.
8580
*/
86-
recentlyOpenedSketches(): Promise<Sketch[]>;
81+
recentlyOpenedSketches(forceUpdate?: boolean): Promise<Sketch[]>;
8782

8883
/**
8984
* Archives the sketch, resolves to the archive URI.
@@ -114,6 +109,19 @@ export namespace SketchRef {
114109
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
115110
};
116111
}
112+
export function is(arg: unknown): arg is SketchRef {
113+
if (typeof arg === 'object') {
114+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115+
const object = arg as any;
116+
return (
117+
'name' in object &&
118+
typeof object['name'] === 'string' &&
119+
'uri' in object &&
120+
typeof object['name'] === 'string'
121+
);
122+
}
123+
return false;
124+
}
117125
}
118126
export interface Sketch extends SketchRef {
119127
readonly mainFileUri: string; // `MainFile`
@@ -122,14 +130,25 @@ export interface Sketch extends SketchRef {
122130
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
123131
}
124132
export namespace Sketch {
125-
export function is(arg: any): arg is Sketch {
126-
return (
127-
!!arg &&
128-
'name' in arg &&
129-
'uri' in arg &&
130-
typeof arg.name === 'string' &&
131-
typeof arg.uri === 'string'
132-
);
133+
export function is(arg: unknown): arg is Sketch {
134+
if (!SketchRef.is(arg)) {
135+
return false;
136+
}
137+
if (typeof arg === 'object') {
138+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
139+
const object = arg as any;
140+
return (
141+
'mainFileUri' in object &&
142+
typeof object['mainFileUri'] === 'string' &&
143+
'otherSketchFileUris' in object &&
144+
Array.isArray(object['otherSketchFileUris']) &&
145+
'additionalFileUris' in object &&
146+
Array.isArray(object['additionalFileUris']) &&
147+
'rootFolderFileUris' in object &&
148+
Array.isArray(object['rootFolderFileUris'])
149+
);
150+
}
151+
return false;
133152
}
134153
export namespace Extensions {
135154
export const MAIN = ['.ino', '.pde'];

‎arduino-ide-extension/src/node/config-service-impl.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
2626
import { Deferred } from '@theia/core/lib/common/promise-util';
2727
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
2828
import { deepClone } from '@theia/core';
29-
import { duration } from '../common/decorators';
3029

3130
const deepmerge = require('deepmerge');
3231

@@ -129,7 +128,6 @@ export class ConfigServiceImpl
129128
return this.daemon.getVersion();
130129
}
131130

132-
@duration()
133131
protected async loadCliConfig(
134132
initializeIfAbsent = true
135133
): Promise<DefaultCliConfig | undefined> {

‎arduino-ide-extension/src/node/core-client-provider.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export class CoreClientProvider {
9494
return this.onClientDidRefreshEmitter.event;
9595
}
9696

97+
async refresh(): Promise<void> {
98+
const client = await this.client;
99+
await this.initInstance(client);
100+
}
101+
97102
/**
98103
* Encapsulates both the gRPC core client creation (`CreateRequest`) and initialization (`InitRequest`).
99104
*/
@@ -415,6 +420,10 @@ export abstract class CoreClientAware {
415420
protected get onClientDidRefresh(): Event<CoreClientProvider.Client> {
416421
return this.coreClientProvider.onClientDidRefresh;
417422
}
423+
424+
refresh(): Promise<void> {
425+
return this.coreClientProvider.refresh();
426+
}
418427
}
419428

420429
class IndexUpdateRequiredBeforeInitError extends Error {

‎arduino-ide-extension/src/node/examples-service-impl.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,10 @@ import {
1111
SketchContainer,
1212
} from '../common/protocol/sketches-service';
1313
import { ExamplesService } from '../common/protocol/examples-service';
14-
import {
15-
LibraryLocation,
16-
LibraryPackage,
17-
LibraryService,
18-
} from '../common/protocol';
19-
import { duration } from '../common/decorators';
14+
import { LibraryLocation, LibraryPackage } from '../common/protocol';
2015
import { URI } from '@theia/core/lib/common/uri';
2116
import { Path } from '@theia/core/lib/common/path';
17+
import { LibraryServiceImpl } from './library-service-impl';
2218

2319
interface BuiltInSketchRef {
2420
readonly name: string;
@@ -84,8 +80,8 @@ export class BuiltInExamplesServiceImpl {
8480

8581
@injectable()
8682
export class ExamplesServiceImpl implements ExamplesService {
87-
@inject(LibraryService)
88-
private readonly libraryService: LibraryService;
83+
@inject(LibraryServiceImpl)
84+
private readonly libraryService: LibraryServiceImpl;
8985

9086
@inject(BuiltInExamplesServiceImpl)
9187
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
@@ -94,15 +90,23 @@ export class ExamplesServiceImpl implements ExamplesService {
9490
return this.builtInExamplesService.builtIns();
9591
}
9692

97-
@duration()
98-
async installed({ fqbn }: { fqbn?: string }): Promise<{
93+
async installed({
94+
fqbn,
95+
forceRefresh,
96+
}: {
97+
fqbn?: string;
98+
forceRefresh?: boolean;
99+
}): Promise<{
99100
user: SketchContainer[];
100101
current: SketchContainer[];
101102
any: SketchContainer[];
102103
}> {
103104
const user: SketchContainer[] = [];
104105
const current: SketchContainer[] = [];
105106
const any: SketchContainer[] = [];
107+
if (forceRefresh) {
108+
await this.libraryService.refresh();
109+
}
106110
const packages: LibraryPackage[] = await this.libraryService.list({
107111
fqbn,
108112
});

‎arduino-ide-extension/src/node/sketches-service-impl.ts

Lines changed: 78 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { injectable, inject } from '@theia/core/shared/inversify';
2-
import * as fs from 'fs';
2+
import { promises as fs, realpath, lstat, Stats, constants, rm } from 'fs';
33
import * as os from 'os';
44
import * as temp from 'temp';
5-
65
import * as path from 'path';
76
import * as crypto from 'crypto';
87
import { ncp } from 'ncp';
@@ -24,7 +23,6 @@ import {
2423
ArchiveSketchRequest,
2524
LoadSketchRequest,
2625
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
27-
import { duration } from '../common/decorators';
2826
import * as glob from 'glob';
2927
import { Deferred } from '@theia/core/lib/common/promise-util';
3028
import { ServiceError } from './service-error';
@@ -41,6 +39,7 @@ export class SketchesServiceImpl
4139
{
4240
private sketchSuffixIndex = 1;
4341
private lastSketchBaseName: string;
42+
private recentSketches: SketchWithDetails[] | undefined;
4443

4544
@inject(ConfigServiceImpl)
4645
private readonly configService: ConfigServiceImpl;
@@ -54,28 +53,7 @@ export class SketchesServiceImpl
5453
@inject(IsTempSketch)
5554
private readonly isTempSketch: IsTempSketch;
5655

57-
async getSketches({
58-
uri,
59-
exclude,
60-
}: {
61-
uri?: string;
62-
exclude?: string[];
63-
}): Promise<SketchContainer> {
64-
const [/*old,*/ _new] = await Promise.all([
65-
// this.getSketchesOld({ uri, exclude }),
66-
this.getSketchesNew({ uri, exclude }),
67-
]);
68-
return _new;
69-
}
70-
71-
@duration()
72-
async getSketchesNew({
73-
uri,
74-
exclude,
75-
}: {
76-
uri?: string;
77-
exclude?: string[];
78-
}): Promise<SketchContainer> {
56+
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
7957
const root = await this.root(uri);
8058
const pathToAllSketchFiles = await new Promise<string[]>(
8159
(resolve, reject) => {
@@ -169,7 +147,7 @@ export class SketchesServiceImpl
169147
if (child) {
170148
child.sketches.push({
171149
name: sketchName,
172-
uri: FileUri.create(pathToSketchFile).toString(),
150+
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
173151
});
174152
}
175153
}
@@ -191,23 +169,16 @@ export class SketchesServiceImpl
191169
const requestSketchPath = FileUri.fsPath(uri);
192170
req.setSketchPath(requestSketchPath);
193171
req.setInstance(instance);
194-
const stat = new Deferred<fs.Stats | Error>();
195-
fs.lstat(requestSketchPath, (err, result) =>
172+
const stat = new Deferred<Stats | Error>();
173+
lstat(requestSketchPath, (err, result) =>
196174
err ? stat.resolve(err) : stat.resolve(result)
197175
);
198176
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
199177
client.loadSketch(req, async (err, resp) => {
200178
if (err) {
201179
reject(
202180
isNotFoundError(err)
203-
? SketchesError.NotFound(
204-
fixErrorMessage(
205-
err,
206-
requestSketchPath,
207-
this.configService.cliConfiguration?.directories.user
208-
),
209-
uri
210-
)
181+
? SketchesError.NotFound(err.details, uri)
211182
: err
212183
);
213184
return;
@@ -257,19 +228,29 @@ export class SketchesServiceImpl
257228
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
258229
}
259230

260-
private async loadRecentSketches(
261-
fsPath: string
262-
): Promise<Record<string, number>> {
231+
private async loadRecentSketches(): Promise<Record<string, number>> {
232+
const fsPath = await this.recentSketchesFsPath;
263233
let data: Record<string, number> = {};
234+
const raw = await fs.readFile(fsPath, {
235+
encoding: 'utf8',
236+
});
264237
try {
265-
const raw = await promisify(fs.readFile)(fsPath, {
266-
encoding: 'utf8',
267-
});
268238
data = JSON.parse(raw);
269-
} catch {}
239+
} catch (err) {
240+
console.error(
241+
`Could not parse recently opened sketches. Raw input was: ${raw}`
242+
);
243+
}
270244
return data;
271245
}
272246

247+
private async saveRecentSketches(
248+
data: Record<string, number>
249+
): Promise<void> {
250+
const fsPath = await this.recentSketchesFsPath;
251+
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
252+
}
253+
273254
async markAsRecentlyOpened(uri: string): Promise<void> {
274255
let sketch: Sketch | undefined = undefined;
275256
try {
@@ -281,8 +262,7 @@ export class SketchesServiceImpl
281262
return;
282263
}
283264

284-
const fsPath = await this.recentSketchesFsPath;
285-
const data = await this.loadRecentSketches(fsPath);
265+
const data = await this.loadRecentSketches();
286266
const now = Date.now();
287267
data[sketch.uri] = now;
288268

@@ -301,42 +281,48 @@ export class SketchesServiceImpl
301281
delete data[toDeleteUri];
302282
}
303283

304-
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
305-
this.recentlyOpenedSketches().then((sketches) =>
284+
await this.saveRecentSketches(data);
285+
this.recentlyOpenedSketches(true).then((sketches) =>
306286
this.notificationService.notifyRecentSketchesDidChange({ sketches })
307287
);
308288
}
309289

310-
async recentlyOpenedSketches(): Promise<Sketch[]> {
311-
const configDirUri = await this.envVariableServer.getConfigDirUri();
312-
const fsPath = path.join(
313-
FileUri.fsPath(configDirUri),
314-
'recent-sketches.json'
315-
);
316-
let data: Record<string, number> = {};
317-
try {
318-
const raw = await promisify(fs.readFile)(fsPath, {
319-
encoding: 'utf8',
320-
});
321-
data = JSON.parse(raw);
322-
} catch {}
323-
324-
const sketches: SketchWithDetails[] = [];
325-
for (const uri of Object.keys(data).sort(
326-
(left, right) => data[right] - data[left]
327-
)) {
328-
try {
329-
const sketch = await this.loadSketch(uri);
330-
sketches.push(sketch);
331-
} catch {}
290+
async recentlyOpenedSketches(forceUpdate?: boolean): Promise<Sketch[]> {
291+
if (!this.recentSketches || forceUpdate) {
292+
const data = await this.loadRecentSketches();
293+
const sketches: SketchWithDetails[] = [];
294+
let needsUpdate = false;
295+
for (const uri of Object.keys(data).sort(
296+
(left, right) => data[right] - data[left]
297+
)) {
298+
let sketch: SketchWithDetails | undefined = undefined;
299+
try {
300+
sketch = await this.loadSketch(uri);
301+
} catch {}
302+
if (!sketch) {
303+
needsUpdate = true;
304+
} else {
305+
sketches.push(sketch);
306+
}
307+
}
308+
if (needsUpdate) {
309+
const data = sketches.reduce((acc, curr) => {
310+
acc[curr.uri] = curr.mtimeMs;
311+
return acc;
312+
}, {} as Record<string, number>);
313+
await this.saveRecentSketches(data);
314+
this.notificationService.notifyRecentSketchesDidChange({ sketches });
315+
}
316+
this.recentSketches = sketches;
332317
}
333-
334-
return sketches;
318+
return this.recentSketches;
335319
}
336320

337321
async cloneExample(uri: string): Promise<Sketch> {
338-
const sketch = await this.loadSketch(uri);
339-
const parentPath = await this.createTempFolder();
322+
const [sketch, parentPath] = await Promise.all([
323+
this.loadSketch(uri),
324+
this.createTempFolder(),
325+
]);
340326
const destinationUri = FileUri.create(
341327
path.join(parentPath, sketch.name)
342328
).toString();
@@ -377,7 +363,7 @@ export class SketchesServiceImpl
377363
this.sketchSuffixIndex++
378364
)}`;
379365
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
380-
const sketchExists = await promisify(fs.exists)(
366+
const sketchExists = await this.exists(
381367
path.join(sketchbookPath, sketchNameCandidate)
382368
);
383369
if (!sketchExists) {
@@ -393,8 +379,8 @@ export class SketchesServiceImpl
393379

394380
const sketchDir = path.join(parentPath, sketchName);
395381
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
396-
await promisify(fs.mkdir)(sketchDir, { recursive: true });
397-
await promisify(fs.writeFile)(
382+
await fs.mkdir(sketchDir, { recursive: true });
383+
await fs.writeFile(
398384
sketchFile,
399385
`void setup() {
400386
// put your setup code here, to run once:
@@ -424,7 +410,7 @@ void loop() {
424410
reject(createError);
425411
return;
426412
}
427-
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => {
413+
realpath.native(dirPath, (resolveError, resolvedDirPath) => {
428414
if (resolveError) {
429415
reject(resolveError);
430416
return;
@@ -478,7 +464,7 @@ void loop() {
478464
{ destinationUri }: { destinationUri: string }
479465
): Promise<string> {
480466
const source = FileUri.fsPath(sketch.uri);
481-
const exists = await promisify(fs.exists)(source);
467+
const exists = await this.exists(source);
482468
if (!exists) {
483469
throw new Error(`Sketch does not exist: ${sketch}`);
484470
}
@@ -520,7 +506,7 @@ void loop() {
520506
const destination = FileUri.fsPath(destinationUri);
521507
let tempDestination = await this.createTempFolder();
522508
tempDestination = path.join(tempDestination, sketch.name);
523-
await fs.promises.mkdir(tempDestination, { recursive: true });
509+
await fs.mkdir(tempDestination, { recursive: true });
524510
await copy(source, tempDestination);
525511
await copy(tempDestination, destination);
526512
return FileUri.create(destination).toString();
@@ -531,8 +517,8 @@ void loop() {
531517
const { client } = await this.coreClient;
532518
const archivePath = FileUri.fsPath(destinationUri);
533519
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
534-
if (await promisify(fs.exists)(archivePath)) {
535-
await promisify(fs.unlink)(archivePath);
520+
if (await this.exists(archivePath)) {
521+
await fs.unlink(archivePath);
536522
}
537523
const req = new ArchiveSketchRequest();
538524
req.setSketchPath(FileUri.fsPath(sketch.uri));
@@ -556,15 +542,15 @@ void loop() {
556542

557543
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
558544
const sketchPath = FileUri.fsPath(sketch.uri);
559-
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
545+
await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
560546
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
561547
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
562548
}
563549

564550
async deleteSketch(sketch: Sketch): Promise<void> {
565551
return new Promise<void>((resolve, reject) => {
566552
const sketchPath = FileUri.fsPath(sketch.uri);
567-
fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
553+
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
568554
if (error) {
569555
console.error(`Failed to delete sketch at ${sketchPath}.`, error);
570556
reject(error);
@@ -575,42 +561,21 @@ void loop() {
575561
});
576562
});
577563
}
564+
565+
private async exists(pathLike: string): Promise<boolean> {
566+
try {
567+
await fs.access(pathLike, constants.R_OK | constants.W_OK);
568+
return true;
569+
} catch {
570+
return false;
571+
}
572+
}
578573
}
579574

580575
interface SketchWithDetails extends Sketch {
581576
readonly mtimeMs: number;
582577
}
583578

584-
// https://github.com/arduino/arduino-cli/issues/1797
585-
function fixErrorMessage(
586-
err: ServiceError,
587-
sketchPath: string,
588-
sketchbookPath: string | undefined
589-
): string {
590-
if (!sketchbookPath) {
591-
return err.details; // No way to repair the error message. The current sketchbook path is not available.
592-
}
593-
// Original: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing /Users/a.kitta/Documents/Arduino/Arduino.ino`
594-
// Fixed: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing $sketchPath`
595-
const message = err.details;
596-
const incorrectMessageSuffix = path.join(sketchbookPath, 'Arduino.ino');
597-
if (
598-
message.startsWith("Can't open sketch: no valid sketch found in") &&
599-
message.endsWith(`${incorrectMessageSuffix}`)
600-
) {
601-
const sketchName = path.basename(sketchPath);
602-
const correctMessagePrefix = message.substring(
603-
0,
604-
message.length - incorrectMessageSuffix.length
605-
);
606-
return `${correctMessagePrefix}${path.join(
607-
sketchPath,
608-
`${sketchName}.ino`
609-
)}`;
610-
}
611-
return err.details;
612-
}
613-
614579
function isNotFoundError(err: unknown): err is ServiceError {
615580
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
616581
}

0 commit comments

Comments
 (0)
Please sign in to comment.