Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / source_frame / TextEditorAutocompleteController.js
blobfc755380a398e468462f08f26742386226b22807
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.
5 /**
6 * @constructor
7 * @implements {WebInspector.SuggestBoxDelegate}
8 * @param {!WebInspector.CodeMirrorTextEditor} textEditor
9 * @param {!CodeMirror} codeMirror
11 WebInspector.TextEditorAutocompleteController = function(textEditor, codeMirror)
13 this._textEditor = textEditor;
14 this._codeMirror = codeMirror;
16 this._onScroll = this._onScroll.bind(this);
17 this._onCursorActivity = this._onCursorActivity.bind(this);
18 this._changes = this._changes.bind(this);
19 this._blur = this._blur.bind(this);
20 this._codeMirror.on("changes", this._changes);
22 this._enabled = true;
23 this._initialized = false;
26 WebInspector.TextEditorAutocompleteController.prototype = {
27 _initializeIfNeeded: function()
29 if (this._initialized)
30 return;
31 this._initialized = true;
32 this._codeMirror.on("scroll", this._onScroll);
33 this._codeMirror.on("cursorActivity", this._onCursorActivity);
34 this._codeMirror.on("blur", this._blur);
35 this._delegate.initialize(this._textEditor);
38 /**
39 * @param {!WebInspector.TextEditorAutocompleteDelegate} delegate
41 setDelegate: function(delegate)
43 if (this._delegate)
44 this._delegate.dispose();
45 this._delegate = delegate;
48 /**
49 * @param {boolean} enabled
51 setEnabled: function(enabled)
53 if (enabled === this._enabled)
54 return;
55 this._enabled = enabled;
56 if (!this._delegate)
57 return;
58 if (!enabled)
59 this._delegate.dispose();
60 else
61 this._delegate.initialize();
64 /**
65 * @param {!CodeMirror} codeMirror
66 * @param {!Array.<!CodeMirror.ChangeObject>} changes
68 _changes: function(codeMirror, changes)
70 if (!changes.length || !this._enabled || !this._delegate)
71 return;
73 var singleCharInput = false;
74 for (var changeIndex = 0; changeIndex < changes.length; ++changeIndex) {
75 var changeObject = changes[changeIndex];
76 singleCharInput = (changeObject.origin === "+input" && changeObject.text.length === 1 && changeObject.text[0].length === 1) ||
77 (this._suggestBox && changeObject.origin === "+delete" && changeObject.removed.length === 1 && changeObject.removed[0].length === 1);
79 if (singleCharInput)
80 setImmediate(this.autocomplete.bind(this));
83 _blur: function()
85 this.finishAutocomplete();
88 /**
89 * @param {!WebInspector.TextRange} mainSelection
90 * @return {boolean}
92 _validateSelectionsContexts: function(mainSelection)
94 var selections = this._codeMirror.listSelections();
95 if (selections.length <= 1)
96 return true;
97 var mainSelectionContext = this._textEditor.copyRange(mainSelection);
98 for (var i = 0; i < selections.length; ++i) {
99 var wordRange = this._delegate.substituteRange(this._textEditor, selections[i].head.line, selections[i].head.ch);
100 if (!wordRange)
101 return false;
102 var context = this._textEditor.copyRange(wordRange);
103 if (context !== mainSelectionContext)
104 return false;
106 return true;
109 autocomplete: function()
111 if (!this._enabled || !this._delegate)
112 return;
113 this._initializeIfNeeded();
114 if (this._codeMirror.somethingSelected()) {
115 this.finishAutocomplete();
116 return;
119 var cursor = this._codeMirror.getCursor("head");
120 var substituteRange = this._delegate.substituteRange(this._textEditor, cursor.line, cursor.ch);
121 if (!substituteRange || !this._validateSelectionsContexts(substituteRange)) {
122 this.finishAutocomplete();
123 return;
126 var prefixRange = substituteRange.clone();
127 prefixRange.endColumn = cursor.ch;
129 var wordsWithPrefix = this._delegate.wordsWithPrefix(this._textEditor, prefixRange, substituteRange);
130 if (!wordsWithPrefix.length) {
131 this.finishAutocomplete();
132 return;
135 if (!this._suggestBox)
136 this._suggestBox = new WebInspector.SuggestBox(this, 6);
137 var oldPrefixRange = this._prefixRange;
138 this._prefixRange = prefixRange;
139 if (!oldPrefixRange || prefixRange.startLine !== oldPrefixRange.startLine || prefixRange.startColumn !== oldPrefixRange.startColumn)
140 this._updateAnchorBox();
141 this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix, 0, true, this._textEditor.copyRange(prefixRange));
142 if (!this._suggestBox.visible())
143 this.finishAutocomplete();
144 this._onSuggestionsShownForTest(wordsWithPrefix);
148 * @param {!Array.<string>} suggestions
150 _onSuggestionsShownForTest: function(suggestions) { },
152 finishAutocomplete: function()
154 if (!this._suggestBox)
155 return;
156 this._suggestBox.hide();
157 this._suggestBox = null;
158 this._prefixRange = null;
159 this._anchorBox = null;
163 * @param {!Event} e
164 * @return {boolean}
166 keyDown: function(e)
168 if (!this._suggestBox)
169 return false;
170 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) {
171 this.finishAutocomplete();
172 return true;
174 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Tab.code) {
175 this._suggestBox.acceptSuggestion();
176 this.finishAutocomplete();
177 return true;
179 return this._suggestBox.keyPressed(e);
183 * @override
184 * @param {string} suggestion
185 * @param {boolean=} isIntermediateSuggestion
187 applySuggestion: function(suggestion, isIntermediateSuggestion)
189 this._currentSuggestion = suggestion;
193 * @override
195 acceptSuggestion: function()
197 if (this._prefixRange.endColumn - this._prefixRange.startColumn === this._currentSuggestion.length)
198 return;
200 var selections = this._codeMirror.listSelections().slice();
201 var prefixLength = this._prefixRange.endColumn - this._prefixRange.startColumn;
202 for (var i = selections.length - 1; i >= 0; --i) {
203 var start = selections[i].head;
204 var end = new CodeMirror.Pos(start.line, start.ch - prefixLength);
205 this._codeMirror.replaceRange(this._currentSuggestion, start, end, "+autocomplete");
209 _onScroll: function()
211 if (!this._suggestBox)
212 return;
213 var cursor = this._codeMirror.getCursor();
214 var scrollInfo = this._codeMirror.getScrollInfo();
215 var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "local");
216 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local");
217 if (cursor.line < topmostLineNumber || cursor.line > bottomLine)
218 this.finishAutocomplete();
219 else {
220 this._updateAnchorBox();
221 this._suggestBox.setPosition(this._anchorBox);
225 _onCursorActivity: function()
227 if (!this._suggestBox)
228 return;
229 var cursor = this._codeMirror.getCursor();
230 if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._prefixRange.endColumn || cursor.ch <= this._prefixRange.startColumn)
231 this.finishAutocomplete();
234 _updateAnchorBox: function()
236 var line = this._prefixRange.startLine;
237 var column = this._prefixRange.startColumn;
238 var metrics = this._textEditor.cursorPositionToCoordinates(line, column);
239 this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null;
244 * @interface
246 WebInspector.TextEditorAutocompleteDelegate = function() {}
248 WebInspector.TextEditorAutocompleteDelegate.prototype = {
250 * @param {!WebInspector.CodeMirrorTextEditor} editor
251 * @param {number} lineNumber
252 * @param {number} columnNumber
253 * @return {?WebInspector.TextRange}
255 substituteRange: function(editor, lineNumber, columnNumber) {},
258 * @param {!WebInspector.CodeMirrorTextEditor} editor
259 * @param {!WebInspector.TextRange} prefixRange
260 * @param {!WebInspector.TextRange} substituteRange
261 * @return {!Array.<string>}
263 wordsWithPrefix: function(editor, prefixRange, substituteRange) {},
266 * @param {!WebInspector.CodeMirrorTextEditor} editor
268 initialize: function(editor) {},
270 dispose: function() {}
274 * @constructor
275 * @implements {WebInspector.TextEditorAutocompleteDelegate}
276 * @param {string=} additionalWordChars
278 WebInspector.SimpleAutocompleteDelegate = function(additionalWordChars)
280 this._additionalWordChars = additionalWordChars;
283 WebInspector.SimpleAutocompleteDelegate.prototype = {
285 * @override
286 * @param {!WebInspector.CodeMirrorTextEditor} editor
288 initialize: function(editor)
290 if (this._dictionary)
291 this._dictionary.dispose();
292 this._dictionary = editor.createTextDictionary(this._additionalWordChars);
296 * @override
298 dispose: function()
300 if (this._dictionary) {
301 this._dictionary.dispose();
302 delete this._dictionary;
307 * @override
308 * @param {!WebInspector.CodeMirrorTextEditor} editor
309 * @param {number} lineNumber
310 * @param {number} columnNumber
311 * @return {?WebInspector.TextRange}
313 substituteRange: function(editor, lineNumber, columnNumber)
315 return editor.wordRangeForCursorPosition(lineNumber, columnNumber, this._dictionary.isWordChar.bind(this._dictionary));
319 * @override
320 * @param {!WebInspector.CodeMirrorTextEditor} editor
321 * @param {!WebInspector.TextRange} prefixRange
322 * @param {!WebInspector.TextRange} substituteRange
323 * @return {!Array.<string>}
325 wordsWithPrefix: function(editor, prefixRange, substituteRange)
327 if (prefixRange.startColumn === prefixRange.endColumn)
328 return [];
330 var dictionary = this._dictionary;
331 var completions = dictionary.wordsWithPrefix(editor.copyRange(prefixRange));
332 var substituteWord = editor.copyRange(substituteRange);
333 if (dictionary.wordCount(substituteWord) === 1)
334 completions = completions.filter(excludeFilter.bind(null, substituteWord));
336 completions.sort(sortSuggestions);
337 return completions;
339 function sortSuggestions(a, b)
341 return dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length;
344 function excludeFilter(excludeWord, word)
346 return word !== excludeWord;