diff options
Diffstat (limited to 'pw_web/webconsole/components/repl/index.tsx')
-rw-r--r-- | pw_web/webconsole/components/repl/index.tsx | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/pw_web/webconsole/components/repl/index.tsx b/pw_web/webconsole/components/repl/index.tsx new file mode 100644 index 000000000..a48e8cd57 --- /dev/null +++ b/pw_web/webconsole/components/repl/index.tsx @@ -0,0 +1,202 @@ +// Copyright 2022 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import {useEffect, useState} from "react"; +import {Device} from "pigweedjs"; +import {EditorView} from "codemirror" +import {basicSetup} from "./basicSetup"; +import {javascript, javascriptLanguage} from "@codemirror/lang-javascript" +import {placeholder} from "@codemirror/view"; +import {oneDark} from "@codemirror/theme-one-dark"; +import {keymap} from "@codemirror/view" +import {Extension} from "@codemirror/state" +import {completeFromGlobalScope} from "./autocomplete"; +import LocalStorageArray from "./localStorageArray"; +import "xterm/css/xterm.css"; +import styles from "../../styles/repl.module.css"; + +const isSSR = () => typeof window === 'undefined'; + +interface ReplProps { + device: Device | undefined +} + +const globalJavaScriptCompletions = javascriptLanguage.data.of({ + autocomplete: completeFromGlobalScope +}) + +const createTerminal = async (container: HTMLElement) => { + const {Terminal} = await import('xterm'); + const {FitAddon} = await import('xterm-addon-fit'); + const terminal = new Terminal({ + // cursorBlink: true, + theme: { + background: '#2c313a' + } + }); + terminal.open(container); + + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + fitAddon.fit(); + return terminal; +}; + +const createPlaceholderText = () => { + var div = document.createElement('div'); + div.innerHTML = `Type code and hit Enter to run. See <b>[?]</b> for more info.` + return div; +} + +const createEditor = (container: HTMLElement, enterKeyMap: Extension) => { + let view = new EditorView({ + extensions: [basicSetup, javascript(), placeholder(createPlaceholderText()), oneDark, globalJavaScriptCompletions, enterKeyMap], + parent: container, + }); + return view; +} + +let currentCommandHistoryIndex = -1; +let historyStorage: LocalStorageArray; +if (typeof window !== 'undefined') { + historyStorage = new LocalStorageArray(); +} + +export default function Repl({device}: ReplProps) { + const [terminal, setTerminal] = useState<any>(null); + const [codeEditor, setCodeEditor] = useState<EditorView | null>(null); + + useEffect(() => { + let cleanupFns: {(): void; (): void;}[] = []; + if (!terminal && !isSSR() && device) { + const futureTerm = createTerminal(document.querySelector('#repl-log-container')!); + futureTerm.then(async (term) => { + cleanupFns.push(() => { + term.dispose(); + setTerminal(null); + }); + setTerminal(term); + }); + + return () => { + cleanupFns.forEach(fn => fn()); + } + } + else if (terminal && !device) { + terminal.dispose(); + setTerminal(null); + } + }, [device]); + + useEffect(() => { + if (!terminal) return; + const enterKeyMap = { + key: "Enter", + run(view: EditorView) { + if (view.state.doc.toString().trim().length === 0) return true; + try { + // To run eval() in global scope, we do (1, eval) here. + const cmdOutput = (1, eval)(view.state.doc.toString()); + // Check if eval returned a promise + if (typeof cmdOutput === "object" && cmdOutput.then !== undefined) { + cmdOutput + .then((result: any) => { + terminal.write(`Promise { ${result} }\r\n`); + }) + .catch((e: any) => { + if (e instanceof Error) { + terminal.write(`\x1b[31;1mUncaught (in promise) Error: ${e.message}\x1b[0m\r\n`) + } + else { + terminal.write(`\x1b[31;1mUncaught (in promise) ${e}\x1b[0m\r\n`) + } + }); + } + else { + terminal.write(cmdOutput + "\r\n"); + } + } + catch (e) { + if (e instanceof Error) terminal.write(`\x1b[31;1m${e.message}\x1b[0m\r\n`) + } + + currentCommandHistoryIndex = -1; + historyStorage.unshift(view.state.doc.toString()); + + // Clear text editor + const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}}); + view.dispatch(transaction); + return true; + } + }; + + const upKeyMap = { + key: "ArrowUp", + run(view: EditorView) { + currentCommandHistoryIndex++; + if (historyStorage.data[currentCommandHistoryIndex]) { + // set text editor + const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}}); + view.dispatch(transaction); + } + else { + currentCommandHistoryIndex = historyStorage.data.length - 1; + } + return true; + } + }; + + const downKeyMap = { + key: "ArrowDown", + run(view: EditorView) { + currentCommandHistoryIndex--; + if (currentCommandHistoryIndex <= -1) { + currentCommandHistoryIndex = -1; + const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}}); + view.dispatch(transaction); + } + else if (historyStorage.data[currentCommandHistoryIndex]) { + // set text editor + const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}}); + view.dispatch(transaction); + } + return true; + } + }; + + const keymaps = keymap.of([enterKeyMap, upKeyMap, downKeyMap]); + let view = createEditor(document.querySelector('#repl-editor-container')!, keymaps); + return () => view.destroy(); + }, [terminal]); + + return ( + <div className={styles.container}> + <div id="repl-log-container" className={styles.logs}></div> + <div className={styles.replWithCaret}> + <div> + <div className={styles.tooltip}>? + <span className={styles.tooltiptext}> + This REPL runs JavaScript. + You can navigate previous commands using <span>Up</span> and <span>Down</span> arrow keys. + <br /><br /> + Call device RPCs using <span>device.rpcs.*</span> API. + </span> + </div> + <span className={styles.caret}>{`> `}</span> + </div> + <div id="repl-editor-container" className={styles.editor}></div> + </div> + </div> + ) +} |