monaco-editor-core
Version:
A browser based code editor
1,410 lines • 54 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js';
import { isFalsyOrWhitespace } from '../../../base/common/strings.js';
import { Scanner } from './scanner.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { localize } from '../../../nls.js';
const CONSTANT_VALUES = new Map();
CONSTANT_VALUES.set('false', false);
CONSTANT_VALUES.set('true', true);
CONSTANT_VALUES.set('isMac', isMacintosh);
CONSTANT_VALUES.set('isLinux', isLinux);
CONSTANT_VALUES.set('isWindows', isWindows);
CONSTANT_VALUES.set('isWeb', isWeb);
CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);
CONSTANT_VALUES.set('isEdge', isEdge);
CONSTANT_VALUES.set('isFirefox', isFirefox);
CONSTANT_VALUES.set('isChrome', isChrome);
CONSTANT_VALUES.set('isSafari', isSafari);
const hasOwnProperty = Object.prototype.hasOwnProperty;
const defaultConfig = {
regexParsingWithErrorRecovery: true
};
const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression");
const hintEmptyString = localize('contextkey.parser.error.emptyString.hint', "Did you forget to write an expression? You can also put 'false' or 'true' to always evaluate to false or true, respectively.");
const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'.");
const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'");
const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token");
const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?");
const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression");
const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?");
/**
* A parser for context key expressions.
*
* Example:
* ```ts
* const parser = new Parser();
* const expr = parser.parse('foo == "bar" && baz == true');
*
* if (expr === undefined) {
* // there were lexing or parsing errors
* // process lexing errors with `parser.lexingErrors`
* // process parsing errors with `parser.parsingErrors`
* } else {
* // expr is a valid expression
* }
* ```
*/
export class Parser {
// Note: this doesn't produce an exact syntax tree but a normalized one
// ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize
static { this._parseError = new Error(); }
constructor(_config = defaultConfig) {
this._config = _config;
// lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse`
this._scanner = new Scanner();
// lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse`
this._tokens = [];
this._current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd`
this._parsingErrors = [];
this._flagsGYRe = /g|y/g;
}
/**
* Parse a context key expression.
*
* @param input the expression to parse
* @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors
*/
parse(input) {
if (input === '') {
this._parsingErrors.push({ message: errorEmptyString, offset: 0, lexeme: '', additionalInfo: hintEmptyString });
return undefined;
}
this._tokens = this._scanner.reset(input).scan();
// @ulugbekna: we do not stop parsing if there are lexing errors to be able to reconstruct regexes with unescaped slashes; TODO@ulugbekna: make this respect config option for recovery
this._current = 0;
this._parsingErrors = [];
try {
const expr = this._expr();
if (!this._isAtEnd()) {
const peek = this._peek();
const additionalInfo = peek.type === 17 /* TokenType.Str */ ? hintUnexpectedToken : undefined;
this._parsingErrors.push({ message: errorUnexpectedToken, offset: peek.offset, lexeme: Scanner.getLexeme(peek), additionalInfo });
throw Parser._parseError;
}
return expr;
}
catch (e) {
if (!(e === Parser._parseError)) {
throw e;
}
return undefined;
}
}
_expr() {
return this._or();
}
_or() {
const expr = [this._and()];
while (this._matchOne(16 /* TokenType.Or */)) {
const right = this._and();
expr.push(right);
}
return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr);
}
_and() {
const expr = [this._term()];
while (this._matchOne(15 /* TokenType.And */)) {
const right = this._term();
expr.push(right);
}
return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr);
}
_term() {
if (this._matchOne(2 /* TokenType.Neg */)) {
const peek = this._peek();
switch (peek.type) {
case 11 /* TokenType.True */:
this._advance();
return ContextKeyFalseExpr.INSTANCE;
case 12 /* TokenType.False */:
this._advance();
return ContextKeyTrueExpr.INSTANCE;
case 0 /* TokenType.LParen */: {
this._advance();
const expr = this._expr();
this._consume(1 /* TokenType.RParen */, errorClosingParenthesis);
return expr?.negate();
}
case 17 /* TokenType.Str */:
this._advance();
return ContextKeyNotExpr.create(peek.lexeme);
default:
throw this._errExpectedButGot(`KEY | true | false | '(' expression ')'`, peek);
}
}
return this._primary();
}
_primary() {
const peek = this._peek();
switch (peek.type) {
case 11 /* TokenType.True */:
this._advance();
return ContextKeyExpr.true();
case 12 /* TokenType.False */:
this._advance();
return ContextKeyExpr.false();
case 0 /* TokenType.LParen */: {
this._advance();
const expr = this._expr();
this._consume(1 /* TokenType.RParen */, errorClosingParenthesis);
return expr;
}
case 17 /* TokenType.Str */: {
// KEY
const key = peek.lexeme;
this._advance();
// =~ regex
if (this._matchOne(9 /* TokenType.RegexOp */)) {
// @ulugbekna: we need to reconstruct the regex from the tokens because some extensions use unescaped slashes in regexes
const expr = this._peek();
if (!this._config.regexParsingWithErrorRecovery) {
this._advance();
if (expr.type !== 10 /* TokenType.RegexStr */) {
throw this._errExpectedButGot(`REGEX`, expr);
}
const regexLexeme = expr.lexeme;
const closingSlashIndex = regexLexeme.lastIndexOf('/');
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
let regexp;
try {
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
}
catch (e) {
throw this._errExpectedButGot(`REGEX`, expr);
}
return ContextKeyRegexExpr.create(key, regexp);
}
switch (expr.type) {
case 10 /* TokenType.RegexStr */:
case 19 /* TokenType.Error */: { // also handle an ErrorToken in case of smth such as /(/file)/
const lexemeReconstruction = [expr.lexeme]; // /REGEX/ or /REGEX/FLAGS
this._advance();
let followingToken = this._peek();
let parenBalance = 0;
for (let i = 0; i < expr.lexeme.length; i++) {
if (expr.lexeme.charCodeAt(i) === 40 /* CharCode.OpenParen */) {
parenBalance++;
}
else if (expr.lexeme.charCodeAt(i) === 41 /* CharCode.CloseParen */) {
parenBalance--;
}
}
while (!this._isAtEnd() && followingToken.type !== 15 /* TokenType.And */ && followingToken.type !== 16 /* TokenType.Or */) {
switch (followingToken.type) {
case 0 /* TokenType.LParen */:
parenBalance++;
break;
case 1 /* TokenType.RParen */:
parenBalance--;
break;
case 10 /* TokenType.RegexStr */:
case 18 /* TokenType.QuotedStr */:
for (let i = 0; i < followingToken.lexeme.length; i++) {
if (followingToken.lexeme.charCodeAt(i) === 40 /* CharCode.OpenParen */) {
parenBalance++;
}
else if (expr.lexeme.charCodeAt(i) === 41 /* CharCode.CloseParen */) {
parenBalance--;
}
}
}
if (parenBalance < 0) {
break;
}
lexemeReconstruction.push(Scanner.getLexeme(followingToken));
this._advance();
followingToken = this._peek();
}
const regexLexeme = lexemeReconstruction.join('');
const closingSlashIndex = regexLexeme.lastIndexOf('/');
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
let regexp;
try {
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
}
catch (e) {
throw this._errExpectedButGot(`REGEX`, expr);
}
return ContextKeyExpr.regex(key, regexp);
}
case 18 /* TokenType.QuotedStr */: {
const serializedValue = expr.lexeme;
this._advance();
// replicate old regex parsing behavior
let regex = null;
if (!isFalsyOrWhitespace(serializedValue)) {
const start = serializedValue.indexOf('/');
const end = serializedValue.lastIndexOf('/');
if (start !== end && start >= 0) {
const value = serializedValue.slice(start + 1, end);
const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : '';
try {
regex = new RegExp(value, caseIgnoreFlag);
}
catch (_e) {
throw this._errExpectedButGot(`REGEX`, expr);
}
}
}
if (regex === null) {
throw this._errExpectedButGot('REGEX', expr);
}
return ContextKeyRegexExpr.create(key, regex);
}
default:
throw this._errExpectedButGot('REGEX', this._peek());
}
}
// [ 'not' 'in' value ]
if (this._matchOne(14 /* TokenType.Not */)) {
this._consume(13 /* TokenType.In */, errorNoInAfterNot);
const right = this._value();
return ContextKeyExpr.notIn(key, right);
}
// [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in') value ]
const maybeOp = this._peek().type;
switch (maybeOp) {
case 3 /* TokenType.Eq */: {
this._advance();
const right = this._value();
if (this._previous().type === 18 /* TokenType.QuotedStr */) { // to preserve old parser behavior: "foo == 'true'" is preserved as "foo == 'true'", but "foo == true" is optimized as "foo"
return ContextKeyExpr.equals(key, right);
}
switch (right) {
case 'true':
return ContextKeyExpr.has(key);
case 'false':
return ContextKeyExpr.not(key);
default:
return ContextKeyExpr.equals(key, right);
}
}
case 4 /* TokenType.NotEq */: {
this._advance();
const right = this._value();
if (this._previous().type === 18 /* TokenType.QuotedStr */) { // same as above with "foo != 'true'"
return ContextKeyExpr.notEquals(key, right);
}
switch (right) {
case 'true':
return ContextKeyExpr.not(key);
case 'false':
return ContextKeyExpr.has(key);
default:
return ContextKeyExpr.notEquals(key, right);
}
}
// TODO: ContextKeyExpr.smaller(key, right) accepts only `number` as `right` AND during eval of this node, we just eval to `false` if `right` is not a number
// consequently, package.json linter should _warn_ the user if they're passing undesired things to ops
case 5 /* TokenType.Lt */:
this._advance();
return ContextKeySmallerExpr.create(key, this._value());
case 6 /* TokenType.LtEq */:
this._advance();
return ContextKeySmallerEqualsExpr.create(key, this._value());
case 7 /* TokenType.Gt */:
this._advance();
return ContextKeyGreaterExpr.create(key, this._value());
case 8 /* TokenType.GtEq */:
this._advance();
return ContextKeyGreaterEqualsExpr.create(key, this._value());
case 13 /* TokenType.In */:
this._advance();
return ContextKeyExpr.in(key, this._value());
default:
return ContextKeyExpr.has(key);
}
}
case 20 /* TokenType.EOF */:
this._parsingErrors.push({ message: errorUnexpectedEOF, offset: peek.offset, lexeme: '', additionalInfo: hintUnexpectedEOF });
throw Parser._parseError;
default:
throw this._errExpectedButGot(`true | false | KEY \n\t| KEY '=~' REGEX \n\t| KEY ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in') value`, this._peek());
}
}
_value() {
const token = this._peek();
switch (token.type) {
case 17 /* TokenType.Str */:
case 18 /* TokenType.QuotedStr */:
this._advance();
return token.lexeme;
case 11 /* TokenType.True */:
this._advance();
return 'true';
case 12 /* TokenType.False */:
this._advance();
return 'false';
case 13 /* TokenType.In */: // we support `in` as a value, e.g., "when": "languageId == in" - exists in existing extensions
this._advance();
return 'in';
default:
// this allows "when": "foo == " which's used by existing extensions
// we do not call `_advance` on purpose - we don't want to eat unintended tokens
return '';
}
}
_removeFlagsGY(flags) {
return flags.replaceAll(this._flagsGYRe, '');
}
// careful: this can throw if current token is the initial one (ie index = 0)
_previous() {
return this._tokens[this._current - 1];
}
_matchOne(token) {
if (this._check(token)) {
this._advance();
return true;
}
return false;
}
_advance() {
if (!this._isAtEnd()) {
this._current++;
}
return this._previous();
}
_consume(type, message) {
if (this._check(type)) {
return this._advance();
}
throw this._errExpectedButGot(message, this._peek());
}
_errExpectedButGot(expected, got, additionalInfo) {
const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got));
const offset = got.offset;
const lexeme = Scanner.getLexeme(got);
this._parsingErrors.push({ message, offset, lexeme, additionalInfo });
return Parser._parseError;
}
_check(type) {
return this._peek().type === type;
}
_peek() {
return this._tokens[this._current];
}
_isAtEnd() {
return this._peek().type === 20 /* TokenType.EOF */;
}
}
export class ContextKeyExpr {
static false() {
return ContextKeyFalseExpr.INSTANCE;
}
static true() {
return ContextKeyTrueExpr.INSTANCE;
}
static has(key) {
return ContextKeyDefinedExpr.create(key);
}
static equals(key, value) {
return ContextKeyEqualsExpr.create(key, value);
}
static notEquals(key, value) {
return ContextKeyNotEqualsExpr.create(key, value);
}
static regex(key, value) {
return ContextKeyRegexExpr.create(key, value);
}
static in(key, value) {
return ContextKeyInExpr.create(key, value);
}
static notIn(key, value) {
return ContextKeyNotInExpr.create(key, value);
}
static not(key) {
return ContextKeyNotExpr.create(key);
}
static and(...expr) {
return ContextKeyAndExpr.create(expr, null, true);
}
static or(...expr) {
return ContextKeyOrExpr.create(expr, null, true);
}
static { this._parser = new Parser({ regexParsingWithErrorRecovery: false }); }
static deserialize(serialized) {
if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported
return undefined;
}
const expr = this._parser.parse(serialized);
return expr;
}
}
export function expressionsAreEqualWithConstantSubstitution(a, b) {
const aExpr = a ? a.substituteConstants() : undefined;
const bExpr = b ? b.substituteConstants() : undefined;
if (!aExpr && !bExpr) {
return true;
}
if (!aExpr || !bExpr) {
return false;
}
return aExpr.equals(bExpr);
}
function cmp(a, b) {
return a.cmp(b);
}
export class ContextKeyFalseExpr {
static { this.INSTANCE = new ContextKeyFalseExpr(); }
constructor() {
this.type = 0 /* ContextKeyExprType.False */;
}
cmp(other) {
return this.type - other.type;
}
equals(other) {
return (other.type === this.type);
}
substituteConstants() {
return this;
}
evaluate(context) {
return false;
}
serialize() {
return 'false';
}
keys() {
return [];
}
negate() {
return ContextKeyTrueExpr.INSTANCE;
}
}
export class ContextKeyTrueExpr {
static { this.INSTANCE = new ContextKeyTrueExpr(); }
constructor() {
this.type = 1 /* ContextKeyExprType.True */;
}
cmp(other) {
return this.type - other.type;
}
equals(other) {
return (other.type === this.type);
}
substituteConstants() {
return this;
}
evaluate(context) {
return true;
}
serialize() {
return 'true';
}
keys() {
return [];
}
negate() {
return ContextKeyFalseExpr.INSTANCE;
}
}
export class ContextKeyDefinedExpr {
static create(key, negated = null) {
const constantValue = CONSTANT_VALUES.get(key);
if (typeof constantValue === 'boolean') {
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
}
return new ContextKeyDefinedExpr(key, negated);
}
constructor(key, negated) {
this.key = key;
this.negated = negated;
this.type = 2 /* ContextKeyExprType.Defined */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp1(this.key, other.key);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key);
}
return false;
}
substituteConstants() {
const constantValue = CONSTANT_VALUES.get(this.key);
if (typeof constantValue === 'boolean') {
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
}
return this;
}
evaluate(context) {
return (!!context.getValue(this.key));
}
serialize() {
return this.key;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyNotExpr.create(this.key, this);
}
return this.negated;
}
}
export class ContextKeyEqualsExpr {
static create(key, value, negated = null) {
if (typeof value === 'boolean') {
return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated));
}
const constantValue = CONSTANT_VALUES.get(key);
if (typeof constantValue === 'boolean') {
const trueValue = constantValue ? 'true' : 'false';
return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
}
return new ContextKeyEqualsExpr(key, value, negated);
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 4 /* ContextKeyExprType.Equals */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
const constantValue = CONSTANT_VALUES.get(this.key);
if (typeof constantValue === 'boolean') {
const trueValue = constantValue ? 'true' : 'false';
return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
}
return this;
}
evaluate(context) {
// Intentional ==
// eslint-disable-next-line eqeqeq
return (context.getValue(this.key) == this.value);
}
serialize() {
return `${this.key} == '${this.value}'`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeyInExpr {
static create(key, valueKey) {
return new ContextKeyInExpr(key, valueKey);
}
constructor(key, valueKey) {
this.key = key;
this.valueKey = valueKey;
this.type = 10 /* ContextKeyExprType.In */;
this.negated = null;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.valueKey, other.key, other.valueKey);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.valueKey === other.valueKey);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
const source = context.getValue(this.valueKey);
const item = context.getValue(this.key);
if (Array.isArray(source)) {
return source.includes(item);
}
if (typeof item === 'string' && typeof source === 'object' && source !== null) {
return hasOwnProperty.call(source, item);
}
return false;
}
serialize() {
return `${this.key} in '${this.valueKey}'`;
}
keys() {
return [this.key, this.valueKey];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey);
}
return this.negated;
}
}
export class ContextKeyNotInExpr {
static create(key, valueKey) {
return new ContextKeyNotInExpr(key, valueKey);
}
constructor(key, valueKey) {
this.key = key;
this.valueKey = valueKey;
this.type = 11 /* ContextKeyExprType.NotIn */;
this._negated = ContextKeyInExpr.create(key, valueKey);
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return this._negated.cmp(other._negated);
}
equals(other) {
if (other.type === this.type) {
return this._negated.equals(other._negated);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
return !this._negated.evaluate(context);
}
serialize() {
return `${this.key} not in '${this.valueKey}'`;
}
keys() {
return this._negated.keys();
}
negate() {
return this._negated;
}
}
export class ContextKeyNotEqualsExpr {
static create(key, value, negated = null) {
if (typeof value === 'boolean') {
if (value) {
return ContextKeyNotExpr.create(key, negated);
}
return ContextKeyDefinedExpr.create(key, negated);
}
const constantValue = CONSTANT_VALUES.get(key);
if (typeof constantValue === 'boolean') {
const falseValue = constantValue ? 'true' : 'false';
return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
}
return new ContextKeyNotEqualsExpr(key, value, negated);
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 5 /* ContextKeyExprType.NotEquals */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
const constantValue = CONSTANT_VALUES.get(this.key);
if (typeof constantValue === 'boolean') {
const falseValue = constantValue ? 'true' : 'false';
return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
}
return this;
}
evaluate(context) {
// Intentional !=
// eslint-disable-next-line eqeqeq
return (context.getValue(this.key) != this.value);
}
serialize() {
return `${this.key} != '${this.value}'`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeyNotExpr {
static create(key, negated = null) {
const constantValue = CONSTANT_VALUES.get(key);
if (typeof constantValue === 'boolean') {
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
}
return new ContextKeyNotExpr(key, negated);
}
constructor(key, negated) {
this.key = key;
this.negated = negated;
this.type = 3 /* ContextKeyExprType.Not */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp1(this.key, other.key);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key);
}
return false;
}
substituteConstants() {
const constantValue = CONSTANT_VALUES.get(this.key);
if (typeof constantValue === 'boolean') {
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
}
return this;
}
evaluate(context) {
return (!context.getValue(this.key));
}
serialize() {
return `!${this.key}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyDefinedExpr.create(this.key, this);
}
return this.negated;
}
}
function withFloatOrStr(value, callback) {
if (typeof value === 'string') {
const n = parseFloat(value);
if (!isNaN(n)) {
value = n;
}
}
if (typeof value === 'string' || typeof value === 'number') {
return callback(value);
}
return ContextKeyFalseExpr.INSTANCE;
}
export class ContextKeyGreaterExpr {
static create(key, _value, negated = null) {
return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated));
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 12 /* ContextKeyExprType.Greater */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
if (typeof this.value === 'string') {
return false;
}
return (parseFloat(context.getValue(this.key)) > this.value);
}
serialize() {
return `${this.key} > ${this.value}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeyGreaterEqualsExpr {
static create(key, _value, negated = null) {
return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated));
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 13 /* ContextKeyExprType.GreaterEquals */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
if (typeof this.value === 'string') {
return false;
}
return (parseFloat(context.getValue(this.key)) >= this.value);
}
serialize() {
return `${this.key} >= ${this.value}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeySmallerExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeySmallerExpr {
static create(key, _value, negated = null) {
return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated));
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 14 /* ContextKeyExprType.Smaller */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
if (typeof this.value === 'string') {
return false;
}
return (parseFloat(context.getValue(this.key)) < this.value);
}
serialize() {
return `${this.key} < ${this.value}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeySmallerEqualsExpr {
static create(key, _value, negated = null) {
return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated));
}
constructor(key, value, negated) {
this.key = key;
this.value = value;
this.negated = negated;
this.type = 15 /* ContextKeyExprType.SmallerEquals */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
equals(other) {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
if (typeof this.value === 'string') {
return false;
}
return (parseFloat(context.getValue(this.key)) <= this.value);
}
serialize() {
return `${this.key} <= ${this.value}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this);
}
return this.negated;
}
}
export class ContextKeyRegexExpr {
static create(key, regexp) {
return new ContextKeyRegexExpr(key, regexp);
}
constructor(key, regexp) {
this.key = key;
this.regexp = regexp;
this.type = 7 /* ContextKeyExprType.Regex */;
this.negated = null;
//
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
const thisSource = this.regexp ? this.regexp.source : '';
const otherSource = other.regexp ? other.regexp.source : '';
if (thisSource < otherSource) {
return -1;
}
if (thisSource > otherSource) {
return 1;
}
return 0;
}
equals(other) {
if (other.type === this.type) {
const thisSource = this.regexp ? this.regexp.source : '';
const otherSource = other.regexp ? other.regexp.source : '';
return (this.key === other.key && thisSource === otherSource);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
const value = context.getValue(this.key);
return this.regexp ? this.regexp.test(value) : false;
}
serialize() {
const value = this.regexp
? `/${this.regexp.source}/${this.regexp.flags}`
: '/invalid/';
return `${this.key} =~ ${value}`;
}
keys() {
return [this.key];
}
negate() {
if (!this.negated) {
this.negated = ContextKeyNotRegexExpr.create(this);
}
return this.negated;
}
}
export class ContextKeyNotRegexExpr {
static create(actual) {
return new ContextKeyNotRegexExpr(actual);
}
constructor(_actual) {
this._actual = _actual;
this.type = 8 /* ContextKeyExprType.NotRegex */;
//
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
return this._actual.cmp(other._actual);
}
equals(other) {
if (other.type === this.type) {
return this._actual.equals(other._actual);
}
return false;
}
substituteConstants() {
return this;
}
evaluate(context) {
return !this._actual.evaluate(context);
}
serialize() {
return `!(${this._actual.serialize()})`;
}
keys() {
return this._actual.keys();
}
negate() {
return this._actual;
}
}
/**
* @returns the same instance if nothing changed.
*/
function eliminateConstantsInArray(arr) {
// Allocate array only if there is a difference
let newArr = null;
for (let i = 0, len = arr.length; i < len; i++) {
const newExpr = arr[i].substituteConstants();
if (arr[i] !== newExpr) {
// something has changed!
// allocate array on first difference
if (newArr === null) {
newArr = [];
for (let j = 0; j < i; j++) {
newArr[j] = arr[j];
}
}
}
if (newArr !== null) {
newArr[i] = newExpr;
}
}
if (newArr === null) {
return arr;
}
return newArr;
}
export class ContextKeyAndExpr {
static create(_expr, negated, extraRedundantCheck) {
return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck);
}
constructor(expr, negated) {
this.expr = expr;
this.negated = negated;
this.type = 6 /* ContextKeyExprType.And */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.expr.length < other.expr.length) {
return -1;
}
if (this.expr.length > other.expr.length) {
return 1;
}
for (let i = 0, len = this.expr.length; i < len; i++) {
const r = cmp(this.expr[i], other.expr[i]);
if (r !== 0) {
return r;
}
}
return 0;
}
equals(other) {
if (other.type === this.type) {
if (this.expr.length !== other.expr.length) {
return false;
}
for (let i = 0, len = this.expr.length; i < len; i++) {
if (!this.expr[i].equals(other.expr[i])) {
return false;
}
}
return true;
}
return false;
}
substituteConstants() {
const exprArr = eliminateConstantsInArray(this.expr);
if (exprArr === this.expr) {
// no change
return this;
}
return ContextKeyAndExpr.create(exprArr, this.negated, false);
}
evaluate(context) {
for (let i = 0, len = this.expr.length; i < len; i++) {
if (!this.expr[i].evaluate(context)) {
return false;
}
}
return true;
}
static _normalizeArr(arr, negated, extraRedundantCheck) {
const expr = [];
let hasTrue = false;
for (const e of arr) {
if (!e) {
continue;
}
if (e.type === 1 /* ContextKeyExprType.True */) {
// anything && true ==> anything
hasTrue = true;
continue;
}
if (e.type === 0 /* ContextKeyExprType.False */) {
// anything && false ==> false
return ContextKeyFalseExpr.INSTANCE;
}
if (e.type === 6 /* ContextKeyExprType.And */) {
expr.push(...e.expr);
continue;
}
expr.push(e);
}
if (expr.length === 0 && hasTrue) {
return ContextKeyTrueExpr.INSTANCE;
}
if (expr.length === 0) {
return undefined;
}
if (expr.length === 1) {
return expr[0];
}
expr.sort(cmp);
// eliminate duplicate terms
for (let i = 1; i < expr.length; i++) {
if (expr[i - 1].equals(expr[i])) {
expr.splice(i, 1);
i--;
}
}
if (expr.length === 1) {
return expr[0];
}
// We must distribute any OR expression because we don't support parens
// OR extensions will be at the end (due to sorting rules)
while (expr.length > 1) {
const lastElement = expr[expr.length - 1];
if (lastElement.type !== 9 /* ContextKeyExprType.Or */) {
break;
}
// pop the last element
expr.pop();
// pop the second to last element
const secondToLastElement = expr.pop();
const isFinished = (expr.length === 0);
// distribute `lastElement` over `secondToLastElement`
const resultElement = ContextKeyOrExpr.create(lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null, extraRedundantCheck)), null, isFinished);
if (resultElement) {
expr.push(resultElement);
expr.sort(cmp);
}
}
if (expr.length === 1) {
return expr[0];
}
// resolve false AND expressions
if (extraRedundantCheck) {
for (let i = 0; i < expr.length; i++) {
for (let j = i + 1; j < expr.length; j++) {
if (expr[i].negate().equals(expr[j])) {
// A && !A case
return ContextKeyFalseExpr.INSTANCE;
}
}
}
if (expr.length === 1) {
return expr[0];
}
}
return new ContextKeyAndExpr(expr, negated);
}
serialize() {
return this.expr.map(e => e.serialize()).join(' && ');
}
keys() {
const result = [];
for (const expr of this.expr) {
result.push(...expr.keys());
}
return result;
}
negate() {
if (!this.negated) {
const result = [];
for (const expr of this.expr) {
result.push(expr.negate());
}
this.negated = ContextKeyOrExpr.create(result, this, true);
}
return this.negated;
}
}
export class ContextKeyOrExpr {
static create(_expr, negated, extraRedundantCheck) {
return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck);
}
constructor(expr, negated) {
this.expr = expr;
this.negated = negated;
this.type = 9 /* ContextKeyExprType.Or */;
}
cmp(other) {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.expr.length < other.expr.length) {
return -1;
}
if (this.expr.length > other.expr.length) {
return 1;
}
for (let i = 0, len = this.expr.length; i < len; i++) {
const r = cmp(this.expr[i], other.expr[i]);
if (r !== 0) {
return r;
}
}
return 0;
}
equals(other) {
if (other.type === this.type) {
if (this.expr.length !== other.expr.length) {
return false;
}
for (let i = 0, len = this.expr.length; i < len; i++) {
if (!this.expr[i].equals(other.expr[i])) {
return false;
}
}
return true;
}
return false;
}
substituteConstants() {
const exprArr = eliminateConstantsInArray(this.expr);
if (exprArr === this.expr) {
// no change
return this;
}
return ContextKeyOrExpr.create(exprArr, this.negated, false);
}
evaluate(context) {
for (let i = 0, len = this.expr.length; i < len; i++) {
if (this.expr[i].evaluate(context)) {
return true;
}
}
return false;
}
static _normalizeArr(arr, negated, extraRedundantCheck) {
let expr = [];
let hasFalse = false;
if (arr) {
for (let i = 0, len = arr.length; i < len; i++) {
const e = arr[i];
if (!e) {
continue;
}
if (e.type === 0 /* ContextKeyExprType.False */) {
// anything || false ==> anything
hasFalse = true;
continue;
}
if (e.type === 1 /* ContextKeyExprType.True */) {
// anything || true ==> true
return ContextKeyTrueExpr.INSTANCE;
}
if (e.type === 9 /* ContextKeyExprType.Or */) {
expr = expr.concat(e.expr);
continue;
}
expr.push(e);
}
if (expr.length === 0 && hasFalse) {
return ContextKeyFalseExpr.INSTANCE;
}
expr.sort(cmp);
}
if (expr.length === 0) {
return undefined;
}
if (expr.length === 1) {
return expr[0];
}
// eliminate duplicate terms
for (let i = 1; i < expr.length; i++) {
if (expr[i - 1].equals(expr[i])) {
expr.splice(i, 1);
i--;
}
}
if (expr.length === 1) {
return expr[0];
}
// resolve true OR expressions
if (extraRedundantCheck) {
for (let i = 0; i < expr.length; i++) {
for (let j = i + 1; j < expr.length; j++) {
if (expr[i].negate().equals(expr[j])) {
// A || !A case
return ContextKeyTrueExpr.INSTANCE;
}
}
}
if (expr.length === 1) {
return expr[0];
}
}
return new ContextKeyOrExpr(expr, negated);
}
serialize() {
return this.expr.map(e => e.serialize()).join(' || ');
}
keys() {
const result = [];
for (const expr of this.expr) {
result.push(...expr.keys());
}
return result;
}
negate() {
if (!this.negated) {
const result = [];
for (const expr of this.expr) {
result.push(expr.negate());
}
// We don't support parens, so here we distribute the AND over the OR terminals
// We always take the first 2 AND pairs and distribu