Compare commits

...

19 Commits

Author SHA1 Message Date
Alberto Iannaccone
439cdfbbff 2.0.0-rc8 (#1105) 2022-06-23 11:33:27 +02:00
Alberto Iannaccone
672fd4e4b0 bump arduino-cli version to 0.24.0 (#1103) 2022-06-23 10:59:07 +02:00
David Simpson
0f1d379e58 reference cli rc1 0.24 in package.json (#1098) 2022-06-22 18:54:30 +02:00
Francesco Spissu
a79c9b4449 Sketchbook tree indentation (#1097) 2022-06-22 18:23:14 +02:00
Akos Kitta
0f8a29a493 Disabled MenuItem roles on macOS.
Closes #969
Upstream-ref: eclipse-theia/theia#11217

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 12:17:36 +02:00
David Simpson
a54d7c8f45 #1032 failing upload flag for monitor mgr (#1040)
* 1032 failing upload flag for monitor mgr

* move upload failure fix logic to frontend

* misc corrections

* avoid starting monitor when upload is in progress

* avoid starting monitor when upload is in progress

* prevent monitor side effects on upload (WIP)

* send upload req after notifying mgr

* dispose instead of pause on upld (code not final)

* Revert "dispose instead of pause on upld (code not final)"

This reverts commit 2d5dff2a2d.

* force wait before upload (test)

* always start queued services after uplaod finishes

* test cli with monitor close delay

* clean up unnecessary await(s)

* remove unused dependency

* revert CLI to 0.23

* use master cli for testing, await in upload finish

* remove upload port from pending monitor requests

* fix startQueuedServices

* refinements queued monitors

* clean up monitor mgr state

* fix typo from prev cleanup

* avoid dupl queued monitor services

* variable name changes

* reference latest cli commit in package.json

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2022-06-22 10:39:14 +02:00
Akos Kitta
84109e416a Fixed widget lookup to eliminate duplicate tabs.
- Removed `@theia/editor-preview`,
 - Patched opener options when repairing layout on start, and
 - Compare widget keys with deepEquals instead of string equal.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 10:11:11 +02:00
Francesco Spissu
083337de1c IDE updater dialog colors fix (#1092) 2022-06-21 18:04:28 +02:00
Francesco Spissu
bd6bc135fd Remote sketchbook tooltips (#1088)
* rename Cloud Sketchbook in Remote Sketchbook

* add tooltips for Sync and Account buttons
2022-06-21 18:03:39 +02:00
Akos Kitta
4611381a38 Merged in #1074.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
Akos Kitta
d6f4096cd0 Reveal the error location after on failed verify.
Closes #608
Closes #229

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
David Simpson
a715da3d18 flush on clear output buffer (#1074) 2022-06-20 09:32:10 +02:00
Akos Kitta
94ceefd960 Can enable debug logging of the gRPC calls.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 14:14:47 +02:00
Akos Kitta
27dd120e5d Cleaned up File menu.
Removed:
 - `New File`,
 - `New Window`.

Closes #1014.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 13:11:08 +02:00
Francesco Spissu
f5cee97fef Implement survey notification (#1035) 2022-06-17 10:17:42 +02:00
Akos Kitta
a9aac0dbb0 Bound the original handler to this.
Closes #977

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-14 14:58:48 +02:00
David Simpson
4c6243176c Output panel optimisation (#1058)
* test interval for output panel

* create buffer provider

* output panel buffer corrections

* output buffer cleanup

* code cleanup
2022-06-14 13:00:20 +02:00
Akos Kitta
a8047660a6 Restored the Settings UI. Deferred model loading.
Closes #1031

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:08:35 +02:00
Akos Kitta
7c2843f7fd Relaxed the error handling of the core client init
For example, `malformed custom board options` was incorrectly detected
as loading JSON index file error.

Closes #1036

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:06:58 +02:00
50 changed files with 2351 additions and 514 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc7",
"version": "2.0.0-rc8",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -25,7 +25,6 @@
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/editor-preview": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
@@ -43,6 +42,7 @@
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
@@ -63,6 +63,7 @@
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fast-safe-stringify": "^2.1.1",
@@ -155,7 +156,7 @@
],
"arduino": {
"cli": {
"version": "0.23.0"
"version": "0.24.0"
},
"fwuploader": {
"version": "2.2.0"

View File

@@ -42,8 +42,8 @@ import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@th
import { KeymapsFrontendContribution } from './theia/keymaps/keymaps-frontend-contribution';
import { KeymapsFrontendContribution as TheiaKeymapsFrontendContribution } from '@theia/keymaps/lib/browser/keymaps-frontend-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { EditorPreviewContribution } from './theia/editor/editor-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
@@ -121,6 +121,7 @@ import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { SurveyNotification } from './contributions/survey-notification';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
import { OpenSketchExternal } from './contributions/open-sketch-external';
@@ -291,6 +292,16 @@ import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-gen
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
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';
MonacoThemingService.register({
id: 'arduino-theme',
@@ -423,6 +434,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(CoreErrorHandler).toSelf().inSingletonScope();
// Serial monitor
bind(MonitorWidget).toSelf();
@@ -475,6 +487,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
// Survey notification
bind(SurveyNotification).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SurveyNotification);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution)
@@ -486,9 +511,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaKeymapsFrontendContribution)
.to(KeymapsFrontendContribution)
.inSingletonScope();
rebind(TheiaEditorPreviewContribution)
.to(EditorPreviewContribution)
.inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaMonacoStatusBarContribution)
.to(MonacoStatusBarContribution)
.inSingletonScope();
@@ -587,6 +610,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
// To remove `New Window` from the `File` menu
bind(WindowContribution).toSelf().inSingletonScope();
rebind(TheiaWindowContribution).toService(WindowContribution);
bind(ArduinoDaemon)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -670,6 +697,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
// 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.
@@ -763,6 +791,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// Preferences
bindArduinoPreferences(bind);

View File

@@ -13,6 +13,32 @@ export enum UpdateChannel {
Stable = 'stable',
Nightly = 'nightly',
}
export const ErrorRevealStrategyLiterals = [
/**
* Scroll vertically as necessary and reveal a line.
*/
'auto',
/**
* Scroll vertically as necessary and reveal a line centered vertically.
*/
'center',
/**
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
*/
'top',
/**
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
}
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
@@ -33,6 +59,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/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}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
@@ -174,12 +217,30 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
},
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
@@ -198,6 +259,8 @@ export interface ArduinoConfiguration {
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@@ -1,11 +1,9 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -13,20 +11,13 @@ import {
import { nls } from '@theia/core/lib/common';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
export class BurnBootloader extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected override readonly outputChannelManager: OutputChannelManager;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(),
@@ -62,7 +53,7 @@ export class BurnBootloader extends SketchContribution {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
};
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
board,
@@ -81,13 +72,7 @@ export class BurnBootloader extends SketchContribution {
}
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
}
}
}

View File

@@ -0,0 +1,656 @@
import {
Command,
CommandRegistry,
Disposable,
DisposableCollection,
Emitter,
MaybePromise,
nls,
notEmpty,
} from '@theia/core';
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Location,
Range,
} from '@theia/core/shared/vscode-languageserver-protocol';
import {
EditorWidget,
TextDocumentChangeEvent,
} from '@theia/editor/lib/browser';
import {
EditorDecoration,
TrackedRangeStickiness,
} from '@theia/editor/lib/browser/decorations/editor-decoration';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import * as monaco from '@theia/monaco-editor-core';
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 { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { Contribution } from './contribution';
import { CoreErrorHandler } from './core-error-handler';
interface ErrorDecoration {
/**
* This is the unique ID of the decoration given by `monaco`.
*/
readonly id: string;
/**
* The resource this decoration belongs to.
*/
readonly uri: string;
}
namespace ErrorDecoration {
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
): Promise<monaco.Range | undefined>;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: MonacoEditor
): monaco.Range | undefined;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider:
| ((uri: string) => Promise<MonacoEditor | undefined>)
| MonacoEditor
): MaybePromise<monaco.Range | undefined> {
if (editorProvider instanceof MonacoEditor) {
const control = editorProvider.getControl();
const model = control.getModel();
if (model) {
return control
.getDecorationsInRange(fullRange(model))
?.find(({ id: candidateId }) => id === candidateId)?.range;
}
return undefined;
}
return editorProvider(uri).then((editor) => {
if (editor) {
return rangeOf({ id, uri }, editor);
}
return undefined;
});
}
// export async function rangeOf(
// { id, uri }: ErrorDecoration,
// editorProvider:
// | ((uri: string) => Promise<MonacoEditor | undefined>)
// | MonacoEditor
// ): Promise<monaco.Range | undefined> {
// const editor =
// editorProvider instanceof MonacoEditor
// ? editorProvider
// : await editorProvider(uri);
// if (editor) {
// const control = editor.getControl();
// const model = control.getModel();
// if (model) {
// return control
// .getDecorationsInRange(fullRange(model))
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
// }
// }
// return undefined;
// }
export function sameAs(
left: ErrorDecoration,
right: ErrorDecoration
): boolean {
return left.id === right.id && left.uri === right.uri;
}
}
@injectable()
export class CompilerErrors
extends Contribution
implements monaco.languages.CodeLensProvider
{
@inject(EditorManager)
private readonly editorManager: EditorManager;
@inject(ProtocolToMonacoConverter)
private readonly p2m: ProtocolToMonacoConverter;
@inject(MonacoToProtocolConverter)
private readonly mp2: MonacoToProtocolConverter;
@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>();
private readonly onCurrentErrorDidChange =
this.currentErrorDidChangEmitter.event;
private readonly toDisposeOnCompilerErrorDidChange =
new DisposableCollection();
private shell: ApplicationShell | undefined;
private revealStrategy = ErrorRevealStrategy.Default;
private currentError: ErrorDecoration | undefined;
private get currentErrorIndex(): number {
const current = this.currentError;
if (!current) {
return -1;
}
return this.errors.findIndex((error) =>
ErrorDecoration.sameAs(error, current)
);
}
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
monaco.languages.registerCodeLensProvider(InoSelector, this);
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
);
this.onCurrentErrorDidChange(async (error) => {
const range = await ErrorDecoration.rangeOf(error, (uri) =>
this.monacoEditor(uri)
);
if (!range) {
console.warn(
'compiler-errors',
`Could not find range of decoration: ${error.id}`
);
return;
}
const editor = await this.revealLocationInEditor({
uri: error.uri,
range: this.mp2.asRange(range),
});
if (!editor) {
console.warn(
'compiler-errors',
`Failed to mark error ${error.id} as the current one.`
);
}
});
this.preferences.ready.then(() => {
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.compile.revealRange') {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
}
});
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to next error. Unknown current error.`
);
return;
}
const nextError =
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
this.markAsCurrentError(nextError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to previous error. Unknown current error.`
);
return;
}
const previousError =
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
this.markAsCurrentError(previousError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
}
get onDidChange(): monaco.IEvent<this> {
return this.onDidChangeEmitter.event;
}
async provideCodeLenses(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.CodeLensList> {
const lenses: monaco.languages.CodeLens[] = [];
if (
this.currentError &&
this.currentError.uri === model.uri.toString() &&
this.errors.length > 1
) {
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
this.monacoEditor(uri)
);
if (range) {
lenses.push(
{
range,
command: {
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
title: nls.localize(
'arduino/editor/previousError',
'Previous Error'
),
arguments: [this.currentError],
},
},
{
range,
command: {
id: CompilerErrors.Commands.NEXT_ERROR.id,
title: nls.localize('arduino/editor/nextError', 'Next Error'),
arguments: [this.currentError],
},
}
);
}
}
return {
lenses,
dispose: () => {
/* NOOP */
},
};
}
private async handleCompilerErrorsDidChange(
errors: CoreError.Compiler[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource(
await this.filter(errors)
);
const decorations = await this.decorateEditors(compilerErrorsPerResource);
this.errors.push(...decorations.errors);
this.toDisposeOnCompilerErrorDidChange.pushAll([
Disposable.create(() => (this.errors.length = 0)),
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
...(await Promise.all([
decorations.dispose,
this.trackEditors(
compilerErrorsPerResource,
(editor) =>
editor.editor.onSelectionChanged((selection) =>
this.handleSelectionChange(editor, selection)
),
(editor) =>
editor.onDidDispose(() =>
this.handleEditorDidDispose(editor.editor.uri.toString())
),
(editor) =>
editor.editor.onDocumentContentChanged((event) =>
this.handleDocumentContentChange(editor, event)
)
),
])),
]);
const currentError = this.errors[0];
if (currentError) {
await this.markAsCurrentError(currentError);
}
}
private async filter(
errors: CoreError.Compiler[]
): Promise<CoreError.Compiler[]> {
if (!errors.length) {
return [];
}
await this.preferences.ready;
if (this.preferences['arduino.compile.experimental']) {
return errors;
}
// Always shows maximum one error; hence the code lens navigation is unavailable.
return [errors[0]];
}
private async decorateEditors(
errors: Map<string, CoreError.Compiler[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) =>
this.decorateEditor(uri, errors)
)
);
return {
dispose: new DisposableCollection(
...composite.map(({ dispose }) => dispose)
),
errors: composite.reduce(
(acc, { errors }) => acc.concat(errors),
[] as ErrorDecoration[]
),
};
}
private async decorateEditor(
uri: string,
errors: CoreError.Compiler[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
return { dispose: Disposable.NULL, errors: [] };
}
const oldDecorations = editor.editor.deltaDecorations({
oldDecorations: [],
newDecorations: errors.map((error) =>
this.compilerErrorDecoration(error.location.range)
),
});
return {
dispose: Disposable.create(() => {
if (editor) {
editor.editor.deltaDecorations({
oldDecorations,
newDecorations: [],
});
}
}),
errors: oldDecorations.map((id) => ({ id, uri })),
};
}
private compilerErrorDecoration(range: Range): EditorDecoration {
return {
range,
options: {
isWholeLine: true,
className: 'compiler-error',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
},
};
}
/**
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
*/
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
const uri = monacoEditor.uri.toString();
const monacoSelection = this.p2m.asRange(selection);
console.log(
'compiler-errors',
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
);
const calculatePriority = (
candidateErrorRange: monaco.Range,
currentSelection: monaco.Range
) => {
console.trace(
'compiler-errors',
`Candidate error range: ${candidateErrorRange.toJSON()}`
);
console.trace(
'compiler-errors',
`Current selection range: ${currentSelection.toJSON()}`
);
if (candidateErrorRange.intersectRanges(currentSelection)) {
console.trace('Intersects.');
return { score: 2 };
}
if (
candidateErrorRange.startLineNumber <=
currentSelection.startLineNumber &&
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
) {
console.trace('Same line.');
return { score: 1 };
}
console.trace('No match');
return undefined;
};
const error = this.errors
.filter((error) => error.uri === uri)
.map((error) => ({
error,
range: ErrorDecoration.rangeOf(error, monacoEditor),
}))
.map(({ error, range }) => {
if (range) {
const priority = calculatePriority(range, monacoSelection);
if (priority) {
return { ...priority, error };
}
}
return undefined;
})
.filter(notEmpty)
.sort((left, right) => right.score - left.score) // highest first
.map(({ error }) => error)
.shift();
if (error) {
this.markAsCurrentError(error);
} else {
console.info(
'compiler-errors',
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
);
}
}
/**
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
* If editor closes, delete the decorators.
*/
private handleEditorDidDispose(uri: string): void {
let i = this.errors.length;
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
while (i--) {
const error = this.errors[i];
if (error.uri === uri) {
this.errors.splice(i, 1);
}
}
this.onDidChangeEmitter.fire(this);
}
/**
* If a document change "destroys" the range of the decoration, the decoration must be removed.
*/
private handleDocumentContentChange(
editor: EditorWidget,
event: TextDocumentChangeEvent
): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
// A decoration location can be "destroyed", hence should be deleted when:
// - deleting range (start != end AND text is empty)
// - inserting text into range (start != end AND text is not empty)
// Filter unrelated delta changes to spare the CPU.
const relevantChanges = event.contentChanges.filter(
({ range: { start, end } }) =>
start.line !== end.line || start.character !== end.character
);
if (!relevantChanges.length) {
return;
}
const resolvedMarkers = this.errors
.filter((error) => error.uri === event.document.uri)
.map((error, index) => {
const range = ErrorDecoration.rangeOf(error, monacoEditor);
if (range) {
return { error, range, index };
}
return undefined;
})
.filter(notEmpty);
const decorationIdsToRemove = relevantChanges
.map(({ range }) => this.p2m.asRange(range))
.map((changeRange) =>
resolvedMarkers.filter(({ range: decorationRange }) =>
changeRange.containsRange(decorationRange)
)
)
.reduce((acc, curr) => acc.concat(curr), [])
.map(({ error, index }) => {
this.errors.splice(index, 1);
return error.id;
});
if (!decorationIdsToRemove.length) {
return;
}
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
this.onDidChangeEmitter.fire(this);
}
private async trackEditors(
errors: Map<string, CoreError.Compiler[]>,
...track: ((editor: EditorWidget) => Disposable)[]
): Promise<Disposable> {
return new DisposableCollection(
...(await Promise.all(
Array.from(errors.keys()).map(async (uri) => {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
return Disposable.NULL;
}
return new DisposableCollection(...track.map((t) => t(editor)));
})
))
);
}
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
const index = this.errors.findIndex((candidate) =>
ErrorDecoration.sameAs(candidate, error)
);
if (index < 0) {
console.warn(
'compiler-errors',
`Failed to mark error ${
error.id
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
({ id }) => id
)}`
);
return;
}
const newError = this.errors[index];
if (
!this.currentError ||
!ErrorDecoration.sameAs(this.currentError, newError)
) {
this.currentError = this.errors[index];
console.log(
'compiler-errors',
`Current error changed to ${this.currentError.id}`
);
this.currentErrorDidChangEmitter.fire(this.currentError);
this.onDidChangeEmitter.fire(this);
}
}
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
private async revealLocationInEditor(
location: Location
): Promise<EditorWidget | undefined> {
const { uri, range } = location;
const editor = await this.editorManager.getByUri(new URI(uri), {
mode: 'activate',
});
if (editor && this.shell) {
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
editor.editor.revealRange(range, { at: this.revealStrategy });
const activeWidget = await this.shell.activateWidget(editor.id);
if (!activeWidget) {
console.warn(
'compiler-errors',
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
);
return editor;
}
if (editor !== activeWidget) {
console.warn(
'compiler-errors',
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
);
}
return editor;
}
console.warn(
'compiler-errors',
`could not found editor widget for URI: ${uri}`
);
return undefined;
}
private groupByResource(
errors: CoreError.Compiler[]
): Map<string, CoreError.Compiler[]> {
return errors.reduce((acc, curr) => {
const {
location: { uri },
} = curr;
let errors = acc.get(uri);
if (!errors) {
errors = [];
acc.set(uri, errors);
}
errors.push(curr);
return acc;
}, new Map<string, CoreError.Compiler[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | EditorWidget
): MaybePromise<MonacoEditor | undefined> {
if (uriOrWidget instanceof EditorWidget) {
const editor = uriOrWidget.editor;
if (editor instanceof MonacoEditor) {
return editor;
}
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);
}
return undefined;
});
}
}
}
export namespace CompilerErrors {
export namespace Commands {
export const NEXT_ERROR: Command = {
id: 'arduino-editor-next-error',
};
export const PREVIOUS_ERROR: Command = {
id: 'arduino-editor-previous-error',
};
}
}

View File

@@ -14,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
MenuModelRegistry,
MenuContribution,
@@ -48,9 +48,15 @@ import {
ConfigService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { CoreErrorHandler } from './core-error-handler';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
export {
Command,
@@ -164,6 +170,56 @@ export abstract class SketchContribution extends Contribution {
}
}
@injectable()
export class CoreServiceContribution extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(CoreErrorHandler)
protected readonly coreErrorHandler: CoreErrorHandler;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
protected handleError(error: unknown): void {
this.coreErrorHandler.tryHandle(error);
this.tryToastErrorMessage(error);
}
private tryToastErrorMessage(error: unknown): void {
let message: undefined | string = undefined;
if (CoreError.is(error)) {
message = error.message;
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
} else {
try {
message = JSON.stringify(error);
} catch {}
}
if (message) {
const copyAction = nls.localize(
'arduino/coreContribution/copyError',
'Copy error messages'
);
this.messageService.error(message, copyAction).then(async (action) => {
if (action === copyAction) {
const content = await this.outputChannelManager.contentOfChannel(
'Arduino'
);
if (content) {
this.clipboardService.writeText(content);
}
}
});
} else {
throw error;
}
}
}
export namespace Contribution {
export function configure(
bind: interfaces.Bind,

View File

@@ -0,0 +1,32 @@
import { Emitter, Event } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { CoreError } from '../../common/protocol/core-service';
@injectable()
export class CoreErrorHandler {
private readonly compilerErrors: CoreError.Compiler[] = [];
private readonly compilerErrorsDidChangeEmitter = new Emitter<
CoreError.Compiler[]
>();
tryHandle(error: unknown): void {
if (CoreError.is(error)) {
this.compilerErrors.length = 0;
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
this.fireCompilerErrorsDidChange();
}
}
reset(): void {
this.compilerErrors.length = 0;
this.fireCompilerErrorsDidChange();
}
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
return this.compilerErrorsDidChangeEmitter.event;
}
private fireCompilerErrorsDidChange(): void {
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
}
}

View File

@@ -2,6 +2,8 @@ import { MaybePromise } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { Formatter } from '../../common/protocol/formatter';
import { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { Contribution, URI } from './contribution';
@injectable()
@@ -15,12 +17,11 @@ export class Format
private readonly formatter: Formatter;
override onStart(): MaybePromise<void> {
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
monaco.languages.registerDocumentRangeFormattingEditProvider(
selector,
InoSelector,
this
);
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
}
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
@@ -39,18 +40,11 @@ export class Format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = this.fullRange(model);
const range = fullRange(model);
const text = await this.format(model, range, options);
return [{ range, text }];
}
private fullRange(model: monaco.editor.ITextModel): monaco.Range {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
const end = new monaco.Position(lastLine, lastLineMaxColumn);
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
}
/**
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
* folder locations where the `.clang-format` file could be.
@@ -82,13 +76,4 @@ export class Format
options,
});
}
private selectorOf(
...languageId: string[]
): monaco.languages.LanguageSelector {
return languageId.map((language) => ({
language,
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}
}

View File

@@ -0,0 +1,78 @@
import { MessageService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { LocalStorageService } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SurveyNotificationService } from '../../common/protocol/survey-service';
const SURVEY_MESSAGE = nls.localize(
'arduino/survey/surveyMessage',
'Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better.'
);
const DO_NOT_SHOW_AGAIN = nls.localize(
'arduino/survey/dismissSurvey',
"Don't show again"
);
const GO_TO_SURVEY = nls.localize(
'arduino/survey/answerSurvey',
'Answer survey'
);
const SURVEY_BASE_URL = 'https://surveys.hotjar.com/';
const surveyId = '17887b40-e1f0-4bd6-b9f0-a37f229ccd8b';
@injectable()
export class SurveyNotification implements FrontendApplicationContribution {
@inject(MessageService)
private readonly messageService: MessageService;
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(WindowService)
private readonly windowService: WindowService;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SurveyNotificationService)
private readonly surveyNotificationService: SurveyNotificationService;
onStart(): void {
this.arduinoPreferences.ready.then(async () => {
if (
(await this.surveyNotificationService.isFirstInstance()) &&
this.arduinoPreferences.get('arduino.survey.notification')
) {
const surveyAnswered = await this.localStorageService.getData(
this.surveyKey(surveyId)
);
if (surveyAnswered !== undefined) {
return;
}
const answer = await this.messageService.info(
SURVEY_MESSAGE,
DO_NOT_SHOW_AGAIN,
GO_TO_SURVEY
);
switch (answer) {
case GO_TO_SURVEY:
this.windowService.openNewWindow(SURVEY_BASE_URL + surveyId, {
external: true,
});
this.localStorageService.setData(this.surveyKey(surveyId), true);
break;
case DO_NOT_SHOW_AGAIN:
this.localStorageService.setData(this.surveyKey(surveyId), false);
break;
}
}
});
}
private surveyKey(id: string): string {
return `answered_survey:${id}`;
}
}

View File

@@ -6,7 +6,7 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -18,10 +18,7 @@ import { DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@@ -201,16 +198,17 @@ export class UploadSketch extends SketchContribution {
return;
}
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
@@ -227,9 +225,8 @@ export class UploadSketch extends SketchContribution {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
};
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort;
@@ -248,7 +245,7 @@ export class UploadSketch extends SketchContribution {
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
sketch,
board,
optimizeForDebug,
programmer,
@@ -260,7 +257,7 @@ export class UploadSketch extends SketchContribution {
};
} else {
options = {
sketchUri,
sketch,
board,
optimizeForDebug,
port,
@@ -281,13 +278,7 @@ export class UploadSketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = '';
if (typeof e === 'string') {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();

View File

@@ -1,12 +1,11 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -17,10 +16,7 @@ import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class VerifySketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
export class VerifySketch extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -96,14 +92,14 @@ export class VerifySketch extends SketchContribution {
// toggle the toolbar button and menu item state.
// verifyInProgress will be set to false whether the compilation fails or not
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
this.verifyInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
@@ -115,12 +111,12 @@ export class VerifySketch extends SketchContribution {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
};
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
sketch,
board,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
@@ -133,13 +129,7 @@ export class VerifySketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();

View File

@@ -0,0 +1,13 @@
import * as monaco from '@theia/monaco-editor-core';
/**
* Exclusive "ino" document selector for monaco.
*/
export const InoSelector = selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
function selectorOf(
...languageId: string[]
): monaco.languages.LanguageSelector {
return languageId.map((language) => ({
language,
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}

View File

@@ -1,7 +1,9 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
OutputChannelManager,
OutputChannelSeverity,
} from '@theia/output/lib/browser/output-channel';
import {
OutputMessage,
ProgressMessage,
@@ -10,13 +12,10 @@ import {
@injectable()
export class ResponseServiceImpl implements ResponseServiceArduino {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
private readonly outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
private readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
@@ -25,13 +24,22 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
}
appendToOutput(message: OutputMessage): void {
const { chunk } = message;
const { chunk, severity } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true });
channel.append(chunk);
channel.append(chunk, mapSeverity(severity));
}
reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress);
}
}
function mapSeverity(severity?: OutputMessage.Severity): OutputChannelSeverity {
if (severity === OutputMessage.Severity.Error) {
return OutputChannelSeverity.Error;
} else if (severity === OutputMessage.Severity.Warning) {
return OutputChannelSeverity.Warning;
}
return OutputChannelSeverity.Info;
}

View File

@@ -8,3 +8,8 @@
.monaco-list-row.show-file-icons.focused {
background-color: #d6ebff;
}
.monaco-editor .view-overlays .compiler-error {
background-color: var(--theia-inputValidation-errorBackground);
opacity: 0.4 !important;
}

View File

@@ -27,8 +27,9 @@
}
.ide-updater-dialog .changelog-container {
background: white;
border: 1px solid #dae3e3;
color: var(--theia-dropdown-foreground);
background-color: var(--theia-dropdown-background);
border: 1px solid var(--theia-tree-indentGuidesStroke);
border-radius: 2px;
font-size: 12px;
height: 180px;

View File

@@ -130,5 +130,5 @@ DockPanel.prototype.handleEvent = function (event) {
case 'p-drop':
return;
}
originalHandleEvent(event);
originalHandleEvent.bind(this)(event);
};

View File

@@ -21,6 +21,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.TOGGLE_MAXIMIZED,
CommonCommands.PIN_TAB,
CommonCommands.UNPIN_TAB,
CommonCommands.NEW_FILE,
]) {
commandRegistry.unregisterCommand(command);
}

View File

@@ -2,10 +2,13 @@ import { notEmpty } from '@theia/core';
import { WidgetDescription } from '@theia/core/lib/browser';
import { ShellLayoutRestorer as TheiaShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { injectable } from '@theia/core/shared/inversify';
import { EditorPreviewWidgetFactory } from '@theia/editor-preview/lib/browser/editor-preview-widget-factory';
import { EditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { FrontendApplication } from './frontend-application';
namespace EditorPreviewWidgetFactory {
export const ID = 'editor-preview-widget'; // The factory ID must be a hard-coded string because IDE2 does not depend on `@theia/editor-preview`.
}
@injectable()
export class ShellLayoutRestorer extends TheiaShellLayoutRestorer {
override async restoreLayout(app: FrontendApplication): Promise<boolean> {
@@ -160,8 +163,8 @@ export class ShellLayoutRestorer extends TheiaShellLayoutRestorer {
constructionOptions: {
factoryId: EditorWidgetFactory.ID,
options: {
uri,
kind: 'navigatable',
uri,
counter: 0,
},
},

View File

@@ -0,0 +1,48 @@
import type { MaybePromise } from '@theia/core';
import type { Widget } from '@theia/core/lib/browser';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { injectable } from '@theia/core/shared/inversify';
import deepEqual = require('deep-equal');
@injectable()
export class WidgetManager extends TheiaWidgetManager {
/**
* Customized to find any existing widget based on `options` deepEquals instead of string equals.
* See https://github.com/eclipse-theia/theia/issues/11309.
*/
protected override doGetWidget<T extends Widget>(
key: string
): MaybePromise<T> | undefined {
const pendingWidget = this.findExistingWidget<T>(key);
if (pendingWidget) {
return pendingWidget as MaybePromise<T>;
}
return undefined;
}
private findExistingWidget<T extends Widget>(
key: string
): MaybePromise<T> | undefined {
const parsed = this.parseJson(key);
for (const [candidateKey, widget] of [
...this.widgetPromises.entries(),
...this.pendingWidgetPromises.entries(),
]) {
const candidate = this.parseJson(candidateKey);
if (deepEqual(candidate, parsed)) {
return widget as MaybePromise<T>;
}
}
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private parseJson(json: string): any {
try {
return JSON.parse(json);
} catch (err) {
console.log(`Failed to parse JSON: <${json}>.`, err);
throw err;
}
}
}

View File

@@ -0,0 +1,15 @@
import { injectable } from '@theia/core/shared/inversify';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
@injectable()
export class WindowContribution extends TheiaWindowContribution {
override registerCommands(): void {
// NOOP
}
override registerKeybindings(): void {
// NOO
}
override registerMenus(): void {
// NOOP;
}
}

View File

@@ -1,21 +1,13 @@
import { injectable } from '@theia/core/shared/inversify';
import { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { TextEditor } from '@theia/editor/lib/browser';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
@injectable()
export class EditorPreviewContribution extends TheiaEditorPreviewContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {}
// protected setCursorPositionStatus(editor: TextEditor | undefined): void {
// if (!editor) {
// this.statusBar.removeElement('editor-status-cursor-position');
// return;
// }
// const { cursor } = editor;
// this.statusBar.setElement('editor-status-cursor-position', {
// text: `${cursor.line + 1}`,
// alignment: StatusBarAlignment.LEFT,
// priority: 100,
// });
// }
export class EditorContribution extends TheiaEditorContribution {
protected override updateLanguageStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
editor: TextEditor | undefined
): void {
// NOOP
}
}

View File

@@ -40,6 +40,14 @@ export class OutputChannelManager extends TheiaOutputChannelManager {
}
return channel;
}
async contentOfChannel(name: string): Promise<string | undefined> {
const resource = this.resources.get(name);
if (resource) {
return resource.readContents();
}
return undefined;
}
}
export class OutputChannel extends TheiaOutputChannel {

View File

@@ -1,16 +1,43 @@
import {
FrontendApplicationState,
FrontendApplicationStateService,
} from '@theia/core/lib/browser/frontend-application-state';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree/tree';
import { injectable } from '@theia/core/shared/inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
@injectable()
export class PreferenceTreeGenerator extends TheiaPreferenceTreeGenerator {
private shouldHandleChangedSchemaOnReady = false;
private state: FrontendApplicationState | undefined;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected override async init(): Promise<void> {
// The IDE2 does not use the default Theia preferences UI.
// There is no need to create and keep the the tree model synchronized when there is no UI for it.
this.appStateService.onStateChanged((state) => {
this.state = state;
// manually trigger a model (and UI) refresh if it was requested during the startup phase.
if (this.state === 'ready' && this.shouldHandleChangedSchemaOnReady) {
this.doHandleChangedSchema();
}
});
return super.init();
}
override doHandleChangedSchema(): void {
if (this.state === 'ready') {
super.doHandleChangedSchema();
}
// don't do anything until the app is `ready`, then invoke `doHandleChangedSchema`.
this.shouldHandleChangedSchemaOnReady = true;
}
// Just returns with the empty root.
override generateTree(): CompositeTreeNode {
if (this.state === 'ready') {
return super.generateTree();
}
// always create an empty root when the app is not ready.
this._root = this.createRootNode();
return this._root;
}

View File

@@ -0,0 +1,8 @@
import * as monaco from '@theia/monaco-editor-core';
export function fullRange(model: monaco.editor.ITextModel): monaco.Range {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
const end = new monaco.Position(lastLine, lastLineMaxColumn);
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
}

View File

@@ -31,8 +31,8 @@ export class CloudSketchbookCompositeWidget extends BaseWidget {
this.compositeNode.appendChild(this.cloudUserStatusNode);
this.node.appendChild(this.compositeNode);
this.title.caption = nls.localize(
'arduino/cloud/cloudSketchbook',
'Cloud Sketchbook'
'arduino/cloud/remoteSketchbook',
'Remote Sketchbook'
);
this.title.iconClass = 'cloud-sketchbook-tree-icon';
this.title.closable = false;

View File

@@ -65,6 +65,7 @@ export class UserStatus extends React.Component<
</div>
<div className="actions item flex-line">
<div
title={nls.localize('arduino/cloud/sync', 'Sync')}
className={`fa fa-reload ${
(this.state.refreshing && 'rotating') || ''
}`}
@@ -74,6 +75,7 @@ export class UserStatus extends React.Component<
</div>
<div className="account item flex-line">
<div
title={nls.localize('arduino/cloud/account', 'Account')}
className="account-icon"
style={{ cursor: 'pointer' }}
onClick={(event) => {

View File

@@ -1,5 +1,9 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { CommandRegistry } from '@theia/core/lib/common/command';
import {
@@ -22,6 +26,11 @@ import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection'
import { Sketch } from '../../contributions/contribution';
import { nls } from '@theia/core/lib/common';
const customTreeProps: TreeProps = {
leftPadding: 20,
expansionTogglePadding: 6,
};
@injectable()
export class SketchbookTreeWidget extends FileTreeWidget {
@inject(CommandRegistry)
@@ -57,10 +66,14 @@ export class SketchbookTreeWidget extends FileTreeWidget {
super.init();
// cache the current open sketch uri
const currentSketch = await this.sketchServiceClient.currentSketch();
this.currentSketchUri = (CurrentSketch.isValid(currentSketch) && currentSketch.uri) || '';
this.currentSketchUri =
(CurrentSketch.isValid(currentSketch) && currentSketch.uri) || '';
}
protected override createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
protected override createNodeClassNames(
node: TreeNode,
props: NodeProps
): string[] {
const classNames = super.createNodeClassNames(node, props);
if (
@@ -73,7 +86,10 @@ export class SketchbookTreeWidget extends FileTreeWidget {
return classNames;
}
protected override renderIcon(node: TreeNode, props: NodeProps): React.ReactNode {
protected override renderIcon(
node: TreeNode,
props: NodeProps
): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) {
return <div className="sketch-folder-icon file-icon"></div>;
}
@@ -199,4 +215,13 @@ export class SketchbookTreeWidget extends FileTreeWidget {
}
event.stopPropagation();
}
protected override getPaddingLeft(node: TreeNode, props: NodeProps): number {
return (
props.depth * customTreeProps.leftPadding +
(this.needsExpansionTogglePadding(node)
? customTreeProps.expansionTogglePadding
: 0)
);
}
}

View File

@@ -1,6 +1,10 @@
import { ApplicationError } from '@theia/core';
import { Location } from '@theia/core/shared/vscode-languageserver-protocol';
import { BoardUserField } from '.';
import { Board, Port } from '../../common/protocol/boards-service';
import { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
import { Programmer } from './boards-service';
import { Sketch } from './sketches-service';
export const CompilerWarningLiterals = [
'None',
@@ -9,6 +13,53 @@ export const CompilerWarningLiterals = [
'All',
] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
export namespace CoreError {
export type ErrorInfo = CliErrorInfo;
export interface Compiler extends ErrorInfo {
readonly message: string;
readonly location: Location;
}
export namespace Compiler {
export function is(error: ErrorInfo): error is Compiler {
const { message, location } = error;
return !!message && !!location;
}
}
export const Codes = {
Verify: 4001,
Upload: 4002,
UploadUsingProgrammer: 4003,
BurnBootloader: 4004,
};
export const VerifyFailed = create(Codes.Verify);
export const UploadFailed = create(Codes.Upload);
export const UploadUsingProgrammerFailed = create(
Codes.UploadUsingProgrammer
);
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
export function is(
error: unknown
): error is ApplicationError<number, ErrorInfo[]> {
return (
error instanceof Error &&
ApplicationError.is(error) &&
Object.values(Codes).includes(error.code)
);
}
function create(
code: number
): ApplicationError.Constructor<number, ErrorInfo[]> {
return ApplicationError.declare(
code,
(message: string, data: ErrorInfo[]) => {
return {
data,
message,
};
}
);
}
}
export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService');
@@ -23,16 +74,12 @@ export interface CoreService {
upload(options: CoreService.Upload.Options): Promise<void>;
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
isUploading(): Promise<boolean>;
}
export namespace CoreService {
export namespace Compile {
export interface Options {
/**
* `file` URI to the sketch folder.
*/
readonly sketchUri: string;
readonly sketch: Sketch;
readonly board?: Board;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;

View File

@@ -2,7 +2,14 @@ import { Event } from '@theia/core/lib/common/event';
export interface OutputMessage {
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
readonly severity?: OutputMessage.Severity;
}
export namespace OutputMessage {
export enum Severity {
Error,
Warning,
Info,
}
}
export interface ProgressMessage {

View File

@@ -127,11 +127,8 @@ export namespace Sketch {
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
}
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
return (
[mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(
uri.toString()
) !== -1
return uris(sketch).includes(
typeof uri === 'string' ? uri : uri.toString()
);
}
export function isSketchFile(arg: string | URI): boolean {
@@ -140,6 +137,10 @@ export namespace Sketch {
}
return Extensions.MAIN.some((ext) => arg.endsWith(ext));
}
export function uris(sketch: Sketch): string[] {
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
}
}
export interface SketchContainer {

View File

@@ -0,0 +1,7 @@
export const SurveyNotificationServicePath =
'/services/survey-notification-service';
export const SurveyNotificationService = Symbol('SurveyNotificationService');
export interface SurveyNotificationService {
isFirstInstance(): Promise<boolean>;
}

View File

@@ -8,6 +8,7 @@ import {
} from '@theia/core/lib/common/menu';
import {
ElectronMainMenuFactory as TheiaElectronMainMenuFactory,
ElectronMenuItemRole,
ElectronMenuOptions,
} from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import {
@@ -123,6 +124,15 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
return { label, submenu };
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected override roleFor(id: string): ElectronMenuItemRole | undefined {
// MenuItem `roles` are completely broken on macOS:
// - https://github.com/eclipse-theia/theia/issues/11217,
// - https://github.com/arduino/arduino-ide/issues/969
// IDE2 uses commands instead.
return undefined;
}
protected override handleElectronDefault(
menuNode: CompositeMenuNode,
args: any[] = [],

View File

@@ -21,6 +21,11 @@ import {
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { TheiaElectronWindow } from './theia/theia-electron-window';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { SurveyNotificationServiceImpl } from '../node/survey-service-impl';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
@@ -61,4 +66,21 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
// Survey notification bindings
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope();
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl);
bind(ElectronMainApplicationContribution).toService(
SurveyNotificationService
);
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () =>
context.container.get<SurveyNotificationService>(
SurveyNotificationService
)
)
)
.inSingletonScope();
});

View File

@@ -1,4 +1,5 @@
import { join } from 'path';
import { promises as fs } from 'fs';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { spawn, ChildProcess } from 'child_process';
import { FileUri } from '@theia/core/lib/node/file-uri';
@@ -142,9 +143,12 @@ export class ArduinoDaemonImpl
}
protected async getSpawnArgs(): Promise<string[]> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
const [configDirUri, debug] = await Promise.all([
this.envVariablesServer.getConfigDirUri(),
this.debugDaemon(),
]);
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
return [
const args = [
'daemon',
'--format',
'jsonmini',
@@ -156,6 +160,41 @@ export class ArduinoDaemonImpl
'--log-format',
'json',
];
if (debug) {
args.push('--debug');
}
return args;
}
private async debugDaemon(): Promise<boolean> {
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
const configDirUri = await this.envVariablesServer.getConfigDirUri();
const configDirPath = FileUri.fsPath(configDirUri);
try {
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
encoding: 'utf8',
});
const json = this.tryParse(raw);
if (json) {
const value = json['arduino.cli.daemon.debug'];
return typeof value === 'boolean' && !!value;
}
return false;
} catch (error) {
if ('code' in error && error.code === 'ENOENT') {
return false;
}
throw error;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private tryParse(raw: string): any | undefined {
try {
return JSON.parse(raw);
} catch {
return undefined;
}
}
protected async spawnDaemonProcess(): Promise<{

View File

@@ -90,7 +90,7 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
} catch (e) {
throw e;
} finally {
this.monitorManager.notifyUploadFinished(board, port);
await this.monitorManager.notifyUploadFinished(board, port);
return output;
}
}

View File

@@ -0,0 +1,234 @@
import { notEmpty } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
Location,
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import { Sketch } from '../common/protocol';
export interface ErrorInfo {
readonly message?: string;
readonly location?: Location;
readonly details?: string;
}
export interface ErrorSource {
readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch;
}
export function tryParseError(source: ErrorSource): ErrorInfo[] {
const { content, sketch } = source;
const err =
typeof content === 'string'
? content
: Buffer.concat(content).toString('utf8');
if (sketch) {
return tryParse(err)
.map(remapErrorMessages)
.filter(isLocationInSketch(sketch))
.map(errorInfo());
}
return [];
}
interface ParseResult {
readonly path: string;
readonly line: number;
readonly column?: number;
readonly errorPrefix: string;
readonly error: string;
readonly message?: string;
}
namespace ParseResult {
export function keyOf(result: ParseResult): string {
/**
* The CLI compiler might return with the same error multiple times. This is the key function for the distinct set calculation.
*/
return JSON.stringify(result);
}
}
function isLocationInSketch(
sketch: Sketch
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
return (result) => {
const uri = FileUri.create(result.path).toString();
if (!Sketch.isInSketch(uri, sketch)) {
console.warn(
`URI <${uri}> is not contained in sketch: <${JSON.stringify(sketch)}>`
);
return false;
}
return true;
};
}
function errorInfo(): (value: ParseResult) => ErrorInfo {
return ({ error, message, path, line, column }) => ({
message: error,
details: message,
location: {
uri: FileUri.create(path).toString(),
range: range(line, column),
},
});
}
function range(line: number, column?: number): Range {
const start = Position.create(
line - 1,
typeof column === 'number' ? column - 1 : 0
);
return {
start,
end: start,
};
}
export function tryParse(raw: string): ParseResult[] {
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
const re = new RegExp(
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
'gm'
);
return [
...new Map(
Array.from(raw.matchAll(re) ?? [])
.map((match) => {
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
(match) => (match ? match.trim() : match)
);
const line = Number.parseInt(rawLine, 10);
if (!Number.isInteger(line)) {
console.warn(
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
);
return undefined;
}
let column: number | undefined = undefined;
if (rawColumn) {
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
column = Number.parseInt(normalizedRawColumn, 10);
if (!Number.isInteger(column)) {
console.warn(
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
);
}
}
return {
path,
line,
column,
errorPrefix,
error,
};
})
.filter(notEmpty)
.map((result) => [ParseResult.keyOf(result), result])
).values(),
];
}
/**
* Converts cryptic and legacy error messages to nice ones. Taken from the Java IDE.
*/
function remapErrorMessages(result: ParseResult): ParseResult {
const knownError = KnownErrors[result.error];
if (!knownError) {
return result;
}
const { message, error } = knownError;
return {
...result,
...(message && { message }),
...(error && { error }),
};
}
// Based on the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L528-L578
const KnownErrors: Record<string, { error: string; message?: string }> = {
'SPI.h: No such file or directory': {
error: nls.localize(
'arduino/cli-error-parser/spiError',
'Please import the SPI library from the Sketch > Import Library menu.'
),
message: nls.localize(
'arduino/cli-error-parser/spiMessage',
'As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.'
),
},
"'BYTE' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/byteError',
"The 'BYTE' keyword is no longer supported."
),
message: nls.localize(
'arduino/cli-error-parser/byteMessage',
"As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead."
),
},
"no matching function for call to 'Server::Server(int)'": {
error: nls.localize(
'arduino/cli-error-parser/serverError',
'The Server class has been renamed EthernetServer.'
),
message: nls.localize(
'arduino/cli-error-parser/serverMessage',
'As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.'
),
},
"no matching function for call to 'Client::Client(byte [4], int)'": {
error: nls.localize(
'arduino/cli-error-parser/clientError',
'The Client class has been renamed EthernetClient.'
),
message: nls.localize(
'arduino/cli-error-parser/clientMessage',
'As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.'
),
},
"'Udp' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/udpError',
'The Udp class has been renamed EthernetUdp.'
),
message: nls.localize(
'arduino/cli-error-parser/udpMessage',
'As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.'
),
},
"'class TwoWire' has no member named 'send'": {
error: nls.localize(
'arduino/cli-error-parser/sendError',
'Wire.send() has been renamed Wire.write().'
),
message: nls.localize(
'arduino/cli-error-parser/sendMessage',
'As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.'
),
},
"'class TwoWire' has no member named 'receive'": {
error: nls.localize(
'arduino/cli-error-parser/receiveError',
'Wire.receive() has been renamed Wire.read().'
),
message: nls.localize(
'arduino/cli-error-parser/receiveMessage',
'As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.'
),
},
"'Mouse' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/mouseError',
"'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
),
},
"'Keyboard' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/keyboardError',
"'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?"
),
},
};

View File

@@ -21,7 +21,10 @@ import {
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { NotificationServiceServer } from '../common/protocol';
import { Deferred, retry } from '@theia/core/lib/common/promise-util';
import { Status as RpcStatus } from './cli-protocol/google/rpc/status_pb';
import {
Status as RpcStatus,
Status,
} from './cli-protocol/google/rpc/status_pb';
@injectable()
export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Client> {
@@ -90,10 +93,11 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
this._initialized.resolve();
this.updateIndex(this._client); // Update the indexes asynchronously
} catch (error: unknown) {
if (
this.isPackageIndexMissingError(error) ||
this.isDiscoveryNotFoundError(error)
) {
console.error(
'Error occurred while initializing the core gRPC client provider',
error
);
if (error instanceof IndexUpdateRequiredBeforeInitError) {
// If it's a first start, IDE2 must run index update before the init request.
await this.updateIndexes(this._client);
await this.initInstance(this._client);
@@ -114,41 +118,6 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
});
}
private isPackageIndexMissingError(error: unknown): boolean {
const assert = (message: string) =>
message.includes('loading json index file');
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return this.isRpcStatusError(error, assert);
}
private isDiscoveryNotFoundError(error: unknown): boolean {
const assert = (message: string) =>
message.includes('discovery') &&
(message.includes('not found') || message.includes('not installed'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
return this.isRpcStatusError(error, assert);
}
private isCancelError(error: unknown): boolean {
return (
error instanceof Error &&
error.message.toLocaleLowerCase().includes('cancelled on client')
);
}
// Final error codes are not yet defined by the CLI. Hence, we do string matching in the message RPC status.
private isRpcStatusError(
error: unknown,
assert: (message: string) => boolean
) {
if (error instanceof RpcStatus) {
const { message } = RpcStatus.toObject(false, error);
return assert(message.toLocaleLowerCase());
}
return false;
}
protected async createClient(
port: string | number
): Promise<CoreClientProvider.Client> {
@@ -192,7 +161,7 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
initReq.setInstance(instance);
return new Promise<void>((resolve, reject) => {
const stream = client.init(initReq);
const errorStatus: RpcStatus[] = [];
const errors: RpcStatus[] = [];
stream.on('data', (res: InitResponse) => {
const progress = res.getInitProgress();
if (progress) {
@@ -210,28 +179,30 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
const error = res.getError();
if (error) {
console.error(error.getMessage());
errorStatus.push(error);
// Cancel the init request. No need to wait until the end of the event. The init has already failed.
// Canceling the request will result in a cancel error, but we need to reject with the original error later.
stream.cancel();
const { code, message } = Status.toObject(false, error);
console.error(
`Detected an error response during the gRPC core client initialization: code: ${code}, message: ${message}`
);
errors.push(error);
}
});
stream.on('error', (error) => {
// On any error during the init request, the request is canceled.
// On cancel, the IDE2 ignores the cancel error and rejects with the original one.
reject(
this.isCancelError(error) && errorStatus.length
? errorStatus[0]
: error
);
stream.on('error', reject);
stream.on('end', () => {
const error = this.evaluateErrorStatus(errors);
if (error) {
reject(error);
return;
}
resolve();
});
stream.on('end', () =>
errorStatus.length ? reject(errorStatus) : resolve()
);
});
}
private evaluateErrorStatus(status: RpcStatus[]): Error | undefined {
const error = isIndexUpdateRequiredBeforeInit(status); // put future error matching here
return error;
}
protected async updateIndexes(
client: CoreClientProvider.Client
): Promise<CoreClientProvider.Client> {
@@ -338,3 +309,58 @@ export abstract class CoreClientAware {
);
}
}
class IndexUpdateRequiredBeforeInitError extends Error {
constructor(causes: RpcStatus.AsObject[]) {
super(`The index of the cores and libraries must be updated before initializing the core gRPC client.
The following problems were detected during the gRPC client initialization:
${causes
.map(({ code, message }) => ` - code: ${code}, message: ${message}`)
.join('\n')}
`);
Object.setPrototypeOf(this, IndexUpdateRequiredBeforeInitError.prototype);
if (!causes.length) {
throw new Error(`expected non-empty 'causes'`);
}
}
}
function isIndexUpdateRequiredBeforeInit(
status: RpcStatus[]
): IndexUpdateRequiredBeforeInitError | undefined {
const causes = status
.filter((s) =>
IndexUpdateRequiredBeforeInit.map((predicate) => predicate(s)).some(
Boolean
)
)
.map((s) => RpcStatus.toObject(false, s));
return causes.length
? new IndexUpdateRequiredBeforeInitError(causes)
: undefined;
}
const IndexUpdateRequiredBeforeInit = [
isPackageIndexMissingStatus,
isDiscoveryNotFoundStatus,
];
function isPackageIndexMissingStatus(status: RpcStatus): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('loading json index file');
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return evaluate(status, predicate);
}
function isDiscoveryNotFoundStatus(status: RpcStatus): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('discovery') &&
(message.includes('not found') || message.includes('not installed'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
return evaluate(status, predicate);
}
function evaluate(
subject: RpcStatus,
predicate: (error: RpcStatus.AsObject) => boolean
): boolean {
const status = RpcStatus.toObject(false, subject);
return predicate(status);
}

View File

@@ -4,7 +4,11 @@ import { relative } from 'path';
import * as jspb from 'google-protobuf';
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
import { ClientReadableStream } from '@grpc/grpc-js';
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
import {
CompilerWarnings,
CoreService,
CoreError,
} from '../common/protocol/core-service';
import {
CompileRequest,
CompileResponse,
@@ -19,25 +23,24 @@ import {
UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service';
import { NotificationServiceServer } from '../common/protocol';
import { Board, OutputMessage, Port, Status } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { nls } from '@theia/core';
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, Disposable, nls } from '@theia/core';
import { MonitorManager } from './monitor-manager';
import { SimpleBuffer } from './utils/simple-buffer';
import { tryParseError } from './cli-error-parser';
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { firstToUpperCase, notEmpty } from '../common/utils';
import { ServiceError } from './service-error';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
private readonly responseService: ResponseService;
@inject(MonitorManager)
protected readonly monitorManager: MonitorManager;
protected uploading = false;
private readonly monitorManager: MonitorManager;
async compile(
options: CoreService.Compile.Options & {
@@ -45,234 +48,298 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, board, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const handler = this.createOnDataHandler();
const request = this.compileRequest(options, instance);
return new Promise<void>((resolve, reject) => {
client
.compile(request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(
'Unexpected error occurred while compiling the sketch.',
error
);
reject(error);
} else {
const compilerErrors = tryParseError({
content: handler.stderr,
sketch: options.sketch,
});
const message = nls.localize(
'arduino/compile/error',
'Compilation error: {0}',
compilerErrors
.map(({ message }) => message)
.filter(notEmpty)
.shift() ?? error.details
);
this.sendResponse(
error.details + '\n\n' + message,
OutputMessage.Severity.Error
);
reject(CoreError.VerifyFailed(message, compilerErrors));
}
})
.on('end', resolve);
}).finally(() => handler.dispose());
}
const compileReq = new CompileRequest();
compileReq.setInstance(instance);
compileReq.setSketchPath(sketchPath);
private compileRequest(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
},
instance: Instance
): CompileRequest {
const { sketch, board, compilerWarnings } = options;
const sketchUri = sketch.uri;
const sketchPath = FileUri.fsPath(sketchUri);
const request = new CompileRequest();
request.setInstance(instance);
request.setSketchPath(sketchPath);
if (board?.fqbn) {
compileReq.setFqbn(board.fqbn);
request.setFqbn(board.fqbn);
}
if (compilerWarnings) {
compileReq.setWarnings(compilerWarnings.toLowerCase());
request.setWarnings(compilerWarnings.toLowerCase());
}
compileReq.setOptimizeForDebug(options.optimizeForDebug);
compileReq.setPreprocess(false);
compileReq.setVerbose(options.verbose);
compileReq.setQuiet(false);
request.setOptimizeForDebug(options.optimizeForDebug);
request.setPreprocess(false);
request.setVerbose(options.verbose);
request.setQuiet(false);
if (typeof options.exportBinaries === 'boolean') {
const exportBinaries = new BoolValue();
exportBinaries.setValue(options.exportBinaries);
compileReq.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(compileReq, options);
const result = client.compile(compileReq);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: CompileResponse) => {
this.responseService.appendToOutput({
chunk: Buffer.from(cr.getOutStream_asU8()).toString(),
});
this.responseService.appendToOutput({
chunk: Buffer.from(cr.getErrStream_asU8()).toString(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk: '\n--------------------------\nCompilation complete.\n',
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/compile/error',
'Compilation error: {0}',
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
request.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(request, options);
return request;
}
async upload(options: CoreService.Upload.Options): Promise<void> {
await this.doUpload(
upload(options: CoreService.Upload.Options): Promise<void> {
return this.doUpload(
options,
() => new UploadRequest(),
(client, req) => client.upload(req)
(client, req) => client.upload(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadFailed(message, info),
'upload'
);
}
async uploadUsingProgrammer(
options: CoreService.Upload.Options
): Promise<void> {
await this.doUpload(
return this.doUpload(
options,
() => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadUsingProgrammerFailed(message, info),
'upload using programmer'
);
}
isUploading(): Promise<boolean> {
return Promise.resolve(this.uploading);
}
protected async doUpload(
options: CoreService.Upload.Options,
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
// tslint:disable-next-line:max-line-length
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
responseHandler: (
client: ArduinoCoreServiceClient,
req: UploadRequest | UploadUsingProgrammerRequest
request: UploadRequest | UploadUsingProgrammerRequest
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
task = 'upload'
errorHandler: (
message: string,
info: CoreError.ErrorInfo[]
) => ApplicationError<number, CoreError.ErrorInfo[]>,
task: string
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
this.uploading = true;
const { sketchUri, board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const request = this.uploadOrUploadUsingProgrammerRequest(
options,
instance,
requestFactory
);
const handler = this.createOnDataHandler();
return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => {
responseHandler(client, request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(`Unexpected error occurred while ${task}.`, error);
reject(error);
} else {
const message = nls.localize(
'arduino/upload/error',
'{0} error: {1}',
firstToUpperCase(task),
error.details
);
this.sendResponse(error.details, OutputMessage.Severity.Error);
reject(
errorHandler(
message,
tryParseError({
content: handler.stderr,
sketch: options.sketch,
})
)
);
}
})
.on('end', resolve);
}).finally(async () => {
handler.dispose();
await this.notifyUploadDidFinish(options);
})
);
}
const req = requestProvider();
req.setInstance(instance);
req.setSketchPath(sketchPath);
private uploadOrUploadUsingProgrammerRequest(
options: CoreService.Upload.Options,
instance: Instance,
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest
): UploadRequest | UploadUsingProgrammerRequest {
const { sketch, board, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketch.uri);
const request = requestFactory();
request.setInstance(instance);
request.setSketchPath(sketchPath);
if (board?.fqbn) {
req.setFqbn(board.fqbn);
request.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
p.setAddress(port.address);
p.setLabel(port.addressLabel);
p.setProtocol(port.protocol);
p.setProtocolLabel(port.protocolLabel);
}
req.setPort(p);
request.setPort(this.createPort(port));
if (programmer) {
req.setProgrammer(programmer.id);
request.setProgrammer(programmer.id);
}
req.setVerbose(options.verbose);
req.setVerify(options.verify);
request.setVerbose(options.verbose);
request.setVerify(options.verify);
options.userFields.forEach((e) => {
req.getUserFieldsMap().set(e.name, e.value);
request.getUserFieldsMap().set(e.name, e.value);
});
const result = responseHandler(client, req);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: UploadResponse) => {
this.responseService.appendToOutput({
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
});
this.responseService.appendToOutput({
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk:
'\n--------------------------\n' +
firstToLowerCase(task) +
' complete.\n',
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/upload/error',
'{0} error: {1}',
firstToUpperCase(task),
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.monitorManager.notifyUploadFinished(board, port);
}
return request;
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
this.uploading = true;
const { board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const burnReq = new BurnBootloaderRequest();
burnReq.setInstance(instance);
const handler = this.createOnDataHandler();
const request = this.burnBootloaderRequest(options, instance);
return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => {
client
.burnBootloader(request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(
'Unexpected error occurred while burning the bootloader.',
error
);
reject(error);
} else {
this.sendResponse(error.details, OutputMessage.Severity.Error);
reject(
CoreError.BurnBootloaderFailed(
nls.localize(
'arduino/burnBootloader/error',
'Error while burning the bootloader: {0}',
error.details
),
tryParseError({ content: handler.stderr })
)
);
}
})
.on('end', resolve);
}).finally(async () => {
handler.dispose();
await this.notifyUploadDidFinish(options);
})
);
}
private burnBootloaderRequest(
options: CoreService.Bootloader.Options,
instance: Instance
): BurnBootloaderRequest {
const { board, port, programmer } = options;
const request = new BurnBootloaderRequest();
request.setInstance(instance);
if (board?.fqbn) {
burnReq.setFqbn(board.fqbn);
request.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
p.setAddress(port.address);
p.setLabel(port.addressLabel);
p.setProtocol(port.protocol);
p.setProtocolLabel(port.protocolLabel);
}
burnReq.setPort(p);
request.setPort(this.createPort(port));
if (programmer) {
burnReq.setProgrammer(programmer.id);
request.setProgrammer(programmer.id);
}
burnReq.setVerify(options.verify);
burnReq.setVerbose(options.verbose);
const result = client.burnBootloader(burnReq);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: BurnBootloaderResponse) => {
this.responseService.appendToOutput({
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
});
this.responseService.appendToOutput({
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
request.setVerify(options.verify);
request.setVerbose(options.verbose);
return request;
}
private createOnDataHandler<R extends StreamingResponse>(): Disposable & {
stderr: Buffer[];
onData: (response: R) => void;
} {
const stderr: Buffer[] = [];
const buffer = new SimpleBuffer((chunks) => {
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
if (chunk) {
this.sendResponse(chunk, severity);
}
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/burnBootloader/error',
'Error while burning the bootloader: {0}',
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
} finally {
this.uploading = false;
await this.monitorManager.notifyUploadFinished(board, port);
}
});
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => {
buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error);
});
return {
dispose: () => buffer.dispose(),
stderr,
onData,
};
}
private sendResponse(
chunk: string,
severity: OutputMessage.Severity = OutputMessage.Severity.Info
): void {
this.responseService.appendToOutput({ chunk, severity });
}
private async notifyUploadWillStart({
board,
port,
}: {
board?: Board | undefined;
port?: Port | undefined;
}): Promise<void> {
return this.monitorManager.notifyUploadStarted(board, port);
}
private async notifyUploadDidFinish({
board,
port,
}: {
board?: Board | undefined;
port?: Port | undefined;
}): Promise<Status> {
return this.monitorManager.notifyUploadFinished(board, port);
}
private mergeSourceOverrides(
req: { getSourceOverrideMap(): jspb.Map<string, string> },
options: CoreService.Compile.Options
): void {
const sketchPath = FileUri.fsPath(options.sketchUri);
const sketchPath = FileUri.fsPath(options.sketch.uri);
for (const uri of Object.keys(options.sourceOverride)) {
const content = options.sourceOverride[uri];
if (content) {
@@ -281,4 +348,34 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}
}
}
private createPort(port: Port | undefined): GrpcPort {
const grpcPort = new GrpcPort();
if (port) {
grpcPort.setAddress(port.address);
grpcPort.setLabel(port.addressLabel);
grpcPort.setProtocol(port.protocol);
grpcPort.setProtocolLabel(port.protocolLabel);
}
return grpcPort;
}
}
type StreamingResponse =
| CompileResponse
| UploadResponse
| UploadUsingProgrammerResponse
| BurnBootloaderResponse;
namespace StreamingResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createOnDataHandler<R extends StreamingResponse>(
stderr: Uint8Array[],
onData: (out: Uint8Array, err: Uint8Array) => void
): (response: R) => void {
return (response: R) => {
const out = response.getOutStream_asU8();
const err = response.getErrStream_asU8();
stderr.push(err);
onData(out, err);
};
}
}

View File

@@ -3,23 +3,19 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { join, basename } from 'path';
import { join } from 'path';
import * as fs from 'fs';
import { promisify } from 'util';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
Sketch,
SketchRef,
SketchContainer,
} from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService } from '../common/protocol/examples-service';
import {
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol';
import { ConfigServiceImpl } from './config-service-impl';
import { duration } from '../common/decorators';
import { URI } from '@theia/core/lib/common/uri';
import { Path } from '@theia/core/lib/common/path';
@@ -88,14 +84,8 @@ export class BuiltInExamplesServiceImpl {
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
private readonly libraryService: LibraryService;
@inject(BuiltInExamplesServiceImpl)
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
@@ -117,7 +107,7 @@ export class ExamplesServiceImpl implements ExamplesService {
fqbn,
});
for (const pkg of packages) {
const container = await this.tryGroupExamplesNew(pkg);
const container = await this.tryGroupExamples(pkg);
const { location } = pkg;
if (location === LibraryLocation.USER) {
user.push(container);
@@ -130,9 +120,6 @@ export class ExamplesServiceImpl implements ExamplesService {
any.push(container);
}
}
// user.sort((left, right) => left.label.localeCompare(right.label));
// current.sort((left, right) => left.label.localeCompare(right.label));
// any.sort((left, right) => left.label.localeCompare(right.label));
return { user, current, any };
}
@@ -141,7 +128,7 @@ export class ExamplesServiceImpl implements ExamplesService {
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
*/
protected async tryGroupExamplesNew({
private async tryGroupExamples({
label,
exampleUris,
installDirUri,
@@ -208,10 +195,6 @@ export class ExamplesServiceImpl implements ExamplesService {
if (!child) {
child = SketchContainer.create(label);
parent.children.push(child);
//TODO: remove or move sort
parent.children.sort((left, right) =>
left.label.localeCompare(right.label)
);
}
return child;
};
@@ -230,65 +213,7 @@ export class ExamplesServiceImpl implements ExamplesService {
container
);
refContainer.sketches.push(ref);
//TODO: remove or move sort
refContainer.sketches.sort((left, right) =>
left.name.localeCompare(right.name)
);
}
return container;
}
// Built-ins are included inside the IDE.
protected async load(path: string): Promise<SketchContainer> {
if (!(await promisify(fs.exists)(path))) {
throw new Error('Examples are not available');
}
const stat = await promisify(fs.stat)(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await promisify(fs.readdir)(path);
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
for (const p of names.map((name) => join(path, name))) {
const stat = await promisify(fs.stat)(p);
if (stat.isDirectory()) {
const sketch = await this.tryLoadSketch(p);
if (sketch) {
sketches.push({ name: sketch.name, uri: sketch.uri });
sketches.sort((left, right) => left.name.localeCompare(right.name));
} else {
const child = await this.load(p);
children.push(child);
children.sort((left, right) => left.label.localeCompare(right.label));
}
}
}
const label = basename(path);
return {
label,
children,
sketches,
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await promisify(fs.stat)(path);
map.set(path, stat);
}
return map;
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(
FileUri.create(path).toString()
);
return sketch;
} catch {
return undefined;
}
}
}

View File

@@ -40,11 +40,14 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy {
if (settings) {
await this.changeMonitorSettings(board, port, settings);
}
const status = await this.manager.startMonitor(board, port);
if (status === Status.ALREADY_CONNECTED || status === Status.OK) {
// Monitor started correctly, connect it with the frontend
this.client.connect(this.manager.getWebsocketAddressPort(board, port));
}
const connectToClient = (status: Status) => {
if (status === Status.ALREADY_CONNECTED || status === Status.OK) {
// Monitor started correctly, connect it with the frontend
this.client.connect(this.manager.getWebsocketAddressPort(board, port));
}
};
return this.manager.startMonitor(board, port, connectToClient);
}
/**

View File

@@ -1,6 +1,6 @@
import { ILogger } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Board, Port, Status } from '../common/protocol';
import { Board, BoardsService, Port, Status } from '../common/protocol';
import { CoreClientAware } from './core-client-provider';
import { MonitorService } from './monitor-service';
import { MonitorServiceFactory } from './monitor-service-factory';
@@ -11,16 +11,34 @@ import {
type MonitorID = string;
type UploadState = 'uploadInProgress' | 'pausedForUpload' | 'disposedForUpload';
type MonitorIDsByUploadState = Record<UploadState, MonitorID[]>;
export const MonitorManagerName = 'monitor-manager';
@injectable()
export class MonitorManager extends CoreClientAware {
@inject(BoardsService)
protected boardsService: BoardsService;
// Map of monitor services that manage the running pluggable monitors.
// Each service handles the lifetime of one, and only one, monitor.
// If either the board or port managed changes, a new service must
// be started.
private monitorServices = new Map<MonitorID, MonitorService>();
private monitorIDsByUploadState: MonitorIDsByUploadState = {
uploadInProgress: [],
pausedForUpload: [],
disposedForUpload: [],
};
private monitorServiceStartQueue: {
monitorID: string;
serviceStartParams: [Board, Port];
connectToClient: (status: Status) => void;
}[] = [];
@inject(MonitorServiceFactory)
private monitorServiceFactory: MonitorServiceFactory;
@@ -48,6 +66,33 @@ export class MonitorManager extends CoreClientAware {
return false;
}
private uploadIsInProgress(): boolean {
return this.monitorIDsByUploadState.uploadInProgress.length > 0;
}
private addToMonitorIDsByUploadState(
state: UploadState,
monitorID: string
): void {
this.monitorIDsByUploadState[state].push(monitorID);
}
private removeFromMonitorIDsByUploadState(
state: UploadState,
monitorID: string
): void {
this.monitorIDsByUploadState[state] = this.monitorIDsByUploadState[
state
].filter((id) => id !== monitorID);
}
private monitorIDIsInUploadState(
state: UploadState,
monitorID: string
): boolean {
return this.monitorIDsByUploadState[state].includes(monitorID);
}
/**
* Start a pluggable monitor that receives and sends messages
* to the specified board and port combination.
@@ -56,13 +101,34 @@ export class MonitorManager extends CoreClientAware {
* @returns a Status object to know if the process has been
* started or if there have been errors.
*/
async startMonitor(board: Board, port: Port): Promise<Status> {
async startMonitor(
board: Board,
port: Port,
connectToClient: (status: Status) => void
): Promise<void> {
const monitorID = this.monitorID(board, port);
let monitor = this.monitorServices.get(monitorID);
if (!monitor) {
monitor = this.createMonitor(board, port);
}
return await monitor.start();
if (this.uploadIsInProgress()) {
this.monitorServiceStartQueue = this.monitorServiceStartQueue.filter(
(request) => request.monitorID !== monitorID
);
this.monitorServiceStartQueue.push({
monitorID,
serviceStartParams: [board, port],
connectToClient,
});
return;
}
const result = await monitor.start();
connectToClient(result);
}
/**
@@ -111,14 +177,18 @@ export class MonitorManager extends CoreClientAware {
// to retrieve if we don't have this information.
return;
}
const monitorID = this.monitorID(board, port);
this.addToMonitorIDsByUploadState('uploadInProgress', monitorID);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
// There's no monitor running there, bail
return;
}
monitor.setUploadInProgress(true);
return await monitor.pause();
this.addToMonitorIDsByUploadState('pausedForUpload', monitorID);
return monitor.pause();
}
/**
@@ -130,19 +200,69 @@ export class MonitorManager extends CoreClientAware {
* started or if there have been errors.
*/
async notifyUploadFinished(board?: Board, port?: Port): Promise<Status> {
if (!board || !port) {
// We have no way of knowing which monitor
// to retrieve if we don't have this information.
return Status.NOT_CONNECTED;
let status: Status = Status.NOT_CONNECTED;
let portDidChangeOnUpload = false;
// We have no way of knowing which monitor
// to retrieve if we don't have this information.
if (board && port) {
const monitorID = this.monitorID(board, port);
this.removeFromMonitorIDsByUploadState('uploadInProgress', monitorID);
const monitor = this.monitorServices.get(monitorID);
if (monitor) {
status = await monitor.start();
}
// this monitorID will only be present in "disposedForUpload"
// if the upload changed the board port
portDidChangeOnUpload = this.monitorIDIsInUploadState(
'disposedForUpload',
monitorID
);
if (portDidChangeOnUpload) {
this.removeFromMonitorIDsByUploadState('disposedForUpload', monitorID);
}
// in case a service was paused but not disposed
this.removeFromMonitorIDsByUploadState('pausedForUpload', monitorID);
}
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
// There's no monitor running there, bail
return Status.NOT_CONNECTED;
await this.startQueuedServices(portDidChangeOnUpload);
return status;
}
async startQueuedServices(portDidChangeOnUpload: boolean): Promise<void> {
// if the port changed during upload with the monitor open, "startMonitorPendingRequests"
// will include a request for our "upload port', most likely at index 0.
// We remove it, as this port was to be used exclusively for the upload
const queued = portDidChangeOnUpload
? this.monitorServiceStartQueue.slice(1)
: this.monitorServiceStartQueue;
this.monitorServiceStartQueue = [];
for (const {
monitorID,
serviceStartParams: [_, port],
connectToClient,
} of queued) {
const boardsState = await this.boardsService.getState();
const boardIsStillOnPort = Object.keys(boardsState)
.map((connection: string) => {
const portAddress = connection.split('|')[0];
return portAddress;
})
.some((portAddress: string) => port.address === portAddress);
if (boardIsStillOnPort) {
const monitorService = this.monitorServices.get(monitorID);
if (monitorService) {
const result = await monitorService.start();
connectToClient(result);
}
}
}
monitor.setUploadInProgress(false);
return await monitor.start();
}
/**
@@ -202,6 +322,18 @@ export class MonitorManager extends CoreClientAware {
this.monitorServices.set(monitorID, monitor);
monitor.onDispose(
(() => {
// if a service is disposed during upload and
// we paused it beforehand we know it was disposed
// of because the upload changed the board port
if (
this.uploadIsInProgress() &&
this.monitorIDIsInUploadState('pausedForUpload', monitorID)
) {
this.removeFromMonitorIDsByUploadState('pausedForUpload', monitorID);
this.addToMonitorIDsByUploadState('disposedForUpload', monitorID);
}
this.monitorServices.delete(monitorID);
}).bind(this)
);

View File

@@ -60,7 +60,6 @@ export class MonitorService extends CoreClientAware implements Disposable {
protected readonly onDisposeEmitter = new Emitter<void>();
readonly onDispose = this.onDisposeEmitter.event;
protected uploadInProgress = false;
protected _initialized = new Deferred<void>();
protected creating: Deferred<Status>;
@@ -114,10 +113,6 @@ export class MonitorService extends CoreClientAware implements Disposable {
return this._initialized.promise;
}
setUploadInProgress(status: boolean): void {
this.uploadInProgress = status;
}
getWebsocketAddressPort(): number {
return this.webSocketProvider.getAddress().port;
}
@@ -161,15 +156,6 @@ export class MonitorService extends CoreClientAware implements Disposable {
return this.creating.promise;
}
if (this.uploadInProgress) {
this.updateClientsSettings({
monitorUISettings: { connected: false, serialPort: this.port.address },
});
this.creating.resolve(Status.UPLOAD_IN_PROGRESS);
return this.creating.promise;
}
this.logger.info('starting monitor');
// get default monitor settings from the CLI

View File

@@ -0,0 +1,23 @@
import { Metadata, StatusObject } from '@grpc/grpc-js';
export type ServiceError = StatusObject & Error;
export namespace ServiceError {
export function is(arg: unknown): arg is ServiceError {
return arg instanceof Error && isStatusObjet(arg);
}
function isStatusObjet(arg: unknown): arg is StatusObject {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const any = arg as any;
return (
!!arg &&
'code' in arg &&
'details' in arg &&
typeof any.details === 'string' &&
'metadata' in arg &&
any.metadata instanceof Metadata
);
}
return false;
}
}

View File

@@ -0,0 +1,20 @@
import { injectable } from '@theia/core/shared/inversify';
import { SurveyNotificationService } from '../common/protocol/survey-service';
/**
* Service for checking if it is the first instance of the IDE, in this case it sets a flag to true.
* This flag is used to prevent the survey notification from being visible in every open window. It must only be shown on one window.
*/
@injectable()
export class SurveyNotificationServiceImpl
implements SurveyNotificationService
{
private surveyDidShow = false;
async isFirstInstance(): Promise<boolean> {
if (this.surveyDidShow) {
return false;
}
this.surveyDidShow = true;
return this.surveyDidShow;
}
}

View File

@@ -0,0 +1,72 @@
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { OutputMessage } from '../../common/protocol';
const DEFAULT_FLUS_TIMEOUT_MS = 32;
export class SimpleBuffer implements Disposable {
private readonly chunks = Chunks.create();
private readonly flush: () => void;
private flushInterval?: NodeJS.Timeout;
constructor(
onFlush: (chunks: Map<OutputMessage.Severity, string | undefined>) => void,
flushTimeout: number = DEFAULT_FLUS_TIMEOUT_MS
) {
this.flush = () => {
if (!Chunks.isEmpty(this.chunks)) {
const chunks = Chunks.toString(this.chunks);
this.clearChunks();
onFlush(chunks);
}
};
this.flushInterval = setInterval(this.flush, flushTimeout);
}
addChunk(
chunk: Uint8Array,
severity: OutputMessage.Severity = OutputMessage.Severity.Info
): void {
this.chunks.get(severity)?.push(chunk);
}
private clearChunks(): void {
Chunks.clear(this.chunks);
}
dispose(): void {
this.flush();
clearInterval(this.flushInterval);
this.clearChunks();
this.flushInterval = undefined;
}
}
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
namespace Chunks {
export function create(): Chunks {
return new Map([
[OutputMessage.Severity.Error, []],
[OutputMessage.Severity.Warning, []],
[OutputMessage.Severity.Info, []],
]);
}
export function clear(chunks: Chunks): Chunks {
for (const chunk of chunks.values()) {
chunk.length = 0;
}
return chunks;
}
export function isEmpty(chunks: Chunks): boolean {
return ![...chunks.values()].some((chunk) => Boolean(chunk.length));
}
export function toString(
chunks: Chunks
): Map<OutputMessage.Severity, string | undefined> {
return new Map(
Array.from(chunks.entries()).map(([severity, buffers]) => [
severity,
buffers.length ? Buffer.concat(buffers).toString() : undefined,
])
);
}
}

View File

@@ -1,13 +1,12 @@
{
"private": true,
"name": "browser-app",
"version": "2.0.0-rc7",
"version": "2.0.0-rc8",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@theia/core": "1.25.0",
"@theia/debug": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/editor-preview": "1.25.0",
"@theia/file-search": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
@@ -20,7 +19,7 @@
"@theia/process": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"arduino-ide-extension": "2.0.0-rc7"
"arduino-ide-extension": "2.0.0-rc8"
},
"devDependencies": {
"@theia/cli": "1.25.0"

View File

@@ -1,14 +1,13 @@
{
"private": true,
"name": "electron-app",
"version": "2.0.0-rc7",
"version": "2.0.0-rc8",
"license": "AGPL-3.0-or-later",
"main": "src-gen/frontend/electron-main.js",
"dependencies": {
"@theia/core": "1.25.0",
"@theia/debug": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/editor-preview": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/file-search": "1.25.0",
"@theia/filesystem": "1.25.0",
@@ -22,7 +21,7 @@
"@theia/process": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"arduino-ide-extension": "2.0.0-rc7"
"arduino-ide-extension": "2.0.0-rc8"
},
"devDependencies": {
"@theia/cli": "1.25.0",

View File

@@ -56,9 +56,27 @@
"uploadRootCertificates": "Upload SSL Root Certificates",
"uploadingCertificates": "Uploading certificates."
},
"cli-error-parser": {
"byteError": "The 'BYTE' keyword is no longer supported.",
"byteMessage": "As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead.",
"clientError": "The Client class has been renamed EthernetClient.",
"clientMessage": "As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.",
"keyboardError": "'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?",
"mouseError": "'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?",
"receiveError": "Wire.receive() has been renamed Wire.read().",
"receiveMessage": "As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.",
"sendError": "Wire.send() has been renamed Wire.write().",
"sendMessage": "As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.",
"serverError": "The Server class has been renamed EthernetServer.",
"serverMessage": "As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.",
"spiError": "Please import the SPI library from the Sketch > Import Library menu.",
"spiMessage": "As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.",
"udpError": "The Udp class has been renamed EthernetUdp.",
"udpMessage": "As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp."
},
"cloud": {
"account": "Account",
"chooseSketchVisibility": "Choose visibility of your Sketch:",
"cloudSketchbook": "Cloud Sketchbook",
"connected": "Connected",
"continue": "Continue",
"donePulling": "Done pulling {0}.",
@@ -82,12 +100,14 @@
"pushSketch": "Push Sketch",
"pushSketchMsg": "This is a Public Sketch. Before pushing, make sure any sensitive information is defined in arduino_secrets.h files. You can make a Sketch private from the Share panel.",
"remote": "Remote",
"remoteSketchbook": "Remote Sketchbook",
"share": "Share...",
"shareSketch": "Share Sketch",
"showHideRemoveSketchbook": "Show/Hide Remote Sketchbook",
"signIn": "SIGN IN",
"signInToCloud": "Sign in to Arduino Cloud",
"signOut": "Sign Out",
"sync": "Sync",
"syncEditSketches": "Sync and edit your Arduino Cloud Sketches",
"visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches."
},
@@ -120,6 +140,9 @@
"fileAdded": "One file added to the sketch.",
"replaceTitle": "Replace"
},
"coreContribution": {
"copyError": "Copy error messages"
},
"debug": {
"debugWithMessage": "Debug - {0}",
"debuggingNotSupported": "Debugging is not supported by '{0}'",
@@ -136,7 +159,9 @@
"decreaseFontSize": "Decrease Font Size",
"decreaseIndent": "Decrease Indent",
"increaseFontSize": "Increase Font Size",
"increaseIndent": "Increase Indent"
"increaseIndent": "Increase Indent",
"nextError": "Next Error",
"previousError": "Previous Error"
},
"electron": {
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
@@ -229,12 +254,15 @@
"board.certificates": "List of certificates that can be uploaded to boards",
"browse": "Browse",
"choose": "Choose",
"cli.daemonDebug": "Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default.",
"cloud.enabled": "True if the sketch sync functions are enabled. Defaults to true.",
"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.",
"compile": "compile",
"compile.experimental": "True if the IDE should handle multiple compiler errors. 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",
"compilerWarnings": "Compiler warnings",
@@ -256,6 +284,7 @@
"showVerbose": "Show verbose output during",
"sketchbook.location": "Sketchbook location",
"sketchbook.showAllFiles": "True to show all sketch files inside the sketch. It is false by default.",
"survey.notification": "True if users should be notified if a survey is available. True by default.",
"unofficialBoardSupport": "Click for a list of unofficial board support URLs",
"upload": "upload",
"upload.verbose": "True for verbose upload output. False by default.",
@@ -305,6 +334,11 @@
"verify": "Verify",
"verifyOrCompile": "Verify/Compile"
},
"survey": {
"answerSurvey": "Answer survey",
"dismissSurvey": "Don't show again",
"surveyMessage": "Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better."
},
"upload": {
"error": "{0} error: {1}"
},

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide",
"version": "2.0.0-rc7",
"version": "2.0.0-rc8",
"description": "Arduino IDE",
"repository": "https://github.com/arduino/arduino-ide.git",
"author": "Arduino SA",

161
yarn.lock
View File

@@ -2569,15 +2569,6 @@
unzip-stream "^0.3.0"
vscode-debugprotocol "^1.32.0"
"@theia/editor-preview@1.25.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@theia/editor-preview/-/editor-preview-1.25.0.tgz#bf01c28d127a3a6934ffdc0ee3fbf85ee09de6ad"
integrity sha512-2OwX2FL8BawlDYZQR+iiPC28fUepEblhxOl2b7u9BiwpJRjRjzNDbF2bU66EEHn2wNKsynVJCyOJH9B7+R9+6A==
dependencies:
"@theia/core" "1.25.0"
"@theia/editor" "1.25.0"
"@theia/navigator" "1.25.0"
"@theia/editor@1.25.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@theia/editor/-/editor-1.25.0.tgz#7faeda4fdf30be28873d9710edf39c146b847b1b"
@@ -3047,6 +3038,11 @@
dependencies:
"@types/ms" "*"
"@types/deep-equal@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
"@types/deepmerge@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/deepmerge/-/deepmerge-2.2.0.tgz#6f63896c217f3164782f52d858d9f3a927139f64"
@@ -4508,6 +4504,11 @@ autosize@^4.0.2:
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03"
integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==
available-typed-arrays@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -6146,6 +6147,27 @@ deep-eql@^3.0.1:
dependencies:
type-detect "^4.0.0"
deep-equal@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9"
integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==
dependencies:
call-bind "^1.0.0"
es-get-iterator "^1.1.1"
get-intrinsic "^1.0.1"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.1.1"
isarray "^2.0.5"
object-is "^1.1.4"
object-keys "^1.1.1"
object.assign "^4.1.2"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.3"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
@@ -6646,7 +6668,7 @@ error-symbol@^0.1.0:
resolved "https://registry.yarnpkg.com/error-symbol/-/error-symbol-0.1.0.tgz#0a4dae37d600d15a29ba453d8ef920f1844333f6"
integrity sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y=
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0:
version "1.20.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
@@ -6675,6 +6697,20 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19
string.prototype.trimstart "^1.0.5"
unbox-primitive "^1.0.2"
es-get-iterator@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.0"
has-symbols "^1.0.1"
is-arguments "^1.1.0"
is-map "^2.0.2"
is-set "^2.0.2"
is-string "^1.0.5"
isarray "^2.0.5"
es-module-lexer@^0.9.0:
version "0.9.3"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
@@ -7413,6 +7449,13 @@ font-awesome@^4.7.0:
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
dependencies:
is-callable "^1.1.3"
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -7671,6 +7714,15 @@ get-func-name@^2.0.0:
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
get-intrinsic@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.3"
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
@@ -8718,6 +8770,14 @@ is-accessor-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-arguments@^1.0.4, is-arguments@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
dependencies:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -8755,7 +8815,7 @@ is-buffer@^2.0.0, is-buffer@~2.0.3:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
is-callable@^1.1.4, is-callable@^1.2.4:
is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -8788,7 +8848,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1:
is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
@@ -8917,6 +8977,11 @@ is-lambda@^1.0.1:
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
is-map@^2.0.1, is-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-natural-number@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
@@ -9012,7 +9077,7 @@ is-redirect@^1.0.0:
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
is-regex@^1.1.4:
is-regex@^1.1.1, is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -9037,6 +9102,11 @@ is-self-closing@^1.0.1:
dependencies:
self-closing-tags "^1.0.1"
is-set@^2.0.1, is-set@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
is-shared-array-buffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
@@ -9082,6 +9152,17 @@ is-text-path@^1.0.1:
dependencies:
text-extensions "^1.0.0"
is-typed-array@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67"
integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
es-abstract "^1.20.0"
for-each "^0.3.3"
has-tostringtag "^1.0.0"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -9104,6 +9185,11 @@ is-valid-path@^0.1.1:
dependencies:
is-invalid-path "^0.1.0"
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakref@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
@@ -9111,6 +9197,14 @@ is-weakref@^1.0.2:
dependencies:
call-bind "^1.0.2"
is-weakset@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d"
integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
is-what@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@@ -9138,6 +9232,11 @@ isarray@1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -11258,6 +11357,14 @@ object-inspect@^1.12.0, object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
object-is@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
object-keys@^1.0.11, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -12769,7 +12876,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
regexp.prototype.flags@^1.3.0, regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
@@ -13376,7 +13483,7 @@ shelljs@^0.8.3:
interpret "^1.0.0"
rechoir "^0.6.2"
side-channel@^1.0.4:
side-channel@^1.0.3, side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
@@ -15276,7 +15383,7 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-boxed-primitive@^1.0.2:
which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
@@ -15287,6 +15394,16 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5"
is-symbol "^1.0.3"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -15297,6 +15414,18 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35"
integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==
which-typed-array@^1.1.2:
version "1.1.8"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
es-abstract "^1.20.0"
for-each "^0.3.3"
has-tostringtag "^1.0.0"
is-typed-array "^1.1.9"
which@1.3.1, which@^1.2.8, which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"