codecombat/app/views/play/level/tome/SpellView.coffee

813 lines
34 KiB
CoffeeScript
Raw Normal View History

2014-07-17 20:20:11 -04:00
CocoView = require 'views/kinds/CocoView'
2014-01-03 13:32:13 -05:00
template = require 'templates/play/level/tome/spell'
{me} = require 'lib/auth'
filters = require 'lib/image_filter'
2014-06-30 22:16:26 -04:00
Range = ace.require('ace/range').Range
UndoManager = ace.require('ace/undomanager').UndoManager
Problem = require './Problem'
SpellDebugView = require './SpellDebugView'
SpellToolbarView = require './SpellToolbarView'
LevelComponent = require 'models/LevelComponent'
UserCodeProblem = require 'models/UserCodeProblem'
2014-01-03 13:32:13 -05:00
2014-07-17 20:20:11 -04:00
module.exports = class SpellView extends CocoView
2014-01-03 13:32:13 -05:00
id: 'spell-view'
className: 'shown'
template: template
controlsEnabled: true
eventsSuppressed: true
writable: true
editModes:
'javascript': 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee'
'python': 'ace/mode/python'
'clojure': 'ace/mode/clojure'
'lua': 'ace/mode/lua'
'io': 'ace/mode/text'
2014-03-13 21:49:58 -04:00
keyBindings:
'default': null
'vim': 'ace/keyboard/vim'
'emacs': 'ace/keyboard/emacs'
2014-01-03 13:32:13 -05:00
subscriptions:
'level:disable-controls': 'onDisableControls'
'level:enable-controls': 'onEnableControls'
2014-01-03 13:32:13 -05:00
'surface:frame-changed': 'onFrameChanged'
2014-03-12 20:50:59 -04:00
'surface:coordinate-selected': 'onCoordinateSelected'
2014-01-03 13:32:13 -05:00
'god:new-world-created': 'onNewWorld'
'god:user-code-problem': 'onUserCodeProblem'
'god:non-user-code-problem': 'onNonUserCodeProblem'
2014-01-03 13:32:13 -05:00
'tome:manual-cast': 'onManualCast'
'tome:reload-code': 'onCodeReload'
'tome:spell-changed': 'onSpellChanged'
'level:session-will-save': 'onSessionWillSave'
'modal:closed': 'focus'
'tome:focus-editor': 'focus'
'tome:spell-statement-index-updated': 'onStatementIndexUpdated'
'tome:change-language': 'onChangeLanguage'
'tome:change-config': 'onChangeEditorConfig'
'tome:update-snippets': 'addZatannaSnippets'
'tome:insert-snippet': 'onInsertSnippet'
'tome:spell-beautify': 'onSpellBeautify'
'tome:maximize-toggled': 'onMaximizeToggled'
'script:state-changed': 'onScriptStateChange'
2014-01-07 00:25:18 -05:00
events:
'mouseout': 'onMouseOut'
2014-01-03 13:32:13 -05:00
constructor: (options) ->
super options
@worker = options.worker
2014-01-03 13:32:13 -05:00
@session = options.session
2014-03-24 12:58:34 -04:00
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
2014-01-03 13:32:13 -05:00
@spell = options.spell
@problems = []
@savedProblems = {} # Cache saved user code problems to prevent duplicates
2014-01-03 13:32:13 -05:00
@writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
$(window).on 'resize', @onWindowResize
2014-01-03 13:32:13 -05:00
afterRender: ->
super()
@createACE()
@createACEShortcuts()
@fillACE()
if @session.get('multiplayer')
2014-01-03 13:32:13 -05:00
@createFirepad()
else
# needs to happen after the code generating this view is complete
_.defer @onAllLoaded
2014-01-03 13:32:13 -05:00
createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
aceConfig = me.get('aceConfig') ? {}
@destroyAceEditor(@ace)
2014-01-03 13:32:13 -05:00
@ace = ace.edit @$el.find('.ace')[0]
@aceSession = @ace.getSession()
@aceDoc = @aceSession.getDocument()
@aceSession.setUseWorker false
@aceSession.setMode @editModes[@spell.language]
2014-01-03 13:32:13 -05:00
@aceSession.setWrapLimitRange null
@aceSession.setUseWrapMode true
2014-06-30 22:16:26 -04:00
@aceSession.setNewLineMode 'unix'
@aceSession.setUseSoftTabs true
2014-01-03 13:32:13 -05:00
@ace.setTheme 'ace/theme/textmate'
@ace.setDisplayIndentGuides aceConfig.indentGuides
2014-01-03 13:32:13 -05:00
@ace.setShowPrintMargin false
@ace.setShowInvisibles aceConfig.invisibles
@ace.setBehavioursEnabled aceConfig.behaviors
2014-04-14 14:18:02 -04:00
@ace.setAnimatedScroll true
@ace.setKeyboardHandler @keyBindings[aceConfig.keyBindings ? 'default']
2014-01-03 13:32:13 -05:00
@toggleControls null, @writable
@aceSession.selection.on 'changeCursor', @onCursorActivity
$(@ace.container).find('.ace_gutter').on 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick
@initAutocomplete aceConfig.liveCompletion ? true
2014-01-03 13:32:13 -05:00
createACEShortcuts: ->
2014-02-12 16:43:17 -05:00
@aceCommands = aceCommands = []
ace = @ace
addCommand = (c) ->
ace.commands.addCommand c
aceCommands.push c.name
2014-02-12 15:41:41 -05:00
addCommand
2014-01-03 13:32:13 -05:00
name: 'run-code'
2014-05-05 18:33:08 -04:00
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
2014-02-12 16:43:17 -05:00
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
addCommand
name: 'run-code-real-time'
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
2014-05-05 18:33:08 -04:00
addCommand
name: 'no-op'
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
exec: -> # just prevent page save call
2014-02-12 15:41:41 -05:00
addCommand
2014-01-03 13:32:13 -05:00
name: 'toggle-playing'
bindKey: {win: 'Ctrl-P', mac: 'Command-P|Ctrl-P'}
exec: -> Backbone.Mediator.publish 'level:toggle-playing', {}
2014-02-12 15:41:41 -05:00
addCommand
2014-01-03 13:32:13 -05:00
name: 'end-current-script'
bindKey: {win: 'Shift-Space', mac: 'Shift-Space'}
# passEvent: true # https://github.com/ajaxorg/ace/blob/master/lib/ace/keyboard/keybinding.js#L114
# No easy way to selectively cancel shift+space, since we don't get access to the event.
# Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active?
2014-08-21 19:27:52 -04:00
exec: =>
if @scriptRunning
Backbone.Mediator.publish 'level:shift-space-pressed', {}
2014-08-21 19:27:52 -04:00
else
@ace.insert ' '
2014-02-12 15:41:41 -05:00
addCommand
2014-01-03 13:32:13 -05:00
name: 'end-all-scripts'
bindKey: {win: 'Escape', mac: 'Escape'}
exec: -> Backbone.Mediator.publish 'level:escape-pressed', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'toggle-grid'
bindKey: {win: 'Ctrl-G', mac: 'Command-G|Ctrl-G'}
exec: -> Backbone.Mediator.publish 'level:toggle-grid', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'toggle-debug'
bindKey: {win: 'Ctrl-\\', mac: 'Command-\\|Ctrl-\\'}
exec: -> Backbone.Mediator.publish 'level:toggle-debug', {}
2014-02-12 15:41:41 -05:00
addCommand
2014-01-31 19:32:46 -05:00
name: 'toggle-pathfinding'
bindKey: {win: 'Ctrl-O', mac: 'Command-O|Ctrl-O'}
exec: -> Backbone.Mediator.publish 'level:toggle-pathfinding', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'level-scrub-forward'
bindKey: {win: 'Ctrl-]', mac: 'Command-]|Ctrl-]'}
exec: -> Backbone.Mediator.publish 'level:scrub-forward', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'level-scrub-back'
bindKey: {win: 'Ctrl-[', mac: 'Command-[|Ctrl-]'}
exec: -> Backbone.Mediator.publish 'level:scrub-back', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'spell-step-forward'
bindKey: {win: 'Ctrl-Alt-]', mac: 'Command-Alt-]|Ctrl-Alt-]'}
exec: -> Backbone.Mediator.publish 'tome:spell-step-forward', {}
2014-02-12 15:41:41 -05:00
addCommand
name: 'spell-step-backward'
bindKey: {win: 'Ctrl-Alt-[', mac: 'Command-Alt-[|Ctrl-Alt-]'}
exec: -> Backbone.Mediator.publish 'tome:spell-step-backward', {}
addCommand
name: 'spell-beautify'
bindKey: {win: 'Ctrl-Shift-B', mac: 'Command-Shift-B|Ctrl-Shift-B'}
exec: -> Backbone.Mediator.publish 'tome:spell-beautify', {}
addCommand
name: 'prevent-line-jump'
bindKey: {win: 'Ctrl-L', mac: 'Command-L'}
passEvent: true
exec: -> # just prevent default ACE go-to-line alert
2014-07-22 15:47:36 -04:00
addCommand
name: 'open-fullscreen-editor'
2014-08-27 20:26:56 -04:00
bindKey: {win: 'Ctrl-Shift-M', mac: 'Command-Shift-M|Ctrl-Shift-M'}
exec: -> Backbone.Mediator.publish 'tome:toggle-maximize', {}
2014-01-03 13:32:13 -05:00
fillACE: ->
@ace.setValue @spell.source
@aceSession.setUndoManager(new UndoManager())
2014-01-03 13:32:13 -05:00
@ace.clearSelection()
initAutocomplete: (@autocomplete) ->
# TODO: Turn on more autocompletion based on level sophistication
# TODO: E.g. using the language default snippets yields a bunch of crazy non-beginner suggestions
# TODO: Options logic shouldn't exist both here and in updateAutocomplete()
@zatanna = new Zatanna @ace,
basic: false
liveCompletion: false
snippets: @autocomplete
snippetsLangDefaults: false
completers:
keywords: false
text: false
autoLineEndings:
javascript: ';'
updateAutocomplete: (@autocomplete) ->
@zatanna?.set 'snippets', @autocomplete
addZatannaSnippets: (e) ->
return unless @zatanna and @autocomplete
snippetEntries = []
for group, props of e.propGroups
for prop in props
if _.isString prop # organizePalette
owner = group
else # organizePaletteHero
owner = prop.owner
prop = prop.prop
2014-06-16 09:29:01 -04:00
doc = _.find (e.allDocs['__' + prop] ? []), (doc) ->
return true if doc.owner is owner
return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
2014-07-15 22:25:53 -04:00
if doc?.snippets?[e.language]
entry =
content: doc.snippets[e.language].code
name: doc.name
tabTrigger: doc.snippets[e.language].tab
snippetEntries.push entry
2014-08-14 19:56:01 -04:00
# window.zatannaInstance = @zatanna
# window.snippetEntries = snippetEntries
lang = @editModes[e.language].substr 'ace/mode/'.length
@zatanna.addSnippets snippetEntries, lang
2014-02-11 18:38:36 -05:00
onMultiplayerChanged: ->
if @session.get('multiplayer')
2014-01-03 13:32:13 -05:00
@createFirepad()
else
@firepad?.dispose()
createFirepad: ->
2014-04-23 19:44:29 -04:00
# load from firebase or the original source if there's nothing there
2014-01-03 13:32:13 -05:00
return if @firepadLoading
@eventsSuppressed = true
@loaded = false
@previousSource = @ace.getValue()
@ace.setValue('')
@aceSession.setUndoManager(new UndoManager())
2014-01-03 13:32:13 -05:00
fireURL = 'https://codecombat.firebaseio.com/' + @spell.pathComponents.join('/')
@fireRef = new Firebase fireURL
firepadOptions = userId: me.id
@firepad = Firepad.fromACE @fireRef, @ace, firepadOptions
@firepad.on 'ready', @onFirepadLoaded
@firepadLoading = true
onFirepadLoaded: =>
@firepadLoading = false
firepadSource = @ace.getValue()
if firepadSource
@spell.source = firepadSource
else
@ace.setValue @previousSource
@aceSession.setUndoManager(new UndoManager())
2014-01-03 13:32:13 -05:00
@ace.clearSelection()
@onAllLoaded()
2014-01-03 13:32:13 -05:00
onAllLoaded: =>
2014-01-03 13:32:13 -05:00
@spell.transpile @spell.source
@spell.loaded = true
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
2014-01-25 18:11:29 -05:00
@createToolbarView()
2014-01-03 13:32:13 -05:00
createDebugView: ->
return if @options.level.get('type', true) is 'hero' # We'll turn this on later, maybe, but not yet.
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
@$el.append @debugView.render().$el.hide()
createToolbarView: ->
@toolbarView = new SpellToolbarView ace: @ace
2014-01-25 18:11:29 -05:00
@$el.append @toolbarView.render().$el
onMouseOut: (e) ->
@debugView?.onMouseOut e
2014-01-03 13:32:13 -05:00
getSource: ->
@ace.getValue() # could also do @firepad.getText()
setThang: (thang) ->
@focus()
@lastScreenLineCount = null
@updateLines()
2014-01-03 13:32:13 -05:00
return if thang.id is @thang?.id
@thang = thang
@spellThang = @spell.thangs[@thang.id]
@createDebugView() unless @debugView
@debugView?.thang = @thang
2014-01-31 19:16:59 -05:00
@toolbarView?.toggleFlow false
@updateAether false, false
# @addZatannaSnippets()
2014-01-03 13:32:13 -05:00
@highlightCurrentLine()
cast: (preload=false, realTime=false) ->
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload, realTime: realTime
2014-01-03 13:32:13 -05:00
notifySpellChanged: =>
Backbone.Mediator.publish 'tome:spell-changed', spell: @spell
notifyEditingEnded: =>
return if @aceDoc.undergoingFirepadOperation # from my Firepad ACE adapter
Backbone.Mediator.publish 'tome:editing-ended', {}
2014-01-03 13:32:13 -05:00
notifyEditingBegan: =>
return if @aceDoc.undergoingFirepadOperation # from my Firepad ACE adapter
Backbone.Mediator.publish 'tome:editing-began', {}
2014-01-03 13:32:13 -05:00
updateLines: =>
# Make sure there are always blank lines for the player to type on, and that the editor resizes to the height of the lines.
lineCount = @aceDoc.getLength()
lastLine = @aceDoc.$lines[lineCount - 1]
if lastLine isnt ''
cursorPosition = @ace.getCursorPosition()
wasAtEnd = cursorPosition.row is lineCount - 1 and cursorPosition.column is lastLine.length
@aceDoc.insertNewLine row: lineCount, column: 0 #lastLine.length
@ace.navigateLeft(1) if wasAtEnd
++lineCount
screenLineCount = @aceSession.getScreenLength()
if screenLineCount isnt @lastScreenLineCount
@lastScreenLineCount = screenLineCount
lineHeight = @ace.renderer.lineHeight or 20
tomeHeight = $('#tome-view').innerHeight()
spellListTabEntryHeight = $('#spell-list-tab-entry-view').outerHeight()
spellToolbarHeight = $('.spell-toolbar-view').outerHeight()
spellPaletteHeight = $('#spell-palette-view').outerHeight()
maxHeight = tomeHeight - spellListTabEntryHeight - spellToolbarHeight - spellPaletteHeight
linesAtMaxHeight = Math.floor(maxHeight / lineHeight)
lines = Math.max 8, Math.min(screenLineCount + 4, linesAtMaxHeight)
# 2 lines buffer is nice, but 4 leaves room to put problem alerts.
@ace.setOptions minLines: lines, maxLines: lines
$('#spell-palette-view').css('top', 38 + 45 + lineHeight * lines) # Move spell palette up, slightly underlapping us.
2014-01-03 13:32:13 -05:00
onManualCast: (e) ->
cast = @$el.parent().length
@recompile cast, e.realTime
2014-01-03 13:32:13 -05:00
@focus() if cast
onCodeReload: (e) ->
return unless e.spell is @spell
@reloadCode true
reloadCode: (cast=true) ->
@updateACEText @spell.originalSource
@recompile cast
2014-02-12 15:41:41 -05:00
recompileIfNeeded: =>
@recompile() if @recompileNeeded
recompile: (cast=true, realTime=false) ->
2014-01-03 13:32:13 -05:00
@setRecompileNeeded false
2014-08-23 17:31:38 -04:00
hasChanged = @spell.source isnt @getSource()
if hasChanged
@spell.transpile @getSource()
@updateAether true, false
if cast and (hasChanged or realTime)
@cast(false, realTime)
if hasChanged
@notifySpellChanged()
2014-01-03 13:32:13 -05:00
updateACEText: (source) ->
@eventsSuppressed = true
if @firepad
@firepad.setText source
else
@ace.setValue source
@aceSession.setUndoManager(new UndoManager())
2014-01-03 13:32:13 -05:00
@eventsSuppressed = false
try
@ace.resize true # hack: @ace may not have updated its text properly, so we force it to refresh
catch error
2014-06-30 22:16:26 -04:00
console.warn 'Error resizing ACE after an update:', error
2014-01-03 13:32:13 -05:00
# Called from CastButtonView initially and whenever the delay is changed
setAutocastDelay: (@autocastDelay) ->
@createOnCodeChangeHandlers()
createOnCodeChangeHandlers: ->
@aceDoc.removeListener 'change', @onCodeChangeMetaHandler if @onCodeChangeMetaHandler
autocastDelay = @autocastDelay ? 3000
onSignificantChange = [
_.debounce @setRecompileNeeded, autocastDelay - 100
2014-02-12 15:41:41 -05:00
@currentAutocastHandler = _.debounce @recompileIfNeeded, autocastDelay
2014-01-03 13:32:13 -05:00
]
onAnyChange = [
_.debounce @updateAether, 500
_.debounce @notifyEditingEnded, 1000
_.throttle @notifyEditingBegan, 250
_.throttle @notifySpellChanged, 300
_.throttle @updateLines, 500
2014-01-03 13:32:13 -05:00
]
@onCodeChangeMetaHandler = =>
return if @eventsSuppressed
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'code-change', volume: 0.5
if @spellThang
@spell.hasChangedSignificantly @getSource(), @spellThang.aether.raw, (hasChanged) =>
if not @spellThang or hasChanged
callback() for callback in onSignificantChange # Do these first
callback() for callback in onAnyChange # Then these
2014-01-03 13:32:13 -05:00
@aceDoc.on 'change', @onCodeChangeMetaHandler
setRecompileNeeded: (@recompileNeeded) =>
2014-01-03 13:32:13 -05:00
onCursorActivity: =>
@currentAutocastHandler?()
# Design for a simpler system?
# * Keep Aether linting, debounced, on any significant change
# - All problems just vanish when you make any change to the code
# * You wouldn't accept any Aether updates/runtime information/errors unless its code was current when you got it
# * Store the last run Aether in each spellThang and use it whenever its code actually is current.
# Use dynamic markers for problem ranges and keep annotations/alerts in when insignificant
2014-01-03 13:32:13 -05:00
# changes happen, but always treat any change in the (trimmed) number of lines as a significant change.
# - All problems have a master representation as a Problem, and we can easily generate all Problems from
# any Aether instance. Then when we switch contexts in any way, we clear, recreate, and reapply the Problems.
# * Problem alerts have their own templated ProblemAlertViews.
2014-01-03 13:32:13 -05:00
# * We'll only show the first problem alert, and it will always be at the bottom.
# Annotations and problem ranges can show all, I guess.
# * The editor will reserve space for one annotation as a codeless area.
# - Problem alerts and ranges will only show on fully cast worlds. Annotations will show continually.
updateAether: (force=false, fromCodeChange=true) =>
# Depending on whether we have any code changes, significant code changes, or have switched
# to a new spellThang, we may want to refresh our Aether display.
return unless aether = @spellThang?.aether
source = @getSource()
@spell.hasChangedSignificantly source, aether.raw, (hasChanged) =>
codeHasChangedSignificantly = force or hasChanged
needsUpdate = codeHasChangedSignificantly or @spellThang isnt @lastUpdatedAetherSpellThang
return if not needsUpdate and aether is @displayedAether
castAether = @spellThang.castAether
codeIsAsCast = castAether and source is castAether.raw
aether = castAether if codeIsAsCast
return if not needsUpdate and aether is @displayedAether
2014-04-23 19:44:29 -04:00
# 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
2014-04-23 19:44:29 -04:00
finishUpdatingAether = (aether) =>
@displayAether aether, codeIsAsCast
2014-04-23 19:44:29 -04:00
@lastUpdatedAetherSpellThang = @spellThang
@guessWhetherFinished aether if fromCodeChange
@clearAetherDisplay()
2014-04-23 19:44:29 -04:00
if codeHasChangedSignificantly and not codeIsAsCast
if @worker
workerMessage =
function: 'transpile'
spellKey: @spell.spellKey
source: source
@worker.addEventListener 'message', (e) =>
workerData = JSON.parse e.data
if workerData.function is 'transpile' and workerData.spellKey is @spell.spellKey
@worker.removeEventListener 'message', arguments.callee, false
aether.problems = workerData.problems
aether.raw = source
finishUpdatingAether(aether)
@worker.postMessage JSON.stringify(workerMessage)
else
aether.transpile source
finishUpdatingAether(aether)
2014-04-23 19:44:29 -04:00
else
finishUpdatingAether(aether)
2014-01-03 13:32:13 -05:00
clearAetherDisplay: ->
problem.destroy() for problem in @problems
@problems = []
@aceSession.setAnnotations []
@highlightCurrentLine {} # This'll remove all highlights
displayAether: (aether, isCast=false) ->
@displayedAether = aether
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.
2014-01-03 13:32:13 -05:00
@problems = []
annotations = []
seenProblemKeys = {}
2014-01-03 13:32:13 -05:00
for aetherProblem, problemIndex in aether.getAllProblems()
continue if key = aetherProblem.userInfo?.key and key of seenProblemKeys
seenProblemKeys[key] = true if key
2014-08-14 14:55:43 -04:00
@problems.push problem = new Problem aether, aetherProblem, @ace, isCast and problemIndex is 0, isCast, @spell.levelID
@saveUserCodeProblem(aether, aetherProblem) if isCast
2014-01-03 13:32:13 -05:00
annotations.push problem.annotation if problem.annotation
@aceSession.setAnnotations annotations
@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 style', aether.style unless _.isEmpty aether.style
#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'
2014-01-03 13:32:13 -05:00
Backbone.Mediator.publish 'tome:problems-updated', spell: @spell, problems: @problems, isCast: isCast
@ace.resize()
2014-01-03 13:32:13 -05:00
saveUserCodeProblem: (aether, aetherProblem) ->
# Skip duplicate problems
hashValue = aether.raw + aetherProblem.message
return if hashValue of @savedProblems
@savedProblems[hashValue] = true
# Save new problem
@userCodeProblem = new UserCodeProblem()
@userCodeProblem.set 'code', aether.raw
if aetherProblem.range
rawLines = aether.raw.split '\n'
errorLines = rawLines.slice aetherProblem.range[0].row, aetherProblem.range[1].row + 1
@userCodeProblem.set 'codeSnippet', errorLines.join '\n'
@userCodeProblem.set 'errHint', aetherProblem.hint if aetherProblem.hint
@userCodeProblem.set 'errId', aetherProblem.id if aetherProblem.id
@userCodeProblem.set 'errLevel', aetherProblem.level if aetherProblem.level
@userCodeProblem.set 'errMessage', aetherProblem.message if aetherProblem.message
@userCodeProblem.set 'errRange', aetherProblem.range if aetherProblem.range
@userCodeProblem.set 'errType', aetherProblem.type if aetherProblem.type
@userCodeProblem.set 'language', aether.language.id if aether.language?.id
@userCodeProblem.set 'levelID', @spell.levelID if @spell.levelID
@userCodeProblem.save()
null
2014-01-03 13:32:13 -05:00
# Autocast:
# Goes immediately if the code is a) changed and b) complete/valid and c) the cursor is at beginning or end of a line
# We originally thought it would:
2014-01-03 13:32:13 -05:00
# - Go after specified delay if a) and b) but not c)
# - Go only when manually cast or deselecting a Thang when there are errors
# But the error message display was delayed, so now trying:
# - Go after specified delay if a) and not b) or c)
guessWhetherFinished: (aether) ->
valid = not aether.getAllProblems().length
cursorPosition = @ace.getCursorPosition()
currentLine = _.string.rtrim(@aceDoc.$lines[cursorPosition.row].replace(/[ \t]*\/\/[^"']*/g, '')) # trim // unless inside "
2014-01-03 13:32:13 -05:00
endOfLine = cursorPosition.column >= currentLine.length # just typed a semicolon or brace, for example
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
incompleteThis = /^(s|se|sel|self|t|th|thi|this)$/.test currentLine.trim()
2014-10-14 20:53:17 -04:00
# console.log "finished=#{valid and (endOfLine or beginningOfLine) and not incompleteThis}", valid, endOfLine, beginningOfLine, incompleteThis, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
if valid and (endOfLine or beginningOfLine) and not incompleteThis
if @autocastDelay > 60000
@preload()
else
@recompile()
preload: ->
# Send this code over to the God for preloading, but don't change the cast state.
oldSource = @spell.source
oldSpellThangAethers = {}
for thangID, spellThang of @spell.thangs
oldSpellThangAethers[thangID] = spellThang.aether.serialize() # Get raw, pure, and problems
@spell.transpile @getSource()
@cast true
@spell.source = oldSource
for thangID, spellThang of @spell.thangs
for key, value of oldSpellThangAethers[thangID]
spellThang.aether[key] = value
2014-01-03 13:32:13 -05:00
onSpellChanged: (e) ->
@spellHasChanged = true
onSessionWillSave: (e) ->
2014-01-25 18:11:29 -05:00
return unless @spellHasChanged
setTimeout(=>
unless @destroyed or @spellHasChanged
@$el.find('.save-status').finish().show().fadeOut(2000)
, 1000)
@spellHasChanged = false
2014-01-03 13:32:13 -05:00
onUserCodeProblem: (e) ->
2014-06-30 22:16:26 -04:00
return @onInfiniteLoop e if e.problem.id is 'runtime_InfiniteLoop'
2014-01-03 13:32:13 -05:00
return unless e.problem.userInfo.methodName is @spell.name
return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID
@spell.hasChangedSignificantly @getSource(), null, (hasChanged) =>
return if hasChanged
spellThang.aether.addProblem e.problem
@lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile
@updateAether false, false
2014-01-03 13:32:13 -05:00
onNonUserCodeProblem: (e) ->
return unless @spellThang
problem = @spellThang.aether.createUserCodeProblem type: 'runtime', kind: 'Unhandled', message: "Unhandled error: #{e.problem.message}"
@spellThang.aether.addProblem problem
@spellThang.castAether?.addProblem problem
@lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile
@updateAether false, false # TODO: doesn't work, error doesn't display
2014-01-03 13:32:13 -05:00
onInfiniteLoop: (e) ->
return unless @spellThang
@spellThang.aether.addProblem e.problem
@spellThang.castAether?.addProblem e.problem
2014-01-03 13:32:13 -05:00
@lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile
@updateAether false, false
onNewWorld: (e) ->
@spell.removeThangID thangID for thangID of @spell.thangs when not e.world.getThangByID thangID
2014-01-03 13:32:13 -05:00
for thangID, spellThang of @spell.thangs
thang = e.world.getThangByID(thangID)
aether = e.world.userCodeMap[thangID]?[@spell.name] # Might not be there if this is a new Programmable Thang.
2014-01-03 13:32:13 -05:00
spellThang.castAether = aether
spellThang.aether = @spell.createAether thang
2014-06-30 22:16:26 -04:00
#console.log thangID, @spell.spellKey, 'ran', aether.metrics.callsExecuted, 'times over', aether.metrics.statementsExecuted, 'statements, with max recursion depth', aether.metrics.maxDepth, 'and full flow/metrics', aether.metrics, aether.flow
@spell.transpile() # TODO: is there any way we can avoid doing this if it hasn't changed? Causes a slight hang.
2014-01-03 13:32:13 -05:00
@updateAether false, false
# --------------------------------------------------------------------------------------------------
focus: ->
# TODO: it's a hack checking if a modal is visible; the events should be removed somehow
# but this view is not part of the normal subview destroying because of how it's swapped
return unless @controlsEnabled and @writable and $('.modal:visible').length is 0
return if @ace.isFocused()
@ace.focus()
@ace.clearSelection()
onFrameChanged: (e) ->
return unless @spellThang and e.selectedThang?.id is @spellThang?.thang.id
2014-01-03 13:32:13 -05:00
@thang = e.selectedThang # update our thang to the current version
@highlightCurrentLine()
2014-03-12 20:50:59 -04:00
onCoordinateSelected: (e) ->
return unless @ace.isFocused() and e.x? and e.y?
2014-03-12 20:50:59 -04:00
@ace.insert "{x: #{e.x}, y: #{e.y}}"
2014-03-13 19:52:18 -04:00
@highlightCurrentLine()
2014-03-12 20:50:59 -04:00
onStatementIndexUpdated: (e) ->
return unless e.ace is @ace
@highlightCurrentLine()
2014-01-03 13:32:13 -05:00
highlightCurrentLine: (flow) =>
# TODO: move this whole thing into SpellDebugView or somewhere?
2014-02-20 20:21:15 -05:00
@highlightComments() unless @destroyed
2014-01-03 13:32:13 -05:00
flow ?= @spellThang?.castAether?.flow
return unless flow
executed = []
2014-03-11 18:47:27 -04:00
executedRows = {}
2014-01-03 13:32:13 -05:00
matched = false
states = flow.states ? []
currentCallIndex = null
for callState, callNumber in states
if not currentCallIndex? and callState.userInfo?.time > @thang.world.age
currentCallIndex = callNumber - 1
2014-01-03 13:32:13 -05:00
if matched
executed.pop()
break
executed.push []
for state, statementNumber in callState.statements
2014-01-03 13:32:13 -05:00
if state.userInfo?.time > @thang.world.age
matched = true
break
_.last(executed).push state
2014-03-11 18:47:27 -04:00
executedRows[state.range[0].row] = true
2014-04-23 19:44:29 -04:00
#state.executing = true if state.userInfo?.time is @thang.world.age # no work
currentCallIndex ?= callNumber - 1
2014-06-30 22:16:26 -04:00
#console.log 'got call index', currentCallIndex, 'for time', @thang.world.age, 'out of', states.length
2014-01-03 13:32:13 -05:00
2014-03-11 20:26:11 -04:00
@decoratedGutter = @decoratedGutter || {}
2014-01-03 13:32:13 -05:00
# TODO: don't redo the markers if they haven't actually changed
for markerRange in (@markerRanges ?= [])
markerRange.start.detach()
markerRange.end.detach()
@aceSession.removeMarker markerRange.id
@markerRanges = []
2014-03-11 18:47:27 -04:00
for row in [0 ... @aceSession.getLength()]
unless executedRows[row]
@aceSession.removeGutterDecoration row, 'executing'
@aceSession.removeGutterDecoration row, 'executed'
@decoratedGutter[row] = ''
lastExecuted = _.last executed
showToolbarView = executed.length and (@spell.name isnt 'plan' or @spellThang.castAether.metrics.statementsExecuted > 20)
if showToolbarView
statementIndex = Math.max 0, lastExecuted.length - 1
@toolbarView?.toggleFlow true
@toolbarView?.setCallState states[currentCallIndex], statementIndex, currentCallIndex, @spellThang.castAether.metrics
lastExecuted = lastExecuted[0 .. @toolbarView.statementIndex] if @toolbarView?.statementIndex?
else
2014-01-25 18:11:29 -05:00
@toolbarView?.toggleFlow false
@debugView?.setVariableStates {}
2014-01-03 13:32:13 -05:00
marked = {}
2014-02-24 11:59:50 -05:00
gotVariableStates = false
for state, i in lastExecuted ? []
[start, end] = state.range
2014-01-03 13:32:13 -05:00
clazz = if i is lastExecuted.length - 1 then 'executing' else 'executed'
if clazz is 'executed'
continue if marked[start.row]
marked[start.row] = true
2014-06-30 22:16:26 -04:00
markerType = 'fullLine'
else
@debugView?.setVariableStates state.variables
2014-02-24 11:59:50 -05:00
gotVariableStates = true
2014-06-30 22:16:26 -04:00
markerType = 'text'
markerRange = new Range start.row, start.col, end.row, end.col
2014-01-03 13:32:13 -05:00
markerRange.start = @aceDoc.createAnchor markerRange.start
markerRange.end = @aceDoc.createAnchor markerRange.end
markerRange.id = @aceSession.addMarker markerRange, clazz, markerType
2014-01-03 13:32:13 -05:00
@markerRanges.push markerRange
2014-03-11 18:47:27 -04:00
if executedRows[start.row] and @decoratedGutter[start.row] isnt clazz
2014-03-11 20:26:11 -04:00
@aceSession.removeGutterDecoration start.row, @decoratedGutter[start.row] if @decoratedGutter[start.row] isnt ''
2014-03-11 18:47:27 -04:00
@aceSession.addGutterDecoration start.row, clazz
@decoratedGutter[start.row] = clazz
2014-10-11 11:34:30 -04:00
Backbone.Mediator.publish("tome:highlight-line", line:start.row) if application.isIPadApp
@debugView?.setVariableStates {} unless gotVariableStates
null
2014-01-03 13:32:13 -05:00
2014-02-20 19:14:31 -05:00
highlightComments: ->
return # Slightly buggy and not that great, so let's not do it.
2014-02-20 19:14:31 -05:00
lines = $(@ace.container).find('.ace_text-layer .ace_line_group')
session = @aceSession
top = Math.floor @ace.renderer.getScrollTopRow()
2014-02-20 19:14:31 -05:00
$(@ace.container).find('.ace_gutter-cell').each (index, el) ->
line = $(lines[index])
index = index - top
2014-02-20 19:14:31 -05:00
session.removeGutterDecoration index, 'comment-line'
if line.find('.ace_comment').length
session.addGutterDecoration index, 'comment-line'
2014-01-03 13:32:13 -05:00
onAnnotationClick: ->
# @ is the gutter element
msg = "Edit line #{$(@).index() + 1} to fix it."
2014-01-03 13:32:13 -05:00
alertBox = $("<div class='alert alert-info fade in'>#{msg}</div>")
offset = $(@).offset()
offset.left -= 162 # default width of the Bootstrap alert here
alertBox.css(offset).css('z-index', 500).css('position', 'absolute')
$('body').append(alertBox.alert())
_.delay (-> alertBox.alert('close')), 2500
onDisableControls: (e) -> @toggleControls e, false
onEnableControls: (e) -> @toggleControls e, @writable
toggleControls: (e, enabled) ->
return if e?.controls and not ('editor' in e.controls)
return if enabled is @controlsEnabled
@controlsEnabled = enabled and @writable
disabled = not enabled
$('body').focus() if disabled and $(document.activeElement).is('.ace_text-input')
@ace.setReadOnly disabled
2014-06-30 22:16:26 -04:00
@ace[if disabled then 'setStyle' else 'unsetStyle'] 'disabled'
2014-01-03 13:32:13 -05:00
@toggleBackground()
toggleBackground: =>
# TODO: make the background an actual background and do the CSS trick
# used in spell_list_entry.sass for disabling
2014-07-23 08:38:12 -04:00
background = @$el.find('img.code-background')[0]
2014-01-03 13:32:13 -05:00
if background.naturalWidth is 0 # not loaded yet
return _.delay @toggleBackground, 100
2014-07-23 08:38:12 -04:00
filters.revertImage background, 'span.code-background' if @controlsEnabled
filters.darkenImage background, 'span.code-background', 0.8 unless @controlsEnabled
2014-01-03 13:32:13 -05:00
onSpellBeautify: (e) ->
return unless @spellThang and (@ace.isFocused() or e.spell is @spell)
ugly = @getSource()
pretty = @spellThang.aether.beautify ugly
@ace.setValue pretty
onMaximizeToggled: (e) ->
_.delay (=> @resize()), 500 + 100 # Wait $level-resize-transition-time, plus a bit.
onWindowResize: (e) =>
_.delay (=> @resize?()), 500 + 100 # Wait $level-resize-transition-time, plus a bit.
resize: ->
@ace?.resize true
@lastScreenLineCount = null
@updateLines()
2014-03-13 21:49:58 -04:00
onChangeEditorConfig: (e) ->
aceConfig = me.get('aceConfig') ? {}
@ace.setDisplayIndentGuides aceConfig.indentGuides # default false
@ace.setShowInvisibles aceConfig.invisibles # default false
@ace.setKeyboardHandler @keyBindings[aceConfig.keyBindings ? 'default']
@updateAutocomplete(aceConfig.liveCompletion ? false)
2014-03-13 21:49:58 -04:00
onChangeLanguage: (e) ->
return unless @spell.canWrite()
@aceSession.setMode @editModes[e.language]
@zatanna?.set 'language', @editModes[e.language].substr('ace/mode/')
wasDefault = @getSource() is @spell.originalSource
@spell.setLanguage e.language
@reloadCode true if wasDefault
onInsertSnippet: (e) ->
snippetCode = null
if e.doc.snippets?[e.language]?.code
snippetCode = e.doc.snippets[e.language].code
else if (e.formatted.type isnt 'snippet') and e.formatted.shortName?
snippetCode = e.formatted.shortName
return unless snippetCode?
snippetManager = ace.require('ace/snippets').snippetManager
snippetManager.insertSnippet @ace, snippetCode
return
2014-01-03 13:32:13 -05:00
dismiss: ->
@spell.hasChangedSignificantly @getSource(), null, (hasChanged) =>
@recompile() if hasChanged
2014-04-23 19:44:29 -04:00
onScriptStateChange: (e) ->
@scriptRunning = if e.currentScript is null then false else true
2014-01-03 13:32:13 -05:00
destroy: ->
2014-02-12 15:41:41 -05:00
$(@ace?.container).find('.ace_gutter').off 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick
2014-01-03 13:32:13 -05:00
@firepad?.dispose()
2014-02-12 15:41:41 -05:00
@ace?.commands.removeCommand command for command in @aceCommands
@ace?.destroy()
@aceDoc?.off 'change', @onCodeChangeMetaHandler
@aceSession?.selection.off 'changeCursor', @onCursorActivity
@destroyAceEditor(@ace)
2014-01-30 18:03:55 -05:00
@debugView?.destroy()
$(window).off 'resize', @onWindowResize
super()