mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-13 01:01:34 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
eb4eeb372d
12 changed files with 203 additions and 84 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
9
app/schemas/subscriptions/web-dev.coffee
Normal file
9
app/schemas/subscriptions/web-dev.coffee
Normal 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' }
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, '<').replace(/>/g, '>')) if s?
|
format = (s) -> marked(s.replace(/</g, '<').replace(/>/g, '>')) 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue