blob: fc755380a398e468462f08f26742386226b22807 [file] [log] [blame]
// 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;
}
}
}