diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts b/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts index 41cb4f450..d8e08c010 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-utils.ts @@ -67,3 +67,7 @@ export function truncateLines( } return [lines, charCount]; } + +export function linesToMergedStr(lines: Line[]) : string { + return lines.map((line: Line) => {return line.message}).join(""); +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx index 98bf53625..b9d8268ae 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx @@ -52,6 +52,14 @@ export namespace SerialMonitor { }, 'vscode/output.contribution/clearOutput.label' ); + export const COPY_OUTPUT = Command.toLocalizedCommand( + { + id: 'serial-monitor-copy-output', + label: 'Copy Output', + iconClass: codicon('copy'), + }, + 'arduino/serial/copyOutput' + ); } } @@ -149,6 +157,14 @@ export class MonitorViewContribution 'Clear Output' ), }); + registry.registerItem({ + id: SerialMonitor.Commands.COPY_OUTPUT.id, + command: SerialMonitor.Commands.COPY_OUTPUT.id, + tooltip: nls.localize( + 'arduino/serial/copyOutput', + 'Copy Output' + ), + }); } override registerCommands(commands: CommandRegistry): void { @@ -161,6 +177,15 @@ export class MonitorViewContribution } }, }); + commands.registerCommand(SerialMonitor.Commands.COPY_OUTPUT, { + isEnabled: (widget) => widget instanceof MonitorWidget, + isVisible: (widget) => widget instanceof MonitorWidget, + execute: (widget) => { + if (widget instanceof MonitorWidget) { + widget.copyOutput(); + } + }, + }); if (this.toggleCommand) { commands.registerCommand(this.toggleCommand, { execute: () => this.toggle(), diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index f5c394603..5d8c77b11 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -28,6 +28,7 @@ import { import { MonitorModel } from '../../monitor-model'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { serialMonitorWidgetLabel } from '../../../common/nls'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; @injectable() export class MonitorWidget extends ReactWidget { @@ -47,6 +48,7 @@ export class MonitorWidget extends ReactWidget { */ protected closing = false; protected readonly clearOutputEmitter = new Emitter(); + protected readonly copyOutputEmitter = new Emitter(); @inject(MonitorModel) private readonly monitorModel: MonitorModel; @@ -56,6 +58,8 @@ export class MonitorWidget extends ReactWidget { private readonly boardsServiceProvider: BoardsServiceProvider; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; + @inject(ClipboardService) + private readonly clipboardService: ClipboardService; private readonly toDisposeOnReset: DisposableCollection; @@ -102,6 +106,11 @@ export class MonitorWidget extends ReactWidget { this.clearOutputEmitter.fire(undefined); this.update(); } + + copyOutput(): void { + this.copyOutputEmitter.fire(); + this.update(); + } override dispose(): void { this.toDisposeOnReset.dispose(); @@ -247,6 +256,8 @@ export class MonitorWidget extends ReactWidget { monitorModel={this.monitorModel} monitorManagerProxy={this.monitorManagerProxy} clearConsoleEvent={this.clearOutputEmitter.event} + copyOutputEvent={this.copyOutputEmitter.event} + clipboardService={this.clipboardService} height={Math.floor(this.widgetHeight - 50)} /> diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx index ec2327ad5..4d075fd7b 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx @@ -3,9 +3,10 @@ import { Event } from '@theia/core/lib/common/event'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { areEqual, FixedSizeList as List } from 'react-window'; import dateFormat from 'dateformat'; -import { messagesToLines, truncateLines } from './monitor-utils'; +import { messagesToLines, truncateLines, linesToMergedStr } from './monitor-utils'; import { MonitorManagerProxyClient } from '../../../common/protocol'; import { MonitorModel } from '../../monitor-model'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; export type Line = { message: string; timestamp?: Date; lineLen: number }; @@ -74,6 +75,9 @@ export class SerialMonitorOutput extends React.Component< this.props.clearConsoleEvent(() => this.setState({ lines: [], charCount: 0 }) ), + this.props.copyOutputEvent(() => + this.props.clipboardService.writeText(linesToMergedStr(this.state.lines)) + ), this.props.monitorModel.onChange(({ property }) => { if (property === 'timestamp') { const { timestamp } = this.props.monitorModel; @@ -130,6 +134,8 @@ export namespace SerialMonitorOutput { readonly monitorModel: MonitorModel; readonly monitorManagerProxy: MonitorManagerProxyClient; readonly clearConsoleEvent: Event; + readonly copyOutputEvent: Event; + readonly clipboardService: ClipboardService; readonly height: number; } diff --git a/arduino-ide-extension/src/test/browser/monitor-utils.test.ts b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts index cf1025740..4d8a9f486 100644 --- a/arduino-ide-extension/src/test/browser/monitor-utils.test.ts +++ b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { messagesToLines, truncateLines, + linesToMergedStr, } from '../../browser/serial/monitor/monitor-utils'; import { Line } from '../../browser/serial/monitor/serial-monitor-send-output'; import { set, reset } from 'mockdate'; @@ -15,6 +16,7 @@ type TestLine = { charCount: number; maxCharacters?: number; }; + expectedMerged?: string; }; const date = new Date(); @@ -22,6 +24,7 @@ const testLines: TestLine[] = [ { messages: ['Hello'], expected: { lines: [{ message: 'Hello', lineLen: 5 }], charCount: 5 }, + expectedMerged: 'Hello', }, { messages: ['Hello', 'Dog!'], @@ -36,6 +39,7 @@ const testLines: TestLine[] = [ ], charCount: 10, }, + expectedMerged: 'Hello\nDog!' }, { messages: ['Dog!'], @@ -67,6 +71,7 @@ const testLines: TestLine[] = [ { message: "You're a good boy!", lineLen: 8 }, ], }, + expectedMerged: "Hello Dog!\n Who's a good boy?\nYou're a good boy!", }, { messages: ['boy?\n', "You're a good boy!"], @@ -116,6 +121,7 @@ const testLines: TestLine[] = [ { message: 'Yo', lineLen: 2 }, ], }, + expectedMerged: "Hello Dog!\nWho's a good boy?\nYo", }, ]; @@ -165,6 +171,10 @@ describe('Monitor Utils', () => { }); expect(totalCharCount).to.equal(charCount); } + if (testLine.expectedMerged) { + const merged_str = linesToMergedStr(testLine.expected.lines); + expect(merged_str).to.equal(testLine.expectedMerged); + } }); }); });