diff --git a/client/constants.js b/client/constants.js index 72f8948c..ef4f9041 100644 --- a/client/constants.js +++ b/client/constants.js @@ -84,6 +84,12 @@ export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_THEME = 'SET_THEME'; export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; +export const SET_AUTOREFRESH = 'SET_AUTOREFRESH'; +export const START_SKETCH_REFRESH = 'START_SKETCH_REFRESH'; +export const END_SKETCH_REFRESH = 'END_SKETCH_REFRESH'; + +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..19cb940a 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -1,11 +1,5 @@ import * as ActionTypes from '../../../constants'; -export function toggleSketch() { - return { - type: ActionTypes.TOGGLE_SKETCH - }; -} - export function startSketch() { return { type: ActionTypes.START_SKETCH @@ -18,6 +12,25 @@ export function stopSketch() { }; } +export function startRefreshSketch() { + return { + type: ActionTypes.START_SKETCH_REFRESH + }; +} + +export function startSketchAndRefresh() { + return (dispatch) => { + dispatch(startSketch()); + dispatch(startRefreshSketch()); + }; +} + +export function endSketchRefresh() { + return { + type: ActionTypes.END_SKETCH_REFRESH + }; +} + export function startTextOutput() { return { type: ActionTypes.START_TEXT_OUTPUT @@ -170,3 +183,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/actions/preferences.js b/client/modules/IDE/actions/preferences.js index 6b3964ef..5d8bc4b1 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -138,9 +138,46 @@ export function setTextOutput(value) { } export function setTheme(value) { - return { - type: ActionTypes.SET_THEME, - value + // return { + // type: ActionTypes.SET_THEME, + // value + // }; + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_THEME, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + theme: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + +export function setAutorefresh(value) { + // return { + // type: ActionTypes.SET_AUTOREFRESH, + // value + // }; + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_AUTOREFRESH, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + autorefresh: value + } + }; + updatePreferences(formParams, dispatch); + } }; } diff --git a/client/modules/IDE/components/About.js b/client/modules/IDE/components/About.js index 0959ffe5..534c6642 100644 --- a/client/modules/IDE/components/About.js +++ b/client/modules/IDE/components/About.js @@ -4,13 +4,17 @@ const exitUrl = require('../../../images/exit.svg'); import { browserHistory } from 'react-router'; class About extends React.Component { + componentDidMount() { + this.refs.about.focus(); + } + closeAboutModal() { browserHistory.goBack(); } render() { return ( -
+

About

- + ); } } diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index ae8c415b..f62cb4a4 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-${this.props.theme}`, 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._cm.on('change', debounce(1000, () => { this.props.setUnsavedChanges(true); this.props.updateFileContent(this.props.file.name, this._cm.getValue()); + this.checkForInfiniteLoop((infiniteLoop, prevs) => { + if (!infiniteLoop && prevs && this.props.autorefresh) { + this.props.startRefreshSketch(); + } + }); })); this._cm.on('keyup', () => { @@ -132,6 +141,77 @@ class Editor extends React.Component { } } + checkForInfiniteLoop(callback) { + const prevIsplaying = this.props.isPlaying; + let infiniteLoop = false; + let prevLine; + this.props.resetInfiniteLoops(); + let iframe; + + for (let i = 0; i < this.widgets.length; ++i) { + this._cm.removeLineWidget(this.widgets[i]); + } + this.widgets.length = 0; + + loopProtect.alias = 'protect'; + + let foundInfiniteLoop = false; + loopProtect.hit = (line) => { + foundInfiniteLoop = true; + if (line !== prevLine) { + this.props.detectInfiniteLoops(); + this.props.stopSketch(); + infiniteLoop = true; + callback(infiniteLoop, prevIsplaying); + const msg = document.createElement('div'); + const loopError = `line ${line}: This loop is taking too long to run. This might be an infinite loop.`; + msg.appendChild(document.createTextNode(loopError)); + msg.className = 'lint-error'; + this.widgets.push(this._cm.addLineWidget(line - 1, msg, { coverGutter: false, noHScroll: true })); + prevLine = line; + } + }; + + const processed = loopProtect(this.props.file.content); + + let iframeForLoop = document.getElementById('iframeForLoop'); + if (iframeForLoop === null) { + iframe = document.createElement('iframe'); + iframe.id = 'iframeForLoop'; + iframe.style.display = 'none'; + document.body.appendChild(iframe); + iframeForLoop = iframe; + } else { + iframeForLoop.srcdoc = ''; + } + const win = iframeForLoop.contentWindow; + const doc = win.document; + doc.open(); + + win.protect = loopProtect; + + doc.write(` + + + + + + + + + `); + win.onerror = () => true; + doc.close(); + + setTimeout(() => { + if (!foundInfiniteLoop) { + callback(infiniteLoop, prevIsplaying, prevLine); + } + }, 200); + } + _cm: CodeMirror.Editor render() { @@ -148,6 +228,7 @@ class Editor extends React.Component { > -