mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 20:06:32 +00:00
Reveal the error location after on failed verify.
Closes #608 Closes #229 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
a715da3d18
commit
d6f4096cd0
@ -298,6 +298,8 @@ import {
|
|||||||
} from '../common/protocol/survey-service';
|
} from '../common/protocol/survey-service';
|
||||||
import { WindowContribution } from './theia/core/window-contribution';
|
import { WindowContribution } from './theia/core/window-contribution';
|
||||||
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/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';
|
||||||
|
|
||||||
MonacoThemingService.register({
|
MonacoThemingService.register({
|
||||||
id: 'arduino-theme',
|
id: 'arduino-theme',
|
||||||
@ -430,6 +432,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.inSingletonScope();
|
.inSingletonScope();
|
||||||
|
bind(CoreErrorHandler).toSelf().inSingletonScope();
|
||||||
|
|
||||||
// Serial monitor
|
// Serial monitor
|
||||||
bind(MonitorWidget).toSelf();
|
bind(MonitorWidget).toSelf();
|
||||||
@ -694,6 +697,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
Contribution.configure(bind, AddZipLibrary);
|
Contribution.configure(bind, AddZipLibrary);
|
||||||
Contribution.configure(bind, PlotterFrontendContribution);
|
Contribution.configure(bind, PlotterFrontendContribution);
|
||||||
Contribution.configure(bind, Format);
|
Contribution.configure(bind, Format);
|
||||||
|
Contribution.configure(bind, CompilerErrors);
|
||||||
|
|
||||||
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
// 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.
|
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||||
|
@ -13,6 +13,32 @@ export enum UpdateChannel {
|
|||||||
Stable = 'stable',
|
Stable = 'stable',
|
||||||
Nightly = 'nightly',
|
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 = {
|
export const ArduinoConfigSchema: PreferenceSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -33,6 +59,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
|||||||
),
|
),
|
||||||
default: false,
|
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': {
|
'arduino.compile.warnings': {
|
||||||
enum: [...CompilerWarningLiterals],
|
enum: [...CompilerWarningLiterals],
|
||||||
description: nls.localize(
|
description: nls.localize(
|
||||||
@ -196,6 +239,8 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
|||||||
export interface ArduinoConfiguration {
|
export interface ArduinoConfiguration {
|
||||||
'arduino.language.log': boolean;
|
'arduino.language.log': boolean;
|
||||||
'arduino.compile.verbose': boolean;
|
'arduino.compile.verbose': boolean;
|
||||||
|
'arduino.compile.experimental': boolean;
|
||||||
|
'arduino.compile.revealRange': ErrorRevealStrategy;
|
||||||
'arduino.compile.warnings': CompilerWarnings;
|
'arduino.compile.warnings': CompilerWarnings;
|
||||||
'arduino.upload.verbose': boolean;
|
'arduino.upload.verbose': boolean;
|
||||||
'arduino.upload.verify': boolean;
|
'arduino.upload.verify': boolean;
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
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 { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
CoreServiceContribution,
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
@ -13,20 +11,13 @@ import {
|
|||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BurnBootloader extends SketchContribution {
|
export class BurnBootloader extends CoreServiceContribution {
|
||||||
@inject(CoreService)
|
|
||||||
protected readonly coreService: CoreService;
|
|
||||||
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
@inject(BoardsDataStore)
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
protected readonly boardsDataStore: BoardsDataStore;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||||
|
|
||||||
@inject(OutputChannelManager)
|
|
||||||
protected override readonly outputChannelManager: OutputChannelManager;
|
|
||||||
|
|
||||||
override registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||||
execute: () => this.burnBootloader(),
|
execute: () => this.burnBootloader(),
|
||||||
@ -62,7 +53,7 @@ export class BurnBootloader extends SketchContribution {
|
|||||||
...boardsConfig.selectedBoard,
|
...boardsConfig.selectedBoard,
|
||||||
name: boardsConfig.selectedBoard?.name || '',
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
fqbn,
|
fqbn,
|
||||||
}
|
};
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
this.outputChannelManager.getChannel('Arduino').clear();
|
||||||
await this.coreService.burnBootloader({
|
await this.coreService.burnBootloader({
|
||||||
board,
|
board,
|
||||||
@ -81,13 +72,7 @@ export class BurnBootloader extends SketchContribution {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorMessage = "";
|
this.handleError(e);
|
||||||
if (typeof e === "string") {
|
|
||||||
errorMessage = e;
|
|
||||||
} else {
|
|
||||||
errorMessage = e.toString();
|
|
||||||
}
|
|
||||||
this.messageService.error(errorMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,656 @@
|
|||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
Emitter,
|
||||||
|
MaybePromise,
|
||||||
|
nls,
|
||||||
|
notEmpty,
|
||||||
|
} from '@theia/core';
|
||||||
|
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||||
|
import URI from '@theia/core/lib/common/uri';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
|
import {
|
||||||
|
EditorWidget,
|
||||||
|
TextDocumentChangeEvent,
|
||||||
|
} from '@theia/editor/lib/browser';
|
||||||
|
import {
|
||||||
|
EditorDecoration,
|
||||||
|
TrackedRangeStickiness,
|
||||||
|
} from '@theia/editor/lib/browser/decorations/editor-decoration';
|
||||||
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
|
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||||
|
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||||
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
|
import {
|
||||||
|
ArduinoPreferences,
|
||||||
|
ErrorRevealStrategy,
|
||||||
|
} from '../arduino-preferences';
|
||||||
|
import { InoSelector } from '../ino-selectors';
|
||||||
|
import { fullRange } from '../utils/monaco';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
import { CoreErrorHandler } from './core-error-handler';
|
||||||
|
|
||||||
|
interface ErrorDecoration {
|
||||||
|
/**
|
||||||
|
* This is the unique ID of the decoration given by `monaco`.
|
||||||
|
*/
|
||||||
|
readonly id: string;
|
||||||
|
/**
|
||||||
|
* The resource this decoration belongs to.
|
||||||
|
*/
|
||||||
|
readonly uri: string;
|
||||||
|
}
|
||||||
|
namespace ErrorDecoration {
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
||||||
|
): Promise<monaco.Range | undefined>;
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider: MonacoEditor
|
||||||
|
): monaco.Range | undefined;
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider:
|
||||||
|
| ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||||
|
| MonacoEditor
|
||||||
|
): MaybePromise<monaco.Range | undefined> {
|
||||||
|
if (editorProvider instanceof MonacoEditor) {
|
||||||
|
const control = editorProvider.getControl();
|
||||||
|
const model = control.getModel();
|
||||||
|
if (model) {
|
||||||
|
return control
|
||||||
|
.getDecorationsInRange(fullRange(model))
|
||||||
|
?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return editorProvider(uri).then((editor) => {
|
||||||
|
if (editor) {
|
||||||
|
return rangeOf({ id, uri }, editor);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function rangeOf(
|
||||||
|
// { id, uri }: ErrorDecoration,
|
||||||
|
// editorProvider:
|
||||||
|
// | ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||||
|
// | MonacoEditor
|
||||||
|
// ): Promise<monaco.Range | undefined> {
|
||||||
|
// const editor =
|
||||||
|
// editorProvider instanceof MonacoEditor
|
||||||
|
// ? editorProvider
|
||||||
|
// : await editorProvider(uri);
|
||||||
|
// if (editor) {
|
||||||
|
// const control = editor.getControl();
|
||||||
|
// const model = control.getModel();
|
||||||
|
// if (model) {
|
||||||
|
// return control
|
||||||
|
// .getDecorationsInRange(fullRange(model))
|
||||||
|
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
export function sameAs(
|
||||||
|
left: ErrorDecoration,
|
||||||
|
right: ErrorDecoration
|
||||||
|
): boolean {
|
||||||
|
return left.id === right.id && left.uri === right.uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CompilerErrors
|
||||||
|
extends Contribution
|
||||||
|
implements monaco.languages.CodeLensProvider
|
||||||
|
{
|
||||||
|
@inject(EditorManager)
|
||||||
|
private readonly editorManager: EditorManager;
|
||||||
|
|
||||||
|
@inject(ProtocolToMonacoConverter)
|
||||||
|
private readonly p2m: ProtocolToMonacoConverter;
|
||||||
|
|
||||||
|
@inject(MonacoToProtocolConverter)
|
||||||
|
private readonly mp2: MonacoToProtocolConverter;
|
||||||
|
|
||||||
|
@inject(CoreErrorHandler)
|
||||||
|
private readonly coreErrorHandler: CoreErrorHandler;
|
||||||
|
|
||||||
|
@inject(ArduinoPreferences)
|
||||||
|
private readonly preferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
private readonly errors: ErrorDecoration[] = [];
|
||||||
|
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
||||||
|
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
||||||
|
private readonly onCurrentErrorDidChange =
|
||||||
|
this.currentErrorDidChangEmitter.event;
|
||||||
|
private readonly toDisposeOnCompilerErrorDidChange =
|
||||||
|
new DisposableCollection();
|
||||||
|
private shell: ApplicationShell | undefined;
|
||||||
|
private revealStrategy = ErrorRevealStrategy.Default;
|
||||||
|
private currentError: ErrorDecoration | undefined;
|
||||||
|
private get currentErrorIndex(): number {
|
||||||
|
const current = this.currentError;
|
||||||
|
if (!current) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.errors.findIndex((error) =>
|
||||||
|
ErrorDecoration.sameAs(error, current)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onStart(app: FrontendApplication): void {
|
||||||
|
this.shell = app.shell;
|
||||||
|
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||||
|
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||||
|
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
||||||
|
);
|
||||||
|
this.onCurrentErrorDidChange(async (error) => {
|
||||||
|
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
||||||
|
this.monacoEditor(uri)
|
||||||
|
);
|
||||||
|
if (!range) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not find range of decoration: ${error.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editor = await this.revealLocationInEditor({
|
||||||
|
uri: error.uri,
|
||||||
|
range: this.mp2.asRange(range),
|
||||||
|
});
|
||||||
|
if (!editor) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${error.id} as the current one.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.preferences.ready.then(() => {
|
||||||
|
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||||
|
if (preferenceName === 'arduino.compile.revealRange') {
|
||||||
|
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||||
|
? newValue
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
|
||||||
|
execute: () => {
|
||||||
|
const index = this.currentErrorIndex;
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not advance to next error. Unknown current error.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextError =
|
||||||
|
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
||||||
|
this.markAsCurrentError(nextError);
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
||||||
|
execute: () => {
|
||||||
|
const index = this.currentErrorIndex;
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not advance to previous error. Unknown current error.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousError =
|
||||||
|
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
||||||
|
this.markAsCurrentError(previousError);
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get onDidChange(): monaco.IEvent<this> {
|
||||||
|
return this.onDidChangeEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideCodeLenses(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.CodeLensList> {
|
||||||
|
const lenses: monaco.languages.CodeLens[] = [];
|
||||||
|
if (
|
||||||
|
this.currentError &&
|
||||||
|
this.currentError.uri === model.uri.toString() &&
|
||||||
|
this.errors.length > 1
|
||||||
|
) {
|
||||||
|
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
||||||
|
this.monacoEditor(uri)
|
||||||
|
);
|
||||||
|
if (range) {
|
||||||
|
lenses.push(
|
||||||
|
{
|
||||||
|
range,
|
||||||
|
command: {
|
||||||
|
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/editor/previousError',
|
||||||
|
'Previous Error'
|
||||||
|
),
|
||||||
|
arguments: [this.currentError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range,
|
||||||
|
command: {
|
||||||
|
id: CompilerErrors.Commands.NEXT_ERROR.id,
|
||||||
|
title: nls.localize('arduino/editor/nextError', 'Next Error'),
|
||||||
|
arguments: [this.currentError],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lenses,
|
||||||
|
dispose: () => {
|
||||||
|
/* NOOP */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCompilerErrorsDidChange(
|
||||||
|
errors: CoreError.Compiler[]
|
||||||
|
): Promise<void> {
|
||||||
|
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||||
|
const compilerErrorsPerResource = this.groupByResource(
|
||||||
|
await this.filter(errors)
|
||||||
|
);
|
||||||
|
const decorations = await this.decorateEditors(compilerErrorsPerResource);
|
||||||
|
this.errors.push(...decorations.errors);
|
||||||
|
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||||
|
Disposable.create(() => (this.errors.length = 0)),
|
||||||
|
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
|
||||||
|
...(await Promise.all([
|
||||||
|
decorations.dispose,
|
||||||
|
this.trackEditors(
|
||||||
|
compilerErrorsPerResource,
|
||||||
|
(editor) =>
|
||||||
|
editor.editor.onSelectionChanged((selection) =>
|
||||||
|
this.handleSelectionChange(editor, selection)
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.onDidDispose(() =>
|
||||||
|
this.handleEditorDidDispose(editor.editor.uri.toString())
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.editor.onDocumentContentChanged((event) =>
|
||||||
|
this.handleDocumentContentChange(editor, event)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
]);
|
||||||
|
const currentError = this.errors[0];
|
||||||
|
if (currentError) {
|
||||||
|
await this.markAsCurrentError(currentError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filter(
|
||||||
|
errors: CoreError.Compiler[]
|
||||||
|
): Promise<CoreError.Compiler[]> {
|
||||||
|
if (!errors.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await this.preferences.ready;
|
||||||
|
if (this.preferences['arduino.compile.experimental']) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
// Always shows maximum one error; hence the code lens navigation is unavailable.
|
||||||
|
return [errors[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decorateEditors(
|
||||||
|
errors: Map<string, CoreError.Compiler[]>
|
||||||
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
|
const composite = await Promise.all(
|
||||||
|
[...errors.entries()].map(([uri, errors]) =>
|
||||||
|
this.decorateEditor(uri, errors)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
dispose: new DisposableCollection(
|
||||||
|
...composite.map(({ dispose }) => dispose)
|
||||||
|
),
|
||||||
|
errors: composite.reduce(
|
||||||
|
(acc, { errors }) => acc.concat(errors),
|
||||||
|
[] as ErrorDecoration[]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decorateEditor(
|
||||||
|
uri: string,
|
||||||
|
errors: CoreError.Compiler[]
|
||||||
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||||
|
if (!editor) {
|
||||||
|
return { dispose: Disposable.NULL, errors: [] };
|
||||||
|
}
|
||||||
|
const oldDecorations = editor.editor.deltaDecorations({
|
||||||
|
oldDecorations: [],
|
||||||
|
newDecorations: errors.map((error) =>
|
||||||
|
this.compilerErrorDecoration(error.location.range)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
dispose: Disposable.create(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.editor.deltaDecorations({
|
||||||
|
oldDecorations,
|
||||||
|
newDecorations: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
errors: oldDecorations.map((id) => ({ id, uri })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private compilerErrorDecoration(range: Range): EditorDecoration {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'compiler-error',
|
||||||
|
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
||||||
|
*/
|
||||||
|
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (!monacoEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uri = monacoEditor.uri.toString();
|
||||||
|
const monacoSelection = this.p2m.asRange(selection);
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
|
||||||
|
);
|
||||||
|
const calculatePriority = (
|
||||||
|
candidateErrorRange: monaco.Range,
|
||||||
|
currentSelection: monaco.Range
|
||||||
|
) => {
|
||||||
|
console.trace(
|
||||||
|
'compiler-errors',
|
||||||
|
`Candidate error range: ${candidateErrorRange.toJSON()}`
|
||||||
|
);
|
||||||
|
console.trace(
|
||||||
|
'compiler-errors',
|
||||||
|
`Current selection range: ${currentSelection.toJSON()}`
|
||||||
|
);
|
||||||
|
if (candidateErrorRange.intersectRanges(currentSelection)) {
|
||||||
|
console.trace('Intersects.');
|
||||||
|
return { score: 2 };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
candidateErrorRange.startLineNumber <=
|
||||||
|
currentSelection.startLineNumber &&
|
||||||
|
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
|
||||||
|
) {
|
||||||
|
console.trace('Same line.');
|
||||||
|
return { score: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.trace('No match');
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const error = this.errors
|
||||||
|
.filter((error) => error.uri === uri)
|
||||||
|
.map((error) => ({
|
||||||
|
error,
|
||||||
|
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
||||||
|
}))
|
||||||
|
.map(({ error, range }) => {
|
||||||
|
if (range) {
|
||||||
|
const priority = calculatePriority(range, monacoSelection);
|
||||||
|
if (priority) {
|
||||||
|
return { ...priority, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty)
|
||||||
|
.sort((left, right) => right.score - left.score) // highest first
|
||||||
|
.map(({ error }) => error)
|
||||||
|
.shift();
|
||||||
|
if (error) {
|
||||||
|
this.markAsCurrentError(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'compiler-errors',
|
||||||
|
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
|
||||||
|
* If editor closes, delete the decorators.
|
||||||
|
*/
|
||||||
|
private handleEditorDidDispose(uri: string): void {
|
||||||
|
let i = this.errors.length;
|
||||||
|
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
|
||||||
|
while (i--) {
|
||||||
|
const error = this.errors[i];
|
||||||
|
if (error.uri === uri) {
|
||||||
|
this.errors.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a document change "destroys" the range of the decoration, the decoration must be removed.
|
||||||
|
*/
|
||||||
|
private handleDocumentContentChange(
|
||||||
|
editor: EditorWidget,
|
||||||
|
event: TextDocumentChangeEvent
|
||||||
|
): void {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (!monacoEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A decoration location can be "destroyed", hence should be deleted when:
|
||||||
|
// - deleting range (start != end AND text is empty)
|
||||||
|
// - inserting text into range (start != end AND text is not empty)
|
||||||
|
// Filter unrelated delta changes to spare the CPU.
|
||||||
|
const relevantChanges = event.contentChanges.filter(
|
||||||
|
({ range: { start, end } }) =>
|
||||||
|
start.line !== end.line || start.character !== end.character
|
||||||
|
);
|
||||||
|
if (!relevantChanges.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMarkers = this.errors
|
||||||
|
.filter((error) => error.uri === event.document.uri)
|
||||||
|
.map((error, index) => {
|
||||||
|
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
||||||
|
if (range) {
|
||||||
|
return { error, range, index };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty);
|
||||||
|
|
||||||
|
const decorationIdsToRemove = relevantChanges
|
||||||
|
.map(({ range }) => this.p2m.asRange(range))
|
||||||
|
.map((changeRange) =>
|
||||||
|
resolvedMarkers.filter(({ range: decorationRange }) =>
|
||||||
|
changeRange.containsRange(decorationRange)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
.map(({ error, index }) => {
|
||||||
|
this.errors.splice(index, 1);
|
||||||
|
return error.id;
|
||||||
|
});
|
||||||
|
if (!decorationIdsToRemove.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async trackEditors(
|
||||||
|
errors: Map<string, CoreError.Compiler[]>,
|
||||||
|
...track: ((editor: EditorWidget) => Disposable)[]
|
||||||
|
): Promise<Disposable> {
|
||||||
|
return new DisposableCollection(
|
||||||
|
...(await Promise.all(
|
||||||
|
Array.from(errors.keys()).map(async (uri) => {
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||||
|
if (!editor) {
|
||||||
|
return Disposable.NULL;
|
||||||
|
}
|
||||||
|
return new DisposableCollection(...track.map((t) => t(editor)));
|
||||||
|
})
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
|
||||||
|
const index = this.errors.findIndex((candidate) =>
|
||||||
|
ErrorDecoration.sameAs(candidate, error)
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${
|
||||||
|
error.id
|
||||||
|
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||||
|
({ id }) => id
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newError = this.errors[index];
|
||||||
|
if (
|
||||||
|
!this.currentError ||
|
||||||
|
!ErrorDecoration.sameAs(this.currentError, newError)
|
||||||
|
) {
|
||||||
|
this.currentError = this.errors[index];
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`Current error changed to ${this.currentError.id}`
|
||||||
|
);
|
||||||
|
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
|
||||||
|
private async revealLocationInEditor(
|
||||||
|
location: Location
|
||||||
|
): Promise<EditorWidget | undefined> {
|
||||||
|
const { uri, range } = location;
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri), {
|
||||||
|
mode: 'activate',
|
||||||
|
});
|
||||||
|
if (editor && this.shell) {
|
||||||
|
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
|
||||||
|
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
|
||||||
|
editor.editor.revealRange(range, { at: this.revealStrategy });
|
||||||
|
const activeWidget = await this.shell.activateWidget(editor.id);
|
||||||
|
if (!activeWidget) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
|
||||||
|
);
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
if (editor !== activeWidget) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`could not found editor widget for URI: ${uri}`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupByResource(
|
||||||
|
errors: CoreError.Compiler[]
|
||||||
|
): Map<string, CoreError.Compiler[]> {
|
||||||
|
return errors.reduce((acc, curr) => {
|
||||||
|
const {
|
||||||
|
location: { uri },
|
||||||
|
} = curr;
|
||||||
|
let errors = acc.get(uri);
|
||||||
|
if (!errors) {
|
||||||
|
errors = [];
|
||||||
|
acc.set(uri, errors);
|
||||||
|
}
|
||||||
|
errors.push(curr);
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, CoreError.Compiler[]>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||||
|
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
||||||
|
private monacoEditor(
|
||||||
|
uriOrWidget: string | EditorWidget
|
||||||
|
): MaybePromise<MonacoEditor | undefined> {
|
||||||
|
if (uriOrWidget instanceof EditorWidget) {
|
||||||
|
const editor = uriOrWidget.editor;
|
||||||
|
if (editor instanceof MonacoEditor) {
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return this.editorManager
|
||||||
|
.getByUri(new URI(uriOrWidget))
|
||||||
|
.then((editor) => {
|
||||||
|
if (editor) {
|
||||||
|
return this.monacoEditor(editor);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace CompilerErrors {
|
||||||
|
export namespace Commands {
|
||||||
|
export const NEXT_ERROR: Command = {
|
||||||
|
id: 'arduino-editor-next-error',
|
||||||
|
};
|
||||||
|
export const PREVIOUS_ERROR: Command = {
|
||||||
|
id: 'arduino-editor-previous-error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
|||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||||
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
|
||||||
import {
|
import {
|
||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
MenuContribution,
|
MenuContribution,
|
||||||
@ -48,9 +48,15 @@ import {
|
|||||||
ConfigService,
|
ConfigService,
|
||||||
FileSystemExt,
|
FileSystemExt,
|
||||||
Sketch,
|
Sketch,
|
||||||
|
CoreService,
|
||||||
|
CoreError,
|
||||||
} from '../../common/protocol';
|
} from '../../common/protocol';
|
||||||
import { ArduinoPreferences } from '../arduino-preferences';
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
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 {
|
export {
|
||||||
Command,
|
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 namespace Contribution {
|
||||||
export function configure(
|
export function configure(
|
||||||
bind: interfaces.Bind,
|
bind: interfaces.Bind,
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Emitter, Event } from '@theia/core';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CoreErrorHandler {
|
||||||
|
private readonly compilerErrors: CoreError.Compiler[] = [];
|
||||||
|
private readonly compilerErrorsDidChangeEmitter = new Emitter<
|
||||||
|
CoreError.Compiler[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
tryHandle(error: unknown): void {
|
||||||
|
if (CoreError.is(error)) {
|
||||||
|
this.compilerErrors.length = 0;
|
||||||
|
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
|
||||||
|
this.fireCompilerErrorsDidChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.compilerErrors.length = 0;
|
||||||
|
this.fireCompilerErrorsDidChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
|
||||||
|
return this.compilerErrorsDidChangeEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireCompilerErrorsDidChange(): void {
|
||||||
|
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ import { MaybePromise } from '@theia/core';
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import * as monaco from '@theia/monaco-editor-core';
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
import { Formatter } from '../../common/protocol/formatter';
|
import { Formatter } from '../../common/protocol/formatter';
|
||||||
|
import { InoSelector } from '../ino-selectors';
|
||||||
|
import { fullRange } from '../utils/monaco';
|
||||||
import { Contribution, URI } from './contribution';
|
import { Contribution, URI } from './contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -15,12 +17,11 @@ export class Format
|
|||||||
private readonly formatter: Formatter;
|
private readonly formatter: Formatter;
|
||||||
|
|
||||||
override onStart(): MaybePromise<void> {
|
override onStart(): MaybePromise<void> {
|
||||||
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
|
|
||||||
monaco.languages.registerDocumentRangeFormattingEditProvider(
|
monaco.languages.registerDocumentRangeFormattingEditProvider(
|
||||||
selector,
|
InoSelector,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
|
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
|
||||||
}
|
}
|
||||||
async provideDocumentRangeFormattingEdits(
|
async provideDocumentRangeFormattingEdits(
|
||||||
model: monaco.editor.ITextModel,
|
model: monaco.editor.ITextModel,
|
||||||
@ -39,18 +40,11 @@ export class Format
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_token: monaco.CancellationToken
|
_token: monaco.CancellationToken
|
||||||
): Promise<monaco.languages.TextEdit[]> {
|
): Promise<monaco.languages.TextEdit[]> {
|
||||||
const range = this.fullRange(model);
|
const range = fullRange(model);
|
||||||
const text = await this.format(model, range, options);
|
const text = await this.format(model, range, options);
|
||||||
return [{ range, text }];
|
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
|
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
|
||||||
* folder locations where the `.clang-format` file could be.
|
* folder locations where the `.clang-format` file could be.
|
||||||
@ -82,13 +76,4 @@ export class Format
|
|||||||
options,
|
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.
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
|||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
CoreServiceContribution,
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
@ -18,10 +18,7 @@ import { DisposableCollection, nls } from '@theia/core/lib/common';
|
|||||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class UploadSketch extends SketchContribution {
|
export class UploadSketch extends CoreServiceContribution {
|
||||||
@inject(CoreService)
|
|
||||||
protected readonly coreService: CoreService;
|
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
protected readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@ -201,16 +198,17 @@ export class UploadSketch extends SketchContribution {
|
|||||||
return;
|
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();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (!CurrentSketch.isValid(sketch)) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 { boardsConfig } = this.boardsServiceClientImpl;
|
||||||
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
|
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -227,9 +225,8 @@ export class UploadSketch extends SketchContribution {
|
|||||||
...boardsConfig.selectedBoard,
|
...boardsConfig.selectedBoard,
|
||||||
name: boardsConfig.selectedBoard?.name || '',
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
fqbn,
|
fqbn,
|
||||||
}
|
};
|
||||||
let options: CoreService.Upload.Options | undefined = undefined;
|
let options: CoreService.Upload.Options | undefined = undefined;
|
||||||
const sketchUri = sketch.uri;
|
|
||||||
const optimizeForDebug = this.editorMode.compileForDebug;
|
const optimizeForDebug = this.editorMode.compileForDebug;
|
||||||
const { selectedPort } = boardsConfig;
|
const { selectedPort } = boardsConfig;
|
||||||
const port = selectedPort;
|
const port = selectedPort;
|
||||||
@ -248,7 +245,7 @@ export class UploadSketch extends SketchContribution {
|
|||||||
if (usingProgrammer) {
|
if (usingProgrammer) {
|
||||||
const programmer = selectedProgrammer;
|
const programmer = selectedProgrammer;
|
||||||
options = {
|
options = {
|
||||||
sketchUri,
|
sketch,
|
||||||
board,
|
board,
|
||||||
optimizeForDebug,
|
optimizeForDebug,
|
||||||
programmer,
|
programmer,
|
||||||
@ -260,7 +257,7 @@ export class UploadSketch extends SketchContribution {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
options = {
|
options = {
|
||||||
sketchUri,
|
sketch,
|
||||||
board,
|
board,
|
||||||
optimizeForDebug,
|
optimizeForDebug,
|
||||||
port,
|
port,
|
||||||
@ -281,13 +278,7 @@ export class UploadSketch extends SketchContribution {
|
|||||||
{ timeout: 3000 }
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorMessage = '';
|
this.handleError(e);
|
||||||
if (typeof e === 'string') {
|
|
||||||
errorMessage = e;
|
|
||||||
} else {
|
|
||||||
errorMessage = e.toString();
|
|
||||||
}
|
|
||||||
this.messageService.error(errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.uploadInProgress = false;
|
this.uploadInProgress = false;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { CoreService } from '../../common/protocol';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
CoreServiceContribution,
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
@ -17,10 +16,7 @@ import { nls } from '@theia/core/lib/common';
|
|||||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class VerifySketch extends SketchContribution {
|
export class VerifySketch extends CoreServiceContribution {
|
||||||
@inject(CoreService)
|
|
||||||
protected readonly coreService: CoreService;
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
@inject(BoardsDataStore)
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
protected readonly boardsDataStore: BoardsDataStore;
|
||||||
|
|
||||||
@ -96,14 +92,14 @@ export class VerifySketch extends SketchContribution {
|
|||||||
|
|
||||||
// toggle the toolbar button and menu item state.
|
// toggle the toolbar button and menu item state.
|
||||||
// verifyInProgress will be set to false whether the compilation fails or not
|
// verifyInProgress will be set to false whether the compilation fails or not
|
||||||
this.verifyInProgress = true;
|
|
||||||
this.onDidChangeEmitter.fire();
|
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
|
||||||
if (!CurrentSketch.isValid(sketch)) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
this.verifyInProgress = true;
|
||||||
|
this.coreErrorHandler.reset();
|
||||||
|
this.onDidChangeEmitter.fire();
|
||||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||||
const [fqbn, sourceOverride] = await Promise.all([
|
const [fqbn, sourceOverride] = await Promise.all([
|
||||||
this.boardsDataStore.appendConfigToFqbn(
|
this.boardsDataStore.appendConfigToFqbn(
|
||||||
@ -115,12 +111,12 @@ export class VerifySketch extends SketchContribution {
|
|||||||
...boardsConfig.selectedBoard,
|
...boardsConfig.selectedBoard,
|
||||||
name: boardsConfig.selectedBoard?.name || '',
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
fqbn,
|
fqbn,
|
||||||
}
|
};
|
||||||
const verbose = this.preferences.get('arduino.compile.verbose');
|
const verbose = this.preferences.get('arduino.compile.verbose');
|
||||||
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
this.outputChannelManager.getChannel('Arduino').clear();
|
||||||
await this.coreService.compile({
|
await this.coreService.compile({
|
||||||
sketchUri: sketch.uri,
|
sketch,
|
||||||
board,
|
board,
|
||||||
optimizeForDebug: this.editorMode.compileForDebug,
|
optimizeForDebug: this.editorMode.compileForDebug,
|
||||||
verbose,
|
verbose,
|
||||||
@ -133,13 +129,7 @@ export class VerifySketch extends SketchContribution {
|
|||||||
{ timeout: 3000 }
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorMessage = "";
|
this.handleError(e);
|
||||||
if (typeof e === "string") {
|
|
||||||
errorMessage = e;
|
|
||||||
} else {
|
|
||||||
errorMessage = e.toString();
|
|
||||||
}
|
|
||||||
this.messageService.error(errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.verifyInProgress = false;
|
this.verifyInProgress = false;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
|
13
arduino-ide-extension/src/browser/ino-selectors.ts
Normal file
13
arduino-ide-extension/src/browser/ino-selectors.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
/**
|
||||||
|
* Exclusive "ino" document selector for monaco.
|
||||||
|
*/
|
||||||
|
export const InoSelector = selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
|
||||||
|
function selectorOf(
|
||||||
|
...languageId: string[]
|
||||||
|
): monaco.languages.LanguageSelector {
|
||||||
|
return languageId.map((language) => ({
|
||||||
|
language,
|
||||||
|
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
|
||||||
|
}));
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
import {
|
||||||
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
OutputChannelManager,
|
||||||
|
OutputChannelSeverity,
|
||||||
|
} from '@theia/output/lib/browser/output-channel';
|
||||||
import {
|
import {
|
||||||
OutputMessage,
|
OutputMessage,
|
||||||
ProgressMessage,
|
ProgressMessage,
|
||||||
@ -10,13 +12,10 @@ import {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ResponseServiceImpl implements ResponseServiceArduino {
|
export class ResponseServiceImpl implements ResponseServiceArduino {
|
||||||
@inject(OutputContribution)
|
|
||||||
protected outputContribution: OutputContribution;
|
|
||||||
|
|
||||||
@inject(OutputChannelManager)
|
@inject(OutputChannelManager)
|
||||||
protected outputChannelManager: OutputChannelManager;
|
private readonly outputChannelManager: OutputChannelManager;
|
||||||
|
|
||||||
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
|
private readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
|
||||||
|
|
||||||
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
|
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
|
||||||
|
|
||||||
@ -25,13 +24,22 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appendToOutput(message: OutputMessage): void {
|
appendToOutput(message: OutputMessage): void {
|
||||||
const { chunk } = message;
|
const { chunk, severity } = message;
|
||||||
const channel = this.outputChannelManager.getChannel('Arduino');
|
const channel = this.outputChannelManager.getChannel('Arduino');
|
||||||
channel.show({ preserveFocus: true });
|
channel.show({ preserveFocus: true });
|
||||||
channel.append(chunk);
|
channel.append(chunk, mapSeverity(severity));
|
||||||
}
|
}
|
||||||
|
|
||||||
reportProgress(progress: ProgressMessage): void {
|
reportProgress(progress: ProgressMessage): void {
|
||||||
this.progressDidChangeEmitter.fire(progress);
|
this.progressDidChangeEmitter.fire(progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapSeverity(severity?: OutputMessage.Severity): OutputChannelSeverity {
|
||||||
|
if (severity === OutputMessage.Severity.Error) {
|
||||||
|
return OutputChannelSeverity.Error;
|
||||||
|
} else if (severity === OutputMessage.Severity.Warning) {
|
||||||
|
return OutputChannelSeverity.Warning;
|
||||||
|
}
|
||||||
|
return OutputChannelSeverity.Info;
|
||||||
|
}
|
||||||
|
@ -8,3 +8,8 @@
|
|||||||
.monaco-list-row.show-file-icons.focused {
|
.monaco-list-row.show-file-icons.focused {
|
||||||
background-color: #d6ebff;
|
background-color: #d6ebff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monaco-editor .view-overlays .compiler-error {
|
||||||
|
background-color: var(--theia-inputValidation-errorBackground);
|
||||||
|
opacity: 0.4 !important;
|
||||||
|
}
|
||||||
|
@ -40,6 +40,14 @@ export class OutputChannelManager extends TheiaOutputChannelManager {
|
|||||||
}
|
}
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async contentOfChannel(name: string): Promise<string | undefined> {
|
||||||
|
const resource = this.resources.get(name);
|
||||||
|
if (resource) {
|
||||||
|
return resource.readContents();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OutputChannel extends TheiaOutputChannel {
|
export class OutputChannel extends TheiaOutputChannel {
|
||||||
|
8
arduino-ide-extension/src/browser/utils/monaco.ts
Normal file
8
arduino-ide-extension/src/browser/utils/monaco.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
|
||||||
|
export function fullRange(model: monaco.editor.ITextModel): monaco.Range {
|
||||||
|
const lastLine = model.getLineCount();
|
||||||
|
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
|
||||||
|
const end = new monaco.Position(lastLine, lastLineMaxColumn);
|
||||||
|
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
|
import { ApplicationError } from '@theia/core';
|
||||||
|
import { Location } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
import { BoardUserField } from '.';
|
import { BoardUserField } from '.';
|
||||||
import { Board, Port } from '../../common/protocol/boards-service';
|
import { Board, Port } from '../../common/protocol/boards-service';
|
||||||
|
import { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
|
||||||
import { Programmer } from './boards-service';
|
import { Programmer } from './boards-service';
|
||||||
|
import { Sketch } from './sketches-service';
|
||||||
|
|
||||||
export const CompilerWarningLiterals = [
|
export const CompilerWarningLiterals = [
|
||||||
'None',
|
'None',
|
||||||
@ -9,6 +13,53 @@ export const CompilerWarningLiterals = [
|
|||||||
'All',
|
'All',
|
||||||
] as const;
|
] as const;
|
||||||
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
||||||
|
export namespace CoreError {
|
||||||
|
export type ErrorInfo = CliErrorInfo;
|
||||||
|
export interface Compiler extends ErrorInfo {
|
||||||
|
readonly message: string;
|
||||||
|
readonly location: Location;
|
||||||
|
}
|
||||||
|
export namespace Compiler {
|
||||||
|
export function is(error: ErrorInfo): error is Compiler {
|
||||||
|
const { message, location } = error;
|
||||||
|
return !!message && !!location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const Codes = {
|
||||||
|
Verify: 4001,
|
||||||
|
Upload: 4002,
|
||||||
|
UploadUsingProgrammer: 4003,
|
||||||
|
BurnBootloader: 4004,
|
||||||
|
};
|
||||||
|
export const VerifyFailed = create(Codes.Verify);
|
||||||
|
export const UploadFailed = create(Codes.Upload);
|
||||||
|
export const UploadUsingProgrammerFailed = create(
|
||||||
|
Codes.UploadUsingProgrammer
|
||||||
|
);
|
||||||
|
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
|
||||||
|
export function is(
|
||||||
|
error: unknown
|
||||||
|
): error is ApplicationError<number, ErrorInfo[]> {
|
||||||
|
return (
|
||||||
|
error instanceof Error &&
|
||||||
|
ApplicationError.is(error) &&
|
||||||
|
Object.values(Codes).includes(error.code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function create(
|
||||||
|
code: number
|
||||||
|
): ApplicationError.Constructor<number, ErrorInfo[]> {
|
||||||
|
return ApplicationError.declare(
|
||||||
|
code,
|
||||||
|
(message: string, data: ErrorInfo[]) => {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const CoreServicePath = '/services/core-service';
|
export const CoreServicePath = '/services/core-service';
|
||||||
export const CoreService = Symbol('CoreService');
|
export const CoreService = Symbol('CoreService');
|
||||||
@ -23,16 +74,12 @@ export interface CoreService {
|
|||||||
upload(options: CoreService.Upload.Options): Promise<void>;
|
upload(options: CoreService.Upload.Options): Promise<void>;
|
||||||
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
|
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
|
||||||
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
|
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
|
||||||
isUploading(): Promise<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace CoreService {
|
export namespace CoreService {
|
||||||
export namespace Compile {
|
export namespace Compile {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
/**
|
readonly sketch: Sketch;
|
||||||
* `file` URI to the sketch folder.
|
|
||||||
*/
|
|
||||||
readonly sketchUri: string;
|
|
||||||
readonly board?: Board;
|
readonly board?: Board;
|
||||||
readonly optimizeForDebug: boolean;
|
readonly optimizeForDebug: boolean;
|
||||||
readonly verbose: boolean;
|
readonly verbose: boolean;
|
||||||
|
@ -2,7 +2,14 @@ import { Event } from '@theia/core/lib/common/event';
|
|||||||
|
|
||||||
export interface OutputMessage {
|
export interface OutputMessage {
|
||||||
readonly chunk: string;
|
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 {
|
export interface ProgressMessage {
|
||||||
|
@ -127,11 +127,8 @@ export namespace Sketch {
|
|||||||
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
|
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
|
||||||
}
|
}
|
||||||
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
|
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
|
||||||
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
|
return uris(sketch).includes(
|
||||||
return (
|
typeof uri === 'string' ? uri : uri.toString()
|
||||||
[mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(
|
|
||||||
uri.toString()
|
|
||||||
) !== -1
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function isSketchFile(arg: string | URI): boolean {
|
export function isSketchFile(arg: string | URI): boolean {
|
||||||
@ -140,6 +137,10 @@ export namespace Sketch {
|
|||||||
}
|
}
|
||||||
return Extensions.MAIN.some((ext) => arg.endsWith(ext));
|
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 {
|
export interface SketchContainer {
|
||||||
|
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal file
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { notEmpty } from '@theia/core';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
|
import {
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
Position,
|
||||||
|
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
|
import { Sketch } from '../common/protocol';
|
||||||
|
|
||||||
|
export interface ErrorInfo {
|
||||||
|
readonly message?: string;
|
||||||
|
readonly location?: Location;
|
||||||
|
readonly details?: string;
|
||||||
|
}
|
||||||
|
export interface ErrorSource {
|
||||||
|
readonly content: string | ReadonlyArray<Uint8Array>;
|
||||||
|
readonly sketch?: Sketch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryParseError(source: ErrorSource): ErrorInfo[] {
|
||||||
|
const { content, sketch } = source;
|
||||||
|
const err =
|
||||||
|
typeof content === 'string'
|
||||||
|
? content
|
||||||
|
: Buffer.concat(content).toString('utf8');
|
||||||
|
if (sketch) {
|
||||||
|
return tryParse(err)
|
||||||
|
.map(remapErrorMessages)
|
||||||
|
.filter(isLocationInSketch(sketch))
|
||||||
|
.map(errorInfo());
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParseResult {
|
||||||
|
readonly path: string;
|
||||||
|
readonly line: number;
|
||||||
|
readonly column?: number;
|
||||||
|
readonly errorPrefix: string;
|
||||||
|
readonly error: string;
|
||||||
|
readonly message?: string;
|
||||||
|
}
|
||||||
|
namespace ParseResult {
|
||||||
|
export function keyOf(result: ParseResult): string {
|
||||||
|
/**
|
||||||
|
* The CLI compiler might return with the same error multiple times. This is the key function for the distinct set calculation.
|
||||||
|
*/
|
||||||
|
return JSON.stringify(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocationInSketch(
|
||||||
|
sketch: Sketch
|
||||||
|
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
|
||||||
|
return (result) => {
|
||||||
|
const uri = FileUri.create(result.path).toString();
|
||||||
|
if (!Sketch.isInSketch(uri, sketch)) {
|
||||||
|
console.warn(
|
||||||
|
`URI <${uri}> is not contained in sketch: <${JSON.stringify(sketch)}>`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorInfo(): (value: ParseResult) => ErrorInfo {
|
||||||
|
return ({ error, message, path, line, column }) => ({
|
||||||
|
message: error,
|
||||||
|
details: message,
|
||||||
|
location: {
|
||||||
|
uri: FileUri.create(path).toString(),
|
||||||
|
range: range(line, column),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function range(line: number, column?: number): Range {
|
||||||
|
const start = Position.create(
|
||||||
|
line - 1,
|
||||||
|
typeof column === 'number' ? column - 1 : 0
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end: start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryParse(raw: string): ParseResult[] {
|
||||||
|
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
|
||||||
|
const re = new RegExp(
|
||||||
|
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
|
||||||
|
'gm'
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
...new Map(
|
||||||
|
Array.from(raw.matchAll(re) ?? [])
|
||||||
|
.map((match) => {
|
||||||
|
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
|
||||||
|
(match) => (match ? match.trim() : match)
|
||||||
|
);
|
||||||
|
const line = Number.parseInt(rawLine, 10);
|
||||||
|
if (!Number.isInteger(line)) {
|
||||||
|
console.warn(
|
||||||
|
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let column: number | undefined = undefined;
|
||||||
|
if (rawColumn) {
|
||||||
|
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
|
||||||
|
column = Number.parseInt(normalizedRawColumn, 10);
|
||||||
|
if (!Number.isInteger(column)) {
|
||||||
|
console.warn(
|
||||||
|
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
errorPrefix,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(notEmpty)
|
||||||
|
.map((result) => [ParseResult.keyOf(result), result])
|
||||||
|
).values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts cryptic and legacy error messages to nice ones. Taken from the Java IDE.
|
||||||
|
*/
|
||||||
|
function remapErrorMessages(result: ParseResult): ParseResult {
|
||||||
|
const knownError = KnownErrors[result.error];
|
||||||
|
if (!knownError) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const { message, error } = knownError;
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
...(message && { message }),
|
||||||
|
...(error && { error }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L528-L578
|
||||||
|
const KnownErrors: Record<string, { error: string; message?: string }> = {
|
||||||
|
'SPI.h: No such file or directory': {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/spiError',
|
||||||
|
'Please import the SPI library from the Sketch > Import Library menu.'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/spiMessage',
|
||||||
|
'As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'BYTE' was not declared in this scope": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/byteError',
|
||||||
|
"The 'BYTE' keyword is no longer supported."
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/byteMessage',
|
||||||
|
"As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"no matching function for call to 'Server::Server(int)'": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/serverError',
|
||||||
|
'The Server class has been renamed EthernetServer.'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/serverMessage',
|
||||||
|
'As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"no matching function for call to 'Client::Client(byte [4], int)'": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/clientError',
|
||||||
|
'The Client class has been renamed EthernetClient.'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/clientMessage',
|
||||||
|
'As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'Udp' was not declared in this scope": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/udpError',
|
||||||
|
'The Udp class has been renamed EthernetUdp.'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/udpMessage',
|
||||||
|
'As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'class TwoWire' has no member named 'send'": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/sendError',
|
||||||
|
'Wire.send() has been renamed Wire.write().'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/sendMessage',
|
||||||
|
'As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'class TwoWire' has no member named 'receive'": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/receiveError',
|
||||||
|
'Wire.receive() has been renamed Wire.read().'
|
||||||
|
),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cli-error-parser/receiveMessage',
|
||||||
|
'As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'Mouse' was not declared in this scope": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/mouseError',
|
||||||
|
"'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"'Keyboard' was not declared in this scope": {
|
||||||
|
error: nls.localize(
|
||||||
|
'arduino/cli-error-parser/keyboardError',
|
||||||
|
"'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
@ -4,7 +4,11 @@ import { relative } from 'path';
|
|||||||
import * as jspb from 'google-protobuf';
|
import * as jspb from 'google-protobuf';
|
||||||
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
||||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
import { ClientReadableStream } from '@grpc/grpc-js';
|
||||||
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
|
import {
|
||||||
|
CompilerWarnings,
|
||||||
|
CoreService,
|
||||||
|
CoreError,
|
||||||
|
} from '../common/protocol/core-service';
|
||||||
import {
|
import {
|
||||||
CompileRequest,
|
CompileRequest,
|
||||||
CompileResponse,
|
CompileResponse,
|
||||||
@ -19,27 +23,24 @@ import {
|
|||||||
UploadUsingProgrammerResponse,
|
UploadUsingProgrammerResponse,
|
||||||
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||||
import { ResponseService } from '../common/protocol/response-service';
|
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 { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||||
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
||||||
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
import { ApplicationError, Disposable, nls } from '@theia/core';
|
||||||
import { nls } from '@theia/core';
|
|
||||||
import { MonitorManager } from './monitor-manager';
|
import { MonitorManager } from './monitor-manager';
|
||||||
import { SimpleBuffer } from './utils/simple-buffer';
|
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';
|
||||||
|
|
||||||
const FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS = 32;
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||||
@inject(ResponseService)
|
@inject(ResponseService)
|
||||||
protected readonly responseService: ResponseService;
|
private readonly responseService: ResponseService;
|
||||||
|
|
||||||
@inject(NotificationServiceServer)
|
|
||||||
protected readonly notificationService: NotificationServiceServer;
|
|
||||||
|
|
||||||
@inject(MonitorManager)
|
@inject(MonitorManager)
|
||||||
protected readonly monitorManager: MonitorManager;
|
private readonly monitorManager: MonitorManager;
|
||||||
|
|
||||||
protected uploading = false;
|
|
||||||
|
|
||||||
async compile(
|
async compile(
|
||||||
options: CoreService.Compile.Options & {
|
options: CoreService.Compile.Options & {
|
||||||
@ -47,254 +48,298 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
compilerWarnings?: CompilerWarnings;
|
compilerWarnings?: CompilerWarnings;
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { sketchUri, board, compilerWarnings } = options;
|
|
||||||
const sketchPath = FileUri.fsPath(sketchUri);
|
|
||||||
|
|
||||||
await this.coreClientProvider.initialized;
|
|
||||||
const coreClient = await this.coreClient();
|
const coreClient = await this.coreClient();
|
||||||
const { client, instance } = coreClient;
|
const { client, instance } = coreClient;
|
||||||
|
const handler = this.createOnDataHandler();
|
||||||
|
const request = this.compileRequest(options, instance);
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
client
|
||||||
|
.compile(request)
|
||||||
|
.on('data', handler.onData)
|
||||||
|
.on('error', (error) => {
|
||||||
|
if (!ServiceError.is(error)) {
|
||||||
|
console.error(
|
||||||
|
'Unexpected error occurred while compiling the sketch.',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
const compilerErrors = tryParseError({
|
||||||
|
content: handler.stderr,
|
||||||
|
sketch: options.sketch,
|
||||||
|
});
|
||||||
|
const message = nls.localize(
|
||||||
|
'arduino/compile/error',
|
||||||
|
'Compilation error: {0}',
|
||||||
|
compilerErrors
|
||||||
|
.map(({ message }) => message)
|
||||||
|
.filter(notEmpty)
|
||||||
|
.shift() ?? error.details
|
||||||
|
);
|
||||||
|
this.sendResponse(
|
||||||
|
error.details + '\n\n' + message,
|
||||||
|
OutputMessage.Severity.Error
|
||||||
|
);
|
||||||
|
reject(CoreError.VerifyFailed(message, compilerErrors));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', resolve);
|
||||||
|
}).finally(() => handler.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
const compileReq = new CompileRequest();
|
private compileRequest(
|
||||||
compileReq.setInstance(instance);
|
options: CoreService.Compile.Options & {
|
||||||
compileReq.setSketchPath(sketchPath);
|
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) {
|
if (board?.fqbn) {
|
||||||
compileReq.setFqbn(board.fqbn);
|
request.setFqbn(board.fqbn);
|
||||||
}
|
}
|
||||||
if (compilerWarnings) {
|
if (compilerWarnings) {
|
||||||
compileReq.setWarnings(compilerWarnings.toLowerCase());
|
request.setWarnings(compilerWarnings.toLowerCase());
|
||||||
}
|
}
|
||||||
compileReq.setOptimizeForDebug(options.optimizeForDebug);
|
request.setOptimizeForDebug(options.optimizeForDebug);
|
||||||
compileReq.setPreprocess(false);
|
request.setPreprocess(false);
|
||||||
compileReq.setVerbose(options.verbose);
|
request.setVerbose(options.verbose);
|
||||||
compileReq.setQuiet(false);
|
request.setQuiet(false);
|
||||||
if (typeof options.exportBinaries === 'boolean') {
|
if (typeof options.exportBinaries === 'boolean') {
|
||||||
const exportBinaries = new BoolValue();
|
const exportBinaries = new BoolValue();
|
||||||
exportBinaries.setValue(options.exportBinaries);
|
exportBinaries.setValue(options.exportBinaries);
|
||||||
compileReq.setExportBinaries(exportBinaries);
|
request.setExportBinaries(exportBinaries);
|
||||||
}
|
|
||||||
this.mergeSourceOverrides(compileReq, options);
|
|
||||||
|
|
||||||
const result = client.compile(compileReq);
|
|
||||||
|
|
||||||
const compileBuffer = new SimpleBuffer(
|
|
||||||
this.flushOutputPanelMessages.bind(this),
|
|
||||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
result.on('data', (cr: CompileResponse) => {
|
|
||||||
compileBuffer.addChunk(cr.getOutStream_asU8());
|
|
||||||
compileBuffer.addChunk(cr.getErrStream_asU8());
|
|
||||||
});
|
|
||||||
result.on('error', (error) => {
|
|
||||||
compileBuffer.clearFlushInterval();
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
result.on('end', () => {
|
|
||||||
compileBuffer.clearFlushInterval();
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
this.mergeSourceOverrides(request, options);
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(options: CoreService.Upload.Options): Promise<void> {
|
async upload(options: CoreService.Upload.Options): Promise<void> {
|
||||||
await this.doUpload(
|
return this.doUpload(
|
||||||
options,
|
options,
|
||||||
() => new UploadRequest(),
|
() => new UploadRequest(),
|
||||||
(client, req) => client.upload(req)
|
(client, req) => client.upload(req),
|
||||||
|
(message: string, info: CoreError.ErrorInfo[]) =>
|
||||||
|
CoreError.UploadFailed(message, info),
|
||||||
|
'upload'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadUsingProgrammer(
|
async uploadUsingProgrammer(
|
||||||
options: CoreService.Upload.Options
|
options: CoreService.Upload.Options
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.doUpload(
|
return this.doUpload(
|
||||||
options,
|
options,
|
||||||
() => new UploadUsingProgrammerRequest(),
|
() => new UploadUsingProgrammerRequest(),
|
||||||
(client, req) => client.uploadUsingProgrammer(req),
|
(client, req) => client.uploadUsingProgrammer(req),
|
||||||
|
(message: string, info: CoreError.ErrorInfo[]) =>
|
||||||
|
CoreError.UploadUsingProgrammerFailed(message, info),
|
||||||
'upload using programmer'
|
'upload using programmer'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploading(): Promise<boolean> {
|
|
||||||
return Promise.resolve(this.uploading);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async doUpload(
|
protected async doUpload(
|
||||||
options: CoreService.Upload.Options,
|
options: CoreService.Upload.Options,
|
||||||
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
|
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||||
// tslint:disable-next-line:max-line-length
|
|
||||||
responseHandler: (
|
responseHandler: (
|
||||||
client: ArduinoCoreServiceClient,
|
client: ArduinoCoreServiceClient,
|
||||||
req: UploadRequest | UploadUsingProgrammerRequest
|
request: UploadRequest | UploadUsingProgrammerRequest
|
||||||
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
||||||
task = 'upload'
|
errorHandler: (
|
||||||
|
message: string,
|
||||||
|
info: CoreError.ErrorInfo[]
|
||||||
|
) => ApplicationError<number, CoreError.ErrorInfo[]>,
|
||||||
|
task: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.compile(Object.assign(options, { exportBinaries: false }));
|
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 coreClient = await this.coreClient();
|
||||||
const { client, instance } = coreClient;
|
const { client, instance } = coreClient;
|
||||||
|
const request = this.uploadOrUploadUsingProgrammerRequest(
|
||||||
|
options,
|
||||||
|
instance,
|
||||||
|
requestFactory
|
||||||
|
);
|
||||||
|
const handler = this.createOnDataHandler();
|
||||||
|
return this.notifyUploadWillStart(options).then(() =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
responseHandler(client, request)
|
||||||
|
.on('data', handler.onData)
|
||||||
|
.on('error', (error) => {
|
||||||
|
if (!ServiceError.is(error)) {
|
||||||
|
console.error(`Unexpected error occurred while ${task}.`, error);
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
const message = nls.localize(
|
||||||
|
'arduino/upload/error',
|
||||||
|
'{0} error: {1}',
|
||||||
|
firstToUpperCase(task),
|
||||||
|
error.details
|
||||||
|
);
|
||||||
|
this.sendResponse(error.details, OutputMessage.Severity.Error);
|
||||||
|
reject(
|
||||||
|
errorHandler(
|
||||||
|
message,
|
||||||
|
tryParseError({
|
||||||
|
content: handler.stderr,
|
||||||
|
sketch: options.sketch,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', resolve);
|
||||||
|
}).finally(async () => {
|
||||||
|
handler.dispose();
|
||||||
|
await this.notifyUploadDidFinish(options);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const req = requestProvider();
|
private uploadOrUploadUsingProgrammerRequest(
|
||||||
req.setInstance(instance);
|
options: CoreService.Upload.Options,
|
||||||
req.setSketchPath(sketchPath);
|
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) {
|
if (board?.fqbn) {
|
||||||
req.setFqbn(board.fqbn);
|
request.setFqbn(board.fqbn);
|
||||||
}
|
}
|
||||||
const p = new Port();
|
request.setPort(this.createPort(port));
|
||||||
if (port) {
|
|
||||||
p.setAddress(port.address);
|
|
||||||
p.setLabel(port.addressLabel);
|
|
||||||
p.setProtocol(port.protocol);
|
|
||||||
p.setProtocolLabel(port.protocolLabel);
|
|
||||||
}
|
|
||||||
req.setPort(p);
|
|
||||||
if (programmer) {
|
if (programmer) {
|
||||||
req.setProgrammer(programmer.id);
|
request.setProgrammer(programmer.id);
|
||||||
}
|
}
|
||||||
req.setVerbose(options.verbose);
|
request.setVerbose(options.verbose);
|
||||||
req.setVerify(options.verify);
|
request.setVerify(options.verify);
|
||||||
|
|
||||||
options.userFields.forEach((e) => {
|
options.userFields.forEach((e) => {
|
||||||
req.getUserFieldsMap().set(e.name, e.value);
|
request.getUserFieldsMap().set(e.name, e.value);
|
||||||
});
|
});
|
||||||
|
return request;
|
||||||
const result = responseHandler(client, req);
|
|
||||||
|
|
||||||
const uploadBuffer = new SimpleBuffer(
|
|
||||||
this.flushOutputPanelMessages.bind(this),
|
|
||||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
result.on('data', (resp: UploadResponse) => {
|
|
||||||
uploadBuffer.addChunk(resp.getOutStream_asU8());
|
|
||||||
uploadBuffer.addChunk(resp.getErrStream_asU8());
|
|
||||||
});
|
|
||||||
result.on('error', (error) => {
|
|
||||||
uploadBuffer.clearFlushInterval();
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
result.on('end', () => {
|
|
||||||
uploadBuffer.clearFlushInterval();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
||||||
this.uploading = true;
|
|
||||||
const { board, port, programmer } = options;
|
|
||||||
await this.monitorManager.notifyUploadStarted(board, port);
|
|
||||||
|
|
||||||
await this.coreClientProvider.initialized;
|
|
||||||
const coreClient = await this.coreClient();
|
const coreClient = await this.coreClient();
|
||||||
const { client, instance } = coreClient;
|
const { client, instance } = coreClient;
|
||||||
const burnReq = new BurnBootloaderRequest();
|
const handler = this.createOnDataHandler();
|
||||||
burnReq.setInstance(instance);
|
const request = this.burnBootloaderRequest(options, instance);
|
||||||
if (board?.fqbn) {
|
return this.notifyUploadWillStart(options).then(() =>
|
||||||
burnReq.setFqbn(board.fqbn);
|
new Promise<void>((resolve, reject) => {
|
||||||
}
|
client
|
||||||
const p = new Port();
|
.burnBootloader(request)
|
||||||
if (port) {
|
.on('data', handler.onData)
|
||||||
p.setAddress(port.address);
|
.on('error', (error) => {
|
||||||
p.setLabel(port.addressLabel);
|
if (!ServiceError.is(error)) {
|
||||||
p.setProtocol(port.protocol);
|
console.error(
|
||||||
p.setProtocolLabel(port.protocolLabel);
|
'Unexpected error occurred while burning the bootloader.',
|
||||||
}
|
error
|
||||||
burnReq.setPort(p);
|
);
|
||||||
if (programmer) {
|
reject(error);
|
||||||
burnReq.setProgrammer(programmer.id);
|
} else {
|
||||||
}
|
this.sendResponse(error.details, OutputMessage.Severity.Error);
|
||||||
burnReq.setVerify(options.verify);
|
reject(
|
||||||
burnReq.setVerbose(options.verbose);
|
CoreError.BurnBootloaderFailed(
|
||||||
const result = client.burnBootloader(burnReq);
|
nls.localize(
|
||||||
|
'arduino/burnBootloader/error',
|
||||||
const bootloaderBuffer = new SimpleBuffer(
|
'Error while burning the bootloader: {0}',
|
||||||
this.flushOutputPanelMessages.bind(this),
|
error.details
|
||||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
),
|
||||||
|
tryParseError({ content: handler.stderr })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', resolve);
|
||||||
|
}).finally(async () => {
|
||||||
|
handler.dispose();
|
||||||
|
await this.notifyUploadDidFinish(options);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
try {
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
result.on('data', (resp: BurnBootloaderResponse) => {
|
private burnBootloaderRequest(
|
||||||
bootloaderBuffer.addChunk(resp.getOutStream_asU8());
|
options: CoreService.Bootloader.Options,
|
||||||
bootloaderBuffer.addChunk(resp.getErrStream_asU8());
|
instance: Instance
|
||||||
});
|
): BurnBootloaderRequest {
|
||||||
result.on('error', (error) => {
|
const { board, port, programmer } = options;
|
||||||
bootloaderBuffer.clearFlushInterval();
|
const request = new BurnBootloaderRequest();
|
||||||
reject(error);
|
request.setInstance(instance);
|
||||||
});
|
if (board?.fqbn) {
|
||||||
result.on('end', () => {
|
request.setFqbn(board.fqbn);
|
||||||
bootloaderBuffer.clearFlushInterval();
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
this.uploading = false;
|
|
||||||
await this.monitorManager.notifyUploadFinished(board, port);
|
|
||||||
}
|
}
|
||||||
|
request.setPort(this.createPort(port));
|
||||||
|
if (programmer) {
|
||||||
|
request.setProgrammer(programmer.id);
|
||||||
|
}
|
||||||
|
request.setVerify(options.verify);
|
||||||
|
request.setVerbose(options.verbose);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createOnDataHandler<R extends StreamingResponse>(): Disposable & {
|
||||||
|
stderr: Buffer[];
|
||||||
|
onData: (response: R) => void;
|
||||||
|
} {
|
||||||
|
const stderr: Buffer[] = [];
|
||||||
|
const buffer = new SimpleBuffer((chunks) => {
|
||||||
|
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
|
||||||
|
if (chunk) {
|
||||||
|
this.sendResponse(chunk, severity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => {
|
||||||
|
buffer.addChunk(out);
|
||||||
|
buffer.addChunk(err, OutputMessage.Severity.Error);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
dispose: () => buffer.dispose(),
|
||||||
|
stderr,
|
||||||
|
onData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendResponse(
|
||||||
|
chunk: string,
|
||||||
|
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
||||||
|
): void {
|
||||||
|
this.responseService.appendToOutput({ chunk, severity });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyUploadWillStart({
|
||||||
|
board,
|
||||||
|
port,
|
||||||
|
}: {
|
||||||
|
board?: Board | undefined;
|
||||||
|
port?: Port | undefined;
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.monitorManager.notifyUploadStarted(board, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyUploadDidFinish({
|
||||||
|
board,
|
||||||
|
port,
|
||||||
|
}: {
|
||||||
|
board?: Board | undefined;
|
||||||
|
port?: Port | undefined;
|
||||||
|
}): Promise<Status> {
|
||||||
|
return this.monitorManager.notifyUploadFinished(board, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeSourceOverrides(
|
private mergeSourceOverrides(
|
||||||
req: { getSourceOverrideMap(): jspb.Map<string, string> },
|
req: { getSourceOverrideMap(): jspb.Map<string, string> },
|
||||||
options: CoreService.Compile.Options
|
options: CoreService.Compile.Options
|
||||||
): void {
|
): void {
|
||||||
const sketchPath = FileUri.fsPath(options.sketchUri);
|
const sketchPath = FileUri.fsPath(options.sketch.uri);
|
||||||
for (const uri of Object.keys(options.sourceOverride)) {
|
for (const uri of Object.keys(options.sourceOverride)) {
|
||||||
const content = options.sourceOverride[uri];
|
const content = options.sourceOverride[uri];
|
||||||
if (content) {
|
if (content) {
|
||||||
@ -304,9 +349,33 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private flushOutputPanelMessages(chunk: string): void {
|
private createPort(port: Port | undefined): GrpcPort {
|
||||||
this.responseService.appendToOutput({
|
const grpcPort = new GrpcPort();
|
||||||
chunk,
|
if (port) {
|
||||||
});
|
grpcPort.setAddress(port.address);
|
||||||
|
grpcPort.setLabel(port.addressLabel);
|
||||||
|
grpcPort.setProtocol(port.protocol);
|
||||||
|
grpcPort.setProtocolLabel(port.protocolLabel);
|
||||||
|
}
|
||||||
|
return grpcPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type StreamingResponse =
|
||||||
|
| CompileResponse
|
||||||
|
| UploadResponse
|
||||||
|
| UploadUsingProgrammerResponse
|
||||||
|
| BurnBootloaderResponse;
|
||||||
|
namespace StreamingResponse {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function createOnDataHandler<R extends StreamingResponse>(
|
||||||
|
stderr: Uint8Array[],
|
||||||
|
onData: (out: Uint8Array, err: Uint8Array) => void
|
||||||
|
): (response: R) => void {
|
||||||
|
return (response: R) => {
|
||||||
|
const out = response.getOutStream_asU8();
|
||||||
|
const err = response.getErrStream_asU8();
|
||||||
|
stderr.push(err);
|
||||||
|
onData(out, err);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,23 +3,19 @@ import {
|
|||||||
injectable,
|
injectable,
|
||||||
postConstruct,
|
postConstruct,
|
||||||
} from '@theia/core/shared/inversify';
|
} from '@theia/core/shared/inversify';
|
||||||
import { join, basename } from 'path';
|
import { join } from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { promisify } from 'util';
|
|
||||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
import {
|
import {
|
||||||
Sketch,
|
|
||||||
SketchRef,
|
SketchRef,
|
||||||
SketchContainer,
|
SketchContainer,
|
||||||
} from '../common/protocol/sketches-service';
|
} from '../common/protocol/sketches-service';
|
||||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
|
||||||
import { ExamplesService } from '../common/protocol/examples-service';
|
import { ExamplesService } from '../common/protocol/examples-service';
|
||||||
import {
|
import {
|
||||||
LibraryLocation,
|
LibraryLocation,
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
} from '../common/protocol';
|
} from '../common/protocol';
|
||||||
import { ConfigServiceImpl } from './config-service-impl';
|
|
||||||
import { duration } from '../common/decorators';
|
import { duration } from '../common/decorators';
|
||||||
import { URI } from '@theia/core/lib/common/uri';
|
import { URI } from '@theia/core/lib/common/uri';
|
||||||
import { Path } from '@theia/core/lib/common/path';
|
import { Path } from '@theia/core/lib/common/path';
|
||||||
@ -88,14 +84,8 @@ export class BuiltInExamplesServiceImpl {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ExamplesServiceImpl implements ExamplesService {
|
export class ExamplesServiceImpl implements ExamplesService {
|
||||||
@inject(SketchesServiceImpl)
|
|
||||||
protected readonly sketchesService: SketchesServiceImpl;
|
|
||||||
|
|
||||||
@inject(LibraryService)
|
@inject(LibraryService)
|
||||||
protected readonly libraryService: LibraryService;
|
private readonly libraryService: LibraryService;
|
||||||
|
|
||||||
@inject(ConfigServiceImpl)
|
|
||||||
protected readonly configService: ConfigServiceImpl;
|
|
||||||
|
|
||||||
@inject(BuiltInExamplesServiceImpl)
|
@inject(BuiltInExamplesServiceImpl)
|
||||||
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
||||||
@ -117,7 +107,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
|||||||
fqbn,
|
fqbn,
|
||||||
});
|
});
|
||||||
for (const pkg of packages) {
|
for (const pkg of packages) {
|
||||||
const container = await this.tryGroupExamplesNew(pkg);
|
const container = await this.tryGroupExamples(pkg);
|
||||||
const { location } = pkg;
|
const { location } = pkg;
|
||||||
if (location === LibraryLocation.USER) {
|
if (location === LibraryLocation.USER) {
|
||||||
user.push(container);
|
user.push(container);
|
||||||
@ -130,9 +120,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
|||||||
any.push(container);
|
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 };
|
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
|
* 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.
|
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
|
||||||
*/
|
*/
|
||||||
protected async tryGroupExamplesNew({
|
private async tryGroupExamples({
|
||||||
label,
|
label,
|
||||||
exampleUris,
|
exampleUris,
|
||||||
installDirUri,
|
installDirUri,
|
||||||
@ -208,10 +195,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
|||||||
if (!child) {
|
if (!child) {
|
||||||
child = SketchContainer.create(label);
|
child = SketchContainer.create(label);
|
||||||
parent.children.push(child);
|
parent.children.push(child);
|
||||||
//TODO: remove or move sort
|
|
||||||
parent.children.sort((left, right) =>
|
|
||||||
left.label.localeCompare(right.label)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
};
|
};
|
||||||
@ -230,65 +213,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
|||||||
container
|
container
|
||||||
);
|
);
|
||||||
refContainer.sketches.push(ref);
|
refContainer.sketches.push(ref);
|
||||||
//TODO: remove or move sort
|
|
||||||
refContainer.sketches.sort((left, right) =>
|
|
||||||
left.name.localeCompare(right.name)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built-ins are included inside the IDE.
|
|
||||||
protected async load(path: string): Promise<SketchContainer> {
|
|
||||||
if (!(await promisify(fs.exists)(path))) {
|
|
||||||
throw new Error('Examples are not available');
|
|
||||||
}
|
|
||||||
const stat = await promisify(fs.stat)(path);
|
|
||||||
if (!stat.isDirectory) {
|
|
||||||
throw new Error(`${path} is not a directory.`);
|
|
||||||
}
|
|
||||||
const names = await promisify(fs.readdir)(path);
|
|
||||||
const sketches: SketchRef[] = [];
|
|
||||||
const children: SketchContainer[] = [];
|
|
||||||
for (const p of names.map((name) => join(path, name))) {
|
|
||||||
const stat = await promisify(fs.stat)(p);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
const sketch = await this.tryLoadSketch(p);
|
|
||||||
if (sketch) {
|
|
||||||
sketches.push({ name: sketch.name, uri: sketch.uri });
|
|
||||||
sketches.sort((left, right) => left.name.localeCompare(right.name));
|
|
||||||
} else {
|
|
||||||
const child = await this.load(p);
|
|
||||||
children.push(child);
|
|
||||||
children.sort((left, right) => left.label.localeCompare(right.label));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const label = basename(path);
|
|
||||||
return {
|
|
||||||
label,
|
|
||||||
children,
|
|
||||||
sketches,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
|
|
||||||
const map = new Map<string, fs.Stats>();
|
|
||||||
for (const path of paths) {
|
|
||||||
const stat = await promisify(fs.stat)(path);
|
|
||||||
map.set(path, stat);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
|
|
||||||
try {
|
|
||||||
const sketch = await this.sketchesService.loadSketch(
|
|
||||||
FileUri.create(path).toString()
|
|
||||||
);
|
|
||||||
return sketch;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
23
arduino-ide-extension/src/node/service-error.ts
Normal file
23
arduino-ide-extension/src/node/service-error.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Metadata, StatusObject } from '@grpc/grpc-js';
|
||||||
|
|
||||||
|
export type ServiceError = StatusObject & Error;
|
||||||
|
export namespace ServiceError {
|
||||||
|
export function is(arg: unknown): arg is ServiceError {
|
||||||
|
return arg instanceof Error && isStatusObjet(arg);
|
||||||
|
}
|
||||||
|
function isStatusObjet(arg: unknown): arg is StatusObject {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const any = arg as any;
|
||||||
|
return (
|
||||||
|
!!arg &&
|
||||||
|
'code' in arg &&
|
||||||
|
'details' in arg &&
|
||||||
|
typeof any.details === 'string' &&
|
||||||
|
'metadata' in arg &&
|
||||||
|
any.metadata instanceof Metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,19 @@
|
|||||||
export class SimpleBuffer {
|
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
private chunks: Uint8Array[] = [];
|
import { OutputMessage } from '../../common/protocol';
|
||||||
|
|
||||||
|
const DEFAULT_FLUS_TIMEOUT_MS = 32;
|
||||||
|
|
||||||
|
export class SimpleBuffer implements Disposable {
|
||||||
|
private readonly flush: () => void;
|
||||||
|
private readonly chunks = Chunks.create();
|
||||||
private flushInterval?: NodeJS.Timeout;
|
private flushInterval?: NodeJS.Timeout;
|
||||||
|
|
||||||
private flush: () => void;
|
|
||||||
|
|
||||||
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
|
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
if (this.chunks.length > 0) {
|
if (this.chunks.length > 0) {
|
||||||
const chunkString = Buffer.concat(this.chunks).toString();
|
const chunkString = Buffer.concat(this.chunks).toString();
|
||||||
this.clearChunks();
|
this.clearChunks();
|
||||||
|
onFlush(chunks);
|
||||||
onFlush(chunkString);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,12 +21,15 @@ export class SimpleBuffer {
|
|||||||
this.flushInterval = setInterval(flush, flushTimeout);
|
this.flushInterval = setInterval(flush, flushTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addChunk(chunk: Uint8Array): void {
|
public addChunk(
|
||||||
this.chunks.push(chunk);
|
chunk: Uint8Array,
|
||||||
|
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
||||||
|
): void {
|
||||||
|
this.chunks.get(severity)?.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearChunks(): void {
|
private clearChunks(): void {
|
||||||
this.chunks = [];
|
Chunks.clear(this.chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearFlushInterval(): void {
|
public clearFlushInterval(): void {
|
||||||
@ -32,6 +37,37 @@ export class SimpleBuffer {
|
|||||||
this.clearChunks();
|
this.clearChunks();
|
||||||
|
|
||||||
clearInterval(this.flushInterval);
|
clearInterval(this.flushInterval);
|
||||||
|
this.clearChunks();
|
||||||
this.flushInterval = undefined;
|
this.flushInterval = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
|
||||||
|
namespace Chunks {
|
||||||
|
export function create(): Chunks {
|
||||||
|
return new Map([
|
||||||
|
[OutputMessage.Severity.Error, []],
|
||||||
|
[OutputMessage.Severity.Warning, []],
|
||||||
|
[OutputMessage.Severity.Info, []],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function clear(chunks: Chunks): Chunks {
|
||||||
|
for (const chunk of chunks.values()) {
|
||||||
|
chunk.length = 0;
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
export function isEmpty(chunks: Chunks): boolean {
|
||||||
|
return ![...chunks.values()].some((chunk) => Boolean(chunk.length));
|
||||||
|
}
|
||||||
|
export function toString(
|
||||||
|
chunks: Chunks
|
||||||
|
): Map<OutputMessage.Severity, string | undefined> {
|
||||||
|
return new Map(
|
||||||
|
Array.from(chunks.entries()).map(([severity, buffers]) => [
|
||||||
|
severity,
|
||||||
|
buffers.length ? Buffer.concat(buffers).toString() : undefined,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
27
i18n/en.json
27
i18n/en.json
@ -56,6 +56,24 @@
|
|||||||
"uploadRootCertificates": "Upload SSL Root Certificates",
|
"uploadRootCertificates": "Upload SSL Root Certificates",
|
||||||
"uploadingCertificates": "Uploading certificates."
|
"uploadingCertificates": "Uploading certificates."
|
||||||
},
|
},
|
||||||
|
"cli-error-parser": {
|
||||||
|
"byteError": "The 'BYTE' keyword is no longer supported.",
|
||||||
|
"byteMessage": "As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead.",
|
||||||
|
"clientError": "The Client class has been renamed EthernetClient.",
|
||||||
|
"clientMessage": "As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.",
|
||||||
|
"keyboardError": "'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?",
|
||||||
|
"mouseError": "'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?",
|
||||||
|
"receiveError": "Wire.receive() has been renamed Wire.read().",
|
||||||
|
"receiveMessage": "As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.",
|
||||||
|
"sendError": "Wire.send() has been renamed Wire.write().",
|
||||||
|
"sendMessage": "As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.",
|
||||||
|
"serverError": "The Server class has been renamed EthernetServer.",
|
||||||
|
"serverMessage": "As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.",
|
||||||
|
"spiError": "Please import the SPI library from the Sketch > Import Library menu.",
|
||||||
|
"spiMessage": "As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.",
|
||||||
|
"udpError": "The Udp class has been renamed EthernetUdp.",
|
||||||
|
"udpMessage": "As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp."
|
||||||
|
},
|
||||||
"cloud": {
|
"cloud": {
|
||||||
"chooseSketchVisibility": "Choose visibility of your Sketch:",
|
"chooseSketchVisibility": "Choose visibility of your Sketch:",
|
||||||
"cloudSketchbook": "Cloud Sketchbook",
|
"cloudSketchbook": "Cloud Sketchbook",
|
||||||
@ -120,6 +138,9 @@
|
|||||||
"fileAdded": "One file added to the sketch.",
|
"fileAdded": "One file added to the sketch.",
|
||||||
"replaceTitle": "Replace"
|
"replaceTitle": "Replace"
|
||||||
},
|
},
|
||||||
|
"coreContribution": {
|
||||||
|
"copyError": "Copy error messages"
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"debugWithMessage": "Debug - {0}",
|
"debugWithMessage": "Debug - {0}",
|
||||||
"debuggingNotSupported": "Debugging is not supported by '{0}'",
|
"debuggingNotSupported": "Debugging is not supported by '{0}'",
|
||||||
@ -136,7 +157,9 @@
|
|||||||
"decreaseFontSize": "Decrease Font Size",
|
"decreaseFontSize": "Decrease Font Size",
|
||||||
"decreaseIndent": "Decrease Indent",
|
"decreaseIndent": "Decrease Indent",
|
||||||
"increaseFontSize": "Increase Font Size",
|
"increaseFontSize": "Increase Font Size",
|
||||||
"increaseIndent": "Increase Indent"
|
"increaseIndent": "Increase Indent",
|
||||||
|
"nextError": "Next Error",
|
||||||
|
"previousError": "Previous Error"
|
||||||
},
|
},
|
||||||
"electron": {
|
"electron": {
|
||||||
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
|
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
|
||||||
@ -236,6 +259,8 @@
|
|||||||
"cloud.pushpublic.warn": "True if users should be warned before pushing a public sketch to the cloud. Defaults to true.",
|
"cloud.pushpublic.warn": "True if users should be warned before pushing a public sketch to the cloud. Defaults to true.",
|
||||||
"cloud.sketchSyncEnpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.",
|
"cloud.sketchSyncEnpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.",
|
||||||
"compile": "compile",
|
"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.verbose": "True for verbose compile output. False by default",
|
||||||
"compile.warnings": "Tells gcc which warning level to use. It's 'None' by default",
|
"compile.warnings": "Tells gcc which warning level to use. It's 'None' by default",
|
||||||
"compilerWarnings": "Compiler warnings",
|
"compilerWarnings": "Compiler warnings",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user