410 lines
13 KiB
JavaScript
410 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const STATE_NORMAL = Symbol("STATE_NORMAL");
|
|
const STATE_QUOTE = Symbol("STATE_QUOTE");
|
|
const STATE_DQUOTE = Symbol("STATE_DQUOTE");
|
|
const STATE_TEMPLATE_LITERAL = Symbol("STATE_TEMPLATE_LITERAL");
|
|
const STATE_ESCAPE_QUOTE = Symbol("STATE_ESCAPE_QUOTE");
|
|
const STATE_ESCAPE_DQUOTE = Symbol("STATE_ESCAPE_DQUOTE");
|
|
const STATE_ESCAPE_TEMPLATE_LITERAL = Symbol("STATE_ESCAPE_TEMPLATE_LITERAL");
|
|
const STATE_SLASH = Symbol("STATE_SLASH");
|
|
const STATE_INLINE_COMMENT = Symbol("STATE_INLINE_COMMENT");
|
|
const STATE_MULTILINE_COMMENT = Symbol("STATE_MULTILINE_COMMENT");
|
|
const STATE_MULTILINE_COMMENT_CLOSE = Symbol("STATE_MULTILINE_COMMENT_CLOSE");
|
|
const STATE_QUESTION_MARK = Symbol("STATE_QUESTION_MARK");
|
|
|
|
const OPEN_BODY = "{[(".split("");
|
|
const CLOSE_BODY = "}])".split("");
|
|
const OPEN_CLOSE_BODY = {
|
|
"{": "}",
|
|
"[": "]",
|
|
"(": ")",
|
|
};
|
|
|
|
const NO_AUTOCOMPLETE_PREFIXES = ["var", "const", "let", "function", "class"];
|
|
const OPERATOR_CHARS_SET = new Set(";,:=<>+-*%|&^~!".split(""));
|
|
|
|
/**
|
|
* Analyses a given string to find the last statement that is interesting for
|
|
* later completion.
|
|
*
|
|
* @param string str
|
|
* A string to analyse.
|
|
*
|
|
* @returns object
|
|
* If there was an error in the string detected, then a object like
|
|
*
|
|
* { err: "ErrorMesssage" }
|
|
*
|
|
* is returned, otherwise a object like
|
|
*
|
|
* {
|
|
* state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
|
|
* lastStatement: the last statement in the string,
|
|
* isElementAccess: boolean that indicates if the lastStatement has an open
|
|
* element access (e.g. `x["match`).
|
|
* isPropertyAccess: boolean indicating if we are accessing property
|
|
* (e.g `true` in `var a = {b: 1};a.b`)
|
|
* matchProp: The part of the expression that should match the properties
|
|
* on the mainExpression (e.g. `que` when expression is `document.body.que`)
|
|
* mainExpression: The part of the expression before any property access,
|
|
* (e.g. `a.b` if expression is `a.b.`)
|
|
* expressionBeforePropertyAccess: The part of the expression before property access
|
|
* (e.g `var a = {b: 1};a` if expression is `var a = {b: 1};a.b`)
|
|
* }
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
exports.analyzeInputString = function (str, timeout = 2500) {
|
|
// work variables.
|
|
const bodyStack = [];
|
|
let state = STATE_NORMAL;
|
|
let previousNonWhitespaceChar;
|
|
let lastStatement = "";
|
|
let currentIndex = -1;
|
|
let dotIndex;
|
|
let pendingWhitespaceChars = "";
|
|
const startingTime = Date.now();
|
|
|
|
// Use a string iterator in order to handle character with a length >= 2 (e.g. 😎).
|
|
for (const c of str) {
|
|
// We are possibly dealing with a very large string that would take a long time to
|
|
// analyze (and freeze the process). If the function has been running for more than
|
|
// a given time, we stop the analysis (this isn't too bad because the only
|
|
// consequence is that we won't provide autocompletion items).
|
|
if (Date.now() - startingTime > timeout) {
|
|
return {
|
|
err: "timeout",
|
|
};
|
|
}
|
|
|
|
currentIndex += 1;
|
|
let resetLastStatement = false;
|
|
const isWhitespaceChar = c.trim() === "";
|
|
switch (state) {
|
|
case STATE_SLASH:
|
|
if (c == "/") {
|
|
state = STATE_INLINE_COMMENT;
|
|
break;
|
|
} else if (c == "*") {
|
|
state = STATE_MULTILINE_COMMENT;
|
|
break;
|
|
} else {
|
|
lastStatement = "";
|
|
state = STATE_NORMAL;
|
|
}
|
|
// fall through
|
|
|
|
// Normal JS state.
|
|
// eslint-disable-next-line no-fallthrough
|
|
case STATE_NORMAL:
|
|
if (lastStatement.endsWith("?.") && /\d/.test(c)) {
|
|
// If the current char is a number, the engine will consider we're not
|
|
// performing an optional chaining, but a ternary (e.g. x ?.4 : 2).
|
|
lastStatement = "";
|
|
}
|
|
|
|
// Storing the index of dot of the input string
|
|
if (c === ".") {
|
|
dotIndex = currentIndex;
|
|
}
|
|
|
|
// If the last characters were spaces, and the current one is not.
|
|
if (pendingWhitespaceChars && !isWhitespaceChar) {
|
|
// If we have a legitimate property/element access, or potential optional
|
|
// chaining call, we append the spaces.
|
|
if (c === "[" || c === "." || c === "?") {
|
|
lastStatement = lastStatement + pendingWhitespaceChars;
|
|
} else {
|
|
// if not, we can be sure the statement was over, and we can start a new one.
|
|
lastStatement = "";
|
|
}
|
|
pendingWhitespaceChars = "";
|
|
}
|
|
|
|
if (c == '"') {
|
|
state = STATE_DQUOTE;
|
|
} else if (c == "'") {
|
|
state = STATE_QUOTE;
|
|
} else if (c == "`") {
|
|
state = STATE_TEMPLATE_LITERAL;
|
|
} else if (c == "/") {
|
|
state = STATE_SLASH;
|
|
} else if (c == "?") {
|
|
state = STATE_QUESTION_MARK;
|
|
} else if (OPERATOR_CHARS_SET.has(c)) {
|
|
// If the character is an operator, we can update the current statement.
|
|
resetLastStatement = true;
|
|
} else if (isWhitespaceChar) {
|
|
// If the previous char isn't a dot or opening bracket, and the current computed
|
|
// statement is not a variable/function/class declaration, we track the number
|
|
// of consecutive spaces, so we can re-use them at some point (or drop them).
|
|
if (
|
|
previousNonWhitespaceChar !== "." &&
|
|
previousNonWhitespaceChar !== "[" &&
|
|
!NO_AUTOCOMPLETE_PREFIXES.includes(lastStatement)
|
|
) {
|
|
pendingWhitespaceChars += c;
|
|
continue;
|
|
}
|
|
} else if (OPEN_BODY.includes(c)) {
|
|
// When opening a bracket or a parens, we store the current statement, in order
|
|
// to be able to retrieve it later.
|
|
bodyStack.push({
|
|
token: c,
|
|
lastStatement,
|
|
index: currentIndex,
|
|
});
|
|
// And we compute a new statement.
|
|
resetLastStatement = true;
|
|
} else if (CLOSE_BODY.includes(c)) {
|
|
const last = bodyStack.pop();
|
|
if (!last || OPEN_CLOSE_BODY[last.token] != c) {
|
|
return {
|
|
err: "syntax error",
|
|
};
|
|
}
|
|
if (c == "}") {
|
|
resetLastStatement = true;
|
|
} else {
|
|
lastStatement = last.lastStatement;
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Escaped quote
|
|
case STATE_ESCAPE_QUOTE:
|
|
state = STATE_QUOTE;
|
|
break;
|
|
case STATE_ESCAPE_DQUOTE:
|
|
state = STATE_DQUOTE;
|
|
break;
|
|
case STATE_ESCAPE_TEMPLATE_LITERAL:
|
|
state = STATE_TEMPLATE_LITERAL;
|
|
break;
|
|
|
|
// Double quote state > " <
|
|
case STATE_DQUOTE:
|
|
if (c == "\\") {
|
|
state = STATE_ESCAPE_DQUOTE;
|
|
} else if (c == "\n") {
|
|
return {
|
|
err: "unterminated string literal",
|
|
};
|
|
} else if (c == '"') {
|
|
state = STATE_NORMAL;
|
|
}
|
|
break;
|
|
|
|
// Template literal state > ` <
|
|
case STATE_TEMPLATE_LITERAL:
|
|
if (c == "\\") {
|
|
state = STATE_ESCAPE_TEMPLATE_LITERAL;
|
|
} else if (c == "`") {
|
|
state = STATE_NORMAL;
|
|
}
|
|
break;
|
|
|
|
// Single quote state > ' <
|
|
case STATE_QUOTE:
|
|
if (c == "\\") {
|
|
state = STATE_ESCAPE_QUOTE;
|
|
} else if (c == "\n") {
|
|
return {
|
|
err: "unterminated string literal",
|
|
};
|
|
} else if (c == "'") {
|
|
state = STATE_NORMAL;
|
|
}
|
|
break;
|
|
|
|
case STATE_INLINE_COMMENT:
|
|
if (c === "\n") {
|
|
state = STATE_NORMAL;
|
|
resetLastStatement = true;
|
|
}
|
|
break;
|
|
|
|
case STATE_MULTILINE_COMMENT:
|
|
if (c === "*") {
|
|
state = STATE_MULTILINE_COMMENT_CLOSE;
|
|
}
|
|
break;
|
|
|
|
case STATE_MULTILINE_COMMENT_CLOSE:
|
|
if (c === "/") {
|
|
state = STATE_NORMAL;
|
|
resetLastStatement = true;
|
|
} else {
|
|
state = STATE_MULTILINE_COMMENT;
|
|
}
|
|
break;
|
|
|
|
case STATE_QUESTION_MARK:
|
|
state = STATE_NORMAL;
|
|
if (c === "?") {
|
|
// If we have a nullish coalescing operator, we start a new statement
|
|
resetLastStatement = true;
|
|
} else if (c !== ".") {
|
|
// If we're not dealing with optional chaining (?.), it means we have a ternary,
|
|
// so we are starting a new statement that includes the current character.
|
|
lastStatement = "";
|
|
} else {
|
|
dotIndex = currentIndex;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!isWhitespaceChar) {
|
|
previousNonWhitespaceChar = c;
|
|
}
|
|
if (resetLastStatement) {
|
|
lastStatement = "";
|
|
} else {
|
|
lastStatement = lastStatement + c;
|
|
}
|
|
|
|
// We update all the open stacks lastStatement so they are up-to-date.
|
|
bodyStack.forEach(stack => {
|
|
if (stack.token !== "}") {
|
|
stack.lastStatement = stack.lastStatement + c;
|
|
}
|
|
});
|
|
}
|
|
|
|
let isElementAccess = false;
|
|
let lastOpeningBracketIndex = -1;
|
|
if (bodyStack.length === 1 && bodyStack[0].token === "[") {
|
|
lastStatement = bodyStack[0].lastStatement;
|
|
lastOpeningBracketIndex = bodyStack[0].index;
|
|
isElementAccess = true;
|
|
|
|
if (
|
|
state === STATE_DQUOTE ||
|
|
state === STATE_QUOTE ||
|
|
state === STATE_TEMPLATE_LITERAL ||
|
|
state === STATE_ESCAPE_QUOTE ||
|
|
state === STATE_ESCAPE_DQUOTE ||
|
|
state === STATE_ESCAPE_TEMPLATE_LITERAL
|
|
) {
|
|
state = STATE_NORMAL;
|
|
}
|
|
} else if (pendingWhitespaceChars) {
|
|
lastStatement = "";
|
|
}
|
|
|
|
const lastCompletionCharIndex = isElementAccess
|
|
? lastOpeningBracketIndex
|
|
: dotIndex;
|
|
|
|
const stringBeforeLastCompletionChar = str.slice(0, lastCompletionCharIndex);
|
|
|
|
const isPropertyAccess =
|
|
lastCompletionCharIndex && lastCompletionCharIndex > 0;
|
|
|
|
// Compute `isOptionalAccess`, so that we can use it
|
|
// later for computing `expressionBeforePropertyAccess`.
|
|
//Check `?.` before `[` for element access ( e.g `a?.["b` or `a ?. ["b` )
|
|
// and `?` before `.` for regular property access ( e.g `a?.b` or `a ?. b` )
|
|
const optionalElementAccessRegex = /\?\.\s*$/;
|
|
const isOptionalAccess = isElementAccess
|
|
? optionalElementAccessRegex.test(stringBeforeLastCompletionChar)
|
|
: isPropertyAccess &&
|
|
str.slice(lastCompletionCharIndex - 1, lastCompletionCharIndex + 1) ===
|
|
"?.";
|
|
|
|
// Get the filtered string for the properties (e.g if `document.qu` then `qu`)
|
|
const matchProp = isPropertyAccess
|
|
? str.slice(lastCompletionCharIndex + 1).trimLeft()
|
|
: null;
|
|
|
|
const expressionBeforePropertyAccess = isPropertyAccess
|
|
? str.slice(
|
|
0,
|
|
// For optional access, we can take all the chars before the last "?" char.
|
|
isOptionalAccess
|
|
? stringBeforeLastCompletionChar.lastIndexOf("?")
|
|
: lastCompletionCharIndex
|
|
)
|
|
: str;
|
|
|
|
let mainExpression = lastStatement;
|
|
if (isPropertyAccess) {
|
|
if (isOptionalAccess) {
|
|
// Strip anything before the last `?`.
|
|
mainExpression = mainExpression.slice(0, mainExpression.lastIndexOf("?"));
|
|
} else {
|
|
mainExpression = mainExpression.slice(
|
|
0,
|
|
-1 * (str.length - lastCompletionCharIndex)
|
|
);
|
|
}
|
|
}
|
|
|
|
mainExpression = mainExpression.trim();
|
|
|
|
return {
|
|
state,
|
|
isElementAccess,
|
|
isPropertyAccess,
|
|
expressionBeforePropertyAccess,
|
|
lastStatement,
|
|
mainExpression,
|
|
matchProp,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Checks whether the analyzed input string is in an appropriate state to autocomplete, e.g. not
|
|
* inside a string, or declaring a variable.
|
|
* @param {object} inputAnalysisState The analyzed string to check
|
|
* @returns {boolean} Whether the input should be autocompleted
|
|
*/
|
|
exports.shouldInputBeAutocompleted = function (inputAnalysisState) {
|
|
const { err, state, lastStatement } = inputAnalysisState;
|
|
|
|
// There was an error analysing the string.
|
|
if (err) {
|
|
return false;
|
|
}
|
|
|
|
// If the current state is not STATE_NORMAL, then we are inside string,
|
|
// which means that no completion is possible.
|
|
if (state != STATE_NORMAL) {
|
|
return false;
|
|
}
|
|
|
|
// Don't complete on just an empty string.
|
|
if (lastStatement.trim() == "") {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
NO_AUTOCOMPLETE_PREFIXES.some(prefix =>
|
|
lastStatement.startsWith(prefix + " ")
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Checks whether the analyzed input string is in an appropriate state to be eagerly evaluated.
|
|
* @param {object} inputAnalysisState
|
|
* @returns {boolean} Whether the input should be eagerly evaluated
|
|
*/
|
|
exports.shouldInputBeEagerlyEvaluated = function ({ lastStatement }) {
|
|
const inComputedProperty =
|
|
lastStatement.lastIndexOf("[") !== -1 &&
|
|
lastStatement.lastIndexOf("[") > lastStatement.lastIndexOf("]");
|
|
|
|
const hasPropertyAccess =
|
|
lastStatement.includes(".") || lastStatement.includes("[");
|
|
|
|
return hasPropertyAccess && !inComputedProperty;
|
|
};
|