ATL-546: Added UI for settings.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2021-01-23 14:57:21 +01:00 committed by Akos Kitta
parent 1742c53015
commit 1f544b2656
24 changed files with 1374 additions and 159 deletions

View File

@ -1,3 +1,4 @@
const debounce = require('lodash.debounce');
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } from '@theia/core';
import {
ContextMenuRenderer,
@ -23,6 +24,7 @@ import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspac
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
@ -42,11 +44,9 @@ import { WorkspaceService } from './theia/workspace/workspace-service';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
const debounce = require('lodash.debounce');
import { OutputService } from '../common/protocol/output-service';
import { NotificationCenter } from './notification-center';
import { Settings } from './contributions/settings';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
@injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
@ -147,11 +147,15 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(ExecutableService)
protected executableService: ExecutableService;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
@ -192,7 +196,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
viewContribution.initializeLayout(app);
}
}
this.boardsServiceClientImpl.onBoardsConfigChanged(async ({ selectedBoard }) => {
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
@ -200,20 +204,22 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
this.startLanguageServer(fqbn, name);
}
}
});
this.notificationCenter.onConfigChanged(({ config }) => {
if (config) {
this.invalidConfigPopup = undefined;
} else {
if (!this.invalidConfigPopup) {
this.invalidConfigPopup = this.messageService.error(`Your CLI configuration is invalid. Do you want to correct it now?`, 'No', 'Yes')
.then(answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(Settings.Commands.OPEN_CLI_CONFIG.id)
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
this.invalidConfigPopup = undefined;
});
}
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
}
@ -221,6 +227,14 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
protected startLanguageServer = debounce((fqbn: string, name: string | undefined) => this.doStartLanguageServer(fqbn, name));
protected async doStartLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileSystem.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath] = await Promise.all([
this.fileSystem.fsPath(new URI(clangdUri)),
@ -231,6 +245,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
board: {
fqbn,
name: name ? `"${name}"` : undefined

View File

@ -136,6 +136,8 @@ import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationCo
import { BoardSelection } from './contributions/board-selection';
import { OpenRecentSketch } from './contributions/open-recent-sketch';
import { Help } from './contributions/help';
import { bindArduinoPreferences } from './arduino-preferences'
import { SettingsService, SettingsDialog, SettingsWidget, SettingsDialogProps } from './settings';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -375,4 +377,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution);
// Preferences
bindArduinoPreferences(bind);
// Settings wrapper for the preferences and the CLI config.
bind(SettingsService).toSelf().inSingletonScope();
// Settings dialog and widget
bind(SettingsWidget).toSelf().inSingletonScope();
bind(SettingsDialog).toSelf().inSingletonScope();
bind(SettingsDialogProps).toConstantValue({
title: 'Preferences'
});
});

View File

@ -0,0 +1,73 @@
import { interfaces } from 'inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceContribution,
PreferenceSchema
} from '@theia/core/lib/browser/preferences';
export const ArduinoConfigSchema: PreferenceSchema = {
'type': 'object',
'properties': {
'arduino.language.log': {
'type': 'boolean',
'description': "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
'default': false
},
'arduino.compile.verbose': {
'type': 'boolean',
'description': 'True for verbose compile output.',
'default': true
},
'arduino.upload.verbose': {
'type': 'boolean',
'description': 'True for verbose upload output.',
'default': true
},
'arduino.upload.verify': {
'type': 'boolean',
'default': false
},
'arduino.window.autoScale': {
'type': 'boolean',
'description': 'True if the user interface automatically scales with the font size.',
'default': true
},
'arduino.window.zoomLevel': {
'type': 'number',
'description': 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
'default': 0
},
'arduino.ide.autoUpdate': {
'type': 'boolean',
'description': 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
'default': true
}
}
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences(preferences: PreferenceService): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema);
}
export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
return createArduinoPreferences(preferences);
});
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema });
}

View File

@ -57,6 +57,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
protected _availablePorts: Port[] = [];
@ -187,6 +188,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.latestBoardsConfig = this._boardsConfig;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
@ -384,7 +386,10 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
await this.storageService.setData(key, selectedBoard);
}
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
await Promise.all([
this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
this.storageService.setData('latest-boards-config', this.latestBoardsConfig)
]);
}
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
@ -393,15 +398,21 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
const storedLatestValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedLatestValidBoardsConfig) {
this.latestValidBoardsConfig = storedLatestValidBoardsConfig;
if (this.canUploadTo(this.latestValidBoardsConfig)) {
this.boardsConfig = this.latestValidBoardsConfig;
}
} else {
// If we could not restore the latest valid config, try to restore something, the board at least.
const storedLatestBoardsConfig = await this.storageService.getData<BoardsConfig.Config | undefined>('latest-boards-config');
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
this.boardsConfig = this.latestBoardsConfig;
}
}
}
}
/**

View File

@ -47,15 +47,19 @@ export class BurnBootloader extends SketchContribution {
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort?.address;
const [fqbn, { selectedProgrammer: programmer }] = await Promise.all([
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose')
]);
this.outputChannelManager.getChannel('Arduino: bootloader').clear();
await this.coreService.burnBootloader({
fqbn,
programmer,
port
port,
verify,
verbose
});
this.messageService.info('Done burning bootloader.', { timeout: 1000 });
} catch (e) {

View File

@ -10,11 +10,13 @@ import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu';
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { ArduinoPreferences } from '../arduino-preferences';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@ -39,6 +41,9 @@ export abstract class Contribution implements CommandContribution, MenuContribut
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SettingsService)
protected readonly settingsService: SettingsService;
onStart(app: FrontendApplication): MaybePromise<void> {
}
@ -77,6 +82,9 @@ export abstract class SketchContribution extends Contribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences;
}
export namespace Contribution {

View File

@ -3,7 +3,6 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { EDITOR_FONT_DEFAULTS } from '@theia/editor/lib/browser/editor-preferences';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@ -31,10 +30,28 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { execute: () => this.run('editor.action.nextMatchFindAction') });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) + 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) - 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {

View File

@ -1,27 +1,49 @@
import { injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { URI, Command, MenuModelRegistry, CommandRegistry, SketchContribution, open } from './contribution';
import { inject, injectable } from 'inversify';
import { Command, MenuModelRegistry, CommandRegistry, SketchContribution, KeybindingRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings';
@injectable()
export class Settings extends SketchContribution {
@inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog;
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN_CLI_CONFIG, {
execute: () => this.configService.getCliConfigFileUri().then(uri => open(this.openerService, new URI(uri)))
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
try {
this.settingsOpened = true;
settings = await this.settingsDialog.open();
} finally {
this.settingsOpened = false;
}
if (settings) {
await this.settingsService.update(settings);
await this.settingsService.save();
} else {
await this.settingsService.reset();
}
},
isEnabled: () => !this.settingsOpened
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: CommonCommands.OPEN_PREFERENCES.id,
commandId: Settings.Commands.OPEN.id,
label: 'Preferences...',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: Settings.Commands.OPEN_CLI_CONFIG.id,
label: 'Open CLI Configuration',
order: '1',
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
});
}
@ -29,9 +51,9 @@ export class Settings extends SketchContribution {
export namespace Settings {
export namespace Commands {
export const OPEN_CLI_CONFIG: Command = {
id: 'arduino-open-cli-config',
label: 'Open CLI Configuration',
export const OPEN: Command = {
id: 'arduino-settings-open',
label: 'Open Preferences...',
category: 'Arduino'
}
}

View File

@ -88,9 +88,11 @@ export class UploadSketch extends SketchContribution {
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }] = await Promise.all([
const [fqbn, { selectedProgrammer }, verify, verbose] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose')
]);
let options: CoreService.Upload.Options | undefined = undefined;
@ -106,14 +108,18 @@ export class UploadSketch extends SketchContribution {
fqbn,
optimizeForDebug,
programmer,
port
port,
verbose,
verify
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port
port,
verbose,
verify
};
}
this.outputChannelManager.getChannel('Arduino: upload').clear();

View File

@ -64,11 +64,13 @@ export class VerifySketch extends SketchContribution {
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn);
const verbose = this.preferences.get('arduino.compile.verbose');
this.outputChannelManager.getChannel('Arduino: compile').clear();
await this.coreService.compile({
sketchUri: uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug
optimizeForDebug: this.editorMode.compileForDebug,
verbose
});
this.messageService.info('Done compiling.', { timeout: 1000 });
} catch (e) {

View File

@ -0,0 +1,603 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { MaybePromise } from '@theia/core/lib/common/types';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { AbstractDialog, DialogProps, PreferenceService, PreferenceScope, DialogError, ReactWidget } from '@theia/core/lib/browser';
import { Index } from '../common/types';
import { ConfigService, FileSystemExt } from '../common/protocol';
export interface Settings extends Index {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: 'on' | 'off'; // `editor.autoSave`
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
checkForUpdates?: boolean; // `arduino.ide.autoUpdate`
verboseOnCompile: boolean; // `arduino.compile.verbose`
verboseOnUpload: boolean; // `arduino.upload.verbose`
verifyAfterUpload: boolean; // `arduino.upload.verify`
enableLsLogs: boolean; // `arduino.language.log`
sketchbookPath: string; // CLI
additionalUrls: string[]; // CLI
}
export namespace Settings {
export function belongsToCli<K extends keyof Settings>(key: K): boolean {
return key === 'sketchbookPath' || key === 'additionalUrls';
}
}
export type SettingsKey = keyof Settings;
@injectable()
export class SettingsService {
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected ready = new Deferred<void>();
protected _settings: Settings;
@postConstruct()
protected async init(): Promise<void> {
await this.appStateService.reachedState('ready'); // Hack for https://github.com/eclipse-theia/theia/issues/8993
const settings = await this.loadSettings();
this._settings = deepClone(settings);
this.ready.resolve();
}
protected async loadSettings(): Promise<Settings> {
await this.preferenceService.ready;
const [
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
cliConfig
] = await Promise.all([
this.preferenceService.get<number>('editor.fontSize', 12),
this.preferenceService.get<string>('workbench.colorTheme', 'arduino-theme'),
this.preferenceService.get<'on' | 'off'>('editor.autoSave', 'on'),
this.preferenceService.get<boolean>('arduino.window.autoScale', true),
this.preferenceService.get<number>('arduino.window.zoomLevel', 0),
// this.preferenceService.get<string>('arduino.ide.autoUpdate', true),
this.preferenceService.get<boolean>('arduino.compile.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verify', true),
this.preferenceService.get<boolean>('arduino.language.log', true),
this.configService.getConfiguration()
]);
const { additionalUrls, sketchDirUri } = cliConfig;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
additionalUrls,
sketchbookPath
};
}
async settings(): Promise<Settings> {
await this.ready.promise;
return this._settings;
}
async update(settings: Settings, fireDidChange: boolean = false): Promise<void> {
await this.ready.promise;
for (const key of Object.keys(settings)) {
this._settings[key] = settings[key];
}
if (fireDidChange) {
this.onDidChangeEmitter.fire(this._settings);
}
}
async reset(): Promise<void> {
const settings = await this.loadSettings();
return this.update(settings, true);
}
async validate(settings: MaybePromise<Settings> = this.settings()): Promise<string | true> {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!await this.fileService.exists(new URI(sketchbookDir))) {
return `Invalid sketchbook location: ${sketchbookPath}`;
}
if (editorFontSize <= 0) {
return `Invalid editor font size. It must be a positive integer.`;
}
if (!ThemeService.get().getThemes().find(({ id }) => id === themeId)) {
return `Invalid theme.`;
}
return true;
} catch (err) {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
}
async save(): Promise<string | true> {
await this.ready.promise;
const {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
sketchbookPath,
additionalUrls
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath)
]);
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
await Promise.all([
this.preferenceService.set('editor.fontSize', editorFontSize, PreferenceScope.User),
this.preferenceService.set('workbench.colorTheme', themeId, PreferenceScope.User),
this.preferenceService.set('editor.autoSave', autoSave, PreferenceScope.User),
this.preferenceService.set('arduino.window.autoScale', autoScaleInterface, PreferenceScope.User),
this.preferenceService.set('arduino.window.zoomLevel', interfaceScale, PreferenceScope.User),
// this.preferenceService.set('arduino.ide.autoUpdate', checkForUpdates, PreferenceScope.User),
this.preferenceService.set('arduino.compile.verbose', verboseOnCompile, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verbose', verboseOnUpload, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verify', verifyAfterUpload, PreferenceScope.User),
this.preferenceService.set('arduino.language.log', enableLsLogs, PreferenceScope.User),
this.configService.setConfiguration(config)
]);
this.onDidChangeEmitter.fire(this._settings);
return true;
}
}
export class SettingsComponent extends React.Component<SettingsComponent.Props, SettingsComponent.State> {
readonly toDispose = new DisposableCollection();
constructor(props: SettingsComponent.Props) {
super(props);
}
componentDidUpdate(_: SettingsComponent.Props, prevState: SettingsComponent.State): void {
if (this.state && prevState && JSON.stringify(this.state) !== JSON.stringify(prevState)) {
this.props.settingsService.update(this.state, true);
}
}
componentDidMount(): void {
this.props.settingsService.settings().then(settings => this.setState(settings));
this.toDispose.push(this.props.settingsService.onDidChange(settings => this.setState(settings)));
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
if (!this.state) {
return <div />;
}
return <div className='content noselect'>
Sketchbook location:
<div className='flex-line'>
<input
className='theia-input stretch'
type='text'
value={this.state.sketchbookPath}
onChange={this.sketchpathDidChange} />
<button className='theia-button shrink' onClick={this.browseSketchbookDidClick}>Browse</button>
</div>
<div className='flex-line'>
<div className='column'>
<div className='flex-line'>Editor font size:</div>
<div className='flex-line'>Interface scale:</div>
<div className='flex-line'>Theme:</div>
<div className='flex-line'>Show verbose output during:</div>
</div>
<div className='column'>
<div className='flex-line'>
<input
className='theia-input small'
type='number'
step={1}
pattern='[0-9]+'
onKeyDown={this.numbersOnlyKeyDown}
value={this.state.editorFontSize}
onChange={this.editorFontSizeDidChange} />
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoScaleInterface}
onChange={this.autoScaleInterfaceDidChange} />
Automatic
</label>
<input
className='theia-input small with-margin'
type='number'
step={20}
pattern='[0-9]+'
onKeyDown={this.noopKeyDown}
value={100 + this.state.interfaceScale * 20}
onChange={this.interfaceScaleDidChange} />
%
</div>
<div className='flex-line'>
<select
className='theia-select'
value={ThemeService.get().getCurrentTheme().label}
onChange={this.themeDidChange}>
{ThemeService.get().getThemes().map(({ id, label }) => <option key={id} value={label}>{label}</option>)}
</select>
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnCompile}
onChange={this.verboseOnCompileDidChange} />
compile
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnUpload}
onChange={this.verboseOnUploadDidChange} />
upload
</label>
</div>
</div>
</div>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verifyAfterUpload}
onChange={this.verifyAfterUploadDidChange} />
Verify code after upload
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.checkForUpdates}
onChange={this.checkForUpdatesDidChange}
disabled={true} />
Check for updates on startup
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoSave === 'on'}
onChange={this.autoSaveDidChange} />
Auto save
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.enableLsLogs}
onChange={this.enableLsLogsDidChange} />
Enable language server logging
</label>
<div className='flex-line'>
Additional boards manager URLs:
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.additionalUrls.join(',')}
onChange={this.additionalUrlsDidChange} />
<i className='fa fa-window-restore theia-button shrink' onClick={this.editAdditionalUrlDidClick} />
</div>
</div>;
}
protected noopKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
}
protected numbersOnlyKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const key = Number(event.key)
if (isNaN(key) || event.key === null || event.key === ' ') {
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
return;
}
}
protected browseSketchbookDidClick = async () => {
const uri = await this.props.fileDialogService.showOpenDialog({
title: 'Select new sketchbook location',
openLabel: 'Chose',
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
this.setState({ sketchbookPath });
}
};
protected editAdditionalUrlDidClick = async () => {
const additionalUrls = await new AdditionalUrlsDialog(this.state.additionalUrls, this.props.windowService).open();
if (additionalUrls) {
this.setState({ additionalUrls });
}
};
protected editorFontSizeDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (value) {
this.setState({ editorFontSize: parseInt(value, 10) });
}
};
protected additionalUrlsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ additionalUrls: event.target.value.split(',').map(url => url.trim()) });
};
protected autoScaleInterfaceDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoScaleInterface: event.target.checked });
};
protected enableLsLogsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ enableLsLogs: event.target.checked });
};
protected interfaceScaleDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
const percentage = parseInt(value, 10);
if (isNaN(percentage)) {
return;
}
let interfaceScale = (percentage - 100) / 20;
if (!isNaN(interfaceScale)) {
this.setState({ interfaceScale });
}
};
protected verifyAfterUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verifyAfterUpload: event.target.checked });
};
protected checkForUpdatesDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ checkForUpdates: event.target.checked });
};
protected autoSaveDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoSave: event.target.checked ? 'on' : 'off' });
};
protected themeDidChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
}
};
protected verboseOnCompileDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnCompile: event.target.checked });
};
protected verboseOnUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnUpload: event.target.checked });
};
protected sketchpathDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const sketchbookPath = event.target.value;
if (sketchbookPath) {
this.setState({ sketchbookPath });
}
};
}
export namespace SettingsComponent {
export interface Props {
readonly settingsService: SettingsService;
readonly fileService: FileService;
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
}
export interface State extends Settings { }
}
@injectable()
export class SettingsWidget extends ReactWidget {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(WindowService)
protected readonly windowService: WindowService;
protected render(): React.ReactNode {
return <SettingsComponent
settingsService={this.settingsService}
fileService={this.fileService}
fileDialogService={this.fileDialogService}
windowService={this.windowService} />;
}
}
@injectable()
export class SettingsDialogProps extends DialogProps {
}
@injectable()
export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
constructor(@inject(SettingsDialogProps) protected readonly props: SettingsDialogProps) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.settingsService.onDidChange(this.validate.bind(this)));
}
protected async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
}
return '';
}
get value(): Promise<Settings> {
return this.settingsService.settings();
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.settingsService.onDidChange(() => this.update()));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}
export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
protected readonly textArea: HTMLTextAreaElement;
constructor(urls: string[], windowService: WindowService) {
super({ title: 'Additional Boards Manager URLs' });
this.contentNode.classList.add('additional-urls-dialog');
const description = document.createElement('div');
description.textContent = 'Enter additional URLs, one for each row';
description.style.marginBottom = '5px';
this.contentNode.appendChild(description);
this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls.filter(url => url.trim()).filter(url => !!url).join('\n');
this.textArea.wrap = 'soft';
this.textArea.cols = 90;
this.textArea.rows = 5;
this.contentNode.appendChild(this.textArea);
const anchor = document.createElement('div');
anchor.classList.add('link');
anchor.textContent = 'Click for a list of unofficial board support URLs';
anchor.style.marginTop = '5px';
anchor.style.cursor = 'pointer';
this.addEventListener(
anchor,
'click',
() => windowService.openNewWindow('https://github.com/arduino/Arduino/wiki/Unofficial-list-of-3rd-party-boards-support-urls', { external: true })
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton('OK');
this.appendCloseButton('Cancel');
}
get value(): string[] {
return this.textArea.value.split('\n').map(url => url.trim());
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}
return false;
}
}

View File

@ -6,6 +6,7 @@
@import './status-bar.css';
@import './terminal.css';
@import './editor.css';
@import './settings-dialog.css';
.theia-input.warning:focus {
outline-width: 1px;

View File

@ -0,0 +1,43 @@
.arduino-settings-dialog {
width: 740px;
}
.arduino-settings-dialog .content {
padding: 5px;
}
.arduino-settings-dialog .flex-line {
display: flex;
align-items: center;
white-space: nowrap;
}
.arduino-settings-dialog .with-margin {
margin-left: 5px;
}
.arduino-settings-dialog .theia-select {
background: var(--theia-input-background) !important;
}
.arduino-settings-dialog .column > div {
height: 26px;
vertical-align: middle;
}
.arduino-settings-dialog .flex-line .theia-button.shrink {
min-width: unset;
}
.arduino-settings-dialog .theia-input.stretch {
width: 100% !important;
}
.arduino-settings-dialog .theia-input.small {
max-width: 40px;
width: 40px;
}
.additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground);
}

View File

@ -24,13 +24,7 @@ export class EditorManager extends TheiaEditorManager {
}
protected async isReadOnly(uri: URI): Promise<boolean> {
const [config, configFileUri] = await Promise.all([
this.configService.getConfiguration(),
this.configService.getCliConfigFileUri()
]);
if (new URI(configFileUri).toString(true) === uri.toString(true)) {
return false;
}
const config = await this.configService.getConfiguration();
return new URI(config.dataDirUri).isEqualOrParent(uri)
}

View File

@ -15,11 +15,7 @@ export class PreferencesContribution extends TheiaPreferencesContribution {
}
registerKeybindings(registry: KeybindingRegistry): void {
// https://github.com/eclipse-theia/theia/issues/8202
registry.registerKeybinding({
command: CommonCommands.OPEN_PREFERENCES.id,
keybinding: 'CtrlCmd+,',
});
registry.unregisterKeybinding(CommonCommands.OPEN_PREFERENCES.id);
}
}

View File

@ -3,7 +3,7 @@ export const ConfigService = Symbol('ConfigService');
export interface ConfigService {
getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>>;
getConfiguration(): Promise<Config>;
getCliConfigFileUri(): Promise<string>;
setConfiguration(config: Config): Promise<void>;
getConfigurationFileSchemaUri(): Promise<string>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
@ -15,3 +15,20 @@ export interface Config {
readonly downloadsDirUri: string;
readonly additionalUrls: string[];
}
export namespace Config {
export function sameAs(left: Config, right: Config): boolean {
const leftUrls = left.additionalUrls.sort();
const rightUrls = right.additionalUrls.sort();
if (leftUrls.length !== rightUrls.length) {
return false;
}
for (let i = 0; i < leftUrls.length; i++) {
if (leftUrls[i] !== rightUrls[i]) {
return false;
}
}
return left.dataDirUri === right.dataDirUri
&& left.downloadsDirUri === right.downloadsDirUri
&& left.sketchDirUri === right.sketchDirUri;
}
}

View File

@ -16,6 +16,7 @@ export namespace CoreService {
readonly sketchUri: string;
readonly fqbn?: string | undefined;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;
}
}
@ -23,6 +24,7 @@ export namespace CoreService {
export interface Options extends Compile.Options {
readonly port?: string | undefined;
readonly programmer?: Programmer | undefined;
readonly verify: boolean;
}
}
@ -31,6 +33,8 @@ export namespace CoreService {
readonly fqbn?: string | undefined;
readonly port?: string | undefined;
readonly programmer?: Programmer | undefined;
readonly verbose: boolean;
readonly verify: boolean;
}
}

View File

@ -1,3 +1,7 @@
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};
export interface Index {
[key: string]: any;
}

View File

@ -13,6 +13,7 @@ interface ISettingsService extends grpc.ServiceDefinition<grpc.UntypedServiceImp
merge: ISettingsService_IMerge;
getValue: ISettingsService_IGetValue;
setValue: ISettingsService_ISetValue;
write: ISettingsService_IWrite;
}
interface ISettingsService_IGetAll extends grpc.MethodDefinition<settings_settings_pb.GetAllRequest, settings_settings_pb.RawData> {
@ -51,6 +52,15 @@ interface ISettingsService_ISetValue extends grpc.MethodDefinition<settings_sett
responseSerialize: grpc.serialize<settings_settings_pb.SetValueResponse>;
responseDeserialize: grpc.deserialize<settings_settings_pb.SetValueResponse>;
}
interface ISettingsService_IWrite extends grpc.MethodDefinition<settings_settings_pb.WriteRequest, settings_settings_pb.WriteResponse> {
path: "/cc.arduino.cli.settings.Settings/Write";
requestStream: false;
responseStream: false;
requestSerialize: grpc.serialize<settings_settings_pb.WriteRequest>;
requestDeserialize: grpc.deserialize<settings_settings_pb.WriteRequest>;
responseSerialize: grpc.serialize<settings_settings_pb.WriteResponse>;
responseDeserialize: grpc.deserialize<settings_settings_pb.WriteResponse>;
}
export const SettingsService: ISettingsService;
@ -59,6 +69,7 @@ export interface ISettingsServer {
merge: grpc.handleUnaryCall<settings_settings_pb.RawData, settings_settings_pb.MergeResponse>;
getValue: grpc.handleUnaryCall<settings_settings_pb.GetValueRequest, settings_settings_pb.Value>;
setValue: grpc.handleUnaryCall<settings_settings_pb.Value, settings_settings_pb.SetValueResponse>;
write: grpc.handleUnaryCall<settings_settings_pb.WriteRequest, settings_settings_pb.WriteResponse>;
}
export interface ISettingsClient {
@ -74,6 +85,9 @@ export interface ISettingsClient {
setValue(request: settings_settings_pb.Value, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
setValue(request: settings_settings_pb.Value, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
setValue(request: settings_settings_pb.Value, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
write(request: settings_settings_pb.WriteRequest, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
write(request: settings_settings_pb.WriteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
write(request: settings_settings_pb.WriteRequest, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
}
export class SettingsClient extends grpc.Client implements ISettingsClient {
@ -90,4 +104,7 @@ export class SettingsClient extends grpc.Client implements ISettingsClient {
public setValue(request: settings_settings_pb.Value, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
public setValue(request: settings_settings_pb.Value, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
public setValue(request: settings_settings_pb.Value, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.SetValueResponse) => void): grpc.ClientUnaryCall;
public write(request: settings_settings_pb.WriteRequest, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
public write(request: settings_settings_pb.WriteRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
public write(request: settings_settings_pb.WriteRequest, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall;
}

View File

@ -85,6 +85,28 @@ function deserialize_cc_arduino_cli_settings_Value(buffer_arg) {
return settings_settings_pb.Value.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_cc_arduino_cli_settings_WriteRequest(arg) {
if (!(arg instanceof settings_settings_pb.WriteRequest)) {
throw new Error('Expected argument of type cc.arduino.cli.settings.WriteRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_cc_arduino_cli_settings_WriteRequest(buffer_arg) {
return settings_settings_pb.WriteRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_cc_arduino_cli_settings_WriteResponse(arg) {
if (!(arg instanceof settings_settings_pb.WriteResponse)) {
throw new Error('Expected argument of type cc.arduino.cli.settings.WriteResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_cc_arduino_cli_settings_WriteResponse(buffer_arg) {
return settings_settings_pb.WriteResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
// The Settings service provides an interface to Arduino CLI's configuration
// options
@ -137,5 +159,17 @@ setValue: {
responseSerialize: serialize_cc_arduino_cli_settings_SetValueResponse,
responseDeserialize: deserialize_cc_arduino_cli_settings_SetValueResponse,
},
// Writes to file settings currently stored in memory
write: {
path: '/cc.arduino.cli.settings.Settings/Write',
requestStream: false,
responseStream: false,
requestType: settings_settings_pb.WriteRequest,
responseType: settings_settings_pb.WriteResponse,
requestSerialize: serialize_cc_arduino_cli_settings_WriteRequest,
requestDeserialize: deserialize_cc_arduino_cli_settings_WriteRequest,
responseSerialize: serialize_cc_arduino_cli_settings_WriteResponse,
responseDeserialize: deserialize_cc_arduino_cli_settings_WriteResponse,
},
};

View File

@ -123,3 +123,41 @@ export namespace SetValueResponse {
export type AsObject = {
}
}
export class WriteRequest extends jspb.Message {
getFilepath(): string;
setFilepath(value: string): WriteRequest;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): WriteRequest.AsObject;
static toObject(includeInstance: boolean, msg: WriteRequest): WriteRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: WriteRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): WriteRequest;
static deserializeBinaryFromReader(message: WriteRequest, reader: jspb.BinaryReader): WriteRequest;
}
export namespace WriteRequest {
export type AsObject = {
filepath: string,
}
}
export class WriteResponse extends jspb.Message {
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): WriteResponse.AsObject;
static toObject(includeInstance: boolean, msg: WriteResponse): WriteResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: WriteResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): WriteResponse;
static deserializeBinaryFromReader(message: WriteResponse, reader: jspb.BinaryReader): WriteResponse;
}
export namespace WriteResponse {
export type AsObject = {
}
}

View File

@ -20,6 +20,8 @@ goog.exportSymbol('proto.cc.arduino.cli.settings.MergeResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.settings.RawData', null, global);
goog.exportSymbol('proto.cc.arduino.cli.settings.SetValueResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.settings.Value', null, global);
goog.exportSymbol('proto.cc.arduino.cli.settings.WriteRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.settings.WriteResponse', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@ -146,6 +148,48 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.cc.arduino.cli.settings.SetValueResponse.displayName = 'proto.cc.arduino.cli.settings.SetValueResponse';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.settings.WriteRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.settings.WriteRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.settings.WriteRequest.displayName = 'proto.cc.arduino.cli.settings.WriteRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.settings.WriteResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.settings.WriteResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.settings.WriteResponse.displayName = 'proto.cc.arduino.cli.settings.WriteResponse';
}
@ -869,4 +913,235 @@ proto.cc.arduino.cli.settings.SetValueResponse.serializeBinaryToWriter = functio
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.settings.WriteRequest.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.settings.WriteRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.settings.WriteRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.settings.WriteRequest.toObject = function(includeInstance, msg) {
var f, obj = {
filepath: jspb.Message.getFieldWithDefault(msg, 1, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.settings.WriteRequest}
*/
proto.cc.arduino.cli.settings.WriteRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.settings.WriteRequest;
return proto.cc.arduino.cli.settings.WriteRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.settings.WriteRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.settings.WriteRequest}
*/
proto.cc.arduino.cli.settings.WriteRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setFilepath(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.settings.WriteRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.settings.WriteRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.settings.WriteRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.settings.WriteRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getFilepath();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
};
/**
* optional string filePath = 1;
* @return {string}
*/
proto.cc.arduino.cli.settings.WriteRequest.prototype.getFilepath = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.settings.WriteRequest} returns this
*/
proto.cc.arduino.cli.settings.WriteRequest.prototype.setFilepath = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.settings.WriteResponse.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.settings.WriteResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.settings.WriteResponse} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.settings.WriteResponse.toObject = function(includeInstance, msg) {
var f, obj = {
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.settings.WriteResponse}
*/
proto.cc.arduino.cli.settings.WriteResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.settings.WriteResponse;
return proto.cc.arduino.cli.settings.WriteResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.settings.WriteResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.settings.WriteResponse}
*/
proto.cc.arduino.cli.settings.WriteResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.settings.WriteResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.settings.WriteResponse.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.settings.WriteResponse} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.settings.WriteResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
};
goog.object.extend(exports, proto.cc.arduino.cli.settings);

View File

@ -12,7 +12,7 @@ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-app
import { ConfigService, Config, NotificationServiceServer } from '../common/protocol';
import * as fs from './fs-extra';
import { spawnCommand } from './exec-util';
import { RawData } from './cli-protocol/settings/settings_pb';
import { RawData, WriteRequest } from './cli-protocol/settings/settings_pb';
import { SettingsClient } from './cli-protocol/settings/settings_grpc_pb';
import * as serviceGrpcPb from './cli-protocol/settings/settings_grpc_pb';
import { ConfigFileValidator } from './config-file-validator';
@ -20,8 +20,8 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import { DefaultCliConfig, CLI_CONFIG_SCHEMA_PATH, 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';
const debounce = require('lodash.debounce');
const track = temp.track();
@injectable()
@ -43,7 +43,6 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
protected updating = false;
protected config: Config;
protected cliConfig: DefaultCliConfig | undefined;
protected ready = new Deferred<void>();
@ -51,18 +50,17 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
async onStart(): Promise<void> {
await this.ensureCliConfigExists();
await this.watchCliConfig();
this.cliConfig = await this.loadCliConfig();
if (this.cliConfig) {
const config = await this.mapCliConfigToAppConfig(this.cliConfig);
if (config) {
this.config = config;
this.ready.resolve();
return;
}
}
} else {
this.fireInvalidConfig();
}
}
async getCliConfigFileUri(): Promise<string> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
@ -78,6 +76,35 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
return this.config;
}
async setConfiguration(config: Config): Promise<void> {
await this.ready.promise;
if (Config.sameAs(this.config, config)) {
return;
}
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(this.cliConfig);
if (!copyDefaultCliConfig) {
copyDefaultCliConfig = await this.getFallbackCliConfig();
}
const { additionalUrls, dataDirUri, downloadsDirUri, sketchDirUri } = config;
copyDefaultCliConfig.directories = {
data: FileUri.fsPath(dataDirUri),
downloads: FileUri.fsPath(downloadsDirUri),
user: FileUri.fsPath(sketchDirUri)
};
copyDefaultCliConfig.board_manager = {
additional_urls: [
...additionalUrls
]
};
const { port } = copyDefaultCliConfig.daemon;
await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port);
this.config = deepClone(config);
this.cliConfig = copyDefaultCliConfig;
this.fireConfigChanged(this.config);
}
get cliConfiguration(): DefaultCliConfig | undefined {
return this.cliConfig;
}
@ -124,7 +151,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
resolve(dirPath);
});
});
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', throwawayDirPath]);
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${throwawayDirPath}"`]);
const rawYaml = await fs.readFile(path.join(throwawayDirPath, CLI_CONFIG), { encoding: 'utf-8' });
const model = yaml.safeLoad(rawYaml.trim());
return model as DefaultCliConfig;
@ -163,63 +190,8 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
};
}
protected async watchCliConfig(): Promise<void> {
const configDirUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(configDirUri);
const listener = debounce(async () => {
if (this.updating) {
return;
} else {
this.updating = true;
}
const cliConfig = await this.loadCliConfig();
// Could not parse the YAML content.
if (!cliConfig) {
this.updating = false;
this.fireInvalidConfig();
return;
}
const valid = await this.validator.validate(cliConfig);
if (!valid) {
this.updating = false;
this.fireInvalidConfig();
return;
}
const shouldUpdate = !this.cliConfig || !DefaultCliConfig.sameAs(this.cliConfig, cliConfig);
if (!shouldUpdate) {
this.fireConfigChanged(this.config);
this.updating = false;
return;
}
// We use the gRPC `Settings` API iff the `daemon.port` has not changed.
// Otherwise, we restart the daemon.
const canUpdateSettings = this.cliConfig && this.cliConfig.daemon.port === cliConfig.daemon.port;
try {
const config = await this.mapCliConfigToAppConfig(cliConfig);
const update = new Promise<void>(resolve => {
if (canUpdateSettings) {
return this.updateDaemon(cliConfig.daemon.port, cliConfig).then(resolve);
}
return this.daemon.stopDaemon()
.then(() => this.daemon.startDaemon())
.then(resolve);
})
update.then(() => {
this.cliConfig = cliConfig;
this.config = config;
this.configChangeEmitter.fire(this.config);
this.notificationService.notifyConfigChanged({ config: this.config });
}).finally(() => this.updating = false);
} catch (err) {
this.logger.error('Failed to update the daemon with the current CLI configuration.', err);
}
}, 200);
fs.watchFile(cliConfigPath, listener);
this.logger.info(`Started watching the Arduino CLI configuration: '${cliConfigPath}'.`);
}
protected fireConfigChanged(config: Config): void {
this.configChangeEmitter.fire(config);
this.notificationService.notifyConfigChanged({ config });
}
@ -227,30 +199,51 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
this.notificationService.notifyConfigChanged({ config: undefined });
}
protected async unwatchCliConfig(): Promise<void> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
fs.unwatchFile(cliConfigPath);
this.logger.info(`Stopped watching the Arduino CLI configuration: '${cliConfigPath}'.`);
}
protected async updateDaemon(port: string | number, config: DefaultCliConfig): Promise<void> {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
// @ts-ignore
const SettingsClient = grpc.makeClientConstructor(serviceGrpcPb['cc.arduino.cli.settings.Settings'], 'SettingsService') as any;
const client = new SettingsClient(`localhost:${port}`, grpc.credentials.createInsecure()) as SettingsClient;
const client = this.createClient(port);
const data = new RawData();
data.setJsondata(JSON.stringify(config, null, 2));
return new Promise<void>((resolve, reject) => {
client.merge(data, error => {
try {
if (error) {
reject(error);
return;
}
client.close();
resolve();
})
} finally {
client.close();
}
});
});
}
protected async writeDaemonState(port: string | number): Promise<void> {
const client = this.createClient(port);
const req = new WriteRequest();
const cliConfigUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigUri);
req.setFilepath(cliConfigPath);
return new Promise<void>((resolve, reject) => {
client.write(req, error => {
try {
if (error) {
reject(error);
return;
}
resolve();
} finally {
client.close();
}
});
});
}
private createClient(port: string | number): SettingsClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
// @ts-ignore
const SettingsClient = grpc.makeClientConstructor(serviceGrpcPb['cc.arduino.cli.settings.Settings'], 'SettingsService') as any;
return new SettingsClient(`localhost:${port}`, grpc.credentials.createInsecure()) as SettingsClient;
}
}

View File

@ -1,12 +1,12 @@
import { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from 'inversify';
import { inject, injectable, postConstruct } from 'inversify';
import { dirname } from 'path';
import { CoreService } from '../common/protocol/core-service';
import { CompileReq, CompileResp } from './cli-protocol/commands/compile_pb';
import { CoreClientProvider } from './core-client-provider';
import { UploadReq, UploadResp, BurnBootloaderReq, BurnBootloaderResp, UploadUsingProgrammerReq, UploadUsingProgrammerResp } from './cli-protocol/commands/upload_pb';
import { OutputService } from '../common/protocol/output-service';
import { NotificationServiceServer } from '../common/protocol';
import { NotificationServiceServer, ConfigService } from '../common/protocol';
import { ClientReadableStream } from '@grpc/grpc-js';
import { ArduinoCoreClient } from './cli-protocol/commands/commands_grpc_pb';
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
@ -23,16 +23,23 @@ export class CoreServiceImpl implements CoreService {
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(ConfigService)
protected readonly configService: ConfigService;
@postConstruct()
protected init(): void {
this.coreClient().then(({ client, instance }) => {
});
}
async compile(options: CoreService.Compile.Options): Promise<void> {
this.outputService.append({ name: 'compile', chunk: 'Compile...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' });
const { sketchUri, fqbn } = options;
const sketchFilePath = FileUri.fsPath(sketchUri);
const sketchpath = dirname(sketchFilePath);
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
}
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const compilerReq = new CompileReq();
@ -43,7 +50,7 @@ export class CoreServiceImpl implements CoreService {
}
compilerReq.setOptimizefordebug(options.optimizeForDebug);
compilerReq.setPreprocess(false);
compilerReq.setVerbose(true);
compilerReq.setVerbose(options.verbose);
compilerReq.setQuiet(false);
const result = client.compile(compilerReq);
@ -84,10 +91,7 @@ export class CoreServiceImpl implements CoreService {
const sketchFilePath = FileUri.fsPath(sketchUri);
const sketchpath = dirname(sketchFilePath);
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
}
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = requestProvider();
@ -102,6 +106,8 @@ export class CoreServiceImpl implements CoreService {
if (programmer) {
req.setProgrammer(programmer.id);
}
req.setVerbose(options.verbose);
req.setVerify(options.verify);
const result = responseHandler(client, req);
try {
@ -121,12 +127,9 @@ export class CoreServiceImpl implements CoreService {
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
}
const { fqbn, port, programmer } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn, port, programmer } = options;
const burnReq = new BurnBootloaderReq();
burnReq.setInstance(instance);
if (fqbn) {
@ -138,6 +141,8 @@ export class CoreServiceImpl implements CoreService {
if (programmer) {
burnReq.setProgrammer(programmer.id);
}
burnReq.setVerify(options.verify);
burnReq.setVerbose(options.verbose);
const result = client.burnBootloader(burnReq);
try {
await new Promise<void>((resolve, reject) => {
@ -154,4 +159,23 @@ export class CoreServiceImpl implements CoreService {
}
}
private async coreClient(): Promise<CoreClientProvider.Client> {
const coreClient = await new Promise<CoreClientProvider.Client>(async resolve => {
const client = await this.coreClientProvider.client();
if (client) {
resolve(client);
return;
}
const toDispose = this.coreClientProvider.onClientReady(async () => {
const client = await this.coreClientProvider.client();
if (client) {
toDispose.dispose();
resolve(client);
return;
}
});
});
return coreClient;
}
}