| // Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.SuggestBoxDelegate} |
| * @param {!WebInspector.CodeMirrorTextEditor} textEditor |
| * @param {!CodeMirror} codeMirror |
| */ |
| WebInspector.TextEditorAutocompleteController = function(textEditor, codeMirror) |
| { |
| this._textEditor = textEditor; |
| this._codeMirror = codeMirror; |
| |
| this._onScroll = this._onScroll.bind(this); |
| this._onCursorActivity = this._onCursorActivity.bind(this); |
| this._changes = this._changes.bind(this); |
| this._blur = this._blur.bind(this); |
| this._codeMirror.on("changes", this._changes); |
| |
| this._enabled = true; |
| this._initialized = false; |
| } |
| |
| WebInspector.TextEditorAutocompleteController.prototype = { |
| _initializeIfNeeded: function() |
| { |
| if (this._initialized) |
| return; |
| this._initialized = true; |
| this._codeMirror.on("scroll", this._onScroll); |
| this._codeMirror.on("cursorActivity", this._onCursorActivity); |
| this._codeMirror.on("blur", this._blur); |
| this._delegate.initialize(this._textEditor); |
| }, |
| |
| /** |
| * @param {!WebInspector.TextEditorAutocompleteDelegate} delegate |
| */ |
| setDelegate: function(delegate) |
| { |
| if (this._delegate) |
| this._delegate.dispose(); |
| this._delegate = delegate; |
| }, |
| |
| /** |
| * @param {boolean} enabled |
| */ |
| setEnabled: function(enabled) |
| { |
| if (enabled === this._enabled) |
| return; |
| this._enabled = enabled; |
| if (!this._delegate) |
| return; |
| if (!enabled) |
| this._delegate.dispose(); |
| else |
| this._delegate.initialize(); |
| }, |
| |
| /** |
| * @param {!CodeMirror} codeMirror |
| * @param {!Array.<!CodeMirror.ChangeObject>} changes |
| */ |
| _changes: function(codeMirror, changes) |
| { |
| if (!changes.length || !this._enabled || !this._delegate) |
| return; |
| |
| var singleCharInput = false; |
| for (var changeIndex = 0; changeIndex < changes.length; ++changeIndex) { |
| var changeObject = changes[changeIndex]; |
| singleCharInput = (changeObject.origin === "+input" && changeObject.text.length === 1 && changeObject.text[0].length === 1) || |
| (this._suggestBox && changeObject.origin === "+delete" && changeObject.removed.length === 1 && changeObject.removed[0].length === 1); |
| } |
| if (singleCharInput) |
| setImmediate(this.autocomplete.bind(this)); |
| }, |
| |
| _blur: function() |
| { |
| this.finishAutocomplete(); |
| }, |
| |
| /** |
| * @param {!WebInspector.TextRange} mainSelection |
| * @return {boolean} |
| */ |
| _validateSelectionsContexts: function(mainSelection) |
| { |
| var selections = this._codeMirror.listSelections(); |
| if (selections.length <= 1) |
| return true; |
| var mainSelectionContext = this._textEditor.copyRange(mainSelection); |
| for (var i = 0; i < selections.length; ++i) { |
| var wordRange = this._delegate.substituteRange(this._textEditor, selections[i].head.line, selections[i].head.ch); |
| if (!wordRange) |
| return false; |
| var context = this._textEditor.copyRange(wordRange); |
| if (context !== mainSelectionContext) |
| return false; |
| } |
| return true; |
| }, |
| |
| autocomplete: function() |
| { |
| if (!this._enabled || !this._delegate) |
| return; |
| this._initializeIfNeeded(); |
| if (this._codeMirror.somethingSelected()) { |
| this.finishAutocomplete(); |
| return; |
| } |
| |
| var cursor = this._codeMirror.getCursor("head"); |
| var substituteRange = this._delegate.substituteRange(this._textEditor, cursor.line, cursor.ch); |
| if (!substituteRange || !this._validateSelectionsContexts(substituteRange)) { |
| this.finishAutocomplete(); |
| return; |
| } |
| |
| var prefixRange = substituteRange.clone(); |
| prefixRange.endColumn = cursor.ch; |
| |
| var wordsWithPrefix = this._delegate.wordsWithPrefix(this._textEditor, prefixRange, substituteRange); |
| if (!wordsWithPrefix.length) { |
| this.finishAutocomplete(); |
| return; |
| } |
| |
| if (!this._suggestBox) |
| this._suggestBox = new WebInspector.SuggestBox(this, 6); |
| var oldPrefixRange = this._prefixRange; |
| this._prefixRange = prefixRange; |
| if (!oldPrefixRange || prefixRange.startLine !== oldPrefixRange.startLine || prefixRange.startColumn !== oldPrefixRange.startColumn) |
| this._updateAnchorBox(); |
| this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix, 0, true, this._textEditor.copyRange(prefixRange)); |
| if (!this._suggestBox.visible()) |
| this.finishAutocomplete(); |
| this._onSuggestionsShownForTest(wordsWithPrefix); |
| }, |
| |
| /** |
| * @param {!Array.<string>} suggestions |
| */ |
| _onSuggestionsShownForTest: function(suggestions) { }, |
| |
| finishAutocomplete: function() |
| { |
| if (!this._suggestBox) |
| return; |
| this._suggestBox.hide(); |
| this._suggestBox = null; |
| this._prefixRange = null; |
| this._anchorBox = null; |
| }, |
| |
| /** |
| * @param {!Event} e |
| * @return {boolean} |
| */ |
| keyDown: function(e) |
| { |
| if (!this._suggestBox) |
| return false; |
| if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) { |
| this.finishAutocomplete(); |
| return true; |
| } |
| if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Tab.code) { |
| this._suggestBox.acceptSuggestion(); |
| this.finishAutocomplete(); |
| return true; |
| } |
| return this._suggestBox.keyPressed(e); |
| }, |
| |
| /** |
| * @override |
| * @param {string} suggestion |
| * @param {boolean=} isIntermediateSuggestion |
| */ |
| applySuggestion: function(suggestion, isIntermediateSuggestion) |
| { |
| this._currentSuggestion = suggestion; |
| }, |
| |
| /** |
| * @override |
| */ |
| acceptSuggestion: function() |
| { |
| if (this._prefixRange.endColumn - this._prefixRange.startColumn === this._currentSuggestion.length) |
| return; |
| |
| var selections = this._codeMirror.listSelections().slice(); |
| var prefixLength = this._prefixRange.endColumn - this._prefixRange.startColumn; |
| for (var i = selections.length - 1; i >= 0; --i) { |
| var start = selections[i].head; |
| var end = new CodeMirror.Pos(start.line, start.ch - prefixLength); |
| this._codeMirror.replaceRange(this._currentSuggestion, start, end, "+autocomplete"); |
| } |
| }, |
| |
| _onScroll: function() |
| { |
| if (!this._suggestBox) |
| return; |
| var cursor = this._codeMirror.getCursor(); |
| var scrollInfo = this._codeMirror.getScrollInfo(); |
| var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "local"); |
| var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); |
| if (cursor.line < topmostLineNumber || cursor.line > bottomLine) |
| this.finishAutocomplete(); |
| else { |
| this._updateAnchorBox(); |
| this._suggestBox.setPosition(this._anchorBox); |
| } |
| }, |
| |
| _onCursorActivity: function() |
| { |
| if (!this._suggestBox) |
| return; |
| var cursor = this._codeMirror.getCursor(); |
| if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._prefixRange.endColumn || cursor.ch <= this._prefixRange.startColumn) |
| this.finishAutocomplete(); |
| }, |
| |
| _updateAnchorBox: function() |
| { |
| var line = this._prefixRange.startLine; |
| var column = this._prefixRange.startColumn; |
| var metrics = this._textEditor.cursorPositionToCoordinates(line, column); |
| this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null; |
| }, |
| } |
| |
| /** |
| * @interface |
| */ |
| WebInspector.TextEditorAutocompleteDelegate = function() {} |
| |
| WebInspector.TextEditorAutocompleteDelegate.prototype = { |
| /** |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @return {?WebInspector.TextRange} |
| */ |
| substituteRange: function(editor, lineNumber, columnNumber) {}, |
| |
| /** |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| * @param {!WebInspector.TextRange} prefixRange |
| * @param {!WebInspector.TextRange} substituteRange |
| * @return {!Array.<string>} |
| */ |
| wordsWithPrefix: function(editor, prefixRange, substituteRange) {}, |
| |
| /** |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| */ |
| initialize: function(editor) {}, |
| |
| dispose: function() {} |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TextEditorAutocompleteDelegate} |
| * @param {string=} additionalWordChars |
| */ |
| WebInspector.SimpleAutocompleteDelegate = function(additionalWordChars) |
| { |
| this._additionalWordChars = additionalWordChars; |
| } |
| |
| WebInspector.SimpleAutocompleteDelegate.prototype = { |
| /** |
| * @override |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| */ |
| initialize: function(editor) |
| { |
| if (this._dictionary) |
| this._dictionary.dispose(); |
| this._dictionary = editor.createTextDictionary(this._additionalWordChars); |
| }, |
| |
| /** |
| * @override |
| */ |
| dispose: function() |
| { |
| if (this._dictionary) { |
| this._dictionary.dispose(); |
| delete this._dictionary; |
| } |
| }, |
| |
| /** |
| * @override |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @return {?WebInspector.TextRange} |
| */ |
| substituteRange: function(editor, lineNumber, columnNumber) |
| { |
| return editor.wordRangeForCursorPosition(lineNumber, columnNumber, this._dictionary.isWordChar.bind(this._dictionary)); |
| }, |
| |
| /** |
| * @override |
| * @param {!WebInspector.CodeMirrorTextEditor} editor |
| * @param {!WebInspector.TextRange} prefixRange |
| * @param {!WebInspector.TextRange} substituteRange |
| * @return {!Array.<string>} |
| */ |
| wordsWithPrefix: function(editor, prefixRange, substituteRange) |
| { |
| if (prefixRange.startColumn === prefixRange.endColumn) |
| return []; |
| |
| var dictionary = this._dictionary; |
| var completions = dictionary.wordsWithPrefix(editor.copyRange(prefixRange)); |
| var substituteWord = editor.copyRange(substituteRange); |
| if (dictionary.wordCount(substituteWord) === 1) |
| completions = completions.filter(excludeFilter.bind(null, substituteWord)); |
| |
| completions.sort(sortSuggestions); |
| return completions; |
| |
| function sortSuggestions(a, b) |
| { |
| return dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length; |
| } |
| |
| function excludeFilter(excludeWord, word) |
| { |
| return word !== excludeWord; |
| } |
| } |
| } |