Skip to content

Commit d6f7640

Browse files
author
Akos Kitta
committed
better codelens.
Signed-off-by: Akos Kitta <[email protected]>
1 parent 8941b64 commit d6f7640

File tree

4 files changed

+388
-195
lines changed

4 files changed

+388
-195
lines changed

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/
292292
import { AboutDialog } from './theia/core/about-dialog';
293293
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
294294
import { CoreErrorHandler } from './contributions/core-error-handler';
295-
import { CompilerErrors } from './contributions/editor-decorations';
295+
import { CompilerErrors } from './contributions/compiler-errors';
296296

297297
MonacoThemingService.register({
298298
id: 'arduino-theme',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import {
2+
Command,
3+
CommandRegistry,
4+
Disposable,
5+
DisposableCollection,
6+
Emitter,
7+
} from '@theia/core';
8+
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
9+
import URI from '@theia/core/lib/common/uri';
10+
import { inject, injectable } from '@theia/core/shared/inversify';
11+
import {
12+
Location,
13+
Range,
14+
} from '@theia/core/shared/vscode-languageserver-protocol';
15+
import { EditorWidget } from '@theia/editor/lib/browser';
16+
import {
17+
EditorDecoration,
18+
TrackedRangeStickiness,
19+
} from '@theia/editor/lib/browser/decorations/editor-decoration';
20+
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
21+
import * as monaco from '@theia/monaco-editor-core';
22+
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
23+
import { CoreError } from '../../common/protocol/core-service';
24+
import { InoSelector } from '../ino-selectors';
25+
import { Contribution } from './contribution';
26+
import { CoreErrorHandler } from './core-error-handler';
27+
28+
@injectable()
29+
export class CompilerErrors
30+
extends Contribution
31+
implements monaco.languages.CodeLensProvider
32+
{
33+
@inject(EditorManager)
34+
private readonly editorManager: EditorManager;
35+
36+
@inject(ProtocolToMonacoConverter)
37+
readonly p2m: ProtocolToMonacoConverter;
38+
39+
@inject(CoreErrorHandler)
40+
private readonly coreErrorHandler: CoreErrorHandler;
41+
42+
private readonly errors: CoreError.Compiler[] = [];
43+
private currentError: CoreError.Compiler | undefined;
44+
/**
45+
* monaco API to rerender the code lens.
46+
*/
47+
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
48+
private readonly currentErrorDidChangEmitter =
49+
new Emitter<CoreError.Compiler>();
50+
private readonly onCurrentErrorDidChange =
51+
this.currentErrorDidChangEmitter.event;
52+
private readonly toDisposeOnCompilerErrorDidChange =
53+
new DisposableCollection();
54+
private shell: ApplicationShell | undefined;
55+
56+
override onStart(app: FrontendApplication): void {
57+
this.shell = app.shell;
58+
monaco.languages.registerCodeLensProvider(InoSelector, this);
59+
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
60+
this.handleCompilerErrorsDidChange(errors)
61+
);
62+
this.onCurrentErrorDidChange(async (error) =>
63+
this.revealLocationInEditor(error.location).then((editor) => {
64+
if (!editor) {
65+
console.warn(
66+
`Failed to mark error ${CoreError.Compiler.toString(
67+
error
68+
)} as the current one.`
69+
);
70+
}
71+
})
72+
);
73+
}
74+
75+
override registerCommands(registry: CommandRegistry): void {
76+
registry.registerCommand(CompilerErrors.Commands.REVEAL_NEXT_ERROR, {
77+
execute: (currentError: CoreError.Compiler) => {
78+
const index = this.errors.findIndex((candidate) =>
79+
CoreError.Compiler.sameAs(candidate, currentError)
80+
);
81+
if (index < 0) {
82+
console.warn(
83+
`Could not advance to next error. ${CoreError.Compiler.toString(
84+
currentError
85+
)} is not a known error.`
86+
);
87+
return;
88+
}
89+
const nextError =
90+
index === this.errors.length - 1
91+
? this.errors[0]
92+
: this.errors[index];
93+
this.markAsCurrentError(nextError);
94+
},
95+
});
96+
registry.registerCommand(CompilerErrors.Commands.REVEAL_PREVIOUS_ERROR, {
97+
execute: (currentError: CoreError.Compiler) => {
98+
const index = this.errors.findIndex((candidate) =>
99+
CoreError.Compiler.sameAs(candidate, currentError)
100+
);
101+
if (index < 0) {
102+
console.warn(
103+
`Could not advance to previous error. ${CoreError.Compiler.toString(
104+
currentError
105+
)} is not a known error.`
106+
);
107+
return;
108+
}
109+
const previousError =
110+
index === 0
111+
? this.errors[this.errors.length - 1]
112+
: this.errors[index];
113+
this.markAsCurrentError(previousError);
114+
},
115+
});
116+
}
117+
118+
get onDidChange(): monaco.IEvent<this> {
119+
return this.onDidChangeEmitter.event;
120+
}
121+
122+
provideCodeLenses(
123+
model: monaco.editor.ITextModel,
124+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
125+
_token: monaco.CancellationToken
126+
): monaco.languages.ProviderResult<monaco.languages.CodeLensList> {
127+
const lenses: monaco.languages.CodeLens[] = [];
128+
if (
129+
this.currentError &&
130+
this.currentError.location.uri === model.uri.toString() &&
131+
this.errors.length > 1
132+
) {
133+
lenses.push(
134+
{
135+
range: this.p2m.asRange(this.currentError.location.range),
136+
command: {
137+
id: CompilerErrors.Commands.REVEAL_PREVIOUS_ERROR.id,
138+
title: 'Go to Previous Error',
139+
arguments: [this.currentError],
140+
},
141+
},
142+
{
143+
range: this.p2m.asRange(this.currentError.location.range),
144+
command: {
145+
id: CompilerErrors.Commands.REVEAL_NEXT_ERROR.id,
146+
title: 'Go to Next Error',
147+
arguments: [this.currentError],
148+
},
149+
}
150+
);
151+
}
152+
return {
153+
lenses,
154+
dispose: () => {
155+
/* NOOP */
156+
},
157+
};
158+
}
159+
160+
private async handleCompilerErrorsDidChange(
161+
errors: CoreError.Compiler[]
162+
): Promise<void> {
163+
this.toDisposeOnCompilerErrorDidChange.dispose();
164+
this.errors.push(...errors);
165+
const compilerErrorsPerResource = this.groupByResource(this.errors);
166+
this.toDisposeOnCompilerErrorDidChange.pushAll([
167+
Disposable.create(() => (this.errors.length = 0)),
168+
...(await Promise.all([
169+
this.decorateEditors(compilerErrorsPerResource),
170+
this.trackEditorsSelection(compilerErrorsPerResource),
171+
])),
172+
]);
173+
const first = errors[0];
174+
if (first) {
175+
await this.markAsCurrentError(first);
176+
}
177+
}
178+
179+
private async decorateEditors(
180+
errors: Map<string, CoreError.Compiler[]>
181+
): Promise<Disposable> {
182+
return new DisposableCollection(
183+
...(await Promise.all(
184+
[...errors.entries()].map(([uri, errors]) =>
185+
this.decorateEditor(uri, errors)
186+
)
187+
))
188+
);
189+
}
190+
191+
private groupByResource(
192+
errors: CoreError.Compiler[]
193+
): Map<string, CoreError.Compiler[]> {
194+
return errors.reduce((acc, curr) => {
195+
const {
196+
location: { uri },
197+
} = curr;
198+
let errors = acc.get(uri);
199+
if (!errors) {
200+
errors = [];
201+
acc.set(uri, errors);
202+
}
203+
errors.push(curr);
204+
return acc;
205+
}, new Map<string, CoreError.Compiler[]>());
206+
}
207+
208+
private async decorateEditor(
209+
uri: string,
210+
errors: CoreError.Compiler[]
211+
): Promise<Disposable> {
212+
const editor = await this.editorManager.getByUri(new URI(uri));
213+
if (!editor) {
214+
return Disposable.NULL;
215+
}
216+
const oldDecorations = editor.editor.deltaDecorations({
217+
oldDecorations: [],
218+
newDecorations: errors.map((error) =>
219+
this.compilerErrorDecoration(error.location.range)
220+
),
221+
});
222+
return Disposable.create(() => {
223+
this.editorManager.getByUri(new URI(uri)).then((e) => {
224+
if (e) {
225+
e.editor.deltaDecorations({ oldDecorations, newDecorations: [] });
226+
}
227+
});
228+
});
229+
}
230+
231+
private compilerErrorDecoration(range: Range): EditorDecoration {
232+
return {
233+
range,
234+
options: {
235+
isWholeLine: true,
236+
className: 'core-error',
237+
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
238+
},
239+
};
240+
}
241+
242+
/**
243+
* 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.
244+
*/
245+
private async trackEditorsSelection(
246+
errors: Map<string, CoreError.Compiler[]>
247+
): Promise<Disposable> {
248+
return new DisposableCollection(
249+
...(await Promise.all(
250+
Array.from(errors.keys()).map(async (uri) => {
251+
const editor = await this.editorManager.getByUri(new URI(uri));
252+
if (!editor) {
253+
return Disposable.NULL;
254+
}
255+
return editor.editor.onSelectionChanged((selection) =>
256+
this.handleSelectionChange(uri, selection)
257+
);
258+
})
259+
))
260+
);
261+
}
262+
263+
private handleSelectionChange(uri: string, selection: Range) {
264+
const monacoSelection = this.p2m.asRange(selection);
265+
console.log(
266+
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
267+
);
268+
const intersectsError = (
269+
candidateErrorRange: monaco.Range,
270+
currentSelection: monaco.Range
271+
) => {
272+
console.trace(`Candidate error range: ${candidateErrorRange.toJSON()}`);
273+
console.trace(`Current selection range: ${currentSelection.toJSON()}`);
274+
// if editor selection intersects with the error range or the selection is in one of the lines of an error.
275+
return (
276+
candidateErrorRange.intersectRanges(currentSelection) ||
277+
(candidateErrorRange.startLineNumber <=
278+
currentSelection.startLineNumber &&
279+
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber)
280+
);
281+
};
282+
const error = this.errors
283+
.filter((error) => error.location.uri === uri)
284+
.find((error) =>
285+
intersectsError(this.p2m.asRange(error.location.range), monacoSelection)
286+
);
287+
if (error) {
288+
this.markAsCurrentError(error);
289+
} else {
290+
console.info(
291+
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
292+
);
293+
}
294+
}
295+
296+
private async markAsCurrentError(error: CoreError.Compiler): Promise<void> {
297+
const index = this.errors.findIndex((candidate) =>
298+
CoreError.Compiler.sameAs(candidate, error)
299+
);
300+
if (index < 0) {
301+
console.warn(
302+
`Failed to mark error ${CoreError.Compiler.toString(
303+
error
304+
)} as the current one. Error is unknown. Known errors are: ${this.errors.map(
305+
CoreError.Compiler.toString
306+
)}`
307+
);
308+
return;
309+
}
310+
this.currentError = this.errors[index];
311+
console.log(
312+
`Current error changed to ${CoreError.Compiler.toString(
313+
this.currentError
314+
)}`
315+
);
316+
this.currentErrorDidChangEmitter.fire(this.currentError);
317+
this.onDidChangeEmitter.fire(this);
318+
}
319+
320+
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
321+
private async revealLocationInEditor(
322+
location: Location
323+
): Promise<EditorWidget | undefined> {
324+
const { uri, range: selection } = location;
325+
const editor = await this.editorManager.getByUri(new URI(uri), {
326+
mode: 'activate',
327+
selection,
328+
});
329+
if (editor && this.shell) {
330+
const activeWidget = await this.shell.activateWidget(editor.id);
331+
if (!activeWidget) {
332+
console.warn(
333+
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
334+
);
335+
return editor;
336+
}
337+
if (editor !== activeWidget) {
338+
console.warn(
339+
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
340+
);
341+
}
342+
return editor;
343+
}
344+
console.warn(`could not found editor widget for URI: ${uri}`);
345+
return undefined;
346+
}
347+
}
348+
export namespace CompilerErrors {
349+
export namespace Commands {
350+
export const REVEAL_NEXT_ERROR: Command = {
351+
id: 'arduino-reveal-next-error',
352+
};
353+
export const REVEAL_PREVIOUS_ERROR: Command = {
354+
id: 'arduino-reveal-previous-error',
355+
};
356+
}
357+
}

0 commit comments

Comments
 (0)