// 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 [?] 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(null); const [codeEditor, setCodeEditor] = useState(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 (
? This REPL runs JavaScript. You can navigate previous commands using Up and Down arrow keys.

Call device RPCs using device.rpcs.* API.
{`> `}
) }