| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * @constructor | |
| 7 * @implements {WebInspector.SuggestBoxDelegate} | |
| 8 * @param {!WebInspector.CodeMirrorTextEditor} textEditor | |
| 9 * @param {!CodeMirror} codeMirror | |
| 10 * @param {!WebInspector.AutocompleteConfig} config | |
| 11 */ | |
| 12 WebInspector.TextEditorAutocompleteController = function(textEditor, codeMirror,
config) | |
| 13 { | |
| 14 this._textEditor = textEditor; | |
| 15 this._codeMirror = codeMirror; | |
| 16 this._config = config; | |
| 17 this._initialized = false; | |
| 18 | |
| 19 this._onScroll = this._onScroll.bind(this); | |
| 20 this._onCursorActivity = this._onCursorActivity.bind(this); | |
| 21 this._changes = this._changes.bind(this); | |
| 22 this._blur = this._blur.bind(this); | |
| 23 this._beforeChange = this._beforeChange.bind(this); | |
| 24 this._mouseDown = this.finishAutocomplete.bind(this); | |
| 25 this._codeMirror.on("changes", this._changes); | |
| 26 | |
| 27 this._hintElement = createElementWithClass("span", "auto-complete-text"); | |
| 28 } | |
| 29 | |
| 30 WebInspector.TextEditorAutocompleteController.HintBookmark = Symbol("hint"); | |
| 31 | |
| 32 WebInspector.TextEditorAutocompleteController.prototype = { | |
| 33 _initializeIfNeeded: function() | |
| 34 { | |
| 35 if (this._initialized) | |
| 36 return; | |
| 37 this._initialized = true; | |
| 38 this._codeMirror.on("scroll", this._onScroll); | |
| 39 this._codeMirror.on("cursorActivity", this._onCursorActivity); | |
| 40 this._codeMirror.on("mousedown", this._mouseDown); | |
| 41 this._codeMirror.on("blur", this._blur); | |
| 42 if (this._config.isWordChar) { | |
| 43 this._codeMirror.on("beforeChange", this._beforeChange); | |
| 44 this._dictionary = new WebInspector.TextDictionary(); | |
| 45 this._addWordsFromText(this._codeMirror.getValue()); | |
| 46 } | |
| 47 }, | |
| 48 | |
| 49 dispose: function() | |
| 50 { | |
| 51 this._codeMirror.off("changes", this._changes); | |
| 52 if (this._initialized) { | |
| 53 this._codeMirror.off("scroll", this._onScroll); | |
| 54 this._codeMirror.off("cursorActivity", this._onCursorActivity); | |
| 55 this._codeMirror.off("mousedown", this._mouseDown); | |
| 56 this._codeMirror.off("blur", this._blur); | |
| 57 } | |
| 58 if (this._dictionary) { | |
| 59 this._codeMirror.off("beforeChange", this._beforeChange); | |
| 60 this._dictionary.reset(); | |
| 61 } | |
| 62 }, | |
| 63 | |
| 64 /** | |
| 65 * @param {!CodeMirror} codeMirror | |
| 66 * @param {!CodeMirror.BeforeChangeObject} changeObject | |
| 67 */ | |
| 68 _beforeChange: function(codeMirror, changeObject) | |
| 69 { | |
| 70 this._updatedLines = this._updatedLines || {}; | |
| 71 for (var i = changeObject.from.line; i <= changeObject.to.line; ++i) | |
| 72 this._updatedLines[i] = this._codeMirror.getLine(i); | |
| 73 }, | |
| 74 | |
| 75 /** | |
| 76 * @param {string} text | |
| 77 */ | |
| 78 _addWordsFromText: function(text) | |
| 79 { | |
| 80 WebInspector.TextUtils.textToWords(text, /** @type {function(string):boo
lean} */ (this._config.isWordChar), addWord.bind(this)); | |
| 81 | |
| 82 /** | |
| 83 * @param {string} word | |
| 84 * @this {WebInspector.TextEditorAutocompleteController} | |
| 85 */ | |
| 86 function addWord(word) | |
| 87 { | |
| 88 if (word.length && (word[0] < "0" || word[0] > "9")) | |
| 89 this._dictionary.addWord(word); | |
| 90 } | |
| 91 }, | |
| 92 | |
| 93 /** | |
| 94 * @param {string} text | |
| 95 */ | |
| 96 _removeWordsFromText: function(text) | |
| 97 { | |
| 98 WebInspector.TextUtils.textToWords(text, /** @type {function(string):boo
lean} */ (this._config.isWordChar), (word) => this._dictionary.removeWord(word)
); | |
| 99 }, | |
| 100 | |
| 101 /** | |
| 102 * @param {number} lineNumber | |
| 103 * @param {number} columnNumber | |
| 104 * @return {?WebInspector.TextRange} | |
| 105 */ | |
| 106 _substituteRange: function(lineNumber, columnNumber) | |
| 107 { | |
| 108 var range = this._config.substituteRangeCallback ? this._config.substitu
teRangeCallback(lineNumber, columnNumber) : null; | |
| 109 if (!range && this._config.isWordChar) | |
| 110 range = this._textEditor.wordRangeForCursorPosition(lineNumber, colu
mnNumber, this._config.isWordChar); | |
| 111 return range; | |
| 112 }, | |
| 113 | |
| 114 /** | |
| 115 * @param {!WebInspector.TextRange} prefixRange | |
| 116 * @param {!WebInspector.TextRange} substituteRange | |
| 117 * @return {!Promise.<!WebInspector.SuggestBox.Suggestions>} | |
| 118 */ | |
| 119 _wordsWithPrefix: function(prefixRange, substituteRange) | |
| 120 { | |
| 121 var external = this._config.suggestionsCallback ? this._config.suggestio
nsCallback(prefixRange, substituteRange) : null; | |
| 122 if (external) | |
| 123 return external; | |
| 124 | |
| 125 if (!this._dictionary || prefixRange.startColumn === prefixRange.endColu
mn) | |
| 126 return Promise.resolve([]); | |
| 127 | |
| 128 var completions = this._dictionary.wordsWithPrefix(this._textEditor.copy
Range(prefixRange)); | |
| 129 var substituteWord = this._textEditor.copyRange(substituteRange); | |
| 130 if (this._dictionary.wordCount(substituteWord) === 1) | |
| 131 completions = completions.filter((word) => word !== substituteWord); | |
| 132 | |
| 133 completions.sort((a, b) => this._dictionary.wordCount(b) - this._diction
ary.wordCount(a) || a.length - b.length); | |
| 134 return Promise.resolve(completions.map(item => ({ title: item }))); | |
| 135 }, | |
| 136 | |
| 137 /** | |
| 138 * @param {!CodeMirror} codeMirror | |
| 139 * @param {!Array.<!CodeMirror.ChangeObject>} changes | |
| 140 */ | |
| 141 _changes: function(codeMirror, changes) | |
| 142 { | |
| 143 if (!changes.length) | |
| 144 return; | |
| 145 | |
| 146 if (this._dictionary && this._updatedLines) { | |
| 147 for (var lineNumber in this._updatedLines) | |
| 148 this._removeWordsFromText(this._updatedLines[lineNumber]); | |
| 149 delete this._updatedLines; | |
| 150 | |
| 151 var linesToUpdate = {}; | |
| 152 for (var changeIndex = 0; changeIndex < changes.length; ++changeInde
x) { | |
| 153 var changeObject = changes[changeIndex]; | |
| 154 var editInfo = WebInspector.CodeMirrorUtils.changeObjectToEditOp
eration(changeObject); | |
| 155 for (var i = editInfo.newRange.startLine; i <= editInfo.newRange
.endLine; ++i) | |
| 156 linesToUpdate[i] = this._codeMirror.getLine(i); | |
| 157 } | |
| 158 for (var lineNumber in linesToUpdate) | |
| 159 this._addWordsFromText(linesToUpdate[lineNumber]); | |
| 160 } | |
| 161 | |
| 162 var singleCharInput = false; | |
| 163 var singleCharDelete = false; | |
| 164 var cursor = this._codeMirror.getCursor("head"); | |
| 165 for (var changeIndex = 0; changeIndex < changes.length; ++changeIndex) { | |
| 166 var changeObject = changes[changeIndex]; | |
| 167 if (changeObject.origin === "+input" | |
| 168 && changeObject.text.length === 1 | |
| 169 && changeObject.text[0].length === 1 | |
| 170 && changeObject.to.line === cursor.line | |
| 171 && changeObject.to.ch + 1 === cursor.ch) { | |
| 172 singleCharInput = true; | |
| 173 break; | |
| 174 } | |
| 175 if (this._suggestBox | |
| 176 && changeObject.origin === "+delete" | |
| 177 && changeObject.removed.length === 1 | |
| 178 && changeObject.removed[0].length === 1 | |
| 179 && changeObject.to.line === cursor.line | |
| 180 && changeObject.to.ch - 1 === cursor.ch) { | |
| 181 singleCharDelete = true; | |
| 182 break; | |
| 183 } | |
| 184 } | |
| 185 if (singleCharInput && this._hintMarker) | |
| 186 this._hintElement.textContent = this._hintElement.textContent.substr
ing(1); | |
| 187 | |
| 188 if (singleCharDelete && this._hintMarker && this._lastPrefix) { | |
| 189 this._hintElement.textContent = this._lastPrefix.charAt(this._lastPr
efix.length - 1) + this._hintElement.textContent; | |
| 190 this._lastPrefix = this._lastPrefix.substring(0, this._lastPrefix.le
ngth - 1); | |
| 191 } | |
| 192 if (singleCharInput || singleCharDelete) | |
| 193 setImmediate(this.autocomplete.bind(this)); | |
| 194 else | |
| 195 this.finishAutocomplete(); | |
| 196 }, | |
| 197 | |
| 198 _blur: function() | |
| 199 { | |
| 200 this.finishAutocomplete(); | |
| 201 }, | |
| 202 | |
| 203 /** | |
| 204 * @param {!WebInspector.TextRange} mainSelection | |
| 205 * @return {boolean} | |
| 206 */ | |
| 207 _validateSelectionsContexts: function(mainSelection) | |
| 208 { | |
| 209 var selections = this._codeMirror.listSelections(); | |
| 210 if (selections.length <= 1) | |
| 211 return true; | |
| 212 var mainSelectionContext = this._textEditor.copyRange(mainSelection); | |
| 213 for (var i = 0; i < selections.length; ++i) { | |
| 214 var wordRange = this._substituteRange(selections[i].head.line, selec
tions[i].head.ch); | |
| 215 if (!wordRange) | |
| 216 return false; | |
| 217 var context = this._textEditor.copyRange(wordRange); | |
| 218 if (context !== mainSelectionContext) | |
| 219 return false; | |
| 220 } | |
| 221 return true; | |
| 222 }, | |
| 223 | |
| 224 autocomplete: function() | |
| 225 { | |
| 226 this._initializeIfNeeded(); | |
| 227 if (this._codeMirror.somethingSelected()) { | |
| 228 this.finishAutocomplete(); | |
| 229 return; | |
| 230 } | |
| 231 | |
| 232 var cursor = this._codeMirror.getCursor("head"); | |
| 233 var substituteRange = this._substituteRange(cursor.line, cursor.ch); | |
| 234 if (!substituteRange || !this._validateSelectionsContexts(substituteRang
e)) { | |
| 235 this.finishAutocomplete(); | |
| 236 return; | |
| 237 } | |
| 238 | |
| 239 var prefixRange = substituteRange.clone(); | |
| 240 prefixRange.endColumn = cursor.ch; | |
| 241 var prefix = this._textEditor.copyRange(prefixRange); | |
| 242 var hadSuggestBox = false; | |
| 243 if (this._suggestBox) | |
| 244 hadSuggestBox = true; | |
| 245 | |
| 246 this._wordsWithPrefix(prefixRange, substituteRange).then(wordsAcquired.b
ind(this)); | |
| 247 | |
| 248 /** | |
| 249 * @param {!WebInspector.SuggestBox.Suggestions} wordsWithPrefix | |
| 250 * @this {WebInspector.TextEditorAutocompleteController} | |
| 251 */ | |
| 252 function wordsAcquired(wordsWithPrefix) | |
| 253 { | |
| 254 if (!wordsWithPrefix.length || (wordsWithPrefix.length === 1 && pref
ix === wordsWithPrefix[0].title) || (!this._suggestBox && hadSuggestBox)) { | |
| 255 this.finishAutocomplete(); | |
| 256 this._onSuggestionsShownForTest([]); | |
| 257 return; | |
| 258 } | |
| 259 if (!this._suggestBox) | |
| 260 this._suggestBox = new WebInspector.SuggestBox(this, 6); | |
| 261 | |
| 262 var oldPrefixRange = this._prefixRange; | |
| 263 this._prefixRange = prefixRange; | |
| 264 if (!oldPrefixRange || prefixRange.startLine !== oldPrefixRange.star
tLine || prefixRange.startColumn !== oldPrefixRange.startColumn) | |
| 265 this._updateAnchorBox(); | |
| 266 this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix,
0, !this._isCursorAtEndOfLine(), prefix); | |
| 267 this._onSuggestionsShownForTest(wordsWithPrefix); | |
| 268 this._addHintMarker(wordsWithPrefix[0].title); | |
| 269 } | |
| 270 }, | |
| 271 | |
| 272 /** | |
| 273 * @param {string} hint | |
| 274 */ | |
| 275 _addHintMarker: function(hint) | |
| 276 { | |
| 277 this._clearHintMarker(); | |
| 278 if (!this._isCursorAtEndOfLine()) | |
| 279 return; | |
| 280 var prefix = this._textEditor.copyRange(this._prefixRange); | |
| 281 this._lastPrefix = prefix; | |
| 282 this._hintElement.textContent = hint.substring(prefix.length).split("\n"
)[0]; | |
| 283 var cursor = this._codeMirror.getCursor("to"); | |
| 284 this._hintMarker = this._textEditor.addBookmark(cursor.line, cursor.ch,
this._hintElement, WebInspector.TextEditorAutocompleteController.HintBookmark, t
rue); | |
| 285 }, | |
| 286 | |
| 287 _clearHintMarker: function() | |
| 288 { | |
| 289 if (!this._hintMarker) | |
| 290 return; | |
| 291 this._hintMarker.clear(); | |
| 292 delete this._hintMarker; | |
| 293 }, | |
| 294 | |
| 295 /** | |
| 296 * @param {!Array<{className: (string|undefined), title: string}>} suggestio
ns | |
| 297 */ | |
| 298 _onSuggestionsShownForTest: function(suggestions) { }, | |
| 299 | |
| 300 finishAutocomplete: function() | |
| 301 { | |
| 302 if (!this._suggestBox) | |
| 303 return; | |
| 304 this._suggestBox.hide(); | |
| 305 this._suggestBox = null; | |
| 306 this._prefixRange = null; | |
| 307 this._anchorBox = null; | |
| 308 this._clearHintMarker(); | |
| 309 }, | |
| 310 | |
| 311 /** | |
| 312 * @param {!Event} event | |
| 313 * @return {boolean} | |
| 314 */ | |
| 315 keyDown: function(event) | |
| 316 { | |
| 317 if (!this._suggestBox) | |
| 318 return false; | |
| 319 switch (event.keyCode) { | |
| 320 case WebInspector.KeyboardShortcut.Keys.Tab.code: | |
| 321 this._suggestBox.acceptSuggestion(); | |
| 322 this.finishAutocomplete(); | |
| 323 return true; | |
| 324 case WebInspector.KeyboardShortcut.Keys.End.code: | |
| 325 case WebInspector.KeyboardShortcut.Keys.Right.code: | |
| 326 if (this._isCursorAtEndOfLine()) { | |
| 327 this._suggestBox.acceptSuggestion(); | |
| 328 this.finishAutocomplete(); | |
| 329 return true; | |
| 330 } else { | |
| 331 this.finishAutocomplete(); | |
| 332 return false; | |
| 333 } | |
| 334 case WebInspector.KeyboardShortcut.Keys.Left.code: | |
| 335 case WebInspector.KeyboardShortcut.Keys.Home.code: | |
| 336 this.finishAutocomplete(); | |
| 337 return false; | |
| 338 case WebInspector.KeyboardShortcut.Keys.Esc.code: | |
| 339 this.finishAutocomplete(); | |
| 340 return true; | |
| 341 } | |
| 342 return this._suggestBox.keyPressed(event); | |
| 343 }, | |
| 344 | |
| 345 /** | |
| 346 * @return {boolean} | |
| 347 */ | |
| 348 _isCursorAtEndOfLine: function() | |
| 349 { | |
| 350 var cursor = this._codeMirror.getCursor("to"); | |
| 351 return cursor.ch === this._codeMirror.getLine(cursor.line).length; | |
| 352 }, | |
| 353 | |
| 354 /** | |
| 355 * @override | |
| 356 * @param {string} suggestion | |
| 357 * @param {boolean=} isIntermediateSuggestion | |
| 358 */ | |
| 359 applySuggestion: function(suggestion, isIntermediateSuggestion) | |
| 360 { | |
| 361 this._currentSuggestion = suggestion; | |
| 362 this._addHintMarker(suggestion); | |
| 363 }, | |
| 364 | |
| 365 /** | |
| 366 * @override | |
| 367 */ | |
| 368 acceptSuggestion: function() | |
| 369 { | |
| 370 if (this._prefixRange.endColumn - this._prefixRange.startColumn === this
._currentSuggestion.length) | |
| 371 return; | |
| 372 | |
| 373 var selections = this._codeMirror.listSelections().slice(); | |
| 374 var prefixLength = this._prefixRange.endColumn - this._prefixRange.start
Column; | |
| 375 for (var i = selections.length - 1; i >= 0; --i) { | |
| 376 var start = selections[i].head; | |
| 377 var end = new CodeMirror.Pos(start.line, start.ch - prefixLength); | |
| 378 this._codeMirror.replaceRange(this._currentSuggestion, start, end, "
+autocomplete"); | |
| 379 } | |
| 380 }, | |
| 381 | |
| 382 _onScroll: function() | |
| 383 { | |
| 384 if (!this._suggestBox) | |
| 385 return; | |
| 386 var cursor = this._codeMirror.getCursor(); | |
| 387 var scrollInfo = this._codeMirror.getScrollInfo(); | |
| 388 var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "l
ocal"); | |
| 389 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollIn
fo.clientHeight, "local"); | |
| 390 if (cursor.line < topmostLineNumber || cursor.line > bottomLine) | |
| 391 this.finishAutocomplete(); | |
| 392 else { | |
| 393 this._updateAnchorBox(); | |
| 394 this._suggestBox.setPosition(this._anchorBox); | |
| 395 } | |
| 396 }, | |
| 397 | |
| 398 _onCursorActivity: function() | |
| 399 { | |
| 400 if (!this._suggestBox) | |
| 401 return; | |
| 402 var cursor = this._codeMirror.getCursor(); | |
| 403 if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._pre
fixRange.endColumn + 1 || cursor.ch <= this._prefixRange.startColumn) | |
| 404 this.finishAutocomplete(); | |
| 405 }, | |
| 406 | |
| 407 _updateAnchorBox: function() | |
| 408 { | |
| 409 var line = this._prefixRange.startLine; | |
| 410 var column = this._prefixRange.startColumn; | |
| 411 var metrics = this._textEditor.cursorPositionToCoordinates(line, column)
; | |
| 412 this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metri
cs.height) : null; | |
| 413 }, | |
| 414 } | |
| 415 | |
| 416 /** | |
| 417 * @typedef {{ | |
| 418 * substituteRangeCallback: ((function(number, number):?WebInspector.TextRan
ge)|undefined), | |
| 419 * suggestionsCallback: ((function(!WebInspector.TextRange, !WebInspector.Te
xtRange):?Promise.<!WebInspector.SuggestBox.Suggestions>)|undefined), | |
| 420 * isWordChar: ((function(string):boolean)|undefined) | |
| 421 * }} | |
| 422 **/ | |
| 423 WebInspector.AutocompleteConfig; | |
| OLD | NEW |