feat: configure sketchbook location without restart

Closes #1764
Closes #796
Closes #569
Closes #655

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-12-14 15:14:43 +01:00 committed by Akos Kitta
parent 3f05396222
commit 76f9f635d8
28 changed files with 655 additions and 266 deletions

View File

@ -343,6 +343,7 @@ import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { ConfigServiceClient } from './config/config-service-client';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@ -404,6 +405,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(ConfigServiceClient).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
// Boards service
bind(BoardsService)

View File

@ -0,0 +1,106 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { deepClone } from '@theia/core/lib/common/objects';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { ConfigService, ConfigState } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
@injectable()
export class ConfigServiceClient implements FrontendApplicationContribution {
@inject(ConfigService)
private readonly delegate: ConfigService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(MessageService)
private readonly messageService: MessageService;
private readonly didChangeSketchDirUriEmitter = new Emitter<
URI | undefined
>();
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
private readonly toDispose = new DisposableCollection(
this.didChangeSketchDirUriEmitter,
this.didChangeDataDirUriEmitter
);
private config: ConfigState | undefined;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(async () => {
const config = await this.fetchConfig();
this.use(config);
});
}
onStart(): void {
this.notificationCenter.onConfigDidChange((config) => this.use(config));
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSketchDirUri(): Event<URI | undefined> {
return this.didChangeSketchDirUriEmitter.event;
}
get onDidChangeDataDirUri(): Event<URI | undefined> {
return this.didChangeDataDirUriEmitter.event;
}
async fetchConfig(): Promise<ConfigState> {
return this.delegate.getConfiguration();
}
/**
* CLI config related error messages if any.
*/
tryGetMessages(): string[] | undefined {
return this.config?.messages;
}
/**
* `directories.user`
*/
tryGetSketchDirUri(): URI | undefined {
return this.config?.config?.sketchDirUri
? new URI(this.config?.config?.sketchDirUri)
: undefined;
}
/**
* `directories.data`
*/
tryGetDataDirUri(): URI | undefined {
return this.config?.config?.dataDirUri
? new URI(this.config?.config?.dataDirUri)
: undefined;
}
private use(config: ConfigState): void {
const oldConfig = deepClone(this.config);
this.config = config;
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
}
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
}
if (this.config.messages?.length) {
const message = this.config.messages.join(' ');
// toast the error later otherwise it might not show up in IDE2
setTimeout(() => this.messageService.error(message), 1_000);
}
}
}

View File

@ -2,7 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
@ -16,9 +15,6 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
private readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;

View File

@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
SketchContribution,
@ -29,10 +28,7 @@ export class ArchiveSketch extends SketchContribution {
}
private async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
@ -40,9 +36,9 @@ export class ArchiveSketch extends SketchContribution {
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const defaultContainerUri = await this.defaultUri();
const defaultUri = defaultContainerUri.resolve(archiveBasename);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{

View File

@ -155,10 +155,7 @@ PID: ${PID}`;
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,

View File

@ -12,6 +12,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
@ -43,7 +44,6 @@ import {
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
@ -62,6 +62,7 @@ import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
export {
Command,
@ -142,8 +143,8 @@ export abstract class SketchContribution extends Contribution {
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigServiceClient)
protected readonly configService: ConfigServiceClient;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@ -160,6 +161,9 @@ export abstract class SketchContribution extends Contribution {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
@ -173,6 +177,25 @@ export abstract class SketchContribution extends Contribution {
}
return override;
}
/**
* Defaults to `directories.user` if defined and not CLI config errors were detected.
* Otherwise, the URI of the user home directory.
*/
protected async defaultUri(): Promise<URI> {
const errors = this.configService.tryGetMessages();
let defaultUri = this.configService.tryGetSketchDirUri();
if (!defaultUri || errors?.length) {
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
}
return defaultUri;
}
protected async defaultPath(): Promise<string> {
const defaultUri = await this.defaultUri();
return this.fileService.fsPath(defaultUri);
}
}
@injectable()

View File

@ -29,6 +29,7 @@ import {
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { unregisterSubmenu } from '../menu/arduino-menus';
@injectable()
export abstract class Examples extends SketchContribution {
@ -36,7 +37,7 @@ export abstract class Examples extends SketchContribution {
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected readonly menuRegistry: MenuModelRegistry;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@ -47,6 +48,9 @@ export abstract class Examples extends SketchContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly toDispose = new DisposableCollection();
protected override init(): void {
@ -54,6 +58,12 @@ export abstract class Examples extends SketchContribution {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
this.notificationCenter.onDidReinitialize(() =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
// No force refresh. The core client was already refreshed.
})
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
@ -120,6 +130,11 @@ export abstract class Examples extends SketchContribution {
const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
this.toDispose.push(
Disposable.create(() =>
unregisterSubmenu(submenuPath, this.menuRegistry)
)
);
sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children);
} else {
@ -239,9 +254,6 @@ export class BuiltInExamples extends Examples {
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void {

View File

@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
this.notificationCenter.onLibraryDidUninstall(() =>
this.updateMenuActions()
);
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
}
override async onReady(): Promise<void> {

View File

@ -82,10 +82,7 @@ export class OpenSketch extends SketchContribution {
}
private async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const defaultPath = await this.defaultPath();
const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{

View File

@ -58,10 +58,7 @@ export class SaveAsSketch extends SketchContribution {
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@ -72,7 +69,7 @@ export class SaveAsSketch extends SketchContribution {
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = new URI(configuration.sketchDirUri);
const sketchbookDirUri = await this.defaultUri();
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch.

View File

@ -11,6 +11,7 @@ import { nls } from '@theia/core/lib/common/nls';
export class Sketchbook extends Examples {
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
this.configService.onDidChangeSketchDirUri(() => this.update());
}
override async onReady(): Promise<void> {

View File

@ -27,6 +27,7 @@ import {
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { DefaultTheme } from '@theia/application-package/lib/application-props';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import type { FileStat } from '@theia/filesystem/lib/common/files';
export const WINDOW_SETTING = 'window';
export const EDITOR_SETTING = 'editor';
@ -171,7 +172,15 @@ export class SettingsService {
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
this.configService.getConfiguration(),
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
const {
config = {
additionalUrls: [],
sketchDirUri: '',
network: Network.Default(),
},
} = cliConfig;
const { additionalUrls, sketchDirUri, network } = config;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
@ -223,7 +232,11 @@ export class SettingsService {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
let sketchbookStat: FileStat | undefined = undefined;
try {
sketchbookStat = await this.fileService.resolve(new URI(sketchbookDir));
} catch {}
if (!sketchbookStat || !sketchbookStat.isDirectory) {
return nls.localize(
'arduino/preferences/invalid.sketchbook.location',
'Invalid sketchbook location: {0}',
@ -274,10 +287,19 @@ export class SettingsService {
network,
sketchbookShowAllFiles,
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
const [cliConfig, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath),
]);
const { config } = cliConfig;
if (!config) {
// Do not check for any error messages. The config might has errors (such as invalid directories.user) right before saving the new values.
return nls.localize(
'arduino/preferences/noCliConfig',
'Could not load the CLI configuration'
);
}
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
(config as any).network = network;

View File

@ -88,8 +88,25 @@ export class LocalCacheFsProvider
}
protected async init(fileService: FileService): Promise<void> {
const config = await this.configService.getConfiguration();
this._localCacheRoot = new URI(config.dataDirUri);
const { config } = await this.configService.getConfiguration();
// Any possible CLI config errors are ignored here. IDE2 does not verify the `directories.data` folder.
// If the data dir is accessible, IDE2 creates the cache folder for the remote sketches. Otherwise, it does not.
// The data folder can be configured outside of the IDE2, and the new data folder will be picked up with a
// subsequent IDE2 start.
if (!config?.dataDirUri) {
return; // the deferred promise will never resolve
}
const localCacheUri = new URI(config.dataDirUri);
try {
await fileService.access(localCacheUri);
} catch (err) {
console.error(
`'directories.data' location is inaccessible at ${config.dataDirUri}`,
err
);
return;
}
this._localCacheRoot = localCacheUri;
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot);

View File

@ -97,6 +97,11 @@ export namespace ArduinoMenus {
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// `Tool` > `Ports` (always visible https://github.com/arduino/arduino-ide/issues/655)
export const TOOLS__PORTS_SUBMENU = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
// -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc.

View File

@ -18,7 +18,7 @@ import {
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
ConfigState,
Sketch,
ProgressMessage,
} from '../common/protocol';
@ -37,6 +37,7 @@ export class NotificationCenter
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private readonly didReinitializeEmitter = new Emitter<void>();
private readonly indexUpdateDidCompleteEmitter =
new Emitter<IndexUpdateDidCompleteParams>();
private readonly indexUpdateWillStartEmitter =
@ -47,9 +48,7 @@ export class NotificationCenter
new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<{
config: Config | undefined;
}>();
private readonly configDidChangeEmitter = new Emitter<ConfigState>();
private readonly platformDidInstallEmitter = new Emitter<{
item: BoardsPackage;
}>();
@ -71,6 +70,7 @@ export class NotificationCenter
new Emitter<FrontendApplicationState>();
private readonly toDispose = new DisposableCollection(
this.didReinitializeEmitter,
this.indexUpdateWillStartEmitter,
this.indexUpdateDidProgressEmitter,
this.indexUpdateDidCompleteEmitter,
@ -85,6 +85,7 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter
);
readonly onDidReinitialize = this.didReinitializeEmitter.event;
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
@ -115,6 +116,10 @@ export class NotificationCenter
this.toDispose.dispose();
}
notifyDidReinitialize(): void {
this.didReinitializeEmitter.fire();
}
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.indexUpdateWillStartEmitter.fire(params);
}
@ -139,7 +144,7 @@ export class NotificationCenter
this.daemonDidStopEmitter.fire();
}
notifyConfigDidChange(event: { config: Config | undefined }): void {
notifyConfigDidChange(event: ConfigState): void {
this.configDidChangeEmitter.fire(event);
}

View File

@ -1,30 +1,35 @@
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
import { ILogger } from '@theia/core/lib/common/logger';
import { EditorWidget } from '@theia/editor/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../../common/protocol/config-service';
import { ConfigServiceClient } from '../../config/config-service-client';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigServiceClient)
private readonly configService: ConfigServiceClient;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
private dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
this.configService
.getConfiguration()
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
.catch((err) =>
this.logger.error(`Failed to determine the data directory: ${err}`)
);
const fireDidChange = () =>
this.appStateService
.reachedState('ready')
.then(() => this.fireDidChangeDecorations());
this.dataDirUri = this.configService.tryGetDataDirUri();
this.configService.onDidChangeDataDirUri((dataDirUri) => {
this.dataDirUri = dataDirUri;
fireDidChange();
});
if (this.dataDirUri) {
fireDidChange();
}
}
override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {

View File

@ -5,31 +5,23 @@ import {
} from '@theia/core/shared/inversify';
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core';
import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../../common/protocol/config-service';
import { ConfigServiceClient } from '../../config/config-service-client';
import debounce = require('lodash.debounce');
@injectable()
export class ProblemManager extends TheiaProblemManager {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigServiceClient)
private readonly configService: ConfigServiceClient;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
private dataDirUri: URI | undefined;
@postConstruct()
protected override init(): void {
super.init();
this.configService
.getConfiguration()
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
.catch((err) =>
this.logger.error(`Failed to determine the data directory: ${err}`)
);
this.dataDirUri = this.configService.tryGetDataDirUri();
this.configService.onDidChangeDataDirUri((uri) => (this.dataDirUri = uri));
}
override setMarkers(

View File

@ -2,7 +2,7 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
import URI from '@theia/core/lib/common/uri';
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ConfigService } from '../../../common/protocol';
import { ConfigServiceClient } from './../../config/config-service-client';
import { SketchbookTree } from './sketchbook-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
@ -36,8 +36,8 @@ export class SketchbookTreeModel extends FileTreeModel {
@inject(CommandRegistry)
public readonly commandRegistry: CommandRegistry;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigServiceClient)
protected readonly configService: ConfigServiceClient;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@ -59,6 +59,12 @@ export class SketchbookTreeModel extends FileTreeModel {
super.init();
this.reportBusyProgress();
this.initializeRoot();
this.toDispose.push(
this.configService.onDidChangeSketchDirUri(async () => {
await this.updateRoot();
this.selectRoot(this.root);
})
);
}
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
@ -121,6 +127,10 @@ export class SketchbookTreeModel extends FileTreeModel {
return;
}
const root = this.root;
this.selectRoot(root);
}
private selectRoot(root: TreeNode | undefined) {
if (CompositeTreeNode.is(root) && root.children.length === 1) {
const child = root.children[0];
if (
@ -161,10 +171,12 @@ export class SketchbookTreeModel extends FileTreeModel {
}
protected async createRoot(): Promise<TreeNode | undefined> {
const config = await this.configService.getConfiguration();
const rootFileStats = await this.fileService.resolve(
new URI(config.sketchDirUri)
);
const sketchDirUri = this.configService.tryGetSketchDirUri();
const errors = this.configService.tryGetMessages();
if (!sketchDirUri || errors?.length) {
return undefined;
}
const rootFileStats = await this.fileService.resolve(sketchDirUri);
if (this.workspaceService.opened && rootFileStats.children) {
// filter out libraries and hardware

View File

@ -6,10 +6,12 @@ export interface ConfigService {
getVersion(): Promise<
Readonly<{ version: string; commit: string; status?: string }>
>;
getCliConfigFileUri(): Promise<string>;
getConfiguration(): Promise<Config>;
getConfiguration(): Promise<ConfigState>;
setConfiguration(config: Config): Promise<void>;
}
export type ConfigState =
| { config: undefined; messages: string[] }
| { config: Config; messages?: string[] };
export interface Daemon {
readonly port: string | number;
@ -119,7 +121,16 @@ export interface Config {
readonly network: Network;
}
export namespace Config {
export function sameAs(left: Config, right: Config): boolean {
export function sameAs(
left: Config | undefined,
right: Config | undefined
): boolean {
if (!left) {
return !right;
}
if (!right) {
return false;
}
const leftUrls = left.additionalUrls.sort();
const rightUrls = right.additionalUrls.sort();
if (leftUrls.length !== rightUrls.length) {
@ -150,7 +161,16 @@ export namespace AdditionalUrls {
export function stringify(additionalUrls: AdditionalUrls): string {
return additionalUrls.join(',');
}
export function sameAs(left: AdditionalUrls, right: AdditionalUrls): boolean {
export function sameAs(
left: AdditionalUrls | undefined,
right: AdditionalUrls | undefined
): boolean {
if (!left) {
return !right;
}
if (!right) {
return false;
}
if (left.length !== right.length) {
return false;
}

View File

@ -2,7 +2,7 @@ import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-facto
import type {
AttachedBoardsChangeEvent,
BoardsPackage,
Config,
ConfigState,
ProgressMessage,
Sketch,
IndexType,
@ -39,6 +39,11 @@ export interface IndexUpdateDidFailParams extends IndexUpdateParams {
}
export interface NotificationServiceClient {
// The cached state of the core client. Libraries, examples, etc. has been updated.
// This can happen without an index update. For example, changing the `directories.user` location.
// An index update always implicitly involves a re-initialization without notifying via this method.
notifyDidReinitialize(): void;
// Index
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
@ -50,7 +55,7 @@ export interface NotificationServiceClient {
notifyDaemonDidStop(): void;
// CLI config
notifyConfigDidChange(event: { config: Config | undefined }): void;
notifyConfigDidChange(event: ConfigState): void;
// Platforms
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;

View File

@ -3,13 +3,15 @@ import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Sketch, SketchesService } from '../../common/protocol';
import { ConfigService } from './config-service';
import { Sketch, SketchesService } from '.';
import { ConfigServiceClient } from '../../browser/config/config-service-client';
import { SketchContainer, SketchesError, SketchRef } from './sketches-service';
import {
ARDUINO_CLOUD_FOLDER,
@ -34,139 +36,143 @@ export class SketchesServiceClientImpl
implements FrontendApplicationContribution
{
@inject(FileService)
protected readonly fileService: FileService;
@inject(MessageService)
protected readonly messageService: MessageService;
private readonly fileService: FileService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
private readonly sketchService: SketchesService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ConfigService)
protected readonly configService: ConfigService;
private readonly workspaceService: WorkspaceService;
@inject(ConfigServiceClient)
private readonly configService: ConfigServiceClient;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected sketches = new Map<string, SketchRef>();
// TODO: rename this + event to the `onBlabla` pattern
protected sketchbookDidChangeEmitter = new Emitter<{
private sketches = new Map<string, SketchRef>();
private onSketchbookDidChangeEmitter = new Emitter<{
created: SketchRef[];
removed: SketchRef[];
}>();
readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event;
protected currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
readonly onSketchbookDidChange = this.onSketchbookDidChangeEmitter.event;
private currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
protected toDispose = new DisposableCollection(
this.sketchbookDidChangeEmitter,
this.currentSketchDidChangeEmitter
private toDisposeBeforeWatchSketchbookDir = new DisposableCollection();
private toDispose = new DisposableCollection(
this.onSketchbookDidChangeEmitter,
this.currentSketchDidChangeEmitter,
this.toDisposeBeforeWatchSketchbookDir
);
private _currentSketch: CurrentSketch | undefined;
private currentSketchLoaded = new Deferred<CurrentSketch>();
onStart(): void {
this.configService.getConfiguration().then(({ sketchDirUri }) => {
this.sketchService
.getSketches({ uri: sketchDirUri })
.then((container) => {
const sketchbookUri = new URI(sketchDirUri);
for (const sketch of SketchContainer.toArray(container)) {
this.sketches.set(sketch.uri, sketch);
}
this.toDispose.push(
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
this.fileService.watch(new URI(sketchDirUri), {
recursive: true,
excludes: [],
})
);
this.toDispose.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
// The file change events have higher precedence in the current sketch over the sketchbook.
if (
CurrentSketch.isValid(this._currentSketch) &&
new URI(this._currentSketch.uri).isEqualOrParent(resource)
) {
// https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656
// On a sketch file rename, the FS watcher will contain two changes:
// - Deletion of the original file,
// - Update of the new file,
// Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event.
// Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2.
if (
type === FileChangeType.UPDATED &&
event.changes.length === 1
) {
// If the event contains only one `UPDATE` change, it cannot be a rename.
return;
}
let reloadedSketch: Sketch | undefined = undefined;
try {
reloadedSketch = await this.sketchService.loadSketch(
this._currentSketch.uri
);
} catch (err) {
if (!SketchesError.NotFound.is(err)) {
throw err;
}
}
if (!reloadedSketch) {
return;
}
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
this.useCurrentSketch(reloadedSketch, true);
}
return;
}
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
if (sketchbookUri.isEqualOrParent(resource)) {
if (Sketch.isSketchFile(resource)) {
if (type === FileChangeType.ADDED) {
try {
const toAdd = await this.sketchService.loadSketch(
resource.parent.toString()
);
if (!this.sketches.has(toAdd.uri)) {
console.log(
`New sketch '${toAdd.name}' was created in sketchbook '${sketchDirUri}'.`
);
this.sketches.set(toAdd.uri, toAdd);
this.fireSoon(toAdd, 'created');
}
} catch {}
} else if (type === FileChangeType.DELETED) {
const uri = resource.parent.toString();
const toDelete = this.sketches.get(uri);
if (toDelete) {
console.log(
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookUri}'.`
);
this.sketches.delete(uri);
this.fireSoon(toDelete, 'removed');
}
}
}
}
}
})
);
});
});
const sketchDirUri = this.configService.tryGetSketchDirUri();
this.watchSketchbookDir(sketchDirUri);
const refreshCurrentSketch = async () => {
const currentSketch = await this.loadCurrentSketch();
this.useCurrentSketch(currentSketch);
};
this.toDispose.push(
this.configService.onDidChangeSketchDirUri((sketchDirUri) => {
this.watchSketchbookDir(sketchDirUri);
refreshCurrentSketch();
})
);
this.appStateService
.reachedState('started_contributions')
.then(async () => {
const currentSketch = await this.loadCurrentSketch();
this.useCurrentSketch(currentSketch);
});
.then(refreshCurrentSketch);
}
private async watchSketchbookDir(
sketchDirUri: URI | undefined
): Promise<void> {
this.toDisposeBeforeWatchSketchbookDir.dispose();
if (!sketchDirUri) {
return;
}
const container = await this.sketchService.getSketches({
uri: sketchDirUri.toString(),
});
for (const sketch of SketchContainer.toArray(container)) {
this.sketches.set(sketch.uri, sketch);
}
this.toDisposeBeforeWatchSketchbookDir.pushAll([
Disposable.create(() => this.sketches.clear()),
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
this.fileService.watch(sketchDirUri, {
recursive: true,
excludes: [],
}),
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
// The file change events have higher precedence in the current sketch over the sketchbook.
if (
CurrentSketch.isValid(this._currentSketch) &&
new URI(this._currentSketch.uri).isEqualOrParent(resource)
) {
// https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656
// On a sketch file rename, the FS watcher will contain two changes:
// - Deletion of the original file,
// - Update of the new file,
// Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event.
// Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2.
if (type === FileChangeType.UPDATED && event.changes.length === 1) {
// If the event contains only one `UPDATE` change, it cannot be a rename.
return;
}
let reloadedSketch: Sketch | undefined = undefined;
try {
reloadedSketch = await this.sketchService.loadSketch(
this._currentSketch.uri
);
} catch (err) {
if (!SketchesError.NotFound.is(err)) {
throw err;
}
}
if (!reloadedSketch) {
return;
}
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
this.useCurrentSketch(reloadedSketch, true);
}
return;
}
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
if (sketchDirUri.isEqualOrParent(resource)) {
if (Sketch.isSketchFile(resource)) {
if (type === FileChangeType.ADDED) {
try {
const toAdd = await this.sketchService.loadSketch(
resource.parent.toString()
);
if (!this.sketches.has(toAdd.uri)) {
console.log(
`New sketch '${toAdd.name}' was created in sketchbook '${sketchDirUri}'.`
);
this.sketches.set(toAdd.uri, toAdd);
this.fireSoon(toAdd, 'created');
}
} catch {}
} else if (type === FileChangeType.DELETED) {
const uri = resource.parent.toString();
const toDelete = this.sketches.get(uri);
if (toDelete) {
console.log(
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchDirUri}'.`
);
this.sketches.delete(uri);
this.fireSoon(toDelete, 'removed');
}
}
}
}
}
}),
]);
}
private useCurrentSketch(
@ -249,7 +255,7 @@ export class SketchesServiceClientImpl
event.removed.push(sketch);
}
}
this.sketchbookDidChangeEmitter.fire(event);
this.onSketchbookDidChangeEmitter.fire(event);
this.bufferedSketchbookEvents.length = 0;
}, 100);
}

View File

@ -180,7 +180,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
if (
CompoundMenuNode.is(menu) &&
menu.children.length &&
this.visibleSubmenu(menu) && // customization for #569 and #655
this.undefinedOrMatch(menu.when, options.context)
) {
const role = CompoundMenuNode.getRole(menu);
@ -193,10 +193,17 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
this.fillMenuTemplate(myItems, child, args, options)
);
if (myItems.length === 0) {
return parentItems;
// customization for #569 and #655
if (!this.visibleLeafSubmenu(menu)) {
return parentItems;
}
}
if (role === CompoundMenuNodeRole.Submenu) {
parentItems.push({ label: menu.label, submenu: myItems });
parentItems.push({
label: menu.label,
submenu: myItems,
enabled: !this.visibleLeafSubmenu(menu), // customization for #569 and #655
});
} else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') {
if (
parentItems.length &&
@ -278,4 +285,31 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
}
return parentItems;
}
/**
* `true` if either has at least `children`, or was forced to be visible.
*/
private visibleSubmenu(node: MenuNode & CompoundMenuNode): boolean {
return node.children.length > 0 || this.visibleLeafSubmenu(node);
}
/**
* The node is a visible submenu if is a compound node but has zero children.
*/
private visibleLeafSubmenu(node: MenuNode): boolean {
if (CompoundMenuNode.is(node)) {
return (
node.children.length === 0 &&
AlwaysVisibleSubmenus.findIndex(
(menuPath) => menuPath[menuPath.length - 1] === node.id
) >= 0
);
}
return false;
}
}
const AlwaysVisibleSubmenus: MenuPath[] = [
ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, // #569
];

View File

@ -79,9 +79,12 @@ export class ClangFormatter implements Formatter {
return `-style="${style(toClangOptions(options))}"`;
}
private async dataDirPath(): Promise<string> {
const { dataDirUri } = await this.configService.getConfiguration();
return FileUri.fsPath(dataDirUri);
private async dataDirPath(): Promise<string | undefined> {
const { config } = await this.configService.getConfiguration();
if (!config?.dataDirUri) {
return undefined;
}
return FileUri.fsPath(config.dataDirUri);
}
private async configDirPath(): Promise<string> {
@ -90,9 +93,13 @@ export class ClangFormatter implements Formatter {
}
private async clangConfigPath(
folderUri: MaybePromise<string>
folderUri: MaybePromise<string | undefined>
): Promise<string | undefined> {
const folderPath = FileUri.fsPath(await folderUri);
const uri = await folderUri;
if (!uri) {
return undefined;
}
const folderPath = FileUri.fsPath(uri);
const clangFormatPath = join(folderPath, ClangFormatFile);
try {
await fs.access(clangFormatPath, constants.R_OK);

View File

@ -13,6 +13,7 @@ import {
Config,
NotificationServiceServer,
Network,
ConfigState,
} from '../common/protocol';
import { spawnCommand } from './exec-util';
import {
@ -25,7 +26,7 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { deepClone } from '@theia/core';
import { deepClone, nls } from '@theia/core';
import { ErrnoException } from './utils/errors';
const deepmerge = require('deepmerge');
@ -36,46 +37,38 @@ export class ConfigServiceImpl
{
@inject(ILogger)
@named('config')
protected readonly logger: ILogger;
private readonly logger: ILogger;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
private readonly envVariablesServer: EnvVariablesServer;
@inject(ArduinoDaemonImpl)
protected readonly daemon: ArduinoDaemonImpl;
private readonly daemon: ArduinoDaemonImpl;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
private readonly notificationService: NotificationServiceServer;
protected config: Config;
protected cliConfig: DefaultCliConfig | undefined;
protected ready = new Deferred<void>();
protected readonly configChangeEmitter = new Emitter<Config>();
private config: ConfigState = {
config: undefined,
messages: ['uninitialized'],
};
private cliConfig: DefaultCliConfig | undefined;
private ready = new Deferred<void>();
private readonly configChangeEmitter = new Emitter<{
oldState: ConfigState;
newState: ConfigState;
}>();
onStart(): void {
this.loadCliConfig().then(async (cliConfig) => {
this.cliConfig = cliConfig;
if (this.cliConfig) {
const [config] = await Promise.all([
this.mapCliConfigToAppConfig(this.cliConfig),
this.ensureUserDirExists(this.cliConfig),
]);
if (config) {
this.config = config;
this.ready.resolve();
return;
}
}
this.fireInvalidConfig();
});
this.initConfig();
}
async getCliConfigFileUri(): Promise<string> {
private async getCliConfigFileUri(): Promise<string> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
}
async getConfiguration(): Promise<Config> {
async getConfiguration(): Promise<ConfigState> {
await this.ready.promise;
return { ...this.config };
}
@ -83,9 +76,10 @@ export class ConfigServiceImpl
// Used by frontend to update the config.
async setConfiguration(config: Config): Promise<void> {
await this.ready.promise;
if (Config.sameAs(this.config, config)) {
if (Config.sameAs(this.config.config, config)) {
return;
}
const oldConfigState = deepClone(this.config);
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
this.cliConfig
);
@ -110,16 +104,30 @@ export class ConfigServiceImpl
await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port);
this.config = deepClone(config);
this.config.config = deepClone(config);
this.cliConfig = copyDefaultCliConfig;
this.fireConfigChanged(this.config);
try {
await this.validateCliConfig(this.cliConfig);
delete this.config.messages;
this.fireConfigChanged(oldConfigState, this.config);
} catch (err) {
if (err instanceof InvalidConfigError) {
this.config.messages = err.errors;
this.fireConfigChanged(oldConfigState, this.config);
} else {
throw err;
}
}
}
get cliConfiguration(): DefaultCliConfig | undefined {
return this.cliConfig;
}
get onConfigChange(): Event<Config> {
get onConfigChange(): Event<{
oldState: ConfigState;
newState: ConfigState;
}> {
return this.configChangeEmitter.event;
}
@ -129,9 +137,42 @@ export class ConfigServiceImpl
return this.daemon.getVersion();
}
protected async loadCliConfig(
private async initConfig(): Promise<void> {
try {
const cliConfig = await this.loadCliConfig();
this.cliConfig = cliConfig;
const [config] = await Promise.all([
this.mapCliConfigToAppConfig(this.cliConfig),
this.ensureUserDirExists(this.cliConfig).catch((reason) => {
if (reason instanceof Error) {
this.logger.warn(
`Could not ensure user directory existence: ${this.cliConfig?.directories.user}`,
reason
);
}
// NOOP. Try to create the folder if missing but swallow any errors.
// The validation will take care of the missing location handling.
}),
]);
this.config.config = config;
await this.validateCliConfig(this.cliConfig);
delete this.config.messages;
if (config) {
this.ready.resolve();
return;
}
} catch (err: unknown) {
this.logger.error('Failed to initialize the CLI configuration.', err);
if (err instanceof InvalidConfigError) {
this.config.messages = err.errors;
this.ready.resolve();
}
}
}
private async loadCliConfig(
initializeIfAbsent = true
): Promise<DefaultCliConfig | undefined> {
): Promise<DefaultCliConfig> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
try {
@ -157,7 +198,7 @@ export class ConfigServiceImpl
}
}
protected async getFallbackCliConfig(): Promise<DefaultCliConfig> {
private async getFallbackCliConfig(): Promise<DefaultCliConfig> {
const cliPath = await this.daemon.getExecPath();
const rawJson = await spawnCommand(`"${cliPath}"`, [
'config',
@ -168,7 +209,7 @@ export class ConfigServiceImpl
return JSON.parse(rawJson);
}
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
private async initCliConfigTo(fsPathToDir: string): Promise<void> {
const cliPath = await this.daemon.getExecPath();
await spawnCommand(`"${cliPath}"`, [
'config',
@ -178,7 +219,7 @@ export class ConfigServiceImpl
]);
}
protected async mapCliConfigToAppConfig(
private async mapCliConfigToAppConfig(
cliConfig: DefaultCliConfig
): Promise<Config> {
const { directories, locale = 'en' } = cliConfig;
@ -199,16 +240,45 @@ export class ConfigServiceImpl
};
}
protected fireConfigChanged(config: Config): void {
this.configChangeEmitter.fire(config);
this.notificationService.notifyConfigDidChange({ config });
private fireConfigChanged(
oldState: ConfigState,
newState: ConfigState
): void {
this.configChangeEmitter.fire({ oldState, newState });
this.notificationService.notifyConfigDidChange(newState);
}
protected fireInvalidConfig(): void {
this.notificationService.notifyConfigDidChange({ config: undefined });
private async validateCliConfig(config: DefaultCliConfig): Promise<void> {
const errors: string[] = [];
errors.push(...(await this.checkAccessible(config)));
if (errors.length) {
throw new InvalidConfigError(errors);
}
}
protected async updateDaemon(
private async checkAccessible({
directories,
}: DefaultCliConfig): Promise<string[]> {
try {
await fs.readdir(directories.user);
return [];
} catch (err) {
console.error(
`Check accessible failed for input: ${directories.user}`,
err
);
return [
nls.localize(
'arduino/configuration/cli/inaccessibleDirectory',
"Could not access the sketchbook location at '{0}': {1}",
directories.user,
String(err)
),
];
}
}
private async updateDaemon(
port: string | number,
config: DefaultCliConfig
): Promise<void> {
@ -216,7 +286,7 @@ export class ConfigServiceImpl
const req = new MergeRequest();
const json = JSON.stringify(config, null, 2);
req.setJsonData(json);
console.log(`Updating daemon with 'data': ${json}`);
this.logger.info(`Updating daemon with 'data': ${json}`);
return new Promise<void>((resolve, reject) => {
client.merge(req, (error) => {
try {
@ -232,7 +302,7 @@ export class ConfigServiceImpl
});
}
protected async writeDaemonState(port: string | number): Promise<void> {
private async writeDaemonState(port: string | number): Promise<void> {
const client = this.createClient(port);
const req = new WriteRequest();
const cliConfigUri = await this.getCliConfigFileUri();
@ -273,3 +343,13 @@ export class ConfigServiceImpl
await fs.mkdir(cliConfig.directories.user, { recursive: true });
}
}
class InvalidConfigError extends Error {
constructor(readonly errors: string[]) {
super('InvalidConfigError:\n - ' + errors.join('\n - '));
if (!errors.length) {
throw new Error("Illegal argument: 'messages'. It must not be empty.");
}
Object.setPrototypeOf(this, InvalidConfigError.prototype);
}
}

View File

@ -25,6 +25,7 @@ import {
IndexUpdateDidFailParams,
IndexUpdateWillStartParams,
NotificationServiceServer,
AdditionalUrls,
} from '../common/protocol';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
@ -75,9 +76,27 @@ export class CoreClientProvider {
});
this.daemon.onDaemonStarted((port) => this.create(port));
this.daemon.onDaemonStopped(() => this.closeClient());
this.configService.onConfigChange(
() => this.client.then((client) => this.updateIndex(client, ['platform'])) // Assuming 3rd party URL changes. No library index update is required.
);
this.configService.onConfigChange(async ({ oldState, newState }) => {
if (
!AdditionalUrls.sameAs(
oldState.config?.additionalUrls,
newState.config?.additionalUrls
)
) {
const client = await this.client;
this.updateIndex(client, ['platform']);
} else if (
!!newState.config?.sketchDirUri &&
oldState.config?.sketchDirUri !== newState.config.sketchDirUri
) {
// If the sketchbook location has changed, the custom libraries has changed.
// Reinitialize the core client and fire an event so that the frontend can refresh.
// https://github.com/arduino/arduino-ide/issues/796 (see the file > examples and sketch > include examples)
const client = await this.client;
await this.initInstance(client);
this.notificationService.notifyDidReinitialize();
}
});
}
get tryGetClient(): CoreClientProvider.Client | undefined {

View File

@ -5,7 +5,7 @@ import type {
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
ConfigState,
Sketch,
ProgressMessage,
IndexUpdateWillStartParams,
@ -19,6 +19,10 @@ export class NotificationServiceServerImpl
{
private readonly clients: NotificationServiceClient[] = [];
notifyDidReinitialize(): void {
this.clients.forEach((client) => client.notifyDidReinitialize());
}
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.clients.forEach((client) => client.notifyIndexUpdateWillStart(params));
}
@ -69,7 +73,7 @@ export class NotificationServiceServerImpl
);
}
notifyConfigDidChange(event: { config: Config | undefined }): void {
notifyConfigDidChange(event: ConfigState): void {
this.clients.forEach((client) => client.notifyConfigDidChange(event));
}

View File

@ -80,6 +80,15 @@ export class SketchesServiceImpl
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
const root = await this.root(uri);
if (!root) {
this.logger.warn(`Could not derive sketchbook root from ${uri}.`);
return SketchContainer.create('');
}
const exists = await this.exists(root);
if (!exists) {
this.logger.warn(`Sketchbook root ${root} does not exist.`);
return SketchContainer.create('');
}
const pathToAllSketchFiles = await new Promise<string[]>(
(resolve, reject) => {
glob(
@ -179,13 +188,23 @@ export class SketchesServiceImpl
return container;
}
private async root(uri?: string | undefined): Promise<string> {
return FileUri.fsPath(uri ?? (await this.sketchbookUri()));
private async root(uri?: string | undefined): Promise<string | undefined> {
if (uri) {
return FileUri.fsPath(uri);
}
const sketchbookUri = await this.sketchbookUri();
if (sketchbookUri) {
return FileUri.fsPath(sketchbookUri);
}
return undefined;
}
private async sketchbookUri(): Promise<string> {
const { sketchDirUri } = await this.configService.getConfiguration();
return sketchDirUri;
private async sketchbookUri(): Promise<string | undefined> {
const { config, messages } = await this.configService.getConfiguration();
if (!config?.sketchDirUri || messages?.length) {
return undefined;
}
return config.sketchDirUri;
}
async loadSketch(uri: string): Promise<SketchWithDetails> {
@ -454,8 +473,10 @@ export class SketchesServiceImpl
const sketchBaseName = `sketch_${
monthNames[today.getMonth()]
}${today.getDate()}`;
const config = await this.configService.getConfiguration();
const sketchbookPath = FileUri.fsPath(config.sketchDirUri);
const { config } = await this.configService.getConfiguration();
const sketchbookPath = config?.sketchDirUri
? FileUri.fsPath(config?.sketchDirUri)
: os.homedir();
let sketchName: string | undefined;
// If it's another day, reset the count of sketches created today

View File

@ -157,6 +157,11 @@
"uninstallMsg": "Do you want to uninstall {0}?",
"version": "Version {0}"
},
"configuration": {
"cli": {
"inaccessibleDirectory": "Could not access the sketchbook location at '{0}': {1}"
}
},
"contributions": {
"addFile": "Add File",
"fileAdded": "One file added to the sketch.",
@ -352,6 +357,7 @@
"manualProxy": "Manual proxy configuration",
"network": "Network",
"newSketchbookLocation": "Select new sketchbook location",
"noCliConfig": "Could not load the CLI configuration",
"noProxy": "No proxy",
"proxySettings": {
"hostname": "Host name",