Use select list in autocomplete package

This commit is contained in:
Kevin Sawicki
2012-12-28 10:52:27 -08:00
parent 694885c22a
commit 0ac47f8ec0
4 changed files with 93 additions and 204 deletions

View File

@@ -17,7 +17,6 @@ class SelectList extends View
maxItems: Infinity
scheduleTimeout: null
inputThrottle: 50
filteredArray: null
cancelling: false
initialize: ->
@@ -113,8 +112,11 @@ class SelectList extends View
getSelectedItem: ->
@list.find('li.selected')
getSelectedElement: ->
@getSelectedItem().data('select-list-element')
confirmSelection: ->
element = @getSelectedItem().data('select-list-element')
element = @getSelectedElement()
@confirmed(element) if element?
cancel: ->

View File

@@ -61,9 +61,9 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [10,10]
expect(editor.getSelection().getBufferRange()).toEqual [[10,7], [10,10]]
expect(autocomplete.matchesList.find('li').length).toBe 2
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText('sort')
expect(autocomplete.matchesList.find('li:eq(1)')).toHaveText('shift')
expect(autocomplete.list.find('li').length).toBe 2
expect(autocomplete.list.find('li:eq(0)')).toHaveText('sort')
expect(autocomplete.list.find('li:eq(1)')).toHaveText('shift')
it 'autocompletes word when there is only a suffix', ->
editor.getBuffer().insert([10,0] ,"extra:n:extra")
@@ -74,9 +74,9 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [10,13]
expect(editor.getSelection().getBufferRange()).toEqual [[10,6], [10,13]]
expect(autocomplete.matchesList.find('li').length).toBe 2
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText('function')
expect(autocomplete.matchesList.find('li:eq(1)')).toHaveText('return')
expect(autocomplete.list.find('li').length).toBe 2
expect(autocomplete.list.find('li:eq(0)')).toHaveText('function')
expect(autocomplete.list.find('li:eq(1)')).toHaveText('return')
it 'autocompletes word when there is a single prefix and suffix match', ->
editor.getBuffer().insert([8,43] ,"q")
@@ -87,14 +87,13 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [8,52]
expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy()
expect(autocomplete.matchesList.find('li').length).toBe 0
expect(autocomplete.list.find('li').length).toBe 0
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"
expect(autocomplete.error).toHaveText "No matches found"
it "autocompletes word and replaces case of prefix with case of word", ->
editor.getBuffer().insert([10,0] ,"extra:SO:extra")
@@ -115,7 +114,7 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [10,11]
expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy()
expect(autocomplete.matchesList.find('li').length).toBe 0
expect(autocomplete.list.find('li').length).toBe 0
it 'autocompletes word when there is only a suffix', ->
editor.getBuffer().insert([10,0] ,"extra:current:extra")
@@ -126,8 +125,8 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [10,14]
expect(editor.getSelection().getBufferRange()).toEqual [[10,6],[10,14]]
expect(autocomplete.matchesList.find('li').length).toBe 7
expect(autocomplete.matchesList.find('li:contains(current)')).not.toExist()
expect(autocomplete.list.find('li').length).toBe 7
expect(autocomplete.list.find('li:contains(current)')).not.toExist()
it 'autocompletes word when there is a prefix and suffix', ->
editor.setSelectedBufferRange [[5,7],[5,12]]
@@ -137,7 +136,7 @@ describe "Autocomplete", ->
expect(editor.getCursorBufferPosition()).toEqual [5,12]
expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy()
expect(autocomplete.matchesList.find('li').length).toBe 0
expect(autocomplete.list.find('li').length).toBe 0
it 'replaces selection with selected match, moves the cursor to the end of the match, and removes the autocomplete menu', ->
editor.getBuffer().insert([10,0] ,"extra:sort:extra")
@@ -173,22 +172,21 @@ describe "Autocomplete", ->
expect(editor.getSelection().isEmpty()).toBeTruthy()
expect(editor.find('.autocomplete')).not.toExist()
describe 'core:cancel event', ->
describe "when there are no matches", ->
it "closes the menu without changing the buffer", ->
editor.getBuffer().insert([10,0] ,"xxx")
editor.setCursorBufferPosition [10, 3]
autocomplete.attach()
expect(autocomplete.matchesList.find('li').length).toBe 1
expect(autocomplete.matchesList.find('li')).toHaveText ('No matches found')
expect(autocomplete.error).toHaveText "No matches found"
miniEditor.trigger "core:confirm"
miniEditor.trigger "core:cancel"
expect(editor.lineForBufferRow(10)).toBe "xxx"
expect(editor.getCursorBufferPosition()).toEqual [10,3]
expect(editor.getSelection().isEmpty()).toBeTruthy()
expect(editor.find('.autocomplete')).not.toExist()
describe 'core:cancel event', ->
it 'does not replace selection, removes autocomplete view and returns focus to editor', ->
editor.getBuffer().insert([10,0] ,"extra:so:extra")
editor.setSelectedBufferRange [[10,7], [10,8]]
@@ -233,23 +231,6 @@ describe "Autocomplete", ->
expect(autocomplete.find('li:eq(7)')).not.toHaveClass('selected')
expect(autocomplete.find('li:eq(6)')).toHaveClass('selected')
it "scrolls to the selected match if it is out of view", ->
editor.getBuffer().insert([10,0] ,"t")
editor.setCursorBufferPosition([10, 0])
editor.attachToDom()
autocomplete.attach()
matchesList = autocomplete.matchesList
matchesList.height(100)
expect(matchesList.height()).toBeLessThan matchesList[0].scrollHeight
matchCount = matchesList.find('li').length
miniEditor.trigger 'core:move-up'
expect(matchesList.scrollBottom()).toBe matchesList[0].scrollHeight
miniEditor.trigger 'core:move-up' for i in [1...matchCount]
expect(matchesList.scrollTop()).toBe 0
describe 'move-down event', ->
it "highlights the next match and replaces the selection with it", ->
editor.getBuffer().insert([10,0] ,"extra:s:extra")
@@ -266,30 +247,13 @@ describe "Autocomplete", ->
expect(autocomplete.find('li:eq(0)')).toHaveClass('selected')
expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected')
it "scrolls to the selected match if it is out of view", ->
editor.getBuffer().insert([10,0] ,"t")
editor.setCursorBufferPosition([10, 0])
editor.attachToDom()
autocomplete.attach()
matchesList = autocomplete.matchesList
matchesList.height(100)
expect(matchesList.height()).toBeLessThan matchesList[0].scrollHeight
matchCount = matchesList.find('li').length
miniEditor.trigger 'core:move-down' for i in [1...matchCount]
expect(matchesList.scrollBottom()).toBe matchesList[0].scrollHeight
miniEditor.trigger 'core:move-down'
expect(matchesList.scrollTop()).toBe 0
describe "when a match is clicked in the match list", ->
it "selects and confirms the match", ->
editor.getBuffer().insert([10,0] ,"t")
editor.setCursorBufferPosition([10, 0])
autocomplete.attach()
matchToSelect = autocomplete.matchesList.find('li:eq(1)')
matchToSelect = autocomplete.list.find('li:eq(1)')
matchToSelect.mousedown()
expect(matchToSelect).toMatchSelector('.selected')
matchToSelect.mouseup()
@@ -297,17 +261,6 @@ describe "Autocomplete", ->
expect(autocomplete.parent()).not.toExist()
expect(editor.lineForBufferRow(10)).toBe matchToSelect.text()
it "cancels the autocomplete when clicking on the 'No matches found' li", ->
editor.getBuffer().insert([10,0] ,"t")
editor.setCursorBufferPosition([10, 0])
autocomplete.attach()
miniEditor.insertText('xxx')
autocomplete.matchesList.find('li').mousedown().mouseup()
expect(autocomplete.parent()).not.toExist()
expect(editor.lineForBufferRow(10)).toBe "t"
describe "when the mini-editor receives keyboard input", ->
describe "when text is removed from the mini-editor", ->
it "reloads the match list based on the mini-editor's text", ->
@@ -315,11 +268,13 @@ describe "Autocomplete", ->
editor.setCursorBufferPosition([10,0])
autocomplete.attach()
expect(autocomplete.matchesList.find('li').length).toBe 8
expect(autocomplete.list.find('li').length).toBe 8
miniEditor.textInput('c')
expect(autocomplete.matchesList.find('li').length).toBe 3
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.list.find('li').length).toBe 3
miniEditor.backspace()
expect(autocomplete.matchesList.find('li').length).toBe 8
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.list.find('li').length).toBe 8
describe "when the text contains only word characters", ->
it "narrows the list of completions with the fuzzy match algorithm", ->
@@ -327,20 +282,22 @@ describe "Autocomplete", ->
editor.setCursorBufferPosition([10,0])
autocomplete.attach()
expect(autocomplete.matchesList.find('li').length).toBe 8
expect(autocomplete.list.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'
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.list.find('li').length).toBe 4
expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot'
expect(autocomplete.list.find('li:eq(0)')).toHaveClass 'selected'
expect(autocomplete.list.find('li:eq(1)')).toHaveText 'shift'
expect(autocomplete.list.find('li:eq(2)')).toHaveText 'right'
expect(autocomplete.list.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'
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.list.find('li').length).toBe 2
expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot'
expect(autocomplete.list.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", ->
@@ -349,9 +306,11 @@ describe "Autocomplete", ->
autocomplete.attach()
miniEditor.textInput('iv')
expect(autocomplete.matchesList.find('li:eq(0)')).toHaveText 'pivot'
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot'
miniEditor.textInput(' ')
window.advanceClock(autocomplete.inputThrottle)
expect(autocomplete.parent()).not.toExist()
expect(editor.lineForBufferRow(10)).toEqual 'pivot '
@@ -395,12 +354,12 @@ describe "Autocomplete", ->
expect(autocompleteBottom).toBe cursorPixelPosition.top
expect(autocomplete.position().left).toBe cursorPixelPosition.left
describe ".detach()", ->
describe ".cancel()", ->
it "clears the mini-editor and unbinds autocomplete event handlers for move-up and move-down", ->
autocomplete.attach()
miniEditor.setText('foo')
autocomplete.detach()
autocomplete.cancel()
expect(miniEditor.getText()).toBe ''
editor.trigger 'core:move-down'

View File

@@ -1,79 +1,58 @@
{View, $$} = require 'space-pen'
{$$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
Range = require 'range'
Editor = require 'editor'
fuzzyFilter = require 'fuzzy-filter'
SelectList = require 'select-list'
module.exports =
class Autocomplete extends View
@content: ->
@div class: 'autocomplete', tabindex: -1, =>
@ol outlet: 'matchesList'
@subview 'miniEditor', new Editor(mini: true)
editor: null
miniEditor: null
currentBuffer: null
wordList: null
wordRegex: /\w+/g
allMatches: null
filteredMatches: null
currentMatchIndex: null
isAutocompleting: false
originalSelectionBufferRange: null
originalSelectedText: null
class Autocomplete extends SelectList
@activate: (rootView) ->
requireStylesheet 'autocomplete.css'
new Autocomplete(editor) for editor in rootView.getEditors()
rootView.on 'editor-open', (e, editor) -> new Autocomplete(editor) unless editor.mini
@viewClass: -> "autocomplete #{super}"
editor: null
currentBuffer: null
wordList: null
wordRegex: /\w+/g
isAutocompleting: false
originalSelectionBufferRange: null
originalSelectedText: null
filterKey: 'word'
initialize: (@editor) ->
requireStylesheet 'autocomplete.css'
super
@handleEvents()
@setCurrentBuffer(@editor.getBuffer())
itemForElement: (match) ->
$$ ->
@li match.word
handleEvents: ->
@editor.on 'editor-path-change', => @setCurrentBuffer(@editor.getBuffer())
@editor.on 'before-remove', => @currentBuffer?.off '.autocomplete'
@editor.command 'autocomplete:attach', => @attach()
@command 'core:cancel', => @cancel()
@command 'core:confirm', => @confirm()
@matchesList.on 'mousedown', (e) =>
index = $(e.target).attr('index')
@selectMatchAtIndex(index) if index?
false
@matchesList.on 'mouseup', =>
if @selectedMatch()
@confirm()
else
@cancel()
@miniEditor.getBuffer().on 'change', (e) =>
if @hasParent()
@filterMatches()
@renderMatchList()
@miniEditor.preempt 'core:move-up', =>
@selectPreviousMatch()
false
@miniEditor.preempt 'core:move-down', =>
@selectNextMatch()
false
@miniEditor.preempt 'textInput', (e) =>
text = e.originalEvent.data
unless text.match(@wordRegex)
@confirm()
@confirmSelection()
@editor.insertText(text)
false
setCurrentBuffer: (@currentBuffer) ->
selectItem: (item) ->
super
match = @getSelectedElement()
@replaceSelectedTextWithMatch(match) if match
buildWordList: () ->
wordHash = {}
matches = @currentBuffer.getText().match(@wordRegex)
@@ -81,50 +60,46 @@ class Autocomplete extends View
@wordList = Object.keys(wordHash)
confirm: ->
@confirmed = true
confirmed: (match) ->
@editor.getSelection().clear()
@detach()
return unless match = @selectedMatch()
@cancel()
return unless match
@replaceSelectedTextWithMatch match
position = @editor.getCursorBufferPosition()
@editor.setCursorBufferPosition([position.row, position.column + match.suffix.length])
cancelled: ->
@miniEditor.setText('')
@editor.rootView()?.focus() if @miniEditor.isFocused
cancel: ->
@detach()
super
@editor.getBuffer().change(@currentMatchBufferRange, @originalSelectedText) if @currentMatchBufferRange
@editor.setSelectedBufferRange(@originalSelectionBufferRange)
attach: ->
@confirmed = false
@miniEditor.on 'focusout', =>
@cancel() unless @confirmed
@originalSelectedText = @editor.getSelectedText()
@originalSelectionBufferRange = @editor.getSelection().getBufferRange()
originalCursorPosition = @editor.getCursorScreenPosition()
@currentMatchBufferRange = null
@buildWordList()
@allMatches = @findMatchesForCurrentSelection()
matches = @findMatchesForCurrentSelection()
@setArray(matches)
originalCursorPosition = @editor.getCursorScreenPosition()
@filterMatches()
if @filteredMatches.length is 1
@currentMatchIndex = 0
@replaceSelectedTextWithMatch @selectedMatch()
@confirm()
if matches.length is 1
@confirmed matches[0]
else
@renderMatchList()
@editor.appendToLinesView(this)
@setPosition(originalCursorPosition)
@miniEditor.focus()
@miniEditor.focus()
detach: ->
@miniEditor.off("focusout")
super
@editor.off(".autocomplete")
@editor.focus()
@miniEditor.setText('')
setPosition: (originalCursorPosition) ->
{ left, top } = @editor.pixelPositionForScreenPosition(originalCursorPosition)
@@ -138,47 +113,6 @@ class Autocomplete extends View
else
@css(left: left, top: potentialTop, bottom: 'inherit')
selectPreviousMatch: ->
return if @filteredMatches.length is 0
previousIndex = @currentMatchIndex - 1
previousIndex = @filteredMatches.length - 1 if previousIndex < 0
@selectMatchAtIndex(previousIndex)
selectNextMatch: ->
return if @filteredMatches.length is 0
nextIndex = (@currentMatchIndex + 1) % @filteredMatches.length
@selectMatchAtIndex(nextIndex)
selectMatchAtIndex: (index) ->
@currentMatchIndex = index
@matchesList.find("li").removeClass "selected"
liToSelect = @matchesList.find("li:eq(#{index})")
liToSelect.addClass "selected"
topOfLiToSelect = liToSelect.position().top + @matchesList.scrollTop()
bottomOfLiToSelect = topOfLiToSelect + liToSelect.outerHeight()
if topOfLiToSelect < @matchesList.scrollTop()
@matchesList.scrollTop(topOfLiToSelect)
else if bottomOfLiToSelect > @matchesList.scrollBottom()
@matchesList.scrollBottom(bottomOfLiToSelect)
@replaceSelectedTextWithMatch @selectedMatch()
selectedMatch: ->
@filteredMatches[@currentMatchIndex]
filterMatches: ->
@filteredMatches = fuzzyFilter(@allMatches, @miniEditor.getText(), key: 'word')
renderMatchList: ->
@matchesList.empty()
if @filteredMatches.length > 0
@matchesList.append($$ -> @li match.word, index: index) for match, index in @filteredMatches
else
@matchesList.append($$ -> @li "No matches found")
@selectMatchAtIndex(0) if @filteredMatches.length > 0
findMatchesForCurrentSelection: ->
selection = @editor.getSelection()

View File

@@ -1,11 +1,11 @@
.autocomplete {
display: table;
position: absolute;
.select-list.autocomplete {
min-width: 150px;
background-color: #444;
border: 2px solid #222;
color: #eee;
-webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5);
webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5);
box-sizing: content-box;
margin-left: 0px;
width: auto;
-webkit-box-shadow: none;
}
.autocomplete ol {
@@ -17,9 +17,3 @@
.autocomplete ol li {
padding: 0.1em 0.2em;
}
.autocomplete ol li.selected {
background-color: #a3fd97;
color: black;
}