Skip to content

Commit 1a2bfc9

Browse files
author
Akos Kitta
committed
IDE2 falls back to new sketch if opening failed.
Closes #1089 Signed-off-by: Akos Kitta <[email protected]>
1 parent a003831 commit 1a2bfc9

12 files changed

+345
-81
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

+53-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Sketch,
1313
LibraryService,
1414
ArduinoDaemon,
15+
SketchesError,
1516
} from '../common/protocol';
1617
import { Mutex } from 'async-mutex';
1718
import {
@@ -20,6 +21,7 @@ import {
2021
MenuModelRegistry,
2122
ILogger,
2223
DisposableCollection,
24+
ApplicationError,
2325
} from '@theia/core';
2426
import {
2527
Dialog,
@@ -76,6 +78,8 @@ import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
7678
import { IDEUpdater } from '../common/protocol/ide-updater';
7779
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
7880
import { HostedPluginEvents } from './hosted-plugin-events';
81+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
82+
import { Notifications } from './contributions/notifications';
7983

8084
const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
8185
export const SKIP_IDE_VERSION = 'skipIDEVersion';
@@ -155,6 +159,9 @@ export class ArduinoFrontendContribution
155159
@inject(ArduinoDaemon)
156160
private readonly daemon: ArduinoDaemon;
157161

162+
@inject(WorkspaceService)
163+
private readonly workspaceService: WorkspaceService;
164+
158165
protected invalidConfigPopup:
159166
| Promise<void | 'No' | 'Yes' | undefined>
160167
| undefined;
@@ -540,13 +547,55 @@ export class ArduinoFrontendContribution
540547
}
541548
});
542549
}
543-
} catch (e) {
544-
console.error(e);
545-
const message = e instanceof Error ? e.message : JSON.stringify(e);
546-
this.messageService.error(message);
550+
} catch (err) {
551+
if (SketchesError.NotFound.is(err)) {
552+
this.openFallbackSketch(err);
553+
} else {
554+
console.error(err);
555+
const message =
556+
err instanceof Error
557+
? err.message
558+
: typeof err === 'string'
559+
? err
560+
: String(err);
561+
this.messageService.error(message);
562+
}
547563
}
548564
}
549565

566+
private openFallbackSketch(
567+
err: ApplicationError<
568+
number,
569+
{
570+
uri: string;
571+
}
572+
>
573+
) {
574+
this.sketchService.createNewSketch().then((sketch) => {
575+
this.workspaceService.open(
576+
new URI(sketch.uri),
577+
Object.assign(
578+
{
579+
preserveWindow: true,
580+
},
581+
{
582+
tasks: [
583+
{
584+
command: Notifications.Commands.NOTIFY.id,
585+
args: [
586+
{
587+
type: 'error',
588+
message: err.message,
589+
},
590+
],
591+
},
592+
],
593+
}
594+
)
595+
);
596+
});
597+
}
598+
550599
protected async ensureOpened(
551600
uri: string,
552601
forceOpen = false,

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,10 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
301301
import { CompilerErrors } from './contributions/compiler-errors';
302302
import { WidgetManager } from './theia/core/widget-manager';
303303
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
304-
import { StartupTask } from './widgets/sketchbook/startup-task';
304+
import { StartupTasks } from './widgets/sketchbook/startup-task';
305305
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
306306
import { Daemon } from './contributions/daemon';
307+
import { Notifications } from './contributions/notifications';
307308

308309
MonacoThemingService.register({
309310
id: 'arduino-theme',
@@ -696,9 +697,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
696697
Contribution.configure(bind, PlotterFrontendContribution);
697698
Contribution.configure(bind, Format);
698699
Contribution.configure(bind, CompilerErrors);
699-
Contribution.configure(bind, StartupTask);
700+
Contribution.configure(bind, StartupTasks);
700701
Contribution.configure(bind, IndexesUpdateProgress);
701702
Contribution.configure(bind, Daemon);
703+
Contribution.configure(bind, StartupTasks);
704+
Contribution.configure(bind, Notifications);
702705

703706
// Disabled the quick-pick customization from Theia when multiple formatters are available.
704707
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

Diff for: arduino-ide-extension/src/browser/boards/boards-service-provider.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ArduinoCommands } from '../arduino-commands';
2121
import { StorageWrapper } from '../storage-wrapper';
2222
import { nls } from '@theia/core/lib/common';
2323
import { Deferred } from '@theia/core/lib/common/promise-util';
24+
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
2425

2526
@injectable()
2627
export class BoardsServiceProvider implements FrontendApplicationContribution {
@@ -39,6 +40,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
3940
@inject(NotificationCenter)
4041
protected notificationCenter: NotificationCenter;
4142

43+
@inject(FrontendApplicationStateService)
44+
private readonly appStateService: FrontendApplicationStateService;
45+
4246
protected readonly onBoardsConfigChangedEmitter =
4347
new Emitter<BoardsConfig.Config>();
4448
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
@@ -87,11 +91,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
8791
this.notifyPlatformUninstalled.bind(this)
8892
);
8993

90-
Promise.all([
91-
this.boardsService.getAttachedBoards(),
92-
this.boardsService.getAvailablePorts(),
93-
this.loadState(),
94-
]).then(async ([attachedBoards, availablePorts]) => {
94+
this.appStateService.reachedState('ready').then(async () => {
95+
const [attachedBoards, availablePorts] = await Promise.all([
96+
this.boardsService.getAttachedBoards(),
97+
this.boardsService.getAvailablePorts(),
98+
this.loadState(),
99+
]);
95100
this._attachedBoards = attachedBoards;
96101
this._availablePorts = availablePorts;
97102
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { Command, CommandRegistry, Contribution } from './contribution';
3+
4+
@injectable()
5+
export class Notifications extends Contribution {
6+
override registerCommands(registry: CommandRegistry): void {
7+
registry.registerCommand(Notifications.Commands.NOTIFY, {
8+
execute: (arg) => {
9+
if (NotifyParams.is(arg)) {
10+
switch (arg.type) {
11+
case 'info':
12+
return this.messageService.info(arg.message);
13+
case 'warn':
14+
return this.messageService.warn(arg.message);
15+
case 'error':
16+
return this.messageService.error(arg.message);
17+
}
18+
}
19+
},
20+
});
21+
}
22+
}
23+
export namespace Notifications {
24+
export namespace Commands {
25+
export const NOTIFY: Command = {
26+
id: 'arduino-notify',
27+
};
28+
}
29+
}
30+
const TypeLiterals = ['info', 'warn', 'error'] as const;
31+
export type Type = typeof TypeLiterals[number];
32+
interface NotifyParams {
33+
readonly type: Type;
34+
readonly message: string;
35+
}
36+
namespace NotifyParams {
37+
export function is(arg: unknown): arg is NotifyParams {
38+
if (typeof arg === 'object') {
39+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
40+
const object = arg as any;
41+
return (
42+
'message' in object &&
43+
'type' in object &&
44+
typeof object['message'] === 'string' &&
45+
typeof object['type'] === 'string' &&
46+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47+
TypeLiterals.includes(object['type'] as any)
48+
);
49+
}
50+
return false;
51+
}
52+
}

Diff for: arduino-ide-extension/src/browser/contributions/sketchbook.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { ArduinoMenus } from '../menu/arduino-menus';
55
import { MainMenuManager } from '../../common/main-menu-manager';
66
import { NotificationCenter } from '../notification-center';
77
import { Examples } from './examples';
8-
import { SketchContainer } from '../../common/protocol';
8+
import {
9+
SketchContainer,
10+
SketchesError,
11+
SketchRef,
12+
} from '../../common/protocol';
913
import { OpenSketch } from './open-sketch';
1014
import { nls } from '@theia/core/lib/common';
1115

@@ -24,15 +28,14 @@ export class Sketchbook extends Examples {
2428
protected readonly notificationCenter: NotificationCenter;
2529

2630
override onStart(): void {
27-
this.sketchServiceClient.onSketchbookDidChange(() => {
28-
this.sketchService.getSketches({}).then((container) => {
29-
this.register(container);
30-
this.mainMenuManager.update();
31-
});
32-
});
31+
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
3332
}
3433

3534
override async onReady(): Promise<void> {
35+
this.update();
36+
}
37+
38+
private update() {
3639
this.sketchService.getSketches({}).then((container) => {
3740
this.register(container);
3841
this.mainMenuManager.update();
@@ -59,11 +62,24 @@ export class Sketchbook extends Examples {
5962
protected override createHandler(uri: string): CommandHandler {
6063
return {
6164
execute: async () => {
62-
const sketch = await this.sketchService.loadSketch(uri);
63-
return this.commandService.executeCommand(
64-
OpenSketch.Commands.OPEN_SKETCH.id,
65-
sketch
66-
);
65+
let sketch: SketchRef | undefined = undefined;
66+
try {
67+
sketch = await this.sketchService.loadSketch(uri);
68+
} catch (err) {
69+
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);
74+
this.update();
75+
}
76+
}
77+
if (sketch) {
78+
await this.commandService.executeCommand(
79+
OpenSketch.Commands.OPEN_SKETCH.id,
80+
sketch
81+
);
82+
}
6783
},
6884
};
6985
}

Diff for: arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts

+47-8
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ import { ConfigService } from '../../../common/protocol/config-service';
1717
import {
1818
SketchesService,
1919
Sketch,
20+
SketchesError,
2021
} from '../../../common/protocol/sketches-service';
2122
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
2223
import { BoardsConfig } from '../../boards/boards-config';
2324
import { FileStat } from '@theia/filesystem/lib/common/files';
24-
import { StartupTask } from '../../widgets/sketchbook/startup-task';
25+
import {
26+
StartupTask,
27+
StartupTasks,
28+
} from '../../widgets/sketchbook/startup-task';
29+
import { setURL } from '../../utils/window';
2530

2631
@injectable()
2732
export class WorkspaceService extends TheiaWorkspaceService {
@@ -60,6 +65,35 @@ export class WorkspaceService extends TheiaWorkspaceService {
6065
this.onCurrentWidgetChange({ newValue, oldValue: null });
6166
}
6267

68+
protected override async toFileStat(
69+
uri: string | URI | undefined
70+
): Promise<FileStat | undefined> {
71+
const stat = await super.toFileStat(uri);
72+
if (!stat) {
73+
return this.toFileStatWithNewSketchFallback(uri);
74+
}
75+
return stat;
76+
}
77+
78+
private async toFileStatWithNewSketchFallback(
79+
uri: string | URI | undefined
80+
): Promise<FileStat | undefined> {
81+
if (!uri) {
82+
return;
83+
}
84+
try {
85+
await this.sketchService.loadSketch(
86+
uri instanceof URI ? uri.toString() : uri
87+
);
88+
} catch (err) {
89+
if (SketchesError.NotFound.is(err)) {
90+
this.messageService.error(err.message);
91+
}
92+
}
93+
const newSketchUri = await this.sketchService.createNewSketch();
94+
return this.toFileStat(newSketchUri.uri);
95+
}
96+
6397
// Was copied from the Theia implementation.
6498
// Unlike the default behavior, IDE2 does not check the existence of the workspace before open.
6599
protected override async doGetDefaultWorkspaceUri(): Promise<
@@ -78,6 +112,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
78112
const wpPath = decodeURI(window.location.hash.substring(1));
79113
const workspaceUri = new URI().withPath(wpPath).withScheme('file');
80114
// ### Customization! Here, we do no check if the workspace exists.
115+
// ### The error or missing sketch handling is done in the customized `toFileStat`.
81116
return workspaceUri.toString();
82117
} else {
83118
// Else, ask the server for its suggested workspace (usually the one
@@ -127,7 +162,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
127162
protected override openWindow(uri: FileStat, options?: WorkspaceInput): void {
128163
const workspacePath = uri.resource.path.toString();
129164
if (this.shouldPreserveWindow(options)) {
130-
this.reloadWindow();
165+
this.reloadWindow(options); // Unlike Theia, IDE2 passes the `input` downstream.
131166
} else {
132167
try {
133168
this.openNewWindow(workspacePath, options); // Unlike Theia, IDE2 passes the `input` downstream.
@@ -139,21 +174,25 @@ export class WorkspaceService extends TheiaWorkspaceService {
139174
}
140175
}
141176

177+
protected override reloadWindow(options?: WorkspaceInput): void {
178+
if (StartupTasks.WorkspaceInput.is(options)) {
179+
setURL(StartupTask.append(options.tasks, new URL(window.location.href)));
180+
}
181+
super.reloadWindow();
182+
}
183+
142184
protected override openNewWindow(
143185
workspacePath: string,
144186
options?: WorkspaceInput
145187
): void {
146188
const { boardsConfig } = this.boardsServiceProvider;
147-
const url = BoardsConfig.Config.setConfig(
189+
let url = BoardsConfig.Config.setConfig(
148190
boardsConfig,
149191
new URL(window.location.href)
150192
); // Set the current boards config for the new browser window.
151193
url.hash = workspacePath;
152-
if (StartupTask.WorkspaceInput.is(options)) {
153-
url.searchParams.set(
154-
StartupTask.QUERY_STRING,
155-
encodeURIComponent(JSON.stringify(options.tasks))
156-
);
194+
if (StartupTasks.WorkspaceInput.is(options)) {
195+
url = StartupTask.append(options.tasks, url);
157196
}
158197

159198
this.windowService.openNewWindow(url.toString());

0 commit comments

Comments
 (0)