mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 08:41:46 -05:00
d14bea3110
Adding up to 10 highest importance hero. duplicate autocomplete entries that include ‘hero.’ Increasing max suggestions allowed from 10 to 20
337 lines
15 KiB
CoffeeScript
337 lines
15 KiB
CoffeeScript
utils = require 'core/utils'
|
|
|
|
defaults =
|
|
autoLineEndings:
|
|
# Mapping ace mode language to line endings to automatically insert
|
|
# E.g. javascript: ";"
|
|
{}
|
|
basic: true
|
|
snippetsLangDefaults: true
|
|
liveCompletion: true
|
|
language: 'javascript'
|
|
languagePrefixes: 'this.,@,self.'
|
|
completers:
|
|
snippets: true
|
|
|
|
|
|
|
|
# TODO: Should we be hooking in completers differently?
|
|
# TODO: https://github.com/ajaxorg/ace/blob/f133231df8c1f39156cc230ce31e66103ef4b1e2/lib/ace/ext/language_tools.js#L202
|
|
|
|
# TODO: Should show popup if we have a snippet match in Autocomplete.filterCompletions
|
|
# TODO: https://github.com/ajaxorg/ace/blob/695e24c41844c17fb2029f073d06338cd73ec33e/lib/ace/autocomplete.js#L449
|
|
|
|
# TODO: Create list of manual test cases
|
|
|
|
module.exports = class Zatanna
|
|
Tokenizer = ''
|
|
BackgroundTokenizer = ''
|
|
|
|
constructor: (aceEditor, options) ->
|
|
{Tokenizer} = ace.require 'ace/tokenizer'
|
|
{BackgroundTokenizer} = ace.require 'ace/background_tokenizer'
|
|
|
|
@editor = aceEditor
|
|
config = ace.require 'ace/config'
|
|
|
|
options ?= {}
|
|
|
|
defaultsCopy = _.extend {}, defaults
|
|
@options = _.merge defaultsCopy, options
|
|
|
|
|
|
#TODO: Renable option validation if we care
|
|
#validationResult = optionsValidator @options
|
|
#unless validationResult.valid
|
|
# throw new Error "Invalid Zatanna options: " + JSON.stringify(validationResult.errors, null, 4)
|
|
|
|
ace.config.loadModule 'ace/ext/language_tools', () =>
|
|
@snippetManager = ace.require('ace/snippets').snippetManager
|
|
|
|
# Prevent tabbing a selection trigging an incorrect autocomplete
|
|
# E.g. Given this.moveRight() selecting ".moveRight" from left to right and hitting tab yields this.this.moveRight()()
|
|
# TODO: Figure out how to intercept this properly
|
|
# TODO: Or, override expandSnippet command
|
|
# TODO: Or, SnippetManager's expandSnippetForSelection
|
|
@snippetManager.expandWithTab = -> return false
|
|
|
|
# Define a background tokenizer that constantly tokenizes the code
|
|
highlightRules = new (@editor.getSession().getMode().HighlightRules)()
|
|
tokenizer = new Tokenizer highlightRules.getRules()
|
|
@bgTokenizer = new BackgroundTokenizer tokenizer, @editor
|
|
aceDocument = @editor.getSession().getDocument()
|
|
@bgTokenizer.setDocument aceDocument
|
|
@bgTokenizer.start(0)
|
|
|
|
@setAceOptions()
|
|
@copyCompleters()
|
|
@activateCompleter()
|
|
@editor.commands.on 'afterExec', @doLiveCompletion
|
|
|
|
setAceOptions: () ->
|
|
aceOptions =
|
|
'enableLiveAutocompletion': @options.liveCompletion
|
|
'enableBasicAutocompletion': @options.basic
|
|
'enableSnippets': @options.completers.snippets
|
|
|
|
@editor.setOptions aceOptions
|
|
@editor.completer?.autoSelect = true
|
|
|
|
copyCompleters: () ->
|
|
@completers = {snippets: {}, text: {}, keywords: {}}
|
|
if @editor.completers?
|
|
[@completers.snippets.comp, @completers.text.comp, @completers.keywords.comp] = @editor.completers
|
|
if @options.completers.snippets
|
|
@completers.snippets = pos: 0
|
|
# Replace the default snippet completer with our custom one
|
|
@completers.snippets.comp = require('./snippets') @snippetManager, @options.autoLineEndings
|
|
|
|
activateCompleter: (comp) ->
|
|
if Array.isArray comp
|
|
@editor.completers = comp
|
|
else if typeof comp is 'string'
|
|
if @completers[comp]? and @editor.completers[@completers[comp].pos] isnt @completers[comp].comp
|
|
@editor.completers.splice(@completers[comp].pos, 0, @completers[comp].comp)
|
|
else
|
|
@editor.completers = []
|
|
for type, comparator of @completers
|
|
if @options.completers[type] is true
|
|
@activateCompleter type
|
|
|
|
addSnippets: (snippets, language) ->
|
|
@options.language = language
|
|
ace.config.loadModule 'ace/ext/language_tools', () =>
|
|
@snippetManager = ace.require('ace/snippets').snippetManager
|
|
snippetModulePath = 'ace/snippets/' + language
|
|
ace.config.loadModule snippetModulePath, (m) =>
|
|
if m?
|
|
@snippetManager.files[language] = m
|
|
@snippetManager.unregister m.snippets if m.snippets?.length > 0
|
|
@snippetManager.unregister @oldSnippets if @oldSnippets?
|
|
m.snippets = if @options.snippetsLangDefaults then @snippetManager.parseSnippetFile m.snippetText else []
|
|
m.snippets.push s for s in snippets
|
|
@snippetManager.register m.snippets
|
|
@oldSnippets = m.snippets
|
|
|
|
setLiveCompletion: (val) ->
|
|
if val is true or val is false
|
|
@options.liveCompletion = val
|
|
@setAceOptions()
|
|
|
|
set: (setting, value) ->
|
|
switch setting
|
|
when 'snippets' or 'completers.snippets'
|
|
return unless typeof value is 'boolean'
|
|
@options.completers.snippets = value
|
|
@setAceOptions()
|
|
@activateCompleter 'snippets'
|
|
when 'basic'
|
|
return unless typeof value is 'boolean'
|
|
@options.basic = value
|
|
@setAceOptions()
|
|
@activateCompleter()
|
|
when 'liveCompletion'
|
|
return unless typeof value is 'boolean'
|
|
@options.liveCompletion = value
|
|
@setAceOptions()
|
|
@activateCompleter()
|
|
when 'language'
|
|
return unless typeof value is 'string'
|
|
@options.language = value
|
|
@setAceOptions()
|
|
@activateCompleter()
|
|
when 'completers.keywords'
|
|
return unless typeof value is 'boolean'
|
|
@options.completers.keywords = value
|
|
@activateCompleter()
|
|
when 'completers.text'
|
|
return unless typeof value is 'boolean'
|
|
@options.completers.text = value
|
|
@activateCompleter()
|
|
return
|
|
|
|
on: -> @paused = false
|
|
off: -> @paused = true
|
|
|
|
doLiveCompletion: (e) =>
|
|
# console.log 'Zatanna doLiveCompletion', e
|
|
return unless @options.basic or @options.liveCompletion or @options.completers.snippets
|
|
return if @paused
|
|
|
|
TokenIterator = TokenIterator or ace.require('ace/token_iterator').TokenIterator
|
|
editor = e.editor
|
|
text = e.args or ""
|
|
hasCompleter = editor.completer and editor.completer.activated
|
|
|
|
# We don't want to autocomplete with no prefix
|
|
if e.command.name is "backspace" or e.command.name is "insertstring"
|
|
pos = editor.getCursorPosition()
|
|
token = (new TokenIterator editor.getSession(), pos.row, pos.column).getCurrentToken()
|
|
if token? and token.type not in ['comment', 'string']
|
|
prefix = @getCompletionPrefix editor
|
|
# Bake a fresh autocomplete every keystroke
|
|
editor.completer?.detach() if hasCompleter
|
|
|
|
# Only autocomplete if there's a prefix that can be matched
|
|
if (prefix)
|
|
unless (editor.completer)
|
|
|
|
# Create new autocompleter
|
|
Autocomplete = ace.require('ace/autocomplete').Autocomplete
|
|
|
|
# Overwrite "Shift-Return" to Esc + Return instead
|
|
# https://github.com/ajaxorg/ace/blob/695e24c41844c17fb2029f073d06338cd73ec33e/lib/ace/autocomplete.js#L208
|
|
# TODO: Need a better way to update this command. This is super shady.
|
|
# TODO: Shift-Return errors when Autocomplete is open, dying on this call:
|
|
# TODO: calls editor.completer.insertMatch(true) in lib/ace/autocomplete.js
|
|
if Autocomplete?.prototype?.commands?
|
|
exitAndReturn = (editor) =>
|
|
# TODO: Execute a proper Return that selects the Autocomplete if open
|
|
editor.completer.detach()
|
|
@editor.insert "\n"
|
|
Autocomplete.prototype.commands["Shift-Return"] = exitAndReturn
|
|
|
|
editor.completer = new Autocomplete()
|
|
|
|
# Disable autoInsert and show popup
|
|
editor.completer.autoSelect = true
|
|
editor.completer.autoInsert = false
|
|
editor.completer.showPopup(editor)
|
|
|
|
# Hide popup if more than 10 suggestions
|
|
# TODO: Completions aren't asked for unless we show popup, so this is super hacky
|
|
# TODO: Backspacing to yield more suggestions does not close popup
|
|
if editor.completer?.completions?.filtered?.length > 20
|
|
editor.completer.detach()
|
|
|
|
# Update popup CSS after it's been launched
|
|
# TODO: Popup has original CSS on first load, and then visibly/weirdly changes based on these updates
|
|
# TODO: Find better way to extend popup.
|
|
else if editor.completer.popup?
|
|
$('.ace_autocomplete').find('.ace_content').css('cursor', 'pointer')
|
|
$('.ace_autocomplete').css('font-size', @options.popupFontSizePx + 'px') if @options.popupFontSizePx?
|
|
$('.ace_autocomplete').css('line-height', @options.popupLineHeightPx + 'px') if @options.popupLineHeightPx?
|
|
$('.ace_autocomplete').css('width', @options.popupWidthPx + 'px') if @options.popupWidthPx?
|
|
editor.completer.popup.resize?()
|
|
|
|
# TODO: Can't change padding before resize(), but changing it afterwards clears new padding
|
|
# TODO: Figure out how to hook into events rather than using setTimeout()
|
|
# fixStuff = =>
|
|
# $('.ace_autocomplete').find('.ace_line').css('color', 'purple')
|
|
# $('.ace_autocomplete').find('.ace_line').css('padding', '20px')
|
|
# # editor.completer.popup.resize?(true)
|
|
# setTimeout fixStuff, 1000
|
|
|
|
# Update tokens for text completer
|
|
if @options.completers.text and e.command.name in ['backspace', 'del', 'insertstring', 'removetolinestart', 'Enter', 'Return', 'Space', 'Tab']
|
|
@bgTokenizer.fireUpdateEvent 0, @editor.getSession().getLength()
|
|
|
|
getCompletionPrefix: (editor) ->
|
|
# TODO: this is not used to get prefix that is passed to completer.getCompletions
|
|
# TODO: Autocomplete.gatherCompletions is using this (no regex 3rd param):
|
|
# TODO: var prefix = util.retrievePrecedingIdentifier(line, pos.column);
|
|
util = util or ace.require 'ace/autocomplete/util'
|
|
pos = editor.getCursorPosition()
|
|
line = editor.session.getLine pos.row
|
|
prefix = null
|
|
editor.completers?.forEach (completer) ->
|
|
if completer?.identifierRegexps
|
|
completer.identifierRegexps.forEach (identifierRegex) ->
|
|
if not prefix and identifierRegex
|
|
prefix = util.retrievePrecedingIdentifier line, pos.column, identifierRegex
|
|
prefix = util.retrievePrecedingIdentifier line, pos.column unless prefix?
|
|
prefix
|
|
|
|
addCodeCombatSnippets: (level, spellView, e) ->
|
|
snippetEntries = []
|
|
source = spellView.getSource()
|
|
haveFindNearestEnemy = false
|
|
haveFindNearest = false
|
|
for group, props of e.propGroups
|
|
for prop in props
|
|
if _.isString prop # organizePalette
|
|
owner = group
|
|
else # organizePaletteHero
|
|
owner = prop.owner
|
|
prop = prop.prop
|
|
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')
|
|
if doc?.snippets?[e.language]
|
|
name = doc.name
|
|
content = doc.snippets[e.language].code
|
|
if /loop/.test(content) and level.get '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
|
|
if /loop/.test(content) and level.isType('course', 'course-ladder')
|
|
# Temporary hackery to make it look like we meant while True: in our loop snippets until we can update everything
|
|
content = switch e.language
|
|
when 'python' then content.replace /loop:/, 'while True:'
|
|
when 'javascript' then content.replace /loop/, 'while (true)'
|
|
when 'lua' then content.replace /loop/, 'while true then'
|
|
when 'coffeescript' then content
|
|
else content
|
|
name = switch e.language
|
|
when 'python' then 'while True'
|
|
when 'coffeescript' then 'loop'
|
|
else 'while true'
|
|
# For now, update autocomplete to use hero instead of self/this, if hero is already used in the source.
|
|
# Later, we should make this happen all the time - or better yet update the snippets.
|
|
if /hero/.test(source) or not /(self[\.\:]|this\.|\@)/.test(source)
|
|
thisToken =
|
|
'python': /self/,
|
|
'javascript': /this/,
|
|
'lua': /self/
|
|
if thisToken[e.language] and thisToken[e.language].test(content)
|
|
content = content.replace thisToken[e.language], 'hero'
|
|
|
|
entry =
|
|
content: content
|
|
meta: $.i18n.t('keyboard_shortcuts.press_enter', defaultValue: 'press enter')
|
|
name: name
|
|
tabTrigger: doc.snippets[e.language].tab
|
|
importance: doc.autoCompletePriority ? 1.0
|
|
haveFindNearestEnemy ||= name is 'findNearestEnemy'
|
|
haveFindNearest ||= name is 'findNearest'
|
|
if name is 'attack'
|
|
# Postpone this until we know if findNearestEnemy is available
|
|
attackEntry = entry
|
|
else
|
|
snippetEntries.push entry
|
|
|
|
if doc.userShouldCaptureReturn
|
|
varName = doc.userShouldCaptureReturn.variableName ? 'result'
|
|
entry.captureReturn = switch e.language
|
|
when 'javascript' then 'var ' + varName + ' = '
|
|
#when 'lua' then 'local ' + varName + ' = ' # TODO: should we do this?
|
|
else varName + ' = '
|
|
|
|
# TODO: Generalize this snippet replacement
|
|
# TODO: Where should this logic live, and what format should it be in?
|
|
if attackEntry?
|
|
unless haveFindNearestEnemy or haveFindNearest or level.get('slug') in ['known-enemy', 'course-known-enemy']
|
|
# No findNearestEnemy, so update attack snippet to string-based target
|
|
# (On Known Enemy, we are introducing enemy2 = "Gert", so we want them to do attack(enemy2).)
|
|
attackEntry.content = attackEntry.content.replace '${1:enemy}', '"${1:Enemy Name}"'
|
|
snippetEntries.push attackEntry
|
|
|
|
# Add copied hero. entries for most important ones that start with hero.
|
|
sortedEntries = _.sortBy snippetEntries, (entry) -> -1 * parseInt(entry.importance ? 0)
|
|
for entry in sortedEntries
|
|
if entry.content?.indexOf('hero.') is 0
|
|
newEntry = _.cloneDeep(entry)
|
|
entry.name = "hero.#{newEntry.name}"
|
|
snippetEntries.push(newEntry)
|
|
break if snippetEntries.length - sortedEntries.length >= 10
|
|
|
|
if haveFindNearest and not haveFindNearestEnemy
|
|
spellView.translateFindNearest()
|
|
|
|
# window.zatannaInstance = @zatanna # For debugging. Make sure to not leave active when committing.
|
|
# window.snippetEntries = snippetEntries
|
|
lang = utils.aceEditModes[e.language].substr 'ace/mode/'.length
|
|
@addSnippets snippetEntries, lang
|
|
spellView.editorLang = lang
|