Merge branch 'master' into production

This commit is contained in:
Phoenix Eliot 2016-08-31 14:09:02 -07:00
commit eb4eeb372d
12 changed files with 203 additions and 84 deletions

View file

@ -1,5 +1,11 @@
// TODO: don't serve this script from codecombat.com; serve it from a harmless extra domain we don't have yet. // TODO: don't serve this script from codecombat.com; serve it from a harmless extra domain we don't have yet.
var lastSource = null;
var lastOrigin = null;
window.onerror = function(message, url, line, column, error){
console.log("User script error on line " + line + ", column " + column + ": ", error);
lastSource.postMessage({ type: 'error', message: message, url: url, line: line, column: column }, lastOrigin);
}
window.addEventListener('message', receiveMessage, false); window.addEventListener('message', receiveMessage, false);
var concreteDom; var concreteDom;
@ -30,8 +36,9 @@ function receiveMessage(event) {
console.log('Ignoring message from bad origin:', origin); console.log('Ignoring message from bad origin:', origin);
return; return;
} }
lastOrigin = origin;
var data = event.data; var data = event.data;
var source = event.source; var source = lastSource = event.source;
switch (data.type) { switch (data.type) {
case 'create': case 'create':
create(_.pick(data, 'dom', 'styles', 'scripts')); create(_.pick(data, 'dom', 'styles', 'scripts'));
@ -79,12 +86,8 @@ function replaceNodes(selector, newNodes){
$newNodes.attr('for', firstNode.attr('for')); $newNodes.attr('for', firstNode.attr('for'));
newFirstNode = $newNodes[0]; newFirstNode = $newNodes[0];
try { firstNode.replaceWith(newFirstNode); // Removes newFirstNode from its array (!!)
firstNode.replaceWith(newFirstNode); // Removes newFirstNode from its array (!!)
} catch (e) {
console.log('Failed to update some nodes:', e);
}
$(newFirstNode).after($newNodes); $(newFirstNode).after($newNodes);
} }

View file

@ -13,6 +13,7 @@ channelSchemas =
'tome': require 'schemas/subscriptions/tome' 'tome': require 'schemas/subscriptions/tome'
'god': require 'schemas/subscriptions/god' 'god': require 'schemas/subscriptions/god'
'scripts': require 'schemas/subscriptions/scripts' 'scripts': require 'schemas/subscriptions/scripts'
'web-dev': require 'schemas/subscriptions/web-dev'
'world': require 'schemas/subscriptions/world' 'world': require 'schemas/subscriptions/world'
definitionSchemas = definitionSchemas =

View file

@ -89,7 +89,7 @@ module.exports =
'tome:problems-updated': c.object {title: 'Problems Updated', description: 'Published when problems have been updated', required: ['spell', 'problems', 'isCast']}, 'tome:problems-updated': c.object {title: 'Problems Updated', description: 'Published when problems have been updated', required: ['spell', 'problems', 'isCast']},
spell: {type: 'object'} spell: {type: 'object'}
problems: {type: 'array'} problems: {type: 'array'}
isCast: {type: 'boolean'} isCast: {type: 'boolean', description: 'Whether the code has been Run yet. Sometimes determines if error displays as just annotation or as full banner.'}
'tome:change-language': c.object {title: 'Tome Change Language', description: 'Published when the Tome should update its programming language', required: ['language']}, 'tome:change-language': c.object {title: 'Tome Change Language', description: 'Published when the Tome should update its programming language', required: ['language']},
language: {type: 'string'} language: {type: 'string'}

View file

@ -0,0 +1,9 @@
c = require 'schemas/schemas'
module.exports =
'web-dev:error': c.object {title: 'Web Dev Error', description: 'Published when an uncaught error occurs in the web-dev iFrame', required: []},
message: { type: 'string' }
url: { type: 'string', description: 'URL of the host iFrame' }
line: { type: 'integer', description: 'Line number of the start of the code that threw the exception (relative to its <script> tag!)' }
column: { type: 'integer', description: 'Column number of the start of the code that threw the exception' }
error: { type: 'string', description: 'The .toString of the originally thrown exception' }

View file

@ -188,10 +188,6 @@
.ace_gutter-cell.ace_error .ace_gutter-cell.ace_error
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMi8xNy8wOCCcqlgAAAQRdEVYdFhNTDpjb20uYWRvYmUueG1wADw/eHBhY2tldCBiZWdpbj0iICAgIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1jMDM0IDQ2LjI3Mjk3NiwgU2F0IEphbiAyNyAyMDA3IDIyOjExOjQxICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFkb2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVEYXRlPjIwMDgtMDItMTdUMDI6MzY6NDVaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpNb2RpZnlEYXRlPjIwMDgtMDMtMjRUMTk6MDA6NDJaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDUdUmQAAAD5SURBVDiNpZMxagMxEEWfgiCXcB3IbXwD7zbaM0nNyjdIl1O4Dk7pbsslEFbEZFKsJsiJrGDy4YM0M//zRyAoINAJyB8cS43RwwIdMFrvaeE8DADxXqQ3Jstn6GaQ5L3M0GQxsyaZoJtA3r2XCS6o+FkvZkdOIG/eywl+UVHrqcYm4BNIjb1rPdXYBTivj3gVtZ5q/p8gAfPhcLOBamzKcW41UI1dgA/qez4bU6muUE0zwVYEgKeKkWruEnTHENg4R8pFZblCyY1zHEMgQTQAe9gB8cE5XkO4GhugmIk76L+z+Wzy6FzT4CWLXf5MF8upSdMB4gC9Xr4AiezTJHGxdq0AAAAASUVORK5CYII=) background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMi8xNy8wOCCcqlgAAAQRdEVYdFhNTDpjb20uYWRvYmUueG1wADw/eHBhY2tldCBiZWdpbj0iICAgIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1jMDM0IDQ2LjI3Mjk3NiwgU2F0IEphbiAyNyAyMDA3IDIyOjExOjQxICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFkb2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVEYXRlPjIwMDgtMDItMTdUMDI6MzY6NDVaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpNb2RpZnlEYXRlPjIwMDgtMDMtMjRUMTk6MDA6NDJaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDUdUmQAAAD5SURBVDiNpZMxagMxEEWfgiCXcB3IbXwD7zbaM0nNyjdIl1O4Dk7pbsslEFbEZFKsJsiJrGDy4YM0M//zRyAoINAJyB8cS43RwwIdMFrvaeE8DADxXqQ3Jstn6GaQ5L3M0GQxsyaZoJtA3r2XCS6o+FkvZkdOIG/eywl+UVHrqcYm4BNIjb1rPdXYBTivj3gVtZ5q/p8gAfPhcLOBamzKcW41UI1dgA/qez4bU6muUE0zwVYEgKeKkWruEnTHENg4R8pFZblCyY1zHEMgQTQAe9gB8cE5XkO4GhugmIk76L+z+Wzy6FzT4CWLXf5MF8upSdMB4gC9Xr4AiezTJHGxdq0AAAAASUVORK5CYII=)
// NOTE! This hides all info annotations because removing specific annotations with listeners means they flicker. This hides that flicker. see: SpellView.onChangeAnnotation
.ace_gutter-cell.ace_info
background-image: none
.ace_marker-layer .ace_marker-layer
.ace_bracket .ace_bracket
// Override faint gray // Override faint gray

View file

@ -88,8 +88,11 @@ module.exports = class WebSurfaceView extends CocoView
switch event.data.type switch event.data.type
when 'goals-updated' when 'goals-updated'
Backbone.Mediator.publish 'god:new-html-goal-states', goalStates: event.data.goalStates, overallStatus: event.data.overallStatus Backbone.Mediator.publish 'god:new-html-goal-states', goalStates: event.data.goalStates, overallStatus: event.data.overallStatus
when 'error'
# NOTE: The line number in this is relative to the script tag, not the user code. The offset is added in SpellView.
Backbone.Mediator.publish 'web-dev:error', _.pick(event.data, ['message', 'line', 'column', 'url'])
else else
console.warn 'Unknown message type', event.data.type, 'for message', e, 'from origin', origin console.warn 'Unknown message type', event.data.type, 'for message', event, 'from origin', origin
destroy: -> destroy: ->
window.removeEventListener 'message', @onIframeMessage window.removeEventListener 'message', @onIframeMessage

View file

@ -1,43 +1,96 @@
Range = ace.require('ace/range').Range Range = ace.require('ace/range').Range
# This class can either wrap an AetherProblem,
# or act as a general runtime error container for web-dev iFrame errors.
# TODO: Use subclasses? Might need a factory pattern for that (bleh)
module.exports = class Problem module.exports = class Problem
annotation: null annotation: null
markerRange: null markerRange: null
constructor: (@aether, @aetherProblem, @ace, isCast=false, @levelID) -> # Construction with AetherProblem will include all but `error`
@buildAnnotation() # Construction with a standard error will have `error`, `isCast`, `levelID`, `ace`
@buildMarkerRange() if isCast constructor: ({ @aether, @aetherProblem, @ace, isCast=false, @levelID, error, userCodeHasChangedSinceLastCast }) ->
if @aetherProblem
@annotation = @buildAnnotationFromAetherProblem(@aetherProblem)
{ @lineMarkerRange, @textMarkerRange } = @buildMarkerRangesFromAetherProblem(@aetherProblem) if isCast
{ @level, @range, @message, @hint, @userInfo } = @aetherProblem
{ @row, @column: col } = @aetherProblem.range?[0]
@createdBy = 'aether'
else
unless userCodeHasChangedSinceLastCast
@annotation = @buildAnnotationFromWebDevError(error)
{ @lineMarkerRange, @textMarkerRange } = @buildMarkerRangesFromWebDevError(error)
@level = 'error'
@row = error.line
@column = error.column
@message = error.message or 'Unknown Error'
if error.line and not userCodeHasChangedSinceLastCast
@message = "Line #{error.line + 1}: " + @message # Ace's gutter numbers are 1-indexed but annotation.rows are 0-indexed
if userCodeHasChangedSinceLastCast
@hint = "This error was generated by old code — Try running your new code first."
else
@hint = undefined
@userInfo = undefined
@createdBy = 'web-dev-iframe'
# TODO: Include runtime/transpile error types depending on something?
# TODO: get ACE screen line, too, for positioning, since any multiline "lines" will mess up positioning # TODO: get ACE screen line, too, for positioning, since any multiline "lines" will mess up positioning
Backbone.Mediator.publish("problem:problem-created", line: @annotation.row, text: @annotation.text) if application.isIPadApp Backbone.Mediator.publish("problem:problem-created", line: @annotation.row, text: @annotation.text) if application.isIPadApp
isEqual: (problem) ->
_.all ['row', 'column', 'level', 'column', 'message', 'hint'], (attr) =>
@[attr] is problem[attr]
destroy: -> destroy: ->
@removeMarkerRanges() @removeMarkerRanges()
@userCodeProblem.off() if @userCodeProblem @userCodeProblem.off() if @userCodeProblem
buildAnnotation: -> buildAnnotationFromWebDevError: (error) ->
return unless @aetherProblem.range {
text = @aetherProblem.message.replace /^Line \d+: /, '' row: error.line
start = @aetherProblem.range[0] column: error.column
@annotation = raw: error.message
text: error.message
type: 'error'
createdBy: 'web-dev-iframe'
}
buildAnnotationFromAetherProblem: (aetherProblem) ->
return unless aetherProblem.range
text = aetherProblem.message.replace /^Line \d+: /, ''
start = aetherProblem.range[0]
{
row: start.row, row: start.row,
column: start.col, column: start.col,
raw: text, raw: text,
text: text, text: text,
type: @aetherProblem.level ? 'error' type: @aetherProblem.level ? 'error'
createdBy: 'aether' createdBy: 'aether'
}
buildMarkerRange: -> buildMarkerRangesFromWebDevError: (error) ->
return unless @aetherProblem.range lineMarkerRange = new Range error.line, 0, error.line, 1
[start, end] = @aetherProblem.range lineMarkerRange.start = @ace.getSession().getDocument().createAnchor lineMarkerRange.start
textClazz = "problem-marker-#{@aetherProblem.level}" lineMarkerRange.end = @ace.getSession().getDocument().createAnchor lineMarkerRange.end
@textMarkerRange = new Range start.row, start.col, end.row, end.col lineMarkerRange.id = @ace.getSession().addMarker lineMarkerRange, 'problem-line', 'fullLine'
@textMarkerRange.start = @ace.getSession().getDocument().createAnchor @textMarkerRange.start textMarkerRange = undefined # We don't get any per-character info from standard errors
@textMarkerRange.end = @ace.getSession().getDocument().createAnchor @textMarkerRange.end { lineMarkerRange, textMarkerRange }
@textMarkerRange.id = @ace.getSession().addMarker @textMarkerRange, textClazz, 'text'
buildMarkerRangesFromAetherProblem: (aetherProblem) ->
return {} unless aetherProblem.range
[start, end] = aetherProblem.range
textClazz = "problem-marker-#{aetherProblem.level}"
textMarkerRange = new Range start.row, start.col, end.row, end.col
textMarkerRange.start = @ace.getSession().getDocument().createAnchor textMarkerRange.start
textMarkerRange.end = @ace.getSession().getDocument().createAnchor textMarkerRange.end
textMarkerRange.id = @ace.getSession().addMarker textMarkerRange, textClazz, 'text'
lineClazz = "problem-line" lineClazz = "problem-line"
@lineMarkerRange = new Range start.row, start.col, end.row, end.col lineMarkerRange = new Range start.row, start.col, end.row, end.col
@lineMarkerRange.start = @ace.getSession().getDocument().createAnchor @lineMarkerRange.start lineMarkerRange.start = @ace.getSession().getDocument().createAnchor lineMarkerRange.start
@lineMarkerRange.end = @ace.getSession().getDocument().createAnchor @lineMarkerRange.end lineMarkerRange.end = @ace.getSession().getDocument().createAnchor lineMarkerRange.end
@lineMarkerRange.id = @ace.getSession().addMarker @lineMarkerRange, lineClazz, 'fullLine' lineMarkerRange.id = @ace.getSession().addMarker lineMarkerRange, lineClazz, 'fullLine'
{ lineMarkerRange, textMarkerRange }
removeMarkerRanges: -> removeMarkerRanges: ->
if @textMarkerRange if @textMarkerRange

View file

@ -20,10 +20,10 @@ module.exports = class ProblemAlertView extends CocoView
'click': -> Backbone.Mediator.publish 'tome:focus-editor', {} 'click': -> Backbone.Mediator.publish 'tome:focus-editor', {}
constructor: (options) -> constructor: (options) ->
@supermodel = options.supermodel # Has to go before super so events are hooked up
super options super options
@level = options.level @level = options.level
@session = options.session @session = options.session
@supermodel = options.supermodel
if options.problem? if options.problem?
@problem = options.problem @problem = options.problem
@onWindowResize() @onWindowResize()
@ -38,31 +38,31 @@ module.exports = class ProblemAlertView extends CocoView
afterRender: -> afterRender: ->
super() super()
if @problem? if @problem?
@$el.addClass('alert').addClass("alert-#{@problem.aetherProblem.level}").hide().fadeIn('slow') @$el.addClass('alert').addClass("alert-#{@problem.level}").hide().fadeIn('slow')
@$el.addClass('no-hint') unless @problem.aetherProblem.hint @$el.addClass('no-hint') unless @problem.hint
@playSound 'error_appear' @playSound 'error_appear'
setProblemMessage: -> setProblemMessage: ->
if @problem? if @problem?
format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s? format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s?
message = @problem.aetherProblem.message message = @problem.message
# Add time to problem message if hint is for a missing null check # Add time to problem message if hint is for a missing null check
# NOTE: This may need to be updated with Aether error hint changes # NOTE: This may need to be updated with Aether error hint changes
if @problem.aetherProblem.hint? and /(?:null|undefined)/.test @problem.aetherProblem.hint if @problem.hint? and /(?:null|undefined)/.test @problem.hint
age = @problem.aetherProblem.userInfo?.age age = @problem.userInfo?.age
if age? if age?
if /^Line \d+:/.test message if /^Line \d+:/.test message
message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}" message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}"
else else
message = "Time #{age.toFixed(1)}: #{message}" message = "Time #{age.toFixed(1)}: #{message}"
@message = format message @message = format message
@hint = format @problem.aetherProblem.hint @hint = format @problem.hint
onShowProblemAlert: (data) -> onShowProblemAlert: (data) ->
return unless $('#code-area').is(":visible") return unless $('#code-area').is(":visible")
if @problem? if @problem?
if @$el.hasClass "alert-#{@problem.aetherProblem.level}" if @$el.hasClass "alert-#{@problem.level}"
@$el.removeClass "alert-#{@problem.aetherProblem.level}" @$el.removeClass "alert-#{@problem.level}"
if @$el.hasClass "no-hint" if @$el.hasClass "no-hint"
@$el.removeClass "no-hint" @$el.removeClass "no-hint"
@problem = data.problem @problem = data.problem

View file

@ -145,6 +145,7 @@ module.exports = class Spell
@thang?.aether.transpile source @thang?.aether.transpile source
null null
# NOTE: By default, I think this compares the current source code with the source *last saved to the server* (not the last time it was run)
hasChanged: (newSource=null, currentSource=null) -> hasChanged: (newSource=null, currentSource=null) ->
(newSource ? @originalSource) isnt (currentSource ? @source) (newSource ? @originalSource) isnt (currentSource ? @source)

View file

@ -49,10 +49,12 @@ module.exports = class SpellView extends CocoView
'tome:insert-snippet': 'onInsertSnippet' 'tome:insert-snippet': 'onInsertSnippet'
'tome:spell-beautify': 'onSpellBeautify' 'tome:spell-beautify': 'onSpellBeautify'
'tome:maximize-toggled': 'onMaximizeToggled' 'tome:maximize-toggled': 'onMaximizeToggled'
'tome:problems-updated': 'onProblemsUpdated'
'script:state-changed': 'onScriptStateChange' 'script:state-changed': 'onScriptStateChange'
'playback:ended-changed': 'onPlaybackEndedChanged' 'playback:ended-changed': 'onPlaybackEndedChanged'
'level:contact-button-pressed': 'onContactButtonPressed' 'level:contact-button-pressed': 'onContactButtonPressed'
'level:show-victory': 'onShowVictory' 'level:show-victory': 'onShowVictory'
'web-dev:error': 'onWebDevError'
events: events:
'mouseout': 'onMouseOut' 'mouseout': 'onMouseOut'
@ -87,6 +89,14 @@ module.exports = class SpellView extends CocoView
@destroyAceEditor(@ace) @destroyAceEditor(@ace)
@ace = ace.edit @$el.find('.ace')[0] @ace = ace.edit @$el.find('.ace')[0]
@aceSession = @ace.getSession() @aceSession = @ace.getSession()
# Override setAnnotations so the Ace html worker doesn't clobber our annotations
@reallySetAnnotations = @aceSession.setAnnotations.bind(@aceSession)
@aceSession.setAnnotations = (annotations) =>
previousAnnotations = @aceSession.getAnnotations()
newAnnotations = _.filter previousAnnotations, (annotation) -> annotation.createdBy? # Keep the ones we generated
.concat _.reject annotations, (annotation) -> # Ignore this particular info-annotation the html worker generates
annotation.text is 'Start tag seen without seeing a doctype first. Expected e.g. <!DOCTYPE html>.'
@reallySetAnnotations newAnnotations
@aceDoc = @aceSession.getDocument() @aceDoc = @aceSession.getDocument()
@aceSession.setUseWorker @spell.language in @languagesThatUseWorkers @aceSession.setUseWorker @spell.language in @languagesThatUseWorkers
@aceSession.setMode utils.aceEditModes[@spell.language] @aceSession.setMode utils.aceEditModes[@spell.language]
@ -94,7 +104,6 @@ module.exports = class SpellView extends CocoView
@aceSession.setUseWrapMode true @aceSession.setUseWrapMode true
@aceSession.setNewLineMode 'unix' @aceSession.setNewLineMode 'unix'
@aceSession.setUseSoftTabs true @aceSession.setUseSoftTabs true
@aceSession.on 'changeAnnotation', @onChangeAnnotation
@ace.setTheme 'ace/theme/textmate' @ace.setTheme 'ace/theme/textmate'
@ace.setDisplayIndentGuides false @ace.setDisplayIndentGuides false
@ace.setShowPrintMargin false @ace.setShowPrintMargin false
@ -670,7 +679,10 @@ module.exports = class SpellView extends CocoView
cast = @$el.parent().length cast = @$el.parent().length
@recompile cast, e.realTime @recompile cast, e.realTime
@focus() if cast @focus() if cast
@updateHTML create: true if @options.level.isType('web-dev') if @options.level.isType('web-dev')
@sourceAtLastCast = @getSource()
@ace.setStyle 'spell-cast'
@updateHTML create: true
onCodeReload: (e) -> onCodeReload: (e) ->
return unless e.spell is @spell or not e.spell return unless e.spell is @spell or not e.spell
@ -736,6 +748,11 @@ module.exports = class SpellView extends CocoView
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment. onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
updateHTML: (options={}) => updateHTML: (options={}) =>
# TODO: Merge with onSpellChanged
# NOTE: Consider what goes in onManualCast only
if @spell.hasChanged(@spell.getSource(), @sourceAtLastCast)
@ace.unsetStyle 'spell-cast' # NOTE: Doesn't do anything for web-dev as of this writing, including for consistency
@clearWebDevErrors()
Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML(@getSource()), create: Boolean(options.create) Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML(@getSource()), create: Boolean(options.create)
# Design for a simpler system? # Design for a simpler system?
@ -770,6 +787,7 @@ module.exports = class SpellView extends CocoView
# Now that that's figured out, perform the update. # Now that that's figured out, perform the update.
# The web worker Aether won't track state, so don't have to worry about updating it # The web worker Aether won't track state, so don't have to worry about updating it
finishUpdatingAether = (aether) => finishUpdatingAether = (aether) =>
@clearAetherDisplay() # In case problems were added since last clearing
@displayAether aether, codeIsAsCast @displayAether aether, codeIsAsCast
@lastUpdatedAetherSpellThang = @spellThang @lastUpdatedAetherSpellThang = @spellThang
@guessWhetherFinished aether if fromCodeChange @guessWhetherFinished aether if fromCodeChange
@ -796,57 +814,92 @@ module.exports = class SpellView extends CocoView
else else
finishUpdatingAether(aether) finishUpdatingAether(aether)
# NOTE! Because this alone causes the doctype annotation to flicker, # Each problem-generating piece (aether, web-dev, ace html worker) clears its own problems/annotations
# all info annotations have been hidden with CSS in spell.sass
# If we ever want info annotations back, we need to remove that.
#
# This function itself removes the unwanted annotations on a later tick.
onChangeAnnotation: (event, session) ->
unfilteredAnnotations = session.getAnnotations()
filteredAnnotations = _.reject unfilteredAnnotations, (annotation) ->
annotation.text is 'Start tag seen without seeing a doctype first. Expected e.g. <!DOCTYPE html>.'
if filteredAnnotations.length < unfilteredAnnotations.length
session.setAnnotations(filteredAnnotations)
# Clear annotations and highlights generated by Aether, but not by the ACE worker
clearAetherDisplay: -> clearAetherDisplay: ->
problem.destroy() for problem in @problems @clearProblemsCreatedBy 'aether'
@problems = []
nonAetherAnnotations = _.reject @aceSession.getAnnotations(), (annotation) -> annotation.createdBy is 'aether'
@aceSession.setAnnotations nonAetherAnnotations
@highlightCurrentLine {} # This'll remove all highlights @highlightCurrentLine {} # This'll remove all highlights
clearWebDevErrors: ->
@clearProblemsCreatedBy 'web-dev-iframe'
clearProblemsCreatedBy: (createdBy) ->
nonAetherAnnotations = _.reject @aceSession.getAnnotations(), (annotation) -> annotation.createdBy is createdBy
@reallySetAnnotations nonAetherAnnotations
problemsToClear = _.filter @problems, (p) -> p.createdBy is createdBy
problemsToClear.forEach (problem) -> problem.destroy()
@problems = _.difference @problems, problemsToClear
Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: false
convertAetherProblems: (aether, aetherProblems, isCast) ->
# TODO: Functional-ify
_.unique(aetherProblems, (p) -> p.userInfo?.key).map (aetherProblem) =>
new Problem { aether, aetherProblem, @ace, isCast, levelID: @options.levelID }
displayAether: (aether, isCast=false) -> displayAether: (aether, isCast=false) ->
@displayedAether = aether @displayedAether = aether
isCast = isCast or not _.isEmpty(aether.metrics) or _.some aether.getAllProblems(), {type: 'runtime'} isCast = isCast or not _.isEmpty(aether.metrics) or _.some aether.getAllProblems(), {type: 'runtime'}
problem.destroy() for problem in @problems # Just in case another problem was added since clearAetherDisplay() ran.
@problems = []
annotations = @aceSession.getAnnotations() annotations = @aceSession.getAnnotations()
seenProblemKeys = {}
for aetherProblem, problemIndex in aether.getAllProblems() newProblems = @convertAetherProblems(aether, aether.getAllProblems(), isCast)
continue if key = aetherProblem.userInfo?.key and key of seenProblemKeys annotations.push problem.annotation for problem in newProblems when problem.annotation
seenProblemKeys[key] = true if key if isCast
@problems.push problem = new Problem aether, aetherProblem, @ace, isCast, @options.levelID @displayProblemBanner(newProblems[0]) if newProblems[0]
if isCast and problemIndex is 0 @saveUserCodeProblem(aether, problem.aetherProblem) for problem in newProblems
if problem.aetherProblem.range? @problems = @problems.concat(newProblems)
lineOffsetPx = 0
for i in [0...problem.aetherProblem.range[0].row]
lineOffsetPx += @aceSession.getRowLength(i) * @ace.renderer.lineHeight
lineOffsetPx -= @ace.session.getScrollTop()
Backbone.Mediator.publish 'tome:show-problem-alert', problem: problem, lineOffsetPx: Math.max lineOffsetPx, 0
@saveUserCodeProblem(aether, aetherProblem) if isCast
annotations.push problem.annotation if problem.annotation
@aceSession.setAnnotations annotations @aceSession.setAnnotations annotations
@highlightCurrentLine aether.flow unless _.isEmpty aether.flow @highlightCurrentLine aether.flow unless _.isEmpty aether.flow
#console.log ' and we could do the metrics', aether.metrics unless _.isEmpty aether.metrics #console.log ' and we could do the metrics', aether.metrics unless _.isEmpty aether.metrics
#console.log ' and we could do the style', aether.style unless _.isEmpty aether.style #console.log ' and we could do the style', aether.style unless _.isEmpty aether.style
#console.log ' and we could do the visualization', aether.visualization unless _.isEmpty aether.visualization #console.log ' and we could do the visualization', aether.visualization unless _.isEmpty aether.visualization
# Could use the user-code-problem style... or we could leave that to other places.
@ace[if @problems.length then 'setStyle' else 'unsetStyle'] 'user-code-problem'
@ace[if isCast then 'setStyle' else 'unsetStyle'] 'spell-cast'
Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: isCast Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: isCast
@ace.resize() @ace.resize()
# Tell ProblemAlertView to display this problem (only)
displayProblemBanner: (problem) ->
lineOffsetPx = 0
if problem.row?
for i in [0...problem.row]
lineOffsetPx += @aceSession.getRowLength(i) * @ace.renderer.lineHeight
lineOffsetPx -= @ace.session.getScrollTop()
Backbone.Mediator.publish 'tome:show-problem-alert', problem: problem, lineOffsetPx: Math.max lineOffsetPx, 0
# Gets the number of lines before the start of <script> content in the usercode
# Because Errors report their line number relative to the <script> tag
linesBeforeScript: (html) ->
# TODO: refactor, make it work with multiple scripts. What to do when error is in level-creator's code?
_.size(html.split('<script>')[0].match(/\n/g))
addAnnotation: (annotation) ->
annotations = @aceSession.getAnnotations()
annotations.push annotation
@reallySetAnnotations annotations
# Handle errors from the web-dev iframe asynchronously
onWebDevError: (error) ->
# TODO: Refactor this and the Aether problem flow to share as much as possible.
# TODO: Handle when the error is in our code, not theirs
# Compensate for line number being relative to <script> tag
offsetError = _.merge {}, error, { line: error.line + @linesBeforeScript(@getSource()) }
userCodeHasChangedSinceLastCast = @spell.hasChanged(@spell.getSource(), @sourceAtLastCast)
problem = new Problem({ error: offsetError, @ace, levelID: @options.levelID, userCodeHasChangedSinceLastCast })
# Ignore the Problem if we already know about it
if _.any(@problems, (preexistingProblem) -> problem.isEqual(preexistingProblem))
problem.destroy()
else # Ok, the problem is worth keeping
@problems.push problem
@displayProblemBanner(problem)
# @saveUserCodeProblem(aether, aetherProblem) # TODO: Enable saving of web-dev user code problems
@addAnnotation(problem.annotation) if problem.annotation
Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: false
onProblemsUpdated: ({ spell, problems, isCast }) ->
# This just handles some ace styles for now; other things handle @problems changes elsewhere
@ace[if problems.length then 'setStyle' else 'unsetStyle'] 'user-code-problem'
@ace[if isCast then 'setStyle' else 'unsetStyle'] 'spell-cast' # Does this still do anything?
saveUserCodeProblem: (aether, aetherProblem) -> saveUserCodeProblem: (aether, aetherProblem) ->
# Skip duplicate problems # Skip duplicate problems
hashValue = aether.raw + aetherProblem.message hashValue = aether.raw + aetherProblem.message
@ -939,6 +992,7 @@ module.exports = class SpellView extends CocoView
@spell.thang.aether[key] = value @spell.thang.aether[key] = value
onSpellChanged: (e) -> onSpellChanged: (e) ->
# TODO: Merge with updateHTML
@spellHasChanged = true @spellHasChanged = true
onSessionWillSave: (e) -> onSessionWillSave: (e) ->

View file

@ -206,4 +206,3 @@ module.exports =
return false if index is -1 return false if index is -1
return false if delta.deltaPath[index+1] in ['en', 'en-US', 'en-GB'] # English speakers are most likely just spamming, so always treat those as patches, not saves. return false if delta.deltaPath[index+1] in ['en', 'en-US', 'en-GB'] # English speakers are most likely just spamming, so always treat those as patches, not saves.
return true return true

View file

@ -29,7 +29,7 @@ describe 'Problem', ->
# TODO: Problems are no longer saved when creating Problems; instead it's in SpellView. Update tests? # TODO: Problems are no longer saved when creating Problems; instead it's in SpellView. Update tests?
xit 'save user code problem', -> xit 'save user code problem', ->
new Problem aether, aetherProblem, ace, false, true, levelID new Problem {aether, aetherProblem, ace, isCast: false, levelID}
expect(jasmine.Ajax.requests.count()).toBe(1) expect(jasmine.Ajax.requests.count()).toBe(1)
request = jasmine.Ajax.requests.mostRecent() request = jasmine.Ajax.requests.mostRecent()
@ -49,7 +49,7 @@ describe 'Problem', ->
xit 'save user code problem no range', -> xit 'save user code problem no range', ->
aetherProblem.range = null aetherProblem.range = null
new Problem aether, aetherProblem, ace, false, true, levelID new Problem {aether, aetherProblem, ace, isCast: false, levelID}
expect(jasmine.Ajax.requests.count()).toBe(1) expect(jasmine.Ajax.requests.count()).toBe(1)
request = jasmine.Ajax.requests.mostRecent() request = jasmine.Ajax.requests.mostRecent()
@ -73,7 +73,7 @@ describe 'Problem', ->
aether.raw = "this.say('hi');\nthis.sad\n('bye');" aether.raw = "this.say('hi');\nthis.sad\n('bye');"
aetherProblem.range = [ { row: 1 }, { row: 2 } ] aetherProblem.range = [ { row: 1 }, { row: 2 } ]
new Problem aether, aetherProblem, ace, false, true, levelID new Problem {aether, aetherProblem, ace, isCast: false, levelID}
expect(jasmine.Ajax.requests.count()).toBe(1) expect(jasmine.Ajax.requests.count()).toBe(1)
request = jasmine.Ajax.requests.mostRecent() request = jasmine.Ajax.requests.mostRecent()