From aa8a6f7a786410fe2f60cced3325eaee510adb09 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 19 Jul 2019 19:23:46 -0400
Subject: Added start of REPL

---
 src/client/views/OverlayView.tsx | 71 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 63 insertions(+), 8 deletions(-)

(limited to 'src/client/views/OverlayView.tsx')

diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index f8fc94274..6b72abebf 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,6 +3,8 @@ import { observer } from "mobx-react";
 import { observable, action } from "mobx";
 import { Utils } from "../../Utils";
 
+import './OverlayView.scss';
+
 export type OverlayDisposer = () => void;
 
 export type OverlayElementOptions = {
@@ -10,13 +12,67 @@ export type OverlayElementOptions = {
     y: number;
     width?: number;
     height?: number;
+    title?: string;
 };
 
+export interface OverlayWindowProps {
+    children: JSX.Element;
+    overlayOptions: OverlayElementOptions;
+    onClick: () => void;
+}
+
+@observer
+export class OverlayWindow extends React.Component<OverlayWindowProps> {
+    @observable x: number;
+    @observable y: number;
+    @observable width?: number;
+    @observable height?: number;
+    constructor(props: OverlayWindowProps) {
+        super(props);
+
+        const opts = props.overlayOptions;
+        this.x = opts.x;
+        this.y = opts.y;
+        this.width = opts.width;
+        this.height = opts.height;
+    }
+
+    onPointerDown = (_: React.PointerEvent) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.addEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointerup", this.onPointerUp);
+    }
+
+    @action
+    onPointerMove = (e: PointerEvent) => {
+        this.x += e.movementX;
+        this.y += e.movementY;
+    }
+
+    onPointerUp = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+    }
+
+    render() {
+        return (
+            <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+                <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
+                    {this.props.overlayOptions.title || "Untitled"}
+                    <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+                </div>
+                {this.props.children}
+            </div>
+        );
+    }
+}
+
 @observer
 export class OverlayView extends React.Component {
     public static Instance: OverlayView;
     @observable.shallow
-    private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = [];
+    private _elements: JSX.Element[] = [];
 
     constructor(props: any) {
         super(props);
@@ -27,20 +83,19 @@ export class OverlayView extends React.Component {
 
     @action
     addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
-        const eleWithPosition = { ele, options, id: Utils.GenerateGuid() };
-        this._elements.push(eleWithPosition);
-        return action(() => {
-            const index = this._elements.indexOf(eleWithPosition);
+        const remove = action(() => {
+            const index = this._elements.indexOf(ele);
             if (index !== -1) this._elements.splice(index, 1);
         });
+        ele = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{ele}</OverlayWindow>;
+        this._elements.push(ele);
+        return remove;
     }
 
     render() {
         return (
             <div>
-                {this._elements.map(({ ele, options: { x, y, width, height }, id }) => (
-                    <div key={id} style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div>
-                ))}
+                {this._elements}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From fe188e93f2f8d61356bfdc3216273f72ae074e10 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tschicke@gmail.com>
Date: Mon, 22 Jul 2019 00:10:01 -0400
Subject: Added a bunch of REPL functionality

---
 src/client/util/Scripting.ts                       |  69 ++++++-
 src/client/views/OverlayView.scss                  |  25 ++-
 src/client/views/OverlayView.tsx                   |  52 ++++-
 src/client/views/ScriptingRepl.scss                |  39 ++++
 src/client/views/ScriptingRepl.tsx                 | 228 +++++++++++++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 7 files changed, 387 insertions(+), 30 deletions(-)

(limited to 'src/client/views/OverlayView.tsx')

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 62c2cfe85..46dc320b0 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,5 +1,7 @@
-// import * as ts from "typescript"
-let ts = (window as any).ts;
+import * as ts from "typescript";
+export { ts };
+// export const ts = (window as any).ts;
+
 // // @ts-ignore
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // // @ts-ignore
@@ -55,13 +57,35 @@ export namespace Scripting {
         }
         scriptingGlobals[n] = obj;
     }
+
+    export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) {
+        return { ..._scriptingGlobals, ...(globals || {}) };
+    }
+
+    export function setScriptingGlobals(globals: { [key: string]: any }) {
+        scriptingGlobals = globals;
+    }
+
+    export function resetScriptingGlobals() {
+        scriptingGlobals = _scriptingGlobals;
+    }
+
+    // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]);
+    export function printNodeType(node: any, indentation = "") {
+        console.log(indentation + ts.SyntaxKind[node.kind]);
+    }
+
+    export function getGlobals() {
+        return Object.keys(scriptingGlobals);
+    }
 }
 
 export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
     Scripting.addGlobal(constructor);
 }
 
-const scriptingGlobals: { [name: string]: any } = {};
+const _scriptingGlobals: { [name: string]: any } = {};
+let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
 
 function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
     const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
@@ -162,6 +186,8 @@ class ScriptingCompilerHost {
     }
 }
 
+export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
+export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser };
 export interface ScriptOptions {
     requiredType?: string;
     addReturn?: boolean;
@@ -169,10 +195,23 @@ export interface ScriptOptions {
     capturedVariables?: { [name: string]: Field };
     typecheck?: boolean;
     editable?: boolean;
+    traverser?: TraverserParam;
+    transformer?: ts.TransformerFactory<ts.SourceFile>;
+    globals?: { [name: string]: any };
+}
+
+// function forEachNode(node:ts.Node, fn:(node:any) => void);
+function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") {
+    return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => {
+        forEachNode(n, onEnter, onExit, indentation + "    ");
+    }) || (onExit && onExit(node, indentation));
 }
 
 export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
     const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+    if (options.globals) {
+        Scripting.setScriptingGlobals(options.globals);
+    }
     let host = new ScriptingCompilerHost;
     let paramNames: string[] = [];
     if ("this" in params || "this" in capturedVariables) {
@@ -192,10 +231,27 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         paramList.push(`${key}: ${capturedVariables[key].constructor.name}`);
     }
     let paramString = paramList.join(", ");
+    if (options.traverser) {
+        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+        const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
+        const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined;
+        forEachNode(sourceFile, onEnter, onLeave);
+    }
+    if (options.transformer) {
+        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+        const result = ts.transform(sourceFile, [options.transformer]);
+        const transformed = result.transformed;
+        const printer = ts.createPrinter({
+            newLine: ts.NewLineKind.LineFeed
+        });
+        script = printer.printFile(transformed[0]);
+        result.dispose();
+    }
     let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
         ${addReturn ? `return ${script};` : script}
     })`;
     host.writeFile("file.ts", funcScript);
+
     if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
     let program = ts.createProgram(["file.ts"], {}, host);
     let testResult = program.emit();
@@ -203,7 +259,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
 
     let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
 
-    return Run(outputText, paramNames, diagnostics, script, options);
+    const result = Run(outputText, paramNames, diagnostics, script, options);
+
+    if (options.globals) {
+        Scripting.resetScriptingGlobals();
+    }
+    return result;
 }
 
 Scripting.addGlobal(CompileScript);
\ No newline at end of file
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 9d0abc96d..4d1e8cf0b 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,21 +1,42 @@
 .overlayWindow-outerDiv {
-    position: absolute;
     border-radius: 5px;
     overflow: hidden;
     display: flex;
     flex-direction: column;
 }
 
+.overlayWindow-outerDiv,
+.overlayView-wrapperDiv {
+    position: absolute;
+    z-index: 1;
+}
+
 .overlayWindow-titleBar {
-    height: 30px;
+    flex: 0 1 30px;
     background: darkslategray;
     color: whitesmoke;
     text-align: center;
     cursor: move;
 }
 
+.overlayWindow-content {
+    flex: 1 1 auto;
+    display: flex;
+    flex-direction: column;
+}
+
 .overlayWindow-closeButton {
     float: right;
     height: 30px;
     width: 30px;
+}
+
+.overlayWindow-resizeDragger {
+    background-color: red;
+    position: absolute;
+    right: 0px;
+    bottom: 0px;
+    width: 10px;
+    height: 10px;
+    cursor: nwse-resize;
 }
\ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 6b72abebf..2f2579057 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -25,16 +25,16 @@ export interface OverlayWindowProps {
 export class OverlayWindow extends React.Component<OverlayWindowProps> {
     @observable x: number;
     @observable y: number;
-    @observable width?: number;
-    @observable height?: number;
+    @observable width: number;
+    @observable height: number;
     constructor(props: OverlayWindowProps) {
         super(props);
 
         const opts = props.overlayOptions;
         this.x = opts.x;
         this.y = opts.y;
-        this.width = opts.width;
-        this.height = opts.height;
+        this.width = opts.width || 200;
+        this.height = opts.height || 200;
     }
 
     onPointerDown = (_: React.PointerEvent) => {
@@ -44,10 +44,27 @@ export class OverlayWindow extends React.Component<OverlayWindowProps> {
         document.addEventListener("pointerup", this.onPointerUp);
     }
 
+    onResizerPointerDown = (_: React.PointerEvent) => {
+        document.removeEventListener("pointermove", this.onResizerPointerMove);
+        document.removeEventListener("pointerup", this.onResizerPointerUp);
+        document.addEventListener("pointermove", this.onResizerPointerMove);
+        document.addEventListener("pointerup", this.onResizerPointerUp);
+    }
+
     @action
     onPointerMove = (e: PointerEvent) => {
         this.x += e.movementX;
+        this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
         this.y += e.movementY;
+        this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+    }
+
+    @action
+    onResizerPointerMove = (e: PointerEvent) => {
+        this.width += e.movementX;
+        this.width = Math.max(this.width, 30);
+        this.height += e.movementY;
+        this.height = Math.max(this.height, 30);
     }
 
     onPointerUp = (e: PointerEvent) => {
@@ -55,6 +72,11 @@ export class OverlayWindow extends React.Component<OverlayWindowProps> {
         document.removeEventListener("pointerup", this.onPointerUp);
     }
 
+    onResizerPointerUp = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.onResizerPointerMove);
+        document.removeEventListener("pointerup", this.onResizerPointerUp);
+    }
+
     render() {
         return (
             <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
@@ -62,7 +84,10 @@ export class OverlayWindow extends React.Component<OverlayWindowProps> {
                     {this.props.overlayOptions.title || "Untitled"}
                     <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
                 </div>
-                {this.props.children}
+                <div className="overlayWindow-content">
+                    {this.props.children}
+                </div>
+                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
             </div>
         );
     }
@@ -87,11 +112,26 @@ export class OverlayView extends React.Component {
             const index = this._elements.indexOf(ele);
             if (index !== -1) this._elements.splice(index, 1);
         });
-        ele = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{ele}</OverlayWindow>;
+        ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+            transform: `translate(${options.x}px, ${options.y}px)`,
+            width: options.width,
+            height: options.height
+        }}>{ele}</div>;
         this._elements.push(ele);
         return remove;
     }
 
+    @action
+    addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+        const remove = action(() => {
+            const index = this._elements.indexOf(contents);
+            if (index !== -1) this._elements.splice(index, 1);
+        });
+        contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+        this._elements.push(contents);
+        return remove;
+    }
+
     render() {
         return (
             <div>
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
index 1eedb52fa..f1ef64193 100644
--- a/src/client/views/ScriptingRepl.scss
+++ b/src/client/views/ScriptingRepl.scss
@@ -1,5 +1,8 @@
 .scriptingRepl-outerContainer {
     background-color: whitesmoke;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
 }
 
 .scriptingRepl-resultContainer {
@@ -8,4 +11,40 @@
 
 .scriptingRepl-commandInput {
     width: 100%;
+}
+
+.scriptingRepl-commandResult,
+.scriptingRepl-commandString {
+    overflow-wrap: break-word;
+}
+
+.scriptingRepl-commandsContainer {
+    flex: 1 1 auto;
+    overflow-y: scroll;
+}
+
+.documentIcon-outerDiv {
+    background-color: white;
+    border-width: 1px;
+    border-style: solid;
+    border-radius: 25%;
+    padding: 2px;
+}
+
+.scriptingObject-icon {
+    padding: 3px;
+    cursor: pointer;
+}
+
+.scriptingObject-iconCollapsed {
+    padding-left: 4px;
+    padding-right: 5px;
+}
+
+.scriptingObject-fields {
+    padding-left: 10px;
+}
+
+.scriptingObject-leaf {
+    margin-left: 15px;
 }
\ No newline at end of file
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index bd6fc9dfb..6eabc7b70 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -2,32 +2,193 @@ import * as React from 'react';
 import { observer } from 'mobx-react';
 import { observable, action } from 'mobx';
 import './ScriptingRepl.scss';
-import { Scripting, CompileScript } from '../util/Scripting';
+import { Scripting, CompileScript, ts } from '../util/Scripting';
+import { DocumentManager } from '../util/DocumentManager';
+import { DocumentView } from './nodes/DocumentView';
+import { OverlayView } from './OverlayView';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
+
+library.add(faCaretDown);
+library.add(faCaretRight);
+
+@observer
+export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
+    render() {
+        this.props.view.props.ScreenToLocalTransform();
+        this.props.view.props.Document.width;
+        this.props.view.props.Document.height;
+        const screenCoords = this.props.view.screenRect();
+
+        return (
+            <div className="documentIcon-outerDiv" style={{
+                position: "absolute",
+                transform: `translate(${screenCoords.left + screenCoords.width / 2}px, ${screenCoords.top}px)`,
+            }}>
+                <p >${this.props.index}</p>
+            </div>
+        );
+    }
+}
+
+@observer
+export class DocumentIconContainer extends React.Component {
+    render() {
+        return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+    }
+}
+
+@observer
+export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> {
+    @observable collapsed = true;
+
+    @action
+    toggle = () => {
+        this.collapsed = !this.collapsed;
+        this.props.scrollToBottom();
+    }
+
+    render() {
+        const val = this.props.value;
+        const proto = Object.getPrototypeOf(val);
+        const name = (proto && proto.constructor && proto.constructor.name) || String(val);
+        const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+        if (this.collapsed) {
+            return (
+                <div className="scriptingObject-collapsed">
+                    <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length})
+                </div>
+            );
+        } else {
+            return (
+                <div className="scriptingObject-open">
+                    <div>
+                        <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title}
+                    </div>
+                    <div className="scriptingObject-fields">
+                        {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)}
+                    </div>
+                </div>
+            );
+        }
+    }
+}
+
+@observer
+export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> {
+    render() {
+        const val = this.props.name ? this.props.value[this.props.name] : this.props.value;
+        if (typeof val === "object") {
+            return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />;
+        } else if (typeof val === "function") {
+            const name = "[Function]";
+            const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+            return <div className="scriptingObject-leaf">{title}</div>;
+        } else {
+            const name = String(val);
+            const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+            return <div className="scriptingObject-leaf">{title}</div>;
+        }
+    }
+}
 
 @observer
 export class ScriptingRepl extends React.Component {
     @observable private commands: { command: string, result: any }[] = [];
 
     @observable private commandString: string = "";
+    private commandBuffer: string = "";
+
+    @observable private historyIndex: number = -1;
+
+    private commandsRef = React.createRef<HTMLDivElement>();
 
     private args: any = {};
 
+    getTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
+        const knownVars: { [name: string]: number } = {};
+        const usedDocuments: number[] = [];
+        Scripting.getGlobals().forEach(global => knownVars[global] = 1);
+        return root => {
+            function visit(node: ts.Node) {
+                node = ts.visitEachChild(node, visit, context);
+
+                if (ts.isIdentifier(node)) {
+                    const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+                    const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+                    if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) {
+                        const match = node.text.match(/\$([0-9]+)/);
+                        if (match) {
+                            const m = parseInt(match[1]);
+                            usedDocuments.push(m);
+                        } else {
+                            return ts.createPropertyAccess(ts.createIdentifier("args"), node);
+                        }
+                    }
+                }
+
+                return node;
+            }
+            return ts.visitNode(root, visit);
+        };
+    }
+
     @action
     onKeyDown = (e: React.KeyboardEvent) => {
-        if (e.key === "Enter") {
-            e.stopPropagation();
+        let stopProp = true;
+        switch (e.key) {
+            case "Enter": {
+                const docGlobals: { [name: string]: any } = {};
+                DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`$${i}`] = dv.props.Document);
+                const globals = Scripting.makeMutableGlobalsCopy(docGlobals);
+                const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer, globals });
+                if (!script.compiled) {
+                    return;
+                }
+                const result = script.run({ args: this.args });
+                if (!result.success) {
+                    return;
+                }
+                this.commands.push({ command: this.commandString, result: result.result });
+
+                this.maybeScrollToBottom();
 
-            const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" } });
-            if (!script.compiled) {
-                return;
+                this.commandString = "";
+                this.commandBuffer = "";
+                this.historyIndex = -1;
+                break;
             }
-            const result = script.run({ args: this.args });
-            if (!result.success) {
-                return;
+            case "ArrowUp": {
+                if (this.historyIndex < this.commands.length - 1) {
+                    this.historyIndex++;
+                    if (this.historyIndex === 0) {
+                        this.commandBuffer = this.commandString;
+                    }
+                    this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command;
+                }
+                break;
             }
-            this.commands.push({ command: this.commandString, result: result.result });
+            case "ArrowDown": {
+                if (this.historyIndex >= 0) {
+                    this.historyIndex--;
+                    if (this.historyIndex === -1) {
+                        this.commandString = this.commandBuffer;
+                        this.commandBuffer = "";
+                    } else {
+                        this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command;
+                    }
+                }
+                break;
+            }
+            default:
+                stopProp = false;
+                break;
+        }
 
-            this.commandString = "";
+        if (stopProp) {
+            e.stopPropagation();
+            e.preventDefault();
         }
     }
 
@@ -36,21 +197,56 @@ export class ScriptingRepl extends React.Component {
         this.commandString = e.target.value;
     }
 
+    private shouldScroll: boolean = false;
+    private maybeScrollToBottom = () => {
+        const ele = this.commandsRef.current;
+        if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) {
+            this.shouldScroll = true;
+            this.forceUpdate();
+        }
+    }
+
+    private scrollToBottom() {
+        const ele = this.commandsRef.current;
+        ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight });
+    }
+
+    componentDidUpdate() {
+        if (this.shouldScroll) {
+            this.shouldScroll = false;
+            this.scrollToBottom();
+        }
+    }
+
+    overlayDisposer?: () => void;
+    onFocus = () => {
+        if (this.overlayDisposer) {
+            this.overlayDisposer();
+        }
+        this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+    }
+
+    onBlur = () => {
+        this.overlayDisposer && this.overlayDisposer();
+    }
+
     render() {
         return (
             <div className="scriptingRepl-outerContainer">
-                <div className="scriptingRepl-commandsContainer">
-                    {this.commands.map(({ command, result }) => {
+                <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}>
+                    {this.commands.map(({ command, result }, i) => {
                         return (
-                            <div className="scriptingRepl-resultContainer">
-                                <div className="scriptingRepl-commandString">{command}</div>
-                                <div className="scriptingRepl-commandResult">{String(result)}</div>
+                            <div className="scriptingRepl-resultContainer" key={i}>
+                                <div className="scriptingRepl-commandString">{command || <br />}</div>
+                                <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div>
                             </div>
                         );
                     })}
                 </div>
                 <input
                     className="scriptingRepl-commandInput"
+                    onFocus={this.onFocus}
+                    onBlur={this.onBlur}
                     value={this.commandString}
                     onChange={this.onChange}
                     onKeyDown={this.onKeyDown}></input>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 58218e641..652aca8f3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -502,7 +502,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         overlayDisposer();
                         setTimeout(() => docs.map(d => d.transition = undefined), 1200);
                     }} />;
-                    overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options);
+                    overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
                 };
                 addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined);
                 addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 52ba643e0..7ff095573 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -557,7 +557,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
             }, icon: "search"
         });
-        cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addElement(<ScriptingRepl />, { x: 100, y: 100 }) });
+        cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200 }) });
         cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
         cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
         cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
-- 
cgit v1.2.3-70-g09d2

