mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 18:24:09 +03:00
Merge branch 'master' of github.com:github/atom
This commit is contained in:
commit
34d41032d4
@ -1,3 +1,4 @@
|
||||
$ = require 'jquery'
|
||||
Autocomplete = require 'autocomplete'
|
||||
Buffer = require 'buffer'
|
||||
Editor = require 'editor'
|
||||
@ -11,33 +12,22 @@ describe "Autocomplete", ->
|
||||
editor.setBuffer new Buffer(require.resolve('fixtures/sample.js'))
|
||||
autocomplete = new Autocomplete(editor)
|
||||
|
||||
describe '.matches(prefix, suffix)', ->
|
||||
it 'returns matches on buffer starting with given prefix and ending with given suffix', ->
|
||||
matches = autocomplete.matches("s", "").map (match) -> match[0]
|
||||
expect(matches.length).toBe 2
|
||||
expect(matches).toContain("sort")
|
||||
expect(matches).toContain("shift")
|
||||
afterEach ->
|
||||
autocomplete.remove()
|
||||
|
||||
matches = autocomplete.matches("l", "t").map (match) -> match[0]
|
||||
expect(matches.length).toBe 1
|
||||
expect(matches).toContain("left")
|
||||
describe 'autocomplete:toggle event', ->
|
||||
it 'shows autocomplete view', ->
|
||||
expect($(document).find('#autocomplete')).not.toExist()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
expect($(document).find('#autocomplete')).toExist()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
expect($(document).find('#autocomplete')).not.toExist()
|
||||
|
||||
it 'ignores case when finding matches', ->
|
||||
matches = autocomplete.matches("S", "").map (match) -> match[0]
|
||||
expect(matches.length).toBe 2
|
||||
expect(matches).toContain("sort")
|
||||
expect(matches).toContain("shift")
|
||||
|
||||
matches = autocomplete.matches("l", "t").map (match) -> match[0]
|
||||
expect(matches.length).toBe 1
|
||||
expect(matches).toContain("left")
|
||||
|
||||
describe ".completeWordAtEditorCursorPosition()", ->
|
||||
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.completeWordAtEditorCursorPosition()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra"
|
||||
expect(editor.getCursorBufferPosition()).toEqual [10,10]
|
||||
@ -46,7 +36,7 @@ describe "Autocomplete", ->
|
||||
it 'autocompletes word when there is only a suffix', ->
|
||||
editor.buffer.insert([10,0] ,"extra:e:extra")
|
||||
editor.setCursorBufferPosition([10,6])
|
||||
autocomplete.completeWordAtEditorCursorPosition()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:while:extra"
|
||||
expect(editor.getCursorBufferPosition()).toEqual [10,10]
|
||||
@ -55,7 +45,7 @@ describe "Autocomplete", ->
|
||||
it 'autocompletes word when there is a prefix and suffix', ->
|
||||
editor.buffer.insert([8,43] ,"q")
|
||||
editor.setCursorBufferPosition([8,44])
|
||||
autocomplete.completeWordAtEditorCursorPosition()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
expect(editor.lineForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(quicksort(right));"
|
||||
expect(editor.getCursorBufferPosition()).toEqual [8,48]
|
||||
@ -65,7 +55,7 @@ describe "Autocomplete", ->
|
||||
it 'autocompletes word when there is only a prefix', ->
|
||||
editor.buffer.insert([10,0] ,"extra:sort:extra")
|
||||
editor.setSelectionBufferRange [[10,7], [10,10]]
|
||||
autocomplete.completeWordAtEditorCursorPosition()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra"
|
||||
expect(editor.getCursorBufferPosition()).toEqual [10,11]
|
||||
@ -74,7 +64,7 @@ describe "Autocomplete", ->
|
||||
it 'autocompletes word when there is only a suffix', ->
|
||||
editor.buffer.insert([10,0] ,"extra:current:extra")
|
||||
editor.setSelectionBufferRange [[10,6],[10,12]]
|
||||
autocomplete.completeWordAtEditorCursorPosition()
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:quicksort:extra"
|
||||
expect(editor.getCursorBufferPosition()).toEqual [10,14]
|
||||
@ -82,23 +72,51 @@ describe "Autocomplete", ->
|
||||
|
||||
it 'autocompletes word when there is a prefix and suffix', ->
|
||||
editor.setSelectionBufferRange [[5,7],[5,12]]
|
||||
autocomplete.completeWordAtEditorCursorPosition()
|
||||
autocomplete.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 'move-up event', ->
|
||||
it 'replaces selection with previous match', ->
|
||||
editor.buffer.insert([10,0] ,"extra:t:extra")
|
||||
editor.setCursorBufferPosition([10,6])
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
autocomplete.trigger "move-up"
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:concat:extra"
|
||||
expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(7)')).toHaveClass('selected')
|
||||
|
||||
autocomplete.trigger "move-up"
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:right:extra"
|
||||
expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(7)')).not.toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(6)')).toHaveClass('selected')
|
||||
|
||||
describe 'move-down event', ->
|
||||
it 'replaces selection with next match', ->
|
||||
editor.buffer.insert([10,0] ,"extra:s:extra")
|
||||
editor.setCursorBufferPosition([10,7])
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
|
||||
autocomplete.trigger "move-down"
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra"
|
||||
expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(1)')).toHaveClass('selected')
|
||||
|
||||
autocomplete.trigger "move-down"
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra"
|
||||
expect(autocomplete.find('li:eq(0)')).toHaveClass('selected')
|
||||
expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected')
|
||||
|
||||
describe 'when changes are made to the buffer', ->
|
||||
it 'updates word list', ->
|
||||
wordList = autocomplete.wordList
|
||||
expect(wordList).toContain "quicksort"
|
||||
expect(wordList).not.toContain "sauron"
|
||||
|
||||
spyOn(autocomplete, 'buildWordList')
|
||||
editor.buffer.change([[0,4],[0,13]], "sauron")
|
||||
|
||||
wordList = autocomplete.wordList
|
||||
expect(wordList).not.toContain "quicksort"
|
||||
expect(wordList).toContain "sauron"
|
||||
expect(autocomplete.buildWordList).toHaveBeenCalled()
|
||||
|
||||
describe "when editor's buffer is changed", ->
|
||||
it 'creates and uses a new word list based on new buffer', ->
|
||||
@ -120,3 +138,57 @@ describe "Autocomplete", ->
|
||||
previousBuffer.change([[0,0],[0,1]], "sauron")
|
||||
|
||||
expect(autocomplete.buildWordList).not.toHaveBeenCalled()
|
||||
|
||||
describe 'when autocomplete changes buffer', ->
|
||||
it 'does not rebuild the word list', ->
|
||||
editor.buffer.insert([10,0] ,"extra:s:extra")
|
||||
|
||||
spyOn(autocomplete, 'buildWordList')
|
||||
editor.setCursorBufferPosition([10,7])
|
||||
autocomplete.trigger "autocomplete:toggle"
|
||||
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]
|
||||
expect(wordMatches.length).toBe 2
|
||||
expect(wordMatches).toContain("sort")
|
||||
expect(wordMatches).toContain("shift")
|
||||
|
||||
wordMatches = autocomplete.wordMatches("l", "t").map (match) -> match[0]
|
||||
expect(wordMatches.length).toBe 1
|
||||
expect(wordMatches).toContain("left")
|
||||
|
||||
it 'ignores case when finding matches', ->
|
||||
wordMatches = autocomplete.wordMatches("S", "").map (match) -> match[0]
|
||||
expect(wordMatches.length).toBe 2
|
||||
expect(wordMatches).toContain("sort")
|
||||
expect(wordMatches).toContain("shift")
|
||||
|
||||
wordMatches = autocomplete.wordMatches("l", "t").map (match) -> match[0]
|
||||
expect(wordMatches.length).toBe 1
|
||||
expect(wordMatches).toContain("left")
|
||||
|
||||
describe ".show()", ->
|
||||
beforeEach ->
|
||||
editor.attachToDom()
|
||||
editor.buffer.insert([10,0] ,"extra:s:extra")
|
||||
editor.setCursorBufferPosition([10,7])
|
||||
autocomplete.show()
|
||||
|
||||
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 "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')
|
||||
|
||||
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.toHaveClass('selected')
|
||||
|
||||
expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra"
|
@ -1,51 +1,109 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class Autocomplete
|
||||
class Autocomplete extends View
|
||||
@content: ->
|
||||
@div id: 'autocomplete', tabindex: -1, =>
|
||||
@ol outlet: 'matchesList'
|
||||
|
||||
editor: null
|
||||
currentBuffer: null
|
||||
wordList = null
|
||||
wordList: null
|
||||
wordRegex: /\w+/g
|
||||
matches: null
|
||||
currentMatchIndex: null
|
||||
isAutocompleting: false
|
||||
|
||||
constructor: (@editor) ->
|
||||
@setCurrentBuffer(@editor.buffer)
|
||||
@editor.on 'autocomplete:complete-word', => @completeWordAtEditorCursorPosition()
|
||||
initialize: (@editor) ->
|
||||
requireStylesheet 'autocomplete.css'
|
||||
@on 'autocomplete:toggle', => @toggle()
|
||||
@on 'move-up', => @previousMatch()
|
||||
@on 'move-down', => @nextMatch()
|
||||
@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()
|
||||
|
||||
@currentBuffer.on 'change.autocomplete', =>
|
||||
@buildWordList() unless @isAutocompleting
|
||||
|
||||
toggle: ->
|
||||
if @parent()[0] then @remove() 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)
|
||||
@focus()
|
||||
|
||||
previousMatch: ->
|
||||
previousIndex = @currentMatchIndex - 1
|
||||
previousIndex = @matches.length - 1 if previousIndex < 0
|
||||
@selectMatch(previousIndex)
|
||||
|
||||
nextMatch: ->
|
||||
nextIndex = (@currentMatchIndex + 1) % @matches.length
|
||||
@selectMatch(nextIndex)
|
||||
|
||||
buildMatchList: ->
|
||||
selection = @editor.getSelection()
|
||||
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
|
||||
currentWord = prefix + @editor.getSelectedText() + suffix
|
||||
|
||||
@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")
|
||||
|
||||
buildWordList: () ->
|
||||
@wordList = _.unique(@currentBuffer.getText().match(@wordRegex))
|
||||
|
||||
completeWordAtEditorCursorPosition: () ->
|
||||
selectionRange = @editor.getSelection().getBufferRange()
|
||||
wordMatches: (prefix, suffix) ->
|
||||
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
|
||||
regex.exec(word) for word in @wordList when regex.test(word)
|
||||
|
||||
selectMatch: (index) ->
|
||||
@currentMatchIndex = index
|
||||
@matchesList.find("li").removeClass "selected"
|
||||
@matchesList.find("li:eq(#{index})").addClass "selected"
|
||||
@completeUsingMatch(index)
|
||||
|
||||
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()
|
||||
lineRange = [[selectionRange.start.row, 0], [selectionRange.end.row, @editor.lineLengthForBufferRow(selectionRange.end.row)]]
|
||||
[prefix, suffix] = ["", ""]
|
||||
|
||||
@currentBuffer.scanInRange @wordRegex, lineRange, (match, range, {stop}) ->
|
||||
stop() if range.start.isGreaterThan(selectionRange.end)
|
||||
|
||||
if range.intersectsWith(selectionRange)
|
||||
prefixOffset = selectionRange.start.column - range.start.column
|
||||
suffixOffset = selectionRange.end.column - range.end.column
|
||||
|
||||
if range.start.isLessThan(selectionRange.start)
|
||||
prefix = match[0][0...prefixOffset]
|
||||
prefix = match[0][0...prefixOffset] if range.start.isLessThan(selectionRange.start)
|
||||
suffix = match[0][suffixOffset..] if range.end.isGreaterThan(selectionRange.end)
|
||||
|
||||
if range.end.isGreaterThan(selectionRange.end)
|
||||
suffix = match[0][suffixOffset..]
|
||||
stop()
|
||||
|
||||
for match in @matches(prefix, suffix)
|
||||
continue if match[0] == prefix + @editor.getSelectedText() + suffix
|
||||
startPosition = @editor.getSelection().getBufferRange().start
|
||||
@editor.insertText(match[1])
|
||||
@editor.setSelectionBufferRange([startPosition, [startPosition.row, startPosition.column + match[1].length]])
|
||||
break
|
||||
|
||||
matches: (prefix, suffix) ->
|
||||
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
|
||||
regex.exec(word) for word in @wordList when regex.test(word)
|
||||
{prefix, suffix}
|
||||
|
@ -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))
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
window.keymap.bindKeys '.editor',
|
||||
'escape': 'autocomplete:complete-word'
|
||||
'escape': 'autocomplete:toggle'
|
||||
|
21
static/autocomplete.css
Normal file
21
static/autocomplete.css
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user