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.text
(prefixRange)); | |
129 var substituteWord = this._textEditor.text(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.text(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.text(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.text(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.text(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 |