Autocomplete extends View

This commit is contained in:
Corey Johnson
2012-04-17 16:54:23 -07:00
parent c96ac1638f
commit d41ac6626f
5 changed files with 202 additions and 100 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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))

View File

@@ -1,2 +1,2 @@
window.keymap.bindKeys '.editor',
'escape': 'autocomplete:complete-word'
'escape': 'autocomplete:toggle'

21
static/autocomplete.css Normal file
View File

@@ -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;
}