mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-12 04:48:33 +00:00
805 lines
25 KiB
TypeScript
805 lines
25 KiB
TypeScript
import {
|
|
Command,
|
|
CommandRegistry,
|
|
Disposable,
|
|
DisposableCollection,
|
|
Emitter,
|
|
MaybeArray,
|
|
MaybePromise,
|
|
nls,
|
|
notEmpty,
|
|
} from '@theia/core';
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
import {
|
|
Location,
|
|
Range,
|
|
} from '@theia/core/shared/vscode-languageserver-protocol';
|
|
import {
|
|
EditorWidget,
|
|
TextDocumentChangeEvent,
|
|
} from '@theia/editor/lib/browser';
|
|
import {
|
|
EditorDecoration,
|
|
TrackedRangeStickiness,
|
|
} from '@theia/editor/lib/browser/decorations/editor-decoration';
|
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
|
import * as monaco from '@theia/monaco-editor-core';
|
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-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 { ErrorRevealStrategy } from '../arduino-preferences';
|
|
import { ArduinoOutputSelector, InoSelector } from '../selectors';
|
|
import { Contribution } from './contribution';
|
|
import { CoreErrorHandler } from './core-error-handler';
|
|
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
|
|
interface ErrorDecorationRef {
|
|
/**
|
|
* This is the unique ID of the decoration given by `monaco`.
|
|
*/
|
|
readonly id: string;
|
|
/**
|
|
* The resource this decoration belongs to.
|
|
*/
|
|
readonly uri: string;
|
|
}
|
|
export namespace ErrorDecorationRef {
|
|
export function is(arg: unknown): arg is ErrorDecorationRef {
|
|
if (typeof arg === 'object') {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const object = arg as any;
|
|
return (
|
|
'uri' in object &&
|
|
typeof object['uri'] === 'string' &&
|
|
'id' in object &&
|
|
typeof object['id'] === 'string'
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
export function sameAs(
|
|
left: ErrorDecorationRef,
|
|
right: ErrorDecorationRef
|
|
): boolean {
|
|
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()
|
|
export class CompilerErrors
|
|
extends Contribution
|
|
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
|
{
|
|
@inject(EditorManager)
|
|
private readonly editorManager: EditorManager;
|
|
|
|
@inject(ProtocolToMonacoConverter)
|
|
private readonly p2m: ProtocolToMonacoConverter;
|
|
|
|
@inject(MonacoToProtocolConverter)
|
|
private readonly m2p: MonacoToProtocolConverter;
|
|
|
|
@inject(CoreErrorHandler)
|
|
private readonly coreErrorHandler: CoreErrorHandler;
|
|
|
|
private revealStrategy = ErrorRevealStrategy.Default;
|
|
private experimental = false;
|
|
|
|
private readonly errors: ErrorDecoration[] = [];
|
|
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
|
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
|
private readonly onCurrentErrorDidChange =
|
|
this.currentErrorDidChangEmitter.event;
|
|
private readonly toDisposeOnCompilerErrorDidChange =
|
|
new DisposableCollection();
|
|
|
|
private shell: ApplicationShell | undefined;
|
|
private currentError: ErrorDecoration | undefined;
|
|
private get currentErrorIndex(): number {
|
|
const current = this.currentError;
|
|
if (!current) {
|
|
return -1;
|
|
}
|
|
return this.errors.findIndex((error) =>
|
|
ErrorDecorationRef.sameAs(error, current)
|
|
);
|
|
}
|
|
|
|
override onStart(app: FrontendApplication): void {
|
|
this.shell = app.shell;
|
|
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
|
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
|
|
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
|
this.handleCompilerErrorsDidChange(errors)
|
|
);
|
|
this.onCurrentErrorDidChange(async (error) => {
|
|
const monacoEditor = await this.monacoEditor(error.uri);
|
|
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
|
|
if (!monacoRange) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`Could not find range of decoration: ${error.id}`
|
|
);
|
|
return;
|
|
}
|
|
const range = this.m2p.asRange(monacoRange);
|
|
const editor = await this.revealLocationInEditor({
|
|
uri: error.uri,
|
|
range,
|
|
});
|
|
if (!editor) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`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.experimental = Boolean(
|
|
this.preferences['arduino.compile.experimental']
|
|
);
|
|
const strategy = this.preferences['arduino.compile.revealRange'];
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
override registerCommands(registry: CommandRegistry): void {
|
|
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
|
|
execute: () => {
|
|
const index = this.currentErrorIndex;
|
|
if (index < 0) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`Could not advance to next error. Unknown current error.`
|
|
);
|
|
return;
|
|
}
|
|
const nextError =
|
|
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
|
return this.markAsCurrentError(nextError, {
|
|
forceReselect: true,
|
|
reveal: true,
|
|
});
|
|
},
|
|
isEnabled: () =>
|
|
this.experimental && !!this.currentError && this.errors.length > 1,
|
|
});
|
|
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
|
execute: () => {
|
|
const index = this.currentErrorIndex;
|
|
if (index < 0) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`Could not advance to previous error. Unknown current error.`
|
|
);
|
|
return;
|
|
}
|
|
const previousError =
|
|
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
|
return this.markAsCurrentError(previousError, {
|
|
forceReselect: true,
|
|
reveal: true,
|
|
});
|
|
},
|
|
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,
|
|
});
|
|
}
|
|
|
|
get onDidChange(): monaco.IEvent<this> {
|
|
return this.onDidChangeEmitter.event;
|
|
}
|
|
|
|
async provideCodeLenses(
|
|
model: monaco.editor.ITextModel,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_token: monaco.CancellationToken
|
|
): Promise<monaco.languages.CodeLensList> {
|
|
const lenses: monaco.languages.CodeLens[] = [];
|
|
if (
|
|
this.experimental &&
|
|
this.currentError &&
|
|
this.currentError.uri === model.uri.toString() &&
|
|
this.errors.length > 1
|
|
) {
|
|
const monacoEditor = await this.monacoEditor(model.uri);
|
|
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
|
if (range) {
|
|
lenses.push(
|
|
{
|
|
range,
|
|
command: {
|
|
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
|
|
title: nls.localize(
|
|
'arduino/editor/previousError',
|
|
'Previous Error'
|
|
),
|
|
arguments: [this.currentError],
|
|
},
|
|
},
|
|
{
|
|
range,
|
|
command: {
|
|
id: CompilerErrors.Commands.NEXT_ERROR.id,
|
|
title: nls.localize('arduino/editor/nextError', 'Next Error'),
|
|
arguments: [this.currentError],
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}
|
|
return {
|
|
lenses,
|
|
dispose: () => {
|
|
/* NOOP */
|
|
},
|
|
};
|
|
}
|
|
|
|
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(
|
|
errors: CoreError.ErrorLocation[]
|
|
): Promise<void> {
|
|
this.toDisposeOnCompilerErrorDidChange.dispose();
|
|
const groupedErrors = this.groupBy(
|
|
errors,
|
|
(error: CoreError.ErrorLocation) => error.location.uri
|
|
);
|
|
const decorations = await this.decorateEditors(groupedErrors);
|
|
this.errors.push(...decorations.errors);
|
|
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
|
Disposable.create(() => (this.errors.length = 0)),
|
|
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
|
|
...(await Promise.all([
|
|
decorations.dispose,
|
|
this.trackEditors(
|
|
groupedErrors,
|
|
(editor) =>
|
|
editor.onSelectionChanged((selection) =>
|
|
this.handleSelectionChange(editor, selection)
|
|
),
|
|
(editor) =>
|
|
editor.onDispose(() =>
|
|
this.handleEditorDidDispose(editor.uri.toString())
|
|
),
|
|
(editor) =>
|
|
editor.onDocumentContentChanged((event) =>
|
|
this.handleDocumentContentChange(editor, event)
|
|
)
|
|
),
|
|
])),
|
|
]);
|
|
const currentError = this.errors[0];
|
|
if (currentError) {
|
|
await this.markAsCurrentError(currentError, {
|
|
forceReselect: true,
|
|
reveal: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async decorateEditors(
|
|
errors: Map<string, CoreError.ErrorLocation[]>
|
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
|
const composite = await Promise.all(
|
|
[...errors.entries()].map(([uri, errors]) =>
|
|
this.decorateEditor(uri, errors)
|
|
)
|
|
);
|
|
return {
|
|
dispose: new DisposableCollection(
|
|
...composite.map(({ dispose }) => dispose)
|
|
),
|
|
errors: composite.reduce(
|
|
(acc, { errors }) => acc.concat(errors),
|
|
[] as ErrorDecoration[]
|
|
),
|
|
};
|
|
}
|
|
|
|
private async decorateEditor(
|
|
uri: string,
|
|
errors: CoreError.ErrorLocation[]
|
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
|
const editor = await this.monacoEditor(uri);
|
|
if (!editor) {
|
|
return { dispose: Disposable.NULL, errors: [] };
|
|
}
|
|
const oldDecorations = editor.deltaDecorations({
|
|
oldDecorations: [],
|
|
newDecorations: errors.map((error) =>
|
|
this.compilerErrorDecoration(error.location.range)
|
|
),
|
|
});
|
|
return {
|
|
dispose: Disposable.create(() => {
|
|
if (editor) {
|
|
editor.deltaDecorations({
|
|
oldDecorations,
|
|
newDecorations: [],
|
|
});
|
|
}
|
|
}),
|
|
errors: oldDecorations.map((id, index) => ({
|
|
id,
|
|
uri,
|
|
rangesInOutput: errors[index].rangesInOutput.map((range) =>
|
|
this.p2m.asRange(range)
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
|
|
private compilerErrorDecoration(range: Range): EditorDecoration {
|
|
return {
|
|
range,
|
|
options: {
|
|
isWholeLine: true,
|
|
className: 'compiler-error',
|
|
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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(
|
|
monacoEditor: MonacoEditor,
|
|
selection: Range
|
|
): void {
|
|
const uri = monacoEditor.uri.toString();
|
|
const monacoSelection = this.p2m.asRange(selection);
|
|
console.log(
|
|
'compiler-errors',
|
|
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
|
|
);
|
|
const calculatePriority = (
|
|
candidateErrorRange: monaco.Range,
|
|
currentSelection: monaco.Range
|
|
) => {
|
|
console.trace(
|
|
'compiler-errors',
|
|
`Candidate error range: ${candidateErrorRange.toJSON()}`
|
|
);
|
|
console.trace(
|
|
'compiler-errors',
|
|
`Current selection range: ${currentSelection.toJSON()}`
|
|
);
|
|
if (candidateErrorRange.intersectRanges(currentSelection)) {
|
|
console.trace('Intersects.');
|
|
return { score: 2 };
|
|
}
|
|
if (
|
|
candidateErrorRange.startLineNumber <=
|
|
currentSelection.startLineNumber &&
|
|
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
|
|
) {
|
|
console.trace('Same line.');
|
|
return { score: 1 };
|
|
}
|
|
|
|
console.trace('No match');
|
|
return undefined;
|
|
};
|
|
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
|
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
|
monacoEditor,
|
|
errorsPerResource
|
|
);
|
|
const error = rangesPerResource
|
|
.map((range, index) => ({ error: errorsPerResource[index], range }))
|
|
.map(({ error, range }) => {
|
|
if (range) {
|
|
const priority = calculatePriority(range, monacoSelection);
|
|
if (priority) {
|
|
return { ...priority, error };
|
|
}
|
|
}
|
|
return undefined;
|
|
})
|
|
.filter(notEmpty)
|
|
.sort((left, right) => right.score - left.score) // highest first
|
|
.map(({ error }) => error)
|
|
.shift();
|
|
if (error) {
|
|
this.markAsCurrentError(error);
|
|
} else {
|
|
console.info(
|
|
'compiler-errors',
|
|
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
|
|
* If editor closes, delete the decorators.
|
|
*/
|
|
private handleEditorDidDispose(uri: string): void {
|
|
let i = this.errors.length;
|
|
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
|
|
while (i--) {
|
|
const error = this.errors[i];
|
|
if (error.uri === uri) {
|
|
this.errors.splice(i, 1);
|
|
}
|
|
}
|
|
this.onDidChangeEmitter.fire(this);
|
|
}
|
|
|
|
/**
|
|
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
|
|
*/
|
|
private handleDocumentContentChange(
|
|
monacoEditor: MonacoEditor,
|
|
event: TextDocumentChangeEvent
|
|
): void {
|
|
const errorsPerResource = this.errors.filter(
|
|
(error) => error.uri === event.document.uri
|
|
);
|
|
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
|
const doc = event.document;
|
|
if (doc instanceof MonacoEditorModel) {
|
|
editorOrModel = doc.textEditorModel;
|
|
}
|
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
|
editorOrModel,
|
|
errorsPerResource
|
|
);
|
|
const resolvedDecorations = rangesPerResource.map((range, index) => ({
|
|
error: errorsPerResource[index],
|
|
range,
|
|
}));
|
|
const decoratorsToRemove = event.contentChanges
|
|
.map(({ range }) => this.p2m.asRange(range))
|
|
.map((changedRange) =>
|
|
resolvedDecorations
|
|
.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), [])
|
|
.sort((left, right) => left.index - right.index); // highest index last
|
|
|
|
if (decoratorsToRemove.length) {
|
|
let i = decoratorsToRemove.length;
|
|
while (i--) {
|
|
this.errors.splice(decoratorsToRemove[i].index, 1);
|
|
}
|
|
monacoEditor.getControl().deltaDecorations(
|
|
decoratorsToRemove.map(({ error }) => error.id),
|
|
[]
|
|
);
|
|
this.onDidChangeEmitter.fire(this);
|
|
}
|
|
}
|
|
|
|
private async trackEditors(
|
|
errors: Map<string, CoreError.ErrorLocation[]>,
|
|
...track: ((editor: MonacoEditor) => Disposable)[]
|
|
): Promise<Disposable> {
|
|
return new DisposableCollection(
|
|
...(await Promise.all(
|
|
Array.from(errors.keys()).map(async (uri) => {
|
|
const editor = await this.monacoEditor(uri);
|
|
if (!editor) {
|
|
return Disposable.NULL;
|
|
}
|
|
return new DisposableCollection(...track.map((t) => t(editor)));
|
|
})
|
|
))
|
|
);
|
|
}
|
|
|
|
private async markAsCurrentError(
|
|
ref: ErrorDecorationRef,
|
|
options?: { forceReselect?: boolean; reveal?: boolean }
|
|
): Promise<void> {
|
|
const index = this.errors.findIndex((candidate) =>
|
|
ErrorDecorationRef.sameAs(candidate, ref)
|
|
);
|
|
if (index < 0) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`Failed to mark error ${
|
|
ref.id
|
|
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
|
({ id }) => id
|
|
)}`
|
|
);
|
|
return;
|
|
}
|
|
const newError = this.errors[index];
|
|
if (
|
|
options?.forceReselect ||
|
|
!this.currentError ||
|
|
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
|
) {
|
|
this.currentError = this.errors[index];
|
|
console.log(
|
|
'compiler-errors',
|
|
`Current error changed to ${this.currentError.id}`
|
|
);
|
|
if (options?.reveal) {
|
|
this.currentErrorDidChangEmitter.fire(this.currentError);
|
|
}
|
|
this.onDidChangeEmitter.fire(this);
|
|
}
|
|
}
|
|
|
|
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
|
|
private async revealLocationInEditor(
|
|
location: Location
|
|
): Promise<EditorWidget | undefined> {
|
|
const { uri, range } = location;
|
|
const editor = await this.editorManager.getByUri(new URI(uri), {
|
|
mode: 'activate',
|
|
});
|
|
if (editor && this.shell) {
|
|
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
|
|
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
|
|
editor.editor.revealRange(range, { at: this.revealStrategy });
|
|
const activeWidget = await this.shell.activateWidget(editor.id);
|
|
if (!activeWidget) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
|
|
);
|
|
return editor;
|
|
}
|
|
if (editor !== activeWidget) {
|
|
console.warn(
|
|
'compiler-errors',
|
|
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
|
|
);
|
|
}
|
|
return editor;
|
|
}
|
|
console.warn(
|
|
'compiler-errors',
|
|
`could not find editor widget for URI: ${uri}`
|
|
);
|
|
return undefined;
|
|
}
|
|
|
|
private groupBy<K, V>(
|
|
elements: V[],
|
|
extractKey: (element: V) => K
|
|
): Map<K, V[]> {
|
|
return elements.reduce((acc, curr) => {
|
|
const key = extractKey(curr);
|
|
let values = acc.get(key);
|
|
if (!values) {
|
|
values = [];
|
|
acc.set(key, values);
|
|
}
|
|
values.push(curr);
|
|
return acc;
|
|
}, new Map<K, V[]>());
|
|
}
|
|
|
|
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
|
private monacoEditor(
|
|
uri: string | monaco.Uri
|
|
): Promise<MonacoEditor | undefined>;
|
|
private monacoEditor(
|
|
uriOrWidget: string | monaco.Uri | EditorWidget
|
|
): MaybePromise<MonacoEditor | undefined> {
|
|
if (uriOrWidget instanceof EditorWidget) {
|
|
const editor = uriOrWidget.editor;
|
|
if (editor instanceof MonacoEditor) {
|
|
return editor;
|
|
}
|
|
return undefined;
|
|
} else {
|
|
return this.editorManager
|
|
.getByUri(new URI(uriOrWidget))
|
|
.then((editor) => {
|
|
if (editor) {
|
|
return this.monacoEditor(editor);
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
export namespace CompilerErrors {
|
|
export namespace Commands {
|
|
export const NEXT_ERROR: Command = {
|
|
id: 'arduino-editor-next-error',
|
|
};
|
|
export const PREVIOUS_ERROR: Command = {
|
|
id: 'arduino-editor-previous-error',
|
|
};
|
|
export const MARK_AS_CURRENT: Command = {
|
|
id: 'arduino-editor-mark-as-current-error',
|
|
};
|
|
}
|
|
}
|