mirror of
https://github.com/atom/atom.git
synced 2026-01-23 22:08:08 -05:00
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.
This commit is contained in:
@@ -69,6 +69,13 @@ describe "Autocomplete", ->
|
||||
expect(autocomplete.matchesList.find('li').length).toBe 1
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText('quicksort')
|
||||
|
||||
it "show's that there are no matches found when there is no prefix or suffix", ->
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
autocomplete.attach()
|
||||
|
||||
expect(autocomplete.matchesList.find('li').length).toBe 1
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText "No matches found"
|
||||
|
||||
describe "when text is selected", ->
|
||||
it 'autocompletes word when there is only a prefix', ->
|
||||
editor.buffer.insert([10,0] ,"extra:sort:extra")
|
||||
@@ -164,6 +171,41 @@ describe "Autocomplete", ->
|
||||
expect(autocomplete.find('li:eq(0)')).toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected')
|
||||
|
||||
describe "when the mini-editor receives text input", ->
|
||||
describe "when the text contains only word characters", ->
|
||||
it "narrows the list of completions with the fuzzy match algorithm", ->
|
||||
editor.buffer.insert([10,0] ,"t")
|
||||
editor.setCursorBufferPosition([10,0])
|
||||
autocomplete.attach()
|
||||
|
||||
expect(autocomplete.matchesList.find('li').length).toBe 8
|
||||
miniEditor.textInput('i')
|
||||
expect(autocomplete.matchesList.find('li').length).toBe 4
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText 'pivot'
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveClass 'selected'
|
||||
expect(autocomplete.matchesList.find('li:eq(1)')).toHaveText 'shift'
|
||||
expect(autocomplete.matchesList.find('li:eq(2)')).toHaveText 'right'
|
||||
expect(autocomplete.matchesList.find('li:eq(3)')).toHaveText 'quicksort'
|
||||
expect(editor.lineForBufferRow(10)).toEqual 'pivot'
|
||||
|
||||
miniEditor.textInput('o')
|
||||
expect(autocomplete.matchesList.find('li').length).toBe 2
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText 'pivot'
|
||||
expect(autocomplete.matchesList.find('li:eq(1)')).toHaveText 'quicksort'
|
||||
|
||||
describe "when a non-word character is typed in the mini-editor", ->
|
||||
it "immediately confirms the current completion choice and inserts that character into the buffer", ->
|
||||
editor.buffer.insert([10,0] ,"t")
|
||||
editor.setCursorBufferPosition([10,0])
|
||||
autocomplete.attach()
|
||||
|
||||
miniEditor.textInput('iv')
|
||||
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText 'pivot'
|
||||
|
||||
miniEditor.textInput(' ')
|
||||
expect(autocomplete.parent()).not.toExist()
|
||||
expect(editor.lineForBufferRow(10)).toEqual 'pivot '
|
||||
|
||||
describe 'when the editor is focused', ->
|
||||
it "cancels the autocomplete", ->
|
||||
autocomplete.attach()
|
||||
@@ -179,17 +221,16 @@ describe "Autocomplete", ->
|
||||
editor.buffer.change([[0,4],[0,13]], "sauron")
|
||||
expect(autocomplete.buildWordList).toHaveBeenCalled()
|
||||
|
||||
describe "when the autocomplete menu is attached", ->
|
||||
describe 'when the change was caused by autocomplete', ->
|
||||
it 'does not rebuild the word list', ->
|
||||
editor.buffer.insert([10,0] ,"extra:s:extra")
|
||||
describe "when the autocomplete menu is attached and the change was caused by autocomplete itself", ->
|
||||
it 'does not rebuild the word list', ->
|
||||
editor.buffer.insert([10,0] ,"extra:s:extra")
|
||||
|
||||
spyOn(autocomplete, 'buildWordList')
|
||||
editor.setCursorBufferPosition([10,7])
|
||||
autocomplete.attach()
|
||||
expect(autocomplete.buildWordList).not.toHaveBeenCalled()
|
||||
spyOn(autocomplete, 'buildWordList')
|
||||
editor.setCursorBufferPosition([10,7])
|
||||
autocomplete.attach()
|
||||
expect(autocomplete.buildWordList).not.toHaveBeenCalled()
|
||||
|
||||
describe "when editor's buffer is assigned a new buffer", ->
|
||||
describe "when a new buffer is assigned on editor", ->
|
||||
it 'creates and uses a new word list based on new buffer', ->
|
||||
wordList = autocomplete.wordList
|
||||
expect(wordList).toContain "quicksort"
|
||||
@@ -253,9 +294,12 @@ describe "Autocomplete", ->
|
||||
expect(autocomplete.position().left).toBeGreaterThan 0
|
||||
|
||||
describe ".detach()", ->
|
||||
it "unbinds autocomplete event handlers for move-up and move-down", ->
|
||||
it "clears the mini-editor and unbinds autocomplete event handlers for move-up and move-down", ->
|
||||
autocomplete.attach()
|
||||
miniEditor.buffer.setText('foo')
|
||||
|
||||
autocomplete.detach()
|
||||
expect(miniEditor.buffer.getText()).toBe ''
|
||||
|
||||
editor.trigger 'move-down'
|
||||
expect(editor.getCursorBufferPosition().row).toBe 1
|
||||
|
||||
@@ -3,6 +3,7 @@ $ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
Range = require 'range'
|
||||
Editor = require 'editor'
|
||||
fuzzyFilter = require 'fuzzy-filter'
|
||||
|
||||
module.exports =
|
||||
class Autocomplete extends View
|
||||
@@ -37,6 +38,9 @@ class Autocomplete extends View
|
||||
@on 'autocomplete:confirm', => @confirm()
|
||||
@on 'autocomplete:cancel', => @cancel()
|
||||
|
||||
@miniEditor.buffer.on 'change', =>
|
||||
@filterMatchList() if @parent()[0]
|
||||
|
||||
@miniEditor.preempt 'move-up', =>
|
||||
@selectPreviousMatch()
|
||||
false
|
||||
@@ -45,6 +49,13 @@ class Autocomplete extends View
|
||||
@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
|
||||
@@ -71,7 +82,6 @@ class Autocomplete extends View
|
||||
@originalSelectedText = @editor.getSelectedText()
|
||||
@originalSelectionBufferRange = @editor.getSelection().getBufferRange()
|
||||
@buildMatchList()
|
||||
@selectMatchAtIndex(0) if @matches.length > 0
|
||||
|
||||
cursorScreenPosition = @editor.getCursorScreenPosition()
|
||||
{left, top} = @editor.pixelOffsetForScreenPosition(cursorScreenPosition)
|
||||
@@ -83,6 +93,7 @@ class Autocomplete extends View
|
||||
@editor.off(".autocomplete")
|
||||
@editor.focus()
|
||||
super
|
||||
@miniEditor.buffer.setText('')
|
||||
|
||||
selectPreviousMatch: ->
|
||||
previousIndex = @currentMatchIndex - 1
|
||||
@@ -108,20 +119,27 @@ class Autocomplete extends View
|
||||
buildMatchList: ->
|
||||
selection = @editor.getSelection()
|
||||
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
|
||||
if (prefix.length + suffix.length) == 0
|
||||
|
||||
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 = []
|
||||
return
|
||||
@renderMatchList()
|
||||
|
||||
currentWord = prefix + @editor.getSelectedText() + suffix
|
||||
|
||||
@matches = (match for match in @wordMatches(prefix, suffix) when match.word != currentWord)
|
||||
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))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
$ = require 'jquery'
|
||||
{View} = require 'space-pen'
|
||||
stringScore = require 'stringscore'
|
||||
fuzzyFilter = require 'fuzzy-filter'
|
||||
Editor = require 'editor'
|
||||
|
||||
module.exports =
|
||||
@@ -58,19 +59,7 @@ class FileFinder extends View
|
||||
.addClass('selected')
|
||||
|
||||
findMatches: (query) ->
|
||||
if not query
|
||||
paths = @paths
|
||||
else
|
||||
scoredPaths = ({path, score: stringScore(path, query)} for path in @paths)
|
||||
scoredPaths.sort (a, b) ->
|
||||
if a.score > b.score then -1
|
||||
else if a.score < b.score then 1
|
||||
else 0
|
||||
window.x = scoredPaths
|
||||
|
||||
paths = (pathAndScore.path for pathAndScore in scoredPaths when pathAndScore.score > 0)
|
||||
|
||||
paths.slice 0, @maxResults
|
||||
fuzzyFilter(@paths, query, maxResults: @maxResults)
|
||||
|
||||
remove: ->
|
||||
$('#panes .editor.active').focus()
|
||||
|
||||
16
src/stdlib/fuzzy-filter.coffee
Normal file
16
src/stdlib/fuzzy-filter.coffee
Normal file
@@ -0,0 +1,16 @@
|
||||
stringScore = require 'stringscore'
|
||||
|
||||
module.exports = (candidates, query, options) ->
|
||||
if query
|
||||
scoredCandidates = candidates.map (candidate) ->
|
||||
string = if options.key? then candidate[options.key] else candidate
|
||||
{ candidate, score: stringScore(string, query) }
|
||||
|
||||
scoredCandidates.sort (a, b) ->
|
||||
if a.score > b.score then -1
|
||||
else if a.score < b.score then 1
|
||||
else 0
|
||||
candidates = (scoredCandidate.candidate for scoredCandidate in scoredCandidates when scoredCandidate.score > 0)
|
||||
|
||||
candidates = candidates[0...options.maxResults] if options.maxResults?
|
||||
candidates
|
||||
@@ -16,8 +16,8 @@ $.fn.containsElement = (element) ->
|
||||
(element[0].compareDocumentPosition(this[0]) & 8) == 8
|
||||
|
||||
$.fn.preempt = (eventName, handler) ->
|
||||
@on eventName, (e) ->
|
||||
if handler() == false then e.stopImmediatePropagation()
|
||||
@on eventName, (e, args...) ->
|
||||
if handler(e, args...) == false then e.stopImmediatePropagation()
|
||||
|
||||
eventNameWithoutNamespace = eventName.split('.')[0]
|
||||
handlers = @data('events')[eventNameWithoutNamespace]
|
||||
|
||||
Reference in New Issue
Block a user