mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-25 20:26:38 +00:00
Link compiler errors to editor.
Closes #118 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
8b3f3c69fc
commit
5226636fed
@ -4,11 +4,13 @@ import {
|
|||||||
Disposable,
|
Disposable,
|
||||||
DisposableCollection,
|
DisposableCollection,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
MaybeArray,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
nls,
|
nls,
|
||||||
notEmpty,
|
notEmpty,
|
||||||
} from '@theia/core';
|
} from '@theia/core';
|
||||||
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||||
|
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import {
|
import {
|
||||||
@ -28,14 +30,15 @@ import * as monaco from '@theia/monaco-editor-core';
|
|||||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||||
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||||
|
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||||
import { CoreError } from '../../common/protocol/core-service';
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
import { ErrorRevealStrategy } from '../arduino-preferences';
|
import { ErrorRevealStrategy } from '../arduino-preferences';
|
||||||
import { InoSelector } from '../ino-selectors';
|
import { ArduinoOutputSelector, InoSelector } from '../selectors';
|
||||||
import { fullRange } from '../utils/monaco';
|
|
||||||
import { Contribution } from './contribution';
|
import { Contribution } from './contribution';
|
||||||
import { CoreErrorHandler } from './core-error-handler';
|
import { CoreErrorHandler } from './core-error-handler';
|
||||||
|
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||||
|
|
||||||
interface ErrorDecoration {
|
interface ErrorDecorationRef {
|
||||||
/**
|
/**
|
||||||
* This is the unique ID of the decoration given by `monaco`.
|
* This is the unique ID of the decoration given by `monaco`.
|
||||||
*/
|
*/
|
||||||
@ -45,72 +48,89 @@ interface ErrorDecoration {
|
|||||||
*/
|
*/
|
||||||
readonly uri: string;
|
readonly uri: string;
|
||||||
}
|
}
|
||||||
namespace ErrorDecoration {
|
export namespace ErrorDecorationRef {
|
||||||
export function rangeOf(
|
export function is(arg: unknown): arg is ErrorDecorationRef {
|
||||||
{ id, uri }: ErrorDecoration,
|
if (typeof arg === 'object') {
|
||||||
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
): Promise<monaco.Range | undefined>;
|
const object = arg as any;
|
||||||
export function rangeOf(
|
return (
|
||||||
{ id, uri }: ErrorDecoration,
|
'uri' in object &&
|
||||||
editorProvider: MonacoEditor
|
typeof object['uri'] === 'string' &&
|
||||||
): monaco.Range | undefined;
|
'id' in object &&
|
||||||
export function rangeOf(
|
typeof object['id'] === 'string'
|
||||||
{ id, uri }: ErrorDecoration,
|
);
|
||||||
editorProvider:
|
|
||||||
| ((uri: string) => Promise<MonacoEditor | undefined>)
|
|
||||||
| MonacoEditor
|
|
||||||
): MaybePromise<monaco.Range | undefined> {
|
|
||||||
if (editorProvider instanceof MonacoEditor) {
|
|
||||||
const control = editorProvider.getControl();
|
|
||||||
const model = control.getModel();
|
|
||||||
if (model) {
|
|
||||||
return control
|
|
||||||
.getDecorationsInRange(fullRange(model))
|
|
||||||
?.find(({ id: candidateId }) => id === candidateId)?.range;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
return editorProvider(uri).then((editor) => {
|
return false;
|
||||||
if (editor) {
|
|
||||||
return rangeOf({ id, uri }, editor);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export async function rangeOf(
|
|
||||||
// { id, uri }: ErrorDecoration,
|
|
||||||
// editorProvider:
|
|
||||||
// | ((uri: string) => Promise<MonacoEditor | undefined>)
|
|
||||||
// | MonacoEditor
|
|
||||||
// ): Promise<monaco.Range | undefined> {
|
|
||||||
// const editor =
|
|
||||||
// editorProvider instanceof MonacoEditor
|
|
||||||
// ? editorProvider
|
|
||||||
// : await editorProvider(uri);
|
|
||||||
// if (editor) {
|
|
||||||
// const control = editor.getControl();
|
|
||||||
// const model = control.getModel();
|
|
||||||
// if (model) {
|
|
||||||
// return control
|
|
||||||
// .getDecorationsInRange(fullRange(model))
|
|
||||||
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return undefined;
|
|
||||||
// }
|
|
||||||
export function sameAs(
|
export function sameAs(
|
||||||
left: ErrorDecoration,
|
left: ErrorDecorationRef,
|
||||||
right: ErrorDecoration
|
right: ErrorDecorationRef
|
||||||
): boolean {
|
): boolean {
|
||||||
return left.id === right.id && left.uri === right.uri;
|
return left.id === right.id && left.uri === right.uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ErrorDecoration extends ErrorDecorationRef {
|
||||||
|
/**
|
||||||
|
* The range of the error location the error in the compiler output from the CLI.
|
||||||
|
*/
|
||||||
|
readonly rangesInOutput: monaco.Range[];
|
||||||
|
}
|
||||||
|
namespace ErrorDecoration {
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration
|
||||||
|
): monaco.Range | undefined;
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration[]
|
||||||
|
): (monaco.Range | undefined)[];
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration | ErrorDecoration[]
|
||||||
|
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
|
||||||
|
if (editorOrModel) {
|
||||||
|
const allDecorations = getAllDecorations(editorOrModel);
|
||||||
|
if (allDecorations) {
|
||||||
|
if (Array.isArray(decorations)) {
|
||||||
|
return decorations.map(({ id: decorationId }) =>
|
||||||
|
findRangeOf(decorationId, allDecorations)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return findRangeOf(decorations.id, allDecorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(decorations)
|
||||||
|
? decorations.map(() => undefined)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
function findRangeOf(
|
||||||
|
decorationId: string,
|
||||||
|
allDecorations: { id: string; range?: monaco.Range }[]
|
||||||
|
): monaco.Range | undefined {
|
||||||
|
return allDecorations.find(
|
||||||
|
({ id: candidateId }) => candidateId === decorationId
|
||||||
|
)?.range;
|
||||||
|
}
|
||||||
|
function getAllDecorations(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel
|
||||||
|
): { id: string; range?: monaco.Range }[] {
|
||||||
|
if (editorOrModel instanceof MonacoEditor) {
|
||||||
|
const model = editorOrModel.getControl().getModel();
|
||||||
|
if (!model) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return model.getAllDecorations();
|
||||||
|
}
|
||||||
|
return editorOrModel.getAllDecorations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CompilerErrors
|
export class CompilerErrors
|
||||||
extends Contribution
|
extends Contribution
|
||||||
implements monaco.languages.CodeLensProvider
|
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
||||||
{
|
{
|
||||||
@inject(EditorManager)
|
@inject(EditorManager)
|
||||||
private readonly editorManager: EditorManager;
|
private readonly editorManager: EditorManager;
|
||||||
@ -119,11 +139,14 @@ export class CompilerErrors
|
|||||||
private readonly p2m: ProtocolToMonacoConverter;
|
private readonly p2m: ProtocolToMonacoConverter;
|
||||||
|
|
||||||
@inject(MonacoToProtocolConverter)
|
@inject(MonacoToProtocolConverter)
|
||||||
private readonly mp2: MonacoToProtocolConverter;
|
private readonly m2p: MonacoToProtocolConverter;
|
||||||
|
|
||||||
@inject(CoreErrorHandler)
|
@inject(CoreErrorHandler)
|
||||||
private readonly coreErrorHandler: CoreErrorHandler;
|
private readonly coreErrorHandler: CoreErrorHandler;
|
||||||
|
|
||||||
|
private revealStrategy = ErrorRevealStrategy.Default;
|
||||||
|
private experimental = false;
|
||||||
|
|
||||||
private readonly errors: ErrorDecoration[] = [];
|
private readonly errors: ErrorDecoration[] = [];
|
||||||
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
||||||
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
||||||
@ -131,8 +154,8 @@ export class CompilerErrors
|
|||||||
this.currentErrorDidChangEmitter.event;
|
this.currentErrorDidChangEmitter.event;
|
||||||
private readonly toDisposeOnCompilerErrorDidChange =
|
private readonly toDisposeOnCompilerErrorDidChange =
|
||||||
new DisposableCollection();
|
new DisposableCollection();
|
||||||
|
|
||||||
private shell: ApplicationShell | undefined;
|
private shell: ApplicationShell | undefined;
|
||||||
private revealStrategy = ErrorRevealStrategy.Default;
|
|
||||||
private currentError: ErrorDecoration | undefined;
|
private currentError: ErrorDecoration | undefined;
|
||||||
private get currentErrorIndex(): number {
|
private get currentErrorIndex(): number {
|
||||||
const current = this.currentError;
|
const current = this.currentError;
|
||||||
@ -140,46 +163,75 @@ export class CompilerErrors
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return this.errors.findIndex((error) =>
|
return this.errors.findIndex((error) =>
|
||||||
ErrorDecoration.sameAs(error, current)
|
ErrorDecorationRef.sameAs(error, current)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStart(app: FrontendApplication): void {
|
override onStart(app: FrontendApplication): void {
|
||||||
this.shell = app.shell;
|
this.shell = app.shell;
|
||||||
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||||
|
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
|
||||||
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||||
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
this.handleCompilerErrorsDidChange(errors)
|
||||||
);
|
);
|
||||||
this.onCurrentErrorDidChange(async (error) => {
|
this.onCurrentErrorDidChange(async (error) => {
|
||||||
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
const monacoEditor = await this.monacoEditor(error.uri);
|
||||||
this.monacoEditor(uri)
|
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||||
);
|
if (!monacoRange) {
|
||||||
if (!range) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
'compiler-errors',
|
'compiler-errors',
|
||||||
`Could not find range of decoration: ${error.id}`
|
`Could not find range of decoration: ${error.id}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const range = this.m2p.asRange(monacoRange);
|
||||||
const editor = await this.revealLocationInEditor({
|
const editor = await this.revealLocationInEditor({
|
||||||
uri: error.uri,
|
uri: error.uri,
|
||||||
range: this.mp2.asRange(range),
|
range,
|
||||||
});
|
});
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'compiler-errors',
|
'compiler-errors',
|
||||||
`Failed to mark error ${error.id} as the current one.`
|
`Failed to mark error ${error.id} as the current one.`
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (monacoEditor) {
|
||||||
|
monacoEditor.cursor = range.start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): MaybePromise<void> {
|
||||||
this.preferences.ready.then(() => {
|
this.preferences.ready.then(() => {
|
||||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
this.experimental = Boolean(
|
||||||
if (preferenceName === 'arduino.compile.revealRange') {
|
this.preferences['arduino.compile.experimental']
|
||||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
);
|
||||||
? newValue
|
const strategy = this.preferences['arduino.compile.revealRange'];
|
||||||
: ErrorRevealStrategy.Default;
|
this.revealStrategy = ErrorRevealStrategy.is(strategy)
|
||||||
|
? strategy
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
this.preferences.onPreferenceChanged(
|
||||||
|
({ preferenceName, newValue, oldValue }) => {
|
||||||
|
if (newValue === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (preferenceName) {
|
||||||
|
case 'arduino.compile.revealRange': {
|
||||||
|
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||||
|
? newValue
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'arduino.compile.experimental': {
|
||||||
|
this.experimental = Boolean(newValue);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,9 +248,13 @@ export class CompilerErrors
|
|||||||
}
|
}
|
||||||
const nextError =
|
const nextError =
|
||||||
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
||||||
this.markAsCurrentError(nextError);
|
return this.markAsCurrentError(nextError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
isEnabled: () =>
|
||||||
|
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||||
});
|
});
|
||||||
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
||||||
execute: () => {
|
execute: () => {
|
||||||
@ -212,9 +268,24 @@ export class CompilerErrors
|
|||||||
}
|
}
|
||||||
const previousError =
|
const previousError =
|
||||||
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
||||||
this.markAsCurrentError(previousError);
|
return this.markAsCurrentError(previousError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
isEnabled: () =>
|
||||||
|
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
|
||||||
|
execute: (arg: unknown) => {
|
||||||
|
if (ErrorDecorationRef.is(arg)) {
|
||||||
|
return this.markAsCurrentError(
|
||||||
|
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
|
||||||
|
{ forceReselect: true, reveal: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.errors.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,13 +300,13 @@ export class CompilerErrors
|
|||||||
): Promise<monaco.languages.CodeLensList> {
|
): Promise<monaco.languages.CodeLensList> {
|
||||||
const lenses: monaco.languages.CodeLens[] = [];
|
const lenses: monaco.languages.CodeLens[] = [];
|
||||||
if (
|
if (
|
||||||
|
this.experimental &&
|
||||||
this.currentError &&
|
this.currentError &&
|
||||||
this.currentError.uri === model.uri.toString() &&
|
this.currentError.uri === model.uri.toString() &&
|
||||||
this.errors.length > 1
|
this.errors.length > 1
|
||||||
) {
|
) {
|
||||||
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
const monacoEditor = await this.monacoEditor(model.uri);
|
||||||
this.monacoEditor(uri)
|
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
||||||
);
|
|
||||||
if (range) {
|
if (range) {
|
||||||
lenses.push(
|
lenses.push(
|
||||||
{
|
{
|
||||||
@ -268,14 +339,81 @@ export class CompilerErrors
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async provideLinks(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.ILinksList> {
|
||||||
|
const links: monaco.languages.ILink[] = [];
|
||||||
|
if (
|
||||||
|
model.uri.scheme === OutputUri.SCHEME &&
|
||||||
|
model.uri.path === '/Arduino'
|
||||||
|
) {
|
||||||
|
links.push(
|
||||||
|
...this.errors
|
||||||
|
.filter((decoration) => !!decoration.rangesInOutput.length)
|
||||||
|
.map(({ rangesInOutput, id, uri }) =>
|
||||||
|
rangesInOutput.map(
|
||||||
|
(range) =>
|
||||||
|
<monaco.languages.ILink>{
|
||||||
|
range,
|
||||||
|
url: monaco.Uri.parse(`command://`).with({
|
||||||
|
query: JSON.stringify({ id, uri }),
|
||||||
|
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
|
||||||
|
}),
|
||||||
|
tooltip: nls.localize(
|
||||||
|
'arduino/editor/revealError',
|
||||||
|
'Reveal Error'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('unexpected URI: ' + model.uri.toString());
|
||||||
|
}
|
||||||
|
return { links };
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveLink(
|
||||||
|
link: monaco.languages.ILink,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.ILink | undefined> {
|
||||||
|
if (!this.experimental) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { url } = link;
|
||||||
|
if (url) {
|
||||||
|
const candidateUri = new URI(
|
||||||
|
typeof url === 'string' ? url : url.toString()
|
||||||
|
);
|
||||||
|
const candidateId = candidateUri.path.toString();
|
||||||
|
const error = this.errors.find((error) => error.id === candidateId);
|
||||||
|
if (error) {
|
||||||
|
const monacoEditor = await this.monacoEditor(error.uri);
|
||||||
|
const range = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||||
|
if (range) {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
url: monaco.Uri.parse(error.uri),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private async handleCompilerErrorsDidChange(
|
private async handleCompilerErrorsDidChange(
|
||||||
errors: CoreError.ErrorLocation[]
|
errors: CoreError.ErrorLocation[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.toDisposeOnCompilerErrorDidChange.dispose();
|
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||||
const compilerErrorsPerResource = this.groupByResource(
|
const groupedErrors = this.groupBy(
|
||||||
await this.filter(errors)
|
errors,
|
||||||
|
(error: CoreError.ErrorLocation) => error.location.uri
|
||||||
);
|
);
|
||||||
const decorations = await this.decorateEditors(compilerErrorsPerResource);
|
const decorations = await this.decorateEditors(groupedErrors);
|
||||||
this.errors.push(...decorations.errors);
|
this.errors.push(...decorations.errors);
|
||||||
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||||
Disposable.create(() => (this.errors.length = 0)),
|
Disposable.create(() => (this.errors.length = 0)),
|
||||||
@ -283,17 +421,17 @@ export class CompilerErrors
|
|||||||
...(await Promise.all([
|
...(await Promise.all([
|
||||||
decorations.dispose,
|
decorations.dispose,
|
||||||
this.trackEditors(
|
this.trackEditors(
|
||||||
compilerErrorsPerResource,
|
groupedErrors,
|
||||||
(editor) =>
|
(editor) =>
|
||||||
editor.editor.onSelectionChanged((selection) =>
|
editor.onSelectionChanged((selection) =>
|
||||||
this.handleSelectionChange(editor, selection)
|
this.handleSelectionChange(editor, selection)
|
||||||
),
|
),
|
||||||
(editor) =>
|
(editor) =>
|
||||||
editor.onDidDispose(() =>
|
editor.onDispose(() =>
|
||||||
this.handleEditorDidDispose(editor.editor.uri.toString())
|
this.handleEditorDidDispose(editor.uri.toString())
|
||||||
),
|
),
|
||||||
(editor) =>
|
(editor) =>
|
||||||
editor.editor.onDocumentContentChanged((event) =>
|
editor.onDocumentContentChanged((event) =>
|
||||||
this.handleDocumentContentChange(editor, event)
|
this.handleDocumentContentChange(editor, event)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -301,24 +439,13 @@ export class CompilerErrors
|
|||||||
]);
|
]);
|
||||||
const currentError = this.errors[0];
|
const currentError = this.errors[0];
|
||||||
if (currentError) {
|
if (currentError) {
|
||||||
await this.markAsCurrentError(currentError);
|
await this.markAsCurrentError(currentError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async filter(
|
|
||||||
errors: CoreError.ErrorLocation[]
|
|
||||||
): Promise<CoreError.ErrorLocation[]> {
|
|
||||||
if (!errors.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
await this.preferences.ready;
|
|
||||||
if (this.preferences['arduino.compile.experimental']) {
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
// Always shows maximum one error; hence the code lens navigation is unavailable.
|
|
||||||
return [errors[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async decorateEditors(
|
private async decorateEditors(
|
||||||
errors: Map<string, CoreError.ErrorLocation[]>
|
errors: Map<string, CoreError.ErrorLocation[]>
|
||||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
@ -342,11 +469,11 @@ export class CompilerErrors
|
|||||||
uri: string,
|
uri: string,
|
||||||
errors: CoreError.ErrorLocation[]
|
errors: CoreError.ErrorLocation[]
|
||||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
const editor = await this.monacoEditor(uri);
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return { dispose: Disposable.NULL, errors: [] };
|
return { dispose: Disposable.NULL, errors: [] };
|
||||||
}
|
}
|
||||||
const oldDecorations = editor.editor.deltaDecorations({
|
const oldDecorations = editor.deltaDecorations({
|
||||||
oldDecorations: [],
|
oldDecorations: [],
|
||||||
newDecorations: errors.map((error) =>
|
newDecorations: errors.map((error) =>
|
||||||
this.compilerErrorDecoration(error.location.range)
|
this.compilerErrorDecoration(error.location.range)
|
||||||
@ -355,13 +482,19 @@ export class CompilerErrors
|
|||||||
return {
|
return {
|
||||||
dispose: Disposable.create(() => {
|
dispose: Disposable.create(() => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
editor.editor.deltaDecorations({
|
editor.deltaDecorations({
|
||||||
oldDecorations,
|
oldDecorations,
|
||||||
newDecorations: [],
|
newDecorations: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
errors: oldDecorations.map((id) => ({ id, uri })),
|
errors: oldDecorations.map((id, index) => ({
|
||||||
|
id,
|
||||||
|
uri,
|
||||||
|
rangesInOutput: errors[index].rangesInOutput.map((range) =>
|
||||||
|
this.p2m.asRange(range)
|
||||||
|
),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +504,7 @@ export class CompilerErrors
|
|||||||
options: {
|
options: {
|
||||||
isWholeLine: true,
|
isWholeLine: true,
|
||||||
className: 'compiler-error',
|
className: 'compiler-error',
|
||||||
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -379,11 +512,10 @@ export class CompilerErrors
|
|||||||
/**
|
/**
|
||||||
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
||||||
*/
|
*/
|
||||||
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
private handleSelectionChange(
|
||||||
const monacoEditor = this.monacoEditor(editor);
|
monacoEditor: MonacoEditor,
|
||||||
if (!monacoEditor) {
|
selection: Range
|
||||||
return;
|
): void {
|
||||||
}
|
|
||||||
const uri = monacoEditor.uri.toString();
|
const uri = monacoEditor.uri.toString();
|
||||||
const monacoSelection = this.p2m.asRange(selection);
|
const monacoSelection = this.p2m.asRange(selection);
|
||||||
console.log(
|
console.log(
|
||||||
@ -418,12 +550,13 @@ export class CompilerErrors
|
|||||||
console.trace('No match');
|
console.trace('No match');
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
const error = this.errors
|
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
|
||||||
.filter((error) => error.uri === uri)
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||||
.map((error) => ({
|
monacoEditor,
|
||||||
error,
|
errorsPerResource
|
||||||
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
);
|
||||||
}))
|
const error = rangesPerResource
|
||||||
|
.map((range, index) => ({ error: errorsPerResource[index], range }))
|
||||||
.map(({ error, range }) => {
|
.map(({ error, range }) => {
|
||||||
if (range) {
|
if (range) {
|
||||||
const priority = calculatePriority(range, monacoSelection);
|
const priority = calculatePriority(range, monacoSelection);
|
||||||
@ -464,66 +597,77 @@ export class CompilerErrors
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a document change "destroys" the range of the decoration, the decoration must be removed.
|
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
|
||||||
*/
|
*/
|
||||||
private handleDocumentContentChange(
|
private handleDocumentContentChange(
|
||||||
editor: EditorWidget,
|
monacoEditor: MonacoEditor,
|
||||||
event: TextDocumentChangeEvent
|
event: TextDocumentChangeEvent
|
||||||
): void {
|
): void {
|
||||||
const monacoEditor = this.monacoEditor(editor);
|
const errorsPerResource = this.errors.filter(
|
||||||
if (!monacoEditor) {
|
(error) => error.uri === event.document.uri
|
||||||
return;
|
|
||||||
}
|
|
||||||
// A decoration location can be "destroyed", hence should be deleted when:
|
|
||||||
// - deleting range (start != end AND text is empty)
|
|
||||||
// - inserting text into range (start != end AND text is not empty)
|
|
||||||
// Filter unrelated delta changes to spare the CPU.
|
|
||||||
const relevantChanges = event.contentChanges.filter(
|
|
||||||
({ range: { start, end } }) =>
|
|
||||||
start.line !== end.line || start.character !== end.character
|
|
||||||
);
|
);
|
||||||
if (!relevantChanges.length) {
|
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
||||||
return;
|
const doc = event.document;
|
||||||
|
if (doc instanceof MonacoEditorModel) {
|
||||||
|
editorOrModel = doc.textEditorModel;
|
||||||
}
|
}
|
||||||
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||||
const resolvedMarkers = this.errors
|
editorOrModel,
|
||||||
.filter((error) => error.uri === event.document.uri)
|
errorsPerResource
|
||||||
.map((error, index) => {
|
);
|
||||||
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
const resolvedDecorations = rangesPerResource.map((range, index) => ({
|
||||||
if (range) {
|
error: errorsPerResource[index],
|
||||||
return { error, range, index };
|
range,
|
||||||
}
|
}));
|
||||||
return undefined;
|
const decoratorsToRemove = event.contentChanges
|
||||||
})
|
|
||||||
.filter(notEmpty);
|
|
||||||
|
|
||||||
const decorationIdsToRemove = relevantChanges
|
|
||||||
.map(({ range }) => this.p2m.asRange(range))
|
.map(({ range }) => this.p2m.asRange(range))
|
||||||
.map((changeRange) =>
|
.map((changedRange) =>
|
||||||
resolvedMarkers.filter(({ range: decorationRange }) =>
|
resolvedDecorations
|
||||||
changeRange.containsRange(decorationRange)
|
.filter(({ range: decorationRange }) => {
|
||||||
)
|
if (!decorationRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const affects =
|
||||||
|
changedRange.startLineNumber <= decorationRange.startLineNumber &&
|
||||||
|
changedRange.endLineNumber >= decorationRange.endLineNumber;
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
|
||||||
|
);
|
||||||
|
return affects;
|
||||||
|
})
|
||||||
|
.map(({ error }) => {
|
||||||
|
const index = this.errors.findIndex((candidate) =>
|
||||||
|
ErrorDecorationRef.sameAs(candidate, error)
|
||||||
|
);
|
||||||
|
return index !== -1 ? { error, index } : undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty)
|
||||||
)
|
)
|
||||||
.reduce((acc, curr) => acc.concat(curr), [])
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
.map(({ error, index }) => {
|
.sort((left, right) => left.index - right.index); // highest index last
|
||||||
this.errors.splice(index, 1);
|
|
||||||
return error.id;
|
if (decoratorsToRemove.length) {
|
||||||
});
|
let i = decoratorsToRemove.length;
|
||||||
if (!decorationIdsToRemove.length) {
|
while (i--) {
|
||||||
return;
|
this.errors.splice(decoratorsToRemove[i].index, 1);
|
||||||
|
}
|
||||||
|
monacoEditor.getControl().deltaDecorations(
|
||||||
|
decoratorsToRemove.map(({ error }) => error.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
}
|
}
|
||||||
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
|
||||||
this.onDidChangeEmitter.fire(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trackEditors(
|
private async trackEditors(
|
||||||
errors: Map<string, CoreError.ErrorLocation[]>,
|
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||||
...track: ((editor: EditorWidget) => Disposable)[]
|
...track: ((editor: MonacoEditor) => Disposable)[]
|
||||||
): Promise<Disposable> {
|
): Promise<Disposable> {
|
||||||
return new DisposableCollection(
|
return new DisposableCollection(
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
Array.from(errors.keys()).map(async (uri) => {
|
Array.from(errors.keys()).map(async (uri) => {
|
||||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
const editor = await this.monacoEditor(uri);
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return Disposable.NULL;
|
return Disposable.NULL;
|
||||||
}
|
}
|
||||||
@ -533,15 +677,18 @@ export class CompilerErrors
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
|
private async markAsCurrentError(
|
||||||
|
ref: ErrorDecorationRef,
|
||||||
|
options?: { forceReselect?: boolean; reveal?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
const index = this.errors.findIndex((candidate) =>
|
const index = this.errors.findIndex((candidate) =>
|
||||||
ErrorDecoration.sameAs(candidate, error)
|
ErrorDecorationRef.sameAs(candidate, ref)
|
||||||
);
|
);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'compiler-errors',
|
'compiler-errors',
|
||||||
`Failed to mark error ${
|
`Failed to mark error ${
|
||||||
error.id
|
ref.id
|
||||||
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||||
({ id }) => id
|
({ id }) => id
|
||||||
)}`
|
)}`
|
||||||
@ -550,15 +697,18 @@ export class CompilerErrors
|
|||||||
}
|
}
|
||||||
const newError = this.errors[index];
|
const newError = this.errors[index];
|
||||||
if (
|
if (
|
||||||
|
options?.forceReselect ||
|
||||||
!this.currentError ||
|
!this.currentError ||
|
||||||
!ErrorDecoration.sameAs(this.currentError, newError)
|
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
||||||
) {
|
) {
|
||||||
this.currentError = this.errors[index];
|
this.currentError = this.errors[index];
|
||||||
console.log(
|
console.log(
|
||||||
'compiler-errors',
|
'compiler-errors',
|
||||||
`Current error changed to ${this.currentError.id}`
|
`Current error changed to ${this.currentError.id}`
|
||||||
);
|
);
|
||||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
if (options?.reveal) {
|
||||||
|
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||||
|
}
|
||||||
this.onDidChangeEmitter.fire(this);
|
this.onDidChangeEmitter.fire(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,27 +748,28 @@ export class CompilerErrors
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupByResource(
|
private groupBy<K, V>(
|
||||||
errors: CoreError.ErrorLocation[]
|
elements: V[],
|
||||||
): Map<string, CoreError.ErrorLocation[]> {
|
extractKey: (element: V) => K
|
||||||
return errors.reduce((acc, curr) => {
|
): Map<K, V[]> {
|
||||||
const {
|
return elements.reduce((acc, curr) => {
|
||||||
location: { uri },
|
const key = extractKey(curr);
|
||||||
} = curr;
|
let values = acc.get(key);
|
||||||
let errors = acc.get(uri);
|
if (!values) {
|
||||||
if (!errors) {
|
values = [];
|
||||||
errors = [];
|
acc.set(key, values);
|
||||||
acc.set(uri, errors);
|
|
||||||
}
|
}
|
||||||
errors.push(curr);
|
values.push(curr);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map<string, CoreError.ErrorLocation[]>());
|
}, new Map<K, V[]>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||||
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
|
||||||
private monacoEditor(
|
private monacoEditor(
|
||||||
uriOrWidget: string | EditorWidget
|
uri: string | monaco.Uri
|
||||||
|
): Promise<MonacoEditor | undefined>;
|
||||||
|
private monacoEditor(
|
||||||
|
uriOrWidget: string | monaco.Uri | EditorWidget
|
||||||
): MaybePromise<MonacoEditor | undefined> {
|
): MaybePromise<MonacoEditor | undefined> {
|
||||||
if (uriOrWidget instanceof EditorWidget) {
|
if (uriOrWidget instanceof EditorWidget) {
|
||||||
const editor = uriOrWidget.editor;
|
const editor = uriOrWidget.editor;
|
||||||
@ -646,5 +797,8 @@ export namespace CompilerErrors {
|
|||||||
export const PREVIOUS_ERROR: Command = {
|
export const PREVIOUS_ERROR: Command = {
|
||||||
id: 'arduino-editor-previous-error',
|
id: 'arduino-editor-previous-error',
|
||||||
};
|
};
|
||||||
|
export const MARK_AS_CURRENT: Command = {
|
||||||
|
id: 'arduino-editor-mark-as-current-error',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import { MaybePromise } from '@theia/core';
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import * as monaco from '@theia/monaco-editor-core';
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
import { Formatter } from '../../common/protocol/formatter';
|
import { Formatter } from '../../common/protocol/formatter';
|
||||||
import { InoSelector } from '../ino-selectors';
|
import { InoSelector } from '../selectors';
|
||||||
import { fullRange } from '../utils/monaco';
|
|
||||||
import { Contribution, URI } from './contribution';
|
import { Contribution, URI } from './contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -40,7 +39,7 @@ export class Format
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_token: monaco.CancellationToken
|
_token: monaco.CancellationToken
|
||||||
): Promise<monaco.languages.TextEdit[]> {
|
): Promise<monaco.languages.TextEdit[]> {
|
||||||
const range = fullRange(model);
|
const range = model.getFullModelRange();
|
||||||
const text = await this.format(model, range, options);
|
const text = await this.format(model, range, options);
|
||||||
return [{ range, text }];
|
return [{ range, text }];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as monaco from '@theia/monaco-editor-core';
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||||
/**
|
/**
|
||||||
* Exclusive "ino" document selector for monaco.
|
* Exclusive "ino" document selector for monaco.
|
||||||
*/
|
*/
|
||||||
@ -11,3 +12,11 @@ function selectorOf(
|
|||||||
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
|
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector for the `monaco` resource in the Arduino _Output_ channel.
|
||||||
|
*/
|
||||||
|
export const ArduinoOutputSelector: monaco.languages.LanguageSelector = {
|
||||||
|
scheme: OutputUri.SCHEME,
|
||||||
|
pattern: '**/Arduino',
|
||||||
|
};
|
@ -1,8 +0,0 @@
|
|||||||
import * as monaco from '@theia/monaco-editor-core';
|
|
||||||
|
|
||||||
export function fullRange(model: monaco.editor.ITextModel): monaco.Range {
|
|
||||||
const lastLine = model.getLineCount();
|
|
||||||
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
|
|
||||||
const end = new monaco.Position(lastLine, lastLineMaxColumn);
|
|
||||||
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
|
|
||||||
}
|
|
@ -1,5 +1,9 @@
|
|||||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||||
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
|
import type {
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
Position,
|
||||||
|
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
import type {
|
import type {
|
||||||
BoardUserField,
|
BoardUserField,
|
||||||
Port,
|
Port,
|
||||||
@ -15,11 +19,41 @@ export const CompilerWarningLiterals = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
||||||
export namespace CoreError {
|
export namespace CoreError {
|
||||||
export interface ErrorLocation {
|
export interface ErrorLocationRef {
|
||||||
readonly message: string;
|
readonly message: string;
|
||||||
readonly location: Location;
|
readonly location: Location;
|
||||||
readonly details?: string;
|
readonly details?: string;
|
||||||
}
|
}
|
||||||
|
export namespace ErrorLocationRef {
|
||||||
|
export function equals(
|
||||||
|
left: ErrorLocationRef,
|
||||||
|
right: ErrorLocationRef
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
left.message === right.message &&
|
||||||
|
left.details === right.details &&
|
||||||
|
equalsLocation(left.location, right.location)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function equalsLocation(left: Location, right: Location): boolean {
|
||||||
|
return left.uri === right.uri && equalsRange(left.range, right.range);
|
||||||
|
}
|
||||||
|
function equalsRange(left: Range, right: Range): boolean {
|
||||||
|
return (
|
||||||
|
equalsPosition(left.start, right.start) &&
|
||||||
|
equalsPosition(left.end, right.end)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function equalsPosition(left: Position, right: Position): boolean {
|
||||||
|
return left.character === right.character && left.line === right.line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface ErrorLocation extends ErrorLocationRef {
|
||||||
|
/**
|
||||||
|
* The range of the error location source from the CLI output.
|
||||||
|
*/
|
||||||
|
readonly rangesInOutput: Range[]; // The same error might show up multiple times in the CLI output: https://github.com/arduino/arduino-cli/issues/1761
|
||||||
|
}
|
||||||
export const Codes = {
|
export const Codes = {
|
||||||
Verify: 4001,
|
Verify: 4001,
|
||||||
Upload: 4002,
|
Upload: 4002,
|
||||||
|
@ -5,25 +5,41 @@ import {
|
|||||||
Range,
|
Range,
|
||||||
Position,
|
Position,
|
||||||
} from '@theia/core/shared/vscode-languageserver-protocol';
|
} 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';
|
import { Sketch } from '../common/protocol/sketches-service';
|
||||||
|
|
||||||
export interface ErrorSource {
|
export interface OutputSource {
|
||||||
readonly content: string | ReadonlyArray<Uint8Array>;
|
readonly content: string | ReadonlyArray<Uint8Array>;
|
||||||
readonly sketch?: Sketch;
|
readonly sketch?: Sketch;
|
||||||
}
|
}
|
||||||
|
export namespace OutputSource {
|
||||||
export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
|
export function content(source: OutputSource): string {
|
||||||
const { content, sketch } = source;
|
const { content } = source;
|
||||||
const err =
|
return typeof content === 'string'
|
||||||
typeof content === 'string'
|
|
||||||
? content
|
? content
|
||||||
: Buffer.concat(content).toString('utf8');
|
: Buffer.concat(content).toString('utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryParseError(source: OutputSource): CoreError.ErrorLocation[] {
|
||||||
|
const { sketch } = source;
|
||||||
|
const content = OutputSource.content(source);
|
||||||
if (sketch) {
|
if (sketch) {
|
||||||
return tryParse(err)
|
return tryParse(content)
|
||||||
.map(remapErrorMessages)
|
.map(remapErrorMessages)
|
||||||
.filter(isLocationInSketch(sketch))
|
.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 [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -35,6 +51,7 @@ interface ParseResult {
|
|||||||
readonly errorPrefix: string;
|
readonly errorPrefix: string;
|
||||||
readonly error: string;
|
readonly error: string;
|
||||||
readonly message?: string;
|
readonly message?: string;
|
||||||
|
readonly rangeInOutput?: Range | undefined;
|
||||||
}
|
}
|
||||||
namespace ParseResult {
|
namespace ParseResult {
|
||||||
export function keyOf(result: ParseResult): string {
|
export function keyOf(result: ParseResult): string {
|
||||||
@ -64,6 +81,7 @@ function toErrorInfo({
|
|||||||
path,
|
path,
|
||||||
line,
|
line,
|
||||||
column,
|
column,
|
||||||
|
rangeInOutput,
|
||||||
}: ParseResult): CoreError.ErrorLocation {
|
}: ParseResult): CoreError.ErrorLocation {
|
||||||
return {
|
return {
|
||||||
message: error,
|
message: error,
|
||||||
@ -72,6 +90,7 @@ function toErrorInfo({
|
|||||||
uri: FileUri.create(path).toString(),
|
uri: FileUri.create(path).toString(),
|
||||||
range: range(line, column),
|
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
|
// 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(
|
const re = new RegExp(
|
||||||
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
|
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
|
||||||
'gm'
|
'gm'
|
||||||
);
|
);
|
||||||
return [
|
return Array.from(content.matchAll(re) ?? [])
|
||||||
...new Map(
|
.map((match) => {
|
||||||
Array.from(raw.matchAll(re) ?? [])
|
const { index: start } = match;
|
||||||
.map((match) => {
|
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
|
||||||
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
|
(match) => (match ? match.trim() : match)
|
||||||
(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(
|
const rangeInOutput = findRangeInOutput(
|
||||||
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
|
start,
|
||||||
);
|
{ path, rawLine, rawColumn },
|
||||||
return undefined;
|
content
|
||||||
}
|
);
|
||||||
let column: number | undefined = undefined;
|
return {
|
||||||
if (rawColumn) {
|
path,
|
||||||
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
|
line,
|
||||||
column = Number.parseInt(normalizedRawColumn, 10);
|
column,
|
||||||
if (!Number.isInteger(column)) {
|
errorPrefix,
|
||||||
console.warn(
|
error,
|
||||||
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
|
rangeInOutput,
|
||||||
);
|
};
|
||||||
}
|
})
|
||||||
}
|
.filter(notEmpty);
|
||||||
return {
|
|
||||||
path,
|
|
||||||
line,
|
|
||||||
column,
|
|
||||||
errorPrefix,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(notEmpty)
|
|
||||||
.map((result) => [ParseResult.keyOf(result), result])
|
|
||||||
).values(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
const compilerErrors = tryParseError({
|
const compilerErrors = tryParseError({
|
||||||
content: handler.stderr,
|
content: handler.content,
|
||||||
sketch: options.sketch,
|
sketch: options.sketch,
|
||||||
});
|
});
|
||||||
const message = nls.localize(
|
const message = nls.localize(
|
||||||
@ -224,7 +224,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
errorCtor(
|
errorCtor(
|
||||||
message,
|
message,
|
||||||
tryParseError({
|
tryParseError({
|
||||||
content: handler.stderr,
|
content: handler.content,
|
||||||
sketch: options.sketch,
|
sketch: options.sketch,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -291,7 +291,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
'Error while burning the bootloader: {0}',
|
'Error while burning the bootloader: {0}',
|
||||||
error.details
|
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?
|
// TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers?
|
||||||
...handlers: ((response: R) => void)[]
|
...handlers: ((response: R) => void)[]
|
||||||
): Disposable & {
|
): Disposable & {
|
||||||
stderr: Buffer[];
|
content: Buffer[];
|
||||||
onData: (response: R) => void;
|
onData: (response: R) => void;
|
||||||
} {
|
} {
|
||||||
const stderr: Buffer[] = [];
|
const content: Buffer[] = [];
|
||||||
const buffer = new AutoFlushingBuffer((chunks) => {
|
const buffer = new AutoFlushingBuffer((chunks) => {
|
||||||
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
|
chunks.forEach(([severity, chunk]) => this.sendResponse(chunk, severity));
|
||||||
if (chunk) {
|
|
||||||
this.sendResponse(chunk, severity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
const onData = StreamingResponse.createOnDataHandler({
|
const onData = StreamingResponse.createOnDataHandler({
|
||||||
stderr,
|
content,
|
||||||
onData: (out, err) => {
|
onData: (out, err) => {
|
||||||
buffer.addChunk(out);
|
buffer.addChunk(out);
|
||||||
buffer.addChunk(err, OutputMessage.Severity.Error);
|
buffer.addChunk(err, OutputMessage.Severity.Error);
|
||||||
@ -363,7 +359,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
dispose: () => buffer.dispose(),
|
dispose: () => buffer.dispose(),
|
||||||
stderr,
|
content,
|
||||||
onData,
|
onData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -432,14 +428,19 @@ namespace StreamingResponse {
|
|||||||
): (response: R) => void {
|
): (response: R) => void {
|
||||||
return (response: R) => {
|
return (response: R) => {
|
||||||
const out = response.getOutStream_asU8();
|
const out = response.getOutStream_asU8();
|
||||||
|
if (out.length) {
|
||||||
|
options.content.push(out);
|
||||||
|
}
|
||||||
const err = response.getErrStream_asU8();
|
const err = response.getErrStream_asU8();
|
||||||
options.stderr.push(err);
|
if (err.length) {
|
||||||
|
options.content.push(err);
|
||||||
|
}
|
||||||
options.onData(out, err);
|
options.onData(out, err);
|
||||||
options.handlers?.forEach((handler) => handler(response));
|
options.handlers?.forEach((handler) => handler(response));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export interface Options<R extends StreamingResponse> {
|
export interface Options<R extends StreamingResponse> {
|
||||||
readonly stderr: Uint8Array[];
|
readonly content: Uint8Array[];
|
||||||
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
|
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
|
||||||
/**
|
/**
|
||||||
* Additional request handlers.
|
* Additional request handlers.
|
||||||
|
@ -3,13 +3,13 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
|||||||
import { OutputMessage } from '../../common/protocol';
|
import { OutputMessage } from '../../common/protocol';
|
||||||
|
|
||||||
export class AutoFlushingBuffer implements Disposable {
|
export class AutoFlushingBuffer implements Disposable {
|
||||||
private readonly chunks = Chunks.create();
|
private readonly chunks: Array<[OutputMessage.Severity, Uint8Array]> = [];
|
||||||
private readonly toDispose;
|
private readonly toDispose;
|
||||||
private timer?: NodeJS.Timeout;
|
private timer?: NodeJS.Timeout;
|
||||||
private disposed = false;
|
private disposed = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
onFlush: (chunks: Map<OutputMessage.Severity, string | undefined>) => void,
|
onFlush: (chunks: Array<[OutputMessage.Severity, string]>) => void,
|
||||||
taskTimeout: number = AutoFlushingBuffer.DEFAULT_FLUSH_TIMEOUT_MS
|
taskTimeout: number = AutoFlushingBuffer.DEFAULT_FLUSH_TIMEOUT_MS
|
||||||
) {
|
) {
|
||||||
const task = () => {
|
const task = () => {
|
||||||
@ -34,7 +34,9 @@ export class AutoFlushingBuffer implements Disposable {
|
|||||||
chunk: Uint8Array,
|
chunk: Uint8Array,
|
||||||
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
||||||
): void {
|
): void {
|
||||||
this.chunks.get(severity)?.push(chunk);
|
if (chunk.length) {
|
||||||
|
this.chunks.push([severity, chunk]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
@ -49,19 +51,10 @@ export namespace AutoFlushingBuffer {
|
|||||||
export const DEFAULT_FLUSH_TIMEOUT_MS = 32;
|
export const DEFAULT_FLUSH_TIMEOUT_MS = 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
|
type Chunks = Array<[OutputMessage.Severity, Uint8Array]>;
|
||||||
namespace Chunks {
|
namespace Chunks {
|
||||||
export function create(): Chunks {
|
|
||||||
return new Map([
|
|
||||||
[OutputMessage.Severity.Error, []],
|
|
||||||
[OutputMessage.Severity.Warning, []],
|
|
||||||
[OutputMessage.Severity.Info, []],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
export function clear(chunks: Chunks): Chunks {
|
export function clear(chunks: Chunks): Chunks {
|
||||||
for (const chunk of chunks.values()) {
|
chunks.length = 0;
|
||||||
chunk.length = 0;
|
|
||||||
}
|
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
export function isEmpty(chunks: Chunks): boolean {
|
export function isEmpty(chunks: Chunks): boolean {
|
||||||
@ -69,12 +62,35 @@ namespace Chunks {
|
|||||||
}
|
}
|
||||||
export function toString(
|
export function toString(
|
||||||
chunks: Chunks
|
chunks: Chunks
|
||||||
): Map<OutputMessage.Severity, string | undefined> {
|
): Array<[OutputMessage.Severity, string]> {
|
||||||
return new Map(
|
const result: Array<[OutputMessage.Severity, string]> = [];
|
||||||
Array.from(chunks.entries()).map(([severity, buffers]) => [
|
let current:
|
||||||
severity,
|
| { severity: OutputMessage.Severity; buffers: Uint8Array[] }
|
||||||
buffers.length ? Buffer.concat(buffers).toString() : undefined,
|
| 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,8 @@
|
|||||||
"increaseFontSize": "Increase Font Size",
|
"increaseFontSize": "Increase Font Size",
|
||||||
"increaseIndent": "Increase Indent",
|
"increaseIndent": "Increase Indent",
|
||||||
"nextError": "Next Error",
|
"nextError": "Next Error",
|
||||||
"previousError": "Previous Error"
|
"previousError": "Previous Error",
|
||||||
|
"revealError": "Reveal Error"
|
||||||
},
|
},
|
||||||
"electron": {
|
"electron": {
|
||||||
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
|
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user