Skip to content

Commit 2f25831

Browse files
author
Akos Kitta
committed
feat: use a monaco editor for the monitor output
Closes arduino#105 Signed-off-by: Akos Kitta <[email protected]>
1 parent c0488d1 commit 2f25831

11 files changed

+554
-158
lines changed

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

+22-14
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ import {
154154
MonitorManagerProxyFactory,
155155
MonitorManagerProxyPath,
156156
} from '../common/protocol';
157-
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
157+
import {
158+
MonacoEditorModelFactory,
159+
MonacoTextModelService as TheiaMonacoTextModelService,
160+
} from '@theia/monaco/lib/browser/monaco-text-model-service';
158161
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
159162
import { ResponseServiceImpl } from './response-service-impl';
160163
import {
@@ -249,7 +252,7 @@ import {
249252
UserFieldsDialog,
250253
UserFieldsDialogProps,
251254
} from './dialogs/user-fields/user-fields-dialog';
252-
import { nls } from '@theia/core/lib/common';
255+
import { nls, ResourceResolver } from '@theia/core/lib/common';
253256
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
254257
import {
255258
IDEUpdater,
@@ -313,6 +316,7 @@ import {
313316
} from './widgets/component-list/filter-renderer';
314317
import { CheckForUpdates } from './contributions/check-for-updates';
315318
import { OutputEditorFactory } from './theia/output/output-editor-factory';
319+
import { OutputEditorFactory as TheiaOutputEditorFactory } from '@theia/output/lib/browser/output-editor-factory';
316320
import { StartupTaskProvider } from '../electron-common/startup-task';
317321
import { DeleteSketch } from './contributions/delete-sketch';
318322
import { UserFields } from './contributions/user-fields';
@@ -331,6 +335,10 @@ import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarc
331335
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
332336
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
333337
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
338+
import { MonitorResourceProvider } from './serial/monitor/monitor-resource-provider';
339+
import { MonitorEditorFactory } from './serial/monitor/monitor-editor-factory';
340+
import { MonitorEditorModelFactory } from './serial/monitor/monitor-editor-model-factory';
341+
import { MonitorContextMenuService } from './serial/monitor/monitor-context-menu-service';
334342

335343
export default new ContainerModule((bind, unbind, isBound, rebind) => {
336344
// Commands and toolbar items
@@ -465,17 +473,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
465473
bind(MonitorModel).toSelf().inSingletonScope();
466474
bindViewContribution(bind, MonitorViewContribution);
467475
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
468-
bind(WidgetFactory).toDynamicValue((context) => ({
476+
bind(WidgetFactory).toDynamicValue(({ container }) => ({
469477
id: MonitorWidget.ID,
470-
createWidget: () => {
471-
return new MonitorWidget(
472-
context.container.get<MonitorModel>(MonitorModel),
473-
context.container.get<MonitorManagerProxyClient>(
474-
MonitorManagerProxyClient
475-
),
476-
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
477-
);
478-
},
478+
createWidget: () => container.get(MonitorWidget),
479479
}));
480480

481481
bind(MonitorManagerProxyFactory).toFactory(
@@ -499,6 +499,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
499499
.to(MonitorManagerProxyClientImpl)
500500
.inSingletonScope();
501501

502+
bind(MonitorResourceProvider).toSelf().inSingletonScope();
503+
bind(ResourceResolver).toService(MonitorResourceProvider);
504+
bind(MonitorEditorFactory).toSelf().inSingletonScope();
505+
bind(MonacoEditorFactory).toService(MonitorEditorFactory);
506+
bind(MonacoEditorModelFactory)
507+
.to(MonitorEditorModelFactory)
508+
.inSingletonScope();
509+
bind(MonitorContextMenuService).toSelf().inSingletonScope();
510+
502511
bind(WorkspaceService).toSelf().inSingletonScope();
503512
rebind(TheiaWorkspaceService).toService(WorkspaceService);
504513
bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
@@ -611,8 +620,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
611620

612621
// To disable the highlighting of non-unicode characters in the _Output_ view
613622
bind(OutputEditorFactory).toSelf().inSingletonScope();
614-
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
615-
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
623+
rebind(TheiaOutputEditorFactory).toService(OutputEditorFactory);
616624

617625
bind(ArduinoDaemon)
618626
.toDynamicValue((context) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { MenuPath } from '@theia/core/lib/common/menu';
2+
import { injectable } from '@theia/core/shared/inversify';
3+
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';
4+
5+
export namespace MonitorContextMenu {
6+
export const MENU_PATH: MenuPath = ['monitor_context_menu'];
7+
export const TEXT_EDIT_GROUP = [...MENU_PATH, '0_text_edit_group'];
8+
export const WIDGET_GROUP = [...MENU_PATH, '1_widget_group'];
9+
}
10+
11+
@injectable()
12+
export class MonitorContextMenuService extends MonacoContextMenuService {
13+
protected override menuPath(): MenuPath {
14+
return MonitorContextMenu.MENU_PATH;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { inject, injectable } from '@theia/core/shared/inversify';
2+
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
3+
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';
4+
import {
5+
EditorServiceOverrides,
6+
MonacoEditor,
7+
} from '@theia/monaco/lib/browser/monaco-editor';
8+
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
9+
import { OutputEditorFactory } from '../../theia/output/output-editor-factory';
10+
import { MonitorContextMenuService } from './monitor-context-menu-service';
11+
import { MonitorUri } from './monitor-uri';
12+
13+
@injectable()
14+
export class MonitorEditorFactory extends OutputEditorFactory {
15+
@inject(MonitorContextMenuService)
16+
private readonly monitorContextMenuService: MonacoContextMenuService;
17+
18+
override readonly scheme: string = MonitorUri.scheme;
19+
20+
protected override createOptions(
21+
model: MonacoEditorModel,
22+
defaultOptions: MonacoEditor.IOptions
23+
): MonacoEditor.IOptions {
24+
return {
25+
...super.createOptions(model, defaultOptions),
26+
// To hide the margin in the editor https://github.com/microsoft/monaco-editor/issues/1960
27+
lineNumbers: 'off',
28+
glyphMargin: false,
29+
folding: false,
30+
lineDecorationsWidth: 0,
31+
lineNumbersMinChars: 0,
32+
};
33+
}
34+
35+
protected override *createOverrides(
36+
model: MonacoEditorModel,
37+
defaultOverrides: EditorServiceOverrides
38+
): EditorServiceOverrides {
39+
yield [IContextMenuService, this.monitorContextMenuService];
40+
for (const [identifier, provider] of defaultOverrides) {
41+
if (identifier !== IContextMenuService) {
42+
yield [identifier, provider];
43+
}
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { OutputEditorModelFactory } from '@theia/output/lib/browser/output-editor-model-factory';
3+
import { MonitorUri } from './monitor-uri';
4+
5+
@injectable()
6+
export class MonitorEditorModelFactory extends OutputEditorModelFactory {
7+
override readonly scheme: string = MonitorUri.scheme;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Deferred } from '@theia/core/lib/common/promise-util';
2+
import { Resource, ResourceResolver } from '@theia/core/lib/common/resource';
3+
import URI from '@theia/core/lib/common/uri';
4+
import { inject, injectable } from '@theia/core/shared/inversify';
5+
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
6+
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
7+
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
8+
import { MonitorResource } from './monitor-resource';
9+
import { MonitorUri } from './monitor-uri';
10+
11+
@injectable()
12+
export class MonitorResourceProvider implements ResourceResolver {
13+
readonly resource: MonitorResource;
14+
15+
constructor(
16+
@inject(MonacoTextModelService) textModelService: MonacoTextModelService
17+
) {
18+
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
19+
this.resource = new MonitorResource(MonitorUri, editorModelRef);
20+
textModelService
21+
.createModelReference(MonitorUri)
22+
.then((ref) => editorModelRef.resolve(ref));
23+
}
24+
25+
async resolve(uri: URI): Promise<Resource> {
26+
if (this.resource.uri.toString() === uri.toString()) {
27+
return this.resource;
28+
}
29+
// Note: this is totally normal. This is the way Theia loads a resource.
30+
throw new Error(`Cannot handle URI: ${uri.toString()}`);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ResourceReadOptions } from '@theia/core/lib/common/resource';
2+
import { OutputResource } from '@theia/output/lib/browser/output-resource';
3+
4+
export class MonitorResource extends OutputResource {
5+
override async readContents(options?: ResourceReadOptions): Promise<string> {
6+
if (!this._textModel) {
7+
return '';
8+
}
9+
return super.readContents(options);
10+
}
11+
12+
async reset(): Promise<void> {
13+
this.textModel?.setValue('');
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import URI from '@theia/core/lib/common/uri';
2+
3+
export const MonitorUri = new URI('monitor:/arduino');

arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx

+59-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as React from '@theia/core/shared/react';
22
import { injectable, inject } from '@theia/core/shared/inversify';
3-
import { AbstractViewContribution, codicon } from '@theia/core/lib/browser';
3+
import {
4+
AbstractViewContribution,
5+
codicon,
6+
CommonCommands,
7+
Widget,
8+
} from '@theia/core/lib/browser';
49
import { MonitorWidget } from './monitor-widget';
510
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
611
import {
@@ -12,6 +17,8 @@ import { ArduinoMenus } from '../../menu/arduino-menus';
1217
import { nls } from '@theia/core/lib/common';
1318
import { MonitorModel } from '../../monitor-model';
1419
import { MonitorManagerProxyClient } from '../../../common/protocol';
20+
import { MonitorContextMenu } from './monitor-context-menu-service';
21+
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
1522

1623
export namespace SerialMonitor {
1724
export namespace Commands {
@@ -36,7 +43,10 @@ export namespace SerialMonitor {
3643
iconClass: codicon('clear-all'),
3744
},
3845
'vscode/output.contribution/clearOutput.label'
39-
);
46+
) as Command & { label: string };
47+
export const COPY_ALL: Command = {
48+
id: 'serial-monitor-copy-all',
49+
};
4050
}
4151
}
4252

@@ -50,6 +60,9 @@ export class MonitorViewContribution
5060
MonitorWidget.ID + ':toggle-toolbar';
5161
static readonly RESET_SERIAL_MONITOR = MonitorWidget.ID + ':reset';
5262

63+
@inject(ClipboardService)
64+
private readonly clipboardService: ClipboardService;
65+
5366
constructor(
5467
@inject(MonitorModel)
5568
protected readonly model: MonitorModel,
@@ -77,6 +90,17 @@ export class MonitorViewContribution
7790
order: '5',
7891
});
7992
}
93+
menus.registerMenuAction(MonitorContextMenu.TEXT_EDIT_GROUP, {
94+
commandId: CommonCommands.COPY.id,
95+
});
96+
menus.registerMenuAction(MonitorContextMenu.TEXT_EDIT_GROUP, {
97+
commandId: SerialMonitor.Commands.COPY_ALL.id,
98+
label: nls.localizeByDefault('Copy All'),
99+
});
100+
menus.registerMenuAction(MonitorContextMenu.WIDGET_GROUP, {
101+
commandId: SerialMonitor.Commands.CLEAR_OUTPUT.id,
102+
label: nls.localizeByDefault('Clear Output'),
103+
});
80104
}
81105

82106
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -104,12 +128,25 @@ export class MonitorViewContribution
104128

105129
override registerCommands(commands: CommandRegistry): void {
106130
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
107-
isEnabled: (widget) => widget instanceof MonitorWidget,
108-
isVisible: (widget) => widget instanceof MonitorWidget,
109-
execute: (widget) => {
110-
if (widget instanceof MonitorWidget) {
111-
widget.clearConsole();
131+
isEnabled: (arg) => {
132+
if (arg instanceof Widget) {
133+
return arg instanceof MonitorWidget;
112134
}
135+
return this.shell.currentWidget instanceof MonitorWidget;
136+
},
137+
isVisible: (arg) => {
138+
if (arg instanceof Widget) {
139+
return arg instanceof MonitorWidget;
140+
}
141+
return this.shell.currentWidget instanceof MonitorWidget;
142+
},
143+
execute: () => {
144+
this.widget.then((widget) => {
145+
this.withWidget(widget, (output) => {
146+
output.clearConsole();
147+
return true;
148+
});
149+
});
113150
},
114151
});
115152
if (this.toggleCommand) {
@@ -129,6 +166,14 @@ export class MonitorViewContribution
129166
{ id: MonitorViewContribution.RESET_SERIAL_MONITOR },
130167
{ execute: () => this.reset() }
131168
);
169+
commands.registerCommand(SerialMonitor.Commands.COPY_ALL, {
170+
execute: () => {
171+
const text = this.tryGetWidget()?.text;
172+
if (text) {
173+
this.clipboardService.writeText(text);
174+
}
175+
},
176+
});
132177
}
133178

134179
protected async toggle(): Promise<void> {
@@ -191,4 +236,11 @@ export class MonitorViewContribution
191236
protected async doToggleTimestamp(): Promise<void> {
192237
this.model.toggleTimestamp();
193238
}
239+
240+
private withWidget(
241+
widget: Widget | undefined = this.tryGetWidget(),
242+
predicate: (monitorWidget: MonitorWidget) => boolean = () => true
243+
): boolean | false {
244+
return widget instanceof MonitorWidget ? predicate(widget) : false;
245+
}
194246
}

0 commit comments

Comments
 (0)