codecombat/app/views/play/level/tome/Spell.coffee
Phoenix Eliot 663c220eaf Show wev-dev iFrame error messages like Aether's
This heavily refactors SpellView and adds infrastructure for receiving and reporting Errors raised by the web-dev iFrame. The web-dev error system, the Aether error system, and the Ace html-worker avoid disturbing each others' errors/annotations (though currently Aether+web-dev errors won't coexist), and they clear/update their own asynchronously.

Show web-dev iFrame errors as Ace annotations

Add functional error banners (with poor messages)

Improve error banners, don't allow duplicate Problems

Refactor setAnnotations override

Convert all constructor calls for Problems

Add comments, clean up

Clean up

Don't clear things unnecessarily

Clean up error message sending from iFrame

Add web-dev:error schema

Clarify error message attributes

Refactor displaying AetherProblems

Refactor displaying user problem banners

Refactor onWebDevError

Set ace styles on updating @problems

Clean up, fix off-by-1 error

Add comment

Show stale web-dev errors differently
Some web-dev errors are generated by "stale" code — code that's still running in the iFrame but doesn't have the player's recent changes.
This shows those errors differently than if they weren't "stale", and suggests they re-run their code.

Hook up web-dev event schema

Destroy ignored duplicate problems

Functionalize a bit of stuff

Fix ProblemAlertView never loading
2016-08-31 10:59:06 -07:00

239 lines
9.9 KiB
CoffeeScript

SpellView = require './SpellView'
SpellTopBarView = require './SpellTopBarView'
{me} = require 'core/auth'
{createAetherOptions} = require 'lib/aether_utils'
utils = require 'core/utils'
module.exports = class Spell
loaded: false
view: null
topBarView: null
constructor: (options) ->
@spellKey = options.spellKey
@pathComponents = options.pathComponents
@session = options.session
@otherSession = options.otherSession
@spectateView = options.spectateView
@spectateOpponentCodeLanguage = options.spectateOpponentCodeLanguage
@observing = options.observing
@supermodel = options.supermodel
@skipProtectAPI = options.skipProtectAPI
@worker = options.worker
@level = options.level
p = options.programmableMethod
@commentI18N = p.i18n
@commentContext = p.context
@languages = p.languages ? {}
@languages.javascript ?= p.source
@name = p.name
@permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams
@team = @permissions.readwrite[0] ? 'common'
if @canWrite()
@setLanguage options.language
else if @otherSession and @team is @otherSession.get 'team'
@setLanguage @otherSession.get('submittedCodeLanguage') or @otherSession.get('codeLanguage')
else
@setLanguage 'javascript'
@source = @originalSource
@parameters = p.parameters
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
if sessionSource isnt '// Should fill in some default source\n' # TODO: figure out why session is getting this default source in there and stop it
@source = sessionSource
if p.aiSource and not @otherSession and not @canWrite()
@source = @originalSource = p.aiSource
@isAISource = true
if @canRead() # We can avoid creating these views if we'll never use them.
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel, levelID: options.levelID}
@view.render() # Get it ready and code loaded in advance
@topBarView = new SpellTopBarView
hintsState: options.hintsState
spell: @
supermodel: @supermodel
codeLanguage: @language
level: options.level
session: options.session
courseID: options.courseID
@topBarView.render()
Backbone.Mediator.publish 'tome:spell-created', spell: @
destroy: ->
@view?.destroy()
@topBarView?.destroy()
@thang = null
@worker = null
setLanguage: (@language) ->
@language = 'html' if @level.isType('web-dev')
@displayCodeLanguage = utils.capitalLanguages[@language]
#console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript
@originalSource = @languages[@language] ? @languages.javascript
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
if @level.isType('web-dev')
# Pull apart the structural wrapper code and the player code, remember the wrapper code, and strip indentation on player code.
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
playerCodeLines = playerCode.split('\n')
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, '' # ☃ serves as placeholder for constructHTML
@originalSource = playerCode
# Translate comments chosen spoken language.
return unless @commentContext
context = $.extend true, {}, @commentContext
if @commentI18N
spokenLanguage = me.get 'preferredLanguage'
while spokenLanguage
spokenLanguage = spokenLanguage.substr 0, spokenLanguage.lastIndexOf('-') if fallingBack?
if spokenLanguageContext = @commentI18N[spokenLanguage]?.context
context = _.merge context, spokenLanguageContext
break
fallingBack = true
try
@originalSource = _.template @originalSource, context
@wrapperCode = _.template @wrapperCode, context
catch e
console.error "Couldn't create example code template of", @originalSource, "\nwith context", context, "\nError:", e
if /loop/.test(@originalSource) and @level.isType('course', 'course-ladder')
# Temporary hackery to make it look like we meant while True: in our sample code until we can update everything
@originalSource = switch @language
when 'python' then @originalSource.replace /loop:/, 'while True:'
when 'javascript' then @originalSource.replace /loop {/, 'while (true) {'
when 'lua' then @originalSource.replace /loop\n/, 'while true then\n'
when 'coffeescript' then @originalSource
else @originalSource
constructHTML: (source) ->
@wrapperCode.replace '', source
addPicoCTFProblem: ->
return @originalSource unless problem = @level.picoCTFProblem
description = """
-- #{problem.name} --
#{problem.description}
""".replace /<p>(.*?)<\/p>/gi, '$1'
("// #{line}" for line in description.split('\n')).join('\n') + '\n' + @originalSource
addThang: (thang) ->
if @thang?.thang.id is thang.id
@thang.thang = thang
else
@thang = {thang: thang, aether: @createAether(thang), castAether: null}
removeThangID: (thangID) ->
@thang = null if @thang?.thang.id is thangID
canRead: (team) ->
(team ? me.team) in @permissions.read or (team ? me.team) in @permissions.readwrite
canWrite: (team) ->
(team ? me.team) in @permissions.readwrite
getSource: ->
@view?.getSource() ? @source
transpile: (source) ->
if source
@source = source
else
source = @getSource()
unless @language is 'html'
@thang?.aether.transpile source
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) ->
(newSource ? @originalSource) isnt (currentSource ? @source)
hasChangedSignificantly: (newSource=null, currentSource=null, cb) ->
unless aether = @thang?.aether
console.error @toString(), 'couldn\'t find a spellThang with aether', @thang
cb false
if @worker
workerMessage =
function: 'hasChangedSignificantly'
a: (newSource ? @originalSource)
spellKey: @spellKey
b: (currentSource ? @source)
careAboutLineNumbers: true
careAboutLint: true
@worker.addEventListener 'message', (e) =>
workerData = JSON.parse e.data
if workerData.function is 'hasChangedSignificantly' and workerData.spellKey is @spellKey
@worker.removeEventListener 'message', arguments.callee, false
cb(workerData.hasChanged)
@worker.postMessage JSON.stringify(workerMessage)
else
cb(aether.hasChangedSignificantly((newSource ? @originalSource), (currentSource ? @source), true, true))
createAether: (thang) ->
writable = @permissions.readwrite.length > 0 and not @isAISource
skipProtectAPI = @skipProtectAPI or not writable or @level.isType('game-dev')
problemContext = @createProblemContext thang
includeFlow = @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') and not skipProtectAPI
aetherOptions = createAetherOptions
functionName: @name
codeLanguage: @language
functionParameters: @parameters
skipProtectAPI: skipProtectAPI
includeFlow: includeFlow
problemContext: problemContext
useInterpreter: true
aether = new Aether aetherOptions
if @worker
workerMessage =
function: 'createAether'
spellKey: @spellKey
options: aetherOptions
@worker.postMessage JSON.stringify workerMessage
aether
updateLanguageAether: (@language) ->
@thang?.aether?.setLanguage @language
@thang?.castAether = null
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
if @worker
workerMessage =
function: 'updateLanguageAether'
newLanguage: @language
@worker.postMessage JSON.stringify workerMessage
@transpile()
toString: ->
"<Spell: #{@spellKey}>"
createProblemContext: (thang) ->
# Create problemContext Aether can use to craft better error messages
# stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak)
# thisMethods: methods available on the 'this' object
# thisProperties: properties available on the 'this' object
# commonThisMethods: methods that are available sometimes, but not awlays
# NOTE: Assuming the first createProblemContext call has everything we need, and we'll use that forevermore
return @problemContext if @problemContext?
@problemContext = { stringReferences: [], thisMethods: [], thisProperties: [] }
# TODO: These should be read from the database
@problemContext.commonThisMethods = ['moveRight', 'moveLeft', 'moveUp', 'moveDown', 'attack', 'findNearestEnemy', 'buildXY', 'moveXY', 'say', 'move', 'distance', 'findEnemies', 'findFriends', 'addFlag', 'findFlag', 'removeFlag', 'findFlags', 'attackRange', 'cast', 'buildTypes', 'jump', 'jumpTo', 'attackXY']
return @problemContext unless thang?
# Populate stringReferences
for key, value of thang.world?.thangMap
if (value.isAttackable or value.isSelectable) and value.id not in @problemContext.stringReferences
@problemContext.stringReferences.push value.id
# Populate thisMethods and thisProperties
if thang.programmableProperties?
for prop in thang.programmableProperties
if _.isFunction(thang[prop])
@problemContext.thisMethods.push prop
else
@problemContext.thisProperties.push prop
# TODO: See SpellPaletteView.createPalette() for other interesting contextual properties
@problemContext