• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15import {useEffect, useState} from "react";
16import {Device} from "pigweedjs";
17import {EditorView} from "codemirror"
18import {basicSetup} from "./basicSetup";
19import {javascript, javascriptLanguage} from "@codemirror/lang-javascript"
20import {placeholder} from "@codemirror/view";
21import {oneDark} from "@codemirror/theme-one-dark";
22import {keymap} from "@codemirror/view"
23import {Extension} from "@codemirror/state"
24import {completeFromGlobalScope} from "./autocomplete";
25import LocalStorageArray from "./localStorageArray";
26import "xterm/css/xterm.css";
27import styles from "../../styles/repl.module.css";
28
29const isSSR = () => typeof window === 'undefined';
30
31interface ReplProps {
32  device: Device | undefined
33}
34
35const globalJavaScriptCompletions = javascriptLanguage.data.of({
36  autocomplete: completeFromGlobalScope
37})
38
39const createTerminal = async (container: HTMLElement) => {
40  const {Terminal} = await import('xterm');
41  const {FitAddon} = await import('xterm-addon-fit');
42  const terminal = new Terminal({
43    // cursorBlink: true,
44    theme: {
45      background: '#2c313a'
46    }
47  });
48  terminal.open(container);
49
50  const fitAddon = new FitAddon();
51  terminal.loadAddon(fitAddon);
52  fitAddon.fit();
53  return terminal;
54};
55
56const createPlaceholderText = () => {
57  var div = document.createElement('div');
58  div.innerHTML = `Type code and hit Enter to run. See <b>[?]</b> for more info.`
59  return div;
60}
61
62const createEditor = (container: HTMLElement, enterKeyMap: Extension) => {
63  let view = new EditorView({
64    extensions: [basicSetup, javascript(), placeholder(createPlaceholderText()), oneDark, globalJavaScriptCompletions, enterKeyMap],
65    parent: container,
66  });
67  return view;
68}
69
70let currentCommandHistoryIndex = -1;
71let historyStorage: LocalStorageArray;
72if (typeof window !== 'undefined') {
73  historyStorage = new LocalStorageArray();
74}
75
76export default function Repl({device}: ReplProps) {
77  const [terminal, setTerminal] = useState<any>(null);
78  const [codeEditor, setCodeEditor] = useState<EditorView | null>(null);
79
80  useEffect(() => {
81    let cleanupFns: {(): void; (): void;}[] = [];
82    if (!terminal && !isSSR() && device) {
83      const futureTerm = createTerminal(document.querySelector('#repl-log-container')!);
84      futureTerm.then(async (term) => {
85        cleanupFns.push(() => {
86          term.dispose();
87          setTerminal(null);
88        });
89        setTerminal(term);
90      });
91
92      return () => {
93        cleanupFns.forEach(fn => fn());
94      }
95    }
96    else if (terminal && !device) {
97      terminal.dispose();
98      setTerminal(null);
99    }
100  }, [device]);
101
102  useEffect(() => {
103    if (!terminal) return;
104    const enterKeyMap = {
105      key: "Enter",
106      run(view: EditorView) {
107        if (view.state.doc.toString().trim().length === 0) return true;
108        try {
109          // To run eval() in global scope, we do (1, eval) here.
110          const cmdOutput = (1, eval)(view.state.doc.toString());
111          // Check if eval returned a promise
112          if (typeof cmdOutput === "object" && cmdOutput.then !== undefined) {
113            cmdOutput
114              .then((result: any) => {
115                terminal.write(`Promise { ${result} }\r\n`);
116              })
117              .catch((e: any) => {
118                if (e instanceof Error) {
119                  terminal.write(`\x1b[31;1mUncaught (in promise) Error: ${e.message}\x1b[0m\r\n`)
120                }
121                else {
122                  terminal.write(`\x1b[31;1mUncaught (in promise) ${e}\x1b[0m\r\n`)
123                }
124              });
125          }
126          else {
127            terminal.write(cmdOutput + "\r\n");
128          }
129        }
130        catch (e) {
131          if (e instanceof Error) terminal.write(`\x1b[31;1m${e.message}\x1b[0m\r\n`)
132        }
133
134        currentCommandHistoryIndex = -1;
135        historyStorage.unshift(view.state.doc.toString());
136
137        // Clear text editor
138        const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}});
139        view.dispatch(transaction);
140        return true;
141      }
142    };
143
144    const upKeyMap = {
145      key: "ArrowUp",
146      run(view: EditorView) {
147        currentCommandHistoryIndex++;
148        if (historyStorage.data[currentCommandHistoryIndex]) {
149          // set text editor
150          const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}});
151          view.dispatch(transaction);
152        }
153        else {
154          currentCommandHistoryIndex = historyStorage.data.length - 1;
155        }
156        return true;
157      }
158    };
159
160    const downKeyMap = {
161      key: "ArrowDown",
162      run(view: EditorView) {
163        currentCommandHistoryIndex--;
164        if (currentCommandHistoryIndex <= -1) {
165          currentCommandHistoryIndex = -1;
166          const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}});
167          view.dispatch(transaction);
168        }
169        else if (historyStorage.data[currentCommandHistoryIndex]) {
170          // set text editor
171          const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}});
172          view.dispatch(transaction);
173        }
174        return true;
175      }
176    };
177
178    const keymaps = keymap.of([enterKeyMap, upKeyMap, downKeyMap]);
179    let view = createEditor(document.querySelector('#repl-editor-container')!, keymaps);
180    return () => view.destroy();
181  }, [terminal]);
182
183  return (
184    <div className={styles.container}>
185      <div id="repl-log-container" className={styles.logs}></div>
186      <div className={styles.replWithCaret}>
187        <div>
188          <div className={styles.tooltip}>?
189            <span className={styles.tooltiptext}>
190              This REPL runs JavaScript.
191              You can navigate previous commands using <span>Up</span> and <span>Down</span> arrow keys.
192              <br /><br />
193              Call device RPCs using <span>device.rpcs.*</span> API.
194            </span>
195          </div>
196        <span className={styles.caret}>{`> `}</span>
197        </div>
198        <div id="repl-editor-container" className={styles.editor}></div>
199      </div>
200    </div>
201  )
202}
203