├── .gitignore ├── spec ├── fixtures │ ├── sample.txt │ ├── css.css │ └── sample.js └── autocomplete-spec.coffee ├── CONTRIBUTING.md ├── menus └── autocomplete.cson ├── keymaps └── autocomplete.cson ├── README.md ├── .pairs ├── package.json ├── stylesheets └── autocomplete.less ├── lib ├── autocomplete.coffee └── autocomplete-view.coffee └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /spec/fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | Some text. 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://atom.io/docs/latest/contributing) 2 | -------------------------------------------------------------------------------- /menus/autocomplete.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | '.overlayer': 3 | 'Autocomplete': 'autocomplete:toggle' 4 | -------------------------------------------------------------------------------- /spec/fixtures/css.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 1234px; 3 | width: 110%; 4 | font-weight: bold !important; 5 | } 6 | -------------------------------------------------------------------------------- /keymaps/autocomplete.cson: -------------------------------------------------------------------------------- 1 | '.editor': 2 | 'ctrl-space': 'autocomplete:toggle' 3 | 4 | '.autocomplete .mini.editor input': 5 | 'enter': 'core:confirm' 6 | 'tab': 'core:confirm' 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autocomplete package 2 | 3 | View and insert possible completions in the editor using `ctrl-space`. 4 | 5 | Looking to use a different keybinding? Copy the following to your 6 | `~/.atom/keymap.cson` to tweak: 7 | 8 | ```coffee 9 | '.editor': 10 | 'ctrl-space': 'autocomplete:toggle' 11 | ``` 12 | 13 | ![](https://f.cloud.github.com/assets/671378/2241254/23bc3d0c-9cc8-11e3-80fe-68f58316a52a.png) 14 | -------------------------------------------------------------------------------- /.pairs: -------------------------------------------------------------------------------- 1 | pairs: 2 | ns: Nathan Sobo; nathan 3 | cj: Corey Johnson; cj 4 | dg: David Graham; dgraham 5 | ks: Kevin Sawicki; kevin 6 | jc: Jerry Cheung; jerry 7 | bl: Brian Lopez; brian 8 | jp: Justin Palmer; justin 9 | gt: Garen Torikian; garen 10 | mc: Matt Colyer; mcolyer 11 | bo: Ben Ogle; benogle 12 | jr: Jason Rudolph; jasonrudolph 13 | jl: Jessica Lord; jlord 14 | email: 15 | domain: github.com 16 | #global: true 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocomplete", 3 | "version": "0.27.0", 4 | "main": "./lib/autocomplete", 5 | "description": "Display possible completions in the editor with `ctrl-space`.", 6 | "activationEvents": { 7 | "autocomplete:toggle": ".editor" 8 | }, 9 | "repository": "https://github.com/atom/autocomplete", 10 | "engines": { 11 | "atom": "*" 12 | }, 13 | "dependencies": { 14 | "underscore-plus": "1.x" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spec/fixtures/sample.js: -------------------------------------------------------------------------------- 1 | var quicksort = function () { 2 | var sort = function(items) { 3 | if (items.length <= 1) return items; 4 | var pivot = items.shift(), current, left = [], right = []; 5 | while(items.length > 0) { 6 | current = items.shift(); 7 | current < pivot ? left.push(current) : right.push(current); 8 | } 9 | return sort(left).concat(pivot).concat(sort(right)); 10 | }; 11 | 12 | return sort(Array.apply(this, arguments)); 13 | }; -------------------------------------------------------------------------------- /stylesheets/autocomplete.less: -------------------------------------------------------------------------------- 1 | .autocomplete { 2 | &.select-list { 3 | box-sizing: content-box; 4 | margin-left: 0; 5 | 6 | ol li { 7 | padding: 5px 0 5px 0; 8 | } 9 | } 10 | 11 | ol { 12 | box-sizing: content-box; 13 | position: relative; 14 | overflow-y: scroll; 15 | max-height: 200px; 16 | } 17 | 18 | span { 19 | padding-left: 5px; 20 | padding-right: 5px; 21 | } 22 | 23 | .error-message { 24 | margin: 2px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/autocomplete.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | AutocompleteView = require './autocomplete-view' 3 | 4 | module.exports = 5 | configDefaults: 6 | includeCompletionsFromAllBuffers: false 7 | 8 | autocompleteViews: [] 9 | editorSubscription: null 10 | 11 | activate: -> 12 | @editorSubscription = atom.workspaceView.eachEditorView (editor) => 13 | if editor.attached and not editor.mini 14 | autocompleteView = new AutocompleteView(editor) 15 | editor.on 'editor:will-be-removed', => 16 | autocompleteView.remove() unless autocompleteView.hasParent() 17 | _.remove(@autocompleteViews, autocompleteView) 18 | @autocompleteViews.push(autocompleteView) 19 | 20 | deactivate: -> 21 | @editorSubscription?.off() 22 | @editorSubscription = null 23 | @autocompleteViews.forEach (autocompleteView) -> autocompleteView.remove() 24 | @autocompleteViews = [] 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/autocomplete-view.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | {$, $$, Range, SelectListView} = require 'atom' 3 | 4 | module.exports = 5 | class AutocompleteView extends SelectListView 6 | currentBuffer: null 7 | wordList: null 8 | wordRegex: /\w+/g 9 | originalSelectionBufferRange: null 10 | originalCursorPosition: null 11 | aboveCursor: false 12 | 13 | initialize: (@editorView) -> 14 | super 15 | @addClass('autocomplete popover-list') 16 | {@editor} = @editorView 17 | @handleEvents() 18 | @setCurrentBuffer(@editor.getBuffer()) 19 | 20 | getFilterKey: -> 21 | 'word' 22 | 23 | viewForItem: ({word}) -> 24 | $$ -> 25 | @li => 26 | @span word 27 | 28 | handleEvents: -> 29 | @list.on 'mousewheel', (event) -> event.stopPropagation() 30 | 31 | @editorView.on 'editor:path-changed', => @setCurrentBuffer(@editor.getBuffer()) 32 | @editorView.command 'autocomplete:toggle', => 33 | if @hasParent() 34 | @cancel() 35 | else 36 | @attach() 37 | @editorView.command 'autocomplete:next', => @selectNextItemView() 38 | @editorView.command 'autocomplete:previous', => @selectPreviousItemView() 39 | 40 | @filterEditorView.preempt 'textInput', ({originalEvent}) => 41 | text = originalEvent.data 42 | unless text.match(@wordRegex) 43 | @confirmSelection() 44 | @editor.insertText(text) 45 | false 46 | 47 | setCurrentBuffer: (@currentBuffer) -> 48 | 49 | selectItemView: (item) -> 50 | super 51 | if match = @getSelectedItem() 52 | @replaceSelectedTextWithMatch(match) 53 | 54 | selectNextItemView: -> 55 | super 56 | false 57 | 58 | selectPreviousItemView: -> 59 | super 60 | false 61 | 62 | getCompletionsForCursorScope: -> 63 | cursorScope = @editor.scopesForBufferPosition(@editor.getCursorBufferPosition()) 64 | completions = atom.syntax.propertiesForScope(cursorScope, 'editor.completions') 65 | completions = completions.map (properties) -> _.valueForKeyPath(properties, 'editor.completions') 66 | _.uniq(_.flatten(completions)) 67 | 68 | buildWordList: -> 69 | wordHash = {} 70 | if atom.config.get('autocomplete.includeCompletionsFromAllBuffers') 71 | buffers = atom.project.getBuffers() 72 | else 73 | buffers = [@currentBuffer] 74 | matches = [] 75 | matches.push(buffer.getText().match(@wordRegex)) for buffer in buffers 76 | wordHash[word] ?= true for word in _.flatten(matches) 77 | wordHash[word] ?= true for word in @getCompletionsForCursorScope() 78 | 79 | @wordList = Object.keys(wordHash).sort (word1, word2) -> 80 | word1.toLowerCase().localeCompare(word2.toLowerCase()) 81 | 82 | confirmed: (match) -> 83 | @editor.getSelection().clear() 84 | @cancel() 85 | return unless match 86 | @replaceSelectedTextWithMatch match 87 | position = @editor.getCursorBufferPosition() 88 | @editor.setCursorBufferPosition([position.row, position.column + match.suffix.length]) 89 | 90 | cancelled: -> 91 | super 92 | 93 | @editor.abortTransaction() 94 | @editor.setSelectedBufferRange(@originalSelectionBufferRange) 95 | @editorView.focus() 96 | 97 | attach: -> 98 | @editor.beginTransaction() 99 | 100 | @aboveCursor = false 101 | @originalSelectionBufferRange = @editor.getSelection().getBufferRange() 102 | @originalCursorPosition = @editor.getCursorScreenPosition() 103 | 104 | @buildWordList() 105 | matches = @findMatchesForCurrentSelection() 106 | @setItems(matches) 107 | 108 | if matches.length is 1 109 | @confirmSelection() 110 | else 111 | @editorView.appendToLinesView(this) 112 | @setPosition() 113 | @focusFilterEditor() 114 | 115 | setPosition: -> 116 | { left, top } = @editorView.pixelPositionForScreenPosition(@originalCursorPosition) 117 | height = @outerHeight() 118 | potentialTop = top + @editorView.lineHeight 119 | potentialBottom = potentialTop - @editorView.scrollTop() + height 120 | 121 | if @aboveCursor or potentialBottom > @editorView.outerHeight() 122 | @aboveCursor = true 123 | @css(left: left, top: top - height, bottom: 'inherit') 124 | else 125 | @css(left: left, top: potentialTop, bottom: 'inherit') 126 | 127 | findMatchesForCurrentSelection: -> 128 | selection = @editor.getSelection() 129 | {prefix, suffix} = @prefixAndSuffixOfSelection(selection) 130 | 131 | if (prefix.length + suffix.length) > 0 132 | regex = new RegExp("^#{prefix}.+#{suffix}$", "i") 133 | currentWord = prefix + @editor.getSelectedText() + suffix 134 | for word in @wordList when regex.test(word) and word != currentWord 135 | {prefix, suffix, word} 136 | else 137 | {word, prefix, suffix} for word in @wordList 138 | 139 | replaceSelectedTextWithMatch: (match) -> 140 | selection = @editor.getSelection() 141 | startPosition = selection.getBufferRange().start 142 | buffer = @editor.getBuffer() 143 | 144 | selection.deleteSelectedText() 145 | cursorPosition = @editor.getCursorBufferPosition() 146 | buffer.delete(Range.fromPointWithDelta(cursorPosition, 0, match.suffix.length)) 147 | buffer.delete(Range.fromPointWithDelta(cursorPosition, 0, -match.prefix.length)) 148 | @editor.insertText(match.word) 149 | 150 | infixLength = match.word.length - match.prefix.length - match.suffix.length 151 | @editor.setSelectedBufferRange([startPosition, [startPosition.row, startPosition.column + infixLength]]) 152 | 153 | prefixAndSuffixOfSelection: (selection) -> 154 | selectionRange = selection.getBufferRange() 155 | lineRange = [[selectionRange.start.row, 0], [selectionRange.end.row, @editor.lineLengthForBufferRow(selectionRange.end.row)]] 156 | [prefix, suffix] = ["", ""] 157 | 158 | @currentBuffer.scanInRange @wordRegex, lineRange, ({match, range, stop}) -> 159 | stop() if range.start.isGreaterThan(selectionRange.end) 160 | 161 | if range.intersectsWith(selectionRange) 162 | prefixOffset = selectionRange.start.column - range.start.column 163 | suffixOffset = selectionRange.end.column - range.end.column 164 | 165 | prefix = match[0][0...prefixOffset] if range.start.isLessThan(selectionRange.start) 166 | suffix = match[0][suffixOffset..] if range.end.isGreaterThan(selectionRange.end) 167 | 168 | {prefix, suffix} 169 | 170 | afterAttach: (onDom) -> 171 | if onDom 172 | widestCompletion = parseInt(@css('min-width')) or 0 173 | @list.find('span').each -> 174 | widestCompletion = Math.max(widestCompletion, $(this).outerWidth()) 175 | @list.width(widestCompletion) 176 | @width(@list.outerWidth()) 177 | 178 | populateList: -> 179 | super 180 | 181 | @setPosition() 182 | -------------------------------------------------------------------------------- /spec/autocomplete-spec.coffee: -------------------------------------------------------------------------------- 1 | {$, EditorView, WorkspaceView} = require 'atom' 2 | AutocompleteView = require '../lib/autocomplete-view' 3 | Autocomplete = require '../lib/autocomplete' 4 | 5 | describe "Autocomplete", -> 6 | [activationPromise] = [] 7 | 8 | beforeEach -> 9 | atom.workspaceView = new WorkspaceView 10 | atom.workspaceView.openSync('sample.js') 11 | atom.workspaceView.simulateDomAttachment() 12 | activationPromise = atom.packages.activatePackage('autocomplete') 13 | 14 | describe "@activate()", -> 15 | it "activates autocomplete on all existing and future editors (but not on autocomplete's own mini editor)", -> 16 | spyOn(AutocompleteView.prototype, 'initialize').andCallThrough() 17 | 18 | expect(AutocompleteView.prototype.initialize).not.toHaveBeenCalled() 19 | 20 | leftEditor = atom.workspaceView.getActiveView() 21 | rightEditor = leftEditor.splitRight() 22 | 23 | leftEditor.trigger 'autocomplete:toggle' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(leftEditor.find('.autocomplete')).toExist() 30 | expect(rightEditor.find('.autocomplete')).not.toExist() 31 | expect(AutocompleteView.prototype.initialize).toHaveBeenCalled() 32 | 33 | autoCompleteView = leftEditor.find('.autocomplete').view() 34 | autoCompleteView.trigger 'core:cancel' 35 | expect(leftEditor.find('.autocomplete')).not.toExist() 36 | 37 | rightEditor.trigger 'autocomplete:toggle' 38 | expect(rightEditor.find('.autocomplete')).toExist() 39 | 40 | describe "@deactivate()", -> 41 | it "removes all autocomplete views and doesn't create new ones when new editors are opened", -> 42 | atom.workspaceView.getActiveView().trigger "autocomplete:toggle" 43 | 44 | waitsForPromise -> 45 | activationPromise 46 | 47 | runs -> 48 | expect(atom.workspaceView.getActiveView().find('.autocomplete')).toExist() 49 | atom.packages.deactivatePackage('autocomplete') 50 | expect(atom.workspaceView.getActiveView().find('.autocomplete')).not.toExist() 51 | atom.workspaceView.getActiveView().splitRight() 52 | atom.workspaceView.getActiveView().trigger "autocomplete:toggle" 53 | expect(atom.workspaceView.getActiveView().find('.autocomplete')).not.toExist() 54 | 55 | describe "AutocompleteView", -> 56 | [autocomplete, editorView, editor, miniEditor] = [] 57 | 58 | beforeEach -> 59 | atom.workspaceView = new WorkspaceView 60 | editorView = new EditorView(editor: atom.project.openSync('sample.js')) 61 | {editor} = editorView 62 | autocomplete = new AutocompleteView(editorView) 63 | miniEditor = autocomplete.filterEditorView 64 | 65 | describe 'autocomplete:toggle event', -> 66 | it "shows autocomplete view and focuses its mini-editor", -> 67 | editorView.attachToDom() 68 | expect(editorView.find('.autocomplete')).not.toExist() 69 | 70 | editorView.trigger "autocomplete:toggle" 71 | expect(editorView.find('.autocomplete')).toExist() 72 | expect(autocomplete.editor.isFocused).toBeFalsy() 73 | expect(autocomplete.filterEditorView.isFocused).toBeTruthy() 74 | 75 | describe "when no text is selected", -> 76 | it 'autocompletes word when there is only a prefix', -> 77 | editor.getBuffer().insert([10,0] ,"extra:s:extra") 78 | editor.setCursorBufferPosition([10,7]) 79 | autocomplete.attach() 80 | 81 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 82 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 83 | expect(editor.getSelection().getBufferRange()).toEqual [[10,7], [10,11]] 84 | 85 | expect(autocomplete.list.find('li').length).toBe 2 86 | expect(autocomplete.list.find('li:eq(0)')).toHaveText('shift') 87 | expect(autocomplete.list.find('li:eq(1)')).toHaveText('sort') 88 | 89 | it 'autocompletes word when there is only a suffix', -> 90 | editor.getBuffer().insert([10,0] ,"extra:n:extra") 91 | editor.setCursorBufferPosition([10,6]) 92 | autocomplete.attach() 93 | 94 | expect(editor.lineForBufferRow(10)).toBe "extra:function:extra" 95 | expect(editor.getCursorBufferPosition()).toEqual [10,13] 96 | expect(editor.getSelection().getBufferRange()).toEqual [[10,6], [10,13]] 97 | 98 | expect(autocomplete.list.find('li').length).toBe 2 99 | expect(autocomplete.list.find('li:eq(0)')).toHaveText('function') 100 | expect(autocomplete.list.find('li:eq(1)')).toHaveText('return') 101 | 102 | it 'autocompletes word when there is a single prefix and suffix match', -> 103 | editor.getBuffer().insert([8,43] ,"q") 104 | editor.setCursorBufferPosition([8,44]) 105 | autocomplete.attach() 106 | 107 | expect(editor.lineForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(quicksort(right));" 108 | expect(editor.getCursorBufferPosition()).toEqual [8,52] 109 | expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy() 110 | 111 | expect(autocomplete.list.find('li').length).toBe 0 112 | 113 | it "shows all words when there is no prefix or suffix", -> 114 | editor.setCursorBufferPosition([10, 0]) 115 | autocomplete.attach() 116 | 117 | expect(autocomplete.list.find('li:eq(0)')).toHaveText('0') 118 | expect(autocomplete.list.find('li:eq(1)')).toHaveText('1') 119 | expect(autocomplete.list.find('li').length).toBe 22 120 | 121 | it "autocompletes word and replaces case of prefix with case of word", -> 122 | editor.getBuffer().insert([10,0] ,"extra:SO:extra") 123 | editor.setCursorBufferPosition([10,8]) 124 | autocomplete.attach() 125 | 126 | expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" 127 | expect(editor.getCursorBufferPosition()).toEqual [10,10] 128 | expect(editor.getSelection().isEmpty()).toBeTruthy() 129 | 130 | describe "when `autocomplete.includeCompletionsFromAllBuffers` is true", -> 131 | it "shows words from all open buffers", -> 132 | atom.config.set('autocomplete.includeCompletionsFromAllBuffers', true) 133 | atom.project.openSync('sample.txt') 134 | editor.getBuffer().insert([10,0] ,"extra:SO:extra") 135 | editor.setCursorBufferPosition([10,8]) 136 | autocomplete.attach() 137 | 138 | expect(autocomplete.list.find('li').length).toBe 2 139 | expect(autocomplete.list.find('li:eq(0)')).toHaveText('Some') 140 | expect(autocomplete.list.find('li:eq(1)')).toHaveText('sort') 141 | 142 | describe "when text is selected", -> 143 | it 'autocompletes word when there is only a prefix', -> 144 | editor.getBuffer().insert([10,0] ,"extra:sort:extra") 145 | editor.setSelectedBufferRange [[10,7], [10,10]] 146 | autocomplete.attach() 147 | 148 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 149 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 150 | expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy() 151 | 152 | expect(autocomplete.list.find('li').length).toBe 0 153 | 154 | it 'autocompletes word when there is only a suffix', -> 155 | editor.getBuffer().insert([10,0] ,"extra:current:extra") 156 | editor.setSelectedBufferRange [[10,6],[10,12]] 157 | autocomplete.attach() 158 | 159 | expect(editor.lineForBufferRow(10)).toBe "extra:concat:extra" 160 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 161 | expect(editor.getSelection().getBufferRange()).toEqual [[10,6],[10,11]] 162 | 163 | expect(autocomplete.list.find('li').length).toBe 7 164 | expect(autocomplete.list.find('li:contains(current)')).not.toExist() 165 | 166 | it 'autocompletes word when there is a prefix and suffix', -> 167 | editor.setSelectedBufferRange [[5,7],[5,12]] 168 | autocomplete.attach() 169 | 170 | expect(editor.lineForBufferRow(5)).toBe " concat = items.shift();" 171 | expect(editor.getCursorBufferPosition()).toEqual [5,12] 172 | expect(editor.getSelection().getBufferRange().isEmpty()).toBeTruthy() 173 | 174 | expect(autocomplete.list.find('li').length).toBe 0 175 | 176 | it 'replaces selection with selected match, moves the cursor to the end of the match, and removes the autocomplete menu', -> 177 | editor.getBuffer().insert([10,0] ,"extra:sort:extra") 178 | editor.setSelectedBufferRange [[10,7], [10,9]] 179 | autocomplete.attach() 180 | 181 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 182 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 183 | expect(editor.getSelection().isEmpty()).toBeTruthy() 184 | expect(editorView.find('.autocomplete')).not.toExist() 185 | 186 | describe "when the editor is scrolled to the right", -> 187 | it "does not scroll it to the left", -> 188 | editorView.width(300) 189 | editorView.height(300) 190 | editorView.attachToDom() 191 | editor.setCursorBufferPosition([6, 6]) 192 | previousScrollLeft = editorView.scrollLeft() 193 | autocomplete.attach() 194 | expect(editorView.scrollLeft()).toBe previousScrollLeft 195 | 196 | describe 'core:confirm event', -> 197 | describe "where there are matches", -> 198 | describe "where there is no selection", -> 199 | it "closes the menu and moves the cursor to the end", -> 200 | editor.getBuffer().insert([10,0] ,"extra:sh:extra") 201 | editor.setCursorBufferPosition([10,8]) 202 | autocomplete.attach() 203 | 204 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 205 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 206 | expect(editor.getSelection().isEmpty()).toBeTruthy() 207 | expect(editorView.find('.autocomplete')).not.toExist() 208 | 209 | describe 'core:cancel event', -> 210 | describe "when there are no matches", -> 211 | it "closes the menu without changing the buffer", -> 212 | editor.getBuffer().insert([10,0] ,"xxx") 213 | editor.setCursorBufferPosition [10, 3] 214 | autocomplete.attach() 215 | expect(autocomplete.error).toHaveText "No matches found" 216 | 217 | miniEditor.trigger "core:cancel" 218 | 219 | expect(editor.lineForBufferRow(10)).toBe "xxx" 220 | expect(editor.getCursorBufferPosition()).toEqual [10,3] 221 | expect(editor.getSelection().isEmpty()).toBeTruthy() 222 | expect(editorView.find('.autocomplete')).not.toExist() 223 | 224 | it 'does not replace selection, removes autocomplete view and returns focus to editor', -> 225 | editor.getBuffer().insert([10,0] ,"extra:so:extra") 226 | editor.setSelectedBufferRange [[10,7], [10,8]] 227 | originalSelectionBufferRange = editor.getSelection().getBufferRange() 228 | 229 | autocomplete.attach() 230 | editor.setCursorBufferPosition [0, 0] # even if selection changes before cancel, it should work 231 | miniEditor.trigger "core:cancel" 232 | 233 | expect(editor.lineForBufferRow(10)).toBe "extra:so:extra" 234 | expect(editor.getSelection().getBufferRange()).toEqual originalSelectionBufferRange 235 | expect(editorView.find('.autocomplete')).not.toExist() 236 | 237 | it "does not clear out a previously confirmed selection when canceling with an empty list", -> 238 | editor.getBuffer().insert([10, 0], "ort\n") 239 | editor.setCursorBufferPosition([10, 0]) 240 | 241 | autocomplete.attach() 242 | miniEditor.trigger 'core:confirm' 243 | expect(editor.lineForBufferRow(10)).toBe 'quicksort' 244 | 245 | editor.setCursorBufferPosition([11, 0]) 246 | autocomplete.attach() 247 | miniEditor.trigger 'core:cancel' 248 | expect(editor.lineForBufferRow(10)).toBe 'quicksort' 249 | 250 | it "restores the case of the prefix to the original value", -> 251 | editor.getBuffer().insert([10,0] ,"extra:S:extra") 252 | editor.setCursorBufferPosition([10,7]) 253 | autocomplete.attach() 254 | 255 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 256 | expect(editor.getCursorBufferPosition()).toEqual [10,11] 257 | autocomplete.trigger 'core:cancel' 258 | expect(editor.lineForBufferRow(10)).toBe "extra:S:extra" 259 | expect(editor.getCursorBufferPosition()).toEqual [10,7] 260 | 261 | it "restores the original buffer contents even if there was an additional operation after autocomplete attached (regression)", -> 262 | editor.getBuffer().insert([10,0] ,"extra:s:extra") 263 | editor.setCursorBufferPosition([10,7]) 264 | autocomplete.attach() 265 | 266 | editor.getBuffer().append('hi') 267 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 268 | autocomplete.trigger 'core:cancel' 269 | expect(editor.lineForBufferRow(10)).toBe "extra:s:extra" 270 | 271 | editor.redo() 272 | expect(editor.lineForBufferRow(10)).toBe "extra:s:extra" 273 | 274 | describe 'move-up event', -> 275 | it "highlights the previous match and replaces the selection with it", -> 276 | editor.getBuffer().insert([10,0] ,"extra:t:extra") 277 | editor.setCursorBufferPosition([10,6]) 278 | autocomplete.attach() 279 | 280 | miniEditor.trigger "core:move-up" 281 | expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" 282 | expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected') 283 | expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected') 284 | expect(autocomplete.find('li:eq(7)')).toHaveClass('selected') 285 | 286 | miniEditor.trigger "core:move-up" 287 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 288 | expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected') 289 | expect(autocomplete.find('li:eq(7)')).not.toHaveClass('selected') 290 | expect(autocomplete.find('li:eq(6)')).toHaveClass('selected') 291 | 292 | describe 'move-down event', -> 293 | it "highlights the next match and replaces the selection with it", -> 294 | editor.getBuffer().insert([10,0] ,"extra:s:extra") 295 | editor.setCursorBufferPosition([10,7]) 296 | autocomplete.attach() 297 | 298 | miniEditor.trigger "core:move-down" 299 | expect(editor.lineForBufferRow(10)).toBe "extra:sort:extra" 300 | expect(autocomplete.find('li:eq(0)')).not.toHaveClass('selected') 301 | expect(autocomplete.find('li:eq(1)')).toHaveClass('selected') 302 | 303 | miniEditor.trigger "core:move-down" 304 | expect(editor.lineForBufferRow(10)).toBe "extra:shift:extra" 305 | expect(autocomplete.find('li:eq(0)')).toHaveClass('selected') 306 | expect(autocomplete.find('li:eq(1)')).not.toHaveClass('selected') 307 | 308 | describe "when a match is clicked in the match list", -> 309 | it "selects and confirms the match", -> 310 | editor.getBuffer().insert([10,0] ,"t") 311 | editor.setCursorBufferPosition([10, 0]) 312 | autocomplete.attach() 313 | 314 | matchToSelect = autocomplete.list.find('li:eq(1)') 315 | matchToSelect.mousedown() 316 | expect(matchToSelect).toMatchSelector('.selected') 317 | matchToSelect.mouseup() 318 | 319 | expect(autocomplete.parent()).not.toExist() 320 | expect(editor.lineForBufferRow(10)).toBe matchToSelect.text() 321 | 322 | describe "when the mini-editor receives keyboard input", -> 323 | beforeEach -> 324 | editorView.attachToDom() 325 | 326 | describe "when text is removed from the mini-editor", -> 327 | it "reloads the match list based on the mini-editor's text", -> 328 | editor.getBuffer().insert([10,0], "t") 329 | editor.setCursorBufferPosition([10,0]) 330 | autocomplete.attach() 331 | 332 | expect(autocomplete.list.find('li').length).toBe 8 333 | miniEditor.textInput('c') 334 | window.advanceClock(autocomplete.inputThrottle) 335 | expect(autocomplete.list.find('li').length).toBe 3 336 | miniEditor.editor.backspace() 337 | window.advanceClock(autocomplete.inputThrottle) 338 | expect(autocomplete.list.find('li').length).toBe 8 339 | 340 | describe "when the text contains only word characters", -> 341 | it "narrows the list of completions with the fuzzy match algorithm", -> 342 | editor.getBuffer().insert([10,0] ,"t") 343 | editor.setCursorBufferPosition([10,0]) 344 | autocomplete.attach() 345 | 346 | expect(autocomplete.list.find('li').length).toBe 8 347 | miniEditor.textInput('i') 348 | window.advanceClock(autocomplete.inputThrottle) 349 | expect(autocomplete.list.find('li').length).toBe 4 350 | expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot' 351 | expect(autocomplete.list.find('li:eq(0)')).toHaveClass 'selected' 352 | expect(autocomplete.list.find('li:eq(1)')).toHaveText 'right' 353 | expect(autocomplete.list.find('li:eq(2)')).toHaveText 'shift' 354 | expect(autocomplete.list.find('li:eq(3)')).toHaveText 'quicksort' 355 | expect(editor.lineForBufferRow(10)).toEqual 'pivot' 356 | 357 | miniEditor.textInput('o') 358 | window.advanceClock(autocomplete.inputThrottle) 359 | expect(autocomplete.list.find('li').length).toBe 2 360 | expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot' 361 | expect(autocomplete.list.find('li:eq(1)')).toHaveText 'quicksort' 362 | 363 | describe "when a non-word character is typed in the mini-editor", -> 364 | it "immediately confirms the current completion choice and inserts that character into the buffer", -> 365 | editor.getBuffer().insert([10,0] ,"t") 366 | editor.setCursorBufferPosition([10,0]) 367 | autocomplete.attach() 368 | 369 | miniEditor.textInput('iv') 370 | window.advanceClock(autocomplete.inputThrottle) 371 | expect(autocomplete.list.find('li:eq(0)')).toHaveText 'pivot' 372 | 373 | miniEditor.textInput(' ') 374 | window.advanceClock(autocomplete.inputThrottle) 375 | expect(autocomplete.parent()).not.toExist() 376 | expect(editor.lineForBufferRow(10)).toEqual 'pivot ' 377 | 378 | describe 'when the mini-editor loses focus before the selection is confirmed', -> 379 | it "cancels the autocomplete", -> 380 | editorView.attachToDom() 381 | autocomplete.attach() 382 | spyOn(autocomplete, "cancel") 383 | 384 | editorView.focus() 385 | 386 | expect(autocomplete.cancel).toHaveBeenCalled() 387 | 388 | describe ".attach()", -> 389 | beforeEach -> 390 | editorView.attachToDom() 391 | setEditorHeightInLines(editorView, 13) 392 | editorView.resetDisplay() # Ensures the editor only has 13 lines visible 393 | 394 | describe "when the autocomplete view fits below the cursor", -> 395 | it "adds the autocomplete view to the editor below the cursor", -> 396 | editor.setCursorBufferPosition [1, 2] 397 | cursorPixelPosition = editorView.pixelPositionForScreenPosition(editor.getCursorScreenPosition()) 398 | autocomplete.attach() 399 | expect(editorView.find('.autocomplete')).toExist() 400 | 401 | expect(autocomplete.position().top).toBe cursorPixelPosition.top + editorView.lineHeight 402 | expect(autocomplete.position().left).toBe cursorPixelPosition.left 403 | 404 | describe "when the autocomplete view does not fit below the cursor", -> 405 | it "adds the autocomplete view to the editor above the cursor", -> 406 | editor.setCursorScreenPosition([11, 0]) 407 | editor.insertText('t ') 408 | editor.setCursorScreenPosition([11, 0]) 409 | cursorPixelPosition = editorView.pixelPositionForScreenPosition(editor.getCursorScreenPosition()) 410 | autocomplete.attach() 411 | 412 | expect(autocomplete.parent()).toExist() 413 | autocompleteBottom = autocomplete.position().top + autocomplete.outerHeight() 414 | expect(autocompleteBottom).toBe cursorPixelPosition.top 415 | expect(autocomplete.position().left).toBe cursorPixelPosition.left 416 | 417 | it "updates the position when the list is filtered and the height of the list decreases", -> 418 | editor.setCursorScreenPosition([11, 0]) 419 | editor.insertText('s') 420 | editor.setCursorScreenPosition([11, 0]) 421 | cursorPixelPosition = editorView.pixelPositionForScreenPosition(editor.getCursorScreenPosition()) 422 | autocomplete.attach() 423 | 424 | expect(autocomplete.parent()).toExist() 425 | autocompleteBottom = autocomplete.position().top + autocomplete.outerHeight() 426 | expect(autocompleteBottom).toBe cursorPixelPosition.top 427 | expect(autocomplete.position().left).toBe cursorPixelPosition.left 428 | 429 | miniEditor.textInput('sh') 430 | window.advanceClock(autocomplete.inputThrottle) 431 | 432 | expect(autocomplete.parent()).toExist() 433 | autocompleteBottom = autocomplete.position().top + autocomplete.outerHeight() 434 | expect(autocompleteBottom).toBe cursorPixelPosition.top 435 | expect(autocomplete.position().left).toBe cursorPixelPosition.left 436 | 437 | describe ".cancel()", -> 438 | it "clears the mini-editor and unbinds autocomplete event handlers for move-up and move-down", -> 439 | autocomplete.attach() 440 | miniEditor.setText('foo') 441 | 442 | autocomplete.cancel() 443 | expect(miniEditor.getText()).toBe '' 444 | 445 | editorView.trigger 'core:move-down' 446 | expect(editor.getCursorBufferPosition().row).toBe 1 447 | 448 | editorView.trigger 'core:move-up' 449 | expect(editor.getCursorBufferPosition().row).toBe 0 450 | 451 | it "sets the width of the view to be wide enough to contain the longest completion without scrolling", -> 452 | editorView.attachToDom() 453 | editor.insertText('thisIsAReallyReallyReallyLongCompletion ') 454 | editor.moveCursorToBottom() 455 | editor.insertNewline() 456 | editor.insertText('t') 457 | autocomplete.attach() 458 | expect(autocomplete.list.prop('scrollWidth')).toBe autocomplete.list.width() 459 | 460 | it "includes completions for the scope's completion preferences", -> 461 | waitsForPromise -> 462 | atom.packages.activatePackage('language-css') 463 | 464 | runs -> 465 | cssEditorView = new EditorView(editor: atom.project.openSync('css.css')) 466 | cssEditor = cssEditorView.editor 467 | autocomplete = new AutocompleteView(cssEditorView) 468 | 469 | cssEditorView.attachToDom() 470 | cssEditor.moveCursorToEndOfLine() 471 | cssEditor.insertText(' out') 472 | cssEditor.moveCursorToEndOfLine() 473 | 474 | autocomplete.attach() 475 | expect(autocomplete.list.find('li').length).toBe 5 476 | expect(autocomplete.list.find('li:eq(0)')).toHaveText 'outline' 477 | expect(autocomplete.list.find('li:eq(1)')).toHaveText 'outline-color' 478 | expect(autocomplete.list.find('li:eq(2)')).toHaveText 'outline-offset' 479 | expect(autocomplete.list.find('li:eq(3)')).toHaveText 'outline-style' 480 | expect(autocomplete.list.find('li:eq(4)')).toHaveText 'outline-width' 481 | --------------------------------------------------------------------------------