mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-24 11:46:32 +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,
|
||||
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 {
|
||||
@ -28,14 +30,15 @@ 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 { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
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 ErrorDecoration {
|
||||
interface ErrorDecorationRef {
|
||||
/**
|
||||
* This is the unique ID of the decoration given by `monaco`.
|
||||
*/
|
||||
@ -45,72 +48,89 @@ interface ErrorDecoration {
|
||||
*/
|
||||
readonly uri: string;
|
||||
}
|
||||
namespace ErrorDecoration {
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
||||
): Promise<monaco.Range | undefined>;
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: MonacoEditor
|
||||
): monaco.Range | undefined;
|
||||
export function rangeOf(
|
||||
{ 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;
|
||||
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 editorProvider(uri).then((editor) => {
|
||||
if (editor) {
|
||||
return rangeOf({ id, uri }, editor);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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(
|
||||
left: ErrorDecoration,
|
||||
right: ErrorDecoration
|
||||
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
|
||||
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
||||
{
|
||||
@inject(EditorManager)
|
||||
private readonly editorManager: EditorManager;
|
||||
@ -119,11 +139,14 @@ export class CompilerErrors
|
||||
private readonly p2m: ProtocolToMonacoConverter;
|
||||
|
||||
@inject(MonacoToProtocolConverter)
|
||||
private readonly mp2: 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>();
|
||||
@ -131,8 +154,8 @@ export class CompilerErrors
|
||||
this.currentErrorDidChangEmitter.event;
|
||||
private readonly toDisposeOnCompilerErrorDidChange =
|
||||
new DisposableCollection();
|
||||
|
||||
private shell: ApplicationShell | undefined;
|
||||
private revealStrategy = ErrorRevealStrategy.Default;
|
||||
private currentError: ErrorDecoration | undefined;
|
||||
private get currentErrorIndex(): number {
|
||||
const current = this.currentError;
|
||||
@ -140,46 +163,75 @@ export class CompilerErrors
|
||||
return -1;
|
||||
}
|
||||
return this.errors.findIndex((error) =>
|
||||
ErrorDecoration.sameAs(error, current)
|
||||
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.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
||||
this.handleCompilerErrorsDidChange(errors)
|
||||
);
|
||||
this.onCurrentErrorDidChange(async (error) => {
|
||||
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
if (!range) {
|
||||
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: this.mp2.asRange(range),
|
||||
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.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.compile.revealRange') {
|
||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||
? newValue
|
||||
: ErrorRevealStrategy.Default;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -196,9 +248,13 @@ export class CompilerErrors
|
||||
}
|
||||
const nextError =
|
||||
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, {
|
||||
execute: () => {
|
||||
@ -212,9 +268,24 @@ export class CompilerErrors
|
||||
}
|
||||
const previousError =
|
||||
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> {
|
||||
const lenses: monaco.languages.CodeLens[] = [];
|
||||
if (
|
||||
this.experimental &&
|
||||
this.currentError &&
|
||||
this.currentError.uri === model.uri.toString() &&
|
||||
this.errors.length > 1
|
||||
) {
|
||||
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
const monacoEditor = await this.monacoEditor(model.uri);
|
||||
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
||||
if (range) {
|
||||
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(
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<void> {
|
||||
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||
const compilerErrorsPerResource = this.groupByResource(
|
||||
await this.filter(errors)
|
||||
const groupedErrors = this.groupBy(
|
||||
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.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||
Disposable.create(() => (this.errors.length = 0)),
|
||||
@ -283,17 +421,17 @@ export class CompilerErrors
|
||||
...(await Promise.all([
|
||||
decorations.dispose,
|
||||
this.trackEditors(
|
||||
compilerErrorsPerResource,
|
||||
groupedErrors,
|
||||
(editor) =>
|
||||
editor.editor.onSelectionChanged((selection) =>
|
||||
editor.onSelectionChanged((selection) =>
|
||||
this.handleSelectionChange(editor, selection)
|
||||
),
|
||||
(editor) =>
|
||||
editor.onDidDispose(() =>
|
||||
this.handleEditorDidDispose(editor.editor.uri.toString())
|
||||
editor.onDispose(() =>
|
||||
this.handleEditorDidDispose(editor.uri.toString())
|
||||
),
|
||||
(editor) =>
|
||||
editor.editor.onDocumentContentChanged((event) =>
|
||||
editor.onDocumentContentChanged((event) =>
|
||||
this.handleDocumentContentChange(editor, event)
|
||||
)
|
||||
),
|
||||
@ -301,24 +439,13 @@ export class CompilerErrors
|
||||
]);
|
||||
const currentError = this.errors[0];
|
||||
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(
|
||||
errors: Map<string, CoreError.ErrorLocation[]>
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
@ -342,11 +469,11 @@ export class CompilerErrors
|
||||
uri: string,
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
return { dispose: Disposable.NULL, errors: [] };
|
||||
}
|
||||
const oldDecorations = editor.editor.deltaDecorations({
|
||||
const oldDecorations = editor.deltaDecorations({
|
||||
oldDecorations: [],
|
||||
newDecorations: errors.map((error) =>
|
||||
this.compilerErrorDecoration(error.location.range)
|
||||
@ -355,13 +482,19 @@ export class CompilerErrors
|
||||
return {
|
||||
dispose: Disposable.create(() => {
|
||||
if (editor) {
|
||||
editor.editor.deltaDecorations({
|
||||
editor.deltaDecorations({
|
||||
oldDecorations,
|
||||
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: {
|
||||
isWholeLine: true,
|
||||
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.
|
||||
*/
|
||||
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
return;
|
||||
}
|
||||
private handleSelectionChange(
|
||||
monacoEditor: MonacoEditor,
|
||||
selection: Range
|
||||
): void {
|
||||
const uri = monacoEditor.uri.toString();
|
||||
const monacoSelection = this.p2m.asRange(selection);
|
||||
console.log(
|
||||
@ -418,12 +550,13 @@ export class CompilerErrors
|
||||
console.trace('No match');
|
||||
return undefined;
|
||||
};
|
||||
const error = this.errors
|
||||
.filter((error) => error.uri === uri)
|
||||
.map((error) => ({
|
||||
error,
|
||||
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
||||
}))
|
||||
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);
|
||||
@ -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(
|
||||
editor: EditorWidget,
|
||||
monacoEditor: MonacoEditor,
|
||||
event: TextDocumentChangeEvent
|
||||
): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
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
|
||||
const errorsPerResource = this.errors.filter(
|
||||
(error) => error.uri === event.document.uri
|
||||
);
|
||||
if (!relevantChanges.length) {
|
||||
return;
|
||||
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
||||
const doc = event.document;
|
||||
if (doc instanceof MonacoEditorModel) {
|
||||
editorOrModel = doc.textEditorModel;
|
||||
}
|
||||
|
||||
const resolvedMarkers = this.errors
|
||||
.filter((error) => error.uri === event.document.uri)
|
||||
.map((error, index) => {
|
||||
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
||||
if (range) {
|
||||
return { error, range, index };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(notEmpty);
|
||||
|
||||
const decorationIdsToRemove = relevantChanges
|
||||
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((changeRange) =>
|
||||
resolvedMarkers.filter(({ range: decorationRange }) =>
|
||||
changeRange.containsRange(decorationRange)
|
||||
)
|
||||
.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), [])
|
||||
.map(({ error, index }) => {
|
||||
this.errors.splice(index, 1);
|
||||
return error.id;
|
||||
});
|
||||
if (!decorationIdsToRemove.length) {
|
||||
return;
|
||||
.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);
|
||||
}
|
||||
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
|
||||
private async trackEditors(
|
||||
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||
...track: ((editor: EditorWidget) => Disposable)[]
|
||||
...track: ((editor: MonacoEditor) => Disposable)[]
|
||||
): Promise<Disposable> {
|
||||
return new DisposableCollection(
|
||||
...(await Promise.all(
|
||||
Array.from(errors.keys()).map(async (uri) => {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
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) =>
|
||||
ErrorDecoration.sameAs(candidate, error)
|
||||
ErrorDecorationRef.sameAs(candidate, ref)
|
||||
);
|
||||
if (index < 0) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Failed to mark error ${
|
||||
error.id
|
||||
ref.id
|
||||
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||
({ id }) => id
|
||||
)}`
|
||||
@ -550,15 +697,18 @@ export class CompilerErrors
|
||||
}
|
||||
const newError = this.errors[index];
|
||||
if (
|
||||
options?.forceReselect ||
|
||||
!this.currentError ||
|
||||
!ErrorDecoration.sameAs(this.currentError, newError)
|
||||
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
||||
) {
|
||||
this.currentError = this.errors[index];
|
||||
console.log(
|
||||
'compiler-errors',
|
||||
`Current error changed to ${this.currentError.id}`
|
||||
);
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
if (options?.reveal) {
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
}
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
}
|
||||
@ -598,27 +748,28 @@ export class CompilerErrors
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private groupByResource(
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Map<string, CoreError.ErrorLocation[]> {
|
||||
return errors.reduce((acc, curr) => {
|
||||
const {
|
||||
location: { uri },
|
||||
} = curr;
|
||||
let errors = acc.get(uri);
|
||||
if (!errors) {
|
||||
errors = [];
|
||||
acc.set(uri, errors);
|
||||
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);
|
||||
}
|
||||
errors.push(curr);
|
||||
values.push(curr);
|
||||
return acc;
|
||||
}, new Map<string, CoreError.ErrorLocation[]>());
|
||||
}, new Map<K, V[]>());
|
||||
}
|
||||
|
||||
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
||||
private monacoEditor(
|
||||
uriOrWidget: string | EditorWidget
|
||||
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;
|
||||
@ -646,5 +797,8 @@ export namespace CompilerErrors {
|
||||
export const PREVIOUS_ERROR: Command = {
|
||||
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 * as monaco from '@theia/monaco-editor-core';
|
||||
import { Formatter } from '../../common/protocol/formatter';
|
||||
import { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
import { InoSelector } from '../selectors';
|
||||
import { Contribution, URI } from './contribution';
|
||||
|
||||
@injectable()
|
||||
@ -40,7 +39,7 @@ export class Format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.TextEdit[]> {
|
||||
const range = fullRange(model);
|
||||
const range = model.getFullModelRange();
|
||||
const text = await this.format(model, range, options);
|
||||
return [{ range, text }];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||
/**
|
||||
* 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.
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import type {
|
||||
Location,
|
||||
Range,
|
||||
Position,
|
||||
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import type {
|
||||
BoardUserField,
|
||||
Port,
|
||||
@ -15,11 +19,41 @@ export const CompilerWarningLiterals = [
|
||||
] as const;
|
||||
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
||||
export namespace CoreError {
|
||||
export interface ErrorLocation {
|
||||
export interface ErrorLocationRef {
|
||||
readonly message: string;
|
||||
readonly location: Location;
|
||||
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 = {
|
||||
Verify: 4001,
|
||||
Upload: 4002,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +155,8 @@
|
||||
"increaseFontSize": "Increase Font Size",
|
||||
"increaseIndent": "Increase Indent",
|
||||
"nextError": "Next Error",
|
||||
"previousError": "Previous Error"
|
||||
"previousError": "Previous Error",
|
||||
"revealError": "Reveal Error"
|
||||
},
|
||||
"electron": {
|
||||
"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