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 { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
|
||||||
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
|
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
|
||||||
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-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) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
// Commands and toolbar items
|
// Commands and toolbar items
|
||||||
@ -404,6 +405,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.inSingletonScope();
|
.inSingletonScope();
|
||||||
|
bind(ConfigServiceClient).toSelf().inSingletonScope();
|
||||||
|
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
|
||||||
|
|
||||||
// Boards service
|
// Boards service
|
||||||
bind(BoardsService)
|
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 * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
@ -16,9 +15,6 @@ import { nls } from '@theia/core/lib/common';
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class AddZipLibrary extends SketchContribution {
|
export class AddZipLibrary extends SketchContribution {
|
||||||
@inject(EnvVariablesServer)
|
|
||||||
private readonly envVariableServer: EnvVariablesServer;
|
|
||||||
|
|
||||||
@inject(ResponseServiceClient)
|
@inject(ResponseServiceClient)
|
||||||
private readonly responseService: ResponseServiceClient;
|
private readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import * as dateFormat from 'dateformat';
|
import * as dateFormat from 'dateformat';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
SketchContribution,
|
||||||
@ -29,10 +28,7 @@ export class ArchiveSketch extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async archiveSketch(): Promise<void> {
|
private async archiveSketch(): Promise<void> {
|
||||||
const [sketch, config] = await Promise.all([
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
this.sketchServiceClient.currentSketch(),
|
|
||||||
this.configService.getConfiguration(),
|
|
||||||
]);
|
|
||||||
if (!CurrentSketch.isValid(sketch)) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -40,9 +36,9 @@ export class ArchiveSketch extends SketchContribution {
|
|||||||
new Date(),
|
new Date(),
|
||||||
'yymmdd'
|
'yymmdd'
|
||||||
)}a.zip`;
|
)}a.zip`;
|
||||||
const defaultPath = await this.fileService.fsPath(
|
const defaultContainerUri = await this.defaultUri();
|
||||||
new URI(config.sketchDirUri).resolve(archiveBasename)
|
const defaultUri = defaultContainerUri.resolve(archiveBasename);
|
||||||
);
|
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||||
remote.getCurrentWindow(),
|
remote.getCurrentWindow(),
|
||||||
{
|
{
|
||||||
|
@ -155,10 +155,7 @@ PID: ${PID}`;
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ports submenu
|
// Ports submenu
|
||||||
const portsSubmenuPath = [
|
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
|
||||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
|
||||||
'2_ports',
|
|
||||||
];
|
|
||||||
const portsSubmenuLabel = config.selectedPort?.address;
|
const portsSubmenuLabel = config.selectedPort?.address;
|
||||||
this.menuModelRegistry.registerSubmenu(
|
this.menuModelRegistry.registerSubmenu(
|
||||||
portsSubmenuPath,
|
portsSubmenuPath,
|
||||||
|
@ -12,6 +12,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
|||||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
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 { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -43,7 +44,6 @@ import {
|
|||||||
} from '../../common/protocol/sketches-service-client-impl';
|
} from '../../common/protocol/sketches-service-client-impl';
|
||||||
import {
|
import {
|
||||||
SketchesService,
|
SketchesService,
|
||||||
ConfigService,
|
|
||||||
FileSystemExt,
|
FileSystemExt,
|
||||||
Sketch,
|
Sketch,
|
||||||
CoreService,
|
CoreService,
|
||||||
@ -62,6 +62,7 @@ import { NotificationManager } from '../theia/messages/notifications-manager';
|
|||||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||||
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
|
import { ConfigServiceClient } from '../config/config-service-client';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
@ -142,8 +143,8 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
@inject(FileSystemExt)
|
@inject(FileSystemExt)
|
||||||
protected readonly fileSystemExt: FileSystemExt;
|
protected readonly fileSystemExt: FileSystemExt;
|
||||||
|
|
||||||
@inject(ConfigService)
|
@inject(ConfigServiceClient)
|
||||||
protected readonly configService: ConfigService;
|
protected readonly configService: ConfigServiceClient;
|
||||||
|
|
||||||
@inject(SketchesService)
|
@inject(SketchesService)
|
||||||
protected readonly sketchService: SketchesService;
|
protected readonly sketchService: SketchesService;
|
||||||
@ -160,6 +161,9 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
@inject(OutputChannelManager)
|
@inject(OutputChannelManager)
|
||||||
protected readonly outputChannelManager: OutputChannelManager;
|
protected readonly outputChannelManager: OutputChannelManager;
|
||||||
|
|
||||||
|
@inject(EnvVariablesServer)
|
||||||
|
protected readonly envVariableServer: EnvVariablesServer;
|
||||||
|
|
||||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||||
const override: Record<string, string> = {};
|
const override: Record<string, string> = {};
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
@ -173,6 +177,25 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
}
|
}
|
||||||
return override;
|
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()
|
@injectable()
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
CoreService,
|
CoreService,
|
||||||
} from '../../common/protocol';
|
} from '../../common/protocol';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class Examples extends SketchContribution {
|
export abstract class Examples extends SketchContribution {
|
||||||
@ -36,7 +37,7 @@ export abstract class Examples extends SketchContribution {
|
|||||||
private readonly commandRegistry: CommandRegistry;
|
private readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
private readonly menuRegistry: MenuModelRegistry;
|
protected readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(ExamplesService)
|
@inject(ExamplesService)
|
||||||
protected readonly examplesService: ExamplesService;
|
protected readonly examplesService: ExamplesService;
|
||||||
@ -47,6 +48,9 @@ export abstract class Examples extends SketchContribution {
|
|||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(NotificationCenter)
|
||||||
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly toDispose = new DisposableCollection();
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
|
||||||
protected override init(): void {
|
protected override init(): void {
|
||||||
@ -54,6 +58,12 @@ export abstract class Examples extends SketchContribution {
|
|||||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||||
this.handleBoardChanged(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
|
// 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;
|
const { label } = sketchContainerOrPlaceholder;
|
||||||
submenuPath = [...menuPath, label];
|
submenuPath = [...menuPath, label];
|
||||||
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
||||||
|
this.toDispose.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
unregisterSubmenu(submenuPath, this.menuRegistry)
|
||||||
|
)
|
||||||
|
);
|
||||||
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
||||||
children.push(...sketchContainerOrPlaceholder.children);
|
children.push(...sketchContainerOrPlaceholder.children);
|
||||||
} else {
|
} else {
|
||||||
@ -239,9 +254,6 @@ export class BuiltInExamples extends Examples {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LibraryExamples extends Examples {
|
export class LibraryExamples extends Examples {
|
||||||
@inject(NotificationCenter)
|
|
||||||
private readonly notificationCenter: NotificationCenter;
|
|
||||||
|
|
||||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
|
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
|
@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||||
this.updateMenuActions()
|
this.updateMenuActions()
|
||||||
);
|
);
|
||||||
|
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
|
@ -82,10 +82,7 @@ export class OpenSketch extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async selectSketch(): Promise<Sketch | undefined> {
|
private async selectSketch(): Promise<Sketch | undefined> {
|
||||||
const config = await this.configService.getConfiguration();
|
const defaultPath = await this.defaultPath();
|
||||||
const defaultPath = await this.fileService.fsPath(
|
|
||||||
new URI(config.sketchDirUri)
|
|
||||||
);
|
|
||||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||||
remote.getCurrentWindow(),
|
remote.getCurrentWindow(),
|
||||||
{
|
{
|
||||||
|
@ -58,10 +58,7 @@ export class SaveAsSketch extends SketchContribution {
|
|||||||
markAsRecentlyOpened,
|
markAsRecentlyOpened,
|
||||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const [sketch, configuration] = await Promise.all([
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
this.sketchServiceClient.currentSketch(),
|
|
||||||
this.configService.getConfiguration(),
|
|
||||||
]);
|
|
||||||
if (!CurrentSketch.isValid(sketch)) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -72,7 +69,7 @@ export class SaveAsSketch extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sketchUri = new URI(sketch.uri);
|
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 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.
|
// 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.
|
// 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 {
|
export class Sketchbook extends Examples {
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||||
|
this.configService.onDidChangeSketchDirUri(() => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||||
import { DefaultTheme } from '@theia/application-package/lib/application-props';
|
import { DefaultTheme } from '@theia/application-package/lib/application-props';
|
||||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
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 WINDOW_SETTING = 'window';
|
||||||
export const EDITOR_SETTING = 'editor';
|
export const EDITOR_SETTING = 'editor';
|
||||||
@ -171,7 +172,15 @@ export class SettingsService {
|
|||||||
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
|
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
|
||||||
this.configService.getConfiguration(),
|
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));
|
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
|
||||||
return {
|
return {
|
||||||
editorFontSize,
|
editorFontSize,
|
||||||
@ -223,7 +232,11 @@ export class SettingsService {
|
|||||||
try {
|
try {
|
||||||
const { sketchbookPath, editorFontSize, themeId } = await settings;
|
const { sketchbookPath, editorFontSize, themeId } = await settings;
|
||||||
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
|
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(
|
return nls.localize(
|
||||||
'arduino/preferences/invalid.sketchbook.location',
|
'arduino/preferences/invalid.sketchbook.location',
|
||||||
'Invalid sketchbook location: {0}',
|
'Invalid sketchbook location: {0}',
|
||||||
@ -274,10 +287,19 @@ export class SettingsService {
|
|||||||
network,
|
network,
|
||||||
sketchbookShowAllFiles,
|
sketchbookShowAllFiles,
|
||||||
} = this._settings;
|
} = this._settings;
|
||||||
const [config, sketchDirUri] = await Promise.all([
|
const [cliConfig, sketchDirUri] = await Promise.all([
|
||||||
this.configService.getConfiguration(),
|
this.configService.getConfiguration(),
|
||||||
this.fileSystemExt.getUri(sketchbookPath),
|
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).additionalUrls = additionalUrls;
|
||||||
(config as any).sketchDirUri = sketchDirUri;
|
(config as any).sketchDirUri = sketchDirUri;
|
||||||
(config as any).network = network;
|
(config as any).network = network;
|
||||||
|
@ -88,8 +88,25 @@ export class LocalCacheFsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async init(fileService: FileService): Promise<void> {
|
protected async init(fileService: FileService): Promise<void> {
|
||||||
const config = await this.configService.getConfiguration();
|
const { config } = await this.configService.getConfiguration();
|
||||||
this._localCacheRoot = new URI(config.dataDirUri);
|
// 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']) {
|
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
|
||||||
this._localCacheRoot = this._localCacheRoot.resolve(segment);
|
this._localCacheRoot = this._localCacheRoot.resolve(segment);
|
||||||
await fileService.createFolder(this._localCacheRoot);
|
await fileService.createFolder(this._localCacheRoot);
|
||||||
|
@ -97,6 +97,11 @@ export namespace ArduinoMenus {
|
|||||||
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
|
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
|
||||||
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
||||||
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
|
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
|
// -- Help
|
||||||
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
|
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
AttachedBoardsChangeEvent,
|
AttachedBoardsChangeEvent,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
Config,
|
ConfigState,
|
||||||
Sketch,
|
Sketch,
|
||||||
ProgressMessage,
|
ProgressMessage,
|
||||||
} from '../common/protocol';
|
} from '../common/protocol';
|
||||||
@ -37,6 +37,7 @@ export class NotificationCenter
|
|||||||
@inject(FrontendApplicationStateService)
|
@inject(FrontendApplicationStateService)
|
||||||
private readonly appStateService: FrontendApplicationStateService;
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
|
private readonly didReinitializeEmitter = new Emitter<void>();
|
||||||
private readonly indexUpdateDidCompleteEmitter =
|
private readonly indexUpdateDidCompleteEmitter =
|
||||||
new Emitter<IndexUpdateDidCompleteParams>();
|
new Emitter<IndexUpdateDidCompleteParams>();
|
||||||
private readonly indexUpdateWillStartEmitter =
|
private readonly indexUpdateWillStartEmitter =
|
||||||
@ -47,9 +48,7 @@ export class NotificationCenter
|
|||||||
new Emitter<IndexUpdateDidFailParams>();
|
new Emitter<IndexUpdateDidFailParams>();
|
||||||
private readonly daemonDidStartEmitter = new Emitter<string>();
|
private readonly daemonDidStartEmitter = new Emitter<string>();
|
||||||
private readonly daemonDidStopEmitter = new Emitter<void>();
|
private readonly daemonDidStopEmitter = new Emitter<void>();
|
||||||
private readonly configDidChangeEmitter = new Emitter<{
|
private readonly configDidChangeEmitter = new Emitter<ConfigState>();
|
||||||
config: Config | undefined;
|
|
||||||
}>();
|
|
||||||
private readonly platformDidInstallEmitter = new Emitter<{
|
private readonly platformDidInstallEmitter = new Emitter<{
|
||||||
item: BoardsPackage;
|
item: BoardsPackage;
|
||||||
}>();
|
}>();
|
||||||
@ -71,6 +70,7 @@ export class NotificationCenter
|
|||||||
new Emitter<FrontendApplicationState>();
|
new Emitter<FrontendApplicationState>();
|
||||||
|
|
||||||
private readonly toDispose = new DisposableCollection(
|
private readonly toDispose = new DisposableCollection(
|
||||||
|
this.didReinitializeEmitter,
|
||||||
this.indexUpdateWillStartEmitter,
|
this.indexUpdateWillStartEmitter,
|
||||||
this.indexUpdateDidProgressEmitter,
|
this.indexUpdateDidProgressEmitter,
|
||||||
this.indexUpdateDidCompleteEmitter,
|
this.indexUpdateDidCompleteEmitter,
|
||||||
@ -85,6 +85,7 @@ export class NotificationCenter
|
|||||||
this.attachedBoardsDidChangeEmitter
|
this.attachedBoardsDidChangeEmitter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
readonly onDidReinitialize = this.didReinitializeEmitter.event;
|
||||||
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
|
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
|
||||||
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
|
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
|
||||||
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
||||||
@ -115,6 +116,10 @@ export class NotificationCenter
|
|||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyDidReinitialize(): void {
|
||||||
|
this.didReinitializeEmitter.fire();
|
||||||
|
}
|
||||||
|
|
||||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||||
this.indexUpdateWillStartEmitter.fire(params);
|
this.indexUpdateWillStartEmitter.fire(params);
|
||||||
}
|
}
|
||||||
@ -139,7 +144,7 @@ export class NotificationCenter
|
|||||||
this.daemonDidStopEmitter.fire();
|
this.daemonDidStopEmitter.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyConfigDidChange(event: { config: Config | undefined }): void {
|
notifyConfigDidChange(event: ConfigState): void {
|
||||||
this.configDidChangeEmitter.fire(event);
|
this.configDidChangeEmitter.fire(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,35 @@
|
|||||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
|
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 { EditorWidget } from '@theia/editor/lib/browser';
|
||||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
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()
|
@injectable()
|
||||||
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
|
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
|
||||||
@inject(ConfigService)
|
@inject(ConfigServiceClient)
|
||||||
protected readonly configService: ConfigService;
|
private readonly configService: ConfigServiceClient;
|
||||||
|
@inject(FrontendApplicationStateService)
|
||||||
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
@inject(ILogger)
|
private dataDirUri: URI | undefined;
|
||||||
protected readonly logger: ILogger;
|
|
||||||
|
|
||||||
protected dataDirUri: URI | undefined;
|
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
this.configService
|
const fireDidChange = () =>
|
||||||
.getConfiguration()
|
this.appStateService
|
||||||
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
|
.reachedState('ready')
|
||||||
.catch((err) =>
|
.then(() => this.fireDidChangeDecorations());
|
||||||
this.logger.error(`Failed to determine the data directory: ${err}`)
|
this.dataDirUri = this.configService.tryGetDataDirUri();
|
||||||
);
|
this.configService.onDidChangeDataDirUri((dataDirUri) => {
|
||||||
|
this.dataDirUri = dataDirUri;
|
||||||
|
fireDidChange();
|
||||||
|
});
|
||||||
|
if (this.dataDirUri) {
|
||||||
|
fireDidChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
|
override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
|
||||||
|
@ -5,31 +5,23 @@ import {
|
|||||||
} from '@theia/core/shared/inversify';
|
} from '@theia/core/shared/inversify';
|
||||||
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-types';
|
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-types';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ILogger } from '@theia/core';
|
|
||||||
import { Marker } from '@theia/markers/lib/common/marker';
|
import { Marker } from '@theia/markers/lib/common/marker';
|
||||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
|
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');
|
import debounce = require('lodash.debounce');
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ProblemManager extends TheiaProblemManager {
|
export class ProblemManager extends TheiaProblemManager {
|
||||||
@inject(ConfigService)
|
@inject(ConfigServiceClient)
|
||||||
protected readonly configService: ConfigService;
|
private readonly configService: ConfigServiceClient;
|
||||||
|
|
||||||
@inject(ILogger)
|
private dataDirUri: URI | undefined;
|
||||||
protected readonly logger: ILogger;
|
|
||||||
|
|
||||||
protected dataDirUri: URI | undefined;
|
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected override init(): void {
|
protected override init(): void {
|
||||||
super.init();
|
super.init();
|
||||||
this.configService
|
this.dataDirUri = this.configService.tryGetDataDirUri();
|
||||||
.getConfiguration()
|
this.configService.onDidChangeDataDirUri((uri) => (this.dataDirUri = uri));
|
||||||
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
|
|
||||||
.catch((err) =>
|
|
||||||
this.logger.error(`Failed to determine the data directory: ${err}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override setMarkers(
|
override setMarkers(
|
||||||
|
@ -2,7 +2,7 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
|
|||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
|
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
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 { SketchbookTree } from './sketchbook-tree';
|
||||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||||
import {
|
import {
|
||||||
@ -36,8 +36,8 @@ export class SketchbookTreeModel extends FileTreeModel {
|
|||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
public readonly commandRegistry: CommandRegistry;
|
public readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(ConfigService)
|
@inject(ConfigServiceClient)
|
||||||
protected readonly configService: ConfigService;
|
protected readonly configService: ConfigServiceClient;
|
||||||
|
|
||||||
@inject(OpenerService)
|
@inject(OpenerService)
|
||||||
protected readonly openerService: OpenerService;
|
protected readonly openerService: OpenerService;
|
||||||
@ -59,6 +59,12 @@ export class SketchbookTreeModel extends FileTreeModel {
|
|||||||
super.init();
|
super.init();
|
||||||
this.reportBusyProgress();
|
this.reportBusyProgress();
|
||||||
this.initializeRoot();
|
this.initializeRoot();
|
||||||
|
this.toDispose.push(
|
||||||
|
this.configService.onDidChangeSketchDirUri(async () => {
|
||||||
|
await this.updateRoot();
|
||||||
|
this.selectRoot(this.root);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
|
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
|
||||||
@ -121,6 +127,10 @@ export class SketchbookTreeModel extends FileTreeModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const root = this.root;
|
const root = this.root;
|
||||||
|
this.selectRoot(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectRoot(root: TreeNode | undefined) {
|
||||||
if (CompositeTreeNode.is(root) && root.children.length === 1) {
|
if (CompositeTreeNode.is(root) && root.children.length === 1) {
|
||||||
const child = root.children[0];
|
const child = root.children[0];
|
||||||
if (
|
if (
|
||||||
@ -161,10 +171,12 @@ export class SketchbookTreeModel extends FileTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async createRoot(): Promise<TreeNode | undefined> {
|
protected async createRoot(): Promise<TreeNode | undefined> {
|
||||||
const config = await this.configService.getConfiguration();
|
const sketchDirUri = this.configService.tryGetSketchDirUri();
|
||||||
const rootFileStats = await this.fileService.resolve(
|
const errors = this.configService.tryGetMessages();
|
||||||
new URI(config.sketchDirUri)
|
if (!sketchDirUri || errors?.length) {
|
||||||
);
|
return undefined;
|
||||||
|
}
|
||||||
|
const rootFileStats = await this.fileService.resolve(sketchDirUri);
|
||||||
|
|
||||||
if (this.workspaceService.opened && rootFileStats.children) {
|
if (this.workspaceService.opened && rootFileStats.children) {
|
||||||
// filter out libraries and hardware
|
// filter out libraries and hardware
|
||||||
|
@ -6,10 +6,12 @@ export interface ConfigService {
|
|||||||
getVersion(): Promise<
|
getVersion(): Promise<
|
||||||
Readonly<{ version: string; commit: string; status?: string }>
|
Readonly<{ version: string; commit: string; status?: string }>
|
||||||
>;
|
>;
|
||||||
getCliConfigFileUri(): Promise<string>;
|
getConfiguration(): Promise<ConfigState>;
|
||||||
getConfiguration(): Promise<Config>;
|
|
||||||
setConfiguration(config: Config): Promise<void>;
|
setConfiguration(config: Config): Promise<void>;
|
||||||
}
|
}
|
||||||
|
export type ConfigState =
|
||||||
|
| { config: undefined; messages: string[] }
|
||||||
|
| { config: Config; messages?: string[] };
|
||||||
|
|
||||||
export interface Daemon {
|
export interface Daemon {
|
||||||
readonly port: string | number;
|
readonly port: string | number;
|
||||||
@ -119,7 +121,16 @@ export interface Config {
|
|||||||
readonly network: Network;
|
readonly network: Network;
|
||||||
}
|
}
|
||||||
export namespace Config {
|
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 leftUrls = left.additionalUrls.sort();
|
||||||
const rightUrls = right.additionalUrls.sort();
|
const rightUrls = right.additionalUrls.sort();
|
||||||
if (leftUrls.length !== rightUrls.length) {
|
if (leftUrls.length !== rightUrls.length) {
|
||||||
@ -150,7 +161,16 @@ export namespace AdditionalUrls {
|
|||||||
export function stringify(additionalUrls: AdditionalUrls): string {
|
export function stringify(additionalUrls: AdditionalUrls): string {
|
||||||
return additionalUrls.join(',');
|
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) {
|
if (left.length !== right.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-facto
|
|||||||
import type {
|
import type {
|
||||||
AttachedBoardsChangeEvent,
|
AttachedBoardsChangeEvent,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
Config,
|
ConfigState,
|
||||||
ProgressMessage,
|
ProgressMessage,
|
||||||
Sketch,
|
Sketch,
|
||||||
IndexType,
|
IndexType,
|
||||||
@ -39,6 +39,11 @@ export interface IndexUpdateDidFailParams extends IndexUpdateParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationServiceClient {
|
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
|
// Index
|
||||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
|
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
|
||||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
|
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
|
||||||
@ -50,7 +55,7 @@ export interface NotificationServiceClient {
|
|||||||
notifyDaemonDidStop(): void;
|
notifyDaemonDidStop(): void;
|
||||||
|
|
||||||
// CLI config
|
// CLI config
|
||||||
notifyConfigDidChange(event: { config: Config | undefined }): void;
|
notifyConfigDidChange(event: ConfigState): void;
|
||||||
|
|
||||||
// Platforms
|
// Platforms
|
||||||
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;
|
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 { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
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 { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
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 { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
import { Sketch, SketchesService } from '../../common/protocol';
|
import { Sketch, SketchesService } from '.';
|
||||||
import { ConfigService } from './config-service';
|
import { ConfigServiceClient } from '../../browser/config/config-service-client';
|
||||||
import { SketchContainer, SketchesError, SketchRef } from './sketches-service';
|
import { SketchContainer, SketchesError, SketchRef } from './sketches-service';
|
||||||
import {
|
import {
|
||||||
ARDUINO_CLOUD_FOLDER,
|
ARDUINO_CLOUD_FOLDER,
|
||||||
@ -34,139 +36,143 @@ export class SketchesServiceClientImpl
|
|||||||
implements FrontendApplicationContribution
|
implements FrontendApplicationContribution
|
||||||
{
|
{
|
||||||
@inject(FileService)
|
@inject(FileService)
|
||||||
protected readonly fileService: FileService;
|
private readonly fileService: FileService;
|
||||||
|
|
||||||
@inject(MessageService)
|
|
||||||
protected readonly messageService: MessageService;
|
|
||||||
|
|
||||||
@inject(SketchesService)
|
@inject(SketchesService)
|
||||||
protected readonly sketchService: SketchesService;
|
private readonly sketchService: SketchesService;
|
||||||
|
|
||||||
@inject(WorkspaceService)
|
@inject(WorkspaceService)
|
||||||
protected readonly workspaceService: WorkspaceService;
|
private readonly workspaceService: WorkspaceService;
|
||||||
|
@inject(ConfigServiceClient)
|
||||||
@inject(ConfigService)
|
private readonly configService: ConfigServiceClient;
|
||||||
protected readonly configService: ConfigService;
|
|
||||||
|
|
||||||
@inject(FrontendApplicationStateService)
|
@inject(FrontendApplicationStateService)
|
||||||
private readonly appStateService: FrontendApplicationStateService;
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
protected sketches = new Map<string, SketchRef>();
|
private sketches = new Map<string, SketchRef>();
|
||||||
// TODO: rename this + event to the `onBlabla` pattern
|
private onSketchbookDidChangeEmitter = new Emitter<{
|
||||||
protected sketchbookDidChangeEmitter = new Emitter<{
|
|
||||||
created: SketchRef[];
|
created: SketchRef[];
|
||||||
removed: SketchRef[];
|
removed: SketchRef[];
|
||||||
}>();
|
}>();
|
||||||
readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event;
|
readonly onSketchbookDidChange = this.onSketchbookDidChangeEmitter.event;
|
||||||
protected currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
|
private currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
|
||||||
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
|
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
|
||||||
|
|
||||||
protected toDispose = new DisposableCollection(
|
private toDisposeBeforeWatchSketchbookDir = new DisposableCollection();
|
||||||
this.sketchbookDidChangeEmitter,
|
private toDispose = new DisposableCollection(
|
||||||
this.currentSketchDidChangeEmitter
|
this.onSketchbookDidChangeEmitter,
|
||||||
|
this.currentSketchDidChangeEmitter,
|
||||||
|
this.toDisposeBeforeWatchSketchbookDir
|
||||||
);
|
);
|
||||||
|
|
||||||
private _currentSketch: CurrentSketch | undefined;
|
private _currentSketch: CurrentSketch | undefined;
|
||||||
private currentSketchLoaded = new Deferred<CurrentSketch>();
|
private currentSketchLoaded = new Deferred<CurrentSketch>();
|
||||||
|
|
||||||
onStart(): void {
|
onStart(): void {
|
||||||
this.configService.getConfiguration().then(({ sketchDirUri }) => {
|
const sketchDirUri = this.configService.tryGetSketchDirUri();
|
||||||
this.sketchService
|
this.watchSketchbookDir(sketchDirUri);
|
||||||
.getSketches({ uri: sketchDirUri })
|
const refreshCurrentSketch = async () => {
|
||||||
.then((container) => {
|
const currentSketch = await this.loadCurrentSketch();
|
||||||
const sketchbookUri = new URI(sketchDirUri);
|
this.useCurrentSketch(currentSketch);
|
||||||
for (const sketch of SketchContainer.toArray(container)) {
|
};
|
||||||
this.sketches.set(sketch.uri, sketch);
|
this.toDispose.push(
|
||||||
}
|
this.configService.onDidChangeSketchDirUri((sketchDirUri) => {
|
||||||
this.toDispose.push(
|
this.watchSketchbookDir(sketchDirUri);
|
||||||
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
|
refreshCurrentSketch();
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.appStateService
|
this.appStateService
|
||||||
.reachedState('started_contributions')
|
.reachedState('started_contributions')
|
||||||
.then(async () => {
|
.then(refreshCurrentSketch);
|
||||||
const currentSketch = await this.loadCurrentSketch();
|
}
|
||||||
this.useCurrentSketch(currentSketch);
|
|
||||||
});
|
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(
|
private useCurrentSketch(
|
||||||
@ -249,7 +255,7 @@ export class SketchesServiceClientImpl
|
|||||||
event.removed.push(sketch);
|
event.removed.push(sketch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.sketchbookDidChangeEmitter.fire(event);
|
this.onSketchbookDidChangeEmitter.fire(event);
|
||||||
this.bufferedSketchbookEvents.length = 0;
|
this.bufferedSketchbookEvents.length = 0;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
CompoundMenuNode.is(menu) &&
|
CompoundMenuNode.is(menu) &&
|
||||||
menu.children.length &&
|
this.visibleSubmenu(menu) && // customization for #569 and #655
|
||||||
this.undefinedOrMatch(menu.when, options.context)
|
this.undefinedOrMatch(menu.when, options.context)
|
||||||
) {
|
) {
|
||||||
const role = CompoundMenuNode.getRole(menu);
|
const role = CompoundMenuNode.getRole(menu);
|
||||||
@ -193,10 +193,17 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
this.fillMenuTemplate(myItems, child, args, options)
|
this.fillMenuTemplate(myItems, child, args, options)
|
||||||
);
|
);
|
||||||
if (myItems.length === 0) {
|
if (myItems.length === 0) {
|
||||||
return parentItems;
|
// customization for #569 and #655
|
||||||
|
if (!this.visibleLeafSubmenu(menu)) {
|
||||||
|
return parentItems;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (role === CompoundMenuNodeRole.Submenu) {
|
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') {
|
} else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') {
|
||||||
if (
|
if (
|
||||||
parentItems.length &&
|
parentItems.length &&
|
||||||
@ -278,4 +285,31 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
}
|
}
|
||||||
return parentItems;
|
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))}"`;
|
return `-style="${style(toClangOptions(options))}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dataDirPath(): Promise<string> {
|
private async dataDirPath(): Promise<string | undefined> {
|
||||||
const { dataDirUri } = await this.configService.getConfiguration();
|
const { config } = await this.configService.getConfiguration();
|
||||||
return FileUri.fsPath(dataDirUri);
|
if (!config?.dataDirUri) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return FileUri.fsPath(config.dataDirUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async configDirPath(): Promise<string> {
|
private async configDirPath(): Promise<string> {
|
||||||
@ -90,9 +93,13 @@ export class ClangFormatter implements Formatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async clangConfigPath(
|
private async clangConfigPath(
|
||||||
folderUri: MaybePromise<string>
|
folderUri: MaybePromise<string | undefined>
|
||||||
): Promise<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);
|
const clangFormatPath = join(folderPath, ClangFormatFile);
|
||||||
try {
|
try {
|
||||||
await fs.access(clangFormatPath, constants.R_OK);
|
await fs.access(clangFormatPath, constants.R_OK);
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
Config,
|
Config,
|
||||||
NotificationServiceServer,
|
NotificationServiceServer,
|
||||||
Network,
|
Network,
|
||||||
|
ConfigState,
|
||||||
} from '../common/protocol';
|
} from '../common/protocol';
|
||||||
import { spawnCommand } from './exec-util';
|
import { spawnCommand } from './exec-util';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +26,7 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
|||||||
import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
||||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
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';
|
import { ErrnoException } from './utils/errors';
|
||||||
|
|
||||||
const deepmerge = require('deepmerge');
|
const deepmerge = require('deepmerge');
|
||||||
@ -36,46 +37,38 @@ export class ConfigServiceImpl
|
|||||||
{
|
{
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
@named('config')
|
@named('config')
|
||||||
protected readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
@inject(EnvVariablesServer)
|
@inject(EnvVariablesServer)
|
||||||
protected readonly envVariablesServer: EnvVariablesServer;
|
private readonly envVariablesServer: EnvVariablesServer;
|
||||||
|
|
||||||
@inject(ArduinoDaemonImpl)
|
@inject(ArduinoDaemonImpl)
|
||||||
protected readonly daemon: ArduinoDaemonImpl;
|
private readonly daemon: ArduinoDaemonImpl;
|
||||||
|
|
||||||
@inject(NotificationServiceServer)
|
@inject(NotificationServiceServer)
|
||||||
protected readonly notificationService: NotificationServiceServer;
|
private readonly notificationService: NotificationServiceServer;
|
||||||
|
|
||||||
protected config: Config;
|
private config: ConfigState = {
|
||||||
protected cliConfig: DefaultCliConfig | undefined;
|
config: undefined,
|
||||||
protected ready = new Deferred<void>();
|
messages: ['uninitialized'],
|
||||||
protected readonly configChangeEmitter = new Emitter<Config>();
|
};
|
||||||
|
private cliConfig: DefaultCliConfig | undefined;
|
||||||
|
private ready = new Deferred<void>();
|
||||||
|
private readonly configChangeEmitter = new Emitter<{
|
||||||
|
oldState: ConfigState;
|
||||||
|
newState: ConfigState;
|
||||||
|
}>();
|
||||||
|
|
||||||
onStart(): void {
|
onStart(): void {
|
||||||
this.loadCliConfig().then(async (cliConfig) => {
|
this.initConfig();
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCliConfigFileUri(): Promise<string> {
|
private async getCliConfigFileUri(): Promise<string> {
|
||||||
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
||||||
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
|
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfiguration(): Promise<Config> {
|
async getConfiguration(): Promise<ConfigState> {
|
||||||
await this.ready.promise;
|
await this.ready.promise;
|
||||||
return { ...this.config };
|
return { ...this.config };
|
||||||
}
|
}
|
||||||
@ -83,9 +76,10 @@ export class ConfigServiceImpl
|
|||||||
// Used by frontend to update the config.
|
// Used by frontend to update the config.
|
||||||
async setConfiguration(config: Config): Promise<void> {
|
async setConfiguration(config: Config): Promise<void> {
|
||||||
await this.ready.promise;
|
await this.ready.promise;
|
||||||
if (Config.sameAs(this.config, config)) {
|
if (Config.sameAs(this.config.config, config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldConfigState = deepClone(this.config);
|
||||||
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
|
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
|
||||||
this.cliConfig
|
this.cliConfig
|
||||||
);
|
);
|
||||||
@ -110,16 +104,30 @@ export class ConfigServiceImpl
|
|||||||
await this.updateDaemon(port, copyDefaultCliConfig);
|
await this.updateDaemon(port, copyDefaultCliConfig);
|
||||||
await this.writeDaemonState(port);
|
await this.writeDaemonState(port);
|
||||||
|
|
||||||
this.config = deepClone(config);
|
this.config.config = deepClone(config);
|
||||||
this.cliConfig = copyDefaultCliConfig;
|
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 {
|
get cliConfiguration(): DefaultCliConfig | undefined {
|
||||||
return this.cliConfig;
|
return this.cliConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
get onConfigChange(): Event<Config> {
|
get onConfigChange(): Event<{
|
||||||
|
oldState: ConfigState;
|
||||||
|
newState: ConfigState;
|
||||||
|
}> {
|
||||||
return this.configChangeEmitter.event;
|
return this.configChangeEmitter.event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,9 +137,42 @@ export class ConfigServiceImpl
|
|||||||
return this.daemon.getVersion();
|
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
|
initializeIfAbsent = true
|
||||||
): Promise<DefaultCliConfig | undefined> {
|
): Promise<DefaultCliConfig> {
|
||||||
const cliConfigFileUri = await this.getCliConfigFileUri();
|
const cliConfigFileUri = await this.getCliConfigFileUri();
|
||||||
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
||||||
try {
|
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 cliPath = await this.daemon.getExecPath();
|
||||||
const rawJson = await spawnCommand(`"${cliPath}"`, [
|
const rawJson = await spawnCommand(`"${cliPath}"`, [
|
||||||
'config',
|
'config',
|
||||||
@ -168,7 +209,7 @@ export class ConfigServiceImpl
|
|||||||
return JSON.parse(rawJson);
|
return JSON.parse(rawJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
private async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
||||||
const cliPath = await this.daemon.getExecPath();
|
const cliPath = await this.daemon.getExecPath();
|
||||||
await spawnCommand(`"${cliPath}"`, [
|
await spawnCommand(`"${cliPath}"`, [
|
||||||
'config',
|
'config',
|
||||||
@ -178,7 +219,7 @@ export class ConfigServiceImpl
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async mapCliConfigToAppConfig(
|
private async mapCliConfigToAppConfig(
|
||||||
cliConfig: DefaultCliConfig
|
cliConfig: DefaultCliConfig
|
||||||
): Promise<Config> {
|
): Promise<Config> {
|
||||||
const { directories, locale = 'en' } = cliConfig;
|
const { directories, locale = 'en' } = cliConfig;
|
||||||
@ -199,16 +240,45 @@ export class ConfigServiceImpl
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fireConfigChanged(config: Config): void {
|
private fireConfigChanged(
|
||||||
this.configChangeEmitter.fire(config);
|
oldState: ConfigState,
|
||||||
this.notificationService.notifyConfigDidChange({ config });
|
newState: ConfigState
|
||||||
|
): void {
|
||||||
|
this.configChangeEmitter.fire({ oldState, newState });
|
||||||
|
this.notificationService.notifyConfigDidChange(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fireInvalidConfig(): void {
|
private async validateCliConfig(config: DefaultCliConfig): Promise<void> {
|
||||||
this.notificationService.notifyConfigDidChange({ config: undefined });
|
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,
|
port: string | number,
|
||||||
config: DefaultCliConfig
|
config: DefaultCliConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -216,7 +286,7 @@ export class ConfigServiceImpl
|
|||||||
const req = new MergeRequest();
|
const req = new MergeRequest();
|
||||||
const json = JSON.stringify(config, null, 2);
|
const json = JSON.stringify(config, null, 2);
|
||||||
req.setJsonData(json);
|
req.setJsonData(json);
|
||||||
console.log(`Updating daemon with 'data': ${json}`);
|
this.logger.info(`Updating daemon with 'data': ${json}`);
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
client.merge(req, (error) => {
|
client.merge(req, (error) => {
|
||||||
try {
|
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 client = this.createClient(port);
|
||||||
const req = new WriteRequest();
|
const req = new WriteRequest();
|
||||||
const cliConfigUri = await this.getCliConfigFileUri();
|
const cliConfigUri = await this.getCliConfigFileUri();
|
||||||
@ -273,3 +343,13 @@ export class ConfigServiceImpl
|
|||||||
await fs.mkdir(cliConfig.directories.user, { recursive: true });
|
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,
|
IndexUpdateDidFailParams,
|
||||||
IndexUpdateWillStartParams,
|
IndexUpdateWillStartParams,
|
||||||
NotificationServiceServer,
|
NotificationServiceServer,
|
||||||
|
AdditionalUrls,
|
||||||
} from '../common/protocol';
|
} from '../common/protocol';
|
||||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
import {
|
import {
|
||||||
@ -75,9 +76,27 @@ export class CoreClientProvider {
|
|||||||
});
|
});
|
||||||
this.daemon.onDaemonStarted((port) => this.create(port));
|
this.daemon.onDaemonStarted((port) => this.create(port));
|
||||||
this.daemon.onDaemonStopped(() => this.closeClient());
|
this.daemon.onDaemonStopped(() => this.closeClient());
|
||||||
this.configService.onConfigChange(
|
this.configService.onConfigChange(async ({ oldState, newState }) => {
|
||||||
() => this.client.then((client) => this.updateIndex(client, ['platform'])) // Assuming 3rd party URL changes. No library index update is required.
|
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 {
|
get tryGetClient(): CoreClientProvider.Client | undefined {
|
||||||
|
@ -5,7 +5,7 @@ import type {
|
|||||||
AttachedBoardsChangeEvent,
|
AttachedBoardsChangeEvent,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
Config,
|
ConfigState,
|
||||||
Sketch,
|
Sketch,
|
||||||
ProgressMessage,
|
ProgressMessage,
|
||||||
IndexUpdateWillStartParams,
|
IndexUpdateWillStartParams,
|
||||||
@ -19,6 +19,10 @@ export class NotificationServiceServerImpl
|
|||||||
{
|
{
|
||||||
private readonly clients: NotificationServiceClient[] = [];
|
private readonly clients: NotificationServiceClient[] = [];
|
||||||
|
|
||||||
|
notifyDidReinitialize(): void {
|
||||||
|
this.clients.forEach((client) => client.notifyDidReinitialize());
|
||||||
|
}
|
||||||
|
|
||||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||||
this.clients.forEach((client) => client.notifyIndexUpdateWillStart(params));
|
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));
|
this.clients.forEach((client) => client.notifyConfigDidChange(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,15 @@ export class SketchesServiceImpl
|
|||||||
|
|
||||||
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
||||||
const root = await this.root(uri);
|
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[]>(
|
const pathToAllSketchFiles = await new Promise<string[]>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
glob(
|
glob(
|
||||||
@ -179,13 +188,23 @@ export class SketchesServiceImpl
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async root(uri?: string | undefined): Promise<string> {
|
private async root(uri?: string | undefined): Promise<string | undefined> {
|
||||||
return FileUri.fsPath(uri ?? (await this.sketchbookUri()));
|
if (uri) {
|
||||||
|
return FileUri.fsPath(uri);
|
||||||
|
}
|
||||||
|
const sketchbookUri = await this.sketchbookUri();
|
||||||
|
if (sketchbookUri) {
|
||||||
|
return FileUri.fsPath(sketchbookUri);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sketchbookUri(): Promise<string> {
|
private async sketchbookUri(): Promise<string | undefined> {
|
||||||
const { sketchDirUri } = await this.configService.getConfiguration();
|
const { config, messages } = await this.configService.getConfiguration();
|
||||||
return sketchDirUri;
|
if (!config?.sketchDirUri || messages?.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return config.sketchDirUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSketch(uri: string): Promise<SketchWithDetails> {
|
async loadSketch(uri: string): Promise<SketchWithDetails> {
|
||||||
@ -454,8 +473,10 @@ export class SketchesServiceImpl
|
|||||||
const sketchBaseName = `sketch_${
|
const sketchBaseName = `sketch_${
|
||||||
monthNames[today.getMonth()]
|
monthNames[today.getMonth()]
|
||||||
}${today.getDate()}`;
|
}${today.getDate()}`;
|
||||||
const config = await this.configService.getConfiguration();
|
const { config } = await this.configService.getConfiguration();
|
||||||
const sketchbookPath = FileUri.fsPath(config.sketchDirUri);
|
const sketchbookPath = config?.sketchDirUri
|
||||||
|
? FileUri.fsPath(config?.sketchDirUri)
|
||||||
|
: os.homedir();
|
||||||
let sketchName: string | undefined;
|
let sketchName: string | undefined;
|
||||||
|
|
||||||
// If it's another day, reset the count of sketches created today
|
// If it's another day, reset the count of sketches created today
|
||||||
|
@ -157,6 +157,11 @@
|
|||||||
"uninstallMsg": "Do you want to uninstall {0}?",
|
"uninstallMsg": "Do you want to uninstall {0}?",
|
||||||
"version": "Version {0}"
|
"version": "Version {0}"
|
||||||
},
|
},
|
||||||
|
"configuration": {
|
||||||
|
"cli": {
|
||||||
|
"inaccessibleDirectory": "Could not access the sketchbook location at '{0}': {1}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"contributions": {
|
"contributions": {
|
||||||
"addFile": "Add File",
|
"addFile": "Add File",
|
||||||
"fileAdded": "One file added to the sketch.",
|
"fileAdded": "One file added to the sketch.",
|
||||||
@ -352,6 +357,7 @@
|
|||||||
"manualProxy": "Manual proxy configuration",
|
"manualProxy": "Manual proxy configuration",
|
||||||
"network": "Network",
|
"network": "Network",
|
||||||
"newSketchbookLocation": "Select new sketchbook location",
|
"newSketchbookLocation": "Select new sketchbook location",
|
||||||
|
"noCliConfig": "Could not load the CLI configuration",
|
||||||
"noProxy": "No proxy",
|
"noProxy": "No proxy",
|
||||||
"proxySettings": {
|
"proxySettings": {
|
||||||
"hostname": "Host name",
|
"hostname": "Host name",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user