mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-16 05:39:28 +00:00
Link compiler errors to editor.
Closes #118 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user