diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index a5dea70dc..713c7868c 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -1,6 +1,6 @@ { "name": "arduino-ide-extension", - "version": "2.0.0-rc9", + "version": "2.0.0-rc9.1", "description": "An extension for Theia building the Arduino IDE", "license": "AGPL-3.0-or-later", "scripts": { @@ -156,7 +156,7 @@ ], "arduino": { "cli": { - "version": "0.25.0" + "version": "0.25.1" }, "fwuploader": { "version": "2.2.0" diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index a8a5f4d44..f2b4af002 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -104,7 +104,7 @@ export class ArduinoFrontendContribution } } }); - this.appStateService.reachedState('initialized_layout').then(() => + this.appStateService.reachedState('ready').then(() => this.arduinoPreferences.ready.then(() => { const webContents = remote.getCurrentWebContents(); const zoomLevel = this.arduinoPreferences.get( @@ -183,34 +183,6 @@ export class ArduinoFrontendContribution registerColors(colors: ColorRegistry): void { colors.register( - { - id: 'arduino.branding.primary', - defaults: { - dark: 'statusBar.background', - light: 'statusBar.background', - }, - description: - 'The primary branding color, such as dialog titles, library, and board manager list labels.', - }, - { - id: 'arduino.branding.secondary', - defaults: { - dark: 'statusBar.background', - light: 'statusBar.background', - }, - description: - 'Secondary branding color for list selections, dropdowns, and widget borders.', - }, - { - id: 'arduino.foreground', - defaults: { - dark: 'editorWidget.background', - light: 'editorWidget.background', - hc: 'editorWidget.background', - }, - description: - 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.', - }, { id: 'arduino.toolbar.button.background', defaults: { @@ -225,8 +197,8 @@ export class ArduinoFrontendContribution id: 'arduino.toolbar.button.hoverBackground', defaults: { dark: 'button.hoverBackground', - light: 'button.foreground', - hc: 'textLink.foreground', + light: 'button.hoverBackground', + hc: 'button.background', }, description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.', @@ -261,24 +233,6 @@ export class ArduinoFrontendContribution description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)', }, - { - id: 'arduino.output.foreground', - defaults: { - dark: 'editor.foreground', - light: 'editor.foreground', - hc: 'editor.foreground', - }, - description: 'Color of the text in the Output view.', - }, - { - id: 'arduino.output.background', - defaults: { - dark: 'editor.background', - light: 'editor.background', - hc: 'editor.background', - }, - description: 'Background color of the Output view.', - }, { id: 'arduino.toolbar.dropdown.border', defaults: { @@ -303,8 +257,8 @@ export class ArduinoFrontendContribution id: 'arduino.toolbar.dropdown.background', defaults: { dark: 'tab.unfocusedActiveBackground', - light: 'tab.unfocusedActiveBackground', - hc: 'tab.unfocusedActiveBackground', + light: 'dropdown.background', + hc: 'dropdown.background', }, description: 'Background color of the Board Selector.', }, @@ -312,18 +266,18 @@ export class ArduinoFrontendContribution { id: 'arduino.toolbar.dropdown.label', defaults: { - dark: 'foreground', - light: 'foreground', - hc: 'foreground', + dark: 'dropdown.foreground', + light: 'dropdown.foreground', + hc: 'dropdown.foreground', }, description: 'Font color of the Board Selector.', }, { id: 'arduino.toolbar.dropdown.iconSelected', defaults: { - dark: 'statusBar.background', - light: 'statusBar.background', - hc: 'statusBar.background', + dark: 'list.activeSelectionIconForeground', + light: 'list.activeSelectionIconForeground', + hc: 'list.activeSelectionIconForeground', }, description: 'Color of the selected protocol icon in the Board Selector.', @@ -331,18 +285,18 @@ export class ArduinoFrontendContribution { id: 'arduino.toolbar.dropdown.option.backgroundHover', defaults: { - dark: 'editor.background', - light: 'editor.background', - hc: 'editor.background', + dark: 'list.hoverBackground', + light: 'list.hoverBackground', + hc: 'list.hoverBackground', }, description: 'Background color on hover of the Board Selector options.', }, { id: 'arduino.toolbar.dropdown.option.backgroundSelected', defaults: { - dark: 'editor.background', - light: 'editor.background', - hc: 'editor.background', + dark: 'list.activeSelectionBackground', + light: 'list.activeSelectionBackground', + hc: 'list.activeSelectionBackground', }, description: 'Background color of the selected board in the Board Selector.', 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 59f2cef86..1e9a82099 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -50,13 +50,17 @@ import { ApplicationShell as TheiaApplicationShell, ShellLayoutRestorer as TheiaShellLayoutRestorer, CommonFrontendContribution as TheiaCommonFrontendContribution, + DockPanelRenderer as TheiaDockPanelRenderer, TabBarRendererFactory, ContextMenuRenderer, createTreeContainer, TreeWidget, } from '@theia/core/lib/browser'; import { MenuContribution } from '@theia/core/lib/common/menu'; -import { ApplicationShell } from './theia/core/application-shell'; +import { + ApplicationShell, + DockPanelRenderer, +} from './theia/core/application-shell'; import { FrontendApplication } from './theia/core/frontend-application'; import { BoardsConfigDialog, @@ -82,7 +86,10 @@ import { BoardsAutoInstaller } from './boards/boards-auto-installer'; import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer'; import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; -import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; +import { + MonacoThemeJson, + MonacoThemingService, +} from '@theia/monaco/lib/browser/monaco-theming-service'; import { ArduinoDaemonPath, ArduinoDaemon, @@ -310,20 +317,36 @@ import { SelectedBoard } from './contributions/selected-board'; import { CheckForUpdates } from './contributions/check-for-updates'; import { OpenBoardsConfig } from './contributions/open-boards-config'; import { SketchFilesTracker } from './contributions/sketch-files-tracker'; - -MonacoThemingService.register({ - id: 'arduino-theme', - label: 'Light (Arduino)', - uiTheme: 'vs', - json: require('../../src/browser/data/default.color-theme.json'), -}); - -MonacoThemingService.register({ - id: 'arduino-theme-dark', - label: 'Dark (Arduino)', - uiTheme: 'vs-dark', - json: require('../../src/browser/data/dark.color-theme.json'), -}); +import { MonacoThemeServiceIsReady } from './utils/window'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { StatusBarImpl } from './theia/core/status-bar'; +import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser'; + +const registerArduinoThemes = () => { + const themes: MonacoThemeJson[] = [ + { + id: 'arduino-theme', + label: 'Light (Arduino)', + uiTheme: 'vs', + json: require('../../src/browser/data/default.color-theme.json'), + }, + { + id: 'arduino-theme-dark', + label: 'Dark (Arduino)', + uiTheme: 'vs-dark', + json: require('../../src/browser/data/dark.color-theme.json'), + }, + ]; + themes.forEach((theme) => MonacoThemingService.register(theme)); +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const global = window as any; +const ready = global[MonacoThemeServiceIsReady] as Deferred; +if (ready) { + ready.promise.then(registerArduinoThemes); +} else { + registerArduinoThemes(); +} export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -566,7 +589,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times. bind(EditorManager).toSelf().inSingletonScope(); - rebind(TheiaEditorManager).to(EditorManager); + rebind(TheiaEditorManager).toService(EditorManager); // replace search icon rebind(TheiaSearchInWorkspaceFactory) @@ -805,6 +828,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WidgetManager).toSelf().inSingletonScope(); rebind(TheiaWidgetManager).toService(WidgetManager); + // To avoid running a status bar update on every single `keypress` event from the editor. + bind(StatusBarImpl).toSelf().inSingletonScope(); + rebind(TheiaStatusBarImpl).toService(StatusBarImpl); + + // Debounced update for the tab-bar toolbar when typing in the editor. + bind(DockPanelRenderer).toSelf(); + rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer); + // Preferences bindArduinoPreferences(bind); diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts index 88adcf0f0..845f97b59 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -237,6 +237,16 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { ); const actions: AutoInstallPromptActions = [ + { + key: manualInstall, + handler: () => { + this.boardsManagerFrontendContribution + .openView({ reveal: true }) + .then((widget) => + widget.refresh(candidate.name.toLocaleLowerCase()) + ); + }, + }, { isAcceptance: true, key: yes, @@ -250,16 +260,6 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { }); }, }, - { - key: manualInstall, - handler: () => { - this.boardsManagerFrontendContribution - .openView({ reveal: true }) - .then((widget) => - widget.refresh(candidate.name.toLocaleLowerCase()) - ); - }, - }, ]; return actions; diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx index a86545ab9..a1ddb7749 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -130,11 +130,14 @@ export class BoardsDropDown extends React.Component { protocolIcon )} /> -
-
+
+
{boardLabel}
-
+
{port.address}
@@ -229,7 +232,8 @@ export class BoardsToolBarItem extends React.Component<
diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 8f5f32eb4..d3d3dbd3a 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -4,11 +4,8 @@ import URI from '@theia/core/lib/common/uri'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { - Installable, - LibraryService, - ResponseServiceClient, -} from '../../common/protocol'; +import { LibraryService, ResponseServiceClient } from '../../common/protocol'; +import { ExecuteWithProgress } from '../../common/protocol/progressible'; import { SketchContribution, Command, @@ -88,7 +85,7 @@ export class AddZipLibrary extends SketchContribution { private async doInstall(zipUri: string, overwrite?: boolean): Promise { try { - await Installable.doWithProgress({ + await ExecuteWithProgress.doWithProgress({ messageService: this.messageService, progressText: nls.localize('arduino/common/processing', 'Processing') + diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index ef2ec75b1..2da1e20df 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -1,23 +1,16 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; +import { nls } from '@theia/core/lib/common'; +import { injectable } from '@theia/core/shared/inversify'; +import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { - CoreServiceContribution, Command, CommandRegistry, + CoreServiceContribution, MenuModelRegistry, } from './contribution'; -import { nls } from '@theia/core/lib/common'; @injectable() export class BurnBootloader extends CoreServiceContribution { - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; - override registerCommands(registry: CommandRegistry): void { registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, { execute: () => this.burnBootloader(), @@ -35,32 +28,19 @@ export class BurnBootloader extends CoreServiceContribution { }); } - async burnBootloader(): Promise { + private async burnBootloader(): Promise { + const options = await this.options(); try { - const { boardsConfig } = this.boardsServiceClientImpl; - const port = boardsConfig.selectedPort; - const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = - await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn - ), - this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), - this.preferences.get('arduino.upload.verify'), - this.preferences.get('arduino.upload.verbose'), - ]); - - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - this.outputChannelManager.getChannel('Arduino').clear(); - await this.coreService.burnBootloader({ - board, - programmer, - port, - verify, - verbose, + await this.doWithProgress({ + progressText: nls.localize( + 'arduino/bootloader/burningBootloader', + 'Burning bootloader...' + ), + task: (progressId, coreService) => + coreService.burnBootloader({ + ...options, + progressId, + }), }); this.messageService.info( nls.localize( @@ -75,6 +55,27 @@ export class BurnBootloader extends CoreServiceContribution { this.handleError(e); } } + + private async options(): Promise { + const { boardsConfig } = this.boardsServiceProvider; + const port = boardsConfig.selectedPort; + const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = + await Promise.all([ + this.boardsDataStore.appendConfigToFqbn( + boardsConfig.selectedBoard?.fqbn + ), + this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), + this.preferences.get('arduino.upload.verify'), + this.preferences.get('arduino.upload.verbose'), + ]); + return { + fqbn, + programmer, + port, + verify, + verbose, + }; + } } export namespace BurnBootloader { diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 096071047..d3ae285f7 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -49,13 +49,16 @@ import { Sketch, CoreService, CoreError, + ResponseServiceClient, } from '../../common/protocol'; import { ArduinoPreferences } from '../arduino-preferences'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { CoreErrorHandler } from './core-error-handler'; import { nls } from '@theia/core'; import { OutputChannelManager } from '../theia/output/output-channel'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; +import { ExecuteWithProgress } from '../../common/protocol/progressible'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { BoardsDataStore } from '../boards/boards-data-store'; export { Command, @@ -167,18 +170,23 @@ export abstract class SketchContribution extends Contribution { } @injectable() -export class CoreServiceContribution extends SketchContribution { - @inject(CoreService) - protected readonly coreService: CoreService; +export abstract class CoreServiceContribution extends SketchContribution { + @inject(BoardsDataStore) + protected readonly boardsDataStore: BoardsDataStore; + + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider; - @inject(CoreErrorHandler) - protected readonly coreErrorHandler: CoreErrorHandler; + @inject(CoreService) + private readonly coreService: CoreService; @inject(ClipboardService) private readonly clipboardService: ClipboardService; + @inject(ResponseServiceClient) + private readonly responseService: ResponseServiceClient; + protected handleError(error: unknown): void { - this.coreErrorHandler.tryHandle(error); this.tryToastErrorMessage(error); } @@ -214,6 +222,25 @@ export class CoreServiceContribution extends SketchContribution { throw error; } } + + protected async doWithProgress(options: { + progressText: string; + keepOutput?: boolean; + task: (progressId: string, coreService: CoreService) => Promise; + }): Promise { + const { progressText, keepOutput, task } = options; + this.outputChannelManager + .getChannel('Arduino') + .show({ preserveFocus: true }); + const result = await ExecuteWithProgress.doWithProgress({ + messageService: this.messageService, + responseService: this.responseService, + progressText, + run: ({ progressId }) => task(progressId, this.coreService), + keepOutput, + }); + return result; + } } export namespace Contribution { diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 21cc2180b..c9b4d2b9f 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -145,6 +145,7 @@ export class InoLanguage extends SketchContribution { name: name ? `"${name}"` : undefined, }, realTimeDiagnostics, + silentOutput: true, } ), ]); diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index 6aa63f30e..36071db0e 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -77,15 +77,11 @@ export class SaveAsSketch extends SketchContribution { const exists = await this.fileService.exists( sketchDirUri.resolve(sketch.name) ); - const defaultUri = exists - ? sketchDirUri.resolve( - sketchDirUri - .resolve( - `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}` - ) - .toString() - ) - : sketchDirUri.resolve(sketch.name); + const defaultUri = sketchDirUri.resolve( + exists + ? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}` + : sketch.name + ); const defaultPath = await this.fileService.fsPath(defaultUri); const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: nls.localize( diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 8da2b618d..e15e612ad 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -3,56 +3,47 @@ import { Emitter } from '@theia/core/lib/common/event'; import { BoardUserField, CoreService } from '../../common/protocol'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { - CoreServiceContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, + CoreServiceContribution, } from './contribution'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { DisposableCollection, nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import type { VerifySketchParams } from './verify-sketch'; @injectable() export class UploadSketch extends CoreServiceContribution { @inject(MenuModelRegistry) - protected readonly menuRegistry: MenuModelRegistry; - - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; + private readonly menuRegistry: MenuModelRegistry; @inject(UserFieldsDialog) - protected readonly userFieldsDialog: UserFieldsDialog; - - protected cachedUserFields: Map = new Map(); - - protected readonly onDidChangeEmitter = new Emitter>(); - readonly onDidChange = this.onDidChangeEmitter.event; + private readonly userFieldsDialog: UserFieldsDialog; - protected uploadInProgress = false; - protected boardRequiresUserFields = false; + private boardRequiresUserFields = false; + private readonly cachedUserFields: Map = new Map(); + private readonly menuActionsDisposables = new DisposableCollection(); - protected readonly menuActionsDisposables = new DisposableCollection(); + private readonly onDidChangeEmitter = new Emitter(); + private readonly onDidChange = this.onDidChangeEmitter.event; + private uploadInProgress = false; protected override init(): void { super.init(); - this.boardsServiceClientImpl.onBoardsConfigChanged(async () => { + this.boardsServiceProvider.onBoardsConfigChanged(async () => { const userFields = - await this.boardsServiceClientImpl.selectedBoardUserFields(); + await this.boardsServiceProvider.selectedBoardUserFields(); this.boardRequiresUserFields = userFields.length > 0; this.registerMenus(this.menuRegistry); }); } private selectedFqbnAddress(): string { - const { boardsConfig } = this.boardsServiceClientImpl; + const { boardsConfig } = this.boardsServiceProvider; const fqbn = boardsConfig.selectedBoard?.fqbn; if (!fqbn) { return ''; @@ -76,7 +67,7 @@ export class UploadSketch extends CoreServiceContribution { if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) { // Deep clone the array of board fields to avoid editing the cached ones this.userFieldsDialog.value = ( - await this.boardsServiceClientImpl.selectedBoardUserFields() + await this.boardsServiceProvider.selectedBoardUserFields() ).map((f) => ({ ...f })); const result = await this.userFieldsDialog.open(); if (!result) { @@ -98,8 +89,7 @@ export class UploadSketch extends CoreServiceContribution { const cached = this.cachedUserFields.get(key); // Deep clone the array of board fields to avoid editing the cached ones this.userFieldsDialog.value = ( - cached ?? - (await this.boardsServiceClientImpl.selectedBoardUserFields()) + cached ?? (await this.boardsServiceProvider.selectedBoardUserFields()) ).map((f) => ({ ...f })); const result = await this.userFieldsDialog.open(); @@ -130,7 +120,6 @@ export class UploadSketch extends CoreServiceContribution { override registerMenus(registry: MenuModelRegistry): void { this.menuActionsDisposables.dispose(); - this.menuActionsDisposables.push( registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, @@ -153,7 +142,7 @@ export class UploadSketch extends CoreServiceContribution { new PlaceholderMenuNode( ArduinoMenus.SKETCH__MAIN_GROUP, // commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, - UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!, + UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, { order: '2' } ) ) @@ -193,54 +182,42 @@ export class UploadSketch extends CoreServiceContribution { } async uploadSketch(usingProgrammer = false): Promise { - // even with buttons disabled, better to double check if an upload is already in progress if (this.uploadInProgress) { return; } - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return; - } - try { // toggle the toolbar button and menu item state. // uploadInProgress will be set to false whether the upload fails or not this.uploadInProgress = true; - this.coreErrorHandler.reset(); this.onDidChangeEmitter.fire(); - const { boardsConfig } = this.boardsServiceClientImpl; - const [ - fqbn, - { selectedProgrammer }, - verify, - verbose, - sourceOverride, - optimizeForDebug, - ] = await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn - ), - this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), - this.preferences.get('arduino.upload.verify'), - this.preferences.get('arduino.upload.verbose'), - this.sourceOverride(), - this.commandService.executeCommand( - 'arduino-is-optimize-for-debug' - ), - ]); - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - let options: CoreService.Upload.Options | undefined = undefined; - const { selectedPort } = boardsConfig; - const port = selectedPort; - const userFields = - this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; - if (userFields.length === 0 && this.boardRequiresUserFields) { + const verifyOptions = + await this.commandService.executeCommand( + 'arduino-verify-sketch', + { + exportBinaries: false, + silent: true, + } + ); + if (!verifyOptions) { + return; + } + + const uploadOptions = await this.uploadOptions( + usingProgrammer, + verifyOptions + ); + if (!uploadOptions) { + return; + } + + // TODO: This does not belong here. + // IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message. + if ( + uploadOptions.userFields.length === 0 && + this.boardRequiresUserFields + ) { this.messageService.error( nls.localize( 'arduino/sketch/userFieldsNotFoundError', @@ -250,37 +227,13 @@ export class UploadSketch extends CoreServiceContribution { return; } - if (usingProgrammer) { - const programmer = selectedProgrammer; - options = { - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - programmer, - port, - verbose, - verify, - sourceOverride, - userFields, - }; - } else { - options = { - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - port, - verbose, - verify, - sourceOverride, - userFields, - }; - } - this.outputChannelManager.getChannel('Arduino').clear(); - if (usingProgrammer) { - await this.coreService.uploadUsingProgrammer(options); - } else { - await this.coreService.upload(options); - } + await this.doWithProgress({ + progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), + task: (progressId, coreService) => + coreService.upload({ ...uploadOptions, progressId }), + keepOutput: true, + }); + this.messageService.info( nls.localize('arduino/sketch/doneUploading', 'Done uploading.'), { timeout: 3000 } @@ -292,6 +245,52 @@ export class UploadSketch extends CoreServiceContribution { this.onDidChangeEmitter.fire(); } } + + private async uploadOptions( + usingProgrammer: boolean, + verifyOptions: CoreService.Options.Compile + ): Promise { + const sketch = await this.sketchServiceClient.currentSketch(); + if (!CurrentSketch.isValid(sketch)) { + return undefined; + } + const userFields = this.userFields(); + const { boardsConfig } = this.boardsServiceProvider; + const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = + await Promise.all([ + verifyOptions.fqbn, // already decorated FQBN + this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)), + this.preferences.get('arduino.upload.verify'), + this.preferences.get('arduino.upload.verbose'), + ]); + const port = boardsConfig.selectedPort; + return { + sketch, + fqbn, + ...(usingProgrammer && { programmer }), + port, + verbose, + verify, + userFields, + }; + } + + private userFields() { + return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; + } + + /** + * Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to + * `VENDOR:ARCHITECTURE:BOARD_ID` format. + * See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties). + */ + private sanitizeFqbn(fqbn: string | undefined): string | undefined { + if (!fqbn) { + return undefined; + } + const [vendor, arch, id] = fqbn.split(':'); + return `${vendor}:${arch}:${id}`; + } } export namespace UploadSketch { @@ -299,7 +298,7 @@ export namespace UploadSketch { export const UPLOAD_SKETCH: Command = { id: 'arduino-upload-sketch', }; - export const UPLOAD_WITH_CONFIGURATION: Command = { + export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = { id: 'arduino-upload-with-configuration-sketch', label: nls.localize( 'arduino/sketch/configureAndUpload', diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index a7cd1e197..3fc2d53a0 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -2,8 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { CoreServiceContribution, Command, @@ -14,27 +12,36 @@ import { } from './contribution'; import { nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { CoreService } from '../../common/protocol'; +import { CoreErrorHandler } from './core-error-handler'; + +export interface VerifySketchParams { + /** + * Same as `CoreService.Options.Compile#exportBinaries` + */ + readonly exportBinaries?: boolean; + /** + * If `true`, there won't be any UI indication of the verify command. It's `false` by default. + */ + readonly silent?: boolean; +} @injectable() export class VerifySketch extends CoreServiceContribution { - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; + @inject(CoreErrorHandler) + private readonly coreErrorHandler: CoreErrorHandler; - protected readonly onDidChangeEmitter = new Emitter>(); - readonly onDidChange = this.onDidChangeEmitter.event; - - protected verifyInProgress = false; + private readonly onDidChangeEmitter = new Emitter(); + private readonly onDidChange = this.onDidChangeEmitter.event; + private verifyInProgress = false; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { - execute: () => this.verifySketch(), + execute: (params?: VerifySketchParams) => this.verifySketch(params), isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { - execute: () => this.verifySketch(true), + execute: () => this.verifySketch({ exportBinaries: true }), isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { @@ -84,61 +91,87 @@ export class VerifySketch extends CoreServiceContribution { }); } - async verifySketch(exportBinaries?: boolean): Promise { - // even with buttons disabled, better to double check if a verify is already in progress + protected override handleError(error: unknown): void { + this.coreErrorHandler.tryHandle(error); + super.handleError(error); + } + + private async verifySketch( + params?: VerifySketchParams + ): Promise { if (this.verifyInProgress) { - return; + return undefined; } - // toggle the toolbar button and menu item state. - // verifyInProgress will be set to false whether the compilation fails or not - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return; - } try { - this.verifyInProgress = true; + if (!params?.silent) { + this.verifyInProgress = true; + this.onDidChangeEmitter.fire(); + } this.coreErrorHandler.reset(); - this.onDidChangeEmitter.fire(); - const { boardsConfig } = this.boardsServiceClientImpl; - const [fqbn, sourceOverride] = await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn + + const options = await this.options(params?.exportBinaries); + if (!options) { + return undefined; + } + + await this.doWithProgress({ + progressText: nls.localize( + 'arduino/sketch/compile', + 'Compiling sketch...' ), - this.sourceOverride(), - ]); - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - const verbose = this.preferences.get('arduino.compile.verbose'); - const compilerWarnings = this.preferences.get('arduino.compile.warnings'); - const optimizeForDebug = - await this.commandService.executeCommand( - 'arduino-is-optimize-for-debug' - ); - this.outputChannelManager.getChannel('Arduino').clear(); - await this.coreService.compile({ - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - verbose, - exportBinaries, - sourceOverride, - compilerWarnings, + task: (progressId, coreService) => + coreService.compile({ + ...options, + progressId, + }), }); this.messageService.info( nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), { timeout: 3000 } ); + // Returns with the used options for the compilation + // so that follow-up tasks (such as upload) can reuse the compiled code. + // Note that the `fqbn` is already decorated with the board settings, if any. + return options; } catch (e) { this.handleError(e); + return undefined; } finally { this.verifyInProgress = false; - this.onDidChangeEmitter.fire(); + if (!params?.silent) { + this.onDidChangeEmitter.fire(); + } } } + + private async options( + exportBinaries?: boolean + ): Promise { + const sketch = await this.sketchServiceClient.currentSketch(); + if (!CurrentSketch.isValid(sketch)) { + return undefined; + } + const { boardsConfig } = this.boardsServiceProvider; + const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([ + this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn), + this.sourceOverride(), + this.commandService.executeCommand( + 'arduino-is-optimize-for-debug' + ), + ]); + const verbose = this.preferences.get('arduino.compile.verbose'); + const compilerWarnings = this.preferences.get('arduino.compile.warnings'); + return { + sketch, + fqbn, + optimizeForDebug: Boolean(optimizeForDebug), + verbose, + exportBinaries, + sourceOverride, + compilerWarnings, + }; + } } export namespace VerifySketch { diff --git a/arduino-ide-extension/src/browser/data/dark.color-theme.json b/arduino-ide-extension/src/browser/data/dark.color-theme.json index c1be9e816..a633dd457 100644 --- a/arduino-ide-extension/src/browser/data/dark.color-theme.json +++ b/arduino-ide-extension/src/browser/data/dark.color-theme.json @@ -8,6 +8,7 @@ "list.inactiveSelectionForeground": "#dae3e3", "list.inactiveSelectionBackground": "#434f54", "list.hoverBackground": "#1f272a", + "list.activeSelectionIconForeground": "#0ca1a6", "progressBar.background": "#005c5f", "editor.background": "#1f272a", "editor.foreground": "#dae3e3", @@ -16,6 +17,7 @@ "editorCursor.foreground": "#434f54", "editorWhitespace.foreground": "#bfbfbf", "editorWidget.background": "#171e21", + "editorWidget.foreground": "#dae3e3", "focusBorder": "#dae3e3", "menubar.selectionBackground": "#ffffff", "menubar.selectionForeground": "#212121", @@ -28,7 +30,7 @@ "titleBar.activeBackground": "#171e21", "titleBar.activeForeground": "#dae3e3", "terminal.background": "#000000", - "terminal.foreground": "#e0e0e0", + "terminal.foreground": "#ffffff", "dropdown.border": "#7fcbcd", "dropdown.background": "#2c353a", "dropdown.foreground": "#dae3e3", @@ -64,7 +66,8 @@ "settings.headerForeground": "#dae3e3", "tree.indentGuidesStroke": "#374146", "tab.unfocusedActiveForeground": "#dae3e3", - "tab.inactiveBackground": "#171e21" + "tab.inactiveBackground": "#171e21", + "textLink.foreground": "#0ca1a6" }, "tokenColors": [ { diff --git a/arduino-ide-extension/src/browser/data/default.color-theme.json b/arduino-ide-extension/src/browser/data/default.color-theme.json index d3a1d2a2d..e15de4ea1 100644 --- a/arduino-ide-extension/src/browser/data/default.color-theme.json +++ b/arduino-ide-extension/src/browser/data/default.color-theme.json @@ -8,6 +8,7 @@ "list.inactiveSelectionForeground": "#4e5b61", "list.inactiveSelectionBackground": "#dae3e3", "list.hoverBackground": "#ecf1f1", + "list.activeSelectionIconForeground": "#008184", "progressBar.background": "#005c5f", "editor.background": "#ffffff", "editor.foreground": "#4e5b61", @@ -16,6 +17,7 @@ "editorCursor.foreground": "#434f54", "editorWhitespace.foreground": "#bfbfbf", "editorWidget.background": "#f7f9f9", + "editorWidget.foreground": "#4e5b61", "focusBorder": "#7fcbcd", "menubar.selectionBackground": "#ffffff", "menubar.selectionForeground": "#212121", @@ -28,7 +30,7 @@ "titleBar.activeBackground": "#006d70", "titleBar.activeForeground": "#f7f9f9", "terminal.background": "#000000", - "terminal.foreground": "#e0e0e0", + "terminal.foreground": "#ffffff", "dropdown.border": "#dae3e3", "dropdown.background": "#ffffff", "dropdown.foreground": "#4e5b61", @@ -64,7 +66,8 @@ "settings.headerForeground": "#4e5b61", "tree.indentGuidesStroke": "#dae3e3", "tab.unfocusedActiveForeground": "#4e5b61", - "tab.inactiveBackground": "#ecf1f1" + "tab.inactiveBackground": "#ecf1f1", + "textLink.foreground": "#008184" }, "tokenColors": [ { diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-step-input.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-step-input.tsx index 458266fa9..8f2fd3a3c 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-step-input.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-step-input.tsx @@ -16,64 +16,30 @@ const SettingsStepInput: React.FC = ( const { value, setSettingsStateValue, step, maxValue, minValue, classNames } = props; - const [stepUpDisabled, setStepUpDisabled] = React.useState(false); - const [stepDownDisabled, setStepDownDisabled] = React.useState(false); - - const onStepUp = (): void => { - const valueRoundedToScale = Math.ceil(value / step) * step; - const calculatedValue = - valueRoundedToScale === value ? value + step : valueRoundedToScale; - const newValue = limitValueByCondition( - calculatedValue, - maxValue, - calculatedValue >= maxValue, - disableStepUp - ); - - setSettingsStateValue(newValue); + const clamp = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); }; - const onStepDown = (): void => { - const valueRoundedToScale = Math.floor(value / step) * step; + const onStep = ( + roundingOperation: 'ceil' | 'floor', + stepOperation: (a: number, b: number) => number + ): void => { + const valueRoundedToScale = Math[roundingOperation](value / step) * step; const calculatedValue = - valueRoundedToScale === value ? value - step : valueRoundedToScale; - const newValue = limitValueByCondition( - calculatedValue, - minValue, - calculatedValue <= minValue, - disableStepDown - ); + valueRoundedToScale === value + ? stepOperation(value, step) + : valueRoundedToScale; + const newValue = clamp(calculatedValue, minValue, maxValue); setSettingsStateValue(newValue); }; - const limitValueByCondition = ( - calculatedValue: number, - limitedValue: number, - condition: boolean, - onConditionTrue: () => void, - onConditionFalse = enableButtons - ): number => { - if (condition) { - onConditionTrue(); - return limitedValue; - } else { - onConditionFalse(); - return calculatedValue; - } - }; - - const enableButtons = (): void => { - setStepUpDisabled(false); - setStepDownDisabled(false); - }; - - const disableStepUp = (): void => { - setStepUpDisabled(true); + const onStepUp = (): void => { + onStep('ceil', (a: number, b: number) => a + b); }; - const disableStepDown = (): void => { - setStepDownDisabled(true); + const onStepDown = (): void => { + onStep('floor', (a: number, b: number) => a - b); }; const onUserInput = (event: React.ChangeEvent): void => { @@ -86,34 +52,14 @@ const SettingsStepInput: React.FC = ( const number = Number(eventValue); if (!isNaN(number) && number !== value) { - let newValue; - if (number > value) { - newValue = limitValueByCondition( - number, - maxValue, - number >= maxValue, - disableStepUp - ); - } else { - newValue = limitValueByCondition( - number, - minValue, - number <= minValue, - disableStepDown - ); - } + const newValue = clamp(number, minValue, maxValue); setSettingsStateValue(newValue); } }; - // the component does not unmount when we close the settings dialog - // in theia which necessitates the below useEffect - React.useEffect(() => { - if (value > minValue && value < maxValue) { - enableButtons(); - } - }, [value, minValue, maxValue]); + const upDisabled = value >= maxValue; + const downDisabled = value <= minValue; return (
@@ -127,14 +73,14 @@ const SettingsStepInput: React.FC = (