mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-15 07:16:38 +00:00
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:
parent
3f05396222
commit
76f9f635d8
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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(),
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
|
@ -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(),
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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[] {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,58 +36,73 @@ 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);
|
||||
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(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.toDispose.push(
|
||||
this.toDisposeBeforeWatchSketchbookDir.pushAll([
|
||||
Disposable.create(() => this.sketches.clear()),
|
||||
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
|
||||
this.fileService.watch(new URI(sketchDirUri), {
|
||||
this.fileService.watch(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.
|
||||
@ -99,10 +116,7 @@ export class SketchesServiceClientImpl
|
||||
// - 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 (type === FileChangeType.UPDATED && event.changes.length === 1) {
|
||||
// If the event contains only one `UPDATE` change, it cannot be a rename.
|
||||
return;
|
||||
}
|
||||
@ -128,7 +142,7 @@ export class SketchesServiceClientImpl
|
||||
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 (sketchDirUri.isEqualOrParent(resource)) {
|
||||
if (Sketch.isSketchFile(resource)) {
|
||||
if (type === FileChangeType.ADDED) {
|
||||
try {
|
||||
@ -148,7 +162,7 @@ export class SketchesServiceClientImpl
|
||||
const toDelete = this.sketches.get(uri);
|
||||
if (toDelete) {
|
||||
console.log(
|
||||
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookUri}'.`
|
||||
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchDirUri}'.`
|
||||
);
|
||||
this.sketches.delete(uri);
|
||||
this.fireSoon(toDelete, 'removed');
|
||||
@ -157,16 +171,8 @@ export class SketchesServiceClientImpl
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
this.appStateService
|
||||
.reachedState('started_contributions')
|
||||
.then(async () => {
|
||||
const currentSketch = await this.loadCurrentSketch();
|
||||
this.useCurrentSketch(currentSketch);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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) {
|
||||
// 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
|
||||
];
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user