diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 143c7d6c5..48103f587 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -1,6 +1,6 @@ { "name": "arduino-ide-extension", - "version": "2.0.0-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" diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 9ef3c2423..5a0b5221b 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -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); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index 0aec020d5..0caf22837 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -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'); diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index 17cf91f30..ef2ec75b1 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -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); } } } diff --git a/arduino-ide-extension/src/browser/contributions/compiler-errors.ts b/arduino-ide-extension/src/browser/contributions/compiler-errors.ts new file mode 100644 index 000000000..728341110 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/compiler-errors.ts @@ -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 + ): Promise; + export function rangeOf( + { id, uri }: ErrorDecoration, + editorProvider: MonacoEditor + ): monaco.Range | undefined; + export function rangeOf( + { id, uri }: ErrorDecoration, + editorProvider: + | ((uri: string) => Promise) + | MonacoEditor + ): MaybePromise { + 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 + // ): Promise { + // 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(); + private readonly currentErrorDidChangEmitter = new Emitter(); + 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 { + return this.onDidChangeEmitter.event; + } + + async provideCodeLenses( + model: monaco.editor.ITextModel, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: monaco.CancellationToken + ): Promise { + 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 { + 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 { + 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 + ): 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, + ...track: ((editor: EditorWidget) => Disposable)[] + ): Promise { + 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 { + 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 { + 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 { + 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()); + } + + private monacoEditor(widget: EditorWidget): MonacoEditor | undefined; + private monacoEditor(uri: string): Promise; + private monacoEditor( + uriOrWidget: string | EditorWidget + ): MaybePromise { + 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', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 1597cac28..fc51d5b65 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -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, diff --git a/arduino-ide-extension/src/browser/contributions/core-error-handler.ts b/arduino-ide-extension/src/browser/contributions/core-error-handler.ts new file mode 100644 index 000000000..9eec07cd6 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/core-error-handler.ts @@ -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 { + return this.compilerErrorsDidChangeEmitter.event; + } + + private fireCompilerErrorsDidChange(): void { + this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice()); + } +} diff --git a/arduino-ide-extension/src/browser/contributions/format.ts b/arduino-ide-extension/src/browser/contributions/format.ts index 17f1edf0a..e270181b0 100644 --- a/arduino-ide-extension/src/browser/contributions/format.ts +++ b/arduino-ide-extension/src/browser/contributions/format.ts @@ -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 { - 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 { - 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. - })); - } } diff --git a/arduino-ide-extension/src/browser/contributions/survey-notification.ts b/arduino-ide-extension/src/browser/contributions/survey-notification.ts new file mode 100644 index 000000000..e1a4817a6 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/survey-notification.ts @@ -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}`; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 76a8f4973..ebfd02c6d 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -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(); diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index fdc5b504f..b7f391bd5 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -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(); diff --git a/arduino-ide-extension/src/browser/ino-selectors.ts b/arduino-ide-extension/src/browser/ino-selectors.ts new file mode 100644 index 000000000..e413dba26 --- /dev/null +++ b/arduino-ide-extension/src/browser/ino-selectors.ts @@ -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. + })); +} diff --git a/arduino-ide-extension/src/browser/response-service-impl.ts b/arduino-ide-extension/src/browser/response-service-impl.ts index 621364a59..c50506c86 100644 --- a/arduino-ide-extension/src/browser/response-service-impl.ts +++ b/arduino-ide-extension/src/browser/response-service-impl.ts @@ -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(); + private readonly progressDidChangeEmitter = new Emitter(); 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; +} diff --git a/arduino-ide-extension/src/browser/style/editor.css b/arduino-ide-extension/src/browser/style/editor.css index 5be2d405d..484e521c9 100644 --- a/arduino-ide-extension/src/browser/style/editor.css +++ b/arduino-ide-extension/src/browser/style/editor.css @@ -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; +} diff --git a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css index 0060e323d..d655cdef2 100644 --- a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css +++ b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css @@ -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; diff --git a/arduino-ide-extension/src/browser/theia/core/application-shell.ts b/arduino-ide-extension/src/browser/theia/core/application-shell.ts index eff6a3a04..b9e0d4c67 100644 --- a/arduino-ide-extension/src/browser/theia/core/application-shell.ts +++ b/arduino-ide-extension/src/browser/theia/core/application-shell.ts @@ -130,5 +130,5 @@ DockPanel.prototype.handleEvent = function (event) { case 'p-drop': return; } - originalHandleEvent(event); + originalHandleEvent.bind(this)(event); }; diff --git a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts index 8a0d30b5d..c92d4972f 100644 --- a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts @@ -21,6 +21,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution CommonCommands.TOGGLE_MAXIMIZED, CommonCommands.PIN_TAB, CommonCommands.UNPIN_TAB, + CommonCommands.NEW_FILE, ]) { commandRegistry.unregisterCommand(command); } diff --git a/arduino-ide-extension/src/browser/theia/core/shell-layout-restorer.ts b/arduino-ide-extension/src/browser/theia/core/shell-layout-restorer.ts index 23a5f9a25..5c13d31ee 100644 --- a/arduino-ide-extension/src/browser/theia/core/shell-layout-restorer.ts +++ b/arduino-ide-extension/src/browser/theia/core/shell-layout-restorer.ts @@ -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 { @@ -160,8 +163,8 @@ export class ShellLayoutRestorer extends TheiaShellLayoutRestorer { constructionOptions: { factoryId: EditorWidgetFactory.ID, options: { - uri, kind: 'navigatable', + uri, counter: 0, }, }, diff --git a/arduino-ide-extension/src/browser/theia/core/widget-manager.ts b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts new file mode 100644 index 000000000..adc32860f --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts @@ -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( + key: string + ): MaybePromise | undefined { + const pendingWidget = this.findExistingWidget(key); + if (pendingWidget) { + return pendingWidget as MaybePromise; + } + return undefined; + } + + private findExistingWidget( + key: string + ): MaybePromise | 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; + } + } + 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; + } + } +} diff --git a/arduino-ide-extension/src/browser/theia/core/window-contribution.ts b/arduino-ide-extension/src/browser/theia/core/window-contribution.ts new file mode 100644 index 000000000..4c2b6c8d6 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/window-contribution.ts @@ -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; + } +} diff --git a/arduino-ide-extension/src/browser/theia/editor/editor-contribution.ts b/arduino-ide-extension/src/browser/theia/editor/editor-contribution.ts index 817359215..f7b01b2a3 100644 --- a/arduino-ide-extension/src/browser/theia/editor/editor-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/editor/editor-contribution.ts @@ -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 + } } diff --git a/arduino-ide-extension/src/browser/theia/output/output-channel.ts b/arduino-ide-extension/src/browser/theia/output/output-channel.ts index b928dce06..aa1c8206c 100644 --- a/arduino-ide-extension/src/browser/theia/output/output-channel.ts +++ b/arduino-ide-extension/src/browser/theia/output/output-channel.ts @@ -40,6 +40,14 @@ export class OutputChannelManager extends TheiaOutputChannelManager { } return channel; } + + async contentOfChannel(name: string): Promise { + const resource = this.resources.get(name); + if (resource) { + return resource.readContents(); + } + return undefined; + } } export class OutputChannel extends TheiaOutputChannel { diff --git a/arduino-ide-extension/src/browser/theia/preferences/preference-tree-generator.ts b/arduino-ide-extension/src/browser/theia/preferences/preference-tree-generator.ts index 0dcb7af66..6ff7df05b 100644 --- a/arduino-ide-extension/src/browser/theia/preferences/preference-tree-generator.ts +++ b/arduino-ide-extension/src/browser/theia/preferences/preference-tree-generator.ts @@ -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 { - // 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; } diff --git a/arduino-ide-extension/src/browser/utils/monaco.ts b/arduino-ide-extension/src/browser/utils/monaco.ts new file mode 100644 index 000000000..0e957a980 --- /dev/null +++ b/arduino-ide-extension/src/browser/utils/monaco.ts @@ -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); +} diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx index 9661c02fe..e60016d8e 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx @@ -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; diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx index 0db8dce92..3611deca7 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx @@ -65,6 +65,7 @@ export class UserStatus extends React.Component<
{ diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx index 240156472..a8581be40 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx @@ -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
; } @@ -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) + ); + } } diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 15aa85bb0..a783eae28 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -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 { + return ( + error instanceof Error && + ApplicationError.is(error) && + Object.values(Codes).includes(error.code) + ); + } + function create( + code: number + ): ApplicationError.Constructor { + 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; uploadUsingProgrammer(options: CoreService.Upload.Options): Promise; burnBootloader(options: CoreService.Bootloader.Options): Promise; - isUploading(): Promise; } 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; diff --git a/arduino-ide-extension/src/common/protocol/response-service.ts b/arduino-ide-extension/src/common/protocol/response-service.ts index 8a31877ab..9c2e4e248 100644 --- a/arduino-ide-extension/src/common/protocol/response-service.ts +++ b/arduino-ide-extension/src/common/protocol/response-service.ts @@ -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 { diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 722ada586..eb07572d7 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -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 { diff --git a/arduino-ide-extension/src/common/protocol/survey-service.ts b/arduino-ide-extension/src/common/protocol/survey-service.ts new file mode 100644 index 000000000..3ab53b230 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/survey-service.ts @@ -0,0 +1,7 @@ +export const SurveyNotificationServicePath = + '/services/survey-notification-service'; +export const SurveyNotificationService = Symbol('SurveyNotificationService'); + +export interface SurveyNotificationService { + isFirstInstance(): Promise; +} diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts index f18b07df8..98f8b76e0 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts @@ -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[] = [], diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index 7df38d649..7da0c7314 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -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 + ) + ) + ) + .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/node/arduino-daemon-impl.ts b/arduino-ide-extension/src/node/arduino-daemon-impl.ts index e53d6afe3..928861680 100644 --- a/arduino-ide-extension/src/node/arduino-daemon-impl.ts +++ b/arduino-ide-extension/src/node/arduino-daemon-impl.ts @@ -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 { - 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 { + // 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<{ diff --git a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts index 78f444fe8..d693ce758 100644 --- a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts +++ b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts @@ -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; } } diff --git a/arduino-ide-extension/src/node/cli-error-parser.ts b/arduino-ide-extension/src/node/cli-error-parser.ts new file mode 100644 index 000000000..25148250f --- /dev/null +++ b/arduino-ide-extension/src/node/cli-error-parser.ts @@ -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; + 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 = { + '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 '?" + ), + }, + "'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 '?" + ), + }, +}; diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index c585afeda..271248706 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -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 { @@ -90,10 +93,11 @@ export class CoreClientProvider extends GrpcClientProvider - 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 { @@ -192,7 +161,7 @@ export class CoreClientProvider extends GrpcClientProvider((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 { - // 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 { @@ -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); +} diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 00d6ca5ed..6d55b307b 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -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 { - 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((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((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 { - await this.doUpload( + upload(options: CoreService.Upload.Options): Promise { + 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 { - 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 { - 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, - task = 'upload' + errorHandler: ( + message: string, + info: CoreError.ErrorInfo[] + ) => ApplicationError, + task: string ): Promise { 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((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); - } - const p = new Port(); - if (port) { - p.setAddress(port.address); - p.setLabel(port.addressLabel); - p.setProtocol(port.protocol); - p.setProtocolLabel(port.protocolLabel); + request.setFqbn(board.fqbn); } - 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((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 { - 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((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); - } - const p = new Port(); - if (port) { - p.setAddress(port.address); - p.setLabel(port.addressLabel); - p.setProtocol(port.protocol); - p.setProtocolLabel(port.protocolLabel); + request.setFqbn(board.fqbn); } - 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((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()); - }); - } 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', + request.setVerify(options.verify); + request.setVerbose(options.verbose); + return request; + } + + private createOnDataHandler(): 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); + } }); - 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 { + return this.monitorManager.notifyUploadStarted(board, port); + } + + private async notifyUploadDidFinish({ + board, + port, + }: { + board?: Board | undefined; + port?: Port | undefined; + }): Promise { + return this.monitorManager.notifyUploadFinished(board, port); } private mergeSourceOverrides( req: { getSourceOverrideMap(): jspb.Map }, 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( + 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); + }; + } } diff --git a/arduino-ide-extension/src/node/examples-service-impl.ts b/arduino-ide-extension/src/node/examples-service-impl.ts index 0a056d0a7..0028791a6 100644 --- a/arduino-ide-extension/src/node/examples-service-impl.ts +++ b/arduino-ide-extension/src/node/examples-service-impl.ts @@ -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 { - 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> { - const map = new Map(); - for (const path of paths) { - const stat = await promisify(fs.stat)(path); - map.set(path, stat); - } - return map; - } - - protected async tryLoadSketch(path: string): Promise { - try { - const sketch = await this.sketchesService.loadSketch( - FileUri.create(path).toString() - ); - return sketch; - } catch { - return undefined; - } - } } diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index c4e9d59d5..7f284ac3f 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -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); } /** diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 5b5669059..fc458a49c 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -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; + 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(); + 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 { + async startMonitor( + board: Board, + port: Port, + connectToClient: (status: Status) => void + ): Promise { 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 { - 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 { + // 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) ); diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 086e98de7..a0ab86f6f 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -60,7 +60,6 @@ export class MonitorService extends CoreClientAware implements Disposable { protected readonly onDisposeEmitter = new Emitter(); readonly onDispose = this.onDisposeEmitter.event; - protected uploadInProgress = false; protected _initialized = new Deferred(); protected creating: Deferred; @@ -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 diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts new file mode 100644 index 000000000..3abbbc0b0 --- /dev/null +++ b/arduino-ide-extension/src/node/service-error.ts @@ -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; + } +} diff --git a/arduino-ide-extension/src/node/survey-service-impl.ts b/arduino-ide-extension/src/node/survey-service-impl.ts new file mode 100644 index 000000000..aea8c0472 --- /dev/null +++ b/arduino-ide-extension/src/node/survey-service-impl.ts @@ -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 { + if (this.surveyDidShow) { + return false; + } + this.surveyDidShow = true; + return this.surveyDidShow; + } +} diff --git a/arduino-ide-extension/src/node/utils/simple-buffer.ts b/arduino-ide-extension/src/node/utils/simple-buffer.ts new file mode 100644 index 000000000..1e3a8293d --- /dev/null +++ b/arduino-ide-extension/src/node/utils/simple-buffer.ts @@ -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) => 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; +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 { + return new Map( + Array.from(chunks.entries()).map(([severity, buffers]) => [ + severity, + buffers.length ? Buffer.concat(buffers).toString() : undefined, + ]) + ); + } +} diff --git a/browser-app/package.json b/browser-app/package.json index 334a0ee56..407ceb86d 100644 --- a/browser-app/package.json +++ b/browser-app/package.json @@ -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" diff --git a/electron-app/package.json b/electron-app/package.json index 1e2117e31..a7791d916 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -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", diff --git a/i18n/en.json b/i18n/en.json index 959f0e989..46e0c4488 100644 --- a/i18n/en.json +++ b/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 '?", + "mouseError": "'Mouse' not found. Does your sketch include the line '#include '?", + "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}" }, diff --git a/package.json b/package.json index f1a8d736a..2c03dcccc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 28eca1e30..8d93321a8 100644 --- a/yarn.lock +++ b/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"