diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts deleted file mode 100644 index 12673d71b..000000000 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Command } from '@theia/core/lib/common/command'; - -/** - * @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions. - */ -export namespace ArduinoCommands { - export const TOGGLE_COMPILE_FOR_DEBUG: Command = { - id: 'arduino-toggle-compile-for-debug', - }; - - /** - * Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...) - */ - export const OPEN_SKETCH_FILES: Command = { - id: 'arduino-open-sketch-files', - }; - - export const OPEN_BOARDS_DIALOG: Command = { - id: 'arduino-open-boards-dialog', - }; -} diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index f9a2f7810..c3f7e6534 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -1,36 +1,22 @@ +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; -import * as remote from '@theia/core/electron-shared/@electron/remote'; -import { - BoardsService, - SketchesService, - ExecutableService, - Sketch, - ArduinoDaemon, -} from '../common/protocol'; -import { Mutex } from 'async-mutex'; +import { SketchesService } from '../common/protocol'; import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, - ILogger, - DisposableCollection, } from '@theia/core'; import { Dialog, FrontendApplication, FrontendApplicationContribution, - LocalStorageService, OnWillStopAction, - SaveableWidget, - StatusBar, - StatusBarAlignment, } from '@theia/core/lib/browser'; -import { nls } from '@theia/core/lib/common'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; @@ -38,45 +24,28 @@ import { TabBarToolbarContribution, TabBarToolbarRegistry, } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { nls } from '@theia/core/lib/common'; import { CommandContribution, CommandRegistry, } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; -import URI from '@theia/core/lib/common/uri'; -import { - EditorCommands, - EditorMainMenu, - EditorManager, - EditorOpenerOptions, -} from '@theia/editor/lib/browser'; +import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { FileChangeType } from '@theia/filesystem/lib/browser'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { ArduinoCommands } from './arduino-commands'; -import { BoardsConfig } from './boards/boards-config'; -import { BoardsConfigDialog } from './boards/boards-config-dialog'; -import { BoardsServiceProvider } from './boards/boards-service-provider'; -import { BoardsToolBarItem } from './boards/boards-toolbar-item'; -import { EditorMode } from './editor-mode'; -import { ArduinoMenus } from './menu/arduino-menus'; -import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution'; -import { ArduinoToolbar } from './toolbar/arduino-toolbar'; -import { ArduinoPreferences } from './arduino-preferences'; import { CurrentSketch, SketchesServiceClientImpl, } from '../common/protocol/sketches-service-client-impl'; +import { ArduinoPreferences } from './arduino-preferences'; +import { BoardsServiceProvider } from './boards/boards-service-provider'; +import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { SaveAsSketch } from './contributions/save-as-sketch'; -import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog'; -import { IDEUpdater } from '../common/protocol/ide-updater'; -import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; -import { HostedPluginEvents } from './hosted-plugin-events'; - -export const SKIP_IDE_VERSION = 'skipIDEVersion'; +import { ArduinoMenus } from './menu/arduino-menus'; +import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution'; +import { ArduinoToolbar } from './toolbar/arduino-toolbar'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @injectable() export class ArduinoFrontendContribution @@ -87,45 +56,18 @@ export class ArduinoFrontendContribution MenuContribution, ColorContribution { - @inject(ILogger) - private readonly logger: ILogger; - @inject(MessageService) private readonly messageService: MessageService; - @inject(BoardsService) - private readonly boardsService: BoardsService; - @inject(BoardsServiceProvider) - private readonly boardsServiceClientImpl: BoardsServiceProvider; - - @inject(EditorManager) - private readonly editorManager: EditorManager; - - @inject(FileService) - private readonly fileService: FileService; + private readonly boardsServiceProvider: BoardsServiceProvider; @inject(SketchesService) private readonly sketchService: SketchesService; - @inject(BoardsConfigDialog) - private readonly boardsConfigDialog: BoardsConfigDialog; - @inject(CommandRegistry) private readonly commandRegistry: CommandRegistry; - @inject(StatusBar) - private readonly statusBar: StatusBar; - - @inject(EditorMode) - private readonly editorMode: EditorMode; - - @inject(HostedPluginEvents) - private readonly hostedPluginEvents: HostedPluginEvents; - - @inject(ExecutableService) - private readonly executableService: ExecutableService; - @inject(ArduinoPreferences) private readonly arduinoPreferences: ArduinoPreferences; @@ -135,26 +77,6 @@ export class ArduinoFrontendContribution @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - @inject(LocalStorageService) - private readonly localStorageService: LocalStorageService; - - @inject(FileSystemFrontendContribution) - private readonly fileSystemFrontendContribution: FileSystemFrontendContribution; - - @inject(IDEUpdater) - private readonly updater: IDEUpdater; - - @inject(IDEUpdaterDialog) - private readonly updaterDialog: IDEUpdaterDialog; - - @inject(ArduinoDaemon) - private readonly daemon: ArduinoDaemon; - - protected invalidConfigPopup: - | Promise - | undefined; - protected toDisposeOnStop = new DisposableCollection(); - @postConstruct() protected async init(): Promise { if (!window.navigator.onLine) { @@ -166,250 +88,32 @@ export class ArduinoFrontendContribution ) ); } - const updateStatusBar = ({ - selectedBoard, - selectedPort, - }: BoardsConfig.Config) => { - this.statusBar.setElement('arduino-selected-board', { - alignment: StatusBarAlignment.RIGHT, - text: selectedBoard - ? `$(microchip) ${selectedBoard.name}` - : `$(close) ${nls.localize( - 'arduino/common/noBoardSelected', - 'No board selected' - )}`, - className: 'arduino-selected-board', - }); - if (selectedBoard) { - this.statusBar.setElement('arduino-selected-port', { - alignment: StatusBarAlignment.RIGHT, - text: selectedPort - ? nls.localize( - 'arduino/common/selectedOn', - 'on {0}', - selectedPort.address - ) - : nls.localize('arduino/common/notConnected', '[not connected]'), - className: 'arduino-selected-port', - }); - } - }; - this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar); - updateStatusBar(this.boardsServiceClientImpl.boardsConfig); - this.appStateService.reachedState('ready').then(async () => { - const sketch = await this.sketchServiceClient.currentSketch(); - if ( - CurrentSketch.isValid(sketch) && - !(await this.sketchService.isTemp(sketch)) - ) { - this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri))); - this.toDisposeOnStop.push( - this.fileService.onDidFilesChange(async (event) => { - for (const { type, resource } of event.changes) { - if ( - type === FileChangeType.ADDED && - resource.parent.toString() === sketch.uri - ) { - const reloadedSketch = await this.sketchService.loadSketch( - sketch.uri - ); - if (Sketch.isInSketch(resource, reloadedSketch)) { - this.ensureOpened(resource.toString(), true, { - mode: 'open', - }); - } - } - } - }) - ); - } - }); } async onStart(app: FrontendApplication): Promise { - this.updater - .init( - this.arduinoPreferences.get('arduino.ide.updateChannel'), - this.arduinoPreferences.get('arduino.ide.updateBaseUrl') - ) - .then(() => this.updater.checkForUpdates(true)) - .then(async (updateInfo) => { - if (!updateInfo) return; - const versionToSkip = await this.localStorageService.getData( - SKIP_IDE_VERSION - ); - if (versionToSkip === updateInfo.version) return; - this.updaterDialog.open(updateInfo); - }) - .catch((e) => { - this.messageService.error( - nls.localize( - 'arduino/ide-updater/errorCheckingForUpdates', - 'Error while checking for Arduino IDE updates.\n{0}', - e.message - ) - ); - }); - - const start = async ( - { selectedBoard }: BoardsConfig.Config, - forceStart = false - ) => { - if (selectedBoard) { - const { name, fqbn } = selectedBoard; - if (fqbn) { - this.startLanguageServer(fqbn, name, forceStart); - } - } - }; - this.boardsServiceClientImpl.onBoardsConfigChanged(start); - this.hostedPluginEvents.onPluginsDidStart(() => - start(this.boardsServiceClientImpl.boardsConfig) - ); - this.hostedPluginEvents.onPluginsWillUnload( - () => (this.languageServerFqbn = undefined) - ); this.arduinoPreferences.onPreferenceChanged((event) => { if (event.newValue !== event.oldValue) { switch (event.preferenceName) { - case 'arduino.language.log': - case 'arduino.language.realTimeDiagnostics': - start(this.boardsServiceClientImpl.boardsConfig, true); - break; case 'arduino.window.zoomLevel': if (typeof event.newValue === 'number') { const webContents = remote.getCurrentWebContents(); webContents.setZoomLevel(event.newValue || 0); } break; - case 'arduino.ide.updateChannel': - case 'arduino.ide.updateBaseUrl': - this.updater.init( - this.arduinoPreferences.get('arduino.ide.updateChannel'), - this.arduinoPreferences.get('arduino.ide.updateBaseUrl') - ); - break; } } }); - this.arduinoPreferences.ready.then(() => { - const webContents = remote.getCurrentWebContents(); - const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel'); - webContents.setZoomLevel(zoomLevel); - }); - - app.shell.leftPanelHandler.removeBottomMenu('settings-menu'); - - this.fileSystemFrontendContribution.onDidChangeEditorFile( - ({ type, editor }) => { - if (type === FileChangeType.DELETED) { - const editorWidget = editor; - if (SaveableWidget.is(editorWidget)) { - editorWidget.closeWithoutSaving(); - } else { - editorWidget.close(); - } - } - } - ); - } - - onStop(): void { - this.toDisposeOnStop.dispose(); - } - - protected languageServerFqbn?: string; - protected languageServerStartMutex = new Mutex(); - protected async startLanguageServer( - fqbn: string, - name: string | undefined, - forceStart = false - ): Promise { - const port = await this.daemon.tryGetPort(); - if (!port) { - return; - } - const release = await this.languageServerStartMutex.acquire(); - try { - await this.hostedPluginEvents.didStart; - const details = await this.boardsService.getBoardDetails({ fqbn }); - if (!details) { - // Core is not installed for the selected board. - console.info( - `Could not start language server for ${fqbn}. The core is not installed for the board.` + this.appStateService.reachedState('initialized_layout').then(() => + this.arduinoPreferences.ready.then(() => { + const webContents = remote.getCurrentWebContents(); + const zoomLevel = this.arduinoPreferences.get( + 'arduino.window.zoomLevel' ); - if (this.languageServerFqbn) { - try { - await this.commandRegistry.executeCommand( - 'arduino.languageserver.stop' - ); - console.info( - `Stopped language server process for ${this.languageServerFqbn}.` - ); - this.languageServerFqbn = undefined; - } catch (e) { - console.error( - `Failed to start language server process for ${this.languageServerFqbn}`, - e - ); - throw e; - } - } - return; - } - if (!forceStart && fqbn === this.languageServerFqbn) { - // NOOP - return; - } - this.logger.info(`Starting language server: ${fqbn}`); - const log = this.arduinoPreferences.get('arduino.language.log'); - const realTimeDiagnostics = this.arduinoPreferences.get( - 'arduino.language.realTimeDiagnostics' - ); - let currentSketchPath: string | undefined = undefined; - if (log) { - const currentSketch = await this.sketchServiceClient.currentSketch(); - if (CurrentSketch.isValid(currentSketch)) { - currentSketchPath = await this.fileService.fsPath( - new URI(currentSketch.uri) - ); - } - } - const { clangdUri, lsUri } = await this.executableService.list(); - const [clangdPath, lsPath] = await Promise.all([ - this.fileService.fsPath(new URI(clangdUri)), - this.fileService.fsPath(new URI(lsUri)), - ]); - - this.languageServerFqbn = await Promise.race([ - new Promise((_, reject) => - setTimeout( - () => reject(new Error(`Timeout after ${20_000} ms.`)), - 20_000 - ) - ), - this.commandRegistry.executeCommand( - 'arduino.languageserver.start', - { - lsPath, - cliDaemonAddr: `localhost:${port}`, - clangdPath, - log: currentSketchPath ? currentSketchPath : log, - cliDaemonInstance: '1', - realTimeDiagnostics, - board: { - fqbn, - name: name ? `"${name}"` : undefined, - }, - } - ), - ]); - } catch (e) { - console.log(`Failed to start language server for ${fqbn}`, e); - this.languageServerFqbn = undefined; - } finally { - release(); - } + webContents.setZoomLevel(zoomLevel); + }) + ); + // Removes the _Settings_ (cog) icon from the left sidebar + app.shell.leftPanelHandler.removeBottomMenu('settings-menu'); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -419,7 +123,7 @@ export class ArduinoFrontendContribution ), isVisible: (widget) => @@ -434,24 +138,6 @@ export class ArduinoFrontendContribution } registerCommands(registry: CommandRegistry): void { - registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, { - execute: () => this.editorMode.toggleCompileForDebug(), - isToggled: () => this.editorMode.compileForDebug, - }); - registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, { - execute: async (uri: URI) => { - this.openSketchFiles(uri); - }, - }); - registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { - execute: async (query?: string | undefined) => { - const boardsConfig = await this.boardsConfigDialog.open(query); - if (boardsConfig) { - this.boardsServiceClientImpl.boardsConfig = boardsConfig; - } - }, - }); - for (const command of [ EditorCommands.SPLIT_EDITOR_DOWN, EditorCommands.SPLIT_EDITOR_LEFT, @@ -484,70 +170,6 @@ export class ArduinoFrontendContribution ArduinoMenus.TOOLS, nls.localize('arduino/menu/tools', 'Tools') ); - registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { - commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id, - label: nls.localize( - 'arduino/debug/optimizeForDebugging', - 'Optimize for Debugging' - ), - order: '5', - }); - } - - protected async openSketchFiles(uri: URI): Promise { - try { - const sketch = await this.sketchService.loadSketch(uri.toString()); - const { mainFileUri, rootFolderFileUris } = sketch; - for (const uri of [mainFileUri, ...rootFolderFileUris]) { - await this.ensureOpened(uri); - } - if (mainFileUri.endsWith('.pde')) { - const message = nls.localize( - 'arduino/common/oldFormat', - "The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?", - sketch.name - ); - const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); - this.messageService - .info(message, nls.localize('arduino/common/later', 'Later'), yes) - .then(async (answer) => { - if (answer === yes) { - this.commandRegistry.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - { - execOnlyIfTemp: false, - openAfterMove: true, - wipeOriginal: false, - } - ); - } - }); - } - } catch (e) { - console.error(e); - const message = e instanceof Error ? e.message : JSON.stringify(e); - this.messageService.error(message); - } - } - - protected async ensureOpened( - uri: string, - forceOpen = false, - options?: EditorOpenerOptions | undefined - ): Promise { - const widget = this.editorManager.all.find( - (widget) => widget.editor.uri.toString() === uri - ); - if (!widget || forceOpen) { - return this.editorManager.open( - new URI(uri), - options ?? { - mode: 'reveal', - preview: false, - counter: 0, - } - ); - } } registerColors(colors: ColorRegistry): void { @@ -699,6 +321,7 @@ export class ArduinoFrontendContribution ); } + // TODO: should be handled by `Close` contribution. https://github.com/arduino/arduino-ide/issues/1016 onWillStop(): OnWillStopAction { return { reason: 'temp-sketch', 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 231b6636a..59f2cef86 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -80,7 +80,6 @@ import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browse import { ProblemManager } from './theia/markers/problem-manager'; import { BoardsAutoInstaller } from './boards/boards-auto-installer'; import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer'; -import { EditorMode } from './editor-mode'; 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'; @@ -301,10 +300,16 @@ import { CoreErrorHandler } from './contributions/core-error-handler'; import { CompilerErrors } from './contributions/compiler-errors'; import { WidgetManager } from './theia/core/widget-manager'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { StartupTask } from './widgets/sketchbook/startup-task'; +import { StartupTasks } from './widgets/sketchbook/startup-task'; import { IndexesUpdateProgress } from './contributions/indexes-update-progress'; import { Daemon } from './contributions/daemon'; import { FirstStartupInstaller } from './contributions/first-startup-installer'; +import { OpenSketchFiles } from './contributions/open-sketch-files'; +import { InoLanguage } from './contributions/ino-language'; +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', @@ -486,10 +491,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { WorkspaceVariableContribution ); - // Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`. - bind(EditorMode).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(EditorMode); - bind(SurveyNotificationService) .toDynamicValue((context) => { return ElectronIpcConnectionProvider.createProxy( @@ -697,10 +698,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, PlotterFrontendContribution); Contribution.configure(bind, Format); Contribution.configure(bind, CompilerErrors); - Contribution.configure(bind, StartupTask); + Contribution.configure(bind, StartupTasks); Contribution.configure(bind, IndexesUpdateProgress); Contribution.configure(bind, Daemon); Contribution.configure(bind, FirstStartupInstaller); + Contribution.configure(bind, OpenSketchFiles); + Contribution.configure(bind, InoLanguage); + Contribution.configure(bind, SelectedBoard); + Contribution.configure(bind, CheckForUpdates); + Contribution.configure(bind, OpenBoardsConfig); + Contribution.configure(bind, SketchFilesTracker); // Disabled the quick-pick customization from Theia when multiple formatters are available. // Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors. diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index 398a4f2c2..3db30a72f 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -92,6 +92,14 @@ export const ArduinoConfigSchema: PreferenceSchema = { ), default: 'None', }, + 'arduino.compile.optimizeForDebug': { + type: 'boolean', + description: nls.localize( + 'arduino/preferences/compile.optimizeForDebug', + "Optimize compile output for debug, not for release. It's 'false' by default." + ), + default: false, + }, 'arduino.upload.verbose': { type: 'boolean', description: nls.localize( @@ -185,10 +193,10 @@ export const ArduinoConfigSchema: PreferenceSchema = { ), default: true, }, - 'arduino.cloud.sketchSyncEnpoint': { + 'arduino.cloud.sketchSyncEndpoint': { type: 'string', description: nls.localize( - 'arduino/preferences/cloud.sketchSyncEnpoint', + 'arduino/preferences/cloud.sketchSyncEndpoint', 'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.' ), default: 'https://api2.arduino.cc/create', @@ -251,6 +259,7 @@ export interface ArduinoConfiguration { 'arduino.compile.experimental': boolean; 'arduino.compile.revealRange': ErrorRevealStrategy; 'arduino.compile.warnings': CompilerWarnings; + 'arduino.compile.optimizeForDebug': boolean; 'arduino.upload.verbose': boolean; 'arduino.upload.verify': boolean; 'arduino.window.autoScale': boolean; @@ -263,7 +272,7 @@ export interface ArduinoConfiguration { 'arduino.cloud.pull.warn': boolean; 'arduino.cloud.push.warn': boolean; 'arduino.cloud.pushpublic.warn': boolean; - 'arduino.cloud.sketchSyncEnpoint': string; + 'arduino.cloud.sketchSyncEndpoint': string; 'arduino.auth.clientID': string; 'arduino.auth.domain': string; 'arduino.auth.audience': string; 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 d0d5fc353..12ad3ec19 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -17,10 +17,10 @@ import { import { BoardsConfig } from './boards-config'; import { naturalCompare } from '../../common/utils'; import { NotificationCenter } from '../notification-center'; -import { ArduinoCommands } from '../arduino-commands'; import { StorageWrapper } from '../storage-wrapper'; import { nls } from '@theia/core/lib/common'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @injectable() export class BoardsServiceProvider implements FrontendApplicationContribution { @@ -39,6 +39,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { @inject(NotificationCenter) protected notificationCenter: NotificationCenter; + @inject(FrontendApplicationStateService) + private readonly appStateService: FrontendApplicationStateService; + protected readonly onBoardsConfigChangedEmitter = new Emitter(); protected readonly onAvailableBoardsChangedEmitter = new Emitter< @@ -87,11 +90,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { this.notifyPlatformUninstalled.bind(this) ); - Promise.all([ - this.boardsService.getAttachedBoards(), - this.boardsService.getAvailablePorts(), - this.loadState(), - ]).then(async ([attachedBoards, availablePorts]) => { + this.appStateService.reachedState('ready').then(async () => { + const [attachedBoards, availablePorts] = await Promise.all([ + this.boardsService.getAttachedBoards(), + this.boardsService.getAvailablePorts(), + this.loadState(), + ]); this._attachedBoards = attachedBoards; this._availablePorts = availablePorts; this.onAvailablePortsChangedEmitter.fire(this._availablePorts); @@ -166,7 +170,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { .then(async (answer) => { if (answer === yes) { this.commandService.executeCommand( - ArduinoCommands.OPEN_BOARDS_DIALOG.id, + 'arduino-open-boards-dialog', selectedBoard.name ); } 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 ed4814370..3cbf1d96f 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -3,7 +3,7 @@ import * as ReactDOM from '@theia/core/shared/react-dom'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Port } from '../../common/protocol'; -import { ArduinoCommands } from '../arduino-commands'; +import { OpenBoardsConfig } from '../contributions/open-boards-config'; import { BoardsServiceProvider, AvailableBoard, @@ -155,7 +155,7 @@ export class BoardsToolBarItem extends React.Component< constructor(props: BoardsToolBarItem.Props) { super(props); - const { availableBoards } = props.boardsServiceClient; + const { availableBoards } = props.boardsServiceProvider; this.state = { availableBoards, coords: 'hidden', @@ -167,8 +167,8 @@ export class BoardsToolBarItem extends React.Component< } override componentDidMount(): void { - this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => - this.setState({ availableBoards }) + this.props.boardsServiceProvider.onAvailableBoardsChanged( + (availableBoards) => this.setState({ availableBoards }) ); } @@ -176,7 +176,7 @@ export class BoardsToolBarItem extends React.Component< this.toDispose.dispose(); } - protected readonly show = (event: React.MouseEvent) => { + protected readonly show = (event: React.MouseEvent): void => { const { currentTarget: element } = event; if (element instanceof HTMLElement) { if (this.state.coords === 'hidden') { @@ -212,7 +212,7 @@ export class BoardsToolBarItem extends React.Component< const protocolIcon = isConnected ? iconNameFromProtocol(selectedBoard?.port?.protocol || '') : null; - const procolIconClassNames = classNames( + const protocolIconClassNames = classNames( 'arduino-boards-toolbar-item--protocol', 'fa', protocolIcon @@ -225,7 +225,7 @@ export class BoardsToolBarItem extends React.Component< title={selectedPortLabel} onClick={this.show} > - {protocolIcon &&
} + {protocolIcon &&
}
{ if (board.state === AvailableBoard.State.incomplete) { - this.props.boardsServiceClient.boardsConfig = { + this.props.boardsServiceProvider.boardsConfig = { selectedPort: board.port, }; this.openDialog(); } else { - this.props.boardsServiceClient.boardsConfig = { + this.props.boardsServiceProvider.boardsConfig = { selectedBoard: board, selectedPort: board.port, }; @@ -264,13 +264,15 @@ export class BoardsToolBarItem extends React.Component< ); } - protected openDialog = () => { - this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id); + protected openDialog = (): void => { + this.props.commands.executeCommand( + OpenBoardsConfig.Commands.OPEN_DIALOG.id + ); }; } export namespace BoardsToolBarItem { export interface Props { - readonly boardsServiceClient: BoardsServiceProvider; + readonly boardsServiceProvider: BoardsServiceProvider; readonly commands: CommandRegistry; } diff --git a/arduino-ide-extension/src/browser/contributions/check-for-updates.ts b/arduino-ide-extension/src/browser/contributions/check-for-updates.ts new file mode 100644 index 000000000..16db7a845 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/check-for-updates.ts @@ -0,0 +1,64 @@ +import { nls } from '@theia/core/lib/common/nls'; +import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + IDEUpdater, + SKIP_IDE_VERSION, +} from '../../common/protocol/ide-updater'; +import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog'; +import { Contribution } from './contribution'; + +@injectable() +export class CheckForUpdates extends Contribution { + @inject(IDEUpdater) + private readonly updater: IDEUpdater; + + @inject(IDEUpdaterDialog) + private readonly updaterDialog: IDEUpdaterDialog; + + @inject(LocalStorageService) + private readonly localStorage: LocalStorageService; + + override onStart(): void { + this.preferences.onPreferenceChanged( + ({ preferenceName, newValue, oldValue }) => { + if (newValue !== oldValue) { + switch (preferenceName) { + case 'arduino.ide.updateChannel': + case 'arduino.ide.updateBaseUrl': + this.updater.init( + this.preferences.get('arduino.ide.updateChannel'), + this.preferences.get('arduino.ide.updateBaseUrl') + ); + } + } + } + ); + } + + override onReady(): void { + this.updater + .init( + this.preferences.get('arduino.ide.updateChannel'), + this.preferences.get('arduino.ide.updateBaseUrl') + ) + .then(() => this.updater.checkForUpdates(true)) + .then(async (updateInfo) => { + if (!updateInfo) return; + const versionToSkip = await this.localStorage.getData( + SKIP_IDE_VERSION + ); + if (versionToSkip === updateInfo.version) return; + this.updaterDialog.open(updateInfo); + }) + .catch((e) => { + this.messageService.error( + nls.localize( + 'arduino/ide-updater/errorCheckingForUpdates', + 'Error while checking for Arduino IDE updates.\n{0}', + e.message + ) + ); + }); + } +} diff --git a/arduino-ide-extension/src/browser/contributions/compiler-errors.ts b/arduino-ide-extension/src/browser/contributions/compiler-errors.ts index 0356d40c8..b3946bfd2 100644 --- a/arduino-ide-extension/src/browser/contributions/compiler-errors.ts +++ b/arduino-ide-extension/src/browser/contributions/compiler-errors.ts @@ -29,10 +29,7 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter'; import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter'; import { CoreError } from '../../common/protocol/core-service'; -import { - ArduinoPreferences, - ErrorRevealStrategy, -} from '../arduino-preferences'; +import { ErrorRevealStrategy } from '../arduino-preferences'; import { InoSelector } from '../ino-selectors'; import { fullRange } from '../utils/monaco'; import { Contribution } from './contribution'; @@ -127,9 +124,6 @@ export class CompilerErrors @inject(CoreErrorHandler) private readonly coreErrorHandler: CoreErrorHandler; - @inject(ArduinoPreferences) - private readonly preferences: ArduinoPreferences; - private readonly errors: ErrorDecoration[] = []; private readonly onDidChangeEmitter = new monaco.Emitter(); private readonly currentErrorDidChangEmitter = new Emitter(); diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index fc51d5b65..096071047 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -37,7 +37,6 @@ import { CommandContribution, CommandService, } from '@theia/core/lib/common/command'; -import { EditorMode } from '../editor-mode'; import { SettingsService } from '../dialogs/settings/settings'; import { CurrentSketch, @@ -90,15 +89,15 @@ export abstract class Contribution @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(EditorMode) - protected readonly editorMode: EditorMode; - @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(SettingsService) protected readonly settingsService: SettingsService; + @inject(ArduinoPreferences) + protected readonly preferences: ArduinoPreferences; + @inject(FrontendApplicationStateService) protected readonly appStateService: FrontendApplicationStateService; @@ -146,9 +145,6 @@ export abstract class SketchContribution extends Contribution { @inject(SketchesServiceClientImpl) protected readonly sketchServiceClient: SketchesServiceClientImpl; - @inject(ArduinoPreferences) - protected readonly preferences: ArduinoPreferences; - @inject(EditorManager) protected readonly editorManager: EditorManager; diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index 8d94df4dd..54137b1e5 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -12,46 +12,54 @@ import { SketchContribution, TabBarToolbarRegistry, } from './contribution'; -import { MaybePromise, nls } from '@theia/core/lib/common'; +import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { + PreferenceScope, + PreferenceService, +} from '@theia/core/lib/browser/preferences/preference-service'; @injectable() export class Debug extends SketchContribution { @inject(HostedPluginSupport) - protected hostedPluginSupport: HostedPluginSupport; + private readonly hostedPluginSupport: HostedPluginSupport; @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; + private readonly notificationCenter: NotificationCenter; @inject(ExecutableService) - protected readonly executableService: ExecutableService; + private readonly executableService: ExecutableService; @inject(BoardsService) - protected readonly boardService: BoardsService; + private readonly boardService: BoardsService; @inject(BoardsServiceProvider) - protected readonly boardsServiceProvider: BoardsServiceProvider; + private readonly boardsServiceProvider: BoardsServiceProvider; + + @inject(PreferenceService) + private readonly preferenceService: PreferenceService; /** * If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled. */ - protected _disabledMessages?: string = nls.localize( + private _disabledMessages?: string = nls.localize( 'arduino/common/noBoardSelected', 'No board selected' ); // Initial pessimism. - protected disabledMessageDidChangeEmitter = new Emitter(); - protected onDisabledMessageDidChange = + private disabledMessageDidChangeEmitter = new Emitter(); + private onDisabledMessageDidChange = this.disabledMessageDidChangeEmitter.event; - protected get disabledMessage(): string | undefined { + private get disabledMessage(): string | undefined { return this._disabledMessages; } - protected set disabledMessage(message: string | undefined) { + private set disabledMessage(message: string | undefined) { this._disabledMessages = message; this.disabledMessageDidChangeEmitter.fire(this._disabledMessages); } - protected readonly debugToolbarItem = { + private readonly debugToolbarItem = { id: Debug.Commands.START_DEBUGGING.id, command: Debug.Commands.START_DEBUGGING.id, tooltip: `${ @@ -98,12 +106,24 @@ export class Debug extends SketchContribution { ArduinoToolbar.is(widget) && widget.side === 'left', isEnabled: () => !this.disabledMessage, }); + registry.registerCommand(Debug.Commands.OPTIMIZE_FOR_DEBUG, { + execute: () => this.toggleOptimizeForDebug(), + isToggled: () => this.isOptimizeForDebug(), + }); } override registerToolbarItems(registry: TabBarToolbarRegistry): void { registry.registerItem(this.debugToolbarItem); } + override registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { + commandId: Debug.Commands.OPTIMIZE_FOR_DEBUG.id, + label: Debug.Commands.OPTIMIZE_FOR_DEBUG.label, + order: '5', + }); + } + private async refreshState( board: Board | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard @@ -145,7 +165,7 @@ export class Debug extends SketchContribution { } } - protected async startDebug( + private async startDebug( board: Board | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard ): Promise { @@ -183,8 +203,19 @@ export class Debug extends SketchContribution { }; return this.commandService.executeCommand('arduino.debug.start', config); } -} + private isOptimizeForDebug(): boolean { + return this.preferences.get('arduino.compile.optimizeForDebug'); + } + + private async toggleOptimizeForDebug(): Promise { + return this.preferenceService.set( + 'arduino.compile.optimizeForDebug', + !this.isOptimizeForDebug(), + PreferenceScope.User + ); + } +} export namespace Debug { export namespace Commands { export const START_DEBUGGING = Command.toLocalizedCommand( @@ -195,5 +226,13 @@ export namespace Debug { }, 'vscode/debug.contribution/startDebuggingHelp' ); + export const OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand( + { + id: 'arduino-optimize-for-debug', + label: 'Optimize for Debugging', + category: 'Arduino', + }, + 'arduino/debug/optimizeForDebugging' + ); } } diff --git a/arduino-ide-extension/src/browser/contributions/edit-contributions.ts b/arduino-ide-extension/src/browser/contributions/edit-contributions.ts index 6b77d5163..c66b41865 100644 --- a/arduino-ide-extension/src/browser/contributions/edit-contributions.ts +++ b/arduino-ide-extension/src/browser/contributions/edit-contributions.ts @@ -1,7 +1,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; 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 { Contribution, @@ -20,13 +19,10 @@ import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/edit @injectable() export class EditContributions extends Contribution { @inject(MonacoEditorService) - protected readonly codeEditorService: MonacoEditorService; + private readonly codeEditorService: MonacoEditorService; @inject(ClipboardService) - protected readonly clipboardService: ClipboardService; - - @inject(PreferenceService) - protected readonly preferences: PreferenceService; + private readonly clipboardService: ClipboardService; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts new file mode 100644 index 000000000..653d59577 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -0,0 +1,154 @@ +import { Mutex } from 'async-mutex'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + ArduinoDaemon, + BoardsService, + ExecutableService, +} from '../../common/protocol'; +import { HostedPluginEvents } from '../hosted-plugin-events'; +import { SketchContribution, URI } from './contribution'; +import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { BoardsConfig } from '../boards/boards-config'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; + +@injectable() +export class InoLanguage extends SketchContribution { + @inject(HostedPluginEvents) + private readonly hostedPluginEvents: HostedPluginEvents; + + @inject(ExecutableService) + private readonly executableService: ExecutableService; + + @inject(ArduinoDaemon) + private readonly daemon: ArduinoDaemon; + + @inject(BoardsService) + private readonly boardsService: BoardsService; + + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + + private languageServerFqbn?: string; + private languageServerStartMutex = new Mutex(); + + override onReady(): void { + const start = ( + { selectedBoard }: BoardsConfig.Config, + forceStart = false + ) => { + if (selectedBoard) { + const { name, fqbn } = selectedBoard; + if (fqbn) { + this.startLanguageServer(fqbn, name, forceStart); + } + } + }; + this.boardsServiceProvider.onBoardsConfigChanged(start); + this.hostedPluginEvents.onPluginsDidStart(() => + start(this.boardsServiceProvider.boardsConfig) + ); + this.hostedPluginEvents.onPluginsWillUnload( + () => (this.languageServerFqbn = undefined) + ); + this.preferences.onPreferenceChanged( + ({ preferenceName, oldValue, newValue }) => { + if (oldValue !== newValue) { + switch (preferenceName) { + case 'arduino.language.log': + case 'arduino.language.realTimeDiagnostics': + start(this.boardsServiceProvider.boardsConfig, true); + } + } + } + ); + start(this.boardsServiceProvider.boardsConfig); + } + + private async startLanguageServer( + fqbn: string, + name: string | undefined, + forceStart = false + ): Promise { + const port = await this.daemon.tryGetPort(); + if (!port) { + return; + } + const release = await this.languageServerStartMutex.acquire(); + try { + await this.hostedPluginEvents.didStart; + const details = await this.boardsService.getBoardDetails({ fqbn }); + if (!details) { + // Core is not installed for the selected board. + console.info( + `Could not start language server for ${fqbn}. The core is not installed for the board.` + ); + if (this.languageServerFqbn) { + try { + await this.commandService.executeCommand( + 'arduino.languageserver.stop' + ); + console.info( + `Stopped language server process for ${this.languageServerFqbn}.` + ); + this.languageServerFqbn = undefined; + } catch (e) { + console.error( + `Failed to start language server process for ${this.languageServerFqbn}`, + e + ); + throw e; + } + } + return; + } + if (!forceStart && fqbn === this.languageServerFqbn) { + // NOOP + return; + } + this.logger.info(`Starting language server: ${fqbn}`); + const log = this.preferences.get('arduino.language.log'); + let currentSketchPath: string | undefined = undefined; + if (log) { + const currentSketch = await this.sketchServiceClient.currentSketch(); + if (CurrentSketch.isValid(currentSketch)) { + currentSketchPath = await this.fileService.fsPath( + new URI(currentSketch.uri) + ); + } + } + const { clangdUri, lsUri } = await this.executableService.list(); + const [clangdPath, lsPath] = await Promise.all([ + this.fileService.fsPath(new URI(clangdUri)), + this.fileService.fsPath(new URI(lsUri)), + ]); + + this.languageServerFqbn = await Promise.race([ + new Promise((_, reject) => + setTimeout( + () => reject(new Error(`Timeout after ${20_000} ms.`)), + 20_000 + ) + ), + this.commandService.executeCommand( + 'arduino.languageserver.start', + { + lsPath, + cliDaemonAddr: `localhost:${port}`, + clangdPath, + log: currentSketchPath ? currentSketchPath : log, + cliDaemonInstance: '1', + board: { + fqbn, + name: name ? `"${name}"` : undefined, + }, + } + ), + ]); + } catch (e) { + console.log(`Failed to start language server for ${fqbn}`, e); + this.languageServerFqbn = undefined; + } finally { + release(); + } + } +} diff --git a/arduino-ide-extension/src/browser/contributions/open-boards-config.ts b/arduino-ide-extension/src/browser/contributions/open-boards-config.ts new file mode 100644 index 000000000..3b8e1294e --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/open-boards-config.ts @@ -0,0 +1,32 @@ +import { CommandRegistry } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { BoardsConfigDialog } from '../boards/boards-config-dialog'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { Contribution, Command } from './contribution'; + +@injectable() +export class OpenBoardsConfig extends Contribution { + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + + @inject(BoardsConfigDialog) + private readonly boardsConfigDialog: BoardsConfigDialog; + + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, { + execute: async (query?: string | undefined) => { + const boardsConfig = await this.boardsConfigDialog.open(query); + if (boardsConfig) { + this.boardsServiceProvider.boardsConfig = boardsConfig; + } + }, + }); + } +} +export namespace OpenBoardsConfig { + export namespace Commands { + export const OPEN_DIALOG: Command = { + id: 'arduino-open-boards-dialog', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts new file mode 100644 index 000000000..8fbea21ae --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts @@ -0,0 +1,109 @@ +import { nls } from '@theia/core/lib/common/nls'; +import { injectable } from '@theia/core/shared/inversify'; +import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager'; +import { SketchesError } from '../../common/protocol'; +import { + Command, + CommandRegistry, + SketchContribution, + URI, +} from './contribution'; +import { SaveAsSketch } from './save-as-sketch'; + +@injectable() +export class OpenSketchFiles extends SketchContribution { + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, { + execute: (uri: URI) => this.openSketchFiles(uri), + }); + registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, { + execute: ( + uri: string, + forceOpen?: boolean, + options?: EditorOpenerOptions + ) => { + this.ensureOpened(uri, forceOpen, options); + }, + }); + } + + private async openSketchFiles(uri: URI): Promise { + try { + const sketch = await this.sketchService.loadSketch(uri.toString()); + const { mainFileUri, rootFolderFileUris } = sketch; + for (const uri of [mainFileUri, ...rootFolderFileUris]) { + await this.ensureOpened(uri); + } + if (mainFileUri.endsWith('.pde')) { + const message = nls.localize( + 'arduino/common/oldFormat', + "The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?", + sketch.name + ); + const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); + this.messageService + .info(message, nls.localize('arduino/common/later', 'Later'), yes) + .then(async (answer) => { + if (answer === yes) { + this.commandService.executeCommand( + SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + { + execOnlyIfTemp: false, + openAfterMove: true, + wipeOriginal: false, + } + ); + } + }); + } + } catch (err) { + if (SketchesError.NotFound.is(err)) { + this.openFallbackSketch(); + } else { + console.error(err); + const message = + err instanceof Error + ? err.message + : typeof err === 'string' + ? err + : String(err); + this.messageService.error(message); + } + } + } + + private async openFallbackSketch(): Promise { + const sketch = await this.sketchService.createNewSketch(); + this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true }); + } + + private async ensureOpened( + uri: string, + forceOpen = false, + options?: EditorOpenerOptions + ): Promise { + const widget = this.editorManager.all.find( + (widget) => widget.editor.uri.toString() === uri + ); + if (!widget || forceOpen) { + return this.editorManager.open( + new URI(uri), + options ?? { + mode: 'reveal', + preview: false, + counter: 0, + } + ); + } + } +} +export namespace OpenSketchFiles { + export namespace Commands { + export const OPEN_SKETCH_FILES: Command = { + id: 'arduino-open-sketch-files', + }; + export const ENSURE_OPENED: Command = { + id: 'arduino-ensure-opened', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/selected-board.ts b/arduino-ide-extension/src/browser/contributions/selected-board.ts new file mode 100644 index 000000000..bf8a84ae8 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/selected-board.ts @@ -0,0 +1,54 @@ +import { + StatusBar, + StatusBarAlignment, +} from '@theia/core/lib/browser/status-bar/status-bar'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { BoardsConfig } from '../boards/boards-config'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { Contribution } from './contribution'; + +@injectable() +export class SelectedBoard extends Contribution { + @inject(StatusBar) + private readonly statusBar: StatusBar; + + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + + override onStart(): void { + this.boardsServiceProvider.onBoardsConfigChanged((config) => + this.update(config) + ); + } + + override onReady(): void { + this.update(this.boardsServiceProvider.boardsConfig); + } + + private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void { + this.statusBar.setElement('arduino-selected-board', { + alignment: StatusBarAlignment.RIGHT, + text: selectedBoard + ? `$(microchip) ${selectedBoard.name}` + : `$(close) ${nls.localize( + 'arduino/common/noBoardSelected', + 'No board selected' + )}`, + className: 'arduino-selected-board', + }); + if (selectedBoard) { + this.statusBar.setElement('arduino-selected-port', { + alignment: StatusBarAlignment.RIGHT, + text: selectedPort + ? nls.localize( + 'arduino/common/selectedOn', + 'on {0}', + selectedPort.address + ) + : nls.localize('arduino/common/notConnected', '[not connected]'), + className: 'arduino-selected-port', + }); + } + } +} diff --git a/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts new file mode 100644 index 000000000..ad7704adb --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts @@ -0,0 +1,69 @@ +import { SaveableWidget } from '@theia/core/lib/browser/saveable'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; +import { FileChangeType } from '@theia/filesystem/lib/common/files'; +import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { Sketch, SketchContribution, URI } from './contribution'; +import { OpenSketchFiles } from './open-sketch-files'; + +@injectable() +export class SketchFilesTracker extends SketchContribution { + @inject(FileSystemFrontendContribution) + private readonly fileSystemFrontendContribution: FileSystemFrontendContribution; + private readonly toDisposeOnStop = new DisposableCollection(); + + override onStart(): void { + this.fileSystemFrontendContribution.onDidChangeEditorFile( + ({ type, editor }) => { + if (type === FileChangeType.DELETED) { + const editorWidget = editor; + if (SaveableWidget.is(editorWidget)) { + editorWidget.closeWithoutSaving(); + } else { + editorWidget.close(); + } + } + } + ); + } + + override onReady(): void { + this.sketchServiceClient.currentSketch().then(async (sketch) => { + if ( + CurrentSketch.isValid(sketch) && + !(await this.sketchService.isTemp(sketch)) + ) { + this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri))); + this.toDisposeOnStop.push( + this.fileService.onDidFilesChange(async (event) => { + for (const { type, resource } of event.changes) { + if ( + type === FileChangeType.ADDED && + resource.parent.toString() === sketch.uri + ) { + const reloadedSketch = await this.sketchService.loadSketch( + sketch.uri + ); + if (Sketch.isInSketch(resource, reloadedSketch)) { + this.commandService.executeCommand( + OpenSketchFiles.Commands.ENSURE_OPENED.id, + resource.toString(), + true, + { + mode: 'open', + } + ); + } + } + } + }) + ); + } + }); + } + + onStop(): void { + this.toDisposeOnStop.dispose(); + } +} diff --git a/arduino-ide-extension/src/browser/contributions/sketchbook.ts b/arduino-ide-extension/src/browser/contributions/sketchbook.ts index 80dc99065..fdac918b7 100644 --- a/arduino-ide-extension/src/browser/contributions/sketchbook.ts +++ b/arduino-ide-extension/src/browser/contributions/sketchbook.ts @@ -5,7 +5,11 @@ import { ArduinoMenus } from '../menu/arduino-menus'; import { MainMenuManager } from '../../common/main-menu-manager'; import { NotificationCenter } from '../notification-center'; import { Examples } from './examples'; -import { SketchContainer } from '../../common/protocol'; +import { + SketchContainer, + SketchesError, + SketchRef, +} from '../../common/protocol'; import { OpenSketch } from './open-sketch'; import { nls } from '@theia/core/lib/common'; @@ -24,15 +28,14 @@ export class Sketchbook extends Examples { protected readonly notificationCenter: NotificationCenter; override onStart(): void { - this.sketchServiceClient.onSketchbookDidChange(() => { - this.sketchService.getSketches({}).then((container) => { - this.register(container); - this.mainMenuManager.update(); - }); - }); + this.sketchServiceClient.onSketchbookDidChange(() => this.update()); } override async onReady(): Promise { + this.update(); + } + + private update() { this.sketchService.getSketches({}).then((container) => { this.register(container); this.mainMenuManager.update(); @@ -59,11 +62,24 @@ export class Sketchbook extends Examples { protected override createHandler(uri: string): CommandHandler { return { execute: async () => { - const sketch = await this.sketchService.loadSketch(uri); - return this.commandService.executeCommand( - OpenSketch.Commands.OPEN_SKETCH.id, - sketch - ); + let sketch: SketchRef | undefined = undefined; + try { + sketch = await this.sketchService.loadSketch(uri); + } catch (err) { + if (SketchesError.NotFound.is(err)) { + // To handle the following: + // Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch. + // Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items. + this.messageService.error(err.message); + this.update(); + } + } + if (sketch) { + await this.commandService.executeCommand( + OpenSketch.Commands.OPEN_SKETCH.id, + sketch + ); + } }, }; } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index ebfd02c6d..0c16daf09 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -227,7 +227,9 @@ export class UploadSketch extends CoreServiceContribution { fqbn, }; let options: CoreService.Upload.Options | undefined = undefined; - const optimizeForDebug = this.editorMode.compileForDebug; + const optimizeForDebug = this.preferences.get( + 'arduino.compile.optimizeForDebug' + ); const { selectedPort } = boardsConfig; const port = selectedPort; const userFields = diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index b7f391bd5..638cbfa16 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -114,11 +114,14 @@ export class VerifySketch extends CoreServiceContribution { }; const verbose = this.preferences.get('arduino.compile.verbose'); const compilerWarnings = this.preferences.get('arduino.compile.warnings'); + const optimizeForDebug = this.preferences.get( + 'arduino.compile.optimizeForDebug' + ); this.outputChannelManager.getChannel('Arduino').clear(); await this.coreService.compile({ sketch, board, - optimizeForDebug: this.editorMode.compileForDebug, + optimizeForDebug, verbose, exportBinaries, sourceOverride, diff --git a/arduino-ide-extension/src/browser/create/create-api.ts b/arduino-ide-extension/src/browser/create/create-api.ts index 1e8740a96..1faf05754 100644 --- a/arduino-ide-extension/src/browser/create/create-api.ts +++ b/arduino-ide-extension/src/browser/create/create-api.ts @@ -507,7 +507,8 @@ export class CreateApi { } private domain(apiVersion = 'v2'): string { - const endpoint = this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint']; + const endpoint = + this.arduinoPreferences['arduino.cloud.sketchSyncEndpoint']; return `${endpoint}/${apiVersion}`; } diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx index 2b0b952bd..231687873 100644 --- a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -12,10 +12,10 @@ import { IDEUpdater, IDEUpdaterClient, ProgressInfo, + SKIP_IDE_VERSION, UpdateInfo, } from '../../../common/protocol/ide-updater'; import { LocalStorageService } from '@theia/core/lib/browser'; -import { SKIP_IDE_VERSION } from '../../arduino-frontend-contribution'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; @injectable() diff --git a/arduino-ide-extension/src/browser/editor-mode.ts b/arduino-ide-extension/src/browser/editor-mode.ts deleted file mode 100644 index 33ebef45f..000000000 --- a/arduino-ide-extension/src/browser/editor-mode.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; -import { - FrontendApplicationContribution, - FrontendApplication, -} from '@theia/core/lib/browser'; -import { MainMenuManager } from '../common/main-menu-manager'; - -@injectable() -export class EditorMode implements FrontendApplicationContribution { - @inject(MainMenuManager) - protected readonly mainMenuManager: MainMenuManager; - - protected app: FrontendApplication; - - onStart(app: FrontendApplication): void { - this.app = app; - } - - get compileForDebug(): boolean { - const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY); - return value === 'true'; - } - - async toggleCompileForDebug(): Promise { - const oldState = this.compileForDebug; - const newState = !oldState; - window.localStorage.setItem( - EditorMode.COMPILE_FOR_DEBUG_KEY, - String(newState) - ); - this.mainMenuManager.update(); - } -} - -export namespace EditorMode { - export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; -} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 50b57cc7a..fe5d9753f 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -134,21 +134,3 @@ button.secondary[disabled], .theia-button.secondary[disabled] { .fa-reload { font-size: 14px; } - -/* restore the old Theia spinner */ -/* https://github.com/eclipse-theia/theia/pull/10761#issuecomment-1131476318 */ -.old-theia-preload { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 50000; - background: var(--theia-editor-background); - background-image: var(--theia-preloader); - background-size: 60px 60px; - background-repeat: no-repeat; - background-attachment: fixed; - background-position: center; - transition: opacity 0.8s; -} diff --git a/arduino-ide-extension/src/browser/theia/core/frontend-application.ts b/arduino-ide-extension/src/browser/theia/core/frontend-application.ts index ba6b2f8bc..cb1a96206 100644 --- a/arduino-ide-extension/src/browser/theia/core/frontend-application.ts +++ b/arduino-ide-extension/src/browser/theia/core/frontend-application.ts @@ -4,7 +4,7 @@ import { CommandService } from '@theia/core/lib/common/command'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { SketchesService } from '../../../common/protocol'; -import { ArduinoCommands } from '../../arduino-commands'; +import { OpenSketchFiles } from '../../contributions/open-sketch-files'; @injectable() export class FrontendApplication extends TheiaFrontendApplication { @@ -25,33 +25,11 @@ export class FrontendApplication extends TheiaFrontendApplication { this.workspaceService.roots.then(async (roots) => { for (const root of roots) { await this.commandService.executeCommand( - ArduinoCommands.OPEN_SKETCH_FILES.id, + OpenSketchFiles.Commands.OPEN_SKETCH_FILES.id, root.resource ); this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu } }); } - - protected override getStartupIndicator( - host: HTMLElement - ): HTMLElement | undefined { - let startupElement = this.doGetStartupIndicator(host, 'old-theia-preload'); // https://github.com/eclipse-theia/theia/pull/10761#issuecomment-1131476318 - if (!startupElement) { - startupElement = this.doGetStartupIndicator(host, 'theia-preload'); // We show the new Theia spinner in dev mode. - } - return startupElement; - } - - private doGetStartupIndicator( - host: HTMLElement, - classNames: string - ): HTMLElement | undefined { - const elements = host.getElementsByClassName(classNames); - const first = elements[0]; - if (first instanceof HTMLElement) { - return first; - } - return undefined; - } } diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts index 3ba955c7c..38f15c9d1 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts @@ -21,7 +21,11 @@ import { import { BoardsServiceProvider } from '../../boards/boards-service-provider'; import { BoardsConfig } from '../../boards/boards-config'; import { FileStat } from '@theia/filesystem/lib/common/files'; -import { StartupTask } from '../../widgets/sketchbook/startup-task'; +import { + StartupTask, + StartupTasks, +} from '../../widgets/sketchbook/startup-task'; +import { setURL } from '../../utils/window'; @injectable() export class WorkspaceService extends TheiaWorkspaceService { @@ -60,6 +64,17 @@ export class WorkspaceService extends TheiaWorkspaceService { this.onCurrentWidgetChange({ newValue, oldValue: null }); } + protected override async toFileStat( + uri: string | URI | undefined + ): Promise { + const stat = await super.toFileStat(uri); + if (!stat) { + const newSketchUri = await this.sketchService.createNewSketch(); + return this.toFileStat(newSketchUri.uri); + } + return stat; + } + // Was copied from the Theia implementation. // Unlike the default behavior, IDE2 does not check the existence of the workspace before open. protected override async doGetDefaultWorkspaceUri(): Promise< @@ -78,6 +93,7 @@ export class WorkspaceService extends TheiaWorkspaceService { const wpPath = decodeURI(window.location.hash.substring(1)); const workspaceUri = new URI().withPath(wpPath).withScheme('file'); // ### Customization! Here, we do no check if the workspace exists. + // ### The error or missing sketch handling is done in the customized `toFileStat`. return workspaceUri.toString(); } else { // Else, ask the server for its suggested workspace (usually the one @@ -127,7 +143,7 @@ export class WorkspaceService extends TheiaWorkspaceService { protected override openWindow(uri: FileStat, options?: WorkspaceInput): void { const workspacePath = uri.resource.path.toString(); if (this.shouldPreserveWindow(options)) { - this.reloadWindow(); + this.reloadWindow(options); // Unlike Theia, IDE2 passes the `input` downstream. } else { try { this.openNewWindow(workspacePath, options); // Unlike Theia, IDE2 passes the `input` downstream. @@ -139,21 +155,25 @@ export class WorkspaceService extends TheiaWorkspaceService { } } + protected override reloadWindow(options?: WorkspaceInput): void { + if (StartupTasks.WorkspaceInput.is(options)) { + setURL(StartupTask.append(options.tasks, new URL(window.location.href))); + } + super.reloadWindow(); + } + protected override openNewWindow( workspacePath: string, options?: WorkspaceInput ): void { const { boardsConfig } = this.boardsServiceProvider; - const url = BoardsConfig.Config.setConfig( + let url = BoardsConfig.Config.setConfig( boardsConfig, new URL(window.location.href) ); // Set the current boards config for the new browser window. url.hash = workspacePath; - if (StartupTask.WorkspaceInput.is(options)) { - url.searchParams.set( - StartupTask.QUERY_STRING, - encodeURIComponent(JSON.stringify(options.tasks)) - ); + if (StartupTasks.WorkspaceInput.is(options)) { + url = StartupTask.append(options.tasks, url); } this.windowService.openNewWindow(url.toString()); diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-variable-contribution.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-variable-contribution.ts index 1ca0b7b85..2f9c88800 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-variable-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-variable-contribution.ts @@ -10,21 +10,31 @@ import { CurrentSketch, SketchesServiceClientImpl, } from '../../../common/protocol/sketches-service-client-impl'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; @injectable() export class WorkspaceVariableContribution extends TheiaWorkspaceVariableContribution { @inject(SketchesServiceClientImpl) - protected readonly sketchesServiceClient: SketchesServiceClientImpl; + private readonly sketchesServiceClient: SketchesServiceClientImpl; - protected currentSketch?: Sketch; + private currentSketch?: Sketch; @postConstruct() protected override init(): void { - this.sketchesServiceClient.currentSketch().then((sketch) => { - if (CurrentSketch.isValid(sketch)) { - this.currentSketch = sketch; - } - }); + const sketch = this.sketchesServiceClient.tryGetCurrentSketch(); + if (CurrentSketch.isValid(sketch)) { + this.currentSketch = sketch; + } else { + const toDispose = new DisposableCollection(); + toDispose.push( + this.sketchesServiceClient.onCurrentSketchDidChange((sketch) => { + if (CurrentSketch.isValid(sketch)) { + this.currentSketch = sketch; + } + toDispose.dispose(); + }) + ); + } } override getResourceUri(): URI | undefined { diff --git a/arduino-ide-extension/src/browser/utils/window.ts b/arduino-ide-extension/src/browser/utils/window.ts new file mode 100644 index 000000000..54e046724 --- /dev/null +++ b/arduino-ide-extension/src/browser/utils/window.ts @@ -0,0 +1,7 @@ +/** + * Changes the `window.location` without navigating away. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function setURL(url: URL, data: any = {}): void { + history.pushState(data, '', url); +} diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts index 47ab60dbc..25cf33a3e 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts @@ -1,36 +1,86 @@ import { injectable } from '@theia/core/shared/inversify'; import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser'; import { Contribution } from '../../contributions/contribution'; +import { setURL } from '../../utils/window'; -export interface Task { +@injectable() +export class StartupTasks extends Contribution { + override onReady(): void { + const tasks = StartupTask.get(new URL(window.location.href)); + console.log(`Executing startup tasks: ${JSON.stringify(tasks)}`); + tasks.forEach(({ command, args = [] }) => + this.commandService + .executeCommand(command, ...args) + .catch((err) => + console.error( + `Error occurred when executing the startup task '${command}'${ + args?.length ? ` with args: '${JSON.stringify(args)}` : '' + }.`, + err + ) + ) + ); + if (tasks.length) { + // Remove the startup tasks after the execution. + // Otherwise, IDE2 executes them again on a window reload event. + setURL(StartupTask.set([], new URL(window.location.href))); + console.info(`Removed startup tasks from URL.`); + } + } +} + +export interface StartupTask { command: string; /** - * This must be JSON serializable. + * Must be JSON serializable. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any args?: any[]; } - -@injectable() -export class StartupTask extends Contribution { - override onReady(): void { - const params = new URLSearchParams(window.location.search); - const encoded = params.get(StartupTask.QUERY_STRING); - if (!encoded) return; - - const commands = JSON.parse(decodeURIComponent(encoded)); - - if (Array.isArray(commands)) { - commands.forEach(({ command, args }) => { - this.commandService.executeCommand(command, ...args); - }); +export namespace StartupTask { + const QUERY = 'startupTasks'; + export function is(arg: unknown): arg is StartupTasks { + if (typeof arg === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = arg as any; + return 'command' in object && typeof object['command'] === 'string'; + } + return false; + } + export function get(url: URL): StartupTask[] { + const { searchParams } = url; + const encodedTasks = searchParams.get(QUERY); + if (encodedTasks) { + const rawTasks = decodeURIComponent(encodedTasks); + const tasks = JSON.parse(rawTasks); + if (Array.isArray(tasks)) { + return tasks.filter((task) => { + if (StartupTask.is(task)) { + return true; + } + console.warn(`Was not a task: ${JSON.stringify(task)}. Ignoring.`); + return false; + }); + } else { + debugger; + console.warn(`Startup tasks was not an array: ${rawTasks}. Ignoring.`); + } } + return []; + } + export function set(tasks: StartupTask[], url: URL): URL { + const copy = new URL(url); + copy.searchParams.set(QUERY, encodeURIComponent(JSON.stringify(tasks))); + return copy; + } + export function append(tasks: StartupTask[], url: URL): URL { + return set([...get(url), ...tasks], url); } } -export namespace StartupTask { - export const QUERY_STRING = 'startupTasks'; + +export namespace StartupTasks { export interface WorkspaceInput extends TheiaWorkspaceInput { - tasks: Task[]; + tasks: StartupTask[]; } export namespace WorkspaceInput { export function is( diff --git a/arduino-ide-extension/src/common/protocol/ide-updater.ts b/arduino-ide-extension/src/common/protocol/ide-updater.ts index 7af4f7cb7..e1c79b188 100644 --- a/arduino-ide-extension/src/common/protocol/ide-updater.ts +++ b/arduino-ide-extension/src/common/protocol/ide-updater.ts @@ -69,3 +69,5 @@ export interface IDEUpdaterClient { notifyDownloadProgressChanged(message: ProgressInfo): void; notifyDownloadFinished(message: UpdateInfo): void; } + +export const SKIP_IDE_VERSION = 'skipIDEVersion'; diff --git a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts index 1e50729d6..0787fe1ab 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts @@ -17,6 +17,7 @@ import { } from '../../browser/utils/constants'; import * as monaco from '@theia/monaco-editor-core'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; const READ_ONLY_FILES = ['sketch.json']; const READ_ONLY_FILES_REMOTE = ['thingProperties.h', 'thingsProperties.h']; @@ -47,7 +48,9 @@ export class SketchesServiceClientImpl @inject(ConfigService) protected readonly configService: ConfigService; - protected toDispose = new DisposableCollection(); + @inject(FrontendApplicationStateService) + private readonly appStateService: FrontendApplicationStateService; + protected sketches = new Map(); // TODO: rename this + event to the `onBlabla` pattern protected sketchbookDidChangeEmitter = new Emitter<{ @@ -55,8 +58,16 @@ export class SketchesServiceClientImpl removed: SketchRef[]; }>(); readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event; + protected currentSketchDidChangeEmitter = new Emitter(); + readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event; + + protected toDispose = new DisposableCollection( + this.sketchbookDidChangeEmitter, + this.currentSketchDidChangeEmitter + ); - private _currentSketch = new Deferred(); + private _currentSketch: CurrentSketch | undefined; + private currentSketchLoaded = new Deferred(); onStart(): void { this.configService.getConfiguration().then(({ sketchDirUri }) => { @@ -110,9 +121,14 @@ export class SketchesServiceClientImpl ); }); }); - this.loadCurrentSketch().then((currentSketch) => - this._currentSketch.resolve(currentSketch) - ); + this.appStateService + .reachedState('started_contributions') + .then(async () => { + const currentSketch = await this.loadCurrentSketch(); + this._currentSketch = currentSketch; + this.currentSketchDidChangeEmitter.fire(this._currentSketch); + this.currentSketchLoaded.resolve(this._currentSketch); + }); } onStop(): void { @@ -143,7 +159,11 @@ export class SketchesServiceClientImpl } async currentSketch(): Promise { - return this._currentSketch.promise; + return this.currentSketchLoaded.promise; + } + + tryGetCurrentSketch(): CurrentSketch | undefined { + return this._currentSketch; } async currentSketchFile(): Promise { diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index eb07572d7..0394e6e94 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -1,5 +1,21 @@ +import { ApplicationError } from '@theia/core/lib/common/application-error'; import URI from '@theia/core/lib/common/uri'; +export namespace SketchesError { + export const Codes = { + NotFound: 5001, + }; + export const NotFound = ApplicationError.declare( + Codes.NotFound, + (message: string, uri: string) => { + return { + message, + data: { uri }, + }; + } + ); +} + export const SketchesServicePath = '/services/sketches-service'; export const SketchesService = Symbol('SketchesService'); export interface SketchesService { diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index c7c74724b..f346f5261 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -10,12 +10,13 @@ import { promisify } from 'util'; import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; -import { ConfigService } from '../common/protocol/config-service'; +import { ConfigServiceImpl } from './config-service-impl'; import { SketchesService, Sketch, SketchRef, SketchContainer, + SketchesError, } from '../common/protocol/sketches-service'; import { firstToLowerCase } from '../common/utils'; import { NotificationServiceServerImpl } from './notification-service-server'; @@ -28,6 +29,7 @@ import { import { duration } from '../common/decorators'; import * as glob from 'glob'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { ServiceError } from './service-error'; const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/; @@ -48,8 +50,8 @@ export class SketchesServiceImpl ? tempDir : maybeNormalizeDrive(fs.realpathSync.native(tempDir)); - @inject(ConfigService) - protected readonly configService: ConfigService; + @inject(ConfigServiceImpl) + protected readonly configService: ConfigServiceImpl; @inject(NotificationServiceServerImpl) protected readonly notificationService: NotificationServiceServerImpl; @@ -201,7 +203,18 @@ export class SketchesServiceImpl const sketch = await new Promise((resolve, reject) => { client.loadSketch(req, async (err, resp) => { if (err) { - reject(err); + reject( + isNotFoundError(err) + ? SketchesError.NotFound( + fixErrorMessage( + err, + requestSketchPath, + this.configService.cliConfiguration?.directories.user + ), + uri + ) + : err + ); return; } const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath()); @@ -448,26 +461,15 @@ void loop() { private async _isSketchFolder( uri: string ): Promise { - const fsPath = FileUri.fsPath(uri); - let stat: fs.Stats | undefined; try { - stat = await promisify(fs.lstat)(fsPath); - } catch {} - if (stat && stat.isDirectory()) { - const basename = path.basename(fsPath); - const files = await promisify(fs.readdir)(fsPath); - for (let i = 0; i < files.length; i++) { - if (files[i] === basename + '.ino' || files[i] === basename + '.pde') { - try { - const sketch = await this.loadSketch( - FileUri.create(fsPath).toString() - ); - return sketch; - } catch {} - } + const sketch = await this.loadSketch(uri); + return sketch; + } catch (err) { + if (SketchesError.NotFound.is(err)) { + return undefined; } + throw err; } - return undefined; } async isTemp(sketch: SketchRef): Promise { @@ -588,6 +590,40 @@ interface SketchWithDetails extends Sketch { readonly mtimeMs: number; } +// https://github.com/arduino/arduino-cli/issues/1797 +function fixErrorMessage( + err: ServiceError, + sketchPath: string, + sketchbookPath: string | undefined +): string { + if (!sketchbookPath) { + return err.details; // No way to repair the error message. The current sketchbook path is not available. + } + // Original: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing /Users/a.kitta/Documents/Arduino/Arduino.ino` + // Fixed: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing $sketchPath` + const message = err.details; + const incorrectMessageSuffix = path.join(sketchbookPath, 'Arduino.ino'); + if ( + message.startsWith("Can't open sketch: no valid sketch found in") && + message.endsWith(`${incorrectMessageSuffix}`) + ) { + const sketchName = path.basename(sketchPath); + const correctMessagePrefix = message.substring( + 0, + message.length - incorrectMessageSuffix.length + ); + return `${correctMessagePrefix}${path.join( + sketchPath, + `${sketchName}.ino` + )}`; + } + return err.details; +} + +function isNotFoundError(err: unknown): err is ServiceError { + return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html +} + /** * If on Windows, will change the input `C:\\path\\to\\somewhere` to `c:\\path\\to\\somewhere`. */ diff --git a/electron/build/scripts/patch-theia-preload.js b/electron/build/scripts/patch-theia-preload.js deleted file mode 100644 index e4b94d7c9..000000000 --- a/electron/build/scripts/patch-theia-preload.js +++ /dev/null @@ -1,16 +0,0 @@ -// Patch the Theia spinner: https://github.com/eclipse-theia/theia/pull/10761#issuecomment-1131476318 -// Replaces the `theia-preload` selector with `old-theia-preload` in the generated `index.html`. -let arg = process.argv.splice(2)[0] -if (!arg) { - console.error("The path to the index.html to patch is missing. Use 'node patch-theia-preload.js ./path/to/index.html'") - process.exit(1) -} -(async () => { - const { promises: fs } = require('fs') - const path = require('path') - const index = path.isAbsolute(arg) ? arg : path.join(process.cwd(), arg) - console.log(`>>> Patching 'theia-preload' with 'old-theia-preload' in ${index}.`) - const content = await fs.readFile(index, { encoding: 'utf-8' }) - await fs.writeFile(index, content.replace(/theia-preload/g, 'old-theia-preload'), { encoding: 'utf-8' }) - console.log(`<<< Successfully patched index.html.`) -})() \ No newline at end of file diff --git a/electron/build/template-package.json b/electron/build/template-package.json index 5a235f783..2c8224fff 100644 --- a/electron/build/template-package.json +++ b/electron/build/template-package.json @@ -23,7 +23,7 @@ "package": "cross-env DEBUG=* && electron-builder --publish=never", "package:publish": "cross-env DEBUG=* && electron-builder --publish=always", "download:plugins": "theia download:plugins", - "patch": "ncp ./patch/backend/main.js ./src-gen/backend/main.js && node ./scripts/patch-theia-preload.js ./lib/index.html" + "patch": "ncp ./patch/backend/main.js ./src-gen/backend/main.js" }, "engines": { "node": ">=14.0.0 <15" diff --git a/i18n/en.json b/i18n/en.json index 8654a9c97..b323b1d94 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -253,9 +253,10 @@ "cloud.pull.warn": "True if users should be warned before pulling a cloud sketch. Defaults to true.", "cloud.push.warn": "True if users should be warned before pushing a cloud sketch. Defaults to true.", "cloud.pushpublic.warn": "True if users should be warned before pushing a public sketch to the cloud. Defaults to true.", - "cloud.sketchSyncEnpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.", + "cloud.sketchSyncEndpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.", "compile": "compile", "compile.experimental": "True if the IDE should handle multiple compiler errors. False by default", + "compile.optimizeForDebug": "Optimize compile output for debug, not for release. It's 'false' by default.", "compile.revealRange": "Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.", "compile.verbose": "True for verbose compile output. False by default", "compile.warnings": "Tells gcc which warning level to use. It's 'None' by default",