Skip to content

Commit c71c830

Browse files
author
Akos Kitta
committed
moved out decorations from core contrib.
Signed-off-by: Akos Kitta <[email protected]>
1 parent c9fcc18 commit c71c830

File tree

9 files changed

+215
-94
lines changed

9 files changed

+215
-94
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-gen
291291
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
292292
import { AboutDialog } from './theia/core/about-dialog';
293293
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
294+
import { CoreErrorHandler } from './contributions/core-error-handler';
295+
import { EditorDecorations } from './contributions/editor-decorations';
294296

295297
MonacoThemingService.register({
296298
id: 'arduino-theme',
@@ -423,6 +425,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
423425
)
424426
)
425427
.inSingletonScope();
428+
bind(CoreErrorHandler).toSelf().inSingletonScope();
426429

427430
// Serial monitor
428431
bind(MonitorWidget).toSelf();
@@ -668,6 +671,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
668671
Contribution.configure(bind, AddZipLibrary);
669672
Contribution.configure(bind, PlotterFrontendContribution);
670673
Contribution.configure(bind, Format);
674+
Contribution.configure(bind, EditorDecorations);
671675

672676
// Disabled the quick-pick customization from Theia when multiple formatters are available.
673677
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

arduino-ide-extension/src/browser/contributions/contribution.ts

+5-80
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
1111
import { MaybePromise } from '@theia/core/lib/common/types';
1212
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
1313
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
14-
import { TextEditor } from '@theia/editor/lib/browser/editor';
15-
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
16-
import type { Range } from '@theia/core/shared/vscode-languageserver-protocol';
1714
import { MessageService } from '@theia/core/lib/common/message-service';
1815
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
1916
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
2017
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
21-
import { TrackedRangeStickiness } from '@theia/editor/lib/browser/decorations/editor-decoration';
18+
2219
import {
2320
MenuModelRegistry,
2421
MenuContribution,
@@ -57,7 +54,7 @@ import {
5754
} from '../../common/protocol';
5855
import { ArduinoPreferences } from '../arduino-preferences';
5956
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
60-
import { notEmpty } from '@theia/core';
57+
import { CoreErrorHandler } from './core-error-handler';
6158

6259
export {
6360
Command,
@@ -176,86 +173,14 @@ export class CoreServiceContribution extends SketchContribution {
176173
@inject(CoreService)
177174
protected readonly coreService: CoreService;
178175

179-
private shell: ApplicationShell | undefined;
180-
/**
181-
* Keys are the file URIs per editor, the values are the delta decorations to remove before creating new ones.
182-
*/
183-
private readonly editorDecorations = new Map<string, string[]>();
184-
185-
override onStart(app: FrontendApplication): MaybePromise<void> {
186-
this.shell = app.shell;
187-
}
188-
189-
protected async discardEditorMarkers(): Promise<void> {
190-
return new Promise<void>((resolve) => {
191-
Promise.all(
192-
Array.from(this.editorDecorations.entries()).map(
193-
async ([uri, decorations]) => {
194-
const editor = await this.editorManager.getByUri(new URI(uri));
195-
if (editor) {
196-
editor.editor.deltaDecorations({
197-
oldDecorations: decorations,
198-
newDecorations: [],
199-
});
200-
}
201-
this.editorDecorations.delete(uri);
202-
}
203-
)
204-
).then(() => resolve());
205-
});
206-
}
176+
@inject(CoreErrorHandler)
177+
protected readonly coreErrorHandler: CoreErrorHandler;
207178

208-
/**
209-
* The returning promise resolves when the error was handled. Rejects if the error could not be handled.
210-
*/
211179
protected handleError(error: unknown): void {
212-
this.tryHighlightErrorLocation(error);
180+
this.coreErrorHandler.tryHandle(error);
213181
this.tryToastErrorMessage(error);
214182
}
215183

216-
private tryHighlightErrorLocation(error: unknown): void {
217-
if (CoreError.is(error)) {
218-
error.data
219-
.map(({ location }) => location)
220-
.filter(notEmpty)
221-
.forEach((location) => {
222-
const { uri, range } = location;
223-
const { start, end } = range;
224-
// The double editor activation logic is apparently required: https://github.com/eclipse-theia/theia/issues/11284;
225-
this.editorManager
226-
.getByUri(new URI(uri), { mode: 'activate', selection: range })
227-
.then(async (editor) => {
228-
if (editor && this.shell) {
229-
await this.shell.activateWidget(editor.id);
230-
this.markErrorLocationInEditor(editor.editor, {
231-
start: start,
232-
end: { ...end, character: 1 << 30 },
233-
});
234-
}
235-
});
236-
});
237-
}
238-
}
239-
240-
private markErrorLocationInEditor(editor: TextEditor, range: Range): void {
241-
this.editorDecorations.set(
242-
editor.uri.toString(),
243-
editor.deltaDecorations({
244-
oldDecorations: [],
245-
newDecorations: [
246-
{
247-
range,
248-
options: {
249-
isWholeLine: true,
250-
className: 'core-error',
251-
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
252-
},
253-
},
254-
],
255-
})
256-
);
257-
}
258-
259184
private tryToastErrorMessage(error: unknown): void {
260185
let message: undefined | string = undefined;
261186
if (CoreError.is(error)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Emitter, Event } from '@theia/core';
2+
import { injectable } from '@theia/core/shared/inversify';
3+
import { CoreError } from '../../common/protocol/core-service';
4+
5+
@injectable()
6+
export class CoreErrorHandler {
7+
private readonly compilerErrors: CoreError.Compiler[] = [];
8+
private readonly compilerErrorsDidChangeEmitter = new Emitter<
9+
CoreError.Compiler[]
10+
>();
11+
12+
tryHandle(error: unknown): void {
13+
if (CoreError.is(error)) {
14+
this.compilerErrors.length = 0;
15+
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
16+
this.fireCompilerErrorsDidChange();
17+
}
18+
}
19+
20+
reset(): void {
21+
this.compilerErrors.length = 0;
22+
this.fireCompilerErrorsDidChange();
23+
}
24+
25+
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
26+
return this.compilerErrorsDidChangeEmitter.event;
27+
}
28+
29+
private fireCompilerErrorsDidChange(): void {
30+
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Disposable, DisposableCollection } from '@theia/core';
2+
import URI from '@theia/core/lib/common/uri';
3+
import { inject, injectable } from '@theia/core/shared/inversify';
4+
import {
5+
Location,
6+
Range,
7+
} from '@theia/core/shared/vscode-languageserver-protocol';
8+
import { CoreError } from '../../common/protocol/core-service';
9+
import { Contribution } from './contribution';
10+
import { CoreErrorHandler } from './core-error-handler';
11+
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
12+
import { EditorWidget } from '@theia/editor/lib/browser';
13+
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
14+
import {
15+
EditorDecoration,
16+
TrackedRangeStickiness,
17+
} from '@theia/editor/lib/browser/decorations/editor-decoration';
18+
19+
@injectable()
20+
export class EditorDecorations extends Contribution {
21+
@inject(EditorManager)
22+
private readonly editorManager: EditorManager;
23+
24+
@inject(CoreErrorHandler)
25+
private readonly coreErrorHandler: CoreErrorHandler;
26+
27+
private shell: ApplicationShell | undefined;
28+
29+
private readonly toDisposeOnCompilerErrorDidChange =
30+
new DisposableCollection();
31+
32+
override onStart(app: FrontendApplication): void {
33+
this.shell = app.shell;
34+
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
35+
this.handleCompilerErrorsDidChange(errors)
36+
);
37+
}
38+
39+
private async handleCompilerErrorsDidChange(
40+
errors: CoreError.Compiler[]
41+
): Promise<void> {
42+
this.toDisposeOnCompilerErrorDidChange.dispose();
43+
this.toDisposeOnCompilerErrorDidChange.pushAll(
44+
await Promise.all([
45+
this.decorateEditors(errors),
46+
this.registerCodeLens(errors),
47+
])
48+
);
49+
const first = errors[0];
50+
if (first) {
51+
await this.revealLocationInEditor(first.location);
52+
}
53+
}
54+
55+
private async decorateEditors(
56+
errors: CoreError.Compiler[]
57+
): Promise<Disposable> {
58+
return new DisposableCollection(
59+
...(await Promise.all(
60+
[
61+
...errors
62+
.reduce((acc, curr) => {
63+
const {
64+
location: { uri },
65+
} = curr;
66+
let errors = acc.get(uri);
67+
if (!errors) {
68+
errors = [];
69+
acc.set(uri, errors);
70+
}
71+
errors.push(curr);
72+
return acc;
73+
}, new Map<string, CoreError.Compiler[]>())
74+
.entries(),
75+
].map(([uri, errors]) => this.decorateEditor(uri, errors))
76+
))
77+
);
78+
}
79+
80+
private async decorateEditor(
81+
uri: string,
82+
errors: CoreError.Compiler[]
83+
): Promise<Disposable> {
84+
const editor = await this.editorManager.getByUri(new URI(uri));
85+
if (!editor) {
86+
return Disposable.NULL;
87+
}
88+
const oldDecorations = editor.editor.deltaDecorations({
89+
oldDecorations: [],
90+
newDecorations: errors.map((error) =>
91+
this.compilerErrorDecoration(error.location.range)
92+
),
93+
});
94+
return Disposable.create(() => {
95+
this.editorManager.getByUri(new URI(uri)).then((e) => {
96+
if (e) {
97+
e.editor.deltaDecorations({ oldDecorations, newDecorations: [] });
98+
}
99+
});
100+
});
101+
}
102+
103+
private compilerErrorDecoration(range: Range): EditorDecoration {
104+
return {
105+
range,
106+
options: {
107+
isWholeLine: true,
108+
className: 'core-error',
109+
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
110+
},
111+
};
112+
}
113+
114+
private async registerCodeLens(
115+
errors: CoreError.Compiler[]
116+
): Promise<Disposable> {
117+
return new DisposableCollection();
118+
}
119+
120+
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
121+
private async revealLocationInEditor(
122+
location: Location
123+
): Promise<EditorWidget | undefined> {
124+
const { uri, range: selection } = location;
125+
const editor = await this.editorManager.getByUri(new URI(uri), {
126+
mode: 'activate',
127+
selection,
128+
});
129+
if (editor && this.shell) {
130+
const activeWidget = await this.shell.activateWidget(editor.id);
131+
if (!activeWidget) {
132+
console.warn(
133+
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
134+
);
135+
return editor;
136+
}
137+
if (editor !== activeWidget) {
138+
console.warn(
139+
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
140+
);
141+
}
142+
return editor;
143+
}
144+
console.warn(`could not found editor widget for URI: ${uri}`);
145+
return undefined;
146+
}
147+
}

arduino-ide-extension/src/browser/contributions/upload-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class UploadSketch extends CoreServiceContribution {
207207
// toggle the toolbar button and menu item state.
208208
// uploadInProgress will be set to false whether the upload fails or not
209209
this.uploadInProgress = true;
210-
await this.discardEditorMarkers();
210+
this.coreErrorHandler.reset();
211211
this.onDidChangeEmitter.fire();
212212
const { boardsConfig } = this.boardsServiceClientImpl;
213213
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =

arduino-ide-extension/src/browser/contributions/verify-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class VerifySketch extends CoreServiceContribution {
9898
}
9999
try {
100100
this.verifyInProgress = true;
101-
await this.discardEditorMarkers();
101+
this.coreErrorHandler.reset();
102102
this.onDidChangeEmitter.fire();
103103
const { boardsConfig } = this.boardsServiceClientImpl;
104104
const [fqbn, sourceOverride] = await Promise.all([

arduino-ide-extension/src/common/protocol/core-service.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ApplicationError } from '@theia/core';
2+
import { Location } from '@theia/core/shared/vscode-languageserver-protocol';
23
import { BoardUserField } from '.';
34
import { Board, Port } from '../../common/protocol/boards-service';
5+
import { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
46
import { Programmer } from './boards-service';
57
import { Sketch } from './sketches-service';
6-
import { ErrorInfo } from '../../node/cli-error-parser';
78

89
export const CompilerWarningLiterals = [
910
'None',
@@ -13,7 +14,17 @@ export const CompilerWarningLiterals = [
1314
] as const;
1415
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
1516
export namespace CoreError {
16-
export type Info = ErrorInfo;
17+
export type ErrorInfo = CliErrorInfo;
18+
export interface Compiler extends ErrorInfo {
19+
readonly message: string;
20+
readonly location: Location;
21+
}
22+
export namespace Compiler {
23+
export function is(error: ErrorInfo): error is Compiler {
24+
const { message, location } = error;
25+
return !!message && !!location;
26+
}
27+
}
1728
export const Codes = {
1829
Verify: 4001,
1930
Upload: 4002,
@@ -28,17 +39,19 @@ export namespace CoreError {
2839
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
2940
export function is(
3041
error: unknown
31-
): error is ApplicationError<number, Info[]> {
42+
): error is ApplicationError<number, ErrorInfo[]> {
3243
return (
3344
error instanceof Error &&
3445
ApplicationError.is(error) &&
3546
Object.values(Codes).includes(error.code)
3647
);
3748
}
38-
function create(code: number): ApplicationError.Constructor<number, Info[]> {
49+
function create(
50+
code: number
51+
): ApplicationError.Constructor<number, ErrorInfo[]> {
3952
return ApplicationError.declare(
4053
code,
41-
({ message, stack }: Error, data: Info[]) => {
54+
({ message, stack }: Error, data: ErrorInfo[]) => {
4255
return {
4356
data,
4457
message,

0 commit comments

Comments
 (0)