/* eslint-disable react/no-array-index-key */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SnappingManager } from '../util/SnappingManager'; import { undoable } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { DocumentView } from './nodes/DocumentView'; import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { ObjectField } from '../../fields/ObjectField'; import { RefField } from '../../fields/RefField'; import { Doc, FieldResult, FieldType, Opt } from '../../fields/Doc'; interface replValueProps { scrollToBottom: () => void; value: Opt>; name?: string; } @observer export class ScriptingValueDisplay extends ObservableReactComponent { constructor(props: replValueProps) { super(props); makeObservable(this); } render() { const val = this._props.value instanceof Doc && this._props.name ? this._props.value[this._props.name] : this._props.value; const title = (name: string) => ( <> {this._props.name ? {this._props.name} : : <> } {name} ); if (typeof val === 'object') { // eslint-disable-next-line no-use-before-define return ; } if (typeof val === 'function') { return
{title('[Function]')}
; } return
{title(String(val))}
; } } interface ReplProps { scrollToBottom: () => void; value: Opt>; name?: string; } @observer export class ScriptingObjectDisplay extends ObservableReactComponent { @observable collapsed = true; constructor(props: ReplProps) { 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} : : null} {name} ); if (val === undefined) return '--undefined--'; if (val instanceof Promise) return '...Promise...'; if (this.collapsed) { return (
setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.toggle)} className="scriptingObject-icon scriptingObject-iconCollapsed"> {title} (+{Object.keys(val).length})
); } return (
{title}
{Object.keys(val).map(key => ( ))}
); } } @observer export class ScriptingRepl extends ObservableReactComponent { constructor(props: object) { super(props); makeObservable(this); } @observable private commands: { command: string; result: unknown }[] = []; private commandsHistory: string[] = []; @observable private commandString: string = ''; private commandBuffer: string = ''; @observable private historyIndex: number = -1; private commandsRef = React.createRef(); getTransformer = (): Transformer => ({ transformer: context => { const knownVars: { [name: string]: number } = {}; const usedDocuments: number[] = []; ScriptingGlobals.getGlobals().forEach((global: string) => { knownVars[global] = 1; }); return root => { function visit(nodeIn: ts.Node) { if (ts.isIdentifier(nodeIn)) { if (ts.isParameter(nodeIn.parent)) { knownVars[nodeIn.text] = 1; } } const node = ts.visitEachChild(nodeIn, 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]: FieldType } = {}; DocumentView.allViews().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 }); this.maybeScrollToBottom(); return; } const result = undoable(() => script.run({}, e => this.commands.push({ command: this.commandString, result: e as string })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); this.commandString = ''; this.commandBuffer = ''; this.historyIndex = -1; } this.maybeScrollToBottom(); 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 && Math.abs(Math.ceil(ele.scrollTop) - (ele.scrollHeight - ele.offsetHeight)) < 2) { this.shouldScroll = true; this.forceUpdate(); } }; private scrollToBottom() { const ele = this.commandsRef.current; ele?.scroll({ behavior: 'smooth', top: ele.scrollHeight }); } componentDidUpdate(prevProps: Readonly) { super.componentDidUpdate(prevProps); if (this.shouldScroll) { this.shouldScroll = false; setTimeout(() => this.scrollToBottom(), 0); } } 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) => (
{command ||
}
))}
); } }