Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fbf3fc6

Browse files
authoredJun 21, 2022
Merge branch 'main' into 1032-uploads-failing-with-monitor-open
2 parents 3830ba2 + 4611381 commit fbf3fc6

26 files changed

+1622
-396
lines changed
 

‎arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ import {
296296
SurveyNotificationService,
297297
SurveyNotificationServicePath,
298298
} from '../common/protocol/survey-service';
299+
import { WindowContribution } from './theia/core/window-contribution';
300+
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
301+
import { CoreErrorHandler } from './contributions/core-error-handler';
302+
import { CompilerErrors } from './contributions/compiler-errors';
299303

300304
MonacoThemingService.register({
301305
id: 'arduino-theme',
@@ -428,6 +432,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
428432
)
429433
)
430434
.inSingletonScope();
435+
bind(CoreErrorHandler).toSelf().inSingletonScope();
431436

432437
// Serial monitor
433438
bind(MonitorWidget).toSelf();
@@ -605,6 +610,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
605610
bind(OutputToolbarContribution).toSelf().inSingletonScope();
606611
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
607612

613+
// To remove `New Window` from the `File` menu
614+
bind(WindowContribution).toSelf().inSingletonScope();
615+
rebind(TheiaWindowContribution).toService(WindowContribution);
616+
608617
bind(ArduinoDaemon)
609618
.toDynamicValue((context) =>
610619
WebSocketConnectionProvider.createProxy(
@@ -688,6 +697,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
688697
Contribution.configure(bind, AddZipLibrary);
689698
Contribution.configure(bind, PlotterFrontendContribution);
690699
Contribution.configure(bind, Format);
700+
Contribution.configure(bind, CompilerErrors);
691701

692702
// Disabled the quick-pick customization from Theia when multiple formatters are available.
693703
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

‎arduino-ide-extension/src/browser/arduino-preferences.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,32 @@ export enum UpdateChannel {
1313
Stable = 'stable',
1414
Nightly = 'nightly',
1515
}
16+
export const ErrorRevealStrategyLiterals = [
17+
/**
18+
* Scroll vertically as necessary and reveal a line.
19+
*/
20+
'auto',
21+
/**
22+
* Scroll vertically as necessary and reveal a line centered vertically.
23+
*/
24+
'center',
25+
/**
26+
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
27+
*/
28+
'top',
29+
/**
30+
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
31+
*/
32+
'centerIfOutsideViewport',
33+
] as const;
34+
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
35+
export namespace ErrorRevealStrategy {
36+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
37+
export function is(arg: any): arg is ErrorRevealStrategy {
38+
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
39+
}
40+
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
41+
}
1642

1743
export const ArduinoConfigSchema: PreferenceSchema = {
1844
type: 'object',
@@ -33,6 +59,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
3359
),
3460
default: false,
3561
},
62+
'arduino.compile.experimental': {
63+
type: 'boolean',
64+
description: nls.localize(
65+
'arduino/preferences/compile.experimental',
66+
'True if the IDE should handle multiple compiler errors. False by default'
67+
),
68+
default: false,
69+
},
70+
'arduino.compile.revealRange': {
71+
enum: [...ErrorRevealStrategyLiterals],
72+
description: nls.localize(
73+
'arduino/preferences/compile.revealRange',
74+
"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}'.",
75+
ErrorRevealStrategy.Default
76+
),
77+
default: ErrorRevealStrategy.Default,
78+
},
3679
'arduino.compile.warnings': {
3780
enum: [...CompilerWarningLiterals],
3881
description: nls.localize(
@@ -182,12 +225,22 @@ export const ArduinoConfigSchema: PreferenceSchema = {
182225
),
183226
default: true,
184227
},
228+
'arduino.cli.daemon.debug': {
229+
type: 'boolean',
230+
description: nls.localize(
231+
'arduino/preferences/cli.daemonDebug',
232+
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
233+
),
234+
default: false,
235+
},
185236
},
186237
};
187238

188239
export interface ArduinoConfiguration {
189240
'arduino.language.log': boolean;
190241
'arduino.compile.verbose': boolean;
242+
'arduino.compile.experimental': boolean;
243+
'arduino.compile.revealRange': ErrorRevealStrategy;
191244
'arduino.compile.warnings': CompilerWarnings;
192245
'arduino.upload.verbose': boolean;
193246
'arduino.upload.verify': boolean;
@@ -207,6 +260,7 @@ export interface ArduinoConfiguration {
207260
'arduino.auth.audience': string;
208261
'arduino.auth.registerUri': string;
209262
'arduino.survey.notification': boolean;
263+
'arduino.cli.daemon.debug': boolean;
210264
}
211265

212266
export const ArduinoPreferences = Symbol('ArduinoPreferences');

‎arduino-ide-extension/src/browser/contributions/burn-bootloader.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
import { inject, injectable } from '@theia/core/shared/inversify';
2-
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
3-
import { CoreService } from '../../common/protocol';
42
import { ArduinoMenus } from '../menu/arduino-menus';
53
import { BoardsDataStore } from '../boards/boards-data-store';
64
import { BoardsServiceProvider } from '../boards/boards-service-provider';
75
import {
8-
SketchContribution,
6+
CoreServiceContribution,
97
Command,
108
CommandRegistry,
119
MenuModelRegistry,
1210
} from './contribution';
1311
import { nls } from '@theia/core/lib/common';
1412

1513
@injectable()
16-
export class BurnBootloader extends SketchContribution {
17-
@inject(CoreService)
18-
protected readonly coreService: CoreService;
19-
20-
14+
export class BurnBootloader extends CoreServiceContribution {
2115
@inject(BoardsDataStore)
2216
protected readonly boardsDataStore: BoardsDataStore;
2317

2418
@inject(BoardsServiceProvider)
2519
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
2620

27-
@inject(OutputChannelManager)
28-
protected override readonly outputChannelManager: OutputChannelManager;
29-
3021
override registerCommands(registry: CommandRegistry): void {
3122
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
3223
execute: () => this.burnBootloader(),
@@ -62,7 +53,7 @@ export class BurnBootloader extends SketchContribution {
6253
...boardsConfig.selectedBoard,
6354
name: boardsConfig.selectedBoard?.name || '',
6455
fqbn,
65-
}
56+
};
6657
this.outputChannelManager.getChannel('Arduino').clear();
6758
await this.coreService.burnBootloader({
6859
board,
@@ -81,13 +72,7 @@ export class BurnBootloader extends SketchContribution {
8172
}
8273
);
8374
} catch (e) {
84-
let errorMessage = "";
85-
if (typeof e === "string") {
86-
errorMessage = e;
87-
} else {
88-
errorMessage = e.toString();
89-
}
90-
this.messageService.error(errorMessage);
75+
this.handleError(e);
9176
}
9277
}
9378
}

‎arduino-ide-extension/src/browser/contributions/compiler-errors.ts

Lines changed: 656 additions & 0 deletions
Large diffs are not rendered by default.

‎arduino-ide-extension/src/browser/contributions/contribution.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
1414
import { MessageService } from '@theia/core/lib/common/message-service';
1515
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
1616
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
17-
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
17+
1818
import {
1919
MenuModelRegistry,
2020
MenuContribution,
@@ -48,9 +48,15 @@ import {
4848
ConfigService,
4949
FileSystemExt,
5050
Sketch,
51+
CoreService,
52+
CoreError,
5153
} from '../../common/protocol';
5254
import { ArduinoPreferences } from '../arduino-preferences';
5355
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
56+
import { CoreErrorHandler } from './core-error-handler';
57+
import { nls } from '@theia/core';
58+
import { OutputChannelManager } from '../theia/output/output-channel';
59+
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
5460

5561
export {
5662
Command,
@@ -164,6 +170,56 @@ export abstract class SketchContribution extends Contribution {
164170
}
165171
}
166172

173+
@injectable()
174+
export class CoreServiceContribution extends SketchContribution {
175+
@inject(CoreService)
176+
protected readonly coreService: CoreService;
177+
178+
@inject(CoreErrorHandler)
179+
protected readonly coreErrorHandler: CoreErrorHandler;
180+
181+
@inject(ClipboardService)
182+
private readonly clipboardService: ClipboardService;
183+
184+
protected handleError(error: unknown): void {
185+
this.coreErrorHandler.tryHandle(error);
186+
this.tryToastErrorMessage(error);
187+
}
188+
189+
private tryToastErrorMessage(error: unknown): void {
190+
let message: undefined | string = undefined;
191+
if (CoreError.is(error)) {
192+
message = error.message;
193+
} else if (error instanceof Error) {
194+
message = error.message;
195+
} else if (typeof error === 'string') {
196+
message = error;
197+
} else {
198+
try {
199+
message = JSON.stringify(error);
200+
} catch {}
201+
}
202+
if (message) {
203+
const copyAction = nls.localize(
204+
'arduino/coreContribution/copyError',
205+
'Copy error messages'
206+
);
207+
this.messageService.error(message, copyAction).then(async (action) => {
208+
if (action === copyAction) {
209+
const content = await this.outputChannelManager.contentOfChannel(
210+
'Arduino'
211+
);
212+
if (content) {
213+
this.clipboardService.writeText(content);
214+
}
215+
}
216+
});
217+
} else {
218+
throw error;
219+
}
220+
}
221+
}
222+
167223
export namespace Contribution {
168224
export function configure(
169225
bind: interfaces.Bind,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Emitter, Event } from '@theia/core';
2+
import { injectable } from '@theia/core/shared/inversify';
3+
import { CoreError } from '../../common/protocol/core-service';
4+
5+
@injectable()
6+
export class CoreErrorHandler {
7+
private readonly compilerErrors: CoreError.Compiler[] = [];
8+
private readonly compilerErrorsDidChangeEmitter = new Emitter<
9+
CoreError.Compiler[]
10+
>();
11+
12+
tryHandle(error: unknown): void {
13+
if (CoreError.is(error)) {
14+
this.compilerErrors.length = 0;
15+
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
16+
this.fireCompilerErrorsDidChange();
17+
}
18+
}
19+
20+
reset(): void {
21+
this.compilerErrors.length = 0;
22+
this.fireCompilerErrorsDidChange();
23+
}
24+
25+
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
26+
return this.compilerErrorsDidChangeEmitter.event;
27+
}
28+
29+
private fireCompilerErrorsDidChange(): void {
30+
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
31+
}
32+
}

‎arduino-ide-extension/src/browser/contributions/format.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { MaybePromise } from '@theia/core';
22
import { inject, injectable } from '@theia/core/shared/inversify';
33
import * as monaco from '@theia/monaco-editor-core';
44
import { Formatter } from '../../common/protocol/formatter';
5+
import { InoSelector } from '../ino-selectors';
6+
import { fullRange } from '../utils/monaco';
57
import { Contribution, URI } from './contribution';
68

79
@injectable()
@@ -15,12 +17,11 @@ export class Format
1517
private readonly formatter: Formatter;
1618

1719
override onStart(): MaybePromise<void> {
18-
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
1920
monaco.languages.registerDocumentRangeFormattingEditProvider(
20-
selector,
21+
InoSelector,
2122
this
2223
);
23-
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
24+
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
2425
}
2526
async provideDocumentRangeFormattingEdits(
2627
model: monaco.editor.ITextModel,
@@ -39,18 +40,11 @@ export class Format
3940
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4041
_token: monaco.CancellationToken
4142
): Promise<monaco.languages.TextEdit[]> {
42-
const range = this.fullRange(model);
43+
const range = fullRange(model);
4344
const text = await this.format(model, range, options);
4445
return [{ range, text }];
4546
}
4647

47-
private fullRange(model: monaco.editor.ITextModel): monaco.Range {
48-
const lastLine = model.getLineCount();
49-
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
50-
const end = new monaco.Position(lastLine, lastLineMaxColumn);
51-
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
52-
}
53-
5448
/**
5549
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
5650
* folder locations where the `.clang-format` file could be.
@@ -82,13 +76,4 @@ export class Format
8276
options,
8377
});
8478
}
85-
86-
private selectorOf(
87-
...languageId: string[]
88-
): monaco.languages.LanguageSelector {
89-
return languageId.map((language) => ({
90-
language,
91-
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
92-
}));
93-
}
9479
}

‎arduino-ide-extension/src/browser/contributions/upload-sketch.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
66
import { BoardsDataStore } from '../boards/boards-data-store';
77
import { BoardsServiceProvider } from '../boards/boards-service-provider';
88
import {
9-
SketchContribution,
9+
CoreServiceContribution,
1010
Command,
1111
CommandRegistry,
1212
MenuModelRegistry,
@@ -18,10 +18,7 @@ import { DisposableCollection, nls } from '@theia/core/lib/common';
1818
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
1919

2020
@injectable()
21-
export class UploadSketch extends SketchContribution {
22-
@inject(CoreService)
23-
protected readonly coreService: CoreService;
24-
21+
export class UploadSketch extends CoreServiceContribution {
2522
@inject(MenuModelRegistry)
2623
protected readonly menuRegistry: MenuModelRegistry;
2724

@@ -201,17 +198,17 @@ export class UploadSketch extends SketchContribution {
201198
return;
202199
}
203200

204-
// toggle the toolbar button and menu item state.
205-
// uploadInProgress will be set to false whether the upload fails or not
206-
this.uploadInProgress = true;
207-
208-
this.onDidChangeEmitter.fire();
209201
const sketch = await this.sketchServiceClient.currentSketch();
210202
if (!CurrentSketch.isValid(sketch)) {
211203
return;
212204
}
213205

214206
try {
207+
// toggle the toolbar button and menu item state.
208+
// uploadInProgress will be set to false whether the upload fails or not
209+
this.uploadInProgress = true;
210+
this.coreErrorHandler.reset();
211+
this.onDidChangeEmitter.fire();
215212
const { boardsConfig } = this.boardsServiceClientImpl;
216213
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
217214
await Promise.all([
@@ -230,7 +227,6 @@ export class UploadSketch extends SketchContribution {
230227
fqbn,
231228
};
232229
let options: CoreService.Upload.Options | undefined = undefined;
233-
const sketchUri = sketch.uri;
234230
const optimizeForDebug = this.editorMode.compileForDebug;
235231
const { selectedPort } = boardsConfig;
236232
const port = selectedPort;
@@ -249,7 +245,7 @@ export class UploadSketch extends SketchContribution {
249245
if (usingProgrammer) {
250246
const programmer = selectedProgrammer;
251247
options = {
252-
sketchUri,
248+
sketch,
253249
board,
254250
optimizeForDebug,
255251
programmer,
@@ -261,7 +257,7 @@ export class UploadSketch extends SketchContribution {
261257
};
262258
} else {
263259
options = {
264-
sketchUri,
260+
sketch,
265261
board,
266262
optimizeForDebug,
267263
port,
@@ -282,13 +278,7 @@ export class UploadSketch extends SketchContribution {
282278
{ timeout: 3000 }
283279
);
284280
} catch (e) {
285-
let errorMessage = '';
286-
if (typeof e === 'string') {
287-
errorMessage = e;
288-
} else {
289-
errorMessage = e.toString();
290-
}
291-
this.messageService.error(errorMessage);
281+
this.handleError(e);
292282
} finally {
293283
this.uploadInProgress = false;
294284

‎arduino-ide-extension/src/browser/contributions/verify-sketch.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { inject, injectable } from '@theia/core/shared/inversify';
22
import { Emitter } from '@theia/core/lib/common/event';
3-
import { CoreService } from '../../common/protocol';
43
import { ArduinoMenus } from '../menu/arduino-menus';
54
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
65
import { BoardsDataStore } from '../boards/boards-data-store';
76
import { BoardsServiceProvider } from '../boards/boards-service-provider';
87
import {
9-
SketchContribution,
8+
CoreServiceContribution,
109
Command,
1110
CommandRegistry,
1211
MenuModelRegistry,
@@ -17,10 +16,7 @@ import { nls } from '@theia/core/lib/common';
1716
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
1817

1918
@injectable()
20-
export class VerifySketch extends SketchContribution {
21-
@inject(CoreService)
22-
protected readonly coreService: CoreService;
23-
19+
export class VerifySketch extends CoreServiceContribution {
2420
@inject(BoardsDataStore)
2521
protected readonly boardsDataStore: BoardsDataStore;
2622

@@ -96,14 +92,14 @@ export class VerifySketch extends SketchContribution {
9692

9793
// toggle the toolbar button and menu item state.
9894
// verifyInProgress will be set to false whether the compilation fails or not
99-
this.verifyInProgress = true;
100-
this.onDidChangeEmitter.fire();
10195
const sketch = await this.sketchServiceClient.currentSketch();
102-
10396
if (!CurrentSketch.isValid(sketch)) {
10497
return;
10598
}
10699
try {
100+
this.verifyInProgress = true;
101+
this.coreErrorHandler.reset();
102+
this.onDidChangeEmitter.fire();
107103
const { boardsConfig } = this.boardsServiceClientImpl;
108104
const [fqbn, sourceOverride] = await Promise.all([
109105
this.boardsDataStore.appendConfigToFqbn(
@@ -115,12 +111,12 @@ export class VerifySketch extends SketchContribution {
115111
...boardsConfig.selectedBoard,
116112
name: boardsConfig.selectedBoard?.name || '',
117113
fqbn,
118-
}
114+
};
119115
const verbose = this.preferences.get('arduino.compile.verbose');
120116
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
121117
this.outputChannelManager.getChannel('Arduino').clear();
122118
await this.coreService.compile({
123-
sketchUri: sketch.uri,
119+
sketch,
124120
board,
125121
optimizeForDebug: this.editorMode.compileForDebug,
126122
verbose,
@@ -133,13 +129,7 @@ export class VerifySketch extends SketchContribution {
133129
{ timeout: 3000 }
134130
);
135131
} catch (e) {
136-
let errorMessage = "";
137-
if (typeof e === "string") {
138-
errorMessage = e;
139-
} else {
140-
errorMessage = e.toString();
141-
}
142-
this.messageService.error(errorMessage);
132+
this.handleError(e);
143133
} finally {
144134
this.verifyInProgress = false;
145135
this.onDidChangeEmitter.fire();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as monaco from '@theia/monaco-editor-core';
2+
/**
3+
* Exclusive "ino" document selector for monaco.
4+
*/
5+
export const InoSelector = selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
6+
function selectorOf(
7+
...languageId: string[]
8+
): monaco.languages.LanguageSelector {
9+
return languageId.map((language) => ({
10+
language,
11+
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
12+
}));
13+
}
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { inject, injectable } from '@theia/core/shared/inversify';
22
import { Emitter } from '@theia/core/lib/common/event';
3-
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
4-
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
3+
import {
4+
OutputChannelManager,
5+
OutputChannelSeverity,
6+
} from '@theia/output/lib/browser/output-channel';
57
import {
68
OutputMessage,
79
ProgressMessage,
@@ -10,13 +12,10 @@ import {
1012

1113
@injectable()
1214
export class ResponseServiceImpl implements ResponseServiceArduino {
13-
@inject(OutputContribution)
14-
protected outputContribution: OutputContribution;
15-
1615
@inject(OutputChannelManager)
17-
protected outputChannelManager: OutputChannelManager;
16+
private readonly outputChannelManager: OutputChannelManager;
1817

19-
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
18+
private readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
2019

2120
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
2221

@@ -25,13 +24,22 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
2524
}
2625

2726
appendToOutput(message: OutputMessage): void {
28-
const { chunk } = message;
27+
const { chunk, severity } = message;
2928
const channel = this.outputChannelManager.getChannel('Arduino');
3029
channel.show({ preserveFocus: true });
31-
channel.append(chunk);
30+
channel.append(chunk, mapSeverity(severity));
3231
}
3332

3433
reportProgress(progress: ProgressMessage): void {
3534
this.progressDidChangeEmitter.fire(progress);
3635
}
3736
}
37+
38+
function mapSeverity(severity?: OutputMessage.Severity): OutputChannelSeverity {
39+
if (severity === OutputMessage.Severity.Error) {
40+
return OutputChannelSeverity.Error;
41+
} else if (severity === OutputMessage.Severity.Warning) {
42+
return OutputChannelSeverity.Warning;
43+
}
44+
return OutputChannelSeverity.Info;
45+
}

‎arduino-ide-extension/src/browser/style/editor.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@
88
.monaco-list-row.show-file-icons.focused {
99
background-color: #d6ebff;
1010
}
11+
12+
.monaco-editor .view-overlays .compiler-error {
13+
background-color: var(--theia-inputValidation-errorBackground);
14+
opacity: 0.4 !important;
15+
}

‎arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
2121
CommonCommands.TOGGLE_MAXIMIZED,
2222
CommonCommands.PIN_TAB,
2323
CommonCommands.UNPIN_TAB,
24+
CommonCommands.NEW_FILE,
2425
]) {
2526
commandRegistry.unregisterCommand(command);
2627
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
3+
4+
@injectable()
5+
export class WindowContribution extends TheiaWindowContribution {
6+
override registerCommands(): void {
7+
// NOOP
8+
}
9+
override registerKeybindings(): void {
10+
// NOO
11+
}
12+
override registerMenus(): void {
13+
// NOOP;
14+
}
15+
}

‎arduino-ide-extension/src/browser/theia/output/output-channel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export class OutputChannelManager extends TheiaOutputChannelManager {
4040
}
4141
return channel;
4242
}
43+
44+
async contentOfChannel(name: string): Promise<string | undefined> {
45+
const resource = this.resources.get(name);
46+
if (resource) {
47+
return resource.readContents();
48+
}
49+
return undefined;
50+
}
4351
}
4452

4553
export class OutputChannel extends TheiaOutputChannel {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as monaco from '@theia/monaco-editor-core';
2+
3+
export function fullRange(model: monaco.editor.ITextModel): monaco.Range {
4+
const lastLine = model.getLineCount();
5+
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
6+
const end = new monaco.Position(lastLine, lastLineMaxColumn);
7+
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
8+
}

‎arduino-ide-extension/src/common/protocol/core-service.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { ApplicationError } from '@theia/core';
2+
import { Location } from '@theia/core/shared/vscode-languageserver-protocol';
13
import { BoardUserField } from '.';
24
import { Board, Port } from '../../common/protocol/boards-service';
5+
import { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
36
import { Programmer } from './boards-service';
7+
import { Sketch } from './sketches-service';
48

59
export const CompilerWarningLiterals = [
610
'None',
@@ -9,6 +13,53 @@ export const CompilerWarningLiterals = [
913
'All',
1014
] as const;
1115
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
16+
export namespace CoreError {
17+
export type ErrorInfo = CliErrorInfo;
18+
export interface Compiler extends ErrorInfo {
19+
readonly message: string;
20+
readonly location: Location;
21+
}
22+
export namespace Compiler {
23+
export function is(error: ErrorInfo): error is Compiler {
24+
const { message, location } = error;
25+
return !!message && !!location;
26+
}
27+
}
28+
export const Codes = {
29+
Verify: 4001,
30+
Upload: 4002,
31+
UploadUsingProgrammer: 4003,
32+
BurnBootloader: 4004,
33+
};
34+
export const VerifyFailed = create(Codes.Verify);
35+
export const UploadFailed = create(Codes.Upload);
36+
export const UploadUsingProgrammerFailed = create(
37+
Codes.UploadUsingProgrammer
38+
);
39+
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
40+
export function is(
41+
error: unknown
42+
): error is ApplicationError<number, ErrorInfo[]> {
43+
return (
44+
error instanceof Error &&
45+
ApplicationError.is(error) &&
46+
Object.values(Codes).includes(error.code)
47+
);
48+
}
49+
function create(
50+
code: number
51+
): ApplicationError.Constructor<number, ErrorInfo[]> {
52+
return ApplicationError.declare(
53+
code,
54+
(message: string, data: ErrorInfo[]) => {
55+
return {
56+
data,
57+
message,
58+
};
59+
}
60+
);
61+
}
62+
}
1263

1364
export const CoreServicePath = '/services/core-service';
1465
export const CoreService = Symbol('CoreService');
@@ -23,16 +74,12 @@ export interface CoreService {
2374
upload(options: CoreService.Upload.Options): Promise<void>;
2475
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
2576
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
26-
isUploading(): Promise<boolean>;
2777
}
2878

2979
export namespace CoreService {
3080
export namespace Compile {
3181
export interface Options {
32-
/**
33-
* `file` URI to the sketch folder.
34-
*/
35-
readonly sketchUri: string;
82+
readonly sketch: Sketch;
3683
readonly board?: Board;
3784
readonly optimizeForDebug: boolean;
3885
readonly verbose: boolean;

‎arduino-ide-extension/src/common/protocol/response-service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { Event } from '@theia/core/lib/common/event';
22

33
export interface OutputMessage {
44
readonly chunk: string;
5-
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
5+
readonly severity?: OutputMessage.Severity;
6+
}
7+
export namespace OutputMessage {
8+
export enum Severity {
9+
Error,
10+
Warning,
11+
Info,
12+
}
613
}
714

815
export interface ProgressMessage {

‎arduino-ide-extension/src/common/protocol/sketches-service.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,8 @@ export namespace Sketch {
127127
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
128128
}
129129
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
130-
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
131-
return (
132-
[mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(
133-
uri.toString()
134-
) !== -1
130+
return uris(sketch).includes(
131+
typeof uri === 'string' ? uri : uri.toString()
135132
);
136133
}
137134
export function isSketchFile(arg: string | URI): boolean {
@@ -140,6 +137,10 @@ export namespace Sketch {
140137
}
141138
return Extensions.MAIN.some((ext) => arg.endsWith(ext));
142139
}
140+
export function uris(sketch: Sketch): string[] {
141+
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
142+
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
143+
}
143144
}
144145

145146
export interface SketchContainer {

‎arduino-ide-extension/src/node/arduino-daemon-impl.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { join } from 'path';
2+
import { promises as fs } from 'fs';
23
import { inject, injectable, named } from '@theia/core/shared/inversify';
34
import { spawn, ChildProcess } from 'child_process';
45
import { FileUri } from '@theia/core/lib/node/file-uri';
@@ -142,9 +143,12 @@ export class ArduinoDaemonImpl
142143
}
143144

144145
protected async getSpawnArgs(): Promise<string[]> {
145-
const configDirUri = await this.envVariablesServer.getConfigDirUri();
146+
const [configDirUri, debug] = await Promise.all([
147+
this.envVariablesServer.getConfigDirUri(),
148+
this.debugDaemon(),
149+
]);
146150
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
147-
return [
151+
const args = [
148152
'daemon',
149153
'--format',
150154
'jsonmini',
@@ -156,6 +160,41 @@ export class ArduinoDaemonImpl
156160
'--log-format',
157161
'json',
158162
];
163+
if (debug) {
164+
args.push('--debug');
165+
}
166+
return args;
167+
}
168+
169+
private async debugDaemon(): Promise<boolean> {
170+
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
171+
const configDirUri = await this.envVariablesServer.getConfigDirUri();
172+
const configDirPath = FileUri.fsPath(configDirUri);
173+
try {
174+
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
175+
encoding: 'utf8',
176+
});
177+
const json = this.tryParse(raw);
178+
if (json) {
179+
const value = json['arduino.cli.daemon.debug'];
180+
return typeof value === 'boolean' && !!value;
181+
}
182+
return false;
183+
} catch (error) {
184+
if ('code' in error && error.code === 'ENOENT') {
185+
return false;
186+
}
187+
throw error;
188+
}
189+
}
190+
191+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192+
private tryParse(raw: string): any | undefined {
193+
try {
194+
return JSON.parse(raw);
195+
} catch {
196+
return undefined;
197+
}
159198
}
160199

161200
protected async spawnDaemonProcess(): Promise<{
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { notEmpty } from '@theia/core';
2+
import { nls } from '@theia/core/lib/common/nls';
3+
import { FileUri } from '@theia/core/lib/node/file-uri';
4+
import {
5+
Location,
6+
Range,
7+
Position,
8+
} from '@theia/core/shared/vscode-languageserver-protocol';
9+
import { Sketch } from '../common/protocol';
10+
11+
export interface ErrorInfo {
12+
readonly message?: string;
13+
readonly location?: Location;
14+
readonly details?: string;
15+
}
16+
export interface ErrorSource {
17+
readonly content: string | ReadonlyArray<Uint8Array>;
18+
readonly sketch?: Sketch;
19+
}
20+
21+
export function tryParseError(source: ErrorSource): ErrorInfo[] {
22+
const { content, sketch } = source;
23+
const err =
24+
typeof content === 'string'
25+
? content
26+
: Buffer.concat(content).toString('utf8');
27+
if (sketch) {
28+
return tryParse(err)
29+
.map(remapErrorMessages)
30+
.filter(isLocationInSketch(sketch))
31+
.map(errorInfo());
32+
}
33+
return [];
34+
}
35+
36+
interface ParseResult {
37+
readonly path: string;
38+
readonly line: number;
39+
readonly column?: number;
40+
readonly errorPrefix: string;
41+
readonly error: string;
42+
readonly message?: string;
43+
}
44+
namespace ParseResult {
45+
export function keyOf(result: ParseResult): string {
46+
/**
47+
* The CLI compiler might return with the same error multiple times. This is the key function for the distinct set calculation.
48+
*/
49+
return JSON.stringify(result);
50+
}
51+
}
52+
53+
function isLocationInSketch(
54+
sketch: Sketch
55+
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
56+
return (result) => {
57+
const uri = FileUri.create(result.path).toString();
58+
if (!Sketch.isInSketch(uri, sketch)) {
59+
console.warn(
60+
`URI <${uri}> is not contained in sketch: <${JSON.stringify(sketch)}>`
61+
);
62+
return false;
63+
}
64+
return true;
65+
};
66+
}
67+
68+
function errorInfo(): (value: ParseResult) => ErrorInfo {
69+
return ({ error, message, path, line, column }) => ({
70+
message: error,
71+
details: message,
72+
location: {
73+
uri: FileUri.create(path).toString(),
74+
range: range(line, column),
75+
},
76+
});
77+
}
78+
79+
function range(line: number, column?: number): Range {
80+
const start = Position.create(
81+
line - 1,
82+
typeof column === 'number' ? column - 1 : 0
83+
);
84+
return {
85+
start,
86+
end: start,
87+
};
88+
}
89+
90+
export function tryParse(raw: string): ParseResult[] {
91+
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
92+
const re = new RegExp(
93+
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
94+
'gm'
95+
);
96+
return [
97+
...new Map(
98+
Array.from(raw.matchAll(re) ?? [])
99+
.map((match) => {
100+
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
101+
(match) => (match ? match.trim() : match)
102+
);
103+
const line = Number.parseInt(rawLine, 10);
104+
if (!Number.isInteger(line)) {
105+
console.warn(
106+
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
107+
);
108+
return undefined;
109+
}
110+
let column: number | undefined = undefined;
111+
if (rawColumn) {
112+
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
113+
column = Number.parseInt(normalizedRawColumn, 10);
114+
if (!Number.isInteger(column)) {
115+
console.warn(
116+
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
117+
);
118+
}
119+
}
120+
return {
121+
path,
122+
line,
123+
column,
124+
errorPrefix,
125+
error,
126+
};
127+
})
128+
.filter(notEmpty)
129+
.map((result) => [ParseResult.keyOf(result), result])
130+
).values(),
131+
];
132+
}
133+
134+
/**
135+
* Converts cryptic and legacy error messages to nice ones. Taken from the Java IDE.
136+
*/
137+
function remapErrorMessages(result: ParseResult): ParseResult {
138+
const knownError = KnownErrors[result.error];
139+
if (!knownError) {
140+
return result;
141+
}
142+
const { message, error } = knownError;
143+
return {
144+
...result,
145+
...(message && { message }),
146+
...(error && { error }),
147+
};
148+
}
149+
150+
// Based on the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L528-L578
151+
const KnownErrors: Record<string, { error: string; message?: string }> = {
152+
'SPI.h: No such file or directory': {
153+
error: nls.localize(
154+
'arduino/cli-error-parser/spiError',
155+
'Please import the SPI library from the Sketch > Import Library menu.'
156+
),
157+
message: nls.localize(
158+
'arduino/cli-error-parser/spiMessage',
159+
'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.'
160+
),
161+
},
162+
"'BYTE' was not declared in this scope": {
163+
error: nls.localize(
164+
'arduino/cli-error-parser/byteError',
165+
"The 'BYTE' keyword is no longer supported."
166+
),
167+
message: nls.localize(
168+
'arduino/cli-error-parser/byteMessage',
169+
"As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead."
170+
),
171+
},
172+
"no matching function for call to 'Server::Server(int)'": {
173+
error: nls.localize(
174+
'arduino/cli-error-parser/serverError',
175+
'The Server class has been renamed EthernetServer.'
176+
),
177+
message: nls.localize(
178+
'arduino/cli-error-parser/serverMessage',
179+
'As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.'
180+
),
181+
},
182+
"no matching function for call to 'Client::Client(byte [4], int)'": {
183+
error: nls.localize(
184+
'arduino/cli-error-parser/clientError',
185+
'The Client class has been renamed EthernetClient.'
186+
),
187+
message: nls.localize(
188+
'arduino/cli-error-parser/clientMessage',
189+
'As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.'
190+
),
191+
},
192+
"'Udp' was not declared in this scope": {
193+
error: nls.localize(
194+
'arduino/cli-error-parser/udpError',
195+
'The Udp class has been renamed EthernetUdp.'
196+
),
197+
message: nls.localize(
198+
'arduino/cli-error-parser/udpMessage',
199+
'As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.'
200+
),
201+
},
202+
"'class TwoWire' has no member named 'send'": {
203+
error: nls.localize(
204+
'arduino/cli-error-parser/sendError',
205+
'Wire.send() has been renamed Wire.write().'
206+
),
207+
message: nls.localize(
208+
'arduino/cli-error-parser/sendMessage',
209+
'As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.'
210+
),
211+
},
212+
"'class TwoWire' has no member named 'receive'": {
213+
error: nls.localize(
214+
'arduino/cli-error-parser/receiveError',
215+
'Wire.receive() has been renamed Wire.read().'
216+
),
217+
message: nls.localize(
218+
'arduino/cli-error-parser/receiveMessage',
219+
'As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.'
220+
),
221+
},
222+
"'Mouse' was not declared in this scope": {
223+
error: nls.localize(
224+
'arduino/cli-error-parser/mouseError',
225+
"'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
226+
),
227+
},
228+
"'Keyboard' was not declared in this scope": {
229+
error: nls.localize(
230+
'arduino/cli-error-parser/keyboardError',
231+
"'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?"
232+
),
233+
},
234+
};

‎arduino-ide-extension/src/node/core-service-impl.ts

Lines changed: 268 additions & 201 deletions
Large diffs are not rendered by default.

‎arduino-ide-extension/src/node/examples-service-impl.ts

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,19 @@ import {
33
injectable,
44
postConstruct,
55
} from '@theia/core/shared/inversify';
6-
import { join, basename } from 'path';
6+
import { join } from 'path';
77
import * as fs from 'fs';
8-
import { promisify } from 'util';
98
import { FileUri } from '@theia/core/lib/node/file-uri';
109
import {
11-
Sketch,
1210
SketchRef,
1311
SketchContainer,
1412
} from '../common/protocol/sketches-service';
15-
import { SketchesServiceImpl } from './sketches-service-impl';
1613
import { ExamplesService } from '../common/protocol/examples-service';
1714
import {
1815
LibraryLocation,
1916
LibraryPackage,
2017
LibraryService,
2118
} from '../common/protocol';
22-
import { ConfigServiceImpl } from './config-service-impl';
2319
import { duration } from '../common/decorators';
2420
import { URI } from '@theia/core/lib/common/uri';
2521
import { Path } from '@theia/core/lib/common/path';
@@ -88,14 +84,8 @@ export class BuiltInExamplesServiceImpl {
8884

8985
@injectable()
9086
export class ExamplesServiceImpl implements ExamplesService {
91-
@inject(SketchesServiceImpl)
92-
protected readonly sketchesService: SketchesServiceImpl;
93-
9487
@inject(LibraryService)
95-
protected readonly libraryService: LibraryService;
96-
97-
@inject(ConfigServiceImpl)
98-
protected readonly configService: ConfigServiceImpl;
88+
private readonly libraryService: LibraryService;
9989

10090
@inject(BuiltInExamplesServiceImpl)
10191
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
@@ -117,7 +107,7 @@ export class ExamplesServiceImpl implements ExamplesService {
117107
fqbn,
118108
});
119109
for (const pkg of packages) {
120-
const container = await this.tryGroupExamplesNew(pkg);
110+
const container = await this.tryGroupExamples(pkg);
121111
const { location } = pkg;
122112
if (location === LibraryLocation.USER) {
123113
user.push(container);
@@ -130,9 +120,6 @@ export class ExamplesServiceImpl implements ExamplesService {
130120
any.push(container);
131121
}
132122
}
133-
// user.sort((left, right) => left.label.localeCompare(right.label));
134-
// current.sort((left, right) => left.label.localeCompare(right.label));
135-
// any.sort((left, right) => left.label.localeCompare(right.label));
136123
return { user, current, any };
137124
}
138125

@@ -141,7 +128,7 @@ export class ExamplesServiceImpl implements ExamplesService {
141128
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
142129
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
143130
*/
144-
protected async tryGroupExamplesNew({
131+
private async tryGroupExamples({
145132
label,
146133
exampleUris,
147134
installDirUri,
@@ -208,10 +195,6 @@ export class ExamplesServiceImpl implements ExamplesService {
208195
if (!child) {
209196
child = SketchContainer.create(label);
210197
parent.children.push(child);
211-
//TODO: remove or move sort
212-
parent.children.sort((left, right) =>
213-
left.label.localeCompare(right.label)
214-
);
215198
}
216199
return child;
217200
};
@@ -230,65 +213,7 @@ export class ExamplesServiceImpl implements ExamplesService {
230213
container
231214
);
232215
refContainer.sketches.push(ref);
233-
//TODO: remove or move sort
234-
refContainer.sketches.sort((left, right) =>
235-
left.name.localeCompare(right.name)
236-
);
237216
}
238217
return container;
239218
}
240-
241-
// Built-ins are included inside the IDE.
242-
protected async load(path: string): Promise<SketchContainer> {
243-
if (!(await promisify(fs.exists)(path))) {
244-
throw new Error('Examples are not available');
245-
}
246-
const stat = await promisify(fs.stat)(path);
247-
if (!stat.isDirectory) {
248-
throw new Error(`${path} is not a directory.`);
249-
}
250-
const names = await promisify(fs.readdir)(path);
251-
const sketches: SketchRef[] = [];
252-
const children: SketchContainer[] = [];
253-
for (const p of names.map((name) => join(path, name))) {
254-
const stat = await promisify(fs.stat)(p);
255-
if (stat.isDirectory()) {
256-
const sketch = await this.tryLoadSketch(p);
257-
if (sketch) {
258-
sketches.push({ name: sketch.name, uri: sketch.uri });
259-
sketches.sort((left, right) => left.name.localeCompare(right.name));
260-
} else {
261-
const child = await this.load(p);
262-
children.push(child);
263-
children.sort((left, right) => left.label.localeCompare(right.label));
264-
}
265-
}
266-
}
267-
const label = basename(path);
268-
return {
269-
label,
270-
children,
271-
sketches,
272-
};
273-
}
274-
275-
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
276-
const map = new Map<string, fs.Stats>();
277-
for (const path of paths) {
278-
const stat = await promisify(fs.stat)(path);
279-
map.set(path, stat);
280-
}
281-
return map;
282-
}
283-
284-
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
285-
try {
286-
const sketch = await this.sketchesService.loadSketch(
287-
FileUri.create(path).toString()
288-
);
289-
return sketch;
290-
} catch {
291-
return undefined;
292-
}
293-
}
294219
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Metadata, StatusObject } from '@grpc/grpc-js';
2+
3+
export type ServiceError = StatusObject & Error;
4+
export namespace ServiceError {
5+
export function is(arg: unknown): arg is ServiceError {
6+
return arg instanceof Error && isStatusObjet(arg);
7+
}
8+
function isStatusObjet(arg: unknown): arg is StatusObject {
9+
if (typeof arg === 'object') {
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
const any = arg as any;
12+
return (
13+
!!arg &&
14+
'code' in arg &&
15+
'details' in arg &&
16+
typeof any.details === 'string' &&
17+
'metadata' in arg &&
18+
any.metadata instanceof Metadata
19+
);
20+
}
21+
return false;
22+
}
23+
}
Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
1-
export class SimpleBuffer {
2-
private chunks: Uint8Array[] = [];
1+
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
2+
import { OutputMessage } from '../../common/protocol';
33

4+
const DEFAULT_FLUS_TIMEOUT_MS = 32;
5+
6+
export class SimpleBuffer implements Disposable {
7+
private readonly chunks = Chunks.create();
8+
private readonly flush: () => void;
49
private flushInterval?: NodeJS.Timeout;
510

6-
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
7-
this.flushInterval = setInterval(() => {
8-
if (this.chunks.length > 0) {
9-
const chunkString = Buffer.concat(this.chunks).toString();
11+
constructor(
12+
onFlush: (chunks: Map<OutputMessage.Severity, string | undefined>) => void,
13+
flushTimeout: number = DEFAULT_FLUS_TIMEOUT_MS
14+
) {
15+
this.flush = () => {
16+
if (!Chunks.isEmpty(this.chunks)) {
17+
const chunks = Chunks.toString(this.chunks);
1018
this.clearChunks();
11-
12-
onFlush(chunkString);
19+
onFlush(chunks);
1320
}
14-
}, flushTimeout);
21+
};
22+
this.flushInterval = setInterval(this.flush, flushTimeout);
1523
}
1624

17-
public addChunk(chunk: Uint8Array): void {
18-
this.chunks.push(chunk);
25+
addChunk(
26+
chunk: Uint8Array,
27+
severity: OutputMessage.Severity = OutputMessage.Severity.Info
28+
): void {
29+
this.chunks.get(severity)?.push(chunk);
1930
}
2031

2132
private clearChunks(): void {
22-
this.chunks = [];
33+
Chunks.clear(this.chunks);
2334
}
2435

25-
public clearFlushInterval(): void {
26-
this.clearChunks();
27-
36+
dispose(): void {
37+
this.flush();
2838
clearInterval(this.flushInterval);
39+
this.clearChunks();
2940
this.flushInterval = undefined;
3041
}
3142
}
43+
44+
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
45+
namespace Chunks {
46+
export function create(): Chunks {
47+
return new Map([
48+
[OutputMessage.Severity.Error, []],
49+
[OutputMessage.Severity.Warning, []],
50+
[OutputMessage.Severity.Info, []],
51+
]);
52+
}
53+
export function clear(chunks: Chunks): Chunks {
54+
for (const chunk of chunks.values()) {
55+
chunk.length = 0;
56+
}
57+
return chunks;
58+
}
59+
export function isEmpty(chunks: Chunks): boolean {
60+
return ![...chunks.values()].some((chunk) => Boolean(chunk.length));
61+
}
62+
export function toString(
63+
chunks: Chunks
64+
): Map<OutputMessage.Severity, string | undefined> {
65+
return new Map(
66+
Array.from(chunks.entries()).map(([severity, buffers]) => [
67+
severity,
68+
buffers.length ? Buffer.concat(buffers).toString() : undefined,
69+
])
70+
);
71+
}
72+
}

‎i18n/en.json

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,24 @@
5656
"uploadRootCertificates": "Upload SSL Root Certificates",
5757
"uploadingCertificates": "Uploading certificates."
5858
},
59+
"cli-error-parser": {
60+
"byteError": "The 'BYTE' keyword is no longer supported.",
61+
"byteMessage": "As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead.",
62+
"clientError": "The Client class has been renamed EthernetClient.",
63+
"clientMessage": "As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.",
64+
"keyboardError": "'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?",
65+
"mouseError": "'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?",
66+
"receiveError": "Wire.receive() has been renamed Wire.read().",
67+
"receiveMessage": "As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.",
68+
"sendError": "Wire.send() has been renamed Wire.write().",
69+
"sendMessage": "As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.",
70+
"serverError": "The Server class has been renamed EthernetServer.",
71+
"serverMessage": "As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.",
72+
"spiError": "Please import the SPI library from the Sketch > Import Library menu.",
73+
"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.",
74+
"udpError": "The Udp class has been renamed EthernetUdp.",
75+
"udpMessage": "As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp."
76+
},
5977
"cloud": {
6078
"chooseSketchVisibility": "Choose visibility of your Sketch:",
6179
"cloudSketchbook": "Cloud Sketchbook",
@@ -120,6 +138,9 @@
120138
"fileAdded": "One file added to the sketch.",
121139
"replaceTitle": "Replace"
122140
},
141+
"coreContribution": {
142+
"copyError": "Copy error messages"
143+
},
123144
"debug": {
124145
"debugWithMessage": "Debug - {0}",
125146
"debuggingNotSupported": "Debugging is not supported by '{0}'",
@@ -136,7 +157,9 @@
136157
"decreaseFontSize": "Decrease Font Size",
137158
"decreaseIndent": "Decrease Indent",
138159
"increaseFontSize": "Increase Font Size",
139-
"increaseIndent": "Increase Indent"
160+
"increaseIndent": "Increase Indent",
161+
"nextError": "Next Error",
162+
"previousError": "Previous Error"
140163
},
141164
"electron": {
142165
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
@@ -229,12 +252,15 @@
229252
"board.certificates": "List of certificates that can be uploaded to boards",
230253
"browse": "Browse",
231254
"choose": "Choose",
255+
"cli.daemonDebug": "Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default.",
232256
"cloud.enabled": "True if the sketch sync functions are enabled. Defaults to true.",
233257
"cloud.pull.warn": "True if users should be warned before pulling a cloud sketch. Defaults to true.",
234258
"cloud.push.warn": "True if users should be warned before pushing a cloud sketch. Defaults to true.",
235259
"cloud.pushpublic.warn": "True if users should be warned before pushing a public sketch to the cloud. Defaults to true.",
236260
"cloud.sketchSyncEnpoint": "The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.",
237261
"compile": "compile",
262+
"compile.experimental": "True if the IDE should handle multiple compiler errors. False by default",
263+
"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}'.",
238264
"compile.verbose": "True for verbose compile output. False by default",
239265
"compile.warnings": "Tells gcc which warning level to use. It's 'None' by default",
240266
"compilerWarnings": "Compiler warnings",

0 commit comments

Comments
 (0)
Please sign in to comment.