#1089: IDE2 falls back to new sketch if opening failed. (#1152)

IDE2 falls back to a new sketch if the opening fails.

Closes #1089

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-07-18 11:10:33 +02:00 committed by GitHub
parent fe31d15b9f
commit 8ad10b5adf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 881 additions and 659 deletions

View File

@ -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',
};
}

View File

@ -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<void | 'No' | 'Yes' | undefined>
| undefined;
protected toDisposeOnStop = new DisposableCollection();
@postConstruct()
protected async init(): Promise<void> {
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<void> {
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<string>(
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<void> {
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<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandRegistry.executeCommand<string>(
'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
<BoardsToolBarItem
key="boardsToolbarItem"
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl}
boardsServiceProvider={this.boardsServiceProvider}
/>
),
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<void> {
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<unknown> {
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',

View File

@ -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.

View File

@ -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;

View File

@ -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<BoardsConfig.Config>();
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
);
}

View File

@ -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<HTMLElement>) => {
protected readonly show = (event: React.MouseEvent<HTMLElement>): 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 && <div className={procolIconClassNames} />}
{protocolIcon && <div className={protocolIconClassNames} />}
<div
className={classNames(
'arduino-boards-toolbar-item--label',
@ -245,12 +245,12 @@ export class BoardsToolBarItem extends React.Component<
...board,
onClick: () => {
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;
}

View File

@ -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<string>(
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
)
);
});
}
}

View File

@ -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<this>();
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();

View File

@ -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;

View File

@ -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<string | undefined>();
protected onDisabledMessageDidChange =
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
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<void> {
@ -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<void> {
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'
);
}
}

View File

@ -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, {

View File

@ -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<void> {
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<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandService.executeCommand<string>(
'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();
}
}
}

View File

@ -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',
};
}
}

View File

@ -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<void> {
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<void> {
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<unknown> {
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',
};
}
}

View File

@ -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',
});
}
}
}

View File

@ -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();
}
}

View File

@ -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<void> {
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
);
}
},
};
}

View File

@ -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 =

View File

@ -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,

View File

@ -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}`;
}

View File

@ -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()

View File

@ -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<void> {
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';
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<FileStat | undefined> {
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());

View File

@ -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 {

View File

@ -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);
}

View File

@ -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(

View File

@ -69,3 +69,5 @@ export interface IDEUpdaterClient {
notifyDownloadProgressChanged(message: ProgressInfo): void;
notifyDownloadFinished(message: UpdateInfo): void;
}
export const SKIP_IDE_VERSION = 'skipIDEVersion';

View File

@ -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<string, SketchRef>();
// 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<CurrentSketch>();
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
private _currentSketch = new Deferred<CurrentSketch>();
protected toDispose = new DisposableCollection(
this.sketchbookDidChangeEmitter,
this.currentSketchDidChangeEmitter
);
private _currentSketch: CurrentSketch | undefined;
private currentSketchLoaded = new Deferred<CurrentSketch>();
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<CurrentSketch> {
return this._currentSketch.promise;
return this.currentSketchLoaded.promise;
}
tryGetCurrentSketch(): CurrentSketch | undefined {
return this._currentSketch;
}
async currentSketchFile(): Promise<string | undefined> {

View File

@ -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 {

View File

@ -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<SketchWithDetails>((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<SketchWithDetails | undefined> {
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<boolean> {
@ -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`.
*/

View File

@ -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.`)
})()

View File

@ -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"

View File

@ -254,9 +254,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",