From 1f544b2656f04084741c231fc560262281f8e772 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Sat, 23 Jan 2021 14:57:21 +0100 Subject: [PATCH] ATL-546: Added UI for settings. Signed-off-by: Akos Kitta --- .../browser/arduino-frontend-contribution.tsx | 55 +- .../browser/arduino-ide-frontend-module.ts | 14 + .../src/browser/arduino-preferences.ts | 73 +++ .../browser/boards/boards-service-provider.ts | 21 +- .../browser/contributions/burn-bootloader.ts | 10 +- .../src/browser/contributions/contribution.ts | 10 +- .../contributions/edit-contributions.ts | 23 +- .../src/browser/contributions/settings.ts | 48 +- .../browser/contributions/upload-sketch.ts | 14 +- .../browser/contributions/verify-sketch.ts | 4 +- .../src/browser/settings.tsx | 603 ++++++++++++++++++ .../src/browser/style/index.css | 1 + .../src/browser/style/settings-dialog.css | 43 ++ .../browser/theia/editor/editor-manager.ts | 8 +- .../preferences/preferences-contribution.ts | 6 +- .../src/common/protocol/config-service.ts | 19 +- .../src/common/protocol/core-service.ts | 4 + arduino-ide-extension/src/common/types.ts | 4 + .../settings/settings_grpc_pb.d.ts | 17 + .../cli-protocol/settings/settings_grpc_pb.js | 34 + .../cli-protocol/settings/settings_pb.d.ts | 38 ++ .../node/cli-protocol/settings/settings_pb.js | 275 ++++++++ .../src/node/config-service-impl.ts | 153 +++-- .../src/node/core-service-impl.ts | 56 +- 24 files changed, 1374 insertions(+), 159 deletions(-) create mode 100644 arduino-ide-extension/src/browser/arduino-preferences.ts create mode 100644 arduino-ide-extension/src/browser/settings.tsx create mode 100644 arduino-ide-extension/src/browser/style/settings-dialog.css diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index e43b98d1..5acc29dd 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -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 | 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.boardsServiceClientImpl.onBoardsConfigChanged(start); + this.arduinoPreferences.onPreferenceChanged(event => { + if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) { + start(this.boardsServiceClientImpl.boardsConfig); + } }); - 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.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 { 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 diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index b3d761a0..61f82cff 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -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' + }); }); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts new file mode 100644 index 00000000..15197839 --- /dev/null +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -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; + +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); + return createArduinoPreferences(preferences); + }); + bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema }); +} diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 6d47c4b1..d8788176 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -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 | 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 { - const storedValidBoardsConfig = await this.storageService.getData>('latest-valid-boards-config'); - if (storedValidBoardsConfig) { - this.latestValidBoardsConfig = storedValidBoardsConfig; + const storedLatestValidBoardsConfig = await this.storageService.getData>('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('latest-boards-config'); + if (storedLatestBoardsConfig) { + this.latestBoardsConfig = storedLatestBoardsConfig; + this.boardsConfig = this.latestBoardsConfig; + } } } - } /** diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index 434f5a55..e04e06ad 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -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) { diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 751bf2a8..e5d85a85 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -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 { } @@ -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 { diff --git a/arduino-ide-extension/src/browser/contributions/edit-contributions.ts b/arduino-ide-extension/src/browser/contributions/edit-contributions.ts index c8983bce..d80be3da 100644 --- a/arduino-ide-extension/src/browser/contributions/edit-contributions.ts +++ b/arduino-ide-extension/src/browser/contributions/edit-contributions.ts @@ -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, { diff --git a/arduino-ide-extension/src/browser/contributions/settings.ts b/arduino-ide-extension/src/browser/contributions/settings.ts index a1e899aa..1b469906 100644 --- a/arduino-ide-extension/src/browser/contributions/settings.ts +++ b/arduino-ide-extension/src/browser/contributions/settings.ts @@ -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' } } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 13b29366..85c287cd 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -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(); diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index 2bc54ae3..0ab3a5f1 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -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) { diff --git a/arduino-ide-extension/src/browser/settings.tsx b/arduino-ide-extension/src/browser/settings.tsx new file mode 100644 index 00000000..1d3e78b2 --- /dev/null +++ b/arduino-ide-extension/src/browser/settings.tsx @@ -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(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 onDidChange = this.onDidChangeEmitter.event; + + protected ready = new Deferred(); + protected _settings: Settings; + + @postConstruct() + protected async init(): Promise { + 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 { + await this.preferenceService.ready; + const [ + editorFontSize, + themeId, + autoSave, + autoScaleInterface, + interfaceScale, + // checkForUpdates, + verboseOnCompile, + verboseOnUpload, + verifyAfterUpload, + enableLsLogs, + cliConfig + ] = await Promise.all([ + this.preferenceService.get('editor.fontSize', 12), + this.preferenceService.get('workbench.colorTheme', 'arduino-theme'), + this.preferenceService.get<'on' | 'off'>('editor.autoSave', 'on'), + this.preferenceService.get('arduino.window.autoScale', true), + this.preferenceService.get('arduino.window.zoomLevel', 0), + // this.preferenceService.get('arduino.ide.autoUpdate', true), + this.preferenceService.get('arduino.compile.verbose', true), + this.preferenceService.get('arduino.upload.verbose', true), + this.preferenceService.get('arduino.upload.verify', true), + this.preferenceService.get('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 { + await this.ready.promise; + return this._settings; + } + + async update(settings: Settings, fireDidChange: boolean = false): Promise { + 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 { + const settings = await this.loadSettings(); + return this.update(settings, true); + } + + async validate(settings: MaybePromise = this.settings()): Promise { + 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 { + 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 { + + 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
; + } + return
+ Sketchbook location: +
+ + +
+
+
+
Editor font size:
+
Interface scale:
+
Theme:
+
Show verbose output during:
+
+
+
+ +
+
+ + + % +
+
+ +
+
+ + +
+
+
+ + + + +
+ Additional boards manager URLs: + + +
+
; + } + + protected noopKeyDown = (event: React.KeyboardEvent) => { + event.nativeEvent.preventDefault(); + event.nativeEvent.returnValue = false; + } + + protected numbersOnlyKeyDown = (event: React.KeyboardEvent) => { + 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) => { + const { value } = event.target; + if (value) { + this.setState({ editorFontSize: parseInt(value, 10) }); + } + }; + + protected additionalUrlsDidChange = (event: React.ChangeEvent) => { + this.setState({ additionalUrls: event.target.value.split(',').map(url => url.trim()) }); + }; + + protected autoScaleInterfaceDidChange = (event: React.ChangeEvent) => { + this.setState({ autoScaleInterface: event.target.checked }); + }; + + protected enableLsLogsDidChange = (event: React.ChangeEvent) => { + this.setState({ enableLsLogs: event.target.checked }); + }; + + protected interfaceScaleDidChange = (event: React.ChangeEvent) => { + 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) => { + this.setState({ verifyAfterUpload: event.target.checked }); + }; + + protected checkForUpdatesDidChange = (event: React.ChangeEvent) => { + this.setState({ checkForUpdates: event.target.checked }); + }; + + protected autoSaveDidChange = (event: React.ChangeEvent) => { + this.setState({ autoSave: event.target.checked ? 'on' : 'off' }); + }; + + protected themeDidChange = (event: React.ChangeEvent) => { + const { selectedIndex } = event.target.options; + const theme = ThemeService.get().getThemes()[selectedIndex]; + if (theme) { + this.setState({ themeId: theme.id }); + } + }; + + protected verboseOnCompileDidChange = (event: React.ChangeEvent) => { + this.setState({ verboseOnCompile: event.target.checked }); + }; + + protected verboseOnUploadDidChange = (event: React.ChangeEvent) => { + this.setState({ verboseOnUpload: event.target.checked }); + }; + + protected sketchpathDidChange = (event: React.ChangeEvent) => { + 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 ; + } + +} + +@injectable() +export class SettingsDialogProps extends DialogProps { +} + +@injectable() +export class SettingsDialog extends AbstractDialog> { + + @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): Promise { + const result = await this.settingsService.validate(settings); + if (typeof result === 'string') { + return result; + } + return ''; + } + + get value(): Promise { + 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 { + + 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; + } + +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 68e984f9..b44b7912 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -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; diff --git a/arduino-ide-extension/src/browser/style/settings-dialog.css b/arduino-ide-extension/src/browser/style/settings-dialog.css new file mode 100644 index 00000000..74c5f7be --- /dev/null +++ b/arduino-ide-extension/src/browser/style/settings-dialog.css @@ -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); +} diff --git a/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts b/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts index 1681943f..9b87bc3a 100644 --- a/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts +++ b/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts @@ -24,13 +24,7 @@ export class EditorManager extends TheiaEditorManager { } protected async isReadOnly(uri: URI): Promise { - 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) } diff --git a/arduino-ide-extension/src/browser/theia/preferences/preferences-contribution.ts b/arduino-ide-extension/src/browser/theia/preferences/preferences-contribution.ts index 71db31d4..6451f6ca 100644 --- a/arduino-ide-extension/src/browser/theia/preferences/preferences-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/preferences/preferences-contribution.ts @@ -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); } } diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index 82c22456..276599da 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -3,7 +3,7 @@ export const ConfigService = Symbol('ConfigService'); export interface ConfigService { getVersion(): Promise>; getConfiguration(): Promise; - getCliConfigFileUri(): Promise; + setConfiguration(config: Config): Promise; getConfigurationFileSchemaUri(): Promise; isInDataDir(uri: string): Promise; isInSketchDir(uri: string): Promise; @@ -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; + } +} diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 3cc6a60d..6854c2a2 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -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; } } diff --git a/arduino-ide-extension/src/common/types.ts b/arduino-ide-extension/src/common/types.ts index 100be9f1..7ff41aa1 100644 --- a/arduino-ide-extension/src/common/types.ts +++ b/arduino-ide-extension/src/common/types.ts @@ -1,3 +1,7 @@ export type RecursiveRequired = { [P in keyof T]-?: RecursiveRequired; }; + +export interface Index { + [key: string]: any; +} diff --git a/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.d.ts index 9cb3f686..362e0247 100644 --- a/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.d.ts @@ -13,6 +13,7 @@ interface ISettingsService extends grpc.ServiceDefinition { @@ -51,6 +52,15 @@ interface ISettingsService_ISetValue extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } +interface ISettingsService_IWrite extends grpc.MethodDefinition { + path: "/cc.arduino.cli.settings.Settings/Write"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} export const SettingsService: ISettingsService; @@ -59,6 +69,7 @@ export interface ISettingsServer { merge: grpc.handleUnaryCall; getValue: grpc.handleUnaryCall; setValue: grpc.handleUnaryCall; + write: grpc.handleUnaryCall; } 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, 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, 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, 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, callback: (error: grpc.ServiceError | null, response: settings_settings_pb.WriteResponse) => void): grpc.ClientUnaryCall; } diff --git a/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.js b/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.js index 65199769..3b6992f6 100644 --- a/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/settings/settings_grpc_pb.js @@ -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, + }, }; diff --git a/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.d.ts index 47b46077..cd9ed820 100644 --- a/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.d.ts @@ -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}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + 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}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + 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 = { + } +} diff --git a/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.js b/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.js index 12903e5f..3fe308c4 100644 --- a/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/settings/settings_pb.js @@ -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_, 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_, 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); diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index dfa24b44..1d1bc7c0 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -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(); @@ -51,17 +50,16 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config async onStart(): Promise { 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(); } + this.fireInvalidConfig(); } async getCliConfigFileUri(): Promise { @@ -78,6 +76,35 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config return this.config; } + async setConfiguration(config: Config): Promise { + 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 { - 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(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 { - 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 { - // 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((resolve, reject) => { client.merge(data, error => { - if (error) { - reject(error); - return; + try { + if (error) { + reject(error); + return; + } + resolve(); + } finally { + client.close(); } - client.close(); - resolve(); - }) + }); }); } + protected async writeDaemonState(port: string | number): Promise { + 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((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; + } + } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 727d6c8d..c5783bd4 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -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 { 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 { - 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((resolve, reject) => { @@ -154,4 +159,23 @@ export class CoreServiceImpl implements CoreService { } } + private async coreClient(): Promise { + const coreClient = await new Promise(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; + } + }