Skip to content

Commit

Permalink
feat: use a monaco editor for the monitor output
Browse files Browse the repository at this point in the history
Closes arduino#105

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
  • Loading branch information
Akos Kitta committed Nov 29, 2022
1 parent d6a4b0f commit 732e1ad
Show file tree
Hide file tree
Showing 11 changed files with 554 additions and 158 deletions.
36 changes: 22 additions & 14 deletions arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ import {
MonitorManagerProxyFactory,
MonitorManagerProxyPath,
} from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import {
MonacoEditorModelFactory,
MonacoTextModelService as TheiaMonacoTextModelService,
} from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
import {
Expand Down Expand Up @@ -249,7 +252,7 @@ import {
UserFieldsDialog,
UserFieldsDialogProps,
} from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/common';
import { nls, ResourceResolver } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
import {
IDEUpdater,
Expand Down Expand Up @@ -313,6 +316,7 @@ import {
} from './widgets/component-list/filter-renderer';
import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { OutputEditorFactory as TheiaOutputEditorFactory } from '@theia/output/lib/browser/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
import { UserFields } from './contributions/user-fields';
Expand All @@ -331,6 +335,10 @@ import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarc
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { MonitorResourceProvider } from './serial/monitor/monitor-resource-provider';
import { MonitorEditorFactory } from './serial/monitor/monitor-editor-factory';
import { MonitorEditorModelFactory } from './serial/monitor/monitor-editor-model-factory';
import { MonitorContextMenuService } from './serial/monitor/monitor-context-menu-service';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
Expand Down Expand Up @@ -465,17 +473,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: MonitorWidget.ID,
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
},
createWidget: () => container.get(MonitorWidget),
}));

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

bind(MonitorResourceProvider).toSelf().inSingletonScope();
bind(ResourceResolver).toService(MonitorResourceProvider);
bind(MonitorEditorFactory).toSelf().inSingletonScope();
bind(MonacoEditorFactory).toService(MonitorEditorFactory);
bind(MonacoEditorModelFactory)
.to(MonitorEditorModelFactory)
.inSingletonScope();
bind(MonitorContextMenuService).toSelf().inSingletonScope();

bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
Expand Down Expand Up @@ -611,8 +620,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {

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

bind(ArduinoDaemon)
.toDynamicValue((context) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { MenuPath } from '@theia/core/lib/common/menu';
import { injectable } from '@theia/core/shared/inversify';
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';

export namespace MonitorContextMenu {
export const MENU_PATH: MenuPath = ['monitor_context_menu'];
export const TEXT_EDIT_GROUP = [...MENU_PATH, '0_text_edit_group'];
export const WIDGET_GROUP = [...MENU_PATH, '1_widget_group'];
}

@injectable()
export class MonitorContextMenuService extends MonacoContextMenuService {
protected override menuPath(): MenuPath {
return MonitorContextMenu.MENU_PATH;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu';
import {
EditorServiceOverrides,
MonacoEditor,
} from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { OutputEditorFactory } from '../../theia/output/output-editor-factory';
import { MonitorContextMenuService } from './monitor-context-menu-service';
import { MonitorUri } from './monitor-uri';

@injectable()
export class MonitorEditorFactory extends OutputEditorFactory {
@inject(MonitorContextMenuService)
private readonly monitorContextMenuService: MonacoContextMenuService;

override readonly scheme: string = MonitorUri.scheme;

protected override createOptions(
model: MonacoEditorModel,
defaultOptions: MonacoEditor.IOptions
): MonacoEditor.IOptions {
return {
...super.createOptions(model, defaultOptions),
// To hide the margin in the editor https://github.com/microsoft/monaco-editor/issues/1960
lineNumbers: 'off',
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
};
}

protected override *createOverrides(
model: MonacoEditorModel,
defaultOverrides: EditorServiceOverrides
): EditorServiceOverrides {
yield [IContextMenuService, this.monitorContextMenuService];
for (const [identifier, provider] of defaultOverrides) {
if (identifier !== IContextMenuService) {
yield [identifier, provider];
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { injectable } from '@theia/core/shared/inversify';
import { OutputEditorModelFactory } from '@theia/output/lib/browser/output-editor-model-factory';
import { MonitorUri } from './monitor-uri';

@injectable()
export class MonitorEditorModelFactory extends OutputEditorModelFactory {
override readonly scheme: string = MonitorUri.scheme;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Resource, ResourceResolver } from '@theia/core/lib/common/resource';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonitorResource } from './monitor-resource';
import { MonitorUri } from './monitor-uri';

@injectable()
export class MonitorResourceProvider implements ResourceResolver {
readonly resource: MonitorResource;

constructor(
@inject(MonacoTextModelService) textModelService: MonacoTextModelService
) {
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
this.resource = new MonitorResource(MonitorUri, editorModelRef);
textModelService
.createModelReference(MonitorUri)
.then((ref) => editorModelRef.resolve(ref));
}

async resolve(uri: URI): Promise<Resource> {
if (this.resource.uri.toString() === uri.toString()) {
return this.resource;
}
// Note: this is totally normal. This is the way Theia loads a resource.
throw new Error(`Cannot handle URI: ${uri.toString()}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ResourceReadOptions } from '@theia/core/lib/common/resource';
import { OutputResource } from '@theia/output/lib/browser/output-resource';

export class MonitorResource extends OutputResource {
override async readContents(options?: ResourceReadOptions): Promise<string> {
if (!this._textModel) {
return '';
}
return super.readContents(options);
}

async reset(): Promise<void> {
this.textModel?.setValue('');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import URI from '@theia/core/lib/common/uri';

export const MonitorUri = new URI('monitor:/arduino');
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { AbstractViewContribution, codicon } from '@theia/core/lib/browser';
import {
AbstractViewContribution,
codicon,
CommonCommands,
Widget,
} from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
import {
Expand All @@ -12,6 +17,8 @@ import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MonitorModel } from '../../monitor-model';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorContextMenu } from './monitor-context-menu-service';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';

export namespace SerialMonitor {
export namespace Commands {
Expand All @@ -36,7 +43,10 @@ export namespace SerialMonitor {
iconClass: codicon('clear-all'),
},
'vscode/output.contribution/clearOutput.label'
);
) as Command & { label: string };
export const COPY_ALL: Command = {
id: 'serial-monitor-copy-all',
};
}
}

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

@inject(ClipboardService)
private readonly clipboardService: ClipboardService;

constructor(
@inject(MonitorModel)
protected readonly model: MonitorModel,
Expand Down Expand Up @@ -77,6 +90,17 @@ export class MonitorViewContribution
order: '5',
});
}
menus.registerMenuAction(MonitorContextMenu.TEXT_EDIT_GROUP, {
commandId: CommonCommands.COPY.id,
});
menus.registerMenuAction(MonitorContextMenu.TEXT_EDIT_GROUP, {
commandId: SerialMonitor.Commands.COPY_ALL.id,
label: nls.localizeByDefault('Copy All'),
});
menus.registerMenuAction(MonitorContextMenu.WIDGET_GROUP, {
commandId: SerialMonitor.Commands.CLEAR_OUTPUT.id,
label: nls.localizeByDefault('Clear Output'),
});
}

registerToolbarItems(registry: TabBarToolbarRegistry): void {
Expand Down Expand Up @@ -104,12 +128,25 @@ export class MonitorViewContribution

override registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
execute: (widget) => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
isEnabled: (arg) => {
if (arg instanceof Widget) {
return arg instanceof MonitorWidget;
}
return this.shell.currentWidget instanceof MonitorWidget;
},
isVisible: (arg) => {
if (arg instanceof Widget) {
return arg instanceof MonitorWidget;
}
return this.shell.currentWidget instanceof MonitorWidget;
},
execute: () => {
this.widget.then((widget) => {
this.withWidget(widget, (output) => {
output.clearConsole();
return true;
});
});
},
});
if (this.toggleCommand) {
Expand All @@ -129,6 +166,14 @@ export class MonitorViewContribution
{ id: MonitorViewContribution.RESET_SERIAL_MONITOR },
{ execute: () => this.reset() }
);
commands.registerCommand(SerialMonitor.Commands.COPY_ALL, {
execute: () => {
const text = this.tryGetWidget()?.text;
if (text) {
this.clipboardService.writeText(text);
}
},
});
}

protected async toggle(): Promise<void> {
Expand Down Expand Up @@ -191,4 +236,11 @@ export class MonitorViewContribution
protected async doToggleTimestamp(): Promise<void> {
this.model.toggleTimestamp();
}

private withWidget(
widget: Widget | undefined = this.tryGetWidget(),
predicate: (monitorWidget: MonitorWidget) => boolean = () => true
): boolean | false {
return widget instanceof MonitorWidget ? predicate(widget) : false;
}
}
Loading

0 comments on commit 732e1ad

Please sign in to comment.