blob: c94a75a967f63d85e7fcef295a9d9ca584ba7c32 [file] [log] [blame]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.CodeMirrorCompletionController = class CodeMirrorCompletionController extends WI.Object
{
constructor(codeMirror, delegate, stopCharactersRegex)
{
super();
console.assert(codeMirror);
this._codeMirror = codeMirror;
this._stopCharactersRegex = stopCharactersRegex || null;
this._delegate = delegate || null;
this._startOffset = NaN;
this._endOffset = NaN;
this._lineNumber = NaN;
this._prefix = "";
this._noEndingSemicolon = false;
this._completions = [];
this._extendedCompletionProviders = {};
this._suggestionsView = new WI.CompletionSuggestionsView(this);
this._keyMap = {
"Up": this._handleUpKey.bind(this),
"Down": this._handleDownKey.bind(this),
"Right": this._handleRightOrEnterKey.bind(this),
"Esc": this._handleEscapeKey.bind(this),
"Enter": this._handleRightOrEnterKey.bind(this),
"Tab": this._handleTabKey.bind(this),
"Cmd-A": this._handleHideKey.bind(this),
"Cmd-Z": this._handleHideKey.bind(this),
"Shift-Cmd-Z": this._handleHideKey.bind(this),
"Cmd-Y": this._handleHideKey.bind(this)
};
this._handleChangeListener = this._handleChange.bind(this);
this._handleCursorActivityListener = this._handleCursorActivity.bind(this);
this._handleHideActionListener = this._handleHideAction.bind(this);
this._codeMirror.addKeyMap(this._keyMap);
this._codeMirror.on("change", this._handleChangeListener);
this._codeMirror.on("cursorActivity", this._handleCursorActivityListener);
this._codeMirror.on("blur", this._handleHideActionListener);
this._codeMirror.on("scroll", this._handleHideActionListener);
this._updatePromise = null;
}
// Public
get delegate()
{
return this._delegate;
}
addExtendedCompletionProvider(modeName, provider)
{
this._extendedCompletionProviders[modeName] = provider;
}
updateCompletions(completions, implicitSuffix)
{
if (isNaN(this._startOffset) || isNaN(this._endOffset) || isNaN(this._lineNumber))
return;
if (!completions || !completions.length) {
this.hideCompletions();
return;
}
this._completions = completions;
if (typeof implicitSuffix === "string")
this._implicitSuffix = implicitSuffix;
var from = {line: this._lineNumber, ch: this._startOffset};
var to = {line: this._lineNumber, ch: this._endOffset};
var firstCharCoords = this._codeMirror.cursorCoords(from);
var lastCharCoords = this._codeMirror.cursorCoords(to);
var bounds = new WI.Rect(firstCharCoords.left, firstCharCoords.top, lastCharCoords.right - firstCharCoords.left, firstCharCoords.bottom - firstCharCoords.top);
// Try to restore the previous selected index, otherwise just select the first.
var index = this._currentCompletion ? completions.indexOf(this._currentCompletion) : 0;
if (index === -1)
index = 0;
if (this._forced || completions.length > 1 || completions[index] !== this._prefix) {
// Update and show the suggestion list.
this._suggestionsView.update(completions, index);
this._suggestionsView.show(bounds);
} else if (this._implicitSuffix) {
// The prefix and the completion exactly match, but there is an implicit suffix.
// Just hide the suggestion list and keep the completion hint for the implicit suffix.
this._suggestionsView.hide();
} else {
// The prefix and the completion exactly match, hide the completions. Return early so
// the completion hint isn't updated.
this.hideCompletions();
return;
}
this._applyCompletionHint(completions[index]);
this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.CompletionsFound);
}
isCompletionChange(change)
{
return this._ignoreChange || change.origin === WI.CodeMirrorCompletionController.CompletionOrigin || change.origin === WI.CodeMirrorCompletionController.DeleteCompletionOrigin;
}
isShowingCompletions()
{
return this._suggestionsView.visible || (this._completionHintMarker && this._completionHintMarker.find());
}
isHandlingClickEvent()
{
return this._suggestionsView.isHandlingClickEvent();
}
hideCompletions()
{
this._suggestionsView.hide();
this._removeCompletionHint();
this._startOffset = NaN;
this._endOffset = NaN;
this._lineNumber = NaN;
this._prefix = "";
this._completions = [];
this._implicitSuffix = "";
this._forced = false;
delete this._currentCompletion;
delete this._ignoreNextCursorActivity;
this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound);
}
close()
{
this._codeMirror.removeKeyMap(this._keyMap);
this._codeMirror.off("change", this._handleChangeListener);
this._codeMirror.off("cursorActivity", this._handleCursorActivityListener);
this._codeMirror.off("blur", this._handleHideActionListener);
this._codeMirror.off("scroll", this._handleHideActionListener);
}
completeAtCurrentPositionIfNeeded(force)
{
this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.Canceled);
var update = this._updatePromise = new WI.WrappedPromise;
this._completeAtCurrentPosition(force);
return update.promise;
}
// Protected
completionSuggestionsSelectedCompletion(suggestionsView, completionText)
{
this._applyCompletionHint(completionText);
}
completionSuggestionsClickedCompletion(suggestionsView, completionText)
{
// The clicked suggestion causes the editor to loose focus. Restore it so the user can keep typing.
this._codeMirror.focus();
this._applyCompletionHint(completionText);
this._commitCompletionHint();
}
set noEndingSemicolon(noEndingSemicolon)
{
this._noEndingSemicolon = noEndingSemicolon;
}
// Private
_resolveUpdatePromise(message)
{
if (!this._updatePromise)
return;
this._updatePromise.resolve(message);
this._updatePromise = null;
}
get _currentReplacementText()
{
return this._currentCompletion + this._implicitSuffix;
}
_hasPendingCompletion()
{
return !isNaN(this._startOffset) && !isNaN(this._endOffset) && !isNaN(this._lineNumber);
}
_notifyCompletionsHiddenSoon()
{
function notify()
{
if (this._completionHintMarker)
return;
if (this._delegate && typeof this._delegate.completionControllerCompletionsHidden === "function")
this._delegate.completionControllerCompletionsHidden(this);
}
if (this._notifyCompletionsHiddenIfNeededTimeout)
clearTimeout(this._notifyCompletionsHiddenIfNeededTimeout);
this._notifyCompletionsHiddenIfNeededTimeout = setTimeout(notify.bind(this), WI.CodeMirrorCompletionController.CompletionsHiddenDelay);
}
_createCompletionHintMarker(position, text)
{
var container = document.createElement("span");
container.classList.add(WI.CodeMirrorCompletionController.CompletionHintStyleClassName);
container.textContent = text;
container.addEventListener("mousedown", (event) => {
event.preventDefault();
this._commitCompletionHint();
// The clicked hint marker causes the editor to loose focus. Restore it so the user can keep typing.
setTimeout(() => { this._codeMirror.focus(); }, 0);
});
this._completionHintMarker = this._codeMirror.setUniqueBookmark(position, {widget: container, insertLeft: true});
}
_applyCompletionHint(completionText)
{
console.assert(completionText);
if (!completionText)
return;
function update()
{
this._currentCompletion = completionText;
this._removeCompletionHint(true, true);
var replacementText = this._currentReplacementText;
var from = {line: this._lineNumber, ch: this._startOffset};
var cursor = {line: this._lineNumber, ch: this._endOffset};
var currentText = this._codeMirror.getRange(from, cursor);
this._createCompletionHintMarker(cursor, replacementText.replace(currentText, ""));
}
this._ignoreChange = true;
this._ignoreNextCursorActivity = true;
this._codeMirror.operation(update.bind(this));
delete this._ignoreChange;
}
_commitCompletionHint()
{
function update()
{
this._removeCompletionHint(true, true);
var replacementText = this._currentReplacementText;
var from = {line: this._lineNumber, ch: this._startOffset};
var cursor = {line: this._lineNumber, ch: this._endOffset};
var to = {line: this._lineNumber, ch: this._startOffset + replacementText.length};
var lastChar = this._currentCompletion.charAt(this._currentCompletion.length - 1);
var isClosing = ")]}".indexOf(lastChar);
if (isClosing !== -1)
to.ch -= 1 + this._implicitSuffix.length;
this._codeMirror.replaceRange(replacementText, from, cursor, WI.CodeMirrorCompletionController.CompletionOrigin);
// Don't call _removeLastChangeFromHistory here to allow the committed completion to be undone.
this._codeMirror.setCursor(to);
this.hideCompletions();
}
this._ignoreChange = true;
this._ignoreNextCursorActivity = true;
this._codeMirror.operation(update.bind(this));
delete this._ignoreChange;
}
_removeLastChangeFromHistory()
{
var history = this._codeMirror.getHistory();
// We don't expect a undone history. But if there is one clear it. If could lead to undefined behavior.
console.assert(!history.undone.length);
history.undone = [];
// Pop the last item from the done history.
console.assert(history.done.length);
history.done.pop();
this._codeMirror.setHistory(history);
}
_removeCompletionHint(nonatomic, dontRestorePrefix)
{
if (!this._completionHintMarker)
return;
this._notifyCompletionsHiddenSoon();
function clearMarker(marker)
{
if (!marker)
return;
var range = marker.find();
if (range)
marker.clear();
return null;
}
function update()
{
this._completionHintMarker = clearMarker(this._completionHintMarker);
if (dontRestorePrefix)
return;
console.assert(!isNaN(this._startOffset));
console.assert(!isNaN(this._endOffset));
console.assert(!isNaN(this._lineNumber));
var from = {line: this._lineNumber, ch: this._startOffset};
var to = {line: this._lineNumber, ch: this._endOffset};
this._codeMirror.replaceRange(this._prefix, from, to, WI.CodeMirrorCompletionController.DeleteCompletionOrigin);
this._removeLastChangeFromHistory();
}
if (nonatomic) {
update.call(this);
return;
}
this._ignoreChange = true;
this._codeMirror.operation(update.bind(this));
delete this._ignoreChange;
}
_scanStringForExpression(modeName, string, startOffset, direction, allowMiddleAndEmpty, includeStopCharacter, ignoreInitialUnmatchedOpenBracket, stopCharactersRegex)
{
console.assert(direction === -1 || direction === 1);
var stopCharactersRegex = stopCharactersRegex || this._stopCharactersRegex || WI.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap[modeName] || WI.CodeMirrorCompletionController.GenericStopCharactersRegex;
function isStopCharacter(character)
{
return stopCharactersRegex.test(character);
}
function isOpenBracketCharacter(character)
{
return WI.CodeMirrorCompletionController.OpenBracketCharactersRegex.test(character);
}
function isCloseBracketCharacter(character)
{
return WI.CodeMirrorCompletionController.CloseBracketCharactersRegex.test(character);
}
function matchingBracketCharacter(character)
{
return WI.CodeMirrorCompletionController.MatchingBrackets[character];
}
var endOffset = Math.min(startOffset, string.length);
var endOfLineOrWord = endOffset === string.length || isStopCharacter(string.charAt(endOffset));
if (!endOfLineOrWord && !allowMiddleAndEmpty)
return null;
var bracketStack = [];
var bracketOffsetStack = [];
var startOffset = endOffset;
var firstOffset = endOffset + direction;
for (var i = firstOffset; direction > 0 ? i < string.length : i >= 0; i += direction) {
var character = string.charAt(i);
// Ignore stop characters when we are inside brackets.
if (isStopCharacter(character) && !bracketStack.length)
break;
if (isCloseBracketCharacter(character)) {
bracketStack.push(character);
bracketOffsetStack.push(i);
} else if (isOpenBracketCharacter(character)) {
if ((!ignoreInitialUnmatchedOpenBracket || i !== firstOffset) && (!bracketStack.length || matchingBracketCharacter(character) !== bracketStack.lastValue))
break;
bracketOffsetStack.pop();
bracketStack.pop();
}
startOffset = i + (direction > 0 ? 1 : 0);
}
if (bracketOffsetStack.length)
startOffset = bracketOffsetStack.pop() + 1;
if (includeStopCharacter && startOffset > 0 && startOffset < string.length)
startOffset += direction;
if (direction > 0) {
var tempEndOffset = endOffset;
endOffset = startOffset;
startOffset = tempEndOffset;
}
return {string: string.substring(startOffset, endOffset), startOffset, endOffset};
}
_completeAtCurrentPosition(force)
{
if (this._codeMirror.somethingSelected()) {
this.hideCompletions();
return;
}
this._removeCompletionHint(true, true);
var cursor = this._codeMirror.getCursor();
var token = this._codeMirror.getTokenAt(cursor);
// Don't try to complete inside comments or strings.
if (token.type && /\b(comment|string)\b/.test(token.type)) {
this.hideCompletions();
return;
}
var mode = this._codeMirror.getMode();
var innerMode = CodeMirror.innerMode(mode, token.state).mode;
var modeName = innerMode.alternateName || innerMode.name;
var lineNumber = cursor.line;
var lineString = this._codeMirror.getLine(lineNumber);
var backwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, -1, force);
if (!backwardScanResult) {
this.hideCompletions();
return;
}
var forwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, 1, true, true);
var suffix = forwardScanResult.string;
this._ignoreNextCursorActivity = true;
this._startOffset = backwardScanResult.startOffset;
this._endOffset = backwardScanResult.endOffset;
this._lineNumber = lineNumber;
this._prefix = backwardScanResult.string;
this._completions = [];
this._implicitSuffix = "";
this._forced = force;
var baseExpressionStopCharactersRegex = WI.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap[modeName];
if (baseExpressionStopCharactersRegex)
var baseScanResult = this._scanStringForExpression(modeName, lineString, this._startOffset, -1, true, false, true, baseExpressionStopCharactersRegex);
if (!force && !backwardScanResult.string && (!baseScanResult || !baseScanResult.string)) {
this.hideCompletions();
return;
}
var defaultCompletions = [];
switch (modeName) {
case "css":
defaultCompletions = this._generateCSSCompletions(token, baseScanResult ? baseScanResult.string : null, suffix);
break;
case "javascript":
defaultCompletions = this._generateJavaScriptCompletions(token, baseScanResult ? baseScanResult.string : null, suffix);
break;
}
var extendedCompletionsProvider = this._extendedCompletionProviders[modeName];
if (extendedCompletionsProvider) {
extendedCompletionsProvider.completionControllerCompletionsNeeded(this, defaultCompletions, baseScanResult ? baseScanResult.string : null, this._prefix, suffix, force);
return;
}
if (this._delegate && typeof this._delegate.completionControllerCompletionsNeeded === "function")
this._delegate.completionControllerCompletionsNeeded(this, this._prefix, defaultCompletions, baseScanResult ? baseScanResult.string : null, suffix, force);
else
this.updateCompletions(defaultCompletions);
}
_generateCSSCompletions(mainToken, base, suffix)
{
// We support completion inside CSS block context and functions.
if (mainToken.state.state === "media" || mainToken.state.state === "top")
return [];
// Don't complete in the middle of a property name.
if (/^[a-z]/i.test(suffix))
return [];
var token = mainToken;
var lineNumber = this._lineNumber;
let getPreviousToken = () => {
// Found the beginning of the line. Go to the previous line.
if (!token.start) {
--lineNumber;
// No more lines, stop.
if (lineNumber < 0)
return null;
}
return this._codeMirror.getTokenAt({line: lineNumber, ch: token.start ? token.start : Number.MAX_VALUE});
};
// Inside a function, determine the function name.
if (token.state.state === "parens") {
// Scan backwards looking for the function paren boundary.
while (token && token.state.state === "parens" && token.string !== "(")
token = getPreviousToken();
// The immediately preceding token should have the function name.
if (token)
token = getPreviousToken();
// No completions if no function name found.
if (!token)
return [];
let functionName = token.string;
if (!functionName)
return [];
let functionCompletions = WI.CSSKeywordCompletions.forFunction(functionName).startsWith(this._prefix);
if (this._delegate && this._delegate.completionControllerCSSFunctionValuesNeeded)
functionCompletions = this._delegate.completionControllerCSSFunctionValuesNeeded(this, functionName, functionCompletions);
return functionCompletions;
}
// Scan backwards looking for the current property.
while (token.state.state === "prop") {
let previousToken = getPreviousToken();
if (!previousToken)
break;
token = previousToken;
}
// If we have a property token and it's not the main token, then we are working on
// the value for that property and should complete allowed values.
if (mainToken !== token && token.type && /\bproperty\b/.test(token.type)) {
var propertyName = token.string;
// If there is a suffix and it isn't a semicolon, then we should use a space since
// the user is editing in the middle. Likewise if the suffix starts with an open
// paren we are changing a function name so don't add a suffix.
this._implicitSuffix = " ";
if (suffix === ";")
this._implicitSuffix = this._noEndingSemicolon ? "" : ";";
else if (suffix.startsWith("("))
this._implicitSuffix = "";
// Don't use an implicit suffix if it would be the same as the existing suffix.
if (this._implicitSuffix === suffix)
this._implicitSuffix = "";
let completions = WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(this._prefix);
if (suffix.startsWith("("))
completions = completions.map((x) => x.replace(/\(\)$/, ""));
return completions;
}
this._implicitSuffix = suffix !== ":" ? ": " : "";
// Complete property names.
return WI.CSSCompletions.cssNameCompletions.startsWith(this._prefix);
}
_generateJavaScriptCompletions(mainToken, base, suffix)
{
// If there is a base expression then we should not attempt to match any keywords or variables.
// Allow only open bracket characters at the end of the base, otherwise leave completions with
// a base up to the delegate to figure out.
if (base && !/[({[]$/.test(base))
return [];
var matchingWords = [];
var prefix = this._prefix;
var localState = mainToken.state.localState ? mainToken.state.localState : mainToken.state;
var declaringVariable = localState.lexical.type === "vardef";
var insideSwitch = localState.lexical.prev ? localState.lexical.prev.info === "switch" : false;
var insideBlock = localState.lexical.prev ? localState.lexical.prev.type === "}" : false;
var insideParenthesis = localState.lexical.type === ")";
var insideBrackets = localState.lexical.type === "]";
// FIXME: Include module keywords if we know this is a module environment.
// var moduleKeywords = ["default", "export", "import"];
const allKeywords = [
"break", "case", "catch", "class", "const", "continue", "debugger", "default",
"delete", "do", "else", "extends", "false", "finally", "for", "function",
"if", "in", "Infinity", "instanceof", "let", "NaN", "new", "null", "of",
"return", "static", "super", "switch", "this", "throw", "true", "try",
"typeof", "undefined", "var", "void", "while", "with", "yield"
];
const valueKeywords = ["false", "Infinity", "NaN", "null", "this", "true", "undefined", "globalThis"];
const allowedKeywordsInsideBlocks = new Set(allKeywords);
const allowedKeywordsWhenDeclaringVariable = new Set(valueKeywords);
const allowedKeywordsInsideParenthesis = new Set(valueKeywords.concat(["class", "function"]));
const allowedKeywordsInsideBrackets = allowedKeywordsInsideParenthesis;
const allowedKeywordsOnlyInsideSwitch = new Set(["case", "default"]);
function matchKeywords(keywords)
{
for (let keyword of keywords) {
if (!insideSwitch && allowedKeywordsOnlyInsideSwitch.has(keyword))
continue;
if (insideBlock && !allowedKeywordsInsideBlocks.has(keyword))
continue;
if (insideBrackets && !allowedKeywordsInsideBrackets.has(keyword))
continue;
if (insideParenthesis && !allowedKeywordsInsideParenthesis.has(keyword))
continue;
if (declaringVariable && !allowedKeywordsWhenDeclaringVariable.has(keyword))
continue;
if (!keyword.startsWith(prefix))
continue;
matchingWords.push(keyword);
}
}
function matchVariables()
{
function filterVariables(variables)
{
for (var variable = variables; variable; variable = variable.next) {
// Don't match the variable if this token is in a variable declaration.
// Otherwise the currently typed text will always match and that isn't useful.
if (declaringVariable && variable.name === prefix)
continue;
if (variable.name.startsWith(prefix) && !matchingWords.includes(variable.name))
matchingWords.push(variable.name);
}
}
var context = localState.context;
while (context) {
if (context.vars)
filterVariables(context.vars);
context = context.prev;
}
if (localState.localVars)
filterVariables(localState.localVars);
if (localState.globalVars)
filterVariables(localState.globalVars);
}
switch (suffix.substring(0, 1)) {
case "":
case " ":
matchVariables();
matchKeywords(allKeywords);
break;
case ".":
case "[":
matchVariables();
matchKeywords(["false", "Infinity", "NaN", "this", "true"]);
break;
case "(":
matchVariables();
matchKeywords(["catch", "else", "for", "function", "if", "return", "switch", "throw", "while", "with", "yield"]);
break;
case "{":
matchKeywords(["do", "else", "finally", "return", "try", "yield"]);
break;
case ":":
if (insideSwitch)
matchKeywords(["case", "default"]);
break;
case ";":
matchVariables();
matchKeywords(valueKeywords);
matchKeywords(["break", "continue", "debugger", "return", "void"]);
break;
}
return matchingWords;
}
_handleUpKey(codeMirror)
{
if (!this._hasPendingCompletion())
return CodeMirror.Pass;
if (!this.isShowingCompletions())
return;
this._suggestionsView.selectPrevious();
}
_handleDownKey(codeMirror)
{
if (!this._hasPendingCompletion())
return CodeMirror.Pass;
if (!this.isShowingCompletions())
return;
this._suggestionsView.selectNext();
}
_handleRightOrEnterKey(codeMirror)
{
if (!this._hasPendingCompletion())
return CodeMirror.Pass;
if (!this.isShowingCompletions())
return;
this._commitCompletionHint();
}
_handleEscapeKey(codeMirror)
{
var delegateImplementsShouldAllowEscapeCompletion = this._delegate && typeof this._delegate.completionControllerShouldAllowEscapeCompletion === "function";
if (this._hasPendingCompletion())
this.hideCompletions();
else if (this._codeMirror.getOption("readOnly"))
return CodeMirror.Pass;
else if (!delegateImplementsShouldAllowEscapeCompletion || this._delegate.completionControllerShouldAllowEscapeCompletion(this))
this._completeAtCurrentPosition(true);
else
return CodeMirror.Pass;
}
_handleTabKey(codeMirror)
{
if (!this._hasPendingCompletion())
return CodeMirror.Pass;
if (!this.isShowingCompletions())
return;
console.assert(this._completions.length);
if (!this._completions.length)
return;
console.assert(this._currentCompletion);
if (!this._currentCompletion)
return;
// Commit the current completion if there is only one suggestion.
if (this._completions.length === 1) {
this._commitCompletionHint();
return;
}
var prefixLength = this._prefix.length;
var commonPrefix = this._completions[0];
for (var i = 1; i < this._completions.length; ++i) {
var completion = this._completions[i];
var lastIndex = Math.min(commonPrefix.length, completion.length);
for (var j = prefixLength; j < lastIndex; ++j) {
if (commonPrefix[j] !== completion[j]) {
commonPrefix = commonPrefix.substr(0, j);
break;
}
}
}
// Commit the current completion if there is no common prefix that is longer.
if (commonPrefix === this._prefix) {
this._commitCompletionHint();
return;
}
// Set the prefix to the common prefix so _applyCompletionHint will insert the
// common prefix as commited text. Adjust _endOffset to match the new prefix.
this._prefix = commonPrefix;
this._endOffset = this._startOffset + commonPrefix.length;
this._applyCompletionHint(this._currentCompletion);
}
_handleChange(codeMirror, change)
{
if (this.isCompletionChange(change))
return;
this._ignoreNextCursorActivity = true;
if (!change.origin || change.origin.charAt(0) !== "+") {
this.hideCompletions();
return;
}
// Only complete on delete if we are showing completions already.
if (change.origin === "+delete" && !this._hasPendingCompletion())
return;
this._completeAtCurrentPosition(false);
}
_handleCursorActivity(codeMirror)
{
if (this._ignoreChange)
return;
if (this._ignoreNextCursorActivity) {
delete this._ignoreNextCursorActivity;
return;
}
this.hideCompletions();
}
_handleHideKey(codeMirror)
{
this.hideCompletions();
return CodeMirror.Pass;
}
_handleHideAction(codeMirror)
{
// Clicking a suggestion causes the editor to blur. We don't want to hide completions in this case.
if (this.isHandlingClickEvent())
return;
this.hideCompletions();
}
};
WI.CodeMirrorCompletionController.UpdatePromise = {
Canceled: "code-mirror-completion-controller-canceled",
CompletionsFound: "code-mirror-completion-controller-completions-found",
NoCompletionsFound: "code-mirror-completion-controller-no-completions-found"
};
WI.CodeMirrorCompletionController.GenericStopCharactersRegex = /[\s=:;,]/;
WI.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap = {"css": /[\s:;,{}()]/, "javascript": /[\s=:;,!+\-*/%&|^~?<>.{}()[\]]/};
WI.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap = {"javascript": /[\s=:;,!+\-*/%&|^~?<>]/};
WI.CodeMirrorCompletionController.OpenBracketCharactersRegex = /[({[]/;
WI.CodeMirrorCompletionController.CloseBracketCharactersRegex = /[)}\]]/;
WI.CodeMirrorCompletionController.MatchingBrackets = {"{": "}", "(": ")", "[": "]", "}": "{", ")": "(", "]": "["};
WI.CodeMirrorCompletionController.CompletionHintStyleClassName = "completion-hint";
WI.CodeMirrorCompletionController.CompletionsHiddenDelay = 250;
WI.CodeMirrorCompletionController.CompletionTypingDelay = 250;
WI.CodeMirrorCompletionController.CompletionOrigin = "+completion";
WI.CodeMirrorCompletionController.DeleteCompletionOrigin = "+delete-completion";