Files
atom/src/app/autocomplete.coffee
Nathan Sobo 0024cf89de Auto-complete matches can be fuzzy-filtered. Typing non-word characters confirms the match automatically.
Extract fuzzyFilter function into a file that is shared between file finder and autocompleter. Fix jQuery.fn.preempt to pass its arguments to the event handler.
2012-04-19 18:17:08 -06:00

177 lines
5.6 KiB
CoffeeScript

{View, $$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
Range = require 'range'
Editor = require 'editor'
fuzzyFilter = require 'fuzzy-filter'
module.exports =
class Autocomplete extends View
@content: ->
@div id: 'autocomplete', =>
@ol outlet: 'matchesList'
@subview 'miniEditor', new Editor(mini: true)
editor: null
miniEditor: null
currentBuffer: null
wordList: null
wordRegex: /\w+/g
matches: null
currentMatchIndex: null
isAutocompleting: false
currentSelectionBufferRange: null
originalSelectionBufferRange: null
originalSelectedText: null
initialize: (@editor) ->
requireStylesheet 'autocomplete.css'
@handleEvents()
@setCurrentBuffer(@editor.buffer)
handleEvents: ->
@editor.on 'buffer-path-change', => @setCurrentBuffer(@editor.buffer)
@editor.on 'before-remove', => @currentBuffer?.off '.autocomplete'
@editor.on 'autocomplete:toggle', => @attach()
@on 'autocomplete:toggle', => @detach()
@on 'autocomplete:confirm', => @confirm()
@on 'autocomplete:cancel', => @cancel()
@miniEditor.buffer.on 'change', =>
@filterMatchList() if @parent()[0]
@miniEditor.preempt 'move-up', =>
@selectPreviousMatch()
false
@miniEditor.preempt 'move-down', =>
@selectNextMatch()
false
@miniEditor.preempt 'textInput', (e) =>
text = e.originalEvent.data
unless text.match(@wordRegex)
@confirm()
@editor.insertText(text)
false
setCurrentBuffer: (buffer) ->
@currentBuffer?.off '.autocomplete'
@currentBuffer = buffer
@buildWordList()
@currentBuffer.on 'change.autocomplete', (e) => @bufferChanged(e)
confirm: ->
@editor.getSelection().clearSelection()
@detach()
match = @selectedMatch()
position = @editor.getCursorBufferPosition()
@editor.setCursorBufferPosition([position.row, position.column + match.suffix.length])
cancel: ->
@detach()
if @currentSelectionBufferRange
@editor.setSelectionBufferRange(@currentSelectionBufferRange)
@editor.getSelection().insertText @originalSelectedText
@editor.setSelectionBufferRange(@originalSelectionBufferRange)
attach: ->
@editor.on 'focus.autocomplete', => @cancel()
@originalSelectedText = @editor.getSelectedText()
@originalSelectionBufferRange = @editor.getSelection().getBufferRange()
@buildMatchList()
cursorScreenPosition = @editor.getCursorScreenPosition()
{left, top} = @editor.pixelOffsetForScreenPosition(cursorScreenPosition)
@css {left: left, top: top + @editor.lineHeight}
$(document.body).append(this)
@miniEditor.focus()
detach: ->
@editor.off(".autocomplete")
@editor.focus()
super
@miniEditor.buffer.setText('')
selectPreviousMatch: ->
previousIndex = @currentMatchIndex - 1
previousIndex = @matches.length - 1 if previousIndex < 0
@selectMatchAtIndex(previousIndex)
selectNextMatch: ->
nextIndex = (@currentMatchIndex + 1) % @matches.length
@selectMatchAtIndex(nextIndex)
selectMatchAtIndex: (index) ->
@currentMatchIndex = index
@matchesList.find("li").removeClass "selected"
@matchesList.find("li:eq(#{index})").addClass "selected"
@completeUsingMatch(@selectedMatch())
selectedMatch: ->
@matches[@currentMatchIndex]
bufferChanged: (e) ->
@buildWordList() unless @isAutocompleting
buildMatchList: ->
selection = @editor.getSelection()
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
if (prefix.length + suffix.length) > 0
currentWord = prefix + @editor.getSelectedText() + suffix
@matches = (match for match in @wordMatches(prefix, suffix) when match.word != currentWord)
else
@matches = []
@renderMatchList()
filterMatchList: ->
@matches = fuzzyFilter(@matches, @miniEditor.buffer.getText(), key: 'word')
@renderMatchList()
renderMatchList: ->
@matchesList.empty()
if @matches.length > 0
@matchesList.append($$ -> @li match.word) for match in @matches
else
@matchesList.append($$ -> @li "No matches found")
@selectMatchAtIndex(0) if @matches.length > 0
buildWordList: () ->
@wordList = _.unique(@currentBuffer.getText().match(@wordRegex))
wordMatches: (prefix, suffix) ->
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
for word in @wordList when regex.test(word)
match = regex.exec(word)
{prefix, suffix, word, infix: match[1]}
completeUsingMatch: (match) ->
selection = @editor.getSelection()
startPosition = selection.getBufferRange().start
@isAutocompleting = true
@editor.insertText(match.infix)
@editor.setSelectionBufferRange([startPosition, [startPosition.row, startPosition.column + match.infix.length]])
@currentSelectionBufferRange = @editor.getSelection().getBufferRange()
@isAutocompleting = false
prefixAndSuffixOfSelection: (selection) ->
selectionRange = selection.getBufferRange()
lineRange = [[selectionRange.start.row, 0], [selectionRange.end.row, @editor.lineLengthForBufferRow(selectionRange.end.row)]]
[prefix, suffix] = ["", ""]
@currentBuffer.scanInRange @wordRegex, lineRange, (match, range, {stop}) ->
stop() if range.start.isGreaterThan(selectionRange.end)
if range.intersectsWith(selectionRange)
prefixOffset = selectionRange.start.column - range.start.column
suffixOffset = selectionRange.end.column - range.end.column
prefix = match[0][0...prefixOffset] if range.start.isLessThan(selectionRange.start)
suffix = match[0][suffixOffset..] if range.end.isGreaterThan(selectionRange.end)
{prefix, suffix}