Link compiler errors to editor.

Closes #118

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-07-14 10:41:17 +02:00
committed by Akos Kitta
parent 8b3f3c69fc
commit 5226636fed
9 changed files with 546 additions and 275 deletions

View File

@@ -5,25 +5,41 @@ import {
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import type { CoreError } from '../common/protocol';
import { CoreError } from '../common/protocol';
import { Sketch } from '../common/protocol/sketches-service';
export interface ErrorSource {
export interface OutputSource {
readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch;
}
export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
const { content, sketch } = source;
const err =
typeof content === 'string'
export namespace OutputSource {
export function content(source: OutputSource): string {
const { content } = source;
return typeof content === 'string'
? content
: Buffer.concat(content).toString('utf8');
}
}
export function tryParseError(source: OutputSource): CoreError.ErrorLocation[] {
const { sketch } = source;
const content = OutputSource.content(source);
if (sketch) {
return tryParse(err)
return tryParse(content)
.map(remapErrorMessages)
.filter(isLocationInSketch(sketch))
.map(toErrorInfo);
.map(toErrorInfo)
.reduce((acc, curr) => {
const existingRef = acc.find((candidate) =>
CoreError.ErrorLocationRef.equals(candidate, curr)
);
if (existingRef) {
existingRef.rangesInOutput.push(...curr.rangesInOutput);
} else {
acc.push(curr);
}
return acc;
}, [] as CoreError.ErrorLocation[]);
}
return [];
}
@@ -35,6 +51,7 @@ interface ParseResult {
readonly errorPrefix: string;
readonly error: string;
readonly message?: string;
readonly rangeInOutput?: Range | undefined;
}
namespace ParseResult {
export function keyOf(result: ParseResult): string {
@@ -64,6 +81,7 @@ function toErrorInfo({
path,
line,
column,
rangeInOutput,
}: ParseResult): CoreError.ErrorLocation {
return {
message: error,
@@ -72,6 +90,7 @@ function toErrorInfo({
uri: FileUri.create(path).toString(),
range: range(line, column),
},
rangesInOutput: rangeInOutput ? [rangeInOutput] : [],
};
}
@@ -86,48 +105,50 @@ function range(line: number, column?: number): Range {
};
}
export function tryParse(raw: string): ParseResult[] {
function tryParse(content: 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)
return Array.from(content.matchAll(re) ?? [])
.map((match) => {
const { index: start } = 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}>.`
);
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(),
];
}
}
const rangeInOutput = findRangeInOutput(
start,
{ path, rawLine, rawColumn },
content
);
return {
path,
line,
column,
errorPrefix,
error,
rangeInOutput,
};
})
.filter(notEmpty);
}
/**
@@ -161,3 +182,47 @@ const KnownErrors: Record<string, { error: string; message?: string }> = {
),
},
};
function findRangeInOutput(
startIndex: number | undefined,
groups: { path: string; rawLine: string; rawColumn: string | null },
content: string // TODO? lines: string[]? can this code break line on `\n`? const lines = content.split(/\r?\n/) ?? [];
): Range | undefined {
if (startIndex === undefined) {
return undefined;
}
// /path/to/location/Sketch/Sketch.ino:36:42
const offset =
groups.path.length +
':'.length +
groups.rawLine.length +
(groups.rawColumn ? groups.rawColumn.length : 0);
const start = toPosition(startIndex, content);
if (!start) {
return undefined;
}
const end = toPosition(startIndex + offset, content);
if (!end) {
return undefined;
}
return { start, end };
}
function toPosition(offset: number, content: string): Position | undefined {
let line = 0;
let character = 0;
const length = content.length;
for (let i = 0; i < length; i++) {
const c = content.charAt(i);
if (i === offset) {
return { line, character };
}
if (c === '\n') {
line++;
character = 0;
} else {
character++;
}
}
return undefined;
}

View File

@@ -86,7 +86,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
reject(error);
} else {
const compilerErrors = tryParseError({
content: handler.stderr,
content: handler.content,
sketch: options.sketch,
});
const message = nls.localize(
@@ -224,7 +224,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
errorCtor(
message,
tryParseError({
content: handler.stderr,
content: handler.content,
sketch: options.sketch,
})
)
@@ -291,7 +291,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
'Error while burning the bootloader: {0}',
error.details
),
tryParseError({ content: handler.stderr })
tryParseError({ content: handler.content })
)
);
}
@@ -342,19 +342,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
// TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers?
...handlers: ((response: R) => void)[]
): Disposable & {
stderr: Buffer[];
content: Buffer[];
onData: (response: R) => void;
} {
const stderr: Buffer[] = [];
const content: Buffer[] = [];
const buffer = new AutoFlushingBuffer((chunks) => {
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
if (chunk) {
this.sendResponse(chunk, severity);
}
});
chunks.forEach(([severity, chunk]) => this.sendResponse(chunk, severity));
});
const onData = StreamingResponse.createOnDataHandler({
stderr,
content,
onData: (out, err) => {
buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error);
@@ -363,7 +359,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
});
return {
dispose: () => buffer.dispose(),
stderr,
content,
onData,
};
}
@@ -432,14 +428,19 @@ namespace StreamingResponse {
): (response: R) => void {
return (response: R) => {
const out = response.getOutStream_asU8();
if (out.length) {
options.content.push(out);
}
const err = response.getErrStream_asU8();
options.stderr.push(err);
if (err.length) {
options.content.push(err);
}
options.onData(out, err);
options.handlers?.forEach((handler) => handler(response));
};
}
export interface Options<R extends StreamingResponse> {
readonly stderr: Uint8Array[];
readonly content: Uint8Array[];
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
/**
* Additional request handlers.

View File

@@ -3,13 +3,13 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { OutputMessage } from '../../common/protocol';
export class AutoFlushingBuffer implements Disposable {
private readonly chunks = Chunks.create();
private readonly chunks: Array<[OutputMessage.Severity, Uint8Array]> = [];
private readonly toDispose;
private timer?: NodeJS.Timeout;
private disposed = false;
constructor(
onFlush: (chunks: Map<OutputMessage.Severity, string | undefined>) => void,
onFlush: (chunks: Array<[OutputMessage.Severity, string]>) => void,
taskTimeout: number = AutoFlushingBuffer.DEFAULT_FLUSH_TIMEOUT_MS
) {
const task = () => {
@@ -34,7 +34,9 @@ export class AutoFlushingBuffer implements Disposable {
chunk: Uint8Array,
severity: OutputMessage.Severity = OutputMessage.Severity.Info
): void {
this.chunks.get(severity)?.push(chunk);
if (chunk.length) {
this.chunks.push([severity, chunk]);
}
}
dispose(): void {
@@ -49,19 +51,10 @@ export namespace AutoFlushingBuffer {
export const DEFAULT_FLUSH_TIMEOUT_MS = 32;
}
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
type Chunks = Array<[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;
}
chunks.length = 0;
return chunks;
}
export function isEmpty(chunks: Chunks): boolean {
@@ -69,12 +62,35 @@ namespace Chunks {
}
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,
])
);
): Array<[OutputMessage.Severity, string]> {
const result: Array<[OutputMessage.Severity, string]> = [];
let current:
| { severity: OutputMessage.Severity; buffers: Uint8Array[] }
| undefined = undefined;
const appendToResult = () => {
if (current && current.buffers) {
result.push([
current.severity,
Buffer.concat(current.buffers).toString('utf-8'),
]);
}
};
for (const [severity, buffer] of chunks) {
if (!buffer.length) {
continue;
}
if (!current) {
current = { severity, buffers: [buffer] };
} else {
if (current.severity === severity) {
current.buffers.push(buffer);
} else {
appendToResult();
current = { severity, buffers: [buffer] };
}
}
}
appendToResult();
return result;
}
}