2014-11-28 20:49:41 -05:00
CocoView = require ' views/core/CocoView '
2014-01-03 13:32:13 -05:00
template = require ' templates/play/level/tome/spell '
2014-11-28 20:49:41 -05:00
{ me } = require ' core/auth '
2014-01-03 13:32:13 -05:00
filters = require ' lib/image_filter '
2014-06-30 22:16:26 -04:00
Range = ace . require ( ' ace/range ' ) . Range
2014-08-11 15:34:02 -04:00
UndoManager = ace . require ( ' ace/undomanager ' ) . UndoManager
2014-07-23 10:02:45 -04:00
Problem = require ' ./Problem '
SpellDebugView = require ' ./SpellDebugView '
SpellToolbarView = require ' ./SpellToolbarView '
2014-06-13 11:12:55 -04:00
LevelComponent = require ' models/LevelComponent '
2014-10-17 18:52:57 -04:00
UserCodeProblem = require ' models/UserCodeProblem '
2014-11-20 15:41:53 -05:00
CampaignOptions = require ' lib/CampaignOptions '
2014-11-10 15:47:24 -05:00
LevelOptions = require ' lib/LevelOptions '
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
2014-11-26 15:33:29 -05:00
@editModes:
2014-03-15 18:14:57 -04:00
' javascript ' : ' ace/mode/javascript '
2014-03-28 09:42:08 -04:00
' coffeescript ' : ' ace/mode/coffee '
2014-05-29 15:26:01 -04:00
' python ' : ' ace/mode/python '
2014-05-12 01:51:44 -04:00
' clojure ' : ' ace/mode/clojure '
' lua ' : ' ace/mode/lua '
2014-05-26 11:59:48 -04:00
' io ' : ' ace/mode/text '
2014-03-15 18:14:57 -04:00
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:
2014-08-27 15:24:03 -04:00
' 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 '
2014-05-26 21:45:00 -04:00
' god:non-user-code-problem ' : ' onNonUserCodeProblem '
2014-01-03 13:32:13 -05:00
' tome:manual-cast ' : ' onManualCast '
' tome:reload-code ' : ' onCodeReload '
2014-01-15 14:58:39 -05:00
' tome:spell-changed ' : ' onSpellChanged '
2014-01-16 11:13:14 -05:00
' level:session-will-save ' : ' onSessionWillSave '
2014-08-27 15:24:03 -04:00
' modal:closed ' : ' focus '
2014-06-25 00:07:36 -04:00
' tome:focus-editor ' : ' focus '
2014-01-21 12:03:04 -05:00
' tome:spell-statement-index-updated ' : ' onStatementIndexUpdated '
2014-03-16 21:14:04 -04:00
' tome:change-language ' : ' onChangeLanguage '
' tome:change-config ' : ' onChangeEditorConfig '
2014-06-13 11:12:55 -04:00
' tome:update-snippets ' : ' addZatannaSnippets '
2014-08-04 17:02:54 -04:00
' tome:insert-snippet ' : ' onInsertSnippet '
2014-08-27 15:24:03 -04:00
' tome:spell-beautify ' : ' onSpellBeautify '
2014-08-27 21:43:17 -04:00
' tome:maximize-toggled ' : ' onMaximizeToggled '
2014-08-14 16:29:57 -04:00
' script:state-changed ' : ' onScriptStateChange '
2014-11-05 20:47:23 -05:00
' playback:ended-changed ' : ' onPlaybackEndedChanged '
2014-01-16 13:10:27 -05:00
2014-01-07 00:25:18 -05:00
events:
2014-01-21 12:03:04 -05:00
' mouseout ' : ' onMouseOut '
2014-01-03 13:32:13 -05:00
constructor: (options) ->
super options
2014-04-18 17:59:08 -04:00
@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
2014-05-11 20:42:32 -04:00
@problems = [ ]
2014-10-17 18:52:57 -04:00
@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
2014-10-16 15:08:21 -04:00
$ ( window ) . on ' resize ' , @ onWindowResize
2014-01-03 13:32:13 -05:00
afterRender: ->
super ( )
@ createACE ( )
@ createACEShortcuts ( )
@ fillACE ( )
2014-11-30 14:47:51 -05:00
@ lockDefaultCode ( )
2014-02-22 15:01:05 -05:00
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
2014-05-02 15:32:41 -04:00
_ . defer @ onAllLoaded
2014-01-03 13:32:13 -05:00
createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
2014-03-14 10:46:36 -04:00
aceConfig = me . get ( ' aceConfig ' ) ? { }
2014-08-29 21:02:29 -04:00
@ 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
2014-11-26 15:33:29 -05:00
@ aceSession . setMode SpellView . 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 '
2014-01-04 19:41:25 -05:00
@ aceSession . setUseSoftTabs true
2014-01-03 13:32:13 -05:00
@ ace . setTheme ' ace/theme/textmate '
2014-03-14 13:14:48 -04:00
@ ace . setDisplayIndentGuides aceConfig . indentGuides
2014-01-03 13:32:13 -05:00
@ ace . setShowPrintMargin false
2014-03-14 13:14:48 -04:00
@ ace . setShowInvisibles aceConfig . invisibles
@ ace . setBehavioursEnabled aceConfig . behaviors
2014-04-14 14:18:02 -04:00
@ ace . setAnimatedScroll true
2014-11-05 16:53:23 -05:00
@ ace . setShowFoldWidgets false
2014-03-14 13:14:48 -04:00
@ ace . setKeyboardHandler @ keyBindings [ aceConfig . keyBindings ? ' default ' ]
2014-01-03 13:32:13 -05:00
@ toggleControls null , @ writable
@ aceSession . selection . on ' changeCursor ' , @ onCursorActivity
2014-11-09 00:51:54 -05:00
$ ( @ ace . container ) . find ( ' .ace_gutter ' ) . on ' click mouseenter ' , ' .ace_error, .ace_warning, .ace_info ' , @ onAnnotationClick
2014-11-13 13:54:50 -05:00
$ ( @ ace . container ) . find ( ' .ace_gutter ' ) . on ' click ' , @ onGutterClick
2014-10-07 18:08:27 -04:00
@ 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 ' , { }
2014-08-23 00:35:08 -04:00
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 ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
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 ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-21 19:27:52 -04:00
exec: =>
2014-08-14 16:29:57 -04:00
if @ scriptRunning
2014-08-27 15:24:03 -04:00
Backbone . Mediator . publish ' level:shift-space-pressed ' , { }
2014-08-21 19:27:52 -04:00
else
2014-08-14 16:29:57 -04:00
@ 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 ' }
2014-11-21 14:07:46 -05:00
readOnly: true
exec: ->
console . log ' esc pressed '
Backbone . Mediator . publish ' level:escape-pressed ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' toggle-grid '
bindKey: { win: ' Ctrl-G ' , mac: ' Command-G|Ctrl-G ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' level:toggle-grid ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' toggle-debug '
bindKey: { win: ' Ctrl- \\ ' , mac: ' Command- \\ |Ctrl- \\ ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
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 ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' level:toggle-pathfinding ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' level-scrub-forward '
bindKey: { win: ' Ctrl-] ' , mac: ' Command-]|Ctrl-] ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' level:scrub-forward ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' level-scrub-back '
bindKey: { win: ' Ctrl-[ ' , mac: ' Command-[|Ctrl-] ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' level:scrub-back ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' spell-step-forward '
bindKey: { win: ' Ctrl-Alt-] ' , mac: ' Command-Alt-]|Ctrl-Alt-] ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' tome:spell-step-forward ' , { }
2014-02-12 15:41:41 -05:00
addCommand
2014-01-21 12:03:04 -05:00
name: ' spell-step-backward '
bindKey: { win: ' Ctrl-Alt-[ ' , mac: ' Command-Alt-[|Ctrl-Alt-] ' }
2014-11-21 14:07:46 -05:00
readOnly: true
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' tome:spell-step-backward ' , { }
2014-02-22 19:58:54 -05:00
addCommand
name: ' spell-beautify '
bindKey: { win: ' Ctrl-Shift-B ' , mac: ' Command-Shift-B|Ctrl-Shift-B ' }
2014-08-27 15:24:03 -04:00
exec: -> Backbone . Mediator . publish ' tome:spell-beautify ' , { }
2014-05-20 18:24:36 -04:00
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 ' }
2014-08-27 21:43:17 -04:00
exec: -> Backbone . Mediator . publish ' tome:toggle-maximize ' , { }
2014-11-09 14:28:00 -05:00
addCommand
2014-11-18 17:11:25 -05:00
# TODO: Restrict to beginner campaign levels, possibly with a CampaignOptions similar to LevelOptions
2014-11-09 14:28:00 -05:00
name: ' enter-skip-delimiters '
bindKey: ' Enter|Return '
2014-11-09 20:35:50 -05:00
exec: =>
2014-11-09 14:28:00 -05:00
if @ aceSession . selection . isEmpty ( )
cursor = @ ace . getCursorPosition ( )
line = @ aceDoc . getLine ( cursor . row )
2014-11-09 20:35:50 -05:00
if delimMatch = line . substring ( cursor . column ) . match / ^ ( [ " | ' ]? \) +;?)/ # Yay for editors misreading regexes: "
2014-11-09 14:28:00 -05:00
newRange = @ ace . getSelectionRange ( )
newRange . setStart newRange . start . row , newRange . start . column + delimMatch [ 1 ] . length
newRange . setEnd newRange . end . row , newRange . end . column + delimMatch [ 1 ] . length
@ aceSession . selection . setSelectionRange newRange
@ ace . execCommand ' insertstring ' , ' \n '
2014-11-18 17:11:25 -05:00
addCommand
name: ' disable-spaces '
bindKey: ' Space '
exec: => @ ace . execCommand ' insertstring ' , ' ' unless LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . disableSpaces
2014-11-22 00:02:35 -05:00
addCommand
name: ' throttle-backspaces '
bindKey: ' Backspace '
exec: =>
# Throttle the backspace speed
# Slow to 500ms when whitespace at beginning of line is first encountered
# Slow to 100ms for remaining whitespace at beginning of line
# Rough testing showed backspaces happen at 150ms when tapping.
# Backspace speed varies by system when holding, 30ms on fastest Macbook setting.
unless CampaignOptions ? . getOption ? ( @ options ? . level ? . get ? ( ' slug ' ) , ' backspaceThrottle ' )
@ ace . remove " left "
return
2014-11-23 15:18:42 -05:00
2014-11-22 00:02:35 -05:00
nowDate = Date . now ( )
if @ aceSession . selection . isEmpty ( )
cursor = @ ace . getCursorPosition ( )
line = @ aceDoc . getLine ( cursor . row )
if /^\s*$/ . test line . substring ( 0 , cursor . column )
@ backspaceThrottleMs ? = 500
# console.log "SpellView @backspaceThrottleMs=#{@backspaceThrottleMs}"
# console.log 'SpellView lastBackspace diff', nowDate - @lastBackspace if @lastBackspace?
if not @ lastBackspace ? or nowDate - @ lastBackspace > @ backspaceThrottleMs
@backspaceThrottleMs = 100
@lastBackspace = nowDate
@ ace . remove " left "
return
@backspaceThrottleMs = null
@lastBackspace = nowDate
@ ace . remove " left "
2014-11-23 15:18:42 -05:00
2014-11-22 00:02:35 -05:00
2014-01-03 13:32:13 -05:00
fillACE: ->
@ ace . setValue @ spell . source
2014-08-11 15:34:02 -04:00
@ aceSession . setUndoManager ( new UndoManager ( ) )
2014-01-03 13:32:13 -05:00
@ ace . clearSelection ( )
2014-11-30 14:47:51 -05:00
lockDefaultCode: (force=false) ->
# TODO: Lock default indent for an empty line?
return unless LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . lockDefaultCode or CampaignOptions ? . getOption ? ( @ options ? . level ? . get ? ( ' slug ' ) , ' lockDefaultCode ' )
return unless @ spell . source is @ spell . originalSource or force
console . info ' Locking down default code. '
intersects = =>
return true for range in @ readOnlyRanges when @ ace . getSelectionRange ( ) . intersects ( range )
false
2014-12-04 14:04:04 -05:00
2014-11-30 14:47:51 -05:00
intersectsLeft = =>
leftRange = @ ace . getSelectionRange ( ) . clone ( )
if leftRange . start . column > 0
leftRange . setStart leftRange . start . row , leftRange . start . column - 1
else if leftRange . start . row > 0
leftRange . setStart leftRange . start . row - 1 , 0
return true for range in @ readOnlyRanges when leftRange . intersects ( range )
false
2014-12-04 19:04:44 -05:00
intersectsRight = =>
rightRange = @ ace . getSelectionRange ( ) . clone ( )
if rightRange . end . column < @ aceDoc . getLine ( rightRange . end . row ) . length
rightRange . setEnd rightRange . end . row , rightRange . end . column + 1
else if rightRange . start . row < @ aceDoc . getLength ( ) - 1
rightRange . setEnd rightRange . end . row + 1 , 0
return true for range in @ readOnlyRanges when rightRange . intersects ( range )
false
2014-11-30 14:47:51 -05:00
preventReadonly = (next) ->
return true if intersects ( )
next ? ( )
interceptCommand = (obj, method, wrapper) ->
orig = obj [ method ]
obj [ method ] = ->
args = Array . prototype . slice . call arguments
wrapper => orig . apply obj , args
obj [ method ]
2014-12-04 19:33:56 -05:00
2014-12-05 17:15:40 -05:00
finishRange = (row, startRow, startColumn) =>
range = new Range startRow , startColumn , row , @ aceSession . getLine ( row ) . length - 1
range.start = @ aceDoc . createAnchor range . start
range.end = @ aceDoc . createAnchor range . end
range.end.$insertRight = true
@ readOnlyRanges . push range
# Remove previous locked code highlighting
if @ lockedCodeMarkerIDs ?
@ aceSession . removeMarker marker for marker in @ lockedCodeMarkerIDs
@lockedCodeMarkerIDs = [ ]
# Create locked default code text ranges
2014-11-30 14:47:51 -05:00
@readOnlyRanges = [ ]
2014-12-08 09:55:53 -05:00
if @ spell . language in [ ' python ' , ' coffeescript ' ]
2014-12-05 17:15:40 -05:00
# Lock contiguous section of default code
# Only works for languages without closing delimeters on blocks currently
lines = @ aceDoc . getAllLines ( )
2014-12-20 19:02:41 -05:00
for line , row in lines when not /^\s*$/ . test ( line )
lastRow = row
2014-12-05 17:15:40 -05:00
if lastRow ?
@ readOnlyRanges . push new Range 0 , 0 , lastRow , lines [ lastRow ] . length - 1
# TODO: Highlighting does not work for multiple ranges
# TODO: Everything looks correct except the actual result.
# TODO: https://github.com/codecombat/codecombat/issues/1852
# else
# # Create a read-only range for each chunk of text not separated by an empty line
# startRow = startColumn = null
# for row in [0...@aceSession.getLength()]
# unless /^\s*$/.test @aceSession.getLine(row)
# unless startRow? and startColumn?
# startRow = row
# startColumn = 0
# else
# if startRow? and startColumn?
# finishRange row - 1, startRow, startColumn
# startRow = startColumn = null
# if startRow? and startColumn?
# finishRange @aceSession.getLength() - 1, startRow, startColumn
# Highlight locked ranges
for range in @ readOnlyRanges
@ lockedCodeMarkerIDs . push @ aceSession . addMarker range , ' locked-code ' , ' fullLine '
2014-11-30 14:47:51 -05:00
# Override write operations that intersect with default code
interceptCommand @ ace , ' onPaste ' , preventReadonly
interceptCommand @ ace , ' onCut ' , preventReadonly
# TODO: can we use interceptCommand for this too? 'exec' and 'onExec' did not work.
@ ace . commands . on ' exec ' , (e) =>
e . stopPropagation ( )
e . preventDefault ( )
if e . command . name is ' insertstring ' and intersects ( )
@ zatanna ? . off ? ( )
return false
if e . command . name in [ ' Backspace ' , ' throttle-backspaces ' ] and intersectsLeft ( )
@ zatanna ? . off ? ( )
return false
2014-12-04 19:04:44 -05:00
if e . command . name is ' del ' and intersectsRight ( )
@ zatanna ? . off ? ( )
return false
2014-11-30 14:47:51 -05:00
if e . command . name in [ ' enter-skip-delimiters ' , ' Enter ' , ' Return ' ]
if intersects ( )
2014-12-03 01:13:55 -05:00
e . editor . navigateDown 1
e . editor . navigateLineStart ( )
2014-11-30 14:47:51 -05:00
return false
2014-12-03 01:13:55 -05:00
else if e . command . name in [ ' Enter ' , ' Return ' ] and not e . editor ? . completer ? . popup ? . isOpen
@ zatanna ? . on ? ( )
return e . editor . execCommand ' enter-skip-delimiters '
2014-11-30 14:47:51 -05:00
@ zatanna ? . on ? ( )
e . command . exec e . editor , e . args or { }
2014-10-07 18:08:27 -04:00
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()
2014-11-20 15:41:53 -05:00
popupFontSizePx = CampaignOptions . getOption ( @ options . level . get ( ' slug ' ) , ' autocompleteFontSizePx ' ) ? 16
2014-10-07 18:08:27 -04:00
@zatanna = new Zatanna @ ace ,
basic: false
liveCompletion: false
snippetsLangDefaults: false
completers:
keywords: false
2014-10-23 19:23:46 -04:00
snippets: @ autocomplete
text: @ autocomplete
2014-10-08 17:22:11 -04:00
autoLineEndings:
javascript: ' ; '
2014-11-20 15:41:53 -05:00
popupFontSizePx: popupFontSizePx
2014-10-23 14:16:58 -04:00
popupWidthPx: 380
2014-10-07 18:08:27 -04:00
updateAutocomplete: (@autocomplete) ->
@ zatanna ? . set ' snippets ' , @ autocomplete
2014-06-13 11:12:55 -04:00
addZatannaSnippets: (e) ->
2014-10-22 15:24:28 -04:00
# Snippet entry format:
# content: code inserted into document
# meta: displayed right-justfied in popup
# name: displayed left-justified in popup, and what's being matched
# tabTrigger: fallback for name field
2014-10-07 18:08:27 -04:00
return unless @ zatanna and @ autocomplete
2014-06-13 11:12:55 -04:00
snippetEntries = [ ]
2014-10-17 00:38:11 -04:00
for group , props of e . propGroups
2014-06-13 11:12:55 -04:00
for prop in props
2014-10-17 00:38:11 -04:00
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) ->
2014-06-13 11:12:55 -04:00
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 ]
2014-11-22 15:29:34 -05:00
content = doc . snippets [ e . language ] . code
if /loop/ . test ( content ) and LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . moveRightLoopSnippet
# Replace default loop snippet with an embedded moveRight()
content = switch e . language
when ' python ' then ' loop: \n self.moveRight() \n ${1:} '
when ' javascript ' then ' loop { \n this.moveRight(); \n ${1:} \n } '
else content
2014-06-24 13:17:38 -04:00
entry =
2014-11-22 15:29:34 -05:00
content: content
2014-12-03 01:13:55 -05:00
meta: ' press enter '
2014-06-13 11:12:55 -04:00
name: doc . name
2014-07-02 14:52:16 -04:00
tabTrigger: doc . snippets [ e . language ] . tab
2014-12-17 20:10:07 -05:00
if doc . name is ' findNearestEnemy ' or doc . name is ' findNearest '
2014-10-24 14:38:48 -04:00
# Remember if we have findNearestEnemy so attack snippet can be updated
haveFindNearestEnemy = true
if doc . name is ' attack '
# Postpone this until we know if findNearestEnemy is available
attackEntry = entry
else
snippetEntries . push entry
# TODO: Generalize this snippet replacement
# TODO: Where should this logic live, and what format should it be in?
if attackEntry ?
unless haveFindNearestEnemy
# No findNearestEnemy, so update attack snippet to string-based target
attackEntry.content = attackEntry . content . replace ' ${1:enemy} ' , ' " ${1:Enemy Name} " '
snippetEntries . push attackEntry
2014-06-13 11:12:55 -04:00
2014-08-14 19:56:01 -04:00
# window.zatannaInstance = @zatanna
2014-07-02 14:52:16 -04:00
# window.snippetEntries = snippetEntries
2014-11-26 15:33:29 -05:00
lang = SpellView . editModes [ e . language ] . substr ' ace/mode/ ' . length
2014-07-02 14:52:16 -04:00
@ zatanna . addSnippets snippetEntries , lang
2014-06-13 11:12:55 -04:00
2014-02-11 18:38:36 -05:00
onMultiplayerChanged: ->
2014-02-22 15:01:05 -05:00
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 ( ' ' )
2014-08-11 15:34:02 -04:00
@ 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
2014-08-11 15:34:02 -04:00
@ aceSession . setUndoManager ( new UndoManager ( ) )
2014-01-03 13:32:13 -05:00
@ ace . clearSelection ( )
2014-04-03 21:43:29 -04:00
@ onAllLoaded ( )
2014-01-03 13:32:13 -05:00
2014-04-03 21:43:29 -04: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
2014-01-19 10:08:28 -05:00
createDebugView: ->
2014-11-10 15:47:24 -05:00
return if @ options . level . get ( ' type ' , true ) in [ ' hero ' , ' hero-ladder ' , ' hero-coop ' ] # We'll turn this on later, maybe, but not yet.
2014-05-06 13:06:32 -04:00
@debugView = new SpellDebugView ace: @ ace , thang: @ thang , spell : @ spell
2014-01-19 10:08:28 -05:00
@ $el . append @ debugView . render ( ) . $el . hide ( )
2014-01-21 12:03:04 -05:00
createToolbarView: ->
@toolbarView = new SpellToolbarView ace: @ ace
2014-01-25 18:11:29 -05:00
@ $el . append @ toolbarView . render ( ) . $el
2014-01-21 12:03:04 -05:00
onMouseOut: (e) ->
2014-09-23 14:39:56 -04:00
@ debugView ? . onMouseOut e
2014-01-21 12:03:04 -05:00
2014-01-03 13:32:13 -05:00
getSource: ->
@ ace . getValue ( ) # could also do @firepad.getText()
setThang: (thang) ->
@ focus ( )
2014-10-16 15:08:21 -04:00
@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 ]
2014-01-24 16:03:04 -05:00
@ createDebugView ( ) unless @ debugView
2014-09-23 14:39:56 -04:00
@ debugView ? . thang = @ thang
2014-01-31 19:16:59 -05:00
@ toolbarView ? . toggleFlow false
2014-05-10 21:24:50 -04:00
@ updateAether false , false
2014-06-13 11:12:55 -04:00
# @addZatannaSnippets()
2014-01-03 13:32:13 -05:00
@ highlightCurrentLine ( )
2014-08-23 00:35:08 -04:00
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
2014-08-27 15:24:03 -04:00
Backbone . Mediator . publish ' tome:editing-ended ' , { }
2014-01-03 13:32:13 -05:00
notifyEditingBegan: =>
return if @ aceDoc . undergoingFirepadOperation # from my Firepad ACE adapter
2014-08-27 15:24:03 -04:00
Backbone . Mediator . publish ' tome:editing-began ' , { }
2014-01-03 13:32:13 -05:00
2014-10-16 15:08:21 -04: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 )
2014-11-07 19:04:35 -05:00
lines = Math . max 8 , Math . min ( screenLineCount + 2 , linesAtMaxHeight )
# 2 lines buffer is nice
2014-10-16 15:08:21 -04:00
@ ace . setOptions minLines: lines , maxLines: lines
2014-11-07 19:04:35 -05:00
$ ( ' # spell-palette-view ' ) . css ( ' top ' , 175 + lineHeight * lines ) # Move spell palette up, slightly overlapping us.
2014-10-16 15:08:21 -04:00
2014-11-09 14:36:17 -05:00
hideProblemAlert: ->
Backbone . Mediator . publish ' tome:hide-problem-alert ' , { }
2014-01-03 13:32:13 -05:00
onManualCast: (e) ->
cast = @ $el . parent ( ) . length
2014-08-23 00:35:08 -04:00
@ recompile cast , e . realTime
2014-01-03 13:32:13 -05:00
@ focus ( ) if cast
onCodeReload: (e) ->
2014-11-19 18:24:50 -05:00
return unless e . spell is @ spell or not e . spell
2014-01-03 13:32:13 -05:00
@ reloadCode true
2014-10-30 01:29:36 -04:00
@ ace . clearSelection ( )
2014-11-13 13:49:37 -05:00
_ . delay ( => @ ace ? . clearSelection ( ) ) , 500 # Make double sure this gets done (saw some timing issues?)
2014-01-03 13:32:13 -05:00
reloadCode: (cast=true) ->
@ updateACEText @ spell . originalSource
2014-11-30 14:47:51 -05:00
@ lockDefaultCode true
2014-01-03 13:32:13 -05:00
@ recompile cast
2014-10-25 17:00:54 -04:00
Backbone . Mediator . publish ' tome:spell-loaded ' , spell: @ spell
2014-12-04 14:04:04 -05:00
@ updateLines ( )
2014-01-03 13:32:13 -05:00
2014-02-12 15:41:41 -05:00
recompileIfNeeded: =>
@ recompile ( ) if @ recompileNeeded
2014-08-23 00:35:08 -04:00
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
2014-11-05 21:06:21 -05:00
if cast #and (hasChanged or realTime) # just always cast now
2014-08-23 17:31:38 -04:00
@ 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
2014-08-11 15:34:02 -04:00
@ aceSession . setUndoManager ( new UndoManager ( ) )
2014-01-03 13:32:13 -05:00
@eventsSuppressed = false
2014-06-25 23:19:11 -04:00
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
2014-10-16 15:08:21 -04:00
_ . throttle @ updateLines , 500
2014-11-09 14:36:17 -05:00
_ . throttle @ hideProblemAlert , 500
2014-01-03 13:32:13 -05:00
]
2014-11-19 16:23:55 -05:00
onSignificantChange . push _ . debounce @ checkRequiredCode , 750 if LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . requiredCode
onSignificantChange . push _ . debounce @ checkSuspectCode , 750 if LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . suspectCode
2014-01-03 13:32:13 -05:00
@onCodeChangeMetaHandler = =>
return if @ eventsSuppressed
2014-09-05 15:39:30 -04:00
Backbone . Mediator . publish ' audio-player:play-sound ' , trigger: ' code-change ' , volume: 0.5
2014-10-17 00:38:11 -04:00
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
2014-05-11 20:42:32 -04:00
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
2014-05-11 20:42:32 -04:00
# * 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.
2014-05-11 20:42:32 -04:00
# * 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 ( )
2014-04-22 11:54:35 -04:00
@ 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
2014-05-01 20:41:06 -04:00
codeIsAsCast = castAether and source is castAether . raw
2014-04-22 11:54:35 -04:00
aether = castAether if codeIsAsCast
return if not needsUpdate and aether is @ displayedAether
2014-04-23 19:44:29 -04:00
2014-04-22 11:54:35 -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) =>
2014-07-14 14:05:51 -04:00
@ displayAether aether , codeIsAsCast
2014-04-23 19:44:29 -04:00
@lastUpdatedAetherSpellThang = @ spellThang
@ guessWhetherFinished aether if fromCodeChange
2014-04-22 11:54:35 -04:00
@ clearAetherDisplay ( )
2014-04-23 19:44:29 -04:00
if codeHasChangedSignificantly and not codeIsAsCast
2014-09-29 02:24:18 -04:00
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
2014-07-14 14:05:51 -04:00
displayAether: (aether, isCast=false) ->
2014-02-03 16:58:25 -05:00
@displayedAether = aether
2014-07-19 23:26:13 -04:00
isCast = isCast or not _ . isEmpty ( aether . metrics ) or _ . some aether . getAllProblems ( ) , { type: ' runtime ' }
2014-05-11 20:42:32 -04:00
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 = [ ]
2014-02-24 17:12:41 -05:00
seenProblemKeys = { }
2014-01-03 13:32:13 -05:00
for aetherProblem , problemIndex in aether . getAllProblems ( )
2014-02-27 23:01:27 -05:00
continue if key = aetherProblem . userInfo ? . key and key of seenProblemKeys
seenProblemKeys [ key ] = true if key
2014-11-07 00:43:39 -05:00
@ problems . push problem = new Problem aether , aetherProblem , @ ace , isCast , @ spell . levelID
if isCast and problemIndex is 0
if problem . aetherProblem . range ?
2014-11-09 00:51:54 -05:00
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
2014-10-17 18:52:57 -04:00
@ 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 '
2014-02-22 21:02:58 -05:00
@ 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
2014-02-22 19:58:54 -05:00
@ ace . resize ( )
2014-01-03 13:32:13 -05:00
2014-10-17 18:52:57 -04:00
saveUserCodeProblem: (aether, aetherProblem) ->
# Skip duplicate problems
hashValue = aether . raw + aetherProblem . message
return if hashValue of @ savedProblems
@ savedProblems [ hashValue ] = true
2014-12-08 09:55:53 -05:00
if Math . random ( ) > 0.01
# Let's only save a tiny fraction of these during HoC to reduce writes.
return
2014-10-17 18:52:57 -04:00
# 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
2014-10-24 17:05:51 -04:00
if aetherProblem . message
@ userCodeProblem . set ' errMessage ' , aetherProblem . message
# Save error message without 'Line N: ' prefix
messageNoLineInfo = aetherProblem . message
if lineInfoMatch = messageNoLineInfo . match / ^ Line [ 0 - 9 ] + \ : /
messageNoLineInfo = messageNoLineInfo . slice ( lineInfoMatch [ 0 ] . length )
@ userCodeProblem . set ' errMessageNoLineInfo ' , messageNoLineInfo
2014-10-17 18:52:57 -04:00
@ 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
2014-05-01 20:41:06 -04:00
# 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 ( )
2014-10-30 01:29:36 -04:00
currentLine = _ . string . rtrim ( @ aceDoc . $lines [ cursorPosition . row ] . replace ( @ singleLineCommentRegex ( ) , ' ' ) ) # 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
2014-10-14 13:39:13 -04:00
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
2014-10-14 13:39:13 -04:00
if valid and ( endOfLine or beginningOfLine ) and not incompleteThis
2014-05-10 21:24:50 -04:00
if @ autocastDelay > 60000
2014-05-11 20:42:32 -04:00
@ preload ( )
2014-05-10 21:24:50 -04:00
else
@ recompile ( )
2014-05-11 20:42:32 -04:00
2014-10-30 01:29:36 -04:00
singleLineCommentRegex: ->
return @ _singleLineCommentRegex if @ _singleLineCommentRegex
commentStarts =
javascript: ' // '
python: ' # '
coffeescript: ' # '
clojure: ' ; '
lua: ' -- '
io: ' // '
commentStart = commentStarts [ @ spell . language ] or ' // '
@ _singleLineCommentRegexp ? = new RegExp " [ \t ]* #{ commentStart } [^ \" ' \n ]* " , ' g '
@ _singleLineCommentRegexp
2014-05-11 20:42:32 -04:00
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
2014-01-15 14:58:39 -05:00
onSpellChanged: (e) ->
2014-01-16 11:13:14 -05:00
@spellHasChanged = true
onSessionWillSave: (e) ->
2014-01-25 18:11:29 -05:00
return unless @ spellHasChanged
2014-01-16 13:10:27 -05:00
setTimeout ( =>
2014-08-23 00:35:08 -04:00
unless @ destroyed or @ spellHasChanged
2014-01-16 13:10:27 -05:00
@ $el . find ( ' .save-status ' ) . finish ( ) . show ( ) . fadeOut ( 2000 )
, 1000 )
2014-01-16 11:13:14 -05:00
@spellHasChanged = false
2014-01-15 14:58:39 -05:00
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
2014-04-22 11:54:35 -04:00
@ 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
2014-05-26 21:45:00 -04: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
2014-05-26 21:45:00 -04:00
@ 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) ->
2014-02-05 18:16:59 -05:00
@ 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
2014-02-05 18:16:59 -05:00
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
2014-02-05 18:16:59 -05:00
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
2014-08-25 00:39:34 -04:00
@ 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) ->
2014-03-13 18:35:28 -04:00
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) ->
2014-05-14 18:29:55 -04:00
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
2014-01-21 12:03:04 -05:00
onStatementIndexUpdated: (e) ->
return unless e . ace is @ ace
@ highlightCurrentLine ( )
2014-01-03 13:32:13 -05:00
highlightCurrentLine: (flow) =>
2014-01-21 12:03:04 -05:00
# 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
2014-10-23 19:36:59 -04:00
return unless flow and @ thang
2014-01-03 13:32:13 -05:00
executed = [ ]
2014-03-11 18:47:27 -04:00
executedRows = { }
2014-01-03 13:32:13 -05:00
matched = false
2014-01-21 12:03:04 -05:00
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 [ ]
2014-01-19 10:08:28 -05:00
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
2014-01-21 12:03:04 -05:00
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 ] = ' '
2014-10-08 01:28:53 -04:00
lastExecuted = _ . last executed
2014-11-10 15:47:24 -05:00
showToolbarView = executed . length and @ spellThang . castAether . metrics . statementsExecuted > 3 and not LevelOptions [ @ options . level . get ( ' slug ' ) ] ? . hidesCodeToolbar # Hide for a while
2014-11-23 22:40:50 -05:00
showToolbarView = false # TODO: fix toolbar styling in new design to have some space for it
2014-10-08 01:28:53 -04:00
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
2014-09-23 14:39:56 -04:00
@ debugView ? . setVariableStates { }
2014-01-03 13:32:13 -05:00
marked = { }
2014-02-24 11:59:50 -05:00
gotVariableStates = false
2014-10-08 01:28:53 -04:00
for state , i in lastExecuted ? [ ]
2014-02-03 16:58:25 -05:00
[ 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 '
2014-01-24 20:48:11 -05:00
continue if marked [ start . row ]
marked [ start . row ] = true
2014-06-30 22:16:26 -04:00
markerType = ' fullLine '
2014-01-19 10:08:28 -05:00
else
2014-09-23 14:39:56 -04:00
@ debugView ? . setVariableStates state . variables
2014-02-24 11:59:50 -05:00
gotVariableStates = true
2014-06-30 22:16:26 -04:00
markerType = ' text '
2014-02-03 16:58:25 -05:00
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
2014-01-24 20:48:11 -05:00
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
2014-09-23 14:39:56 -04:00
@ debugView ? . setVariableStates { } unless gotVariableStates
2014-01-24 11:01:00 -05:00
null
2014-01-03 13:32:13 -05:00
2014-02-20 19:14:31 -05:00
highlightComments: ->
2014-07-11 21:07:00 -04:00
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
2014-03-23 19:00:23 -04:00
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 ] )
2014-03-23 19:00:23 -04:00
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: ->
2014-10-10 16:36:13 -04:00
# @ is the gutter element
2014-11-08 01:46:12 -05:00
Backbone . Mediator . publish ' tome:jiggle-problem-alert ' , { }
2014-01-03 13:32:13 -05:00
2014-11-13 13:54:50 -05:00
onGutterClick: =>
@ ace . clearSelection ( )
2014-01-03 13:32:13 -05:00
onDisableControls: (e) -> @ toggleControls e , false
onEnableControls: (e) -> @ toggleControls e , @ writable
toggleControls: (e, enabled) ->
2014-11-21 12:53:38 -05:00
return if @ destroyed
2014-01-03 13:32:13 -05:00
return if e ? . controls and not ( ' editor ' in e . controls )
return if enabled is @ controlsEnabled
@controlsEnabled = enabled and @ writable
disabled = not enabled
2014-11-21 14:07:46 -05:00
wasFocused = @ ace . isFocused ( )
2014-01-03 13:32:13 -05:00
@ 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 ( )
2014-11-21 14:07:46 -05:00
$ ( ' body ' ) . focus ( ) if disabled and wasFocused
2014-01-03 13:32:13 -05:00
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
2014-02-22 19:58:54 -05:00
onSpellBeautify: (e) ->
2014-02-22 20:18:58 -05:00
return unless @ spellThang and ( @ ace . isFocused ( ) or e . spell is @ spell )
2014-02-22 19:58:54 -05:00
ugly = @ getSource ( )
pretty = @ spellThang . aether . beautify ugly
@ ace . setValue pretty
2014-08-27 21:43:17 -04:00
onMaximizeToggled: (e) ->
2014-10-16 15:08:21 -04:00
_ . 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-08-27 21:43:17 -04:00
2014-03-13 21:49:58 -04:00
onChangeEditorConfig: (e) ->
2014-03-28 09:42:08 -04:00
aceConfig = me . get ( ' aceConfig ' ) ? { }
2014-03-15 18:14:57 -04:00
@ ace . setDisplayIndentGuides aceConfig . indentGuides # default false
2014-03-16 21:14:04 -04:00
@ ace . setShowInvisibles aceConfig . invisibles # default false
2014-03-15 18:14:57 -04:00
@ ace . setKeyboardHandler @ keyBindings [ aceConfig . keyBindings ? ' default ' ]
2014-10-07 18:08:27 -04:00
@ updateAutocomplete ( aceConfig . liveCompletion ? false )
2014-03-13 21:49:58 -04:00
2014-03-16 21:14:04 -04:00
onChangeLanguage: (e) ->
2014-06-18 01:17:44 -04:00
return unless @ spell . canWrite ( )
2014-11-26 15:33:29 -05:00
@ aceSession . setMode SpellView . editModes [ e . language ]
@ zatanna ? . set ' language ' , SpellView . editModes [ e . language ] . substr ( ' ace/mode/ ' )
2014-06-18 01:17:44 -04:00
wasDefault = @ getSource ( ) is @ spell . originalSource
@ spell . setLanguage e . language
@ reloadCode true if wasDefault
2014-03-16 21:14:04 -04:00
2014-08-04 17:02:54 -04:00
onInsertSnippet: (e) ->
2014-08-05 05:19:15 -04:00
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 ?
2014-08-04 17:02:54 -04:00
snippetManager = ace . require ( ' ace/snippets ' ) . snippetManager
2014-08-05 05:19:15 -04:00
snippetManager . insertSnippet @ ace , snippetCode
2014-08-04 17:02:54 -04:00
return
2014-01-03 13:32:13 -05:00
dismiss: ->
2014-04-22 11:54:35 -04:00
@ spell . hasChangedSignificantly @ getSource ( ) , null , (hasChanged) =>
@ recompile ( ) if hasChanged
2014-04-23 19:44:29 -04:00
2014-08-14 16:29:57 -04:00
onScriptStateChange: (e) ->
@scriptRunning = if e . currentScript is null then false else true
2014-11-05 20:47:23 -05:00
onPlaybackEndedChanged: (e) ->
$ ( @ ace ? . container ) . toggleClass ' playback-ended ' , e . ended
2014-10-30 01:29:36 -04:00
checkRequiredCode: =>
return if @ destroyed
source = @ getSource ( ) . replace @ singleLineCommentRegex ( ) , ' '
2014-11-19 16:23:55 -05:00
requiredCodeFragments = LevelOptions [ @ options . level . get ( ' slug ' ) ] . requiredCode
for requiredCodeFragment in requiredCodeFragments
# Could make this obey regular expressions like suspectCode if needed
2014-10-30 01:29:36 -04:00
if source . indexOf ( requiredCodeFragment ) is - 1
@ warnedCodeFragments ? = { }
unless @ warnedCodeFragments [ requiredCodeFragment ]
Backbone . Mediator . publish ' tome:required-code-fragment-deleted ' , codeFragment: requiredCodeFragment
@ warnedCodeFragments [ requiredCodeFragment ] = true
2014-11-19 16:23:55 -05:00
checkSuspectCode: =>
return if @ destroyed
source = @ getSource ( ) . replace @ singleLineCommentRegex ( ) , ' '
suspectCodeFragments = LevelOptions [ @ options . level . get ( ' slug ' ) ] . suspectCode
2014-11-23 15:18:42 -05:00
detectedSuspectCodeFragmentNames = [ ]
2014-11-19 16:23:55 -05:00
for suspectCodeFragment in suspectCodeFragments
if suspectCodeFragment . pattern . test source
@ warnedCodeFragments ? = { }
unless @ warnedCodeFragments [ suspectCodeFragment . name ]
Backbone . Mediator . publish ' tome:suspect-code-fragment-added ' , codeFragment: suspectCodeFragment . name , codeLanguage: @ spell . language
2014-11-23 15:18:42 -05:00
@ warnedCodeFragments [ suspectCodeFragment . name ] = true
detectedSuspectCodeFragmentNames . push suspectCodeFragment . name
for lastDetectedSuspectCodeFragmentName in @ lastDetectedSuspectCodeFragmentNames ? [ ]
unless lastDetectedSuspectCodeFragmentName in detectedSuspectCodeFragmentNames
Backbone . Mediator . publish ' tome:suspect-code-fragment-deleted ' , codeFragment: lastDetectedSuspectCodeFragmentName , codeLanguage: @ spell . language
@lastDetectedSuspectCodeFragmentNames = detectedSuspectCodeFragmentNames
2014-11-19 16:23:55 -05:00
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-11-13 13:54:50 -05:00
$ ( @ ace ? . container ) . find ( ' .ace_gutter ' ) . off ' click ' , @ onGutterClick
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
2014-08-29 21:02:29 -04:00
@ destroyAceEditor ( @ ace )
2014-01-30 18:03:55 -05:00
@ debugView ? . destroy ( )
2014-10-16 15:08:21 -04:00
$ ( window ) . off ' resize ' , @ onWindowResize
2014-02-14 13:57:47 -05:00
super ( )