Path: blob/main/src/vs/platform/contextkey/common/contextkey.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../base/common/charCode.js';6import { Event } from '../../../base/common/event.js';7import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js';8import { isFalsyOrWhitespace } from '../../../base/common/strings.js';9import { Scanner, LexingError, Token, TokenType } from './scanner.js';10import { createDecorator } from '../../instantiation/common/instantiation.js';11import { localize } from '../../../nls.js';12import { IDisposable } from '../../../base/common/lifecycle.js';13import { illegalArgument } from '../../../base/common/errors.js';1415const CONSTANT_VALUES = new Map<string, boolean>();16CONSTANT_VALUES.set('false', false);17CONSTANT_VALUES.set('true', true);18CONSTANT_VALUES.set('isMac', isMacintosh);19CONSTANT_VALUES.set('isLinux', isLinux);20CONSTANT_VALUES.set('isWindows', isWindows);21CONSTANT_VALUES.set('isWeb', isWeb);22CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);23CONSTANT_VALUES.set('isEdge', isEdge);24CONSTANT_VALUES.set('isFirefox', isFirefox);25CONSTANT_VALUES.set('isChrome', isChrome);26CONSTANT_VALUES.set('isSafari', isSafari);2728/** allow register constant context keys that are known only after startup; requires running `substituteConstants` on the context key - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127 */29export function setConstant(key: string, value: boolean) {30if (CONSTANT_VALUES.get(key) !== undefined) { throw illegalArgument('contextkey.setConstant(k, v) invoked with already set constant `k`'); }3132CONSTANT_VALUES.set(key, value);33}3435const hasOwnProperty = Object.prototype.hasOwnProperty;3637export const enum ContextKeyExprType {38False = 0,39True = 1,40Defined = 2,41Not = 3,42Equals = 4,43NotEquals = 5,44And = 6,45Regex = 7,46NotRegex = 8,47Or = 9,48In = 10,49NotIn = 11,50Greater = 12,51GreaterEquals = 13,52Smaller = 14,53SmallerEquals = 15,54}5556export interface IContextKeyExprMapper {57mapDefined(key: string): ContextKeyExpression;58mapNot(key: string): ContextKeyExpression;59mapEquals(key: string, value: any): ContextKeyExpression;60mapNotEquals(key: string, value: any): ContextKeyExpression;61mapGreater(key: string, value: any): ContextKeyExpression;62mapGreaterEquals(key: string, value: any): ContextKeyExpression;63mapSmaller(key: string, value: any): ContextKeyExpression;64mapSmallerEquals(key: string, value: any): ContextKeyExpression;65mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr;66mapIn(key: string, valueKey: string): ContextKeyInExpr;67mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr;68}6970export interface IContextKeyExpression {71cmp(other: ContextKeyExpression): number;72equals(other: ContextKeyExpression): boolean;73substituteConstants(): ContextKeyExpression | undefined;74evaluate(context: IContext): boolean;75serialize(): string;76keys(): string[];77map(mapFnc: IContextKeyExprMapper): ContextKeyExpression;78negate(): ContextKeyExpression;7980}8182export type ContextKeyExpression = (83ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr84| ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr85| ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr86| ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr87| ContextKeySmallerExpr | ContextKeySmallerEqualsExpr88);899091/*9293Syntax grammar:9495```ebnf9697expression ::= or9899or ::= and { '||' and }*100101and ::= term { '&&' term }*102103term ::=104| '!' (KEY | true | false | parenthesized)105| primary106107primary ::=108| 'true'109| 'false'110| parenthesized111| KEY '=~' REGEX112| KEY [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'not' 'in' | 'in') value ]113114parenthesized ::=115| '(' expression ')'116117value ::=118| 'true'119| 'false'120| 'in' // we support `in` as a value because there's an extension that uses it, ie "when": "languageId == in"121| VALUE // matched by the same regex as KEY; consider putting the value in single quotes if it's a string (e.g., with spaces)122| SINGLE_QUOTED_STR123| EMPTY_STR // this allows "when": "foo == " which's used by existing extensions124125```126*/127128export type ParserConfig = {129/**130* with this option enabled, the parser can recover from regex parsing errors, e.g., unescaped slashes: `/src//` is accepted as `/src\//` would be131*/132regexParsingWithErrorRecovery: boolean;133};134135const defaultConfig: ParserConfig = {136regexParsingWithErrorRecovery: true137};138139export type ParsingError = {140message: string;141offset: number;142lexeme: string;143additionalInfo?: string;144};145146const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression");147const 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.");148const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'.");149const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'");150const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token");151const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?");152const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression");153const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?");154155/**156* A parser for context key expressions.157*158* Example:159* ```ts160* const parser = new Parser();161* const expr = parser.parse('foo == "bar" && baz == true');162*163* if (expr === undefined) {164* // there were lexing or parsing errors165* // process lexing errors with `parser.lexingErrors`166* // process parsing errors with `parser.parsingErrors`167* } else {168* // expr is a valid expression169* }170* ```171*/172export class Parser {173// Note: this doesn't produce an exact syntax tree but a normalized one174// ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize175176private static _parseError = new Error();177178// lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse`179private readonly _scanner = new Scanner();180181// lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse`182private _tokens: Token[] = [];183private _current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd`184private _parsingErrors: ParsingError[] = [];185186get lexingErrors(): Readonly<LexingError[]> {187return this._scanner.errors;188}189190get parsingErrors(): Readonly<ParsingError[]> {191return this._parsingErrors;192}193194constructor(private readonly _config: ParserConfig = defaultConfig) {195}196197/**198* Parse a context key expression.199*200* @param input the expression to parse201* @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors202*/203parse(input: string): ContextKeyExpression | undefined {204205if (input === '') {206this._parsingErrors.push({ message: errorEmptyString, offset: 0, lexeme: '', additionalInfo: hintEmptyString });207return undefined;208}209210this._tokens = this._scanner.reset(input).scan();211// @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 recovery212213this._current = 0;214this._parsingErrors = [];215216try {217const expr = this._expr();218if (!this._isAtEnd()) {219const peek = this._peek();220const additionalInfo = peek.type === TokenType.Str ? hintUnexpectedToken : undefined;221this._parsingErrors.push({ message: errorUnexpectedToken, offset: peek.offset, lexeme: Scanner.getLexeme(peek), additionalInfo });222throw Parser._parseError;223}224return expr;225} catch (e) {226if (!(e === Parser._parseError)) {227throw e;228}229return undefined;230}231}232233private _expr(): ContextKeyExpression | undefined {234return this._or();235}236237private _or(): ContextKeyExpression | undefined {238const expr = [this._and()];239240while (this._matchOne(TokenType.Or)) {241const right = this._and();242expr.push(right);243}244245return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr);246}247248private _and(): ContextKeyExpression | undefined {249const expr = [this._term()];250251while (this._matchOne(TokenType.And)) {252const right = this._term();253expr.push(right);254}255256return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr);257}258259private _term(): ContextKeyExpression | undefined {260if (this._matchOne(TokenType.Neg)) {261const peek = this._peek();262switch (peek.type) {263case TokenType.True:264this._advance();265return ContextKeyFalseExpr.INSTANCE;266case TokenType.False:267this._advance();268return ContextKeyTrueExpr.INSTANCE;269case TokenType.LParen: {270this._advance();271const expr = this._expr();272this._consume(TokenType.RParen, errorClosingParenthesis);273return expr?.negate();274}275case TokenType.Str:276this._advance();277return ContextKeyNotExpr.create(peek.lexeme);278default:279throw this._errExpectedButGot(`KEY | true | false | '(' expression ')'`, peek);280}281}282return this._primary();283}284285private _primary(): ContextKeyExpression | undefined {286287const peek = this._peek();288switch (peek.type) {289case TokenType.True:290this._advance();291return ContextKeyExpr.true();292293case TokenType.False:294this._advance();295return ContextKeyExpr.false();296297case TokenType.LParen: {298this._advance();299const expr = this._expr();300this._consume(TokenType.RParen, errorClosingParenthesis);301return expr;302}303304case TokenType.Str: {305// KEY306const key = peek.lexeme;307this._advance();308309// =~ regex310if (this._matchOne(TokenType.RegexOp)) {311312// @ulugbekna: we need to reconstruct the regex from the tokens because some extensions use unescaped slashes in regexes313const expr = this._peek();314315if (!this._config.regexParsingWithErrorRecovery) {316this._advance();317if (expr.type !== TokenType.RegexStr) {318throw this._errExpectedButGot(`REGEX`, expr);319}320const regexLexeme = expr.lexeme;321const closingSlashIndex = regexLexeme.lastIndexOf('/');322const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));323let regexp: RegExp | null;324try {325regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);326} catch (e) {327throw this._errExpectedButGot(`REGEX`, expr);328}329return ContextKeyRegexExpr.create(key, regexp);330}331332switch (expr.type) {333case TokenType.RegexStr:334case TokenType.Error: { // also handle an ErrorToken in case of smth such as /(/file)/335const lexemeReconstruction = [expr.lexeme]; // /REGEX/ or /REGEX/FLAGS336this._advance();337338let followingToken = this._peek();339let parenBalance = 0;340for (let i = 0; i < expr.lexeme.length; i++) {341if (expr.lexeme.charCodeAt(i) === CharCode.OpenParen) {342parenBalance++;343} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {344parenBalance--;345}346}347348while (!this._isAtEnd() && followingToken.type !== TokenType.And && followingToken.type !== TokenType.Or) {349switch (followingToken.type) {350case TokenType.LParen:351parenBalance++;352break;353case TokenType.RParen:354parenBalance--;355break;356case TokenType.RegexStr:357case TokenType.QuotedStr:358for (let i = 0; i < followingToken.lexeme.length; i++) {359if (followingToken.lexeme.charCodeAt(i) === CharCode.OpenParen) {360parenBalance++;361} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {362parenBalance--;363}364}365}366if (parenBalance < 0) {367break;368}369lexemeReconstruction.push(Scanner.getLexeme(followingToken));370this._advance();371followingToken = this._peek();372}373374const regexLexeme = lexemeReconstruction.join('');375const closingSlashIndex = regexLexeme.lastIndexOf('/');376const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));377let regexp: RegExp | null;378try {379regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);380} catch (e) {381throw this._errExpectedButGot(`REGEX`, expr);382}383return ContextKeyExpr.regex(key, regexp);384}385386case TokenType.QuotedStr: {387const serializedValue = expr.lexeme;388this._advance();389// replicate old regex parsing behavior390391let regex: RegExp | null = null;392393if (!isFalsyOrWhitespace(serializedValue)) {394const start = serializedValue.indexOf('/');395const end = serializedValue.lastIndexOf('/');396if (start !== end && start >= 0) {397398const value = serializedValue.slice(start + 1, end);399const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : '';400try {401regex = new RegExp(value, caseIgnoreFlag);402} catch (_e) {403throw this._errExpectedButGot(`REGEX`, expr);404}405}406}407408if (regex === null) {409throw this._errExpectedButGot('REGEX', expr);410}411412return ContextKeyRegexExpr.create(key, regex);413}414415default:416throw this._errExpectedButGot('REGEX', this._peek());417}418}419420// [ 'not' 'in' value ]421if (this._matchOne(TokenType.Not)) {422this._consume(TokenType.In, errorNoInAfterNot);423const right = this._value();424return ContextKeyExpr.notIn(key, right);425}426427// [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in') value ]428const maybeOp = this._peek().type;429switch (maybeOp) {430case TokenType.Eq: {431this._advance();432433const right = this._value();434if (this._previous().type === TokenType.QuotedStr) { // to preserve old parser behavior: "foo == 'true'" is preserved as "foo == 'true'", but "foo == true" is optimized as "foo"435return ContextKeyExpr.equals(key, right);436}437switch (right) {438case 'true':439return ContextKeyExpr.has(key);440case 'false':441return ContextKeyExpr.not(key);442default:443return ContextKeyExpr.equals(key, right);444}445}446447case TokenType.NotEq: {448this._advance();449450const right = this._value();451if (this._previous().type === TokenType.QuotedStr) { // same as above with "foo != 'true'"452return ContextKeyExpr.notEquals(key, right);453}454switch (right) {455case 'true':456return ContextKeyExpr.not(key);457case 'false':458return ContextKeyExpr.has(key);459default:460return ContextKeyExpr.notEquals(key, right);461}462}463// 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 number464// consequently, package.json linter should _warn_ the user if they're passing undesired things to ops465case TokenType.Lt:466this._advance();467return ContextKeySmallerExpr.create(key, this._value());468469case TokenType.LtEq:470this._advance();471return ContextKeySmallerEqualsExpr.create(key, this._value());472473case TokenType.Gt:474this._advance();475return ContextKeyGreaterExpr.create(key, this._value());476477case TokenType.GtEq:478this._advance();479return ContextKeyGreaterEqualsExpr.create(key, this._value());480481case TokenType.In:482this._advance();483return ContextKeyExpr.in(key, this._value());484485default:486return ContextKeyExpr.has(key);487}488}489490case TokenType.EOF:491this._parsingErrors.push({ message: errorUnexpectedEOF, offset: peek.offset, lexeme: '', additionalInfo: hintUnexpectedEOF });492throw Parser._parseError;493494default:495throw this._errExpectedButGot(`true | false | KEY \n\t| KEY '=~' REGEX \n\t| KEY ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in') value`, this._peek());496497}498}499500private _value(): string {501const token = this._peek();502switch (token.type) {503case TokenType.Str:504case TokenType.QuotedStr:505this._advance();506return token.lexeme;507case TokenType.True:508this._advance();509return 'true';510case TokenType.False:511this._advance();512return 'false';513case TokenType.In: // we support `in` as a value, e.g., "when": "languageId == in" - exists in existing extensions514this._advance();515return 'in';516default:517// this allows "when": "foo == " which's used by existing extensions518// we do not call `_advance` on purpose - we don't want to eat unintended tokens519return '';520}521}522523private _flagsGYRe = /g|y/g;524private _removeFlagsGY(flags: string): string {525return flags.replaceAll(this._flagsGYRe, '');526}527528// careful: this can throw if current token is the initial one (ie index = 0)529private _previous() {530return this._tokens[this._current - 1];531}532533private _matchOne(token: TokenType) {534if (this._check(token)) {535this._advance();536return true;537}538539return false;540}541542private _advance() {543if (!this._isAtEnd()) {544this._current++;545}546return this._previous();547}548549private _consume(type: TokenType, message: string) {550if (this._check(type)) {551return this._advance();552}553554throw this._errExpectedButGot(message, this._peek());555}556557private _errExpectedButGot(expected: string, got: Token, additionalInfo?: string) {558const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got));559const offset = got.offset;560const lexeme = Scanner.getLexeme(got);561this._parsingErrors.push({ message, offset, lexeme, additionalInfo });562return Parser._parseError;563}564565private _check(type: TokenType) {566return this._peek().type === type;567}568569private _peek() {570return this._tokens[this._current];571}572573private _isAtEnd() {574return this._peek().type === TokenType.EOF;575}576}577578export abstract class ContextKeyExpr {579580public static false(): ContextKeyExpression {581return ContextKeyFalseExpr.INSTANCE;582}583public static true(): ContextKeyExpression {584return ContextKeyTrueExpr.INSTANCE;585}586public static has(key: string): ContextKeyExpression {587return ContextKeyDefinedExpr.create(key);588}589public static equals(key: string, value: any): ContextKeyExpression {590return ContextKeyEqualsExpr.create(key, value);591}592public static notEquals(key: string, value: any): ContextKeyExpression {593return ContextKeyNotEqualsExpr.create(key, value);594}595public static regex(key: string, value: RegExp): ContextKeyExpression {596return ContextKeyRegexExpr.create(key, value);597}598public static in(key: string, value: string): ContextKeyExpression {599return ContextKeyInExpr.create(key, value);600}601public static notIn(key: string, value: string): ContextKeyExpression {602return ContextKeyNotInExpr.create(key, value);603}604public static not(key: string): ContextKeyExpression {605return ContextKeyNotExpr.create(key);606}607public static and(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {608return ContextKeyAndExpr.create(expr, null, true);609}610public static or(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {611return ContextKeyOrExpr.create(expr, null, true);612}613public static greater(key: string, value: number): ContextKeyExpression {614return ContextKeyGreaterExpr.create(key, value);615}616public static greaterEquals(key: string, value: number): ContextKeyExpression {617return ContextKeyGreaterEqualsExpr.create(key, value);618}619public static smaller(key: string, value: number): ContextKeyExpression {620return ContextKeySmallerExpr.create(key, value);621}622public static smallerEquals(key: string, value: number): ContextKeyExpression {623return ContextKeySmallerEqualsExpr.create(key, value);624}625626private static _parser = new Parser({ regexParsingWithErrorRecovery: false });627public static deserialize(serialized: string | null | undefined): ContextKeyExpression | undefined {628if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported629return undefined;630}631632const expr = this._parser.parse(serialized);633return expr;634}635636}637638639export function validateWhenClauses(whenClauses: string[]): any {640641const parser = new Parser({ regexParsingWithErrorRecovery: false }); // we run with no recovery to guide users to use correct regexes642643return whenClauses.map(whenClause => {644parser.parse(whenClause);645646if (parser.lexingErrors.length > 0) {647return parser.lexingErrors.map((se: LexingError) => ({648errorMessage: se.additionalInfo ?649localize('contextkey.scanner.errorForLinterWithHint', "Unexpected token. Hint: {0}", se.additionalInfo) :650localize('contextkey.scanner.errorForLinter', "Unexpected token."),651offset: se.offset,652length: se.lexeme.length,653}));654} else if (parser.parsingErrors.length > 0) {655return parser.parsingErrors.map((pe: ParsingError) => ({656errorMessage: pe.additionalInfo ? `${pe.message}. ${pe.additionalInfo}` : pe.message,657offset: pe.offset,658length: pe.lexeme.length,659}));660} else {661return [];662}663});664}665666export function expressionsAreEqualWithConstantSubstitution(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {667const aExpr = a ? a.substituteConstants() : undefined;668const bExpr = b ? b.substituteConstants() : undefined;669if (!aExpr && !bExpr) {670return true;671}672if (!aExpr || !bExpr) {673return false;674}675return aExpr.equals(bExpr);676}677678function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number {679return a.cmp(b);680}681682export class ContextKeyFalseExpr implements IContextKeyExpression {683public static INSTANCE = new ContextKeyFalseExpr();684685public readonly type = ContextKeyExprType.False;686687protected constructor() {688}689690public cmp(other: ContextKeyExpression): number {691return this.type - other.type;692}693694public equals(other: ContextKeyExpression): boolean {695return (other.type === this.type);696}697698public substituteConstants(): ContextKeyExpression | undefined {699return this;700}701702public evaluate(context: IContext): boolean {703return false;704}705706public serialize(): string {707return 'false';708}709710public keys(): string[] {711return [];712}713714public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {715return this;716}717718public negate(): ContextKeyExpression {719return ContextKeyTrueExpr.INSTANCE;720}721}722723export class ContextKeyTrueExpr implements IContextKeyExpression {724public static INSTANCE = new ContextKeyTrueExpr();725726public readonly type = ContextKeyExprType.True;727728protected constructor() {729}730731public cmp(other: ContextKeyExpression): number {732return this.type - other.type;733}734735public equals(other: ContextKeyExpression): boolean {736return (other.type === this.type);737}738739public substituteConstants(): ContextKeyExpression | undefined {740return this;741}742743public evaluate(context: IContext): boolean {744return true;745}746747public serialize(): string {748return 'true';749}750751public keys(): string[] {752return [];753}754755public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {756return this;757}758759public negate(): ContextKeyExpression {760return ContextKeyFalseExpr.INSTANCE;761}762}763764export class ContextKeyDefinedExpr implements IContextKeyExpression {765public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {766const constantValue = CONSTANT_VALUES.get(key);767if (typeof constantValue === 'boolean') {768return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;769}770return new ContextKeyDefinedExpr(key, negated);771}772773public readonly type = ContextKeyExprType.Defined;774775protected constructor(776readonly key: string,777private negated: ContextKeyExpression | null778) {779}780781public cmp(other: ContextKeyExpression): number {782if (other.type !== this.type) {783return this.type - other.type;784}785return cmp1(this.key, other.key);786}787788public equals(other: ContextKeyExpression): boolean {789if (other.type === this.type) {790return (this.key === other.key);791}792return false;793}794795public substituteConstants(): ContextKeyExpression | undefined {796const constantValue = CONSTANT_VALUES.get(this.key);797if (typeof constantValue === 'boolean') {798return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;799}800return this;801}802803public evaluate(context: IContext): boolean {804return (!!context.getValue(this.key));805}806807public serialize(): string {808return this.key;809}810811public keys(): string[] {812return [this.key];813}814815public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {816return mapFnc.mapDefined(this.key);817}818819public negate(): ContextKeyExpression {820if (!this.negated) {821this.negated = ContextKeyNotExpr.create(this.key, this);822}823return this.negated;824}825}826827export class ContextKeyEqualsExpr implements IContextKeyExpression {828829public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {830if (typeof value === 'boolean') {831return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated));832}833const constantValue = CONSTANT_VALUES.get(key);834if (typeof constantValue === 'boolean') {835const trueValue = constantValue ? 'true' : 'false';836return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);837}838return new ContextKeyEqualsExpr(key, value, negated);839}840841public readonly type = ContextKeyExprType.Equals;842843private constructor(844private readonly key: string,845private readonly value: any,846private negated: ContextKeyExpression | null847) {848}849850public cmp(other: ContextKeyExpression): number {851if (other.type !== this.type) {852return this.type - other.type;853}854return cmp2(this.key, this.value, other.key, other.value);855}856857public equals(other: ContextKeyExpression): boolean {858if (other.type === this.type) {859return (this.key === other.key && this.value === other.value);860}861return false;862}863864public substituteConstants(): ContextKeyExpression | undefined {865const constantValue = CONSTANT_VALUES.get(this.key);866if (typeof constantValue === 'boolean') {867const trueValue = constantValue ? 'true' : 'false';868return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);869}870return this;871}872873public evaluate(context: IContext): boolean {874// Intentional ==875// eslint-disable-next-line eqeqeq876return (context.getValue(this.key) == this.value);877}878879public serialize(): string {880return `${this.key} == '${this.value}'`;881}882883public keys(): string[] {884return [this.key];885}886887public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {888return mapFnc.mapEquals(this.key, this.value);889}890891public negate(): ContextKeyExpression {892if (!this.negated) {893this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this);894}895return this.negated;896}897}898899export class ContextKeyInExpr implements IContextKeyExpression {900901public static create(key: string, valueKey: string): ContextKeyInExpr {902return new ContextKeyInExpr(key, valueKey);903}904905public readonly type = ContextKeyExprType.In;906private negated: ContextKeyExpression | null = null;907908private constructor(909private readonly key: string,910private readonly valueKey: string,911) {912}913914public cmp(other: ContextKeyExpression): number {915if (other.type !== this.type) {916return this.type - other.type;917}918return cmp2(this.key, this.valueKey, other.key, other.valueKey);919}920921public equals(other: ContextKeyExpression): boolean {922if (other.type === this.type) {923return (this.key === other.key && this.valueKey === other.valueKey);924}925return false;926}927928public substituteConstants(): ContextKeyExpression | undefined {929return this;930}931932public evaluate(context: IContext): boolean {933const source = context.getValue(this.valueKey);934935const item = context.getValue(this.key);936937if (Array.isArray(source)) {938return source.includes(item as any);939}940941if (typeof item === 'string' && typeof source === 'object' && source !== null) {942return hasOwnProperty.call(source, item);943}944return false;945}946947public serialize(): string {948return `${this.key} in '${this.valueKey}'`;949}950951public keys(): string[] {952return [this.key, this.valueKey];953}954955public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr {956return mapFnc.mapIn(this.key, this.valueKey);957}958959public negate(): ContextKeyExpression {960if (!this.negated) {961this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey);962}963return this.negated;964}965}966967export class ContextKeyNotInExpr implements IContextKeyExpression {968969public static create(key: string, valueKey: string): ContextKeyNotInExpr {970return new ContextKeyNotInExpr(key, valueKey);971}972973public readonly type = ContextKeyExprType.NotIn;974975private readonly _negated: ContextKeyInExpr;976977private constructor(978private readonly key: string,979private readonly valueKey: string,980) {981this._negated = ContextKeyInExpr.create(key, valueKey);982}983984public cmp(other: ContextKeyExpression): number {985if (other.type !== this.type) {986return this.type - other.type;987}988return this._negated.cmp(other._negated);989}990991public equals(other: ContextKeyExpression): boolean {992if (other.type === this.type) {993return this._negated.equals(other._negated);994}995return false;996}997998public substituteConstants(): ContextKeyExpression | undefined {999return this;1000}10011002public evaluate(context: IContext): boolean {1003return !this._negated.evaluate(context);1004}10051006public serialize(): string {1007return `${this.key} not in '${this.valueKey}'`;1008}10091010public keys(): string[] {1011return this._negated.keys();1012}10131014public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1015return mapFnc.mapNotIn(this.key, this.valueKey);1016}10171018public negate(): ContextKeyExpression {1019return this._negated;1020}1021}10221023export class ContextKeyNotEqualsExpr implements IContextKeyExpression {10241025public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {1026if (typeof value === 'boolean') {1027if (value) {1028return ContextKeyNotExpr.create(key, negated);1029}1030return ContextKeyDefinedExpr.create(key, negated);1031}1032const constantValue = CONSTANT_VALUES.get(key);1033if (typeof constantValue === 'boolean') {1034const falseValue = constantValue ? 'true' : 'false';1035return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);1036}1037return new ContextKeyNotEqualsExpr(key, value, negated);1038}10391040public readonly type = ContextKeyExprType.NotEquals;10411042private constructor(1043private readonly key: string,1044private readonly value: any,1045private negated: ContextKeyExpression | null1046) {1047}10481049public cmp(other: ContextKeyExpression): number {1050if (other.type !== this.type) {1051return this.type - other.type;1052}1053return cmp2(this.key, this.value, other.key, other.value);1054}10551056public equals(other: ContextKeyExpression): boolean {1057if (other.type === this.type) {1058return (this.key === other.key && this.value === other.value);1059}1060return false;1061}10621063public substituteConstants(): ContextKeyExpression | undefined {1064const constantValue = CONSTANT_VALUES.get(this.key);1065if (typeof constantValue === 'boolean') {1066const falseValue = constantValue ? 'true' : 'false';1067return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);1068}1069return this;1070}10711072public evaluate(context: IContext): boolean {1073// Intentional !=1074// eslint-disable-next-line eqeqeq1075return (context.getValue(this.key) != this.value);1076}10771078public serialize(): string {1079return `${this.key} != '${this.value}'`;1080}10811082public keys(): string[] {1083return [this.key];1084}10851086public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1087return mapFnc.mapNotEquals(this.key, this.value);1088}10891090public negate(): ContextKeyExpression {1091if (!this.negated) {1092this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this);1093}1094return this.negated;1095}1096}10971098export class ContextKeyNotExpr implements IContextKeyExpression {10991100public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {1101const constantValue = CONSTANT_VALUES.get(key);1102if (typeof constantValue === 'boolean') {1103return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);1104}1105return new ContextKeyNotExpr(key, negated);1106}11071108public readonly type = ContextKeyExprType.Not;11091110private constructor(1111private readonly key: string,1112private negated: ContextKeyExpression | null1113) {1114}11151116public cmp(other: ContextKeyExpression): number {1117if (other.type !== this.type) {1118return this.type - other.type;1119}1120return cmp1(this.key, other.key);1121}11221123public equals(other: ContextKeyExpression): boolean {1124if (other.type === this.type) {1125return (this.key === other.key);1126}1127return false;1128}11291130public substituteConstants(): ContextKeyExpression | undefined {1131const constantValue = CONSTANT_VALUES.get(this.key);1132if (typeof constantValue === 'boolean') {1133return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);1134}1135return this;1136}11371138public evaluate(context: IContext): boolean {1139return (!context.getValue(this.key));1140}11411142public serialize(): string {1143return `!${this.key}`;1144}11451146public keys(): string[] {1147return [this.key];1148}11491150public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1151return mapFnc.mapNot(this.key);1152}11531154public negate(): ContextKeyExpression {1155if (!this.negated) {1156this.negated = ContextKeyDefinedExpr.create(this.key, this);1157}1158return this.negated;1159}1160}11611162function withFloatOrStr<T extends ContextKeyExpression>(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr {1163if (typeof value === 'string') {1164const n = parseFloat(value);1165if (!isNaN(n)) {1166value = n;1167}1168}1169if (typeof value === 'string' || typeof value === 'number') {1170return callback(value);1171}1172return ContextKeyFalseExpr.INSTANCE;1173}11741175export class ContextKeyGreaterExpr implements IContextKeyExpression {11761177public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {1178return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated));1179}11801181public readonly type = ContextKeyExprType.Greater;11821183private constructor(1184private readonly key: string,1185private readonly value: number | string,1186private negated: ContextKeyExpression | null1187) { }11881189public cmp(other: ContextKeyExpression): number {1190if (other.type !== this.type) {1191return this.type - other.type;1192}1193return cmp2(this.key, this.value, other.key, other.value);1194}11951196public equals(other: ContextKeyExpression): boolean {1197if (other.type === this.type) {1198return (this.key === other.key && this.value === other.value);1199}1200return false;1201}12021203public substituteConstants(): ContextKeyExpression | undefined {1204return this;1205}12061207public evaluate(context: IContext): boolean {1208if (typeof this.value === 'string') {1209return false;1210}1211return (parseFloat(<any>context.getValue(this.key)) > this.value);1212}12131214public serialize(): string {1215return `${this.key} > ${this.value}`;1216}12171218public keys(): string[] {1219return [this.key];1220}12211222public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1223return mapFnc.mapGreater(this.key, this.value);1224}12251226public negate(): ContextKeyExpression {1227if (!this.negated) {1228this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this);1229}1230return this.negated;1231}1232}12331234export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression {12351236public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {1237return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated));1238}12391240public readonly type = ContextKeyExprType.GreaterEquals;12411242private constructor(1243private readonly key: string,1244private readonly value: number | string,1245private negated: ContextKeyExpression | null1246) { }12471248public cmp(other: ContextKeyExpression): number {1249if (other.type !== this.type) {1250return this.type - other.type;1251}1252return cmp2(this.key, this.value, other.key, other.value);1253}12541255public equals(other: ContextKeyExpression): boolean {1256if (other.type === this.type) {1257return (this.key === other.key && this.value === other.value);1258}1259return false;1260}12611262public substituteConstants(): ContextKeyExpression | undefined {1263return this;1264}12651266public evaluate(context: IContext): boolean {1267if (typeof this.value === 'string') {1268return false;1269}1270return (parseFloat(<any>context.getValue(this.key)) >= this.value);1271}12721273public serialize(): string {1274return `${this.key} >= ${this.value}`;1275}12761277public keys(): string[] {1278return [this.key];1279}12801281public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1282return mapFnc.mapGreaterEquals(this.key, this.value);1283}12841285public negate(): ContextKeyExpression {1286if (!this.negated) {1287this.negated = ContextKeySmallerExpr.create(this.key, this.value, this);1288}1289return this.negated;1290}1291}12921293export class ContextKeySmallerExpr implements IContextKeyExpression {12941295public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {1296return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated));1297}12981299public readonly type = ContextKeyExprType.Smaller;13001301private constructor(1302private readonly key: string,1303private readonly value: number | string,1304private negated: ContextKeyExpression | null1305) {1306}13071308public cmp(other: ContextKeyExpression): number {1309if (other.type !== this.type) {1310return this.type - other.type;1311}1312return cmp2(this.key, this.value, other.key, other.value);1313}13141315public equals(other: ContextKeyExpression): boolean {1316if (other.type === this.type) {1317return (this.key === other.key && this.value === other.value);1318}1319return false;1320}13211322public substituteConstants(): ContextKeyExpression | undefined {1323return this;1324}13251326public evaluate(context: IContext): boolean {1327if (typeof this.value === 'string') {1328return false;1329}1330return (parseFloat(<any>context.getValue(this.key)) < this.value);1331}13321333public serialize(): string {1334return `${this.key} < ${this.value}`;1335}13361337public keys(): string[] {1338return [this.key];1339}13401341public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1342return mapFnc.mapSmaller(this.key, this.value);1343}13441345public negate(): ContextKeyExpression {1346if (!this.negated) {1347this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this);1348}1349return this.negated;1350}1351}13521353export class ContextKeySmallerEqualsExpr implements IContextKeyExpression {13541355public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {1356return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated));1357}13581359public readonly type = ContextKeyExprType.SmallerEquals;13601361private constructor(1362private readonly key: string,1363private readonly value: number | string,1364private negated: ContextKeyExpression | null1365) {1366}13671368public cmp(other: ContextKeyExpression): number {1369if (other.type !== this.type) {1370return this.type - other.type;1371}1372return cmp2(this.key, this.value, other.key, other.value);1373}13741375public equals(other: ContextKeyExpression): boolean {1376if (other.type === this.type) {1377return (this.key === other.key && this.value === other.value);1378}1379return false;1380}13811382public substituteConstants(): ContextKeyExpression | undefined {1383return this;1384}13851386public evaluate(context: IContext): boolean {1387if (typeof this.value === 'string') {1388return false;1389}1390return (parseFloat(<any>context.getValue(this.key)) <= this.value);1391}13921393public serialize(): string {1394return `${this.key} <= ${this.value}`;1395}13961397public keys(): string[] {1398return [this.key];1399}14001401public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1402return mapFnc.mapSmallerEquals(this.key, this.value);1403}14041405public negate(): ContextKeyExpression {1406if (!this.negated) {1407this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this);1408}1409return this.negated;1410}1411}14121413export class ContextKeyRegexExpr implements IContextKeyExpression {14141415public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr {1416return new ContextKeyRegexExpr(key, regexp);1417}14181419public readonly type = ContextKeyExprType.Regex;1420private negated: ContextKeyExpression | null = null;14211422private constructor(1423private readonly key: string,1424private readonly regexp: RegExp | null1425) {1426//1427}14281429public cmp(other: ContextKeyExpression): number {1430if (other.type !== this.type) {1431return this.type - other.type;1432}1433if (this.key < other.key) {1434return -1;1435}1436if (this.key > other.key) {1437return 1;1438}1439const thisSource = this.regexp ? this.regexp.source : '';1440const otherSource = other.regexp ? other.regexp.source : '';1441if (thisSource < otherSource) {1442return -1;1443}1444if (thisSource > otherSource) {1445return 1;1446}1447return 0;1448}14491450public equals(other: ContextKeyExpression): boolean {1451if (other.type === this.type) {1452const thisSource = this.regexp ? this.regexp.source : '';1453const otherSource = other.regexp ? other.regexp.source : '';1454return (this.key === other.key && thisSource === otherSource);1455}1456return false;1457}14581459public substituteConstants(): ContextKeyExpression | undefined {1460return this;1461}14621463public evaluate(context: IContext): boolean {1464const value = context.getValue<any>(this.key);1465return this.regexp ? this.regexp.test(value) : false;1466}14671468public serialize(): string {1469const value = this.regexp1470? `/${this.regexp.source}/${this.regexp.flags}`1471: '/invalid/';1472return `${this.key} =~ ${value}`;1473}14741475public keys(): string[] {1476return [this.key];1477}14781479public map(mapFnc: IContextKeyExprMapper): ContextKeyRegexExpr {1480return mapFnc.mapRegex(this.key, this.regexp);1481}14821483public negate(): ContextKeyExpression {1484if (!this.negated) {1485this.negated = ContextKeyNotRegexExpr.create(this);1486}1487return this.negated;1488}1489}14901491export class ContextKeyNotRegexExpr implements IContextKeyExpression {14921493public static create(actual: ContextKeyRegexExpr): ContextKeyExpression {1494return new ContextKeyNotRegexExpr(actual);1495}14961497public readonly type = ContextKeyExprType.NotRegex;14981499private constructor(private readonly _actual: ContextKeyRegexExpr) {1500//1501}15021503public cmp(other: ContextKeyExpression): number {1504if (other.type !== this.type) {1505return this.type - other.type;1506}1507return this._actual.cmp(other._actual);1508}15091510public equals(other: ContextKeyExpression): boolean {1511if (other.type === this.type) {1512return this._actual.equals(other._actual);1513}1514return false;1515}15161517public substituteConstants(): ContextKeyExpression | undefined {1518return this;1519}15201521public evaluate(context: IContext): boolean {1522return !this._actual.evaluate(context);1523}15241525public serialize(): string {1526return `!(${this._actual.serialize()})`;1527}15281529public keys(): string[] {1530return this._actual.keys();1531}15321533public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1534return new ContextKeyNotRegexExpr(this._actual.map(mapFnc));1535}15361537public negate(): ContextKeyExpression {1538return this._actual;1539}1540}15411542/**1543* @returns the same instance if nothing changed.1544*/1545function eliminateConstantsInArray(arr: ContextKeyExpression[]): (ContextKeyExpression | undefined)[] {1546// Allocate array only if there is a difference1547let newArr: (ContextKeyExpression | undefined)[] | null = null;1548for (let i = 0, len = arr.length; i < len; i++) {1549const newExpr = arr[i].substituteConstants();15501551if (arr[i] !== newExpr) {1552// something has changed!15531554// allocate array on first difference1555if (newArr === null) {1556newArr = [];1557for (let j = 0; j < i; j++) {1558newArr[j] = arr[j];1559}1560}1561}15621563if (newArr !== null) {1564newArr[i] = newExpr;1565}1566}15671568if (newArr === null) {1569return arr;1570}1571return newArr;1572}15731574export class ContextKeyAndExpr implements IContextKeyExpression {15751576public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {1577return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck);1578}15791580public readonly type = ContextKeyExprType.And;15811582private constructor(1583public readonly expr: ContextKeyExpression[],1584private negated: ContextKeyExpression | null1585) {1586}15871588public cmp(other: ContextKeyExpression): number {1589if (other.type !== this.type) {1590return this.type - other.type;1591}1592if (this.expr.length < other.expr.length) {1593return -1;1594}1595if (this.expr.length > other.expr.length) {1596return 1;1597}1598for (let i = 0, len = this.expr.length; i < len; i++) {1599const r = cmp(this.expr[i], other.expr[i]);1600if (r !== 0) {1601return r;1602}1603}1604return 0;1605}16061607public equals(other: ContextKeyExpression): boolean {1608if (other.type === this.type) {1609if (this.expr.length !== other.expr.length) {1610return false;1611}1612for (let i = 0, len = this.expr.length; i < len; i++) {1613if (!this.expr[i].equals(other.expr[i])) {1614return false;1615}1616}1617return true;1618}1619return false;1620}16211622public substituteConstants(): ContextKeyExpression | undefined {1623const exprArr = eliminateConstantsInArray(this.expr);1624if (exprArr === this.expr) {1625// no change1626return this;1627}1628return ContextKeyAndExpr.create(exprArr, this.negated, false);1629}16301631public evaluate(context: IContext): boolean {1632for (let i = 0, len = this.expr.length; i < len; i++) {1633if (!this.expr[i].evaluate(context)) {1634return false;1635}1636}1637return true;1638}16391640private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {1641const expr: ContextKeyExpression[] = [];1642let hasTrue = false;16431644for (const e of arr) {1645if (!e) {1646continue;1647}16481649if (e.type === ContextKeyExprType.True) {1650// anything && true ==> anything1651hasTrue = true;1652continue;1653}16541655if (e.type === ContextKeyExprType.False) {1656// anything && false ==> false1657return ContextKeyFalseExpr.INSTANCE;1658}16591660if (e.type === ContextKeyExprType.And) {1661expr.push(...e.expr);1662continue;1663}16641665expr.push(e);1666}16671668if (expr.length === 0 && hasTrue) {1669return ContextKeyTrueExpr.INSTANCE;1670}16711672if (expr.length === 0) {1673return undefined;1674}16751676if (expr.length === 1) {1677return expr[0];1678}16791680expr.sort(cmp);16811682// eliminate duplicate terms1683for (let i = 1; i < expr.length; i++) {1684if (expr[i - 1].equals(expr[i])) {1685expr.splice(i, 1);1686i--;1687}1688}16891690if (expr.length === 1) {1691return expr[0];1692}16931694// We must distribute any OR expression because we don't support parens1695// OR extensions will be at the end (due to sorting rules)1696while (expr.length > 1) {1697const lastElement = expr[expr.length - 1];1698if (lastElement.type !== ContextKeyExprType.Or) {1699break;1700}1701// pop the last element1702expr.pop();17031704// pop the second to last element1705const secondToLastElement = expr.pop()!;17061707const isFinished = (expr.length === 0);17081709// distribute `lastElement` over `secondToLastElement`1710const resultElement = ContextKeyOrExpr.create(1711lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null, extraRedundantCheck)),1712null,1713isFinished1714);17151716if (resultElement) {1717expr.push(resultElement);1718expr.sort(cmp);1719}1720}17211722if (expr.length === 1) {1723return expr[0];1724}17251726// resolve false AND expressions1727if (extraRedundantCheck) {1728for (let i = 0; i < expr.length; i++) {1729for (let j = i + 1; j < expr.length; j++) {1730if (expr[i].negate().equals(expr[j])) {1731// A && !A case1732return ContextKeyFalseExpr.INSTANCE;1733}1734}1735}17361737if (expr.length === 1) {1738return expr[0];1739}1740}17411742return new ContextKeyAndExpr(expr, negated);1743}17441745public serialize(): string {1746return this.expr.map(e => e.serialize()).join(' && ');1747}17481749public keys(): string[] {1750const result: string[] = [];1751for (const expr of this.expr) {1752result.push(...expr.keys());1753}1754return result;1755}17561757public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1758return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc)), null);1759}17601761public negate(): ContextKeyExpression {1762if (!this.negated) {1763const result: ContextKeyExpression[] = [];1764for (const expr of this.expr) {1765result.push(expr.negate());1766}1767this.negated = ContextKeyOrExpr.create(result, this, true)!;1768}1769return this.negated;1770}1771}17721773export class ContextKeyOrExpr implements IContextKeyExpression {17741775public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {1776return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck);1777}17781779public readonly type = ContextKeyExprType.Or;17801781private constructor(1782public readonly expr: ContextKeyExpression[],1783private negated: ContextKeyExpression | null1784) {1785}17861787public cmp(other: ContextKeyExpression): number {1788if (other.type !== this.type) {1789return this.type - other.type;1790}1791if (this.expr.length < other.expr.length) {1792return -1;1793}1794if (this.expr.length > other.expr.length) {1795return 1;1796}1797for (let i = 0, len = this.expr.length; i < len; i++) {1798const r = cmp(this.expr[i], other.expr[i]);1799if (r !== 0) {1800return r;1801}1802}1803return 0;1804}18051806public equals(other: ContextKeyExpression): boolean {1807if (other.type === this.type) {1808if (this.expr.length !== other.expr.length) {1809return false;1810}1811for (let i = 0, len = this.expr.length; i < len; i++) {1812if (!this.expr[i].equals(other.expr[i])) {1813return false;1814}1815}1816return true;1817}1818return false;1819}18201821public substituteConstants(): ContextKeyExpression | undefined {1822const exprArr = eliminateConstantsInArray(this.expr);1823if (exprArr === this.expr) {1824// no change1825return this;1826}1827return ContextKeyOrExpr.create(exprArr, this.negated, false);1828}18291830public evaluate(context: IContext): boolean {1831for (let i = 0, len = this.expr.length; i < len; i++) {1832if (this.expr[i].evaluate(context)) {1833return true;1834}1835}1836return false;1837}18381839private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {1840let expr: ContextKeyExpression[] = [];1841let hasFalse = false;18421843if (arr) {1844for (let i = 0, len = arr.length; i < len; i++) {1845const e = arr[i];1846if (!e) {1847continue;1848}18491850if (e.type === ContextKeyExprType.False) {1851// anything || false ==> anything1852hasFalse = true;1853continue;1854}18551856if (e.type === ContextKeyExprType.True) {1857// anything || true ==> true1858return ContextKeyTrueExpr.INSTANCE;1859}18601861if (e.type === ContextKeyExprType.Or) {1862expr = expr.concat(e.expr);1863continue;1864}18651866expr.push(e);1867}18681869if (expr.length === 0 && hasFalse) {1870return ContextKeyFalseExpr.INSTANCE;1871}18721873expr.sort(cmp);1874}18751876if (expr.length === 0) {1877return undefined;1878}18791880if (expr.length === 1) {1881return expr[0];1882}18831884// eliminate duplicate terms1885for (let i = 1; i < expr.length; i++) {1886if (expr[i - 1].equals(expr[i])) {1887expr.splice(i, 1);1888i--;1889}1890}18911892if (expr.length === 1) {1893return expr[0];1894}18951896// resolve true OR expressions1897if (extraRedundantCheck) {1898for (let i = 0; i < expr.length; i++) {1899for (let j = i + 1; j < expr.length; j++) {1900if (expr[i].negate().equals(expr[j])) {1901// A || !A case1902return ContextKeyTrueExpr.INSTANCE;1903}1904}1905}19061907if (expr.length === 1) {1908return expr[0];1909}1910}19111912return new ContextKeyOrExpr(expr, negated);1913}19141915public serialize(): string {1916return this.expr.map(e => e.serialize()).join(' || ');1917}19181919public keys(): string[] {1920const result: string[] = [];1921for (const expr of this.expr) {1922result.push(...expr.keys());1923}1924return result;1925}19261927public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {1928return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc)), null);1929}19301931public negate(): ContextKeyExpression {1932if (!this.negated) {1933const result: ContextKeyExpression[] = [];1934for (const expr of this.expr) {1935result.push(expr.negate());1936}19371938// We don't support parens, so here we distribute the AND over the OR terminals1939// We always take the first 2 AND pairs and distribute them1940while (result.length > 1) {1941const LEFT = result.shift()!;1942const RIGHT = result.shift()!;19431944const all: ContextKeyExpression[] = [];1945for (const left of getTerminals(LEFT)) {1946for (const right of getTerminals(RIGHT)) {1947all.push(ContextKeyAndExpr.create([left, right], null, false)!);1948}1949}19501951result.unshift(ContextKeyOrExpr.create(all, null, false)!);1952}19531954this.negated = ContextKeyOrExpr.create(result, this, true)!;1955}1956return this.negated;1957}1958}19591960export interface ContextKeyInfo {1961readonly key: string;1962readonly type?: string;1963readonly description?: string;1964}19651966export class RawContextKey<T extends ContextKeyValue> extends ContextKeyDefinedExpr {19671968private static _info: ContextKeyInfo[] = [];19691970static all(): IterableIterator<ContextKeyInfo> {1971return RawContextKey._info.values();1972}19731974private readonly _defaultValue: T | undefined;19751976constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string; description: string }) {1977super(key, null);1978this._defaultValue = defaultValue;19791980// collect all context keys into a central place1981if (typeof metaOrHide === 'object') {1982RawContextKey._info.push({ ...metaOrHide, key });1983} else if (metaOrHide !== true) {1984RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined });1985}1986}19871988public bindTo(target: IContextKeyService): IContextKey<T> {1989return target.createKey(this.key, this._defaultValue);1990}19911992public getValue(target: IContextKeyService): T | undefined {1993return target.getContextKeyValue<T>(this.key);1994}19951996public toNegated(): ContextKeyExpression {1997return this.negate();1998}19992000public isEqualTo(value: any): ContextKeyExpression {2001return ContextKeyEqualsExpr.create(this.key, value);2002}20032004public notEqualsTo(value: any): ContextKeyExpression {2005return ContextKeyNotEqualsExpr.create(this.key, value);2006}20072008public greater(value: any): ContextKeyExpression {2009return ContextKeyGreaterExpr.create(this.key, value);2010}2011}20122013export type ContextKeyValue = null | undefined | boolean | number | string2014| Array<null | undefined | boolean | number | string>2015| Record<string, null | undefined | boolean | number | string>;20162017export interface IContext {2018getValue<T extends ContextKeyValue = ContextKeyValue>(key: string): T | undefined;2019}20202021export interface IContextKey<T extends ContextKeyValue = ContextKeyValue> {2022set(value: T): void;2023reset(): void;2024get(): T | undefined;2025}20262027export interface IContextKeyServiceTarget {2028parentElement: IContextKeyServiceTarget | null;2029setAttribute(attr: string, value: string): void;2030removeAttribute(attr: string): void;2031hasAttribute(attr: string): boolean;2032getAttribute(attr: string): string | null;2033}20342035export const IContextKeyService = createDecorator<IContextKeyService>('contextKeyService');20362037export interface IReadableSet<T> {2038has(value: T): boolean;2039}20402041export interface IContextKeyChangeEvent {2042affectsSome(keys: IReadableSet<string>): boolean;2043allKeysContainedIn(keys: IReadableSet<string>): boolean;2044}20452046export type IScopedContextKeyService = IContextKeyService & IDisposable;20472048export interface IContextKeyService {2049readonly _serviceBrand: undefined;20502051onDidChangeContext: Event<IContextKeyChangeEvent>;2052bufferChangeEvents(callback: Function): void;20532054createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T>;2055contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;2056getContextKeyValue<T>(key: string): T | undefined;20572058createScoped(target: IContextKeyServiceTarget): IScopedContextKeyService;2059createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;2060getContext(target: IContextKeyServiceTarget | null): IContext;20612062updateParent(parentContextKeyService: IContextKeyService): void;2063}20642065function cmp1(key1: string, key2: string): number {2066if (key1 < key2) {2067return -1;2068}2069if (key1 > key2) {2070return 1;2071}2072return 0;2073}20742075function cmp2(key1: string, value1: any, key2: string, value2: any): number {2076if (key1 < key2) {2077return -1;2078}2079if (key1 > key2) {2080return 1;2081}2082if (value1 < value2) {2083return -1;2084}2085if (value1 > value2) {2086return 1;2087}2088return 0;2089}20902091/**2092* Returns true if it is provable `p` implies `q`.2093*/2094export function implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean {20952096if (p.type === ContextKeyExprType.False || q.type === ContextKeyExprType.True) {2097// false implies anything2098// anything implies true2099return true;2100}21012102if (p.type === ContextKeyExprType.Or) {2103if (q.type === ContextKeyExprType.Or) {2104// `a || b || c` can only imply something like `a || b || c || d`2105return allElementsIncluded(p.expr, q.expr);2106}2107return false;2108}21092110if (q.type === ContextKeyExprType.Or) {2111for (const element of q.expr) {2112if (implies(p, element)) {2113return true;2114}2115}2116return false;2117}21182119if (p.type === ContextKeyExprType.And) {2120if (q.type === ContextKeyExprType.And) {2121// `a && b && c` implies `a && c`2122return allElementsIncluded(q.expr, p.expr);2123}2124for (const element of p.expr) {2125if (implies(element, q)) {2126return true;2127}2128}2129return false;2130}21312132return p.equals(q);2133}21342135/**2136* Returns true if all elements in `p` are also present in `q`.2137* The two arrays are assumed to be sorted2138*/2139function allElementsIncluded(p: ContextKeyExpression[], q: ContextKeyExpression[]): boolean {2140let pIndex = 0;2141let qIndex = 0;2142while (pIndex < p.length && qIndex < q.length) {2143const cmp = p[pIndex].cmp(q[qIndex]);21442145if (cmp < 0) {2146// an element from `p` is missing from `q`2147return false;2148} else if (cmp === 0) {2149pIndex++;2150qIndex++;2151} else {2152qIndex++;2153}2154}2155return (pIndex === p.length);2156}21572158function getTerminals(node: ContextKeyExpression) {2159if (node.type === ContextKeyExprType.Or) {2160return node.expr;2161}2162return [node];2163}216421652166