Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5226636

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
Link compiler errors to editor.
Closes #118 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 8b3f3c6 commit 5226636

File tree

9 files changed

+544
-273
lines changed

9 files changed

+544
-273
lines changed

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

Lines changed: 333 additions & 179 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 3 deletions
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
}

‎arduino-ide-extension/src/browser/ino-selectors.ts‎ renamed to ‎arduino-ide-extension/src/browser/selectors.ts‎

Lines changed: 9 additions & 0 deletions
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+
};

‎arduino-ide-extension/src/browser/utils/monaco.ts‎

Lines changed: 0 additions & 8 deletions
This file was deleted.

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

Lines changed: 36 additions & 2 deletions
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,

‎arduino-ide-extension/src/node/cli-error-parser.ts‎

Lines changed: 110 additions & 45 deletions
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-
exportfunction 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+
}

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

Lines changed: 15 additions & 14 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /