diff --git a/src/app/select-list.coffee b/src/app/select-list.coffee index 373bce542..7b9c43dfd 100644 --- a/src/app/select-list.coffee +++ b/src/app/select-list.coffee @@ -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: -> diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index 8190f3d31..8d4f5d10c 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -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' diff --git a/src/packages/autocomplete/src/autocomplete.coffee b/src/packages/autocomplete/src/autocomplete.coffee index 73d6f66ec..b45be8970 100644 --- a/src/packages/autocomplete/src/autocomplete.coffee +++ b/src/packages/autocomplete/src/autocomplete.coffee @@ -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() diff --git a/static/autocomplete.css b/static/autocomplete.css index 6d247eaf0..7f6c17bdb 100644 --- a/static/autocomplete.css +++ b/static/autocomplete.css @@ -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; -} -