From b36b4471c9da9b6c7e949e5b70c7a27279eb467c Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Tue, 28 Sep 2021 11:59:24 +0200 Subject: [PATCH 01/11] wip --- arduino-ide-extension/package.json | 2 +- .../browser/arduino-ide-frontend-module.ts | 17 +-- .../src/browser/monitor/monitor-connection.ts | 6 +- .../src/browser/monitor/monitor-widget.tsx | 20 ++- .../src/common/protocol/monitor-service.ts | 2 + .../src/node/arduino-daemon-impl.ts | 2 +- yarn.lock | 127 +----------------- 7 files changed, 26 insertions(+), 150 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 9977146d3..c15a6efa9 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -18,7 +18,7 @@ "test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\"" }, "dependencies": { - "@grpc/grpc-js": "^1.1.1", + "@grpc/grpc-js": "^1.3.7", "@theia/application-package": "next", "@theia/core": "next", "@theia/editor": "next", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 028ff657d..f8beeb57f 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -399,25 +399,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Frontend binding for the serial monitor service bind(MonitorService) .toDynamicValue((context) => { + debugger; const connection = context.container.get(WebSocketConnectionProvider); - const client = context.container.get(MonitorServiceClientImpl); + const client = + context.container.get(MonitorServiceClient); return connection.createProxy(MonitorServicePath, client); }) .inSingletonScope(); bind(MonitorConnection).toSelf().inSingletonScope(); // Serial monitor service client to receive and delegate notifications from the backend. - bind(MonitorServiceClientImpl).toSelf().inSingletonScope(); - bind(MonitorServiceClient) - .toDynamicValue((context) => { - const client = context.container.get(MonitorServiceClientImpl); - WebSocketConnectionProvider.createProxy( - context.container, - MonitorServicePath, - client - ); - return client; - }) - .inSingletonScope(); + bind(MonitorServiceClient).to(MonitorServiceClientImpl).inSingletonScope(); bind(WorkspaceService).toSelf().inSingletonScope(); rebind(TheiaWorkspaceService).toService(WorkspaceService); diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index cbf1b53e6..024a94a4c 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -8,6 +8,7 @@ import { MonitorConfig, MonitorError, Status, + MonitorServiceClient, } from '../../common/protocol/monitor-service'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { @@ -16,7 +17,6 @@ import { BoardsService, AttachedBoardsChangeEvent, } from '../../common/protocol/boards-service'; -import { MonitorServiceClientImpl } from './monitor-service-client-impl'; import { BoardsConfig } from '../boards/boards-config'; import { MonitorModel } from './monitor-model'; import { NotificationCenter } from '../notification-center'; @@ -29,8 +29,8 @@ export class MonitorConnection { @inject(MonitorService) protected readonly monitorService: MonitorService; - @inject(MonitorServiceClientImpl) - protected readonly monitorServiceClient: MonitorServiceClientImpl; + @inject(MonitorServiceClient) + protected readonly monitorServiceClient: MonitorServiceClient; @inject(BoardsService) protected readonly boardsService: BoardsService; diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 7fc5520ff..1ccd523f3 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -20,7 +20,6 @@ import { MonitorConfig } from '../../common/protocol/monitor-service'; import { ArduinoSelect } from '../widgets/arduino-select'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; -import { MonitorServiceClientImpl } from './monitor-service-client-impl'; @injectable() export class MonitorWidget extends ReactWidget { @@ -32,9 +31,6 @@ export class MonitorWidget extends ReactWidget { @inject(MonitorConnection) protected readonly monitorConnection: MonitorConnection; - @inject(MonitorServiceClientImpl) - protected readonly monitorServiceClient: MonitorServiceClientImpl; - protected widgetHeight: number; /** @@ -303,7 +299,7 @@ export namespace SerialMonitorOutput { readonly clearConsoleEvent: Event; } export interface State { - content: string; + lines: string[]; timestamp: boolean; } } @@ -321,7 +317,7 @@ export class SerialMonitorOutput extends React.Component< constructor(props: Readonly) { super(props); this.state = { - content: '', + lines: [], timestamp: this.props.monitorModel.timestamp, }; } @@ -330,7 +326,7 @@ export class SerialMonitorOutput extends React.Component< return (
- {this.state.content} + {this.state.lines.map((l) => `${l}\n`)}
` : ''; for (let i = 0; i < rawLines.length; i++) { - if (i === 0 && this.state.content.length !== 0) { + if ( + i === 0 + // && this.state.content.length !== 0 + ) { lines.push(rawLines[i]); } else { lines.push(timestamp() + rawLines[i]); } } - const content = this.state.content + lines.join('\n'); - this.setState({ content }); + this.setState({ lines: this.state.lines.concat(lines) }); }), - this.props.clearConsoleEvent(() => this.setState({ content: '' })), + this.props.clearConsoleEvent(() => this.setState({ lines: [] })), this.props.monitorModel.onChange(({ property }) => { if (property === 'timestamp') { const { timestamp } = this.props.monitorModel; diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 7d187ebc5..9506a60af 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -1,5 +1,6 @@ import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { Board, Port } from './boards-service'; +import { Event } from '@theia/core/lib/common/event'; export interface Status {} export type OK = Status; @@ -61,6 +62,7 @@ export namespace MonitorConfig { export const MonitorServiceClient = Symbol('MonitorServiceClient'); export interface MonitorServiceClient { notifyError(event: MonitorError): void; + onError: Event; } export interface MonitorError { diff --git a/arduino-ide-extension/src/node/arduino-daemon-impl.ts b/arduino-ide-extension/src/node/arduino-daemon-impl.ts index 8e72703a4..1413c7cbc 100644 --- a/arduino-ide-extension/src/node/arduino-daemon-impl.ts +++ b/arduino-ide-extension/src/node/arduino-daemon-impl.ts @@ -94,7 +94,7 @@ export class ArduinoDaemonImpl } async stopDaemon(): Promise { - this.toDispose.dispose(); + this.toDispose.dispose(); } get onDaemonStarted(): Event { diff --git a/yarn.lock b/yarn.lock index 8f4fd61eb..4645972ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1048,14 +1048,12 @@ unique-filename "^1.1.1" which "^1.3.1" -"@grpc/grpc-js@^1.1.1": - version "1.2.11" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.11.tgz#68faa56bded64844294dc6429185503376f05ff1" - integrity sha512-DZqx3nHBm2OGY7NKq4sppDEfx4nBAsQH/d/H/yxo/+BwpVLWLGs+OorpwQ+Fqd6EgpDEoi4MhqndjGUeLl/5GA== +"@grpc/grpc-js@^1.3.7": + version "1.3.7" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.3.7.tgz#58b687aff93b743aafde237fd2ee9a3259d7f2d8" + integrity sha512-CKQVuwuSPh40tgOkR7c0ZisxYRiN05PcKPW72mQL5y++qd7CwBRoaJZvU5xfXnCJDFBmS3qZGQ71Frx6Ofo2XA== dependencies: "@types/node" ">=12.12.47" - google-auth-library "^6.1.1" - semver "^6.2.0" "@lerna/add@3.21.0": version "3.21.0" @@ -3443,13 +3441,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -4047,7 +4038,7 @@ arrify@^1.0.0, arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -arrify@^2.0.0, arrify@^2.0.1: +arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== @@ -5023,11 +5014,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bignumber.js@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== - binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -5279,11 +5265,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -7154,13 +7135,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - editions@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698" @@ -7677,11 +7651,6 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -8002,11 +7971,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fast-text-encoding@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" - integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== - fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -8533,25 +8497,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaxios@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.2.0.tgz#33bdc4fc241fc33b8915a4b8c07cfb368b932e46" - integrity sha512-Ms7fNifGv0XVU+6eIyL9LB7RVESeML9+cMvkwGS70xyD6w2Z80wl6RiqiJ9k1KFlJCUTQqFFc8tXmPQfSKUe8g== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gcp-metadata@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" - integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== - dependencies: - gaxios "^4.0.0" - json-bigint "^1.0.0" - genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -8940,28 +8885,6 @@ globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" -google-auth-library@^6.1.1: - version "6.1.6" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.6.tgz#deacdcdb883d9ed6bac78bb5d79a078877fdf572" - integrity sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ== - dependencies: - arrify "^2.0.0" - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^4.0.0" - gcp-metadata "^4.2.0" - gtoken "^5.0.4" - jws "^4.0.0" - lru-cache "^6.0.0" - -google-p12-pem@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" - integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== - dependencies: - node-forge "^0.10.0" - google-protobuf@3.12.4: version "3.12.4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.12.4.tgz#fd89b7e5052cdb35a80f9b455612851d542a5c9f" @@ -9082,15 +9005,6 @@ grpc_tools_node_protoc_ts@^4.1.0: handlebars "4.7.4" handlebars-helpers "0.10.0" -gtoken@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.2.1.tgz#4dae1fea17270f457954b4a45234bba5fc796d16" - integrity sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw== - dependencies: - gaxios "^4.0.0" - google-p12-pem "^3.0.3" - jws "^4.0.0" - gulp-header@^1.7.1: version "1.8.12" resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" @@ -10491,13 +10405,6 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -10602,23 +10509,6 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282" integrity sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA== -jwa@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" - integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" - integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== - dependencies: - jwa "^2.0.0" - safe-buffer "^5.0.1" - jwt-decode@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" @@ -11967,16 +11857,11 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - node-gyp@^5.0.2: version "5.1.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-5.1.1.tgz#eb915f7b631c937d282e33aed44cb7a025f62a3e" From 08993361fa108b591c867547eda9f7fb18e7116c Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Wed, 29 Sep 2021 12:53:59 +0200 Subject: [PATCH 02/11] experimenting with websockets --- arduino-ide-extension/package.json | 1 + .../browser/arduino-ide-frontend-module.ts | 12 +- .../src/browser/monitor/monitor-connection.ts | 42 ++++- .../monitor/monitor-service-client-impl.ts | 7 + .../src/browser/monitor/monitor-widget.tsx | 49 ++++-- .../src/common/protocol/monitor-service.ts | 6 +- .../src/node/arduino-ide-backend-module.ts | 1 + .../src/node/monitor/monitor-service-impl.ts | 46 ++++- yarn.lock | 164 +++++++++++++++++- 9 files changed, 293 insertions(+), 35 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index c15a6efa9..90eb972c2 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -64,6 +64,7 @@ "fuzzy": "^0.1.3", "glob": "^7.1.6", "google-protobuf": "^3.11.4", + "grpc": "^1.24.11", "hash.js": "^1.1.7", "is-valid-path": "^0.1.1", "js-yaml": "^3.13.1", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index f8beeb57f..479deaf4c 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -397,9 +397,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { createWidget: () => context.container.get(MonitorWidget), })); // Frontend binding for the serial monitor service + // bind(MonitorServiceClient) + // .toDynamicValue((context) => { + // const client = context.container.get(MonitorServiceClientImpl); + // WebSocketConnectionProvider.createProxy( + // context.container, + // MonitorServicePath, + // client + // ); + // return client; + // }) + // .inSingletonScope(); bind(MonitorService) .toDynamicValue((context) => { - debugger; const connection = context.container.get(WebSocketConnectionProvider); const client = context.container.get(MonitorServiceClient); diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index 024a94a4c..9b63eeeb4 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -59,7 +59,7 @@ export class MonitorConnection { /** * This emitter forwards all read events **iff** the connection is established. */ - protected readonly onReadEmitter = new Emitter<{ message: string }>(); + protected readonly onReadEmitter = new Emitter<{ messages: string[] }>(); /** * Array for storing previous monitor errors received from the server, and based on the number of elements in this array, @@ -71,6 +71,31 @@ export class MonitorConnection { @postConstruct() protected init(): void { + this.monitorServiceClient.onMessage(async (port) => { + const w = new WebSocket(`ws://localhost:${port}`); + let h = 0; + w.onmessage = (res) => { + const messages = JSON.parse(res.data); + h += messages.length; + + if (h > 1000) { + h = 0; + console.log('read 1000 messages'); + } + // console.log(`received ${messages.length} messages`); + // this.onReadEmitter.fire({ messages }); + }; + }); + + // let i = 0; + // this.monitorServiceClient.onMessage(async (msg) => { + // // msg received + // i++; + // if (i % 1000 === 0) { + // // console.log(msg); + // } + // }); + this.monitorServiceClient.onError(async (error) => { let shouldReconnect = false; if (this.state) { @@ -231,15 +256,22 @@ export class MonitorConnection { ); const connectStatus = await this.monitorService.connect(config); if (Status.isOK(connectStatus)) { + let j = 0; const requestMessage = () => { - this.monitorService.request().then(({ message }) => { + this.monitorService.request().then(({ messages }) => { if (this.connected) { - this.onReadEmitter.fire({ message }); + // this.onReadEmitter.fire({ messages }); + j += messages.length; + if (j > 1000) { + j = 0; + // console.log(`read more than 1000 messages`); + } + requestMessage(); } }); }; - requestMessage(); + // requestMessage(); this.state = { config }; console.info( `<<< Serial monitor connection created for ${Board.toString( @@ -300,7 +332,7 @@ export class MonitorConnection { return this.onConnectionChangedEmitter.event; } - get onRead(): Event<{ message: string }> { + get onRead(): Event<{ messages: string[] }> { return this.onReadEmitter.event; } diff --git a/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts index a0bc9d5bf..9ab757ef4 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts @@ -10,7 +10,14 @@ export class MonitorServiceClientImpl implements MonitorServiceClient { protected readonly onErrorEmitter = new Emitter(); readonly onError = this.onErrorEmitter.event; + protected readonly onMessageEmitter = new Emitter(); + readonly onMessage = this.onMessageEmitter.event; + notifyError(error: MonitorError): void { this.onErrorEmitter.fire(error); } + + notifyMessage(message: string): void { + this.onMessageEmitter.fire(message); + } } diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 1ccd523f3..9e5a8397a 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -326,7 +326,9 @@ export class SerialMonitorOutput extends React.Component< return (
- {this.state.lines.map((l) => `${l}\n`)} + {this.state.lines.map((line, i) => ( + + ))}
{ - const rawLines = message.split('\n'); - const lines: string[] = []; - const timestamp = () => - this.state.timestamp - ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` - : ''; - for (let i = 0; i < rawLines.length; i++) { - if ( - i === 0 - // && this.state.content.length !== 0 - ) { - lines.push(rawLines[i]); - } else { - lines.push(timestamp() + rawLines[i]); + this.props.monitorConnection.onRead(({ messages }) => { + messages.forEach((message) => { + const rawLines = message.split('\n'); + const lines: string[] = []; + const timestamp = () => + this.state.timestamp + ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` + : ''; + + for (let i = 0; i < rawLines.length; i++) { + if (i === 0 && this.state.lines.length !== 0) { + lines.push(rawLines[i]); + } else { + lines.push(timestamp() + rawLines[i]); + } } - } - this.setState({ lines: this.state.lines.concat(lines) }); + + this.setState((prevState) => ({ + lines: [...prevState.lines, lines.join('\n')], + })); + + // const content = this.state.content + lines.join('\n'); + // this.setState({ content }); + }); }), this.props.clearConsoleEvent(() => this.setState({ lines: [] })), this.props.monitorModel.onChange(({ property }) => { @@ -386,6 +394,11 @@ export class SerialMonitorOutput extends React.Component< } } +const _MonitorTextLine = ({ text }: { text: string }): React.ReactElement => { + return
{text}
; +}; +export const MonitorTextLine = React.memo(_MonitorTextLine); + export interface SelectOption { readonly label: string; readonly value: T; diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 9506a60af..6a5ba7123 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -24,7 +24,7 @@ export interface MonitorService extends JsonRpcServer { connect(config: MonitorConfig): Promise; disconnect(): Promise; send(message: string): Promise; - request(): Promise<{ message: string }>; + request(): Promise<{ messages: string[] }>; } export interface MonitorConfig { @@ -61,8 +61,10 @@ export namespace MonitorConfig { export const MonitorServiceClient = Symbol('MonitorServiceClient'); export interface MonitorServiceClient { - notifyError(event: MonitorError): void; onError: Event; + onMessage: Event; + notifyError(event: MonitorError): void; + notifyMessage(message: string): void; } export interface MonitorError { diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 642388bbc..a6679124a 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -200,6 +200,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonitorClientProvider).toSelf().inSingletonScope(); bind(MonitorServiceImpl).toSelf().inSingletonScope(); bind(MonitorService).toService(MonitorServiceImpl); + bindBackendService( MonitorServicePath, MonitorService, diff --git a/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts index d3e288176..194c12d2d 100644 --- a/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts +++ b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts @@ -1,5 +1,5 @@ import { ClientDuplexStream } from '@grpc/grpc-js'; -import { TextDecoder, TextEncoder } from 'util'; +import { TextEncoder } from 'util'; import { injectable, inject, named } from 'inversify'; import { Struct } from 'google-protobuf/google/protobuf/struct_pb'; import { Emitter } from '@theia/core/lib/common/event'; @@ -18,6 +18,7 @@ import { } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb'; import { MonitorClientProvider } from './monitor-client-provider'; import { Board, Port } from '../../common/protocol/boards-service'; +import * as WebSocket from 'ws'; interface ErrorWithCode extends Error { readonly code: number; @@ -74,6 +75,13 @@ export class MonitorServiceImpl implements MonitorService { protected messages: string[] = []; protected onMessageDidReadEmitter = new Emitter(); + private rand = 0; + + constructor() { + this.rand = Math.floor(1000 * Math.random()); + console.log(`Spawning ${this.rand}`); + } + setClient(client: MonitorServiceClient | undefined): void { this.client = client; } @@ -122,14 +130,36 @@ export class MonitorServiceImpl implements MonitorService { }).bind(this) ); + const ws = new WebSocket.Server({ port: 0 }); + const address: any = ws.address(); + this.client?.notifyMessage(address.port); + let wsConn: WebSocket | null = null; + ws.on('connection', (ws) => { + wsConn = ws; + }); + + const emptyTheQueue = () => { + if (this.messages.length) { + wsConn?.send(JSON.stringify(this.messages)); + this.messages = []; + } + }; + + // empty the queue every 16ms (~60fps) + setInterval(emptyTheQueue, 32); + duplex.on( 'data', ((resp: StreamingOpenResponse) => { + // eslint-disable-next-line unused-imports/no-unused-vars const raw = resp.getData(); + const message = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw); + // this.client?.notifyMessage(message); this.messages.push(message); - this.onMessageDidReadEmitter.fire(); + // this.onMessageDidReadEmitter.fire(); + // wsConn?.send(message); }).bind(this) ); @@ -207,12 +237,14 @@ export class MonitorServiceImpl implements MonitorService { }); } - async request(): Promise<{ message: string }> { - const message = this.messages.shift(); - if (message) { - return { message }; + async request(): Promise<{ messages: string[] }> { + // const messages = this.messages.shift(); + const messages = this.messages; + if (messages.length) { + this.messages = []; + return { messages }; } - return new Promise<{ message: string }>((resolve) => { + return new Promise<{ messages: string[] }>((resolve) => { const toDispose = this.onMessageDidReadEmitter.event(() => { toDispose.dispose(); resolve(this.request()); diff --git a/yarn.lock b/yarn.lock index 4645972ba..4d824ceb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1740,6 +1740,21 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@mapbox/node-pre-gyp@^1.0.4": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950" + integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA== + dependencies: + detect-libc "^1.0.3" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.1" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.4" + tar "^6.1.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2673,6 +2688,14 @@ dependencies: "@types/node" "*" +"@types/bytebuffer@^5.0.40": + version "5.0.42" + resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.42.tgz#1c602a77942d34c5c0879ad75c58d5d8c07dfb3b" + integrity sha512-lEgKojWUAc/MG2t649oZS5AfYFP2xRNPoDuwDBlBMjHXd8MaGPgFgtCXUK7inZdBOygmVf10qxc1Us8GXC96aw== + dependencies: + "@types/long" "*" + "@types/node" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -2832,6 +2855,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== +"@types/long@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/mime-types@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73" @@ -4048,6 +4076,14 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +ascli@~1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" + integrity sha1-vPpZdKYvGOgcq660lzKrSoj5Brw= + dependencies: + colour "~0.7.1" + optjs "~3.2.2" + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -5327,6 +5363,13 @@ byte-size@^5.0.1: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +bytebuffer@~5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" + integrity sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0= + dependencies: + long "~3" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -5496,7 +5539,7 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@^2.0.0: +camelcase@^2.0.0, camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= @@ -5827,6 +5870,15 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +cliui@^3.0.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" @@ -5993,6 +6045,11 @@ colors@~1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= +colour@~0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" + integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= + columnify@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" @@ -8767,6 +8824,18 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.5: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-agent@^2.0.2: version "2.1.12" resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.1.12.tgz#e4ae3812b731a9e81cbf825f9377ef450a8e4195" @@ -8996,6 +9065,18 @@ grpc-tools@^1.9.0: dependencies: node-pre-gyp "^0.15.0" +grpc@^1.24.11: + version "1.24.11" + resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.24.11.tgz#7039da9f6f22ce35168535a6d5dda618398a5966" + integrity sha512-8/AQdFCzCeCDWW3SoaMNp6ccbRvTQEH1O1u1uFtt29eWsg5gSZCJ3m6fbkduEIh3smY7WAPP+LgVJ5n3nZRxcA== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.4" + "@types/bytebuffer" "^5.0.40" + lodash.camelcase "^4.3.0" + lodash.clone "^4.5.0" + nan "^2.13.2" + protobufjs "^5.0.3" + grpc_tools_node_protoc_ts@^4.1.0: version "4.1.5" resolved "https://registry.yarnpkg.com/grpc_tools_node_protoc_ts/-/grpc_tools_node_protoc_ts-4.1.5.tgz#ad540a51867ff407196538d2d6370b27d6d3cfc8" @@ -9702,6 +9783,11 @@ inversify@^5.0.1: resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.0.5.tgz#bd1f8e6d8e0f739331acd8ba9bc954635aae0bbf" integrity sha512-60QsfPz8NAU/GZqXu8hJ+BhNf/C/c+Hp0eDc6XMIJTxBiP36AQyyQKpBkOVTLWBFDQWYVHpbbEuIsHu9dLuJDA== +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= + invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" @@ -10575,6 +10661,13 @@ lazy-cache@^2.0.1, lazy-cache@^2.0.2: dependencies: set-getter "^0.1.0" +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= + dependencies: + invert-kv "^1.0.0" + lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -10854,6 +10947,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -11005,6 +11103,11 @@ logging-helpers@^1.0.0: isobject "^3.0.0" log-utils "^0.2.1" +long@~3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -11734,6 +11837,11 @@ nan@^2.0.0, nan@^2.12.1, nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nan@^2.13.2: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -12418,6 +12526,11 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +optjs@~3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" + integrity sha1-aabOicRCpEQDFBrS+bNwvVu29O4= + ora@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" @@ -12450,6 +12563,13 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= + dependencies: + lcid "^1.0.0" + os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -13436,6 +13556,16 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protobufjs@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.3.tgz#e4dfe9fb67c90b2630d15868249bcc4961467a17" + integrity sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA== + dependencies: + ascli "~1" + bytebuffer "~5" + glob "^7.0.5" + yargs "^3.10.0" + protoc@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/protoc/-/protoc-1.0.4.tgz#fd0ba07132c459df80c6135889bd5cc92f0afec2" @@ -15588,6 +15718,18 @@ tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.1.0: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -16700,6 +16842,11 @@ winchan@^0.2.2: resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.2.tgz#6766917b88e5e1cb75f455ffc7cc13f51e5c834e" integrity sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ== +window-size@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY= + windows-release@^3.1.0: version "3.3.3" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999" @@ -16915,7 +17062,7 @@ xterm@~4.11.0: resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.11.0.tgz#d7dabc7af5299579e4663fedf2b3a179af9aaff9" integrity sha512-NeJH909WTO2vth/ZlC0gkP3AGzupbvVHVlmtrpBw56/sGFXaF9bNdKgqKa3tf8qbGvXMzL2JhCcHVklqFztIRw== -y18n@^3.2.1: +y18n@^3.2.0, y18n@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== @@ -17058,6 +17205,19 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^3.10.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" + integrity sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU= + dependencies: + camelcase "^2.0.1" + cliui "^3.0.3" + decamelize "^1.1.1" + os-locale "^1.4.0" + string-width "^1.0.1" + window-size "^0.1.4" + y18n "^3.2.0" + yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 006f44b8bf5584f9c1681b7963719816872f2151 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Wed, 29 Sep 2021 18:22:04 +0200 Subject: [PATCH 03/11] WIP frontend performances --- arduino-ide-extension/package.json | 4 ++ .../src/browser/monitor/monitor-connection.ts | 10 +-- .../src/browser/monitor/monitor-widget.tsx | 64 +++++++++++++++---- yarn.lock | 39 +++++++++++ 4 files changed, 94 insertions(+), 23 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 90eb972c2..70847582b 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -79,6 +79,8 @@ "react-disable": "^0.1.0", "react-select": "^3.0.4", "react-tabs": "^3.1.2", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", "semver": "^7.3.2", "string-natural-compare": "^2.0.3", "temp": "^0.9.1", @@ -90,6 +92,8 @@ "@types/chai": "^4.2.7", "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", "chai": "^4.2.0", "chai-string": "^1.5.0", "decompress": "^4.2.0", diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index 9b63eeeb4..e37d15f3d 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -73,17 +73,9 @@ export class MonitorConnection { protected init(): void { this.monitorServiceClient.onMessage(async (port) => { const w = new WebSocket(`ws://localhost:${port}`); - let h = 0; w.onmessage = (res) => { const messages = JSON.parse(res.data); - h += messages.length; - - if (h > 1000) { - h = 0; - console.log('read 1000 messages'); - } - // console.log(`received ${messages.length} messages`); - // this.onReadEmitter.fire({ messages }); + this.onReadEmitter.fire({ messages }); }; }); diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 9e5a8397a..7a89efcf6 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -20,6 +20,8 @@ import { MonitorConfig } from '../../common/protocol/monitor-service'; import { ArduinoSelect } from '../widgets/arduino-select'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; @injectable() export class MonitorWidget extends ReactWidget { @@ -299,7 +301,7 @@ export namespace SerialMonitorOutput { readonly clearConsoleEvent: Event; } export interface State { - lines: string[]; + lines: any; timestamp: boolean; } } @@ -322,14 +324,32 @@ export class SerialMonitorOutput extends React.Component< }; } + listRef: any = React.createRef(); + render(): React.ReactNode { return ( -
+ + {({ height, width }) => ( + this.scrollToBottom()} + > + {Row} + + )} + + {/*
{this.state.lines.map((line, i) => ( ))} -
+
*/}
{ @@ -340,11 +360,16 @@ export class SerialMonitorOutput extends React.Component< ); } + shouldComponentUpdate(): boolean { + return true; + } + componentDidMount(): void { - this.scrollToBottom(); + // this.scrollToBottom(); this.toDisposeBeforeUnmount.pushAll([ this.props.monitorConnection.onRead(({ messages }) => { - messages.forEach((message) => { + const linesToAdd: string[] = []; + for (const message of messages) { const rawLines = message.split('\n'); const lines: string[] = []; const timestamp = () => @@ -359,14 +384,14 @@ export class SerialMonitorOutput extends React.Component< lines.push(timestamp() + rawLines[i]); } } - - this.setState((prevState) => ({ - lines: [...prevState.lines, lines.join('\n')], - })); + linesToAdd.push(lines.join('\n')); // const content = this.state.content + lines.join('\n'); // this.setState({ content }); - }); + } + this.setState((prevState) => ({ + lines: [...prevState.lines, ...linesToAdd], + })); }), this.props.clearConsoleEvent(() => this.setState({ lines: [] })), this.props.monitorModel.onChange(({ property }) => { @@ -378,9 +403,9 @@ export class SerialMonitorOutput extends React.Component< ]); } - componentDidUpdate(): void { - this.scrollToBottom(); - } + // componentDidUpdate(): void { + // this.scrollToBottom(); + // } componentWillUnmount(): void { // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? @@ -389,7 +414,8 @@ export class SerialMonitorOutput extends React.Component< protected scrollToBottom(): void { if (this.props.monitorModel.autoscroll && this.anchor) { - this.anchor.scrollIntoView(); + // this.anchor.scrollIntoView(); + this.listRef.current.scrollToItem(this.state.lines.length); } } } @@ -399,6 +425,16 @@ const _MonitorTextLine = ({ text }: { text: string }): React.ReactElement => { }; export const MonitorTextLine = React.memo(_MonitorTextLine); +const Row = ({ + index, + style, + data, +}: { + index: number; + style: any; + data: string; +}) =>
{data[index]}
; + export interface SelectOption { readonly label: string; readonly value: T; diff --git a/yarn.lock b/yarn.lock index 4d824ceb7..ec04154f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -826,6 +826,13 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/runtime@^7.0.0": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" + integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.10.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" @@ -3013,6 +3020,13 @@ dependencies: "@types/react" "*" +"@types/react-virtualized-auto-sizer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" + integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong== + dependencies: + "@types/react" "*" + "@types/react-virtualized@^9.18.3": version "9.21.11" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.11.tgz#8eb60ed12ef0b2625769819f9fd10ad4bb1bdce0" @@ -3021,6 +3035,13 @@ "@types/prop-types" "*" "@types/react" "*" +"@types/react-window@^1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" + integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== + dependencies: + "@types/react" "*" + "@types/react@*": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" @@ -11364,6 +11385,11 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memoize-one@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -13916,6 +13942,11 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtualized-auto-sizer@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" + integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== + react-virtualized@^9.20.0: version "9.22.3" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421" @@ -13928,6 +13959,14 @@ react-virtualized@^9.20.0: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-window@^1.8.6: + version "1.8.6" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" + integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^16.8.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" From 0c4b3d012c370377fe45e7b2a9af8457167fc372 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Thu, 30 Sep 2021 13:08:25 +0200 Subject: [PATCH 04/11] rewrote monitor print lines logic --- .../src/browser/monitor/monitor-widget.tsx | 108 ++++++++++-------- .../src/node/monitor/monitor-service-impl.ts | 29 ++++- 2 files changed, 86 insertions(+), 51 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 7a89efcf6..b01a2bd79 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import * as dateFormat from 'dateformat'; import { postConstruct, injectable, inject } from 'inversify'; import { OptionsType } from 'react-select/src/types'; import { isOSX } from '@theia/core/lib/common/os'; @@ -22,6 +21,7 @@ import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; import { FixedSizeList as List } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; +import dateFormat = require('dateformat'); @injectable() export class MonitorWidget extends ReactWidget { @@ -300,11 +300,13 @@ export namespace SerialMonitorOutput { readonly monitorConnection: MonitorConnection; readonly clearConsoleEvent: Event; } + export interface State { - lines: any; + lines: Line[]; timestamp: boolean; } } +export type Line = { message: string; timestamp?: Date }; export class SerialMonitorOutput extends React.Component< SerialMonitorOutput.Props, @@ -319,13 +321,11 @@ export class SerialMonitorOutput extends React.Component< constructor(props: Readonly) { super(props); this.state = { - lines: [], + lines: [{ message: '' }], timestamp: this.props.monitorModel.timestamp, }; } - listRef: any = React.createRef(); - render(): React.ReactNode { return ( @@ -334,12 +334,15 @@ export class SerialMonitorOutput extends React.Component< this.scrollToBottom()} > {Row} @@ -364,36 +367,41 @@ export class SerialMonitorOutput extends React.Component< return true; } + messageToLines( + messages: string[], + prevLines: Line[], + separator = '\n' + ): Line[] { + const linesToAdd: Line[] = [this.state.lines[this.state.lines.length - 1]]; + + for (const message of messages) { + const lastLine = linesToAdd[linesToAdd.length - 1]; + + if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { + linesToAdd.push({ message, timestamp: new Date() }); + } else { + linesToAdd[linesToAdd.length - 1].message += message; + if (!linesToAdd[linesToAdd.length - 1].timestamp) { + linesToAdd[linesToAdd.length - 1].timestamp = new Date(); + } + } + } + + prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); + return prevLines; + } + componentDidMount(): void { - // this.scrollToBottom(); + this.scrollToBottom(); this.toDisposeBeforeUnmount.pushAll([ this.props.monitorConnection.onRead(({ messages }) => { - const linesToAdd: string[] = []; - for (const message of messages) { - const rawLines = message.split('\n'); - const lines: string[] = []; - const timestamp = () => - this.state.timestamp - ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` - : ''; - - for (let i = 0; i < rawLines.length; i++) { - if (i === 0 && this.state.lines.length !== 0) { - lines.push(rawLines[i]); - } else { - lines.push(timestamp() + rawLines[i]); - } - } - linesToAdd.push(lines.join('\n')); - - // const content = this.state.content + lines.join('\n'); - // this.setState({ content }); - } - this.setState((prevState) => ({ - lines: [...prevState.lines, ...linesToAdd], - })); + const newLines = this.messageToLines(messages, this.state.lines); + + this.setState({ lines: newLines }); }), - this.props.clearConsoleEvent(() => this.setState({ lines: [] })), + this.props.clearConsoleEvent(() => + this.setState({ lines: [{ message: '' }] }) + ), this.props.monitorModel.onChange(({ property }) => { if (property === 'timestamp') { const { timestamp } = this.props.monitorModel; @@ -403,9 +411,9 @@ export class SerialMonitorOutput extends React.Component< ]); } - // componentDidUpdate(): void { - // this.scrollToBottom(); - // } + componentDidUpdate(): void { + this.scrollToBottom(); + } componentWillUnmount(): void { // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? @@ -414,17 +422,12 @@ export class SerialMonitorOutput extends React.Component< protected scrollToBottom(): void { if (this.props.monitorModel.autoscroll && this.anchor) { - // this.anchor.scrollIntoView(); - this.listRef.current.scrollToItem(this.state.lines.length); + this.anchor.scrollIntoView(); + // this.listRef.current.scrollToItem(this.state.lines.length); } } } -const _MonitorTextLine = ({ text }: { text: string }): React.ReactElement => { - return
{text}
; -}; -export const MonitorTextLine = React.memo(_MonitorTextLine); - const Row = ({ index, style, @@ -432,8 +435,19 @@ const Row = ({ }: { index: number; style: any; - data: string; -}) =>
{data[index]}
; + data: { lines: Line[]; timestamp: boolean }; +}) => { + const timestamp = + (data.timestamp && + `${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) || + ''; + return ( +
+ {timestamp} + {data.lines[index].message} +
+ ); +}; export interface SelectOption { readonly label: string; diff --git a/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts index 194c12d2d..d9c91dba3 100644 --- a/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts +++ b/arduino-ide-extension/src/node/monitor/monitor-service-impl.ts @@ -148,6 +148,27 @@ export class MonitorServiceImpl implements MonitorService { // empty the queue every 16ms (~60fps) setInterval(emptyTheQueue, 32); + // converts 'ab\nc\nd' => [ab\n,c\n,d] + const stringToArray = (string: string, separator = '\n') => { + const retArray: string[] = []; + + let prevChar = separator; + + for (let i = 0; i < string.length; i++) { + const currChar = string[i]; + + if (prevChar === separator) { + retArray.push(currChar); + } else { + const lastWord = retArray[retArray.length - 1]; + retArray[retArray.length - 1] = lastWord + currChar; + } + + prevChar = currChar; + } + return retArray; + }; + duplex.on( 'data', ((resp: StreamingOpenResponse) => { @@ -156,10 +177,10 @@ export class MonitorServiceImpl implements MonitorService { const message = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw); - // this.client?.notifyMessage(message); - this.messages.push(message); - // this.onMessageDidReadEmitter.fire(); - // wsConn?.send(message); + + // split the message if it contains more lines + const messages = stringToArray(message); + this.messages.push(...messages); }).bind(this) ); From 6cbbf2a33232e686c01d9ce06b2f9539f9c35719 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Thu, 30 Sep 2021 18:08:41 +0200 Subject: [PATCH 05/11] truncate serial monitor output --- .../src/browser/monitor/monitor-widget.tsx | 119 +++++++++++------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index b01a2bd79..03cb89b8b 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -294,18 +294,6 @@ export class SerialMonitorSendInput extends React.Component< } } -export namespace SerialMonitorOutput { - export interface Props { - readonly monitorModel: MonitorModel; - readonly monitorConnection: MonitorConnection; - readonly clearConsoleEvent: Event; - } - - export interface State { - lines: Line[]; - timestamp: boolean; - } -} export type Line = { message: string; timestamp?: Date }; export class SerialMonitorOutput extends React.Component< @@ -321,8 +309,9 @@ export class SerialMonitorOutput extends React.Component< constructor(props: Readonly) { super(props); this.state = { - lines: [{ message: '' }], + lines: [], timestamp: this.props.monitorModel.timestamp, + charCount: 0, }; } @@ -367,41 +356,25 @@ export class SerialMonitorOutput extends React.Component< return true; } - messageToLines( - messages: string[], - prevLines: Line[], - separator = '\n' - ): Line[] { - const linesToAdd: Line[] = [this.state.lines[this.state.lines.length - 1]]; - - for (const message of messages) { - const lastLine = linesToAdd[linesToAdd.length - 1]; - - if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { - linesToAdd.push({ message, timestamp: new Date() }); - } else { - linesToAdd[linesToAdd.length - 1].message += message; - if (!linesToAdd[linesToAdd.length - 1].timestamp) { - linesToAdd[linesToAdd.length - 1].timestamp = new Date(); - } - } - } - - prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); - return prevLines; - } - componentDidMount(): void { this.scrollToBottom(); this.toDisposeBeforeUnmount.pushAll([ this.props.monitorConnection.onRead(({ messages }) => { - const newLines = this.messageToLines(messages, this.state.lines); - - this.setState({ lines: newLines }); + const [newLines, charsToAddCount] = messageToLines( + messages, + this.state.lines + ); + const [lines, charCount] = truncateLines( + newLines, + this.state.charCount + charsToAddCount + ); + + this.setState({ + lines, + charCount, + }); }), - this.props.clearConsoleEvent(() => - this.setState({ lines: [{ message: '' }] }) - ), + this.props.clearConsoleEvent(() => this.setState({ lines: [] })), this.props.monitorModel.onChange(({ property }) => { if (property === 'timestamp') { const { timestamp } = this.props.monitorModel; @@ -453,3 +426,63 @@ export interface SelectOption { readonly label: string; readonly value: T; } + +export namespace SerialMonitorOutput { + export interface Props { + readonly monitorModel: MonitorModel; + readonly monitorConnection: MonitorConnection; + readonly clearConsoleEvent: Event; + } + + export interface State { + lines: Line[]; + timestamp: boolean; + charCount: number; + } + + export const MAX_CHARACTERS = 1_000_000; +} + +function messageToLines( + messages: string[], + prevLines: Line[], + separator = '\n' +): [Line[], number] { + const linesToAdd: Line[] = prevLines.length + ? [prevLines[prevLines.length - 1]] + : [{ message: '' }]; + let charCount = 0; + + for (const message of messages) { + charCount += message.length; + const lastLine = linesToAdd[linesToAdd.length - 1]; + + if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { + linesToAdd.push({ message, timestamp: new Date() }); + } else { + linesToAdd[linesToAdd.length - 1].message += message; + if (!linesToAdd[linesToAdd.length - 1].timestamp) { + linesToAdd[linesToAdd.length - 1].timestamp = new Date(); + } + } + } + + prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); + return [prevLines, charCount]; +} + +function truncateLines(lines: Line[], charCount: number): [Line[], number] { + let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS; + while (charsToDelete > 0) { + const firstLineLength = lines[0]?.message?.length; + const newFirstLine = lines[0]?.message?.substring(charsToDelete); + const deletedCharsCount = firstLineLength - newFirstLine.length; + charCount -= deletedCharsCount; + charsToDelete -= deletedCharsCount; + lines[0].message = newFirstLine; + if (!newFirstLine?.length) { + lines.shift(); + } + } + return [lines, charCount]; +} From 483c0a338979543ffb5dfae417f4073205aa5576 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Fri, 1 Oct 2021 10:48:02 +0200 Subject: [PATCH 06/11] split monitor widget in different files to improve readability and testability --- .../src/browser/monitor/monitor-utils.ts | 48 +++ .../src/browser/monitor/monitor-widget.tsx | 296 +----------------- .../monitor/serial-monitor-send-input.tsx | 81 +++++ .../monitor/serial-monitor-send-output.tsx | 158 ++++++++++ 4 files changed, 299 insertions(+), 284 deletions(-) create mode 100644 arduino-ide-extension/src/browser/monitor/monitor-utils.ts create mode 100644 arduino-ide-extension/src/browser/monitor/serial-monitor-send-input.tsx create mode 100644 arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx diff --git a/arduino-ide-extension/src/browser/monitor/monitor-utils.ts b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts new file mode 100644 index 000000000..3ea1eea05 --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts @@ -0,0 +1,48 @@ +import { Line, SerialMonitorOutput } from './serial-monitor-send-output'; + +export function messageToLines( + messages: string[], + prevLines: Line[], + separator = '\n' +): [Line[], number] { + const linesToAdd: Line[] = prevLines.length + ? [prevLines[prevLines.length - 1]] + : [{ message: '' }]; + let charCount = 0; + + for (const message of messages) { + charCount += message.length; + const lastLine = linesToAdd[linesToAdd.length - 1]; + + if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { + linesToAdd.push({ message, timestamp: new Date() }); + } else { + linesToAdd[linesToAdd.length - 1].message += message; + if (!linesToAdd[linesToAdd.length - 1].timestamp) { + linesToAdd[linesToAdd.length - 1].timestamp = new Date(); + } + } + } + + prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); + return [prevLines, charCount]; +} + +export function truncateLines( + lines: Line[], + charCount: number +): [Line[], number] { + let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS; + while (charsToDelete > 0) { + const firstLineLength = lines[0]?.message?.length; + const newFirstLine = lines[0]?.message?.substring(charsToDelete); + const deletedCharsCount = firstLineLength - newFirstLine.length; + charCount -= deletedCharsCount; + charsToDelete -= deletedCharsCount; + lines[0].message = newFirstLine; + if (!newFirstLine?.length) { + lines.shift(); + } + } + return [lines, charCount]; +} diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 03cb89b8b..6575a69d4 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -1,27 +1,20 @@ import * as React from 'react'; import { postConstruct, injectable, inject } from 'inversify'; import { OptionsType } from 'react-select/src/types'; -import { isOSX } from '@theia/core/lib/common/os'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { Key, KeyCode } from '@theia/core/lib/browser/keys'; -import { - DisposableCollection, - Disposable, -} from '@theia/core/lib/common/disposable'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Disposable } from '@theia/core/lib/common/disposable'; import { ReactWidget, Message, Widget, MessageLoop, } from '@theia/core/lib/browser/widgets'; -import { Board, Port } from '../../common/protocol/boards-service'; import { MonitorConfig } from '../../common/protocol/monitor-service'; import { ArduinoSelect } from '../widgets/arduino-select'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; -import { FixedSizeList as List } from 'react-window'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import dateFormat = require('dateformat'); +import { SerialMonitorSendInput } from './serial-monitor-send-input'; +import { SerialMonitorOutput } from './serial-monitor-send-output'; @injectable() export class MonitorWidget extends ReactWidget { @@ -120,7 +113,9 @@ export class MonitorWidget extends ReactWidget { ); }; - protected get lineEndings(): OptionsType> { + protected get lineEndings(): OptionsType< + SerialMonitorOutput.SelectOption + > { return [ { label: 'No Line Ending', @@ -141,7 +136,9 @@ export class MonitorWidget extends ReactWidget { ]; } - protected get baudRates(): OptionsType> { + protected get baudRates(): OptionsType< + SerialMonitorOutput.SelectOption + > { const baudRates: Array = [ 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, ]; @@ -206,283 +203,14 @@ export class MonitorWidget extends ReactWidget { } protected readonly onChangeLineEnding = ( - option: SelectOption + option: SerialMonitorOutput.SelectOption ) => { this.monitorModel.lineEnding = option.value; }; protected readonly onChangeBaudRate = ( - option: SelectOption + option: SerialMonitorOutput.SelectOption ) => { this.monitorModel.baudRate = option.value; }; } - -export namespace SerialMonitorSendInput { - export interface Props { - readonly monitorConfig?: MonitorConfig; - readonly onSend: (text: string) => void; - readonly resolveFocus: (element: HTMLElement | undefined) => void; - } - export interface State { - text: string; - } -} - -export class SerialMonitorSendInput extends React.Component< - SerialMonitorSendInput.Props, - SerialMonitorSendInput.State -> { - constructor(props: Readonly) { - super(props); - this.state = { text: '' }; - this.onChange = this.onChange.bind(this); - this.onSend = this.onSend.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - } - - render(): React.ReactNode { - return ( - - ); - } - - protected get placeholder(): string { - const { monitorConfig } = this.props; - if (!monitorConfig) { - return 'Not connected. Select a board and a port to connect automatically.'; - } - const { board, port } = monitorConfig; - return `Message (${ - isOSX ? '⌘' : 'Ctrl' - }+Enter to send message to '${Board.toString(board, { - useFqbn: false, - })}' on '${Port.toString(port)}')`; - } - - protected setRef = (element: HTMLElement | null) => { - if (this.props.resolveFocus) { - this.props.resolveFocus(element || undefined); - } - }; - - protected onChange(event: React.ChangeEvent): void { - this.setState({ text: event.target.value }); - } - - protected onSend(): void { - this.props.onSend(this.state.text); - this.setState({ text: '' }); - } - - protected onKeyDown(event: React.KeyboardEvent): void { - const keyCode = KeyCode.createKeyCode(event.nativeEvent); - if (keyCode) { - const { key, meta, ctrl } = keyCode; - if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) { - this.onSend(); - } - } - } -} - -export type Line = { message: string; timestamp?: Date }; - -export class SerialMonitorOutput extends React.Component< - SerialMonitorOutput.Props, - SerialMonitorOutput.State -> { - /** - * Do not touch it. It is used to be able to "follow" the serial monitor log. - */ - protected anchor: HTMLElement | null; - protected toDisposeBeforeUnmount = new DisposableCollection(); - - constructor(props: Readonly) { - super(props); - this.state = { - lines: [], - timestamp: this.props.monitorModel.timestamp, - charCount: 0, - }; - } - - render(): React.ReactNode { - return ( - - - {({ height, width }) => ( - - {Row} - - )} - - {/*
- {this.state.lines.map((line, i) => ( - - ))} -
*/} -
{ - this.anchor = element; - }} - /> - - ); - } - - shouldComponentUpdate(): boolean { - return true; - } - - componentDidMount(): void { - this.scrollToBottom(); - this.toDisposeBeforeUnmount.pushAll([ - this.props.monitorConnection.onRead(({ messages }) => { - const [newLines, charsToAddCount] = messageToLines( - messages, - this.state.lines - ); - const [lines, charCount] = truncateLines( - newLines, - this.state.charCount + charsToAddCount - ); - - this.setState({ - lines, - charCount, - }); - }), - this.props.clearConsoleEvent(() => this.setState({ lines: [] })), - this.props.monitorModel.onChange(({ property }) => { - if (property === 'timestamp') { - const { timestamp } = this.props.monitorModel; - this.setState({ timestamp }); - } - }), - ]); - } - - componentDidUpdate(): void { - this.scrollToBottom(); - } - - componentWillUnmount(): void { - // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? - this.toDisposeBeforeUnmount.dispose(); - } - - protected scrollToBottom(): void { - if (this.props.monitorModel.autoscroll && this.anchor) { - this.anchor.scrollIntoView(); - // this.listRef.current.scrollToItem(this.state.lines.length); - } - } -} - -const Row = ({ - index, - style, - data, -}: { - index: number; - style: any; - data: { lines: Line[]; timestamp: boolean }; -}) => { - const timestamp = - (data.timestamp && - `${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) || - ''; - return ( -
- {timestamp} - {data.lines[index].message} -
- ); -}; - -export interface SelectOption { - readonly label: string; - readonly value: T; -} - -export namespace SerialMonitorOutput { - export interface Props { - readonly monitorModel: MonitorModel; - readonly monitorConnection: MonitorConnection; - readonly clearConsoleEvent: Event; - } - - export interface State { - lines: Line[]; - timestamp: boolean; - charCount: number; - } - - export const MAX_CHARACTERS = 1_000_000; -} - -function messageToLines( - messages: string[], - prevLines: Line[], - separator = '\n' -): [Line[], number] { - const linesToAdd: Line[] = prevLines.length - ? [prevLines[prevLines.length - 1]] - : [{ message: '' }]; - let charCount = 0; - - for (const message of messages) { - charCount += message.length; - const lastLine = linesToAdd[linesToAdd.length - 1]; - - if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { - linesToAdd.push({ message, timestamp: new Date() }); - } else { - linesToAdd[linesToAdd.length - 1].message += message; - if (!linesToAdd[linesToAdd.length - 1].timestamp) { - linesToAdd[linesToAdd.length - 1].timestamp = new Date(); - } - } - } - - prevLines.splice(prevLines.length - 1, 1, ...linesToAdd); - return [prevLines, charCount]; -} - -function truncateLines(lines: Line[], charCount: number): [Line[], number] { - let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS; - while (charsToDelete > 0) { - const firstLineLength = lines[0]?.message?.length; - const newFirstLine = lines[0]?.message?.substring(charsToDelete); - const deletedCharsCount = firstLineLength - newFirstLine.length; - charCount -= deletedCharsCount; - charsToDelete -= deletedCharsCount; - lines[0].message = newFirstLine; - if (!newFirstLine?.length) { - lines.shift(); - } - } - return [lines, charCount]; -} diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-input.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-input.tsx new file mode 100644 index 000000000..6bbe032c6 --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-input.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { Key, KeyCode } from '@theia/core/lib/browser/keys'; +import { Board, Port } from '../../common/protocol/boards-service'; +import { MonitorConfig } from '../../common/protocol/monitor-service'; +import { isOSX } from '@theia/core/lib/common/os'; + +export namespace SerialMonitorSendInput { + export interface Props { + readonly monitorConfig?: MonitorConfig; + readonly onSend: (text: string) => void; + readonly resolveFocus: (element: HTMLElement | undefined) => void; + } + export interface State { + text: string; + } +} + +export class SerialMonitorSendInput extends React.Component< + SerialMonitorSendInput.Props, + SerialMonitorSendInput.State +> { + constructor(props: Readonly) { + super(props); + this.state = { text: '' }; + this.onChange = this.onChange.bind(this); + this.onSend = this.onSend.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + render(): React.ReactNode { + return ( + + ); + } + + protected get placeholder(): string { + const { monitorConfig } = this.props; + if (!monitorConfig) { + return 'Not connected. Select a board and a port to connect automatically.'; + } + const { board, port } = monitorConfig; + return `Message (${ + isOSX ? '⌘' : 'Ctrl' + }+Enter to send message to '${Board.toString(board, { + useFqbn: false, + })}' on '${Port.toString(port)}')`; + } + + protected setRef = (element: HTMLElement | null) => { + if (this.props.resolveFocus) { + this.props.resolveFocus(element || undefined); + } + }; + + protected onChange(event: React.ChangeEvent): void { + this.setState({ text: event.target.value }); + } + + protected onSend(): void { + this.props.onSend(this.state.text); + this.setState({ text: '' }); + } + + protected onKeyDown(event: React.KeyboardEvent): void { + const keyCode = KeyCode.createKeyCode(event.nativeEvent); + if (keyCode) { + const { key, meta, ctrl } = keyCode; + if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) { + this.onSend(); + } + } + } +} diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx new file mode 100644 index 000000000..cda1d1f6f --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -0,0 +1,158 @@ +import * as React from 'react'; +import { Event } from '@theia/core/lib/common/event'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { MonitorModel } from './monitor-model'; +import { MonitorConnection } from './monitor-connection'; +import dateFormat = require('dateformat'); +import { messageToLines, truncateLines } from './monitor-utils'; + +export type Line = { message: string; timestamp?: Date }; + +export class SerialMonitorOutput extends React.Component< + SerialMonitorOutput.Props, + SerialMonitorOutput.State +> { + /** + * Do not touch it. It is used to be able to "follow" the serial monitor log. + */ + protected anchor: HTMLElement | null; + protected toDisposeBeforeUnmount = new DisposableCollection(); + + constructor(props: Readonly) { + super(props); + this.state = { + lines: [], + timestamp: this.props.monitorModel.timestamp, + charCount: 0, + }; + } + + render(): React.ReactNode { + return ( + + + {({ height, width }) => ( + + {Row} + + )} + + {/*
+ {this.state.lines.map((line, i) => ( + + ))} +
*/} +
{ + this.anchor = element; + }} + /> + + ); + } + + shouldComponentUpdate(): boolean { + return true; + } + + componentDidMount(): void { + this.scrollToBottom(); + this.toDisposeBeforeUnmount.pushAll([ + this.props.monitorConnection.onRead(({ messages }) => { + const [newLines, charsToAddCount] = messageToLines( + messages, + this.state.lines + ); + const [lines, charCount] = truncateLines( + newLines, + this.state.charCount + charsToAddCount + ); + + this.setState({ + lines, + charCount, + }); + }), + this.props.clearConsoleEvent(() => this.setState({ lines: [] })), + this.props.monitorModel.onChange(({ property }) => { + if (property === 'timestamp') { + const { timestamp } = this.props.monitorModel; + this.setState({ timestamp }); + } + }), + ]); + } + + componentDidUpdate(): void { + this.scrollToBottom(); + } + + componentWillUnmount(): void { + // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? + this.toDisposeBeforeUnmount.dispose(); + } + + protected scrollToBottom(): void { + if (this.props.monitorModel.autoscroll && this.anchor) { + this.anchor.scrollIntoView(); + // this.listRef.current.scrollToItem(this.state.lines.length); + } + } +} + +const Row = ({ + index, + style, + data, +}: { + index: number; + style: any; + data: { lines: Line[]; timestamp: boolean }; +}) => { + const timestamp = + (data.timestamp && + `${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) || + ''; + return ( +
+ {timestamp} + {data.lines[index].message} +
+ ); +}; + +export namespace SerialMonitorOutput { + export interface Props { + readonly monitorModel: MonitorModel; + readonly monitorConnection: MonitorConnection; + readonly clearConsoleEvent: Event; + } + + export interface State { + lines: Line[]; + timestamp: boolean; + charCount: number; + } + + export interface SelectOption { + readonly label: string; + readonly value: T; + } + + export const MAX_CHARACTERS = 1_000_000; +} From 8c91be95ac07090649ac8ff7dbb8252e93862eeb Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Sun, 3 Oct 2021 17:20:31 +0200 Subject: [PATCH 07/11] serial monitor style and perfo --- .../src/browser/monitor/monitor-utils.ts | 32 ++++++++++--- .../monitor/serial-monitor-send-output.tsx | 47 ++++++++----------- .../src/browser/style/monitor.css | 5 ++ 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor/monitor-utils.ts b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts index 3ea1eea05..30607d724 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-utils.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts @@ -7,17 +7,25 @@ export function messageToLines( ): [Line[], number] { const linesToAdd: Line[] = prevLines.length ? [prevLines[prevLines.length - 1]] - : [{ message: '' }]; + : [{ message: '', lineLen: 0 }]; let charCount = 0; for (const message of messages) { - charCount += message.length; + const messageLen = message.length; + charCount += messageLen; const lastLine = linesToAdd[linesToAdd.length - 1]; + // if the previous messages ends with "separator" add a new line if (lastLine.message.charAt(lastLine.message.length - 1) === separator) { - linesToAdd.push({ message, timestamp: new Date() }); + linesToAdd.push({ + message, + timestamp: new Date(), + lineLen: messageLen, + }); } else { + // concatenate to the last line linesToAdd[linesToAdd.length - 1].message += message; + linesToAdd[linesToAdd.length - 1].lineLen += messageLen; if (!linesToAdd[linesToAdd.length - 1].timestamp) { linesToAdd[linesToAdd.length - 1].timestamp = new Date(); } @@ -33,16 +41,26 @@ export function truncateLines( charCount: number ): [Line[], number] { let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS; + let lineIndex = 0; while (charsToDelete > 0) { - const firstLineLength = lines[0]?.message?.length; + const firstLineLength = lines[lineIndex]?.lineLen; + + if (charsToDelete >= firstLineLength) { + // every time a full line to delete is found, move the index. + lineIndex++; + charsToDelete -= firstLineLength; + charCount -= firstLineLength; + continue; + } + + // delete all previous lines + lines.splice(0, lineIndex); + const newFirstLine = lines[0]?.message?.substring(charsToDelete); const deletedCharsCount = firstLineLength - newFirstLine.length; charCount -= deletedCharsCount; charsToDelete -= deletedCharsCount; lines[0].message = newFirstLine; - if (!newFirstLine?.length) { - lines.shift(); - } } return [lines, charCount]; } diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx index cda1d1f6f..78b40fff1 100644 --- a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -8,7 +8,7 @@ import { MonitorConnection } from './monitor-connection'; import dateFormat = require('dateformat'); import { messageToLines, truncateLines } from './monitor-utils'; -export type Line = { message: string; timestamp?: Date }; +export type Line = { message: string; timestamp?: Date; lineLen: number }; export class SerialMonitorOutput extends React.Component< SerialMonitorOutput.Props, @@ -17,11 +17,12 @@ export class SerialMonitorOutput extends React.Component< /** * Do not touch it. It is used to be able to "follow" the serial monitor log. */ - protected anchor: HTMLElement | null; protected toDisposeBeforeUnmount = new DisposableCollection(); + private listRef: React.RefObject; constructor(props: Readonly) { super(props); + this.listRef = React.createRef(); this.state = { lines: [], timestamp: this.props.monitorModel.timestamp, @@ -35,7 +36,7 @@ export class SerialMonitorOutput extends React.Component< {({ height, width }) => ( {Row} )} - {/*
- {this.state.lines.map((line, i) => ( - - ))} -
*/} -
{ - this.anchor = element; - }} - /> ); } @@ -94,25 +86,23 @@ export class SerialMonitorOutput extends React.Component< const { timestamp } = this.props.monitorModel; this.setState({ timestamp }); } + if (property === 'autoscroll') { + this.scrollToBottom(); + } }), ]); } - componentDidUpdate(): void { - this.scrollToBottom(); - } - componentWillUnmount(): void { // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? this.toDisposeBeforeUnmount.dispose(); } - protected scrollToBottom(): void { - if (this.props.monitorModel.autoscroll && this.anchor) { - this.anchor.scrollIntoView(); - // this.listRef.current.scrollToItem(this.state.lines.length); + scrollToBottom = ((): void => { + if (this.listRef.current && this.props.monitorModel.autoscroll) { + this.listRef.current.scrollToItem(this.state.lines.length, 'end'); } - } + }).bind(this); } const Row = ({ @@ -129,10 +119,13 @@ const Row = ({ `${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) || ''; return ( -
- {timestamp} - {data.lines[index].message} -
+ (data.lines[index].lineLen && ( +
+ {timestamp} + {data.lines[index].message} +
+ )) || + null ); }; diff --git a/arduino-ide-extension/src/browser/style/monitor.css b/arduino-ide-extension/src/browser/style/monitor.css index 61a4a58b9..9e8bd44cc 100644 --- a/arduino-ide-extension/src/browser/style/monitor.css +++ b/arduino-ide-extension/src/browser/style/monitor.css @@ -9,6 +9,11 @@ flex-direction: column; } +.serial-monitor-messages { + white-space: 'pre'; + font-family: monospace +} + .serial-monitor .head { display: flex; padding: 5px; From b7839eb2197aca4b44fe73f3c22921de8da0498f Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Tue, 5 Oct 2021 10:16:30 +0200 Subject: [PATCH 08/11] fix serial-monitor-messages box height --- .../src/browser/monitor/serial-monitor-send-output.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx index 78b40fff1..d1f44e8cf 100644 --- a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -32,7 +32,7 @@ export class SerialMonitorOutput extends React.Component< render(): React.ReactNode { return ( - +
{({ height, width }) => ( )} - +
); } From e550c8d24e1aa41fec3fb5529534df1ed7f4b0af Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Tue, 5 Oct 2021 11:50:31 +0200 Subject: [PATCH 09/11] removed react autosizer --- arduino-ide-extension/package.json | 2 - .../src/browser/monitor/monitor-widget.tsx | 1 + .../monitor/serial-monitor-send-output.tsx | 47 +++++++++---------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 70847582b..6becd8ae4 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -79,7 +79,6 @@ "react-disable": "^0.1.0", "react-select": "^3.0.4", "react-tabs": "^3.1.2", - "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "semver": "^7.3.2", "string-natural-compare": "^2.0.3", @@ -92,7 +91,6 @@ "@types/chai": "^4.2.7", "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", - "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "chai": "^4.2.0", "chai-string": "^1.5.0", diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 6575a69d4..c4f0030f9 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -191,6 +191,7 @@ export class MonitorWidget extends ReactWidget { monitorModel={this.monitorModel} monitorConnection={this.monitorConnection} clearConsoleEvent={this.clearOutputEmitter.event} + height={Math.floor(this.widgetHeight - 50)} />
diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx index d1f44e8cf..29f60e212 100644 --- a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { Event } from '@theia/core/lib/common/event'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { FixedSizeList as List } from 'react-window'; -import AutoSizer from 'react-virtualized-auto-sizer'; +import { areEqual, FixedSizeList as List } from 'react-window'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; import dateFormat = require('dateformat'); @@ -32,29 +31,23 @@ export class SerialMonitorOutput extends React.Component< render(): React.ReactNode { return ( -
- - {({ height, width }) => ( - - {Row} - - )} - -
+ + {Row} + ); } @@ -105,7 +98,7 @@ export class SerialMonitorOutput extends React.Component< }).bind(this); } -const Row = ({ +const _Row = ({ index, style, data, @@ -128,12 +121,14 @@ const Row = ({ null ); }; +const Row = React.memo(_Row, areEqual); export namespace SerialMonitorOutput { export interface Props { readonly monitorModel: MonitorModel; readonly monitorConnection: MonitorConnection; readonly clearConsoleEvent: Event; + readonly height: number; } export interface State { From 4135a18265b2f432fccff5da1afb7577695e7b4b Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Tue, 5 Oct 2021 10:57:58 +0100 Subject: [PATCH 10/11] add unit tests for monitor utils (#522) * wip tests * test monitor utils --- arduino-ide-extension/package.json | 1 + .../src/browser/monitor/monitor-connection.ts | 237 +++++++++--------- .../src/browser/monitor/monitor-utils.ts | 14 +- .../monitor/serial-monitor-send-output.tsx | 12 +- .../src/test/browser/monitor-utils.test.ts | 171 +++++++++++++ yarn.lock | 5 + 6 files changed, 307 insertions(+), 133 deletions(-) create mode 100644 arduino-ide-extension/src/test/browser/monitor-utils.test.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 6becd8ae4..1279d7c32 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -100,6 +100,7 @@ "download": "^7.1.0", "grpc_tools_node_protoc_ts": "^4.1.0", "mocha": "^7.0.0", + "mockdate": "^3.0.5", "moment": "^2.24.0", "protoc": "^1.0.4", "shelljs": "^0.8.3", diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index e37d15f3d..f04e33b2d 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -71,129 +71,15 @@ export class MonitorConnection { @postConstruct() protected init(): void { - this.monitorServiceClient.onMessage(async (port) => { - const w = new WebSocket(`ws://localhost:${port}`); - w.onmessage = (res) => { - const messages = JSON.parse(res.data); - this.onReadEmitter.fire({ messages }); - }; - }); - - // let i = 0; - // this.monitorServiceClient.onMessage(async (msg) => { - // // msg received - // i++; - // if (i % 1000 === 0) { - // // console.log(msg); - // } - // }); - - this.monitorServiceClient.onError(async (error) => { - let shouldReconnect = false; - if (this.state) { - const { code, config } = error; - const { board, port } = config; - const options = { timeout: 3000 }; - switch (code) { - case MonitorError.ErrorCodes.CLIENT_CANCEL: { - console.debug( - `Connection was canceled by client: ${MonitorConnection.State.toString( - this.state - )}.` - ); - break; - } - case MonitorError.ErrorCodes.DEVICE_BUSY: { - this.messageService.warn( - `Connection failed. Serial port is busy: ${Port.toString(port)}.`, - options - ); - shouldReconnect = this.autoConnect; - this.monitorErrors.push(error); - break; - } - case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: { - this.messageService.info( - `Disconnected ${Board.toString(board, { - useFqbn: false, - })} from ${Port.toString(port)}.`, - options - ); - break; - } - case undefined: { - this.messageService.error( - `Unexpected error. Reconnecting ${Board.toString( - board - )} on port ${Port.toString(port)}.`, - options - ); - console.error(JSON.stringify(error)); - shouldReconnect = this.connected && this.autoConnect; - break; - } - } - const oldState = this.state; - this.state = undefined; - this.onConnectionChangedEmitter.fire(this.state); - if (shouldReconnect) { - if (this.monitorErrors.length >= 10) { - this.messageService.warn( - `Failed to reconnect ${Board.toString(board, { - useFqbn: false, - })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString( - port - )} serial port is busy. after 10 consecutive attempts.` - ); - this.monitorErrors.length = 0; - } else { - const attempts = this.monitorErrors.length || 1; - if (this.reconnectTimeout !== undefined) { - // Clear the previous timer. - window.clearTimeout(this.reconnectTimeout); - } - const timeout = attempts * 1000; - this.messageService.warn( - `Reconnecting ${Board.toString(board, { - useFqbn: false, - })} to ${Port.toString(port)} in ${attempts} seconds...`, - { timeout } - ); - this.reconnectTimeout = window.setTimeout( - () => this.connect(oldState.config), - timeout - ); - } - } - } - }); + this.monitorServiceClient.onMessage(this.handleMessage.bind(this)); + this.monitorServiceClient.onError(this.handleError.bind(this)); this.boardsServiceProvider.onBoardsConfigChanged( this.handleBoardConfigChange.bind(this) ); - this.notificationCenter.onAttachedBoardsChanged((event) => { - if (this.autoConnect && this.connected) { - const { boardsConfig } = this.boardsServiceProvider; - if ( - this.boardsServiceProvider.canUploadTo(boardsConfig, { - silent: false, - }) - ) { - const { attached } = AttachedBoardsChangeEvent.diff(event); - if ( - attached.boards.some( - (board) => - !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board) - ) - ) { - const { selectedBoard: board, selectedPort: port } = boardsConfig; - const { baudRate } = this.monitorModel; - this.disconnect().then(() => - this.connect({ board, port, baudRate }) - ); - } - } - } - }); + this.notificationCenter.onAttachedBoardsChanged( + this.handleAttachedBoardsChanged.bind(this) + ); + // Handles the `baudRate` changes by reconnecting if required. this.monitorModel.onChange(({ property }) => { if (property === 'baudRate' && this.autoConnect && this.connected) { @@ -203,6 +89,14 @@ export class MonitorConnection { }); } + async handleMessage(port: string): Promise { + const w = new WebSocket(`ws://localhost:${port}`); + w.onmessage = (res) => { + const messages = JSON.parse(res.data); + this.onReadEmitter.fire({ messages }); + }; + } + get connected(): boolean { return !!this.state; } @@ -234,6 +128,109 @@ export class MonitorConnection { } } + handleError(error: MonitorError): void { + let shouldReconnect = false; + if (this.state) { + const { code, config } = error; + const { board, port } = config; + const options = { timeout: 3000 }; + switch (code) { + case MonitorError.ErrorCodes.CLIENT_CANCEL: { + console.debug( + `Connection was canceled by client: ${MonitorConnection.State.toString( + this.state + )}.` + ); + break; + } + case MonitorError.ErrorCodes.DEVICE_BUSY: { + this.messageService.warn( + `Connection failed. Serial port is busy: ${Port.toString(port)}.`, + options + ); + shouldReconnect = this.autoConnect; + this.monitorErrors.push(error); + break; + } + case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: { + this.messageService.info( + `Disconnected ${Board.toString(board, { + useFqbn: false, + })} from ${Port.toString(port)}.`, + options + ); + break; + } + case undefined: { + this.messageService.error( + `Unexpected error. Reconnecting ${Board.toString( + board + )} on port ${Port.toString(port)}.`, + options + ); + console.error(JSON.stringify(error)); + shouldReconnect = this.connected && this.autoConnect; + break; + } + } + const oldState = this.state; + this.state = undefined; + this.onConnectionChangedEmitter.fire(this.state); + if (shouldReconnect) { + if (this.monitorErrors.length >= 10) { + this.messageService.warn( + `Failed to reconnect ${Board.toString(board, { + useFqbn: false, + })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString( + port + )} serial port is busy. after 10 consecutive attempts.` + ); + this.monitorErrors.length = 0; + } else { + const attempts = this.monitorErrors.length || 1; + if (this.reconnectTimeout !== undefined) { + // Clear the previous timer. + window.clearTimeout(this.reconnectTimeout); + } + const timeout = attempts * 1000; + this.messageService.warn( + `Reconnecting ${Board.toString(board, { + useFqbn: false, + })} to ${Port.toString(port)} in ${attempts} seconds...`, + { timeout } + ); + this.reconnectTimeout = window.setTimeout( + () => this.connect(oldState.config), + timeout + ); + } + } + } + } + + handleAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { + if (this.autoConnect && this.connected) { + const { boardsConfig } = this.boardsServiceProvider; + if ( + this.boardsServiceProvider.canUploadTo(boardsConfig, { + silent: false, + }) + ) { + const { attached } = AttachedBoardsChangeEvent.diff(event); + if ( + attached.boards.some( + (board) => + !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board) + ) + ) { + const { selectedBoard: board, selectedPort: port } = boardsConfig; + const { baudRate } = this.monitorModel; + this.disconnect().then(() => this.connect({ board, port, baudRate })); + } + } + } + } + async connect(config: MonitorConfig): Promise { if (this.connected) { const disconnectStatus = await this.disconnect(); diff --git a/arduino-ide-extension/src/browser/monitor/monitor-utils.ts b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts index 30607d724..586eea146 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-utils.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-utils.ts @@ -1,14 +1,14 @@ import { Line, SerialMonitorOutput } from './serial-monitor-send-output'; -export function messageToLines( +export function messagesToLines( messages: string[], - prevLines: Line[], + prevLines: Line[] = [], + charCount = 0, separator = '\n' ): [Line[], number] { const linesToAdd: Line[] = prevLines.length ? [prevLines[prevLines.length - 1]] : [{ message: '', lineLen: 0 }]; - let charCount = 0; for (const message of messages) { const messageLen = message.length; @@ -38,11 +38,12 @@ export function messageToLines( export function truncateLines( lines: Line[], - charCount: number + charCount: number, + maxCharacters: number = SerialMonitorOutput.MAX_CHARACTERS ): [Line[], number] { - let charsToDelete = charCount - SerialMonitorOutput.MAX_CHARACTERS; + let charsToDelete = charCount - maxCharacters; let lineIndex = 0; - while (charsToDelete > 0) { + while (charsToDelete > 0 || lineIndex > 0) { const firstLineLength = lines[lineIndex]?.lineLen; if (charsToDelete >= firstLineLength) { @@ -55,6 +56,7 @@ export function truncateLines( // delete all previous lines lines.splice(0, lineIndex); + lineIndex = 0; const newFirstLine = lines[0]?.message?.substring(charsToDelete); const deletedCharsCount = firstLineLength - newFirstLine.length; diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx index 29f60e212..ab505fd9a 100644 --- a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -5,7 +5,7 @@ import { areEqual, FixedSizeList as List } from 'react-window'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; import dateFormat = require('dateformat'); -import { messageToLines, truncateLines } from './monitor-utils'; +import { messagesToLines, truncateLines } from './monitor-utils'; export type Line = { message: string; timestamp?: Date; lineLen: number }; @@ -59,14 +59,12 @@ export class SerialMonitorOutput extends React.Component< this.scrollToBottom(); this.toDisposeBeforeUnmount.pushAll([ this.props.monitorConnection.onRead(({ messages }) => { - const [newLines, charsToAddCount] = messageToLines( + const [newLines, totalCharCount] = messagesToLines( messages, - this.state.lines - ); - const [lines, charCount] = truncateLines( - newLines, - this.state.charCount + charsToAddCount + this.state.lines, + this.state.charCount ); + const [lines, charCount] = truncateLines(newLines, totalCharCount); this.setState({ lines, diff --git a/arduino-ide-extension/src/test/browser/monitor-utils.test.ts b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts new file mode 100644 index 000000000..3e1e90e6d --- /dev/null +++ b/arduino-ide-extension/src/test/browser/monitor-utils.test.ts @@ -0,0 +1,171 @@ +import { expect } from 'chai'; +import { + messagesToLines, + truncateLines, +} from '../../browser/monitor/monitor-utils'; +import { Line } from '../../browser/monitor/serial-monitor-send-output'; +import { set, reset } from 'mockdate'; + +type TestLine = { + messages: string[]; + prevLines?: { lines: Line[]; charCount: number }; + expected: { lines: Line[]; charCount: number }; + expectedTruncated?: { + lines: Line[]; + charCount: number; + maxCharacters?: number; + }; +}; + +const date = new Date(); +const testLines: TestLine[] = [ + { + messages: ['Hello'], + expected: { lines: [{ message: 'Hello', lineLen: 5 }], charCount: 5 }, + }, + { + messages: ['Hello', 'Dog!'], + expected: { lines: [{ message: 'HelloDog!', lineLen: 9 }], charCount: 9 }, + }, + { + messages: ['Hello\n', 'Dog!'], + expected: { + lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!', lineLen: 4 }, + ], + charCount: 10, + }, + }, + { + messages: ['Dog!'], + prevLines: { lines: [{ message: 'Hello\n', lineLen: 6 }], charCount: 6 }, + expected: { + lines: [ + { message: 'Hello\n', lineLen: 6 }, + { message: 'Dog!', lineLen: 4 }, + ], + charCount: 10, + }, + }, + { + messages: [' Dog!\n', " Who's a good ", 'boy?\n', "You're a good boy!"], + prevLines: { lines: [{ message: 'Hello', lineLen: 5 }], charCount: 5 }, + expected: { + lines: [ + { message: 'Hello Dog!\n', lineLen: 11 }, + { message: " Who's a good boy?\n", lineLen: 19 }, + { message: "You're a good boy!", lineLen: 8 }, + ], + charCount: 48, + }, + expectedTruncated: { + maxCharacters: 20, + charCount: 20, + lines: [ + { message: '?\n', lineLen: 2 }, + { message: "You're a good boy!", lineLen: 8 }, + ], + }, + }, + { + messages: ['boy?\n', "You're a good boy!"], + prevLines: { + lines: [ + { message: 'Hello Dog!\n', lineLen: 11 }, + { message: " Who's a good ", lineLen: 14 }, + ], + charCount: 25, + }, + expected: { + lines: [ + { message: 'Hello Dog!\n', lineLen: 11 }, + { message: " Who's a good boy?\n", lineLen: 19 }, + { message: "You're a good boy!", lineLen: 8 }, + ], + charCount: 48, + }, + expectedTruncated: { + maxCharacters: 20, + charCount: 20, + lines: [ + { message: '?\n', lineLen: 2 }, + { message: "You're a good boy!", lineLen: 8 }, + ], + }, + }, + { + messages: ["Who's a good boy?\n", 'Yo'], + prevLines: { + lines: [{ message: 'Hello Dog!\n', lineLen: 11 }], + charCount: 11, + }, + expected: { + lines: [ + { message: 'Hello Dog!\n', lineLen: 11 }, + { message: "Who's a good boy?\n", lineLen: 18 }, + { message: 'Yo', lineLen: 2 }, + ], + charCount: 31, + }, + expectedTruncated: { + maxCharacters: 20, + charCount: 20, + lines: [ + { message: "Who's a good boy?\n", lineLen: 18 }, + { message: 'Yo', lineLen: 2 }, + ], + }, + }, +]; + +testLines.forEach((t) => + [...t.expected.lines, ...(t.prevLines?.lines || [])].forEach( + (l) => (l.timestamp = date) + ) +); + +describe.only('Monitor Utils', () => { + beforeEach(() => { + set(date); + }); + + afterEach(() => { + reset(); + }); + + testLines.forEach((testLine) => { + context('when converting messages', () => { + it('should give the right result', () => { + const [newLines, addedCharCount] = messagesToLines( + testLine.messages, + testLine.prevLines?.lines, + testLine.prevLines?.charCount + ); + newLines.forEach((line, index) => { + expect(line.message).to.equal(testLine.expected.lines[index].message); + expect(line.timestamp).to.deep.equal( + testLine.expected.lines[index].timestamp + ); + }); + expect(addedCharCount).to.equal(testLine.expected.charCount); + + const [truncatedLines, totalCharCount] = truncateLines( + newLines, + addedCharCount, + testLine.expectedTruncated?.maxCharacters + ); + let charCount = 0; + if (testLine.expectedTruncated) { + truncatedLines.forEach((line, index) => { + expect(line.message).to.equal( + testLine.expectedTruncated?.lines[index].message + ); + charCount += line.message.length; + }); + expect(totalCharCount).to.equal(charCount); + } + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index ec04154f3..9ca2ebebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11743,6 +11743,11 @@ mocha@^7.0.0: yargs-parser "13.1.2" yargs-unparser "1.6.0" +mockdate@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb" + integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" From beca35401ac529363045237f46717f81f0d5ae64 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Tue, 5 Oct 2021 17:39:47 +0200 Subject: [PATCH 11/11] fix scroll issue --- .../browser/monitor/serial-monitor-send-output.tsx | 2 +- yarn.lock | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx index ab505fd9a..ff7bdba90 100644 --- a/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/monitor/serial-monitor-send-output.tsx @@ -44,7 +44,6 @@ export class SerialMonitorOutput extends React.Component< itemSize={18} width={'100%'} ref={this.listRef} - onItemsRendered={this.scrollToBottom} > {Row} @@ -70,6 +69,7 @@ export class SerialMonitorOutput extends React.Component< lines, charCount, }); + this.scrollToBottom(); }), this.props.clearConsoleEvent(() => this.setState({ lines: [] })), this.props.monitorModel.onChange(({ property }) => { diff --git a/yarn.lock b/yarn.lock index 9ca2ebebd..7f885a12f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3020,13 +3020,6 @@ dependencies: "@types/react" "*" -"@types/react-virtualized-auto-sizer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" - integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong== - dependencies: - "@types/react" "*" - "@types/react-virtualized@^9.18.3": version "9.21.11" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.11.tgz#8eb60ed12ef0b2625769819f9fd10ad4bb1bdce0" @@ -13947,11 +13940,6 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-virtualized-auto-sizer@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" - integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== - react-virtualized@^9.20.0: version "9.22.3" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"