import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SettingsManager } from '../util/SettingsManager'; import { undoable } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; interface ReplProps { scrollToBottom: () => void; value: { [key: string]: any }; name?: string; } export class ScriptingObjectDisplay extends ObservableReactComponent { @observable collapsed = true; constructor(props: any) { super(props); makeObservable(this); } @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 ? ( <> {this._props.name} : {name} ) : ( name ); if (this.collapsed) { return (
{title} (+{Object.keys(val).length})
); } else { return (
{title}
{Object.keys(val).map(key => ( ))}
); } } } interface replValueProps { scrollToBottom: () => void; value: any; name?: string; } @observer export class ScriptingValueDisplay extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } render() { const val = this._props.name ? this._props.value[this._props.name] : this._props.value; if (typeof val === 'object') { return ; } else if (typeof val === 'function') { const name = '[Function]'; const title = this._props.name ? ( <> {this._props.name} : {name} ) : ( name ); return
{title}
; } else { const name = String(val); const title = this._props.name ? ( <> {this._props.name} : {name} ) : ( name ); return
{title}
; } } } @observer export class ScriptingRepl extends ObservableReactComponent<{}> { constructor(props: any) { super(props); makeObservable(this); } @observable private commands: { command: string; result: any }[] = []; private commandsHistory: string[] = []; @observable private commandString: string = ''; private commandBuffer: string = ''; @observable private historyIndex: number = -1; private commandsRef = React.createRef(); private args: any = {}; getTransformer = (): Transformer => { return { transformer: context => { const knownVars: { [name: string]: number } = {}; const usedDocuments: number[] = []; ScriptingGlobals.getGlobals().forEach((global: any) => (knownVars[global] = 1)); return root => { function visit(node: ts.Node) { let skip = false; if (ts.isIdentifier(node)) { if (ts.isParameter(node.parent)) { skip = true; knownVars[node.text] = 1; } } 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 (ts.isParameter(node.parent)) { // delete knownVars[node.text]; } else if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { const match = node.text.match(/d([0-9]+)/); if (match) { const m = parseInt(match[1]); usedDocuments.push(m); } else { return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('args'), node); // ts.createPropertyAccess(ts.createIdentifier('args'), node); } } } return node; } return ts.visitNode(root, visit); }; }, }; }; @action onKeyDown = (e: React.KeyboardEvent) => { let stopProp = true; switch (e.key) { case 'Enter': { e.stopPropagation(); const docGlobals: { [name: string]: any } = {}; DocumentManager.Instance.DocumentViews.forEach((dv, i) => (docGlobals[`d${i}`] = dv.Document)); const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals); const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals }); if (!script.compiled) { this.commands.push({ command: this.commandString, result: script.errors }); return; } const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); this.maybeScrollToBottom(); this.commandString = ''; this.commandBuffer = ''; this.historyIndex = -1; } break; } case 'ArrowUp': { if (this.historyIndex < this.commands.length - 1) { this.historyIndex++; if (this.historyIndex === 0) { this.commandBuffer = this.commandString; } this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex]; } break; } case 'ArrowDown': { if (this.historyIndex >= 0) { this.historyIndex--; if (this.historyIndex === -1) { this.commandString = this.commandBuffer; this.commandBuffer = ''; } else { this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex]; } } break; } default: stopProp = false; break; } if (stopProp) { e.stopPropagation(); e.preventDefault(); } }; @action onChange = (e: React.ChangeEvent) => { 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 = () => { this.overlayDisposer?.(); this.overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); }; onBlur = () => this.overlayDisposer?.(); render() { return (
{this.commands.map(({ command, result }, i) => { return (
{command ||
}
{}
); })}
); } }