927 lines
33 KiB
JavaScript
927 lines
33 KiB
JavaScript
/*
|
|
* 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(mode, codeMirror, delegate, stopCharactersRegex)
|
|
{
|
|
console.assert(Object.values(WI.CodeMirrorCompletionController.Mode).includes(mode), mode);
|
|
console.assert(codeMirror instanceof CodeMirror, codeMirror);
|
|
|
|
super();
|
|
|
|
this._mode = mode;
|
|
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 mode() { return this._mode; }
|
|
|
|
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();
|
|
}
|
|
|
|
commitCurrentCompletion()
|
|
{
|
|
this._removeCompletionHint(true, true);
|
|
|
|
let replacementText = this._currentReplacementText;
|
|
if (!replacementText)
|
|
return;
|
|
|
|
let from = {line: this._lineNumber, ch: this._startOffset};
|
|
let cursor = {line: this._lineNumber, ch: this._endOffset};
|
|
let to = {line: this._lineNumber, ch: this._startOffset + replacementText.length};
|
|
|
|
let lastChar = this._currentCompletion.charAt(this._currentCompletion.length - 1);
|
|
let 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();
|
|
}
|
|
|
|
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()
|
|
{
|
|
this._ignoreChange = true;
|
|
this._ignoreNextCursorActivity = true;
|
|
|
|
this._codeMirror.operation(this.commitCurrentCompletion.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.cssManager.propertyNameCompletions.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.Mode = {
|
|
Basic: "basic",
|
|
EventBreakpoint: "event-breakpoint",
|
|
ExceptionBreakpoint: "exception-breakpoint",
|
|
FullConsoleCommandLineAPI: "full-console-command-line-api",
|
|
PausedConsoleCommandLineAPI: "paused-console-command-line-api",
|
|
};
|
|
|
|
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";
|