diff --git a/spec/app/autocomplete-spec.coffee b/spec/app/autocomplete-spec.coffee index c03fd4529..4f216422d 100644 --- a/spec/app/autocomplete-spec.coffee +++ b/spec/app/autocomplete-spec.coffee @@ -1,3 +1,4 @@ +$ = require 'jquery' Autocomplete = require 'autocomplete' Buffer = require 'buffer' Editor = require 'editor' @@ -11,6 +12,99 @@ describe "Autocomplete", -> editor.setBuffer new Buffer(require.resolve('fixtures/sample.js')) autocomplete = new Autocomplete(editor) + afterEach -> + autocomplete.remove() + + describe 'autocomplete:toggle event', -> + it 'shows autocomplete view', -> + expect($(document).find('#autocomplete')).not.toExist() + editor.trigger "autocomplete:toggle" + expect($(document).find('#autocomplete')).toExist() + editor.trigger "autocomplete:toggle" + expect($(document).find('#autocomplete')).not.toExist() + + describe "when no text is selected", -> + it 'autocompletes word when there is only a prefix', -> + editor.buffer.insert([10,0] ,"extra:s:extra") + editor.setCursorBufferPosition([10,7]) + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" + expect(editor.getCursorBufferPosition()).toEqual [10,10] + expect(editor.getSelection().getBufferRange()).toEqual [[10,7], [10,10]] + + it 'autocompletes word when there is only a suffix', -> + editor.buffer.insert([10,0] ,"extra:e:extra") + editor.setCursorBufferPosition([10,6]) + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(10)).toBe "extra:while:extra" + expect(editor.getCursorBufferPosition()).toEqual [10,10] + expect(editor.getSelection().getBufferRange()).toEqual [[10,6], [10,10]] + + it 'autocompletes word when there is a prefix and suffix', -> + editor.buffer.insert([8,43] ,"q") + editor.setCursorBufferPosition([8,44]) + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(quicksort(right));" + expect(editor.getCursorBufferPosition()).toEqual [8,48] + expect(editor.getSelection().getBufferRange()).toEqual [[8,44], [8,48]] + + describe "when text is selected", -> + it 'autocompletes word when there is only a prefix', -> + editor.buffer.insert([10,0] ,"extra:sort:extra") + editor.setSelectionBufferRange [[10,7], [10,10]] + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" + expect(editor.getCursorBufferPosition()).toEqual [10,11] + expect(editor.getSelection().getBufferRange()).toEqual [[10,7],[10,11]] + + it 'autocompletes word when there is only a suffix', -> + editor.buffer.insert([10,0] ,"extra:current:extra") + editor.setSelectionBufferRange [[10,6],[10,12]] + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(10)).toBe "extra:quicksort:extra" + expect(editor.getCursorBufferPosition()).toEqual [10,14] + expect(editor.getSelection().getBufferRange()).toEqual [[10,6],[10,14]] + + it 'autocompletes word when there is a prefix and suffix', -> + editor.setSelectionBufferRange [[5,7],[5,12]] + editor.trigger "autocomplete:toggle" + + expect(editor.lineForBufferRow(5)).toBe " concat = items.shift();" + expect(editor.getCursorBufferPosition()).toEqual [5,11] + expect(editor.getSelection().getBufferRange()).toEqual [[5,7], [5,11]] + + describe 'when changes are made to the buffer', -> + it 'updates word list', -> + spyOn(autocomplete, 'buildWordList') + editor.buffer.change([[0,4],[0,13]], "sauron") + expect(autocomplete.buildWordList).toHaveBeenCalled() + + describe "when editor's buffer is changed", -> + it 'creates and uses a new word list based on new buffer', -> + wordList = autocomplete.wordList + expect(wordList).toContain "quicksort" + expect(wordList).not.toContain "Some" + + editor.setBuffer new Buffer(require.resolve('fixtures/sample.txt')) + + wordList = autocomplete.wordList + expect(wordList).not.toContain "quicksort" + expect(wordList).toContain "Some" + + it 'stops listening to previous buffers change events', -> + previousBuffer = editor.buffer + editor.setBuffer new Buffer(require.resolve('fixtures/sample.txt')) + spyOn(autocomplete, "buildWordList") + + previousBuffer.change([[0,0],[0,1]], "sauron") + + expect(autocomplete.buildWordList).not.toHaveBeenCalled() + describe '.wordMatches(prefix, suffix)', -> it 'returns wordMatches on buffer starting with given prefix and ending with given suffix', -> wordMatches = autocomplete.wordMatches("s", "").map (match) -> match[0] @@ -32,91 +126,35 @@ describe "Autocomplete", -> expect(wordMatches.length).toBe 1 expect(wordMatches).toContain("left") - describe ".completeWord()", -> - describe "when no text is selected", -> - it 'autocompletes word when there is only a prefix', -> - editor.buffer.insert([10,0] ,"extra:s:extra") - editor.setCursorBufferPosition([10,7]) - autocomplete.completeWord() + describe ".show()", -> + beforeEach -> + editor.attachToDom() + editor.buffer.insert([10,0] ,"extra:s:extra") + editor.setCursorBufferPosition([10,7]) + autocomplete.show() - expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" - expect(editor.getCursorBufferPosition()).toEqual [10,10] - expect(editor.getSelection().getBufferRange()).toEqual [[10,7], [10,10]] + it "adds the autocomplete view to the editor", -> + expect($(document).find('#autocomplete')).toExist() + expect(autocomplete.position().top).toBeGreaterThan 0 + expect(autocomplete.position().left).toBeGreaterThan 0 - it 'autocompletes word when there is only a suffix', -> - editor.buffer.insert([10,0] ,"extra:e:extra") - editor.setCursorBufferPosition([10,6]) - autocomplete.completeWord() + it "displays words that match letters surrounding the current selection", -> + 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(editor.lineForBufferRow(10)).toBe "extra:while:extra" - expect(editor.getCursorBufferPosition()).toEqual [10,10] - expect(editor.getSelection().getBufferRange()).toEqual [[10,6], [10,10]] + it "selects the first match and replaces the seleced text with it", -> + expect(autocomplete.matchesList.find('li').length).toBe 2 + expect(autocomplete.matchesList.find('li:eq(0)')).toHaveClass('selected') + expect(autocomplete.matchesList.find('li:eq(1)')).not.toHaveText('selected') - it 'autocompletes word when there is a prefix and suffix', -> - editor.buffer.insert([8,43] ,"q") - editor.setCursorBufferPosition([8,44]) - autocomplete.completeWord() + expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" - expect(editor.lineForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(quicksort(right));" - expect(editor.getCursorBufferPosition()).toEqual [8,48] - expect(editor.getSelection().getBufferRange()).toEqual [[8,44], [8,48]] + describe 'when autocomplete changes buffer', -> + it 'does not rebuild the word list', -> + editor.buffer.insert([10,0] ,"extra:s:extra") - describe "when text is selected", -> - it 'autocompletes word when there is only a prefix', -> - editor.buffer.insert([10,0] ,"extra:sort:extra") - editor.setSelectionBufferRange [[10,7], [10,10]] - autocomplete.completeWord() - - expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" - expect(editor.getCursorBufferPosition()).toEqual [10,11] - expect(editor.getSelection().getBufferRange()).toEqual [[10,7],[10,11]] - - it 'autocompletes word when there is only a suffix', -> - editor.buffer.insert([10,0] ,"extra:current:extra") - editor.setSelectionBufferRange [[10,6],[10,12]] - autocomplete.completeWord() - - expect(editor.lineForBufferRow(10)).toBe "extra:quicksort:extra" - expect(editor.getCursorBufferPosition()).toEqual [10,14] - expect(editor.getSelection().getBufferRange()).toEqual [[10,6],[10,14]] - - it 'autocompletes word when there is a prefix and suffix', -> - editor.setSelectionBufferRange [[5,7],[5,12]] - autocomplete.completeWord() - - expect(editor.lineForBufferRow(5)).toBe " concat = items.shift();" - expect(editor.getCursorBufferPosition()).toEqual [5,11] - expect(editor.getSelection().getBufferRange()).toEqual [[5,7], [5,11]] - - describe 'when changes are made to the buffer', -> - it 'updates word list', -> - wordList = autocomplete.wordList - expect(wordList).toContain "quicksort" - expect(wordList).not.toContain "sauron" - - editor.buffer.change([[0,4],[0,13]], "sauron") - - wordList = autocomplete.wordList - expect(wordList).not.toContain "quicksort" - expect(wordList).toContain "sauron" - - describe "when editor's buffer is changed", -> - it 'creates and uses a new word list based on new buffer', -> - wordList = autocomplete.wordList - expect(wordList).toContain "quicksort" - expect(wordList).not.toContain "Some" - - editor.setBuffer new Buffer(require.resolve('fixtures/sample.txt')) - - wordList = autocomplete.wordList - expect(wordList).not.toContain "quicksort" - expect(wordList).toContain "Some" - - it 'stops listening to previous buffers change events', -> - previousBuffer = editor.buffer - editor.setBuffer new Buffer(require.resolve('fixtures/sample.txt')) - spyOn(autocomplete, "buildWordList") - - previousBuffer.change([[0,0],[0,1]], "sauron") - - expect(autocomplete.buildWordList).not.toHaveBeenCalled() + spyOn(autocomplete, 'buildWordList') + editor.setCursorBufferPosition([10,7]) + editor.trigger "autocomplete:toggle" + expect(autocomplete.buildWordList).not.toHaveBeenCalled() \ No newline at end of file diff --git a/src/app/autocomplete.coffee b/src/app/autocomplete.coffee index dc6991656..2623f7052 100644 --- a/src/app/autocomplete.coffee +++ b/src/app/autocomplete.coffee @@ -1,42 +1,84 @@ {View, $$} = require 'space-pen' +$ = require 'jquery' _ = require 'underscore' Range = require 'range' module.exports = class Autocomplete extends View @content: -> - @div class: 'autocomplete', => + @div id: 'autocomplete', => @ol outlet: 'matchesList' editor: null currentBuffer: null - wordList = null + wordList: null wordRegex: /\w+/g + matches: null + isAutocompleting: false initialize: (@editor) -> - @setCurrentBuffer(@editor.buffer) - @editor.on 'autocomplete:complete-word', => @completeWordAtEditorCursorPosition() + requireStylesheet 'autocomplete.css' + @editor.on 'autocomplete:toggle', => @toggle() @editor.on 'buffer-path-change', => @setCurrentBuffer(@editor.buffer) + @setCurrentBuffer(@editor.buffer) + setCurrentBuffer: (buffer) -> @currentBuffer.off '.autocomplete' if @currentBuffer @currentBuffer = buffer - @currentBuffer.on 'change.autocomplete', => @buildWordList() @buildWordList() - buildWordList: () -> - @wordList = _.unique(@currentBuffer.getText().match(@wordRegex)) + @currentBuffer.on 'change.autocomplete', => + @buildWordList() unless @isAutocompleting - completeWord: -> + + toggle: -> + if @parent()[0] then @hide() else @show() + + show: -> + @buildMatchList() + @selectMatch(0) if @matches.length > 0 + + cursorScreenPosition = @editor.getCursorScreenPosition() + {left, top} = @editor.pixelOffsetForScreenPosition(cursorScreenPosition) + @css {left: left, top: top + @editor.lineHeight} + $(document.body).append(this) + + hide: -> + @remove() + + buildMatchList: -> selection = @editor.getSelection() {prefix, suffix} = @prefixAndSuffixOfSelection(selection) currentWord = prefix + @editor.getSelectedText() + suffix - for match in @wordMatches(prefix, suffix) when match[0] != currentWord - startPosition = selection.getBufferRange().start - @editor.insertText(match[1]) - @editor.setSelectionBufferRange([startPosition, [startPosition.row, startPosition.column + match[1].length]]) - break + @matches = (match for match in @wordMatches(prefix, suffix) when match[0] != currentWord) + + @matchesList.empty() + if @matches.length > 0 + @matchesList.append($$ -> @li match[0]) for match in @matches + else + @matchesList.append($$ -> @li "No matches found") + + wordMatches: (prefix, suffix) -> + regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i") + regex.exec(word) for word in @wordList when regex.test(word) + + selectMatch: (index) -> + @matchesList.find("li:eq(#{index})").addClass "selected" + @completeUsingMatch(index) + + buildWordList: () -> + @wordList = _.unique(@currentBuffer.getText().match(@wordRegex)) + + completeUsingMatch: (matchIndex) -> + match = @matches[matchIndex] + selection = @editor.getSelection() + startPosition = selection.getBufferRange().start + @isAutocompleting = true + @editor.insertText(match[1]) + @editor.setSelectionBufferRange([startPosition, [startPosition.row, startPosition.column + match[1].length]]) + @isAutocompleting = false prefixAndSuffixOfSelection: (selection) -> selectionRange = selection.getBufferRange() @@ -56,7 +98,3 @@ class Autocomplete extends View stop() {prefix, suffix} - - wordMatches: (prefix, suffix) -> - regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i") - regex.exec(word) for word in @wordList when regex.test(word) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 36ac30709..2700c8857 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -379,6 +379,11 @@ class Editor extends View position = Point.fromObject(position) { top: position.row * @lineHeight, left: position.column * @charWidth } + pixelOffsetForScreenPosition: (position) -> + {top, left} = @pixelPositionForScreenPosition(position) + offset = @lines.offset() + {top: top + offset.top, left: left + offset.left} + screenPositionFromPixelPosition: ({top, left}) -> screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth)) diff --git a/src/app/keymaps/autocomplete.coffee b/src/app/keymaps/autocomplete.coffee index caa2867f0..92e213c7a 100644 --- a/src/app/keymaps/autocomplete.coffee +++ b/src/app/keymaps/autocomplete.coffee @@ -1,2 +1,2 @@ window.keymap.bindKeys '.editor', - 'escape': 'autocomplete:complete-word' + 'escape': 'autocomplete:toggle' diff --git a/static/autocomplete.css b/static/autocomplete.css new file mode 100644 index 000000000..76b7601a7 --- /dev/null +++ b/static/autocomplete.css @@ -0,0 +1,21 @@ +#autocomplete { + position: absolute; + width: 200px; + height: 300px; + overflow: scroll; + overflow-x: hidden; + background-color: #444; + border: 2px solid #222; + color: #eee; + -webkit-box-shadow: 0 0 5px 5px #222; + margin: 5px; +} + +#autocomplete ol { + overflow: hidden; +} + +#autocomplete ol li.selected { + background-color: #a3fd97; + color: black; +} \ No newline at end of file