diff --git a/client/constants.js b/client/constants.js index a4235717..b3772398 100644 --- a/client/constants.js +++ b/client/constants.js @@ -84,5 +84,8 @@ export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; +export const DETECT_INFINITE_LOOPS = 'DETECT_INFINITE_LOOPS'; +export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS'; + // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 05725fca..a2915232 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -170,3 +170,14 @@ export function setUnsavedChanges(value) { }; } +export function detectInfiniteLoops() { + return { + type: ActionTypes.DETECT_INFINITE_LOOPS + }; +} + +export function resetInfiniteLoops() { + return { + type: ActionTypes.RESET_INFINITE_LOOPS + }; +} diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index d035a104..6b814067 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -15,6 +15,7 @@ import 'codemirror/addon/lint/html-lint'; import 'codemirror/addon/comment/comment'; import 'codemirror/keymap/sublime'; import 'codemirror/addon/search/jump-to-line'; + import { JSHINT } from 'jshint'; window.JSHINT = JSHINT; import { CSSLint } from 'csslint'; @@ -27,15 +28,16 @@ const downArrowUrl = require('../../../images/down-arrow.svg'); import classNames from 'classnames'; import { debounce } from 'throttle-debounce'; +import loopProtect from 'loop-protect'; class Editor extends React.Component { constructor(props) { super(props); this.tidyCode = this.tidyCode.bind(this); } - componentDidMount() { this.beep = new Audio(beepUrl); + this.widgets = []; this._cm = CodeMirror(this.refs.container, { // eslint-disable-line theme: 'p5-widget', value: this.props.file.content, @@ -47,23 +49,30 @@ class Editor extends React.Component { gutters: ['CodeMirror-lint-markers'], keyMap: 'sublime', lint: { - onUpdateLinting: debounce(2000, (annotations) => { - this.props.clearLintMessage(); - annotations.forEach((x) => { - if (x.from.line > -1) { - this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message); + onUpdateLinting: () => { + debounce(2000, (annotations) => { + this.props.clearLintMessage(); + annotations.forEach((x) => { + if (x.from.line > -1) { + this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message); + } + }); + if (this.props.lintMessages.length > 0 && this.props.lintWarning) { + this.beep.play(); } }); - if (this.props.lintMessages.length > 0 && this.props.lintWarning) { - this.beep.play(); - } - }) + } } }); this._cm.on('change', debounce(200, () => { this.props.setUnsavedChanges(true); this.props.updateFileContent(this.props.file.name, this._cm.getValue()); + this.checkForInfiniteLoop(debounce(200, (infiniteLoop, prevs) => { + if (!infiniteLoop && prevs) { + this.props.startSketch(); + } + })); })); this._cm.on('keyup', () => { @@ -128,6 +137,65 @@ class Editor extends React.Component { } } + checkForInfiniteLoop(callback) { + const prevIsplaying = this.props.isPlaying; + let infiniteLoop = false; + this.props.stopSketch(); + this.props.resetInfiniteLoops(); + + for (let i = 0; i < this.widgets.length; ++i) { + this._cm.removeLineWidget(this.widgets[i]); + } + this.widgets.length = 0; + const OriginalIframe = document.getElementById('OriginalIframe'); + if (OriginalIframe !== null) { + document.body.removeChild(OriginalIframe); + } + + loopProtect.alias = 'protect'; + + loopProtect.hit = (line) => { + this.props.detectInfiniteLoops(); + infiniteLoop = true; + callback(infiniteLoop, prevIsplaying); + const msg = document.createElement('div'); + const loopError = `line ${line}: This loop is taking too long to run.`; + msg.appendChild(document.createTextNode(loopError)); + msg.className = 'lint-error'; + this.widgets.push(this._cm.addLineWidget(line - 1, msg, { coverGutter: false, noHScroll: true })); + }; + + const processed = loopProtect(this.props.file.content); + + const iframe = document.createElement('iframe'); + iframe.id = 'OriginalIframe'; + iframe.style.display = 'none'; + + document.body.appendChild(iframe); + + const win = iframe.contentWindow; + const doc = win.document; + doc.open(); + + win.protect = loopProtect; + + doc.write(` + + + + + + + + + + `); + doc.close(); + callback(infiniteLoop, prevIsplaying); + } + _cm: CodeMirror.Editor render() { @@ -191,7 +259,13 @@ Editor.propTypes = { showEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired, showKeyboardShortcutModal: PropTypes.func.isRequired, - setUnsavedChanges: PropTypes.func.isRequired + setUnsavedChanges: PropTypes.func.isRequired, + infiniteLoop: PropTypes.bool.isRequired, + detectInfiniteLoops: PropTypes.func.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired, + stopSketch: PropTypes.func.isRequired, + startSketch: PropTypes.func.isRequired, + isPlaying: PropTypes.bool.isRequired }; export default Editor; diff --git a/client/modules/IDE/components/PreviewFrame.js b/client/modules/IDE/components/PreviewFrame.js index 16545376..9fe1f6f1 100644 --- a/client/modules/IDE/components/PreviewFrame.js +++ b/client/modules/IDE/components/PreviewFrame.js @@ -205,7 +205,13 @@ class PreviewFrame extends React.Component { renderSketch() { const doc = ReactDOM.findDOMNode(this); - if (this.props.isPlaying) { + if (this.props.infiniteLoop) { + window.alert('There is an infinite loop in the code, please remove it before running the sketch.'); + this.props.resetInfiniteLoops(); + doc.srcdoc = ''; + srcDoc.set(doc, ' '); + } + if (this.props.isPlaying && !this.props.infiniteLoop) { srcDoc.set(doc, this.injectLocalFiles()); } else { doc.srcdoc = ''; @@ -250,7 +256,9 @@ PreviewFrame.propTypes = { cssFiles: PropTypes.array.isRequired, files: PropTypes.array.isRequired, dispatchConsoleEvent: PropTypes.func, - children: PropTypes.element + children: PropTypes.element, + infiniteLoop: PropTypes.bool.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired }; export default PreviewFrame; diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index a95249fd..430273e7 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -265,6 +265,12 @@ class IDEView extends React.Component { closeEditorOptions={this.props.closeEditorOptions} showKeyboardShortcutModal={this.props.showKeyboardShortcutModal} setUnsavedChanges={this.props.setUnsavedChanges} + infiniteLoop={this.props.ide.infiniteLoop} + detectInfiniteLoops={this.props.detectInfiniteLoops} + resetInfiniteLoops={this.props.resetInfiniteLoops} + stopSketch={this.props.stopSketch} + startSketch={this.props.startSketch} + isPlaying={this.props.ide.isPlaying} /> @@ -403,12 +411,15 @@ IDEView.propTypes = { shareModalVisible: PropTypes.bool.isRequired, editorOptionsVisible: PropTypes.bool.isRequired, keyboardShortcutVisible: PropTypes.bool.isRequired, - unsavedChanges: PropTypes.bool.isRequired + unsavedChanges: PropTypes.bool.isRequired, + infiniteLoop: PropTypes.bool.isRequired, }).isRequired, startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired, + detectInfiniteLoops: PropTypes.func.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired, project: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string.isRequired, diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 8d73f6a0..f8c16aa8 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -16,7 +16,8 @@ const initialState = { shareModalVisible: false, editorOptionsVisible: false, keyboardShortcutVisible: false, - unsavedChanges: false + unsavedChanges: false, + infiniteLoop: false }; const ide = (state = initialState, action) => { @@ -71,8 +72,15 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { keyboardShortcutVisible: true }); case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: return Object.assign({}, state, { keyboardShortcutVisible: false }); +<<<<<<< HEAD case ActionTypes.SET_UNSAVED_CHANGES: return Object.assign({}, state, { unsavedChanges: action.value }); +======= + case ActionTypes.DETECT_INFINITE_LOOPS: + return Object.assign({}, state, { infiniteLoop: true }); + case ActionTypes.RESET_INFINITE_LOOPS: + return Object.assign({}, state, { infiniteLoop: false }); +>>>>>>> 43052cb675fa0bc500467f5c7c6643f3636493ae default: return state; } diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index a3aa9588..0b40661e 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -80,4 +80,12 @@ .editor--options & { display: block; } -} \ No newline at end of file +} + +.lint-error { + font-family: Inconsolata, monospace; + font-size: 100%; + background: #FFBEC1; + color: red; + padding: 2px 5px 3px; +} diff --git a/package.json b/package.json index 56e95a68..a01147f5 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "jshint": "^2.9.2", "jszip": "^3.0.0", "jszip-utils": "0.0.2", + "loop-protect": "git+https://git@github.com/sagar-sm/loop-protect.git", "moment": "^2.14.1", "mongoose": "^4.4.16", "node-uuid": "^1.4.7",