mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-21 17:18:32 +00:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
439cdfbbff | ||
![]() |
672fd4e4b0 | ||
![]() |
0f1d379e58 | ||
![]() |
a79c9b4449 | ||
![]() |
0f8a29a493 | ||
![]() |
a54d7c8f45 | ||
![]() |
84109e416a | ||
![]() |
083337de1c | ||
![]() |
bd6bc135fd | ||
![]() |
4611381a38 | ||
![]() |
d6f4096cd0 | ||
![]() |
a715da3d18 | ||
![]() |
94ceefd960 | ||
![]() |
27dd120e5d | ||
![]() |
f5cee97fef | ||
![]() |
a9aac0dbb0 | ||
![]() |
4c6243176c | ||
![]() |
a8047660a6 | ||
![]() |
7c2843f7fd |
@@ -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"
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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');
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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.
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -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}`;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
13
arduino-ide-extension/src/browser/ino-selectors.ts
Normal file
13
arduino-ide-extension/src/browser/ino-selectors.ts
Normal 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.
|
||||
}));
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -130,5 +130,5 @@ DockPanel.prototype.handleEvent = function (event) {
|
||||
case 'p-drop':
|
||||
return;
|
||||
}
|
||||
originalHandleEvent(event);
|
||||
originalHandleEvent.bind(this)(event);
|
||||
};
|
||||
|
@@ -21,6 +21,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
|
||||
CommonCommands.TOGGLE_MAXIMIZED,
|
||||
CommonCommands.PIN_TAB,
|
||||
CommonCommands.UNPIN_TAB,
|
||||
CommonCommands.NEW_FILE,
|
||||
]) {
|
||||
commandRegistry.unregisterCommand(command);
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
8
arduino-ide-extension/src/browser/utils/monaco.ts
Normal file
8
arduino-ide-extension/src/browser/utils/monaco.ts
Normal 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);
|
||||
}
|
@@ -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;
|
||||
|
@@ -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) => {
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
export const SurveyNotificationServicePath =
|
||||
'/services/survey-notification-service';
|
||||
export const SurveyNotificationService = Symbol('SurveyNotificationService');
|
||||
|
||||
export interface SurveyNotificationService {
|
||||
isFirstInstance(): Promise<boolean>;
|
||||
}
|
@@ -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[] = [],
|
||||
|
@@ -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();
|
||||
});
|
||||
|
@@ -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<{
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal file
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal 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>'?"
|
||||
),
|
||||
},
|
||||
};
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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)
|
||||
);
|
||||
|
@@ -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
|
||||
|
23
arduino-ide-extension/src/node/service-error.ts
Normal file
23
arduino-ide-extension/src/node/service-error.ts
Normal 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;
|
||||
}
|
||||
}
|
20
arduino-ide-extension/src/node/survey-service-impl.ts
Normal file
20
arduino-ide-extension/src/node/survey-service-impl.ts
Normal 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;
|
||||
}
|
||||
}
|
72
arduino-ide-extension/src/node/utils/simple-buffer.ts
Normal file
72
arduino-ide-extension/src/node/utils/simple-buffer.ts
Normal 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,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
@@ -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",
|
||||
|
38
i18n/en.json
38
i18n/en.json
@@ -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}"
|
||||
},
|
||||
|
@@ -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
161
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user