mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-18 19:42:36 -05:00
269 lines
13 KiB
CoffeeScript
269 lines
13 KiB
CoffeeScript
###
|
|
This is essentially a copy from the snippet completer from Ace's ext/language-tools.js
|
|
However this completer assigns a score to the snippets to ensure that snippet suggestions are
|
|
treated better in the autocomplete than local values
|
|
###
|
|
|
|
{score} = fuzzaldrin
|
|
#score = (a, b) -> 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
|
|
completions = []
|
|
|
|
#If the prefix is a member expression, supress completions
|
|
fullPrefix = getFullIdentifier session, pos
|
|
fullPrefixParts = fullPrefix.split /[.:]/g
|
|
word = getCurrentWord session, pos
|
|
|
|
if fullPrefixParts.length > 2
|
|
@completions = []
|
|
return callback null, completions
|
|
|
|
beginningOfLine = session.getLine(pos.row).substring(0,pos.column - prefix.length)
|
|
|
|
unless (fullPrefixParts.length < 3 and /^(hero|self|this|@)$/.test(fullPrefixParts[0]) ) or /^\s*$/.test(beginningOfLine)
|
|
console.log "Bailing", fullPrefixParts, '|', prefix, '|', beginningOfLine, '|', pos.column - prefix.length
|
|
@completions = completions
|
|
return callback null, completions
|
|
|
|
snippetMap = SnippetManager.snippetMap
|
|
|
|
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')
|
|
, @
|
|
|
|
#If the prefix is a reserved word, only exact prefix snippets match
|
|
keywords = session.getMode()?.$highlightRules?.$keywordList
|
|
if keywords and prefix in keywords
|
|
@completions = _.filter(completions, (x) -> x.caption.indexOf prefix is 0)
|
|
return callback null, @completions
|
|
|
|
# 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
|
|
|
|
getFullIdentifier = (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]
|