Skip to content

Commit 5226636

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
Link compiler errors to editor.
Closes #118 Signed-off-by: Akos Kitta <[email protected]>
1 parent 8b3f3c6 commit 5226636

File tree

9 files changed

+544
-273
lines changed

9 files changed

+544
-273
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/compiler-errors.ts

+333-179
Large diffs are not rendered by default.

Diff for: arduino-ide-extension/src/browser/contributions/format.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ 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';
5+
import { InoSelector } from '../selectors';
76
import { Contribution, URI } from './contribution';
87

98
@injectable()
@@ -40,7 +39,7 @@ export class Format
4039
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4140
_token: monaco.CancellationToken
4241
): Promise<monaco.languages.TextEdit[]> {
43-
const range = fullRange(model);
42+
const range = model.getFullModelRange();
4443
const text = await this.format(model, range, options);
4544
return [{ range, text }];
4645
}

Diff for: arduino-ide-extension/src/browser/ino-selectors.ts renamed to arduino-ide-extension/src/browser/selectors.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as monaco from '@theia/monaco-editor-core';
2+
import { OutputUri } from '@theia/output/lib/common/output-uri';
23
/**
34
* Exclusive "ino" document selector for monaco.
45
*/
@@ -11,3 +12,11 @@ function selectorOf(
1112
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
1213
}));
1314
}
15+
16+
/**
17+
* Selector for the `monaco` resource in the Arduino _Output_ channel.
18+
*/
19+
export const ArduinoOutputSelector: monaco.languages.LanguageSelector = {
20+
scheme: OutputUri.SCHEME,
21+
pattern: '**/Arduino',
22+
};

Diff for: arduino-ide-extension/src/browser/utils/monaco.ts

-8
This file was deleted.

Diff for: arduino-ide-extension/src/common/protocol/core-service.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { ApplicationError } from '@theia/core/lib/common/application-error';
2-
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
2+
import type {
3+
Location,
4+
Range,
5+
Position,
6+
} from '@theia/core/shared/vscode-languageserver-protocol';
37
import type {
48
BoardUserField,
59
Port,
@@ -15,11 +19,41 @@ export const CompilerWarningLiterals = [
1519
] as const;
1620
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
1721
export namespace CoreError {
18-
export interface ErrorLocation {
22+
export interface ErrorLocationRef {
1923
readonly message: string;
2024
readonly location: Location;
2125
readonly details?: string;
2226
}
27+
export namespace ErrorLocationRef {
28+
export function equals(
29+
left: ErrorLocationRef,
30+
right: ErrorLocationRef
31+
): boolean {
32+
return (
33+
left.message === right.message &&
34+
left.details === right.details &&
35+
equalsLocation(left.location, right.location)
36+
);
37+
}
38+
function equalsLocation(left: Location, right: Location): boolean {
39+
return left.uri === right.uri && equalsRange(left.range, right.range);
40+
}
41+
function equalsRange(left: Range, right: Range): boolean {
42+
return (
43+
equalsPosition(left.start, right.start) &&
44+
equalsPosition(left.end, right.end)
45+
);
46+
}
47+
function equalsPosition(left: Position, right: Position): boolean {
48+
return left.character === right.character && left.line === right.line;
49+
}
50+
}
51+
export interface ErrorLocation extends ErrorLocationRef {
52+
/**
53+
* The range of the error location source from the CLI output.
54+
*/
55+
readonly rangesInOutput: Range[]; // The same error might show up multiple times in the CLI output: https://github.com/arduino/arduino-cli/issues/1761
56+
}
2357
export const Codes = {
2458
Verify: 4001,
2559
Upload: 4002,

Diff for: arduino-ide-extension/src/node/cli-error-parser.ts

+110-45
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@ import {
55
Range,
66
Position,
77
} from '@theia/core/shared/vscode-languageserver-protocol';
8-
import type { CoreError } from '../common/protocol';
8+
import { CoreError } from '../common/protocol';
99
import { Sketch } from '../common/protocol/sketches-service';
1010

11-
export interface ErrorSource {
11+
export interface OutputSource {
1212
readonly content: string | ReadonlyArray<Uint8Array>;
1313
readonly sketch?: Sketch;
1414
}
15-
16-
export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
17-
const { content, sketch } = source;
18-
const err =
19-
typeof content === 'string'
15+
export namespace OutputSource {
16+
export function content(source: OutputSource): string {
17+
const { content } = source;
18+
return typeof content === 'string'
2019
? content
2120
: Buffer.concat(content).toString('utf8');
21+
}
22+
}
23+
24+
export function tryParseError(source: OutputSource): CoreError.ErrorLocation[] {
25+
const { sketch } = source;
26+
const content = OutputSource.content(source);
2227
if (sketch) {
23-
return tryParse(err)
28+
return tryParse(content)
2429
.map(remapErrorMessages)
2530
.filter(isLocationInSketch(sketch))
26-
.map(toErrorInfo);
31+
.map(toErrorInfo)
32+
.reduce((acc, curr) => {
33+
const existingRef = acc.find((candidate) =>
34+
CoreError.ErrorLocationRef.equals(candidate, curr)
35+
);
36+
if (existingRef) {
37+
existingRef.rangesInOutput.push(...curr.rangesInOutput);
38+
} else {
39+
acc.push(curr);
40+
}
41+
return acc;
42+
}, [] as CoreError.ErrorLocation[]);
2743
}
2844
return [];
2945
}
@@ -35,6 +51,7 @@ interface ParseResult {
3551
readonly errorPrefix: string;
3652
readonly error: string;
3753
readonly message?: string;
54+
readonly rangeInOutput?: Range | undefined;
3855
}
3956
namespace ParseResult {
4057
export function keyOf(result: ParseResult): string {
@@ -64,6 +81,7 @@ function toErrorInfo({
6481
path,
6582
line,
6683
column,
84+
rangeInOutput,
6785
}: ParseResult): CoreError.ErrorLocation {
6886
return {
6987
message: error,
@@ -72,6 +90,7 @@ function toErrorInfo({
7290
uri: FileUri.create(path).toString(),
7391
range: range(line, column),
7492
},
93+
rangesInOutput: rangeInOutput ? [rangeInOutput] : [],
7594
};
7695
}
7796

@@ -86,48 +105,50 @@ function range(line: number, column?: number): Range {
86105
};
87106
}
88107

89-
export function tryParse(raw: string): ParseResult[] {
108+
function tryParse(content: string): ParseResult[] {
90109
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
91110
const re = new RegExp(
92111
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
93112
'gm'
94113
);
95-
return [
96-
...new Map(
97-
Array.from(raw.matchAll(re) ?? [])
98-
.map((match) => {
99-
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
100-
(match) => (match ? match.trim() : match)
114+
return Array.from(content.matchAll(re) ?? [])
115+
.map((match) => {
116+
const { index: start } = match;
117+
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
118+
(match) => (match ? match.trim() : match)
119+
);
120+
const line = Number.parseInt(rawLine, 10);
121+
if (!Number.isInteger(line)) {
122+
console.warn(
123+
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
124+
);
125+
return undefined;
126+
}
127+
let column: number | undefined = undefined;
128+
if (rawColumn) {
129+
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
130+
column = Number.parseInt(normalizedRawColumn, 10);
131+
if (!Number.isInteger(column)) {
132+
console.warn(
133+
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
101134
);
102-
const line = Number.parseInt(rawLine, 10);
103-
if (!Number.isInteger(line)) {
104-
console.warn(
105-
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
106-
);
107-
return undefined;
108-
}
109-
let column: number | undefined = undefined;
110-
if (rawColumn) {
111-
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
112-
column = Number.parseInt(normalizedRawColumn, 10);
113-
if (!Number.isInteger(column)) {
114-
console.warn(
115-
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
116-
);
117-
}
118-
}
119-
return {
120-
path,
121-
line,
122-
column,
123-
errorPrefix,
124-
error,
125-
};
126-
})
127-
.filter(notEmpty)
128-
.map((result) => [ParseResult.keyOf(result), result])
129-
).values(),
130-
];
135+
}
136+
}
137+
const rangeInOutput = findRangeInOutput(
138+
start,
139+
{ path, rawLine, rawColumn },
140+
content
141+
);
142+
return {
143+
path,
144+
line,
145+
column,
146+
errorPrefix,
147+
error,
148+
rangeInOutput,
149+
};
150+
})
151+
.filter(notEmpty);
131152
}
132153

133154
/**
@@ -161,3 +182,47 @@ const KnownErrors: Record<string, { error: string; message?: string }> = {
161182
),
162183
},
163184
};
185+
186+
function findRangeInOutput(
187+
startIndex: number | undefined,
188+
groups: { path: string; rawLine: string; rawColumn: string | null },
189+
content: string // TODO? lines: string[]? can this code break line on `\n`? const lines = content.split(/\r?\n/) ?? [];
190+
): Range | undefined {
191+
if (startIndex === undefined) {
192+
return undefined;
193+
}
194+
// /path/to/location/Sketch/Sketch.ino:36:42
195+
const offset =
196+
groups.path.length +
197+
':'.length +
198+
groups.rawLine.length +
199+
(groups.rawColumn ? groups.rawColumn.length : 0);
200+
const start = toPosition(startIndex, content);
201+
if (!start) {
202+
return undefined;
203+
}
204+
const end = toPosition(startIndex + offset, content);
205+
if (!end) {
206+
return undefined;
207+
}
208+
return { start, end };
209+
}
210+
211+
function toPosition(offset: number, content: string): Position | undefined {
212+
let line = 0;
213+
let character = 0;
214+
const length = content.length;
215+
for (let i = 0; i < length; i++) {
216+
const c = content.charAt(i);
217+
if (i === offset) {
218+
return { line, character };
219+
}
220+
if (c === '\n') {
221+
line++;
222+
character = 0;
223+
} else {
224+
character++;
225+
}
226+
}
227+
return undefined;
228+
}

Diff for: arduino-ide-extension/src/node/core-service-impl.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
8686
reject(error);
8787
} else {
8888
const compilerErrors = tryParseError({
89-
content: handler.stderr,
89+
content: handler.content,
9090
sketch: options.sketch,
9191
});
9292
const message = nls.localize(
@@ -224,7 +224,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
224224
errorCtor(
225225
message,
226226
tryParseError({
227-
content: handler.stderr,
227+
content: handler.content,
228228
sketch: options.sketch,
229229
})
230230
)
@@ -291,7 +291,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
291291
'Error while burning the bootloader: {0}',
292292
error.details
293293
),
294-
tryParseError({ content: handler.stderr })
294+
tryParseError({ content: handler.content })
295295
)
296296
);
297297
}
@@ -342,19 +342,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
342342
// TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers?
343343
...handlers: ((response: R) => void)[]
344344
): Disposable & {
345-
stderr: Buffer[];
345+
content: Buffer[];
346346
onData: (response: R) => void;
347347
} {
348-
const stderr: Buffer[] = [];
348+
const content: Buffer[] = [];
349349
const buffer = new AutoFlushingBuffer((chunks) => {
350-
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
351-
if (chunk) {
352-
this.sendResponse(chunk, severity);
353-
}
354-
});
350+
chunks.forEach(([severity, chunk]) => this.sendResponse(chunk, severity));
355351
});
356352
const onData = StreamingResponse.createOnDataHandler({
357-
stderr,
353+
content,
358354
onData: (out, err) => {
359355
buffer.addChunk(out);
360356
buffer.addChunk(err, OutputMessage.Severity.Error);
@@ -363,7 +359,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
363359
});
364360
return {
365361
dispose: () => buffer.dispose(),
366-
stderr,
362+
content,
367363
onData,
368364
};
369365
}
@@ -432,14 +428,19 @@ namespace StreamingResponse {
432428
): (response: R) => void {
433429
return (response: R) => {
434430
const out = response.getOutStream_asU8();
431+
if (out.length) {
432+
options.content.push(out);
433+
}
435434
const err = response.getErrStream_asU8();
436-
options.stderr.push(err);
435+
if (err.length) {
436+
options.content.push(err);
437+
}
437438
options.onData(out, err);
438439
options.handlers?.forEach((handler) => handler(response));
439440
};
440441
}
441442
export interface Options<R extends StreamingResponse> {
442-
readonly stderr: Uint8Array[];
443+
readonly content: Uint8Array[];
443444
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
444445
/**
445446
* Additional request handlers.

0 commit comments

Comments
 (0)