Skip to content

Commit 483c0a3

Browse files
author
Alberto Iannaccone
committed
split monitor widget in different files to improve readability and testability
1 parent 6cbbf2a commit 483c0a3

File tree

4 files changed

+299
-284
lines changed

4 files changed

+299
-284
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Line, SerialMonitorOutput } from './serial-monitor-send-output';
2+
3+
export function messageToLines(
4+
messages: string[],
5+
prevLines: Line[],
6+
separator = '\n'
7+
): [Line[], number] {
8+
const linesToAdd: Line[] = prevLines.length
9+
? [prevLines[prevLines.length - 1]]
10+
: [{ message: '' }];
11+
let charCount = 0;
12+
13+
for (const message of messages) {
14+
charCount += message.length;
15+
const lastLine = linesToAdd[linesToAdd.length - 1];
16+
17+
if (lastLine.message.charAt(lastLine.message.length - 1) === separator) {
18+
linesToAdd.push({ message, timestamp: new Date() });
19+
} else {
20+
linesToAdd[linesToAdd.length - 1].message += message;
21+
if (!linesToAdd[linesToAdd.length - 1].timestamp) {
22+
linesToAdd[linesToAdd.length - 1].timestamp = new Date();
23+
}
24+
}
25+
}
26+
27+
prevLines.splice(prevLines.length - 1, 1, ...linesToAdd);
28+
return [prevLines, charCount];
29+
}
30+
31+
export function truncateLines(
32+
lines: Line[],
33+
charCount: number
34+
): [Line[], number] {
35+
let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS;
36+
while (charsToDelete > 0) {
37+
const firstLineLength = lines[0]?.message?.length;
38+
const newFirstLine = lines[0]?.message?.substring(charsToDelete);
39+
const deletedCharsCount = firstLineLength - newFirstLine.length;
40+
charCount -= deletedCharsCount;
41+
charsToDelete -= deletedCharsCount;
42+
lines[0].message = newFirstLine;
43+
if (!newFirstLine?.length) {
44+
lines.shift();
45+
}
46+
}
47+
return [lines, charCount];
48+
}
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
import * as React from 'react';
22
import { postConstruct, injectable, inject } from 'inversify';
33
import { OptionsType } from 'react-select/src/types';
4-
import { isOSX } from '@theia/core/lib/common/os';
5-
import { Event, Emitter } from '@theia/core/lib/common/event';
6-
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
7-
import {
8-
DisposableCollection,
9-
Disposable,
10-
} from '@theia/core/lib/common/disposable';
4+
import { Emitter } from '@theia/core/lib/common/event';
5+
import { Disposable } from '@theia/core/lib/common/disposable';
116
import {
127
ReactWidget,
138
Message,
149
Widget,
1510
MessageLoop,
1611
} from '@theia/core/lib/browser/widgets';
17-
import { Board, Port } from '../../common/protocol/boards-service';
1812
import { MonitorConfig } from '../../common/protocol/monitor-service';
1913
import { ArduinoSelect } from '../widgets/arduino-select';
2014
import { MonitorModel } from './monitor-model';
2115
import { MonitorConnection } from './monitor-connection';
22-
import { FixedSizeList as List } from 'react-window';
23-
import AutoSizer from 'react-virtualized-auto-sizer';
24-
import dateFormat = require('dateformat');
16+
import { SerialMonitorSendInput } from './serial-monitor-send-input';
17+
import { SerialMonitorOutput } from './serial-monitor-send-output';
2518

2619
@injectable()
2720
export class MonitorWidget extends ReactWidget {
@@ -120,7 +113,9 @@ export class MonitorWidget extends ReactWidget {
120113
);
121114
};
122115

123-
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
116+
protected get lineEndings(): OptionsType<
117+
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
118+
> {
124119
return [
125120
{
126121
label: 'No Line Ending',
@@ -141,7 +136,9 @@ export class MonitorWidget extends ReactWidget {
141136
];
142137
}
143138

144-
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
139+
protected get baudRates(): OptionsType<
140+
SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
141+
> {
145142
const baudRates: Array<MonitorConfig.BaudRate> = [
146143
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
147144
];
@@ -206,283 +203,14 @@ export class MonitorWidget extends ReactWidget {
206203
}
207204

208205
protected readonly onChangeLineEnding = (
209-
option: SelectOption<MonitorModel.EOL>
206+
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
210207
) => {
211208
this.monitorModel.lineEnding = option.value;
212209
};
213210

214211
protected readonly onChangeBaudRate = (
215-
option: SelectOption<MonitorConfig.BaudRate>
212+
option: SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
216213
) => {
217214
this.monitorModel.baudRate = option.value;
218215
};
219216
}
220-
221-
export namespace SerialMonitorSendInput {
222-
export interface Props {
223-
readonly monitorConfig?: MonitorConfig;
224-
readonly onSend: (text: string) => void;
225-
readonly resolveFocus: (element: HTMLElement | undefined) => void;
226-
}
227-
export interface State {
228-
text: string;
229-
}
230-
}
231-
232-
export class SerialMonitorSendInput extends React.Component<
233-
SerialMonitorSendInput.Props,
234-
SerialMonitorSendInput.State
235-
> {
236-
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
237-
super(props);
238-
this.state = { text: '' };
239-
this.onChange = this.onChange.bind(this);
240-
this.onSend = this.onSend.bind(this);
241-
this.onKeyDown = this.onKeyDown.bind(this);
242-
}
243-
244-
render(): React.ReactNode {
245-
return (
246-
<input
247-
ref={this.setRef}
248-
type="text"
249-
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
250-
placeholder={this.placeholder}
251-
value={this.state.text}
252-
onChange={this.onChange}
253-
onKeyDown={this.onKeyDown}
254-
/>
255-
);
256-
}
257-
258-
protected get placeholder(): string {
259-
const { monitorConfig } = this.props;
260-
if (!monitorConfig) {
261-
return 'Not connected. Select a board and a port to connect automatically.';
262-
}
263-
const { board, port } = monitorConfig;
264-
return `Message (${
265-
isOSX ? '⌘' : 'Ctrl'
266-
}+Enter to send message to '${Board.toString(board, {
267-
useFqbn: false,
268-
})}' on '${Port.toString(port)}')`;
269-
}
270-
271-
protected setRef = (element: HTMLElement | null) => {
272-
if (this.props.resolveFocus) {
273-
this.props.resolveFocus(element || undefined);
274-
}
275-
};
276-
277-
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
278-
this.setState({ text: event.target.value });
279-
}
280-
281-
protected onSend(): void {
282-
this.props.onSend(this.state.text);
283-
this.setState({ text: '' });
284-
}
285-
286-
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
287-
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
288-
if (keyCode) {
289-
const { key, meta, ctrl } = keyCode;
290-
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
291-
this.onSend();
292-
}
293-
}
294-
}
295-
}
296-
297-
export type Line = { message: string; timestamp?: Date };
298-
299-
export class SerialMonitorOutput extends React.Component<
300-
SerialMonitorOutput.Props,
301-
SerialMonitorOutput.State
302-
> {
303-
/**
304-
* Do not touch it. It is used to be able to "follow" the serial monitor log.
305-
*/
306-
protected anchor: HTMLElement | null;
307-
protected toDisposeBeforeUnmount = new DisposableCollection();
308-
309-
constructor(props: Readonly<SerialMonitorOutput.Props>) {
310-
super(props);
311-
this.state = {
312-
lines: [],
313-
timestamp: this.props.monitorModel.timestamp,
314-
charCount: 0,
315-
};
316-
}
317-
318-
render(): React.ReactNode {
319-
return (
320-
<React.Fragment>
321-
<AutoSizer>
322-
{({ height, width }) => (
323-
<List
324-
className="List"
325-
height={height}
326-
itemData={
327-
{
328-
lines: this.state.lines,
329-
timestamp: this.state.timestamp,
330-
} as any
331-
}
332-
itemCount={this.state.lines.length}
333-
itemSize={20}
334-
width={width}
335-
>
336-
{Row}
337-
</List>
338-
)}
339-
</AutoSizer>
340-
{/* <div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}>
341-
{this.state.lines.map((line, i) => (
342-
<MonitorTextLine text={line} key={i} />
343-
))}
344-
</div> */}
345-
<div
346-
style={{ float: 'left', clear: 'both' }}
347-
ref={(element) => {
348-
this.anchor = element;
349-
}}
350-
/>
351-
</React.Fragment>
352-
);
353-
}
354-
355-
shouldComponentUpdate(): boolean {
356-
return true;
357-
}
358-
359-
componentDidMount(): void {
360-
this.scrollToBottom();
361-
this.toDisposeBeforeUnmount.pushAll([
362-
this.props.monitorConnection.onRead(({ messages }) => {
363-
const [newLines, charsToAddCount] = messageToLines(
364-
messages,
365-
this.state.lines
366-
);
367-
const [lines, charCount] = truncateLines(
368-
newLines,
369-
this.state.charCount + charsToAddCount
370-
);
371-
372-
this.setState({
373-
lines,
374-
charCount,
375-
});
376-
}),
377-
this.props.clearConsoleEvent(() => this.setState({ lines: [] })),
378-
this.props.monitorModel.onChange(({ property }) => {
379-
if (property === 'timestamp') {
380-
const { timestamp } = this.props.monitorModel;
381-
this.setState({ timestamp });
382-
}
383-
}),
384-
]);
385-
}
386-
387-
componentDidUpdate(): void {
388-
this.scrollToBottom();
389-
}
390-
391-
componentWillUnmount(): void {
392-
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
393-
this.toDisposeBeforeUnmount.dispose();
394-
}
395-
396-
protected scrollToBottom(): void {
397-
if (this.props.monitorModel.autoscroll && this.anchor) {
398-
this.anchor.scrollIntoView();
399-
// this.listRef.current.scrollToItem(this.state.lines.length);
400-
}
401-
}
402-
}
403-
404-
const Row = ({
405-
index,
406-
style,
407-
data,
408-
}: {
409-
index: number;
410-
style: any;
411-
data: { lines: Line[]; timestamp: boolean };
412-
}) => {
413-
const timestamp =
414-
(data.timestamp &&
415-
`${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) ||
416-
'';
417-
return (
418-
<div style={style}>
419-
{timestamp}
420-
{data.lines[index].message}
421-
</div>
422-
);
423-
};
424-
425-
export interface SelectOption<T> {
426-
readonly label: string;
427-
readonly value: T;
428-
}
429-
430-
export namespace SerialMonitorOutput {
431-
export interface Props {
432-
readonly monitorModel: MonitorModel;
433-
readonly monitorConnection: MonitorConnection;
434-
readonly clearConsoleEvent: Event<void>;
435-
}
436-
437-
export interface State {
438-
lines: Line[];
439-
timestamp: boolean;
440-
charCount: number;
441-
}
442-
443-
export const MAX_CHARACTERS = 1_000_000;
444-
}
445-
446-
function messageToLines(
447-
messages: string[],
448-
prevLines: Line[],
449-
separator = '\n'
450-
): [Line[], number] {
451-
const linesToAdd: Line[] = prevLines.length
452-
? [prevLines[prevLines.length - 1]]
453-
: [{ message: '' }];
454-
let charCount = 0;
455-
456-
for (const message of messages) {
457-
charCount += message.length;
458-
const lastLine = linesToAdd[linesToAdd.length - 1];
459-
460-
if (lastLine.message.charAt(lastLine.message.length - 1) === separator) {
461-
linesToAdd.push({ message, timestamp: new Date() });
462-
} else {
463-
linesToAdd[linesToAdd.length - 1].message += message;
464-
if (!linesToAdd[linesToAdd.length - 1].timestamp) {
465-
linesToAdd[linesToAdd.length - 1].timestamp = new Date();
466-
}
467-
}
468-
}
469-
470-
prevLines.splice(prevLines.length - 1, 1, ...linesToAdd);
471-
return [prevLines, charCount];
472-
}
473-
474-
function truncateLines(lines: Line[], charCount: number): [Line[], number] {
475-
let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS;
476-
while (charsToDelete > 0) {
477-
const firstLineLength = lines[0]?.message?.length;
478-
const newFirstLine = lines[0]?.message?.substring(charsToDelete);
479-
const deletedCharsCount = firstLineLength - newFirstLine.length;
480-
charCount -= deletedCharsCount;
481-
charsToDelete -= deletedCharsCount;
482-
lines[0].message = newFirstLine;
483-
if (!newFirstLine?.length) {
484-
lines.shift();
485-
}
486-
}
487-
return [lines, charCount];
488-
}

0 commit comments

Comments
 (0)