Skip to content

Commit 76f9f63

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
feat: configure sketchbook location without restart
Closes #1764 Closes #796 Closes #569 Closes #655 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 3f05396 commit 76f9f63

28 files changed

+651
-262
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
343343
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
344344
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
345345
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
346+
import { ConfigServiceClient } from './config/config-service-client';
346347

347348
export default new ContainerModule((bind, unbind, isBound, rebind) => {
348349
// Commands and toolbar items
@@ -404,6 +405,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
404405
)
405406
)
406407
.inSingletonScope();
408+
bind(ConfigServiceClient).toSelf().inSingletonScope();
409+
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
407410

408411
// Boards service
409412
bind(BoardsService)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
2+
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
3+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
4+
import { Emitter, Event } from '@theia/core/lib/common/event';
5+
import { MessageService } from '@theia/core/lib/common/message-service';
6+
import { deepClone } from '@theia/core/lib/common/objects';
7+
import URI from '@theia/core/lib/common/uri';
8+
import {
9+
inject,
10+
injectable,
11+
postConstruct,
12+
} from '@theia/core/shared/inversify';
13+
import { ConfigService, ConfigState } from '../../common/protocol';
14+
import { NotificationCenter } from '../notification-center';
15+
16+
@injectable()
17+
export class ConfigServiceClient implements FrontendApplicationContribution {
18+
@inject(ConfigService)
19+
private readonly delegate: ConfigService;
20+
@inject(NotificationCenter)
21+
private readonly notificationCenter: NotificationCenter;
22+
@inject(FrontendApplicationStateService)
23+
private readonly appStateService: FrontendApplicationStateService;
24+
@inject(MessageService)
25+
private readonly messageService: MessageService;
26+
27+
private readonly didChangeSketchDirUriEmitter = new Emitter<
28+
URI | undefined
29+
>();
30+
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
31+
private readonly toDispose = new DisposableCollection(
32+
this.didChangeSketchDirUriEmitter,
33+
this.didChangeDataDirUriEmitter
34+
);
35+
36+
private config: ConfigState | undefined;
37+
38+
@postConstruct()
39+
protected init(): void {
40+
this.appStateService.reachedState('ready').then(async () => {
41+
const config = await this.fetchConfig();
42+
this.use(config);
43+
});
44+
}
45+
46+
onStart(): void {
47+
this.notificationCenter.onConfigDidChange((config) => this.use(config));
48+
}
49+
50+
onStop(): void {
51+
this.toDispose.dispose();
52+
}
53+
54+
get onDidChangeSketchDirUri(): Event<URI | undefined> {
55+
return this.didChangeSketchDirUriEmitter.event;
56+
}
57+
58+
get onDidChangeDataDirUri(): Event<URI | undefined> {
59+
return this.didChangeDataDirUriEmitter.event;
60+
}
61+
62+
async fetchConfig(): Promise<ConfigState> {
63+
return this.delegate.getConfiguration();
64+
}
65+
66+
/**
67+
* CLI config related error messages if any.
68+
*/
69+
tryGetMessages(): string[] | undefined {
70+
return this.config?.messages;
71+
}
72+
73+
/**
74+
* `directories.user`
75+
*/
76+
tryGetSketchDirUri(): URI | undefined {
77+
return this.config?.config?.sketchDirUri
78+
? new URI(this.config?.config?.sketchDirUri)
79+
: undefined;
80+
}
81+
82+
/**
83+
* `directories.data`
84+
*/
85+
tryGetDataDirUri(): URI | undefined {
86+
return this.config?.config?.dataDirUri
87+
? new URI(this.config?.config?.dataDirUri)
88+
: undefined;
89+
}
90+
91+
private use(config: ConfigState): void {
92+
const oldConfig = deepClone(this.config);
93+
this.config = config;
94+
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
95+
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
96+
}
97+
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
98+
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
99+
}
100+
if (this.config.messages?.length) {
101+
const message = this.config.messages.join(' ');
102+
// toast the error later otherwise it might not show up in IDE2
103+
setTimeout(() => this.messageService.error(message), 1_000);
104+
}
105+
}
106+
}

arduino-ide-extension/src/browser/contributions/add-zip-library.ts

-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
22
import * as remote from '@theia/core/electron-shared/@electron/remote';
33
import URI from '@theia/core/lib/common/uri';
44
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
5-
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
65
import { ArduinoMenus } from '../menu/arduino-menus';
76
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
87
import { ExecuteWithProgress } from '../../common/protocol/progressible';
@@ -16,9 +15,6 @@ import { nls } from '@theia/core/lib/common';
1615

1716
@injectable()
1817
export class AddZipLibrary extends SketchContribution {
19-
@inject(EnvVariablesServer)
20-
private readonly envVariableServer: EnvVariablesServer;
21-
2218
@inject(ResponseServiceClient)
2319
private readonly responseService: ResponseServiceClient;
2420

arduino-ide-extension/src/browser/contributions/archive-sketch.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { injectable } from '@theia/core/shared/inversify';
22
import * as remote from '@theia/core/electron-shared/@electron/remote';
33
import * as dateFormat from 'dateformat';
4-
import URI from '@theia/core/lib/common/uri';
54
import { ArduinoMenus } from '../menu/arduino-menus';
65
import {
76
SketchContribution,
@@ -29,20 +28,17 @@ export class ArchiveSketch extends SketchContribution {
2928
}
3029

3130
private async archiveSketch(): Promise<void> {
32-
const [sketch, config] = await Promise.all([
33-
this.sketchServiceClient.currentSketch(),
34-
this.configService.getConfiguration(),
35-
]);
31+
const sketch = await this.sketchServiceClient.currentSketch();
3632
if (!CurrentSketch.isValid(sketch)) {
3733
return;
3834
}
3935
const archiveBasename = `${sketch.name}-${dateFormat(
4036
new Date(),
4137
'yymmdd'
4238
)}a.zip`;
43-
const defaultPath = await this.fileService.fsPath(
44-
new URI(config.sketchDirUri).resolve(archiveBasename)
45-
);
39+
const defaultContainerUri = await this.defaultUri();
40+
const defaultUri = defaultContainerUri.resolve(archiveBasename);
41+
const defaultPath = await this.fileService.fsPath(defaultUri);
4642
const { filePath, canceled } = await remote.dialog.showSaveDialog(
4743
remote.getCurrentWindow(),
4844
{

arduino-ide-extension/src/browser/contributions/board-selection.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,7 @@ PID: ${PID}`;
155155
);
156156

157157
// Ports submenu
158-
const portsSubmenuPath = [
159-
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
160-
'2_ports',
161-
];
158+
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
162159
const portsSubmenuLabel = config.selectedPort?.address;
163160
this.menuModelRegistry.registerSubmenu(
164161
portsSubmenuPath,

arduino-ide-extension/src/browser/contributions/contribution.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
1212
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
1313
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
1414
import { MessageService } from '@theia/core/lib/common/message-service';
15+
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
1516
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
1617

1718
import {
@@ -43,7 +44,6 @@ import {
4344
} from '../../common/protocol/sketches-service-client-impl';
4445
import {
4546
SketchesService,
46-
ConfigService,
4747
FileSystemExt,
4848
Sketch,
4949
CoreService,
@@ -62,6 +62,7 @@ import { NotificationManager } from '../theia/messages/notifications-manager';
6262
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
6363
import { WorkspaceService } from '../theia/workspace/workspace-service';
6464
import { MainMenuManager } from '../../common/main-menu-manager';
65+
import { ConfigServiceClient } from '../config/config-service-client';
6566

6667
export {
6768
Command,
@@ -142,8 +143,8 @@ export abstract class SketchContribution extends Contribution {
142143
@inject(FileSystemExt)
143144
protected readonly fileSystemExt: FileSystemExt;
144145

145-
@inject(ConfigService)
146-
protected readonly configService: ConfigService;
146+
@inject(ConfigServiceClient)
147+
protected readonly configService: ConfigServiceClient;
147148

148149
@inject(SketchesService)
149150
protected readonly sketchService: SketchesService;
@@ -160,6 +161,9 @@ export abstract class SketchContribution extends Contribution {
160161
@inject(OutputChannelManager)
161162
protected readonly outputChannelManager: OutputChannelManager;
162163

164+
@inject(EnvVariablesServer)
165+
protected readonly envVariableServer: EnvVariablesServer;
166+
163167
protected async sourceOverride(): Promise<Record<string, string>> {
164168
const override: Record<string, string> = {};
165169
const sketch = await this.sketchServiceClient.currentSketch();
@@ -173,6 +177,25 @@ export abstract class SketchContribution extends Contribution {
173177
}
174178
return override;
175179
}
180+
181+
/**
182+
* Defaults to `directories.user` if defined and not CLI config errors were detected.
183+
* Otherwise, the URI of the user home directory.
184+
*/
185+
protected async defaultUri(): Promise<URI> {
186+
const errors = this.configService.tryGetMessages();
187+
let defaultUri = this.configService.tryGetSketchDirUri();
188+
if (!defaultUri || errors?.length) {
189+
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
190+
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
191+
}
192+
return defaultUri;
193+
}
194+
195+
protected async defaultPath(): Promise<string> {
196+
const defaultUri = await this.defaultUri();
197+
return this.fileService.fsPath(defaultUri);
198+
}
176199
}
177200

178201
@injectable()

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

+16-4
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ import {
2929
CoreService,
3030
} from '../../common/protocol';
3131
import { nls } from '@theia/core/lib/common';
32+
import { unregisterSubmenu } from '../menu/arduino-menus';
3233

3334
@injectable()
3435
export abstract class Examples extends SketchContribution {
3536
@inject(CommandRegistry)
3637
private readonly commandRegistry: CommandRegistry;
3738

3839
@inject(MenuModelRegistry)
39-
private readonly menuRegistry: MenuModelRegistry;
40+
protected readonly menuRegistry: MenuModelRegistry;
4041

4142
@inject(ExamplesService)
4243
protected readonly examplesService: ExamplesService;
@@ -47,13 +48,22 @@ export abstract class Examples extends SketchContribution {
4748
@inject(BoardsServiceProvider)
4849
protected readonly boardsServiceClient: BoardsServiceProvider;
4950

51+
@inject(NotificationCenter)
52+
protected readonly notificationCenter: NotificationCenter;
53+
5054
protected readonly toDispose = new DisposableCollection();
5155

5256
protected override init(): void {
5357
super.init();
5458
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
5559
this.handleBoardChanged(selectedBoard)
5660
);
61+
this.notificationCenter.onDidReinitialize(() =>
62+
this.update({
63+
board: this.boardsServiceClient.boardsConfig.selectedBoard,
64+
// No force refresh. The core client was already refreshed.
65+
})
66+
);
5767
}
5868

5969
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
@@ -120,6 +130,11 @@ export abstract class Examples extends SketchContribution {
120130
const { label } = sketchContainerOrPlaceholder;
121131
submenuPath = [...menuPath, label];
122132
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
133+
this.toDispose.push(
134+
Disposable.create(() =>
135+
unregisterSubmenu(submenuPath, this.menuRegistry)
136+
)
137+
);
123138
sketches.push(...sketchContainerOrPlaceholder.sketches);
124139
children.push(...sketchContainerOrPlaceholder.children);
125140
} else {
@@ -239,9 +254,6 @@ export class BuiltInExamples extends Examples {
239254

240255
@injectable()
241256
export class LibraryExamples extends Examples {
242-
@inject(NotificationCenter)
243-
private readonly notificationCenter: NotificationCenter;
244-
245257
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
246258

247259
override onStart(): void {

arduino-ide-extension/src/browser/contributions/include-library.ts

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
5353
this.notificationCenter.onLibraryDidUninstall(() =>
5454
this.updateMenuActions()
5555
);
56+
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
5657
}
5758

5859
override async onReady(): Promise<void> {

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,7 @@ export class OpenSketch extends SketchContribution {
8282
}
8383

8484
private async selectSketch(): Promise<Sketch | undefined> {
85-
const config = await this.configService.getConfiguration();
86-
const defaultPath = await this.fileService.fsPath(
87-
new URI(config.sketchDirUri)
88-
);
85+
const defaultPath = await this.defaultPath();
8986
const { filePaths } = await remote.dialog.showOpenDialog(
9087
remote.getCurrentWindow(),
9188
{

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ export class SaveAsSketch extends SketchContribution {
5858
markAsRecentlyOpened,
5959
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
6060
): Promise<boolean> {
61-
const [sketch, configuration] = await Promise.all([
62-
this.sketchServiceClient.currentSketch(),
63-
this.configService.getConfiguration(),
64-
]);
61+
const sketch = await this.sketchServiceClient.currentSketch();
6562
if (!CurrentSketch.isValid(sketch)) {
6663
return false;
6764
}
@@ -72,7 +69,7 @@ export class SaveAsSketch extends SketchContribution {
7269
}
7370

7471
const sketchUri = new URI(sketch.uri);
75-
const sketchbookDirUri = new URI(configuration.sketchDirUri);
72+
const sketchbookDirUri = await this.defaultUri();
7673
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
7774
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
7875
// Otherwise, it proposes the parent folder of the current sketch.

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

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { nls } from '@theia/core/lib/common/nls';
1111
export class Sketchbook extends Examples {
1212
override onStart(): void {
1313
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
14+
this.configService.onDidChangeSketchDirUri(() => this.update());
1415
}
1516

1617
override async onReady(): Promise<void> {

0 commit comments

Comments
 (0)