From 9c7320e26ae6e81f13b128f2a8de619e1779fefa Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 1 Jul 2016 16:39:06 -0700 Subject: [PATCH 1/3] - Move Zatanna out of own repo and into CodeCombat - Move snippet creation out of SpellView - Disable text based completions. --- app/views/play/level/tome/SpellView.coffee | 86 +-- app/views/play/level/tome/editor/fuzziac.js | 497 +++++++++++++ .../play/level/tome/editor/snippets.coffee | 244 +++++++ .../play/level/tome/editor/zatanna.coffee | 332 +++++++++ bower.json | 1 - vendor/scripts/fuzzaldrin.js | 656 ++++++++++++++++++ 6 files changed, 1732 insertions(+), 84 deletions(-) create mode 100644 app/views/play/level/tome/editor/fuzziac.js create mode 100644 app/views/play/level/tome/editor/snippets.coffee create mode 100644 app/views/play/level/tome/editor/zatanna.coffee create mode 100644 vendor/scripts/fuzzaldrin.js diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index bce6ae849..14edde8cf 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -12,6 +12,7 @@ LevelComponent = require 'models/LevelComponent' UserCodeProblem = require 'models/UserCodeProblem' utils = require 'core/utils' CodeLog = require 'models/CodeLog' +Zatanna = require './editor/zatanna' module.exports = class SpellView extends CocoView id: 'spell-view' @@ -484,7 +485,6 @@ module.exports = class SpellView extends CocoView completers: keywords: false snippets: @autocomplete - text: @autocomplete autoLineEndings: javascript: ';' popupFontSizePx: popupFontSizePx @@ -501,89 +501,9 @@ module.exports = class SpellView extends CocoView # name: displayed left-justified in popup, and what's being matched # tabTrigger: fallback for name field return unless @zatanna and @autocomplete - snippetEntries = [] - 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 @options.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 @options.level.get('type') in ['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. - source = @getSource() - 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' + @zatanna.addCodeCombatSnippets @options.level, @, e - 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 @options.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 - - if haveFindNearest and not haveFindNearestEnemy - @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 - @zatanna.addSnippets snippetEntries, lang - @editorLang = lang + translateFindNearest: -> # If they have advanced glasses but are playing a level which assumes earlier glasses, we'll adjust the sample code to use the more advanced APIs instead. diff --git a/app/views/play/level/tome/editor/fuzziac.js b/app/views/play/level/tome/editor/fuzziac.js new file mode 100644 index 000000000..0fe27a6db --- /dev/null +++ b/app/views/play/level/tome/editor/fuzziac.js @@ -0,0 +1,497 @@ +/** + * Based upon: + * A Dynamic Programming Algorithm for Name Matching + * Top, P.; Dowla, F.; Gansemer, J.; + * Sch. of Electr. & Comput. Eng., Purdue Univ., West Lafayette, IN + * + * Variation in JavaScript + * Copyright © 2011, Christopher Stoll + * @author Christopher Stoll + * + * @constructor + * @param {String} [pNameSource=''] The source name, the name of interest + * @param {Boolean} [pDebug=false] The instance is in debugging mode + * @param {String} [pDebugOutputArea=''] Where to put debuging output + */ +function fuzziac(pNameSource, pDebug, pDebugOutputArea){ + var tNameSource = pNameSource || ''; + + if(tNameSource){ + // convert "last, first" to "first last" + if(tNameSource.indexOf(',') > 0){ + var tIndex = tNameSource.indexOf(','), + tFirst = tNameSource.slice(tIndex+1), + tLast = tNameSource.slice(0, tIndex); + tNameSource = tFirst + ' ' + tLast; + } + + // all lowercase, no special characters, and no double sapces + tNameSource = tNameSource.toLowerCase(); + tNameSource = tNameSource.replace(/[.'"]/ig, ' '); + tNameSource = tNameSource.replace(/\s{2,}/g, ' '); + } + + // TODO: remove when converted for string matching only + // debug variables + this.DEBUG = pDebug || false; + this.DEBUG_AREA = pDebugOutputArea; + + // y axis in matrix, the name in question + this.nameSource = tNameSource; + this.nameSourceLength = this.nameSource.length + 1; + this.nameSourceScore = 0; + this._reset(); +} + +fuzziac.prototype = { + /** + * Reset class variables + * @private + */ + _reset: function(pNameTarget){ + var tNameTarget = pNameTarget || ''; + + // TODO: remove when converted for string matching only + if(tNameTarget){ + tNameTarget = tNameTarget.toLowerCase(); + tNameTarget = tNameTarget.replace(/[.,'"]/ig, ''); + tNameTarget = tNameTarget.replace(/\s{2,}/g, ' '); + } + + // x axis in matrix, the name to check against + this.nameTarget = tNameTarget; + this.nameTargetLength = this.nameTarget.length + 1; + this.nameTargetScore = 0; + + // DV, the dunamic programming matrix + this.dynamicMatrix = []; + + // Max value in the matrix + this.maxMatrixValue = 0; + + // the score for the string + this.overallScore = 0; + + // weighted average of string and tokens + this.finalScore = 0; + }, + + /** + * CM, character mismatch lookup, + * Abreviated 2D array for hex values + * + * @static + * @field + */ + characterMatrix: [ + //bcdefghijklmnopqrstuvwxyz + 'a0004000000000400000000000', // a + '0a000000000000000000000000', // b + '00a00000004000002000000000', // c + '000a0000000000000002000000', // d + '4000a000000000000000000020', // e + '00000a00000000020000020000', // f + '000000a0000000000000000000', // g + '0000000a040000000000000000', // h + '00000000a20400000000000020', // i + '000000042a0000000000000040', // j + '0040000000a000002000000000', // k + '00000000400a00000000000000', // l + '000000000000a4000000000000', // m + '0000000000004a000000000000', // n + '40000000000000a00000000000', // o + '000002000000000a0000000000', // p + '0020000000200000a000000000', // q + '00000000000000000a00000000', // r + '000000000000000000a0000000', // s + '0002000000000000000a000000', // t + '00000000000000000000a00000', // u + '000002000000000000000a4000', // v + '0000000000000000000004a000', // w + '00000000000000000000000a00', // x + '000020002400000000000000a0', // y + '0000000000000000002000000a', // z + '00000000000000400000000000', // 0 + '00000000400400000000000000', // 1 + '00000000000000000100000002', // 2 + '00002000000000000000000001', // 3 + '20000002000000000000000000', // 4 + '00000000000000000020000000', // 5 + '01000010000000000000000000', // 6 + '00000000100100000002000000', // 7 + '01000000000000000000000000', // 8 + '00000020000000000000000000' // 9 + ], + + /** + * Dictionary to speed lookups in the character matrix + * + * @static + * @field + */ + charMatrixDictionary: { + a: 0, + b: 1, + c: 2, + d: 3, + e: 4, + f: 5, + g: 6, + h: 7, + i: 8, + j: 9, + k: 10, + l: 11, + m: 12, + n: 13, + o: 14, + p: 15, + q: 16, + r: 17, + s: 18, + t: 19, + u: 20, + v: 21, + w: 22, + x: 23, + y: 24, + z: 25, + 0: 26, + 1: 27, + 2: 28, + 3: 29, + 4: 30, + 5: 31, + 6: 32, + 7: 33, + 8: 34, + 9: 35 + }, + + /** + * Return a matching score for two characters + * + * @private + * @param {String} pCharA The first character to test + * @param {String} pCharB The second character to test + * @returns {Number} Score for the current characters + */ + _characterScore: function(pCharA, pCharB){ + var matchScore = 10, + mismatchScore = 0, + mismatchPenalty = -4, + charIndexA = 0, + charIndexB = 0, + refValue = 0; + + if(pCharA && pCharB){ + if(pCharA == pCharB){ + return matchScore; + }else{ + charIndexA = this.charMatrixDictionary[pCharA]; + charIndexB = this.charMatrixDictionary[pCharB]; + + if(charIndexA && charIndexB){ + mismatchScore = this.characterMatrix[charIndexA][charIndexB] + refValue = parseInt(mismatchScore, 16); + + if(refValue){ + return refValue; + }else{ + return mismatchPenalty; + } + }else{ + return mismatchPenalty; + } + } + }else{ + return mismatchPenalty; + } + }, + + /** + * Return a score for string gaps + * + * @private + * @param {String} pCharA The first character to test + * @param {String} pCharB The second character to test + * @returns {Number} Score for the current characters + */ + _gappedScore: function(pCharA, pCharB){ + var gapPenalty = -3, + mismatchPenalty = -4; + + if((pCharA == ' ') || (pCharB == ' ')){ + return gapPenalty; + }else{ + return mismatchPenalty; + } + }, + + /** + * Return a score for transposed strings + * TODO: Either actuallly check for transposed characters or eliminate + * + * @private + * @param {String} pCharA The first character to test + * @param {String} pCharB The second character to test + * @returns {Number} Score for the current characters + */ + _transposedScore: function(pCharA, pCharB){ + var transposePenalty = -2; + return transposePenalty; + }, + + /** + * Build the dynamic programming matrix for the two current strings + * @private + */ + _buildMatrix: function(){ + var tmpArray = [], + tCharA = '', + tCharB = '', + gapScore = 0; + + // fill DV, the dynamic programming matrix, with zeros + for(var ix=0; ix this.dynamicMatrix[iy-1][ix-1]) && + (this.dynamicMatrix[iy][ix-1] > this.dynamicMatrix[iy-1][ix-1])){ + + this.dynamicMatrix[iy-1][ix-1] = Math.max( + this.dynamicMatrix[iy-1][ix], + this.dynamicMatrix[iy][ix-1] + ); + this.dynamicMatrix[iy][ix] = Math.max( + this.dynamicMatrix[iy-1][ix-1] + this._transposedScore(tCharA, tCharB), + this.dynamicMatrix[iy][ix] + ); + } + } + } + }, + + /** + * Backtrack through the matrix to find the best path + * @private + */ + _backtrack: function(){ + var tmaxi = 0, + maxix = 0; + + // find the intial local max + for(var ix=this.nameTargetLength-1; ix>0; ix--){ + if(this.dynamicMatrix[this.nameSourceLength-1][ix] > tmaxi){ + tmaxi = this.dynamicMatrix[this.nameSourceLength-1][ix]; + maxix = ix; + } + + // break out of loop if we have reached zeros after non zeros + if((tmaxi > 0) && (this.dynamicMatrix[this.nameSourceLength-1][ix+1] == 0)){ + break; + } + } + + if(tmaxi <= 0){ + return false; + } + + var ix = maxix, + iy = this.nameSourceLength-1, + ixLast = 0, + iyLast = 0, + diagonal = 0, + above = 0, + left = 0; + + // TODO: replace with better algo or refactor + while((iy>0) && (ix>0)){ + // store max value + if(this.dynamicMatrix[iy][ix] > this.maxMatrixValue){ + this.maxMatrixValue = this.dynamicMatrix[iy][ix]; + } + + // DEBUG + if(this.DEBUG){ + $('#'+this.DEBUG_AC+'-'+(iy+1)+'-'+(ix+1)).css('background-color','#ccc'); + } + + // calculate values for possible paths + diagonal = this.dynamicMatrix[iy-1][ix-1]; + above = this.dynamicMatrix[iy][ix-1]; + left = this.dynamicMatrix[iy-1][ix]; + + // choose next path + if((diagonal>=above) && (diagonal>=left)){ + iy--; + ix--; + }else if((above>=diagonal) && (above>=left)){ + ix--; + }else if((left>=diagonal) && (left>=above)){ + iy--; + } + + // end while if we have all zeros + if((diagonal == 0) && (above == 0) && (left == 0)){ + iy = 0; + ix = 0; + } + } + + return true; + }, + + /** + * Calculate the final match score for this pair of names + * @private + */ + _finalMatchScore: function(){ + var averageNameLength = (this.nameSourceLength + this.nameTargetLength) / 2 + this.overallScore = (2 * this.maxMatrixValue) / averageNameLength; + this.finalScore = this.overallScore / 10; + }, + + /** + * Display debug information + * TODO: remove when converted for string matching only + * + * @private + */ + _debug_ShowDVtable: function(){ + var DEBUG_AA = 0, + DEBUG_AB = ''; + DEBUG_AC = Math.round(Math.random() * 9999); + + this.DEBUG_AC = DEBUG_AC; + + DEBUG_AB += ''; + for(var iy=0; iy<=(this.nameSourceLength); iy++){ + DEBUG_AB += ''; + for(var ix=0; ix<=(this.nameTargetLength); ix++){ + if(iy==0){ + if(ix>1){ + DEBUG_AB += ''; + }else{ + DEBUG_AB += ''; + } + }else{ + if(ix>0){ + DEBUG_AA = Math.round(this.dynamicMatrix[iy-1][ix-1] * 100) / 100; + DEBUG_AB += ''; + }else{ + if(iy>1){ + DEBUG_AB += ''; + }else{ + DEBUG_AB += ''; + } + } + } + } + DEBUG_AB += ''; + } + DEBUG_AB += '
'+this.nameTarget[ix-2]+''+DEBUG_AA+''+this.nameSource[iy-2]+'
'; + $(this.DEBUG_AREA).append(DEBUG_AB); + }, + + /** + * Public method to perform a search + * + * @param {String} pNameTarget The target to compare the source with + * @returns The match score of the two strings + */ + score: function(pNameTarget){ + this._reset(pNameTarget); + + this._buildMatrix(); + + if(this.DEBUG){ + this._debug_ShowDVtable(); + } + + this._backtrack(); + this._finalMatchScore(); + return this.finalScore; + }, + + /** + * Find matches from an array of choices + * + * @param {String[]} pArray The array of strings to check against + * @param {Number} [10] pLimit The number of resutls to return + * @returns {string[]} The top matching strings + */ + topMatchesFromArray: function(pArray, pLimit){ + var tmpValue = 0, + tmpValRound = 0, + worstValue = 0, + resultLimit = pLimit || 10, + resultArray = []; + + for(var i=0; i resultArray[resultLimit-1].v){ + newObj = {v:tmpValue,n:pArray[i]}; + tmpObj = {v:0,n:''}; + for(var j=0; j resultArray[j].v){ + tmpObj.v = resultArray[j].v; + tmpObj.n = resultArray[j].n; + resultArray[j].v = newObj.v; + resultArray[j].n = newObj.n; + newObj.v = tmpObj.v; + newObj.n = tmpObj.n; + } + } + } + } + + for(var i=0; i new Fuzziac(a).score b +lineBreak = /\r\n|[\n\r\u2028\u2029]/g +identifierRegex = /[\.a-zA-Z_0-9\$\-\u00A2-\uFFFF]/ +Fuzziac = require './fuzziac' # https://github.com/stollcri/fuzziac.js + +module.exports = (SnippetManager, autoLineEndings) -> + {Range} = ace.require 'ace/range' + util = ace.require 'ace/autocomplete/util' + identifierRegexps: [identifierRegex] + + # Cleanup surrounding text + baseInsertSnippet = SnippetManager.insertSnippet + SnippetManager.insertSnippet = (editor, snippet) -> + # Remove dangling snippet prefixes + # Examples: + # "self self.moveUp()" + # "elf.self.moveUp()" + # "ssefl.moveUp()" + # "slef.moveUp()" + # TODO: This function is a mess + # TODO: Can some of this nonsense be done upstream in scrubSnippet? + cursor = editor.getCursorPosition() + line = editor.session.getLine cursor.row + if cursor.column > 0 + prevWord = util.retrievePrecedingIdentifier line, cursor.column - 1, identifierRegex + if prevWord.length > 0 + # Remove previous word if it's at the beginning of the snippet + prevWordIndex = snippet.toLowerCase().indexOf prevWord.toLowerCase() + if prevWordIndex is 0 + range = new Range cursor.row, cursor.column - 1 - prevWord.length, cursor.row, cursor.column + editor.session.remove range + else + # console.log "Zatanna cursor.column=#{cursor.column} snippet='#{snippet}' line='#{line}' prevWord='#{prevWord}'" + # console.log "Zatanna prevWordIndex=#{prevWordIndex}" + + # Lookup original completion + # TODO: Can we identify correct completer somehow? + for completer in editor.completers + if completer.completions? + for completion in completer.completions + if completion.snippet is snippet + originalCompletion = completion + break + break if originalCompletion + + if originalCompletion? + # console.log 'Zatanna original completion', originalCompletion + # Get original snippet prefix (accounting for extra '\n' and possibly autoLineEndings at end) + lang = editor.session.getMode()?.$id?.substr 'ace/mode/'.length + # console.log 'Zatanna lang', lang, autoLineEndings[lang]?.length + extraEndLength = 1 + extraEndLength += autoLineEndings[lang].length if autoLineEndings[lang]? + if snippetIndex = originalCompletion.content.indexOf snippet.substr(0, snippet.length - extraEndLength) + originalPrefix = originalCompletion.content.substring 0, snippetIndex + else + originalPrefix = '' + snippetStart = cursor.column - originalPrefix.length + # console.log "Zatanna originalPrefix='#{originalPrefix}' snippetStart=#{snippetStart}" + + if snippetStart > 0 and snippetStart <= line.length + extraIndex = snippetStart - 1 + # console.log "Zatanna prev char='#{line[extraIndex]}'" + + if line[extraIndex] is '.' + # Fuzzy string match previous word before '.', and remove if a match to beginning of snippet + originalObject = originalCompletion.content.substring(0, originalCompletion.content.indexOf('.')) + prevObjectIndex = extraIndex - 1 + # console.log "Zatanna prevObjectIndex=#{prevObjectIndex}" + if prevObjectIndex >= 0 and /\w/.test(line[prevObjectIndex]) + prevObjectIndex-- while prevObjectIndex >= 0 and /\w/.test(line[prevObjectIndex]) + prevObjectIndex++ if prevObjectIndex < 0 or not /\w/.test(line[prevObjectIndex]) + # console.log "Zatanna prevObjectIndex=#{prevObjectIndex} extraIndex=#{extraIndex}" + prevObject = line.substring prevObjectIndex, extraIndex + + #TODO: We use to use fuzziac here, but we forgot why. Using + # fuzzaldren for now. + #fuzzer = {score: (n) -> score originalObject, n} + fuzzer = new Fuzziac originalObject + finalScore = 0 + if fuzzer + finalScore = fuzzer.score prevObject + + # console.log "Zatanna originalObject='#{originalObject}' prevObject='#{prevObject}'", finalScore + if finalScore > 0.5 + range = new Range cursor.row, prevObjectIndex, cursor.row, snippetStart + editor.session.remove range + else if /^[^.]+\./.test snippet + # Remove the first part of the snippet, and use whats there. + snippet = snippet.replace /^[^.]+\./, '' + + else if /\w/.test(line[extraIndex]) + # Remove any alphanumeric characters on this line immediately before prefix + extraIndex-- while extraIndex >= 0 and /\w/.test(line[extraIndex]) + extraIndex++ if extraIndex < 0 or not /\w/.test(line[extraIndex]) + range = new Range cursor.row, extraIndex, cursor.row, snippetStart + editor.session.remove range + + #Remove anything that looks like an identifier after the completion + afterIndex = cursor.column + trailingText = line.substring afterIndex + match = trailingText.match /^[a-zA-Z_0-9]*(\(\s*\))?/ + afterIndex += match[0].length if match + afterRange = new Range cursor.row, cursor.column, cursor.row, afterIndex + editor.session.remove afterRange + + baseInsertSnippet.call @, editor, snippet + + getCompletions: (editor, session, pos, prefix, callback) -> + # console.log "Zatanna getCompletions pos.column=#{pos.column} prefix=#{prefix}" + # Completion format: + # prefix: text that will be replaced by snippet + # caption: displayed left-justified in popup, and what's being matched + # snippet: what will be inserted into document + # score: used to order autocomplete snippet suggestions + # meta: displayed right-justfied in popup + lang = session.getMode()?.$id?.substr 'ace/mode/'.length + line = session.getLine pos.row + + #If the prefix is a reserved word, don't autocomplete + keywords = session.getMode()?.$highlightRules?.$keywordList + if keywords and prefix in keywords + @completions = [] + return callback null, @completions + + word = getCurrentWord session, pos + snippetMap = SnippetManager.snippetMap + completions = [] + SnippetManager.getActiveScopes(editor).forEach (scope) -> + snippets = snippetMap[scope] or [] + for s in snippets + caption = s.name or s.tabTrigger + continue unless caption + [snippet, fuzzScore] = scrubSnippet s.content, caption, line, prefix, pos, lang, autoLineEndings, s.captureReturn + completions.push + content: s.content # Used internally by Zatanna, not by ace autocomplete + caption: caption + snippet: snippet + score: fuzzScore * s.importance ? 1.0 + meta: s.meta or (if s.tabTrigger and not s.name then s.tabTrigger + '\u21E5' else 'snippets') + , @ + # console.log 'Zatanna snippet completions', completions + @completions = completions + callback null, completions + + # TODO: This shim doesn't work because our version of ace isn't updated to this change: + # TODO: https://github.com/ajaxorg/ace/commit/7b01a4273e91985c9177f53d238d6b83fe99dc56 + # TODO: But, if it was we could use this and pass a 'completer: @' property for each completion + # insertMatch: (editor, data) -> + # console.log 'Zatanna snippets insertMatch', editor, data + # if data.snippet + # SnippetManager.insertSnippet editor, data.snippet + # else + # editor.execCommand "insertstring", data.value || data + +getCurrentWord = (doc, pos) -> + end = pos.column + start = end - 1 + text = doc.getLine(pos.row) + start-- while start >= 0 and not text[start].match /\s+|[\.\@]/ + start++ if start >= 0 + text.substring start, end + +scrubSnippet = (snippet, caption, line, input, pos, lang, autoLineEndings, captureReturn) -> + # console.log "Zatanna snippet=#{snippet} caption=#{caption} line=#{line} input=#{input} pos.column=#{pos.column} lang=#{lang}" + fuzzScore = 0.1 + # input will be replaced by snippet + # trim snippet prefix and suffix if already in the document (line) + if prefixStart = snippet.toLowerCase().indexOf(input.toLowerCase()) > -1 + snippetLines = (snippet.match(lineBreak) || []).length + captionStart = snippet.indexOf caption + + # Calculate snippet prefixes and suffixes. E.g. full snippet might be: "self." + "moveLeft" + "()" + snippetPrefix = snippet.substring 0, captionStart + snippetSuffix = snippet.substring snippetPrefix.length + caption.length + + # Calculate line prefixes and suffixes + # linePrefix: beginning portion of snippet that already exists + linePrefixIndex = pos.column - input.length - 1 + if linePrefixIndex >= 0 and snippetPrefix.length > 0 and line[linePrefixIndex] is snippetPrefix[snippetPrefix.length - 1] + snippetPrefixIndex = snippetPrefix.length - 1 + while line[linePrefixIndex] is snippetPrefix[snippetPrefixIndex] + break if linePrefixIndex is 0 or snippetPrefixIndex is 0 + linePrefixIndex-- + snippetPrefixIndex-- + linePrefix = line.substr linePrefixIndex, pos.column - input.length - linePrefixIndex + else + linePrefix = '' + lineSuffix = line.substr pos.column, snippetSuffix.length - 1 + caption.length - input.length + 1 + lineSuffix = '' if snippet.indexOf(lineSuffix) < 0 + + # TODO: This is broken for attack(find in Python, but seems ok in JavaScript. + + # Don't eat existing matched parentheses + # console.log "Zatanna checking parentheses lineSuffix=#{lineSuffix} pos.column=#{pos.column} input.length=#{input.length}, prevChar=#{line[pos.column - input.length - 1]} line.length=#{line.length} nextChar=#{line[pos.column]}" + if pos.column - input.length >= 0 and line[pos.column - input.length - 1] is '(' and pos.column < line.length and line[pos.column] is ')' and lineSuffix is ')' + lineSuffix = '' + + # Score match before updating snippet + fuzzScore += score snippet, linePrefix + input + lineSuffix + + # Update snippet based on surrounding document/line + snippet = snippet.slice snippetPrefix.length if snippetPrefix.length > 0 and snippetPrefix is linePrefix + snippet = snippet.slice 0, snippet.length - lineSuffix.length if lineSuffix.length > 0 + + # Append automatic line ending and newline + # If at end of line + # And, no parentheses are before snippet. E.g. 'if (' + # And, line doesn't start with whitespace followed by 'if ' or 'elif ' + # console.log "Zatanna autoLineEndings linePrefixIndex='#{linePrefixIndex}'" + if lineSuffix.length is 0 and /^\s*$/.test line.slice pos.column + # console.log 'Zatanna atLineEnd', pos.column, lineSuffix.length, line.slice(pos.column + lineSuffix.length), line + toLinePrefix = line.substring 0, linePrefixIndex + if linePrefixIndex < 0 or linePrefixIndex >= 0 and not /[\(\)]/.test(toLinePrefix) and not /^[ \t]*(?:if\b|elif\b)/.test(toLinePrefix) + snippet += autoLineEndings[lang] if snippetLines is 0 and autoLineEndings[lang] + snippet += "\n" if snippetLines is 0 and not /\$\{/.test(snippet) + + if captureReturn and /^\s*$/.test(toLinePrefix) + snippet = captureReturn + linePrefix + snippet + + # console.log "Zatanna snippetPrefix=#{snippetPrefix} linePrefix=#{linePrefix} snippetSuffix=#{snippetSuffix} lineSuffix=#{lineSuffix} snippet=#{snippet} score=#{fuzzScore}" + else + fuzzScore += score snippet, input + + startsWith = (string, searchString, position) -> + position = position or 0 + return string.substr(position, searchString.length) is searchString + + # Prefixing is twice as good as fuzzy mathing? + fuzzScore *= 2 if startsWith(caption, input) + + # All things equal, a shorter snippet is better + fuzzScore -= caption.length / 500 + + # Exact match is really good. + fuzzScore = 10 if caption == input + + [snippet, fuzzScore] diff --git a/app/views/play/level/tome/editor/zatanna.coffee b/app/views/play/level/tome/editor/zatanna.coffee new file mode 100644 index 000000000..92ad1c22f --- /dev/null +++ b/app/views/play/level/tome/editor/zatanna.coffee @@ -0,0 +1,332 @@ +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: + keywords: true + snippets: true + text: 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 + if @options.completers.keywords + @completers.keywords = pos: 1 + + 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 or @options.completers.text + 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 > 10 + 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 = [] + 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.get('type') in ['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. + source = spellView.getSource() + 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 @options.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 + + 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 diff --git a/bower.json b/bower.json index 454979b4e..333ffe8ec 100644 --- a/bower.json +++ b/bower.json @@ -43,7 +43,6 @@ "bootstrap": "~3.2.0", "validated-backbone-mediator": "~0.1.3", "jquery.browser": "~0.0.6", - "zatanna": "https://github.com/differentmatt/zatanna.git#master", "modernizr": "~2.8.3", "backfire": "~0.3.0", "fastclick": "~1.0.3", diff --git a/vendor/scripts/fuzzaldrin.js b/vendor/scripts/fuzzaldrin.js new file mode 100644 index 000000000..13cf0abba --- /dev/null +++ b/vendor/scripts/fuzzaldrin.js @@ -0,0 +1,656 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":2}],2:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +(function () { + try { + cachedSetTimeout = setTimeout; + } catch (e) { + cachedSetTimeout = function () { + throw new Error('setTimeout is not defined'); + } + } + try { + cachedClearTimeout = clearTimeout; + } catch (e) { + cachedClearTimeout = function () { + throw new Error('clearTimeout is not defined'); + } + } +} ()) +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = cachedSetTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + cachedClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + cachedSetTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],3:[function(require,module,exports){ +(function() { + var pluckCandidates, scorer, sortCandidates; + + scorer = require('./scorer'); + + pluckCandidates = function(a) { + return a.candidate; + }; + + sortCandidates = function(a, b) { + return b.score - a.score; + }; + + module.exports = function(candidates, query, queryHasSlashes, _arg) { + var candidate, key, maxResults, score, scoredCandidates, string, _i, _len, _ref; + _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults; + if (query) { + scoredCandidates = []; + for (_i = 0, _len = candidates.length; _i < _len; _i++) { + candidate = candidates[_i]; + string = key != null ? candidate[key] : candidate; + if (!string) { + continue; + } + score = scorer.score(string, query, queryHasSlashes); + if (!queryHasSlashes) { + score = scorer.basenameScore(string, query, score); + } + if (score > 0) { + scoredCandidates.push({ + candidate: candidate, + score: score + }); + } + } + scoredCandidates.sort(sortCandidates); + candidates = scoredCandidates.map(pluckCandidates); + } + if (maxResults != null) { + candidates = candidates.slice(0, maxResults); + } + return candidates; + }; + +}).call(this); + +},{"./scorer":6}],4:[function(require,module,exports){ +(function() { + var PathSeparator, SpaceRegex, filter, matcher, scorer; + + scorer = require('./scorer'); + + filter = require('./filter'); + + matcher = require('./matcher'); + + PathSeparator = require('path').sep; + + SpaceRegex = /\ /g; + + module.exports = { + filter: function(candidates, query, options) { + var queryHasSlashes; + if (query) { + queryHasSlashes = query.indexOf(PathSeparator) !== -1; + query = query.replace(SpaceRegex, ''); + } + return filter(candidates, query, queryHasSlashes, options); + }, + score: function(string, query) { + var queryHasSlashes, score; + if (!string) { + return 0; + } + if (!query) { + return 0; + } + if (string === query) { + return 2; + } + queryHasSlashes = query.indexOf(PathSeparator) !== -1; + query = query.replace(SpaceRegex, ''); + score = scorer.score(string, query); + if (!queryHasSlashes) { + score = scorer.basenameScore(string, query, score); + } + return score; + }, + match: function(string, query) { + var baseMatches, index, matches, queryHasSlashes, seen, _i, _ref, _results; + if (!string) { + return []; + } + if (!query) { + return []; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + queryHasSlashes = query.indexOf(PathSeparator) !== -1; + query = query.replace(SpaceRegex, ''); + matches = matcher.match(string, query); + if (!queryHasSlashes) { + baseMatches = matcher.basenameMatch(string, query); + matches = matches.concat(baseMatches).sort(function(a, b) { + return a - b; + }); + seen = null; + index = 0; + while (index < matches.length) { + if (index && seen === matches[index]) { + matches.splice(index, 1); + } else { + seen = matches[index]; + index++; + } + } + } + return matches; + } + }; + +}).call(this); + +},{"./filter":3,"./matcher":5,"./scorer":6,"path":1}],5:[function(require,module,exports){ +(function() { + var PathSeparator; + + PathSeparator = require('path').sep; + + exports.basenameMatch = function(string, query) { + var base, index, lastCharacter, slashCount; + index = string.length - 1; + while (string[index] === PathSeparator) { + index--; + } + slashCount = 0; + lastCharacter = index; + base = null; + while (index >= 0) { + if (string[index] === PathSeparator) { + slashCount++; + if (base == null) { + base = string.substring(index + 1, lastCharacter + 1); + } + } else if (index === 0) { + if (lastCharacter < string.length - 1) { + if (base == null) { + base = string.substring(0, lastCharacter + 1); + } + } else { + if (base == null) { + base = string; + } + } + } + index--; + } + return exports.match(base, query, string.length - base.length); + }; + + exports.match = function(string, query, stringOffset) { + var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results; + if (stringOffset == null) { + stringOffset = 0; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + matches = []; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return []; + } + matches.push(stringOffset + indexInString); + stringOffset += indexInString + 1; + string = string.substring(indexInString + 1, stringLength); + } + return matches; + }; + +}).call(this); + +},{"path":1}],6:[function(require,module,exports){ +(function() { + var PathSeparator, queryIsLastPathSegment; + + PathSeparator = require('path').sep; + + exports.basenameScore = function(string, query, score) { + var base, depth, index, lastCharacter, segmentCount, slashCount; + index = string.length - 1; + while (string[index] === PathSeparator) { + index--; + } + slashCount = 0; + lastCharacter = index; + base = null; + while (index >= 0) { + if (string[index] === PathSeparator) { + slashCount++; + if (base == null) { + base = string.substring(index + 1, lastCharacter + 1); + } + } else if (index === 0) { + if (lastCharacter < string.length - 1) { + if (base == null) { + base = string.substring(0, lastCharacter + 1); + } + } else { + if (base == null) { + base = string; + } + } + } + index--; + } + if (base === string) { + score *= 2; + } else if (base) { + score += exports.score(base, query); + } + segmentCount = slashCount + 1; + depth = Math.max(1, 10 - segmentCount); + score *= depth * 0.01; + return score; + }; + + exports.score = function(string, query) { + var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref; + if (string === query) { + return 1; + } + if (queryIsLastPathSegment(string, query)) { + return 1; + } + totalCharacterScore = 0; + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return 0; + } + characterScore = 0.1; + if (string[indexInString] === character) { + characterScore += 0.1; + } + if (indexInString === 0 || string[indexInString - 1] === PathSeparator) { + characterScore += 0.8; + } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') { + characterScore += 0.7; + } + string = string.substring(indexInString + 1, stringLength); + totalCharacterScore += characterScore; + } + queryScore = totalCharacterScore / queryLength; + return ((queryScore * (queryLength / stringLength)) + queryScore) / 2; + }; + + queryIsLastPathSegment = function(string, query) { + if (string[string.length - query.length - 1] === PathSeparator) { + return string.lastIndexOf(query) === string.length - query.length; + } + }; + +}).call(this); + +},{"path":1}],7:[function(require,module,exports){ +(function pushToGlobal(root, factory) { + root["fuzzaldrin"] = factory(); +})(Function("return this")(), function() { + var x = require('fuzzaldrin'); + return x; +}); + +},{"fuzzaldrin":4}]},{},[7]); From 5dd4db7676b77fd660251563688b6d1938de86d5 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 1 Jul 2016 16:44:16 -0700 Subject: [PATCH 2/3] Correctly feature detect WeakMap. --- app/assets/javascripts/workers/aether_worker.js | 2 +- app/assets/javascripts/workers/worker_world.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js index 4d2db81d4..80c72bf54 100644 --- a/app/assets/javascripts/workers/aether_worker.js +++ b/app/assets/javascripts/workers/aether_worker.js @@ -5,7 +5,7 @@ importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); try { //Detect very modern javascript support. - (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });")); + (0,eval("'use strict'; let test = WeakMap && (class Test { *gen(a=7) { yield yield * () => true ; } });")); console.log("Modern javascript detected, aw yeah!"); self.importScripts('/javascripts/esper.modern.js'); } catch (e) { diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index 154678395..2004f7c4e 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -66,7 +66,7 @@ self.console = console; self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js'); try { //Detect very modern javascript support. - (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });")); + (0,eval("'use strict'; let test = WeakMap && (class Test { *gen(a=7) { yield yield * () => true ; } });")); console.log("Modern javascript detected, aw yeah!"); self.importScripts('/javascripts/esper.modern.js'); } catch (e) { From acaebcdbbb5cad311066bcd21da977b3ac63a815 Mon Sep 17 00:00:00 2001 From: Bryukhanov Valentin Date: Tue, 5 Jul 2016 09:30:33 +0300 Subject: [PATCH 3/3] update ru.coffee - typo "classroom_in_a_box" Russian typo fixed. --- app/locale/ru.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index 24bb9a6b9..3324871ba 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -24,7 +24,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi im_a_teacher: "Я учитель" im_a_student: "Я ученик" learn_more: "Узнать больше" - classroom_in_a_box: "Готовая учебный кабинет из коробки для обучения информатике." + classroom_in_a_box: "Готовый учебный кабинет из коробки для обучения информатике." codecombat_is: "CodeCombat это платформа для студентов чтобы изучать информатику во время игры." our_courses: "Наши курсы были тщательно проработаны чтобы качественно обучать, даже если учителя не имееют особого опыта в программировании." top_screenshots_hint: "Студенты пишут код и видят как их изменения обновляются в реальном времени"