Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/contextkey/common/contextkey.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CharCode } from '../../../base/common/charCode.js';
7
import { Event } from '../../../base/common/event.js';
8
import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js';
9
import { isFalsyOrWhitespace } from '../../../base/common/strings.js';
10
import { Scanner, LexingError, Token, TokenType } from './scanner.js';
11
import { createDecorator } from '../../instantiation/common/instantiation.js';
12
import { localize } from '../../../nls.js';
13
import { IDisposable } from '../../../base/common/lifecycle.js';
14
import { illegalArgument } from '../../../base/common/errors.js';
15
16
const CONSTANT_VALUES = new Map<string, boolean>();
17
CONSTANT_VALUES.set('false', false);
18
CONSTANT_VALUES.set('true', true);
19
CONSTANT_VALUES.set('isMac', isMacintosh);
20
CONSTANT_VALUES.set('isLinux', isLinux);
21
CONSTANT_VALUES.set('isWindows', isWindows);
22
CONSTANT_VALUES.set('isWeb', isWeb);
23
CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);
24
CONSTANT_VALUES.set('isEdge', isEdge);
25
CONSTANT_VALUES.set('isFirefox', isFirefox);
26
CONSTANT_VALUES.set('isChrome', isChrome);
27
CONSTANT_VALUES.set('isSafari', isSafari);
28
29
/** 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 */
30
export function setConstant(key: string, value: boolean) {
31
if (CONSTANT_VALUES.get(key) !== undefined) { throw illegalArgument('contextkey.setConstant(k, v) invoked with already set constant `k`'); }
32
33
CONSTANT_VALUES.set(key, value);
34
}
35
36
const hasOwnProperty = Object.prototype.hasOwnProperty;
37
38
export const enum ContextKeyExprType {
39
False = 0,
40
True = 1,
41
Defined = 2,
42
Not = 3,
43
Equals = 4,
44
NotEquals = 5,
45
And = 6,
46
Regex = 7,
47
NotRegex = 8,
48
Or = 9,
49
In = 10,
50
NotIn = 11,
51
Greater = 12,
52
GreaterEquals = 13,
53
Smaller = 14,
54
SmallerEquals = 15,
55
}
56
57
export interface IContextKeyExprMapper {
58
mapDefined(key: string): ContextKeyExpression;
59
mapNot(key: string): ContextKeyExpression;
60
mapEquals(key: string, value: any): ContextKeyExpression;
61
mapNotEquals(key: string, value: any): ContextKeyExpression;
62
mapGreater(key: string, value: any): ContextKeyExpression;
63
mapGreaterEquals(key: string, value: any): ContextKeyExpression;
64
mapSmaller(key: string, value: any): ContextKeyExpression;
65
mapSmallerEquals(key: string, value: any): ContextKeyExpression;
66
mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr;
67
mapIn(key: string, valueKey: string): ContextKeyInExpr;
68
mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr;
69
}
70
71
export interface IContextKeyExpression {
72
cmp(other: ContextKeyExpression): number;
73
equals(other: ContextKeyExpression): boolean;
74
substituteConstants(): ContextKeyExpression | undefined;
75
evaluate(context: IContext): boolean;
76
serialize(): string;
77
keys(): string[];
78
map(mapFnc: IContextKeyExprMapper): ContextKeyExpression;
79
negate(): ContextKeyExpression;
80
81
}
82
83
export type ContextKeyExpression = (
84
ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr
85
| ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr
86
| ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr
87
| ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr
88
| ContextKeySmallerExpr | ContextKeySmallerEqualsExpr
89
);
90
91
92
/*
93
94
Syntax grammar:
95
96
```ebnf
97
98
expression ::= or
99
100
or ::= and { '||' and }*
101
102
and ::= term { '&&' term }*
103
104
term ::=
105
| '!' (KEY | true | false | parenthesized)
106
| primary
107
108
primary ::=
109
| 'true'
110
| 'false'
111
| parenthesized
112
| KEY '=~' REGEX
113
| KEY [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'not' 'in' | 'in') value ]
114
115
parenthesized ::=
116
| '(' expression ')'
117
118
value ::=
119
| 'true'
120
| 'false'
121
| 'in' // we support `in` as a value because there's an extension that uses it, ie "when": "languageId == in"
122
| VALUE // matched by the same regex as KEY; consider putting the value in single quotes if it's a string (e.g., with spaces)
123
| SINGLE_QUOTED_STR
124
| EMPTY_STR // this allows "when": "foo == " which's used by existing extensions
125
126
```
127
*/
128
129
export type ParserConfig = {
130
/**
131
* with this option enabled, the parser can recover from regex parsing errors, e.g., unescaped slashes: `/src//` is accepted as `/src\//` would be
132
*/
133
regexParsingWithErrorRecovery: boolean;
134
};
135
136
const defaultConfig: ParserConfig = {
137
regexParsingWithErrorRecovery: true
138
};
139
140
export type ParsingError = {
141
message: string;
142
offset: number;
143
lexeme: string;
144
additionalInfo?: string;
145
};
146
147
const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression");
148
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.");
149
const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'.");
150
const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'");
151
const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token");
152
const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?");
153
const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression");
154
const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?");
155
156
/**
157
* A parser for context key expressions.
158
*
159
* Example:
160
* ```ts
161
* const parser = new Parser();
162
* const expr = parser.parse('foo == "bar" && baz == true');
163
*
164
* if (expr === undefined) {
165
* // there were lexing or parsing errors
166
* // process lexing errors with `parser.lexingErrors`
167
* // process parsing errors with `parser.parsingErrors`
168
* } else {
169
* // expr is a valid expression
170
* }
171
* ```
172
*/
173
export class Parser {
174
// Note: this doesn't produce an exact syntax tree but a normalized one
175
// ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize
176
177
private static _parseError = new Error();
178
179
// lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse`
180
private readonly _scanner = new Scanner();
181
182
// lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse`
183
private _tokens: Token[] = [];
184
private _current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd`
185
private _parsingErrors: ParsingError[] = [];
186
187
get lexingErrors(): Readonly<LexingError[]> {
188
return this._scanner.errors;
189
}
190
191
get parsingErrors(): Readonly<ParsingError[]> {
192
return this._parsingErrors;
193
}
194
195
constructor(private readonly _config: ParserConfig = defaultConfig) {
196
}
197
198
/**
199
* Parse a context key expression.
200
*
201
* @param input the expression to parse
202
* @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors
203
*/
204
parse(input: string): ContextKeyExpression | undefined {
205
206
if (input === '') {
207
this._parsingErrors.push({ message: errorEmptyString, offset: 0, lexeme: '', additionalInfo: hintEmptyString });
208
return undefined;
209
}
210
211
this._tokens = this._scanner.reset(input).scan();
212
// @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
213
214
this._current = 0;
215
this._parsingErrors = [];
216
217
try {
218
const expr = this._expr();
219
if (!this._isAtEnd()) {
220
const peek = this._peek();
221
const additionalInfo = peek.type === TokenType.Str ? hintUnexpectedToken : undefined;
222
this._parsingErrors.push({ message: errorUnexpectedToken, offset: peek.offset, lexeme: Scanner.getLexeme(peek), additionalInfo });
223
throw Parser._parseError;
224
}
225
return expr;
226
} catch (e) {
227
if (!(e === Parser._parseError)) {
228
throw e;
229
}
230
return undefined;
231
}
232
}
233
234
private _expr(): ContextKeyExpression | undefined {
235
return this._or();
236
}
237
238
private _or(): ContextKeyExpression | undefined {
239
const expr = [this._and()];
240
241
while (this._matchOne(TokenType.Or)) {
242
const right = this._and();
243
expr.push(right);
244
}
245
246
return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr);
247
}
248
249
private _and(): ContextKeyExpression | undefined {
250
const expr = [this._term()];
251
252
while (this._matchOne(TokenType.And)) {
253
const right = this._term();
254
expr.push(right);
255
}
256
257
return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr);
258
}
259
260
private _term(): ContextKeyExpression | undefined {
261
if (this._matchOne(TokenType.Neg)) {
262
const peek = this._peek();
263
switch (peek.type) {
264
case TokenType.True:
265
this._advance();
266
return ContextKeyFalseExpr.INSTANCE;
267
case TokenType.False:
268
this._advance();
269
return ContextKeyTrueExpr.INSTANCE;
270
case TokenType.LParen: {
271
this._advance();
272
const expr = this._expr();
273
this._consume(TokenType.RParen, errorClosingParenthesis);
274
return expr?.negate();
275
}
276
case TokenType.Str:
277
this._advance();
278
return ContextKeyNotExpr.create(peek.lexeme);
279
default:
280
throw this._errExpectedButGot(`KEY | true | false | '(' expression ')'`, peek);
281
}
282
}
283
return this._primary();
284
}
285
286
private _primary(): ContextKeyExpression | undefined {
287
288
const peek = this._peek();
289
switch (peek.type) {
290
case TokenType.True:
291
this._advance();
292
return ContextKeyExpr.true();
293
294
case TokenType.False:
295
this._advance();
296
return ContextKeyExpr.false();
297
298
case TokenType.LParen: {
299
this._advance();
300
const expr = this._expr();
301
this._consume(TokenType.RParen, errorClosingParenthesis);
302
return expr;
303
}
304
305
case TokenType.Str: {
306
// KEY
307
const key = peek.lexeme;
308
this._advance();
309
310
// =~ regex
311
if (this._matchOne(TokenType.RegexOp)) {
312
313
// @ulugbekna: we need to reconstruct the regex from the tokens because some extensions use unescaped slashes in regexes
314
const expr = this._peek();
315
316
if (!this._config.regexParsingWithErrorRecovery) {
317
this._advance();
318
if (expr.type !== TokenType.RegexStr) {
319
throw this._errExpectedButGot(`REGEX`, expr);
320
}
321
const regexLexeme = expr.lexeme;
322
const closingSlashIndex = regexLexeme.lastIndexOf('/');
323
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
324
let regexp: RegExp | null;
325
try {
326
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
327
} catch (e) {
328
throw this._errExpectedButGot(`REGEX`, expr);
329
}
330
return ContextKeyRegexExpr.create(key, regexp);
331
}
332
333
switch (expr.type) {
334
case TokenType.RegexStr:
335
case TokenType.Error: { // also handle an ErrorToken in case of smth such as /(/file)/
336
const lexemeReconstruction = [expr.lexeme]; // /REGEX/ or /REGEX/FLAGS
337
this._advance();
338
339
let followingToken = this._peek();
340
let parenBalance = 0;
341
for (let i = 0; i < expr.lexeme.length; i++) {
342
if (expr.lexeme.charCodeAt(i) === CharCode.OpenParen) {
343
parenBalance++;
344
} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {
345
parenBalance--;
346
}
347
}
348
349
while (!this._isAtEnd() && followingToken.type !== TokenType.And && followingToken.type !== TokenType.Or) {
350
switch (followingToken.type) {
351
case TokenType.LParen:
352
parenBalance++;
353
break;
354
case TokenType.RParen:
355
parenBalance--;
356
break;
357
case TokenType.RegexStr:
358
case TokenType.QuotedStr:
359
for (let i = 0; i < followingToken.lexeme.length; i++) {
360
if (followingToken.lexeme.charCodeAt(i) === CharCode.OpenParen) {
361
parenBalance++;
362
} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {
363
parenBalance--;
364
}
365
}
366
}
367
if (parenBalance < 0) {
368
break;
369
}
370
lexemeReconstruction.push(Scanner.getLexeme(followingToken));
371
this._advance();
372
followingToken = this._peek();
373
}
374
375
const regexLexeme = lexemeReconstruction.join('');
376
const closingSlashIndex = regexLexeme.lastIndexOf('/');
377
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
378
let regexp: RegExp | null;
379
try {
380
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
381
} catch (e) {
382
throw this._errExpectedButGot(`REGEX`, expr);
383
}
384
return ContextKeyExpr.regex(key, regexp);
385
}
386
387
case TokenType.QuotedStr: {
388
const serializedValue = expr.lexeme;
389
this._advance();
390
// replicate old regex parsing behavior
391
392
let regex: RegExp | null = null;
393
394
if (!isFalsyOrWhitespace(serializedValue)) {
395
const start = serializedValue.indexOf('/');
396
const end = serializedValue.lastIndexOf('/');
397
if (start !== end && start >= 0) {
398
399
const value = serializedValue.slice(start + 1, end);
400
const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : '';
401
try {
402
regex = new RegExp(value, caseIgnoreFlag);
403
} catch (_e) {
404
throw this._errExpectedButGot(`REGEX`, expr);
405
}
406
}
407
}
408
409
if (regex === null) {
410
throw this._errExpectedButGot('REGEX', expr);
411
}
412
413
return ContextKeyRegexExpr.create(key, regex);
414
}
415
416
default:
417
throw this._errExpectedButGot('REGEX', this._peek());
418
}
419
}
420
421
// [ 'not' 'in' value ]
422
if (this._matchOne(TokenType.Not)) {
423
this._consume(TokenType.In, errorNoInAfterNot);
424
const right = this._value();
425
return ContextKeyExpr.notIn(key, right);
426
}
427
428
// [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in') value ]
429
const maybeOp = this._peek().type;
430
switch (maybeOp) {
431
case TokenType.Eq: {
432
this._advance();
433
434
const right = this._value();
435
if (this._previous().type === TokenType.QuotedStr) { // to preserve old parser behavior: "foo == 'true'" is preserved as "foo == 'true'", but "foo == true" is optimized as "foo"
436
return ContextKeyExpr.equals(key, right);
437
}
438
switch (right) {
439
case 'true':
440
return ContextKeyExpr.has(key);
441
case 'false':
442
return ContextKeyExpr.not(key);
443
default:
444
return ContextKeyExpr.equals(key, right);
445
}
446
}
447
448
case TokenType.NotEq: {
449
this._advance();
450
451
const right = this._value();
452
if (this._previous().type === TokenType.QuotedStr) { // same as above with "foo != 'true'"
453
return ContextKeyExpr.notEquals(key, right);
454
}
455
switch (right) {
456
case 'true':
457
return ContextKeyExpr.not(key);
458
case 'false':
459
return ContextKeyExpr.has(key);
460
default:
461
return ContextKeyExpr.notEquals(key, right);
462
}
463
}
464
// 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
465
// consequently, package.json linter should _warn_ the user if they're passing undesired things to ops
466
case TokenType.Lt:
467
this._advance();
468
return ContextKeySmallerExpr.create(key, this._value());
469
470
case TokenType.LtEq:
471
this._advance();
472
return ContextKeySmallerEqualsExpr.create(key, this._value());
473
474
case TokenType.Gt:
475
this._advance();
476
return ContextKeyGreaterExpr.create(key, this._value());
477
478
case TokenType.GtEq:
479
this._advance();
480
return ContextKeyGreaterEqualsExpr.create(key, this._value());
481
482
case TokenType.In:
483
this._advance();
484
return ContextKeyExpr.in(key, this._value());
485
486
default:
487
return ContextKeyExpr.has(key);
488
}
489
}
490
491
case TokenType.EOF:
492
this._parsingErrors.push({ message: errorUnexpectedEOF, offset: peek.offset, lexeme: '', additionalInfo: hintUnexpectedEOF });
493
throw Parser._parseError;
494
495
default:
496
throw this._errExpectedButGot(`true | false | KEY \n\t| KEY '=~' REGEX \n\t| KEY ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in') value`, this._peek());
497
498
}
499
}
500
501
private _value(): string {
502
const token = this._peek();
503
switch (token.type) {
504
case TokenType.Str:
505
case TokenType.QuotedStr:
506
this._advance();
507
return token.lexeme;
508
case TokenType.True:
509
this._advance();
510
return 'true';
511
case TokenType.False:
512
this._advance();
513
return 'false';
514
case TokenType.In: // we support `in` as a value, e.g., "when": "languageId == in" - exists in existing extensions
515
this._advance();
516
return 'in';
517
default:
518
// this allows "when": "foo == " which's used by existing extensions
519
// we do not call `_advance` on purpose - we don't want to eat unintended tokens
520
return '';
521
}
522
}
523
524
private _flagsGYRe = /g|y/g;
525
private _removeFlagsGY(flags: string): string {
526
return flags.replaceAll(this._flagsGYRe, '');
527
}
528
529
// careful: this can throw if current token is the initial one (ie index = 0)
530
private _previous() {
531
return this._tokens[this._current - 1];
532
}
533
534
private _matchOne(token: TokenType) {
535
if (this._check(token)) {
536
this._advance();
537
return true;
538
}
539
540
return false;
541
}
542
543
private _advance() {
544
if (!this._isAtEnd()) {
545
this._current++;
546
}
547
return this._previous();
548
}
549
550
private _consume(type: TokenType, message: string) {
551
if (this._check(type)) {
552
return this._advance();
553
}
554
555
throw this._errExpectedButGot(message, this._peek());
556
}
557
558
private _errExpectedButGot(expected: string, got: Token, additionalInfo?: string) {
559
const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got));
560
const offset = got.offset;
561
const lexeme = Scanner.getLexeme(got);
562
this._parsingErrors.push({ message, offset, lexeme, additionalInfo });
563
return Parser._parseError;
564
}
565
566
private _check(type: TokenType) {
567
return this._peek().type === type;
568
}
569
570
private _peek() {
571
return this._tokens[this._current];
572
}
573
574
private _isAtEnd() {
575
return this._peek().type === TokenType.EOF;
576
}
577
}
578
579
export abstract class ContextKeyExpr {
580
581
public static false(): ContextKeyExpression {
582
return ContextKeyFalseExpr.INSTANCE;
583
}
584
public static true(): ContextKeyExpression {
585
return ContextKeyTrueExpr.INSTANCE;
586
}
587
public static has(key: string): ContextKeyExpression {
588
return ContextKeyDefinedExpr.create(key);
589
}
590
public static equals(key: string, value: any): ContextKeyExpression {
591
return ContextKeyEqualsExpr.create(key, value);
592
}
593
public static notEquals(key: string, value: any): ContextKeyExpression {
594
return ContextKeyNotEqualsExpr.create(key, value);
595
}
596
public static regex(key: string, value: RegExp): ContextKeyExpression {
597
return ContextKeyRegexExpr.create(key, value);
598
}
599
public static in(key: string, value: string): ContextKeyExpression {
600
return ContextKeyInExpr.create(key, value);
601
}
602
public static notIn(key: string, value: string): ContextKeyExpression {
603
return ContextKeyNotInExpr.create(key, value);
604
}
605
public static not(key: string): ContextKeyExpression {
606
return ContextKeyNotExpr.create(key);
607
}
608
public static and(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {
609
return ContextKeyAndExpr.create(expr, null, true);
610
}
611
public static or(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {
612
return ContextKeyOrExpr.create(expr, null, true);
613
}
614
public static greater(key: string, value: number): ContextKeyExpression {
615
return ContextKeyGreaterExpr.create(key, value);
616
}
617
public static greaterEquals(key: string, value: number): ContextKeyExpression {
618
return ContextKeyGreaterEqualsExpr.create(key, value);
619
}
620
public static smaller(key: string, value: number): ContextKeyExpression {
621
return ContextKeySmallerExpr.create(key, value);
622
}
623
public static smallerEquals(key: string, value: number): ContextKeyExpression {
624
return ContextKeySmallerEqualsExpr.create(key, value);
625
}
626
627
private static _parser = new Parser({ regexParsingWithErrorRecovery: false });
628
public static deserialize(serialized: string | null | undefined): ContextKeyExpression | undefined {
629
if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported
630
return undefined;
631
}
632
633
const expr = this._parser.parse(serialized);
634
return expr;
635
}
636
637
}
638
639
640
export function validateWhenClauses(whenClauses: string[]): any {
641
642
const parser = new Parser({ regexParsingWithErrorRecovery: false }); // we run with no recovery to guide users to use correct regexes
643
644
return whenClauses.map(whenClause => {
645
parser.parse(whenClause);
646
647
if (parser.lexingErrors.length > 0) {
648
return parser.lexingErrors.map((se: LexingError) => ({
649
errorMessage: se.additionalInfo ?
650
localize('contextkey.scanner.errorForLinterWithHint', "Unexpected token. Hint: {0}", se.additionalInfo) :
651
localize('contextkey.scanner.errorForLinter', "Unexpected token."),
652
offset: se.offset,
653
length: se.lexeme.length,
654
}));
655
} else if (parser.parsingErrors.length > 0) {
656
return parser.parsingErrors.map((pe: ParsingError) => ({
657
errorMessage: pe.additionalInfo ? `${pe.message}. ${pe.additionalInfo}` : pe.message,
658
offset: pe.offset,
659
length: pe.lexeme.length,
660
}));
661
} else {
662
return [];
663
}
664
});
665
}
666
667
export function expressionsAreEqualWithConstantSubstitution(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
668
const aExpr = a ? a.substituteConstants() : undefined;
669
const bExpr = b ? b.substituteConstants() : undefined;
670
if (!aExpr && !bExpr) {
671
return true;
672
}
673
if (!aExpr || !bExpr) {
674
return false;
675
}
676
return aExpr.equals(bExpr);
677
}
678
679
function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number {
680
return a.cmp(b);
681
}
682
683
export class ContextKeyFalseExpr implements IContextKeyExpression {
684
public static INSTANCE = new ContextKeyFalseExpr();
685
686
public readonly type = ContextKeyExprType.False;
687
688
protected constructor() {
689
}
690
691
public cmp(other: ContextKeyExpression): number {
692
return this.type - other.type;
693
}
694
695
public equals(other: ContextKeyExpression): boolean {
696
return (other.type === this.type);
697
}
698
699
public substituteConstants(): ContextKeyExpression | undefined {
700
return this;
701
}
702
703
public evaluate(context: IContext): boolean {
704
return false;
705
}
706
707
public serialize(): string {
708
return 'false';
709
}
710
711
public keys(): string[] {
712
return [];
713
}
714
715
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
716
return this;
717
}
718
719
public negate(): ContextKeyExpression {
720
return ContextKeyTrueExpr.INSTANCE;
721
}
722
}
723
724
export class ContextKeyTrueExpr implements IContextKeyExpression {
725
public static INSTANCE = new ContextKeyTrueExpr();
726
727
public readonly type = ContextKeyExprType.True;
728
729
protected constructor() {
730
}
731
732
public cmp(other: ContextKeyExpression): number {
733
return this.type - other.type;
734
}
735
736
public equals(other: ContextKeyExpression): boolean {
737
return (other.type === this.type);
738
}
739
740
public substituteConstants(): ContextKeyExpression | undefined {
741
return this;
742
}
743
744
public evaluate(context: IContext): boolean {
745
return true;
746
}
747
748
public serialize(): string {
749
return 'true';
750
}
751
752
public keys(): string[] {
753
return [];
754
}
755
756
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
757
return this;
758
}
759
760
public negate(): ContextKeyExpression {
761
return ContextKeyFalseExpr.INSTANCE;
762
}
763
}
764
765
export class ContextKeyDefinedExpr implements IContextKeyExpression {
766
public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
767
const constantValue = CONSTANT_VALUES.get(key);
768
if (typeof constantValue === 'boolean') {
769
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
770
}
771
return new ContextKeyDefinedExpr(key, negated);
772
}
773
774
public readonly type = ContextKeyExprType.Defined;
775
776
protected constructor(
777
readonly key: string,
778
private negated: ContextKeyExpression | null
779
) {
780
}
781
782
public cmp(other: ContextKeyExpression): number {
783
if (other.type !== this.type) {
784
return this.type - other.type;
785
}
786
return cmp1(this.key, other.key);
787
}
788
789
public equals(other: ContextKeyExpression): boolean {
790
if (other.type === this.type) {
791
return (this.key === other.key);
792
}
793
return false;
794
}
795
796
public substituteConstants(): ContextKeyExpression | undefined {
797
const constantValue = CONSTANT_VALUES.get(this.key);
798
if (typeof constantValue === 'boolean') {
799
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
800
}
801
return this;
802
}
803
804
public evaluate(context: IContext): boolean {
805
return (!!context.getValue(this.key));
806
}
807
808
public serialize(): string {
809
return this.key;
810
}
811
812
public keys(): string[] {
813
return [this.key];
814
}
815
816
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
817
return mapFnc.mapDefined(this.key);
818
}
819
820
public negate(): ContextKeyExpression {
821
if (!this.negated) {
822
this.negated = ContextKeyNotExpr.create(this.key, this);
823
}
824
return this.negated;
825
}
826
}
827
828
export class ContextKeyEqualsExpr implements IContextKeyExpression {
829
830
public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
831
if (typeof value === 'boolean') {
832
return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated));
833
}
834
const constantValue = CONSTANT_VALUES.get(key);
835
if (typeof constantValue === 'boolean') {
836
const trueValue = constantValue ? 'true' : 'false';
837
return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
838
}
839
return new ContextKeyEqualsExpr(key, value, negated);
840
}
841
842
public readonly type = ContextKeyExprType.Equals;
843
844
private constructor(
845
private readonly key: string,
846
private readonly value: any,
847
private negated: ContextKeyExpression | null
848
) {
849
}
850
851
public cmp(other: ContextKeyExpression): number {
852
if (other.type !== this.type) {
853
return this.type - other.type;
854
}
855
return cmp2(this.key, this.value, other.key, other.value);
856
}
857
858
public equals(other: ContextKeyExpression): boolean {
859
if (other.type === this.type) {
860
return (this.key === other.key && this.value === other.value);
861
}
862
return false;
863
}
864
865
public substituteConstants(): ContextKeyExpression | undefined {
866
const constantValue = CONSTANT_VALUES.get(this.key);
867
if (typeof constantValue === 'boolean') {
868
const trueValue = constantValue ? 'true' : 'false';
869
return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
870
}
871
return this;
872
}
873
874
public evaluate(context: IContext): boolean {
875
// Intentional ==
876
// eslint-disable-next-line eqeqeq
877
return (context.getValue(this.key) == this.value);
878
}
879
880
public serialize(): string {
881
return `${this.key} == '${this.value}'`;
882
}
883
884
public keys(): string[] {
885
return [this.key];
886
}
887
888
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
889
return mapFnc.mapEquals(this.key, this.value);
890
}
891
892
public negate(): ContextKeyExpression {
893
if (!this.negated) {
894
this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this);
895
}
896
return this.negated;
897
}
898
}
899
900
export class ContextKeyInExpr implements IContextKeyExpression {
901
902
public static create(key: string, valueKey: string): ContextKeyInExpr {
903
return new ContextKeyInExpr(key, valueKey);
904
}
905
906
public readonly type = ContextKeyExprType.In;
907
private negated: ContextKeyExpression | null = null;
908
909
private constructor(
910
private readonly key: string,
911
private readonly valueKey: string,
912
) {
913
}
914
915
public cmp(other: ContextKeyExpression): number {
916
if (other.type !== this.type) {
917
return this.type - other.type;
918
}
919
return cmp2(this.key, this.valueKey, other.key, other.valueKey);
920
}
921
922
public equals(other: ContextKeyExpression): boolean {
923
if (other.type === this.type) {
924
return (this.key === other.key && this.valueKey === other.valueKey);
925
}
926
return false;
927
}
928
929
public substituteConstants(): ContextKeyExpression | undefined {
930
return this;
931
}
932
933
public evaluate(context: IContext): boolean {
934
const source = context.getValue(this.valueKey);
935
936
const item = context.getValue(this.key);
937
938
if (Array.isArray(source)) {
939
return source.includes(item as any);
940
}
941
942
if (typeof item === 'string' && typeof source === 'object' && source !== null) {
943
return hasOwnProperty.call(source, item);
944
}
945
return false;
946
}
947
948
public serialize(): string {
949
return `${this.key} in '${this.valueKey}'`;
950
}
951
952
public keys(): string[] {
953
return [this.key, this.valueKey];
954
}
955
956
public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr {
957
return mapFnc.mapIn(this.key, this.valueKey);
958
}
959
960
public negate(): ContextKeyExpression {
961
if (!this.negated) {
962
this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey);
963
}
964
return this.negated;
965
}
966
}
967
968
export class ContextKeyNotInExpr implements IContextKeyExpression {
969
970
public static create(key: string, valueKey: string): ContextKeyNotInExpr {
971
return new ContextKeyNotInExpr(key, valueKey);
972
}
973
974
public readonly type = ContextKeyExprType.NotIn;
975
976
private readonly _negated: ContextKeyInExpr;
977
978
private constructor(
979
private readonly key: string,
980
private readonly valueKey: string,
981
) {
982
this._negated = ContextKeyInExpr.create(key, valueKey);
983
}
984
985
public cmp(other: ContextKeyExpression): number {
986
if (other.type !== this.type) {
987
return this.type - other.type;
988
}
989
return this._negated.cmp(other._negated);
990
}
991
992
public equals(other: ContextKeyExpression): boolean {
993
if (other.type === this.type) {
994
return this._negated.equals(other._negated);
995
}
996
return false;
997
}
998
999
public substituteConstants(): ContextKeyExpression | undefined {
1000
return this;
1001
}
1002
1003
public evaluate(context: IContext): boolean {
1004
return !this._negated.evaluate(context);
1005
}
1006
1007
public serialize(): string {
1008
return `${this.key} not in '${this.valueKey}'`;
1009
}
1010
1011
public keys(): string[] {
1012
return this._negated.keys();
1013
}
1014
1015
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1016
return mapFnc.mapNotIn(this.key, this.valueKey);
1017
}
1018
1019
public negate(): ContextKeyExpression {
1020
return this._negated;
1021
}
1022
}
1023
1024
export class ContextKeyNotEqualsExpr implements IContextKeyExpression {
1025
1026
public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1027
if (typeof value === 'boolean') {
1028
if (value) {
1029
return ContextKeyNotExpr.create(key, negated);
1030
}
1031
return ContextKeyDefinedExpr.create(key, negated);
1032
}
1033
const constantValue = CONSTANT_VALUES.get(key);
1034
if (typeof constantValue === 'boolean') {
1035
const falseValue = constantValue ? 'true' : 'false';
1036
return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1037
}
1038
return new ContextKeyNotEqualsExpr(key, value, negated);
1039
}
1040
1041
public readonly type = ContextKeyExprType.NotEquals;
1042
1043
private constructor(
1044
private readonly key: string,
1045
private readonly value: any,
1046
private negated: ContextKeyExpression | null
1047
) {
1048
}
1049
1050
public cmp(other: ContextKeyExpression): number {
1051
if (other.type !== this.type) {
1052
return this.type - other.type;
1053
}
1054
return cmp2(this.key, this.value, other.key, other.value);
1055
}
1056
1057
public equals(other: ContextKeyExpression): boolean {
1058
if (other.type === this.type) {
1059
return (this.key === other.key && this.value === other.value);
1060
}
1061
return false;
1062
}
1063
1064
public substituteConstants(): ContextKeyExpression | undefined {
1065
const constantValue = CONSTANT_VALUES.get(this.key);
1066
if (typeof constantValue === 'boolean') {
1067
const falseValue = constantValue ? 'true' : 'false';
1068
return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1069
}
1070
return this;
1071
}
1072
1073
public evaluate(context: IContext): boolean {
1074
// Intentional !=
1075
// eslint-disable-next-line eqeqeq
1076
return (context.getValue(this.key) != this.value);
1077
}
1078
1079
public serialize(): string {
1080
return `${this.key} != '${this.value}'`;
1081
}
1082
1083
public keys(): string[] {
1084
return [this.key];
1085
}
1086
1087
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1088
return mapFnc.mapNotEquals(this.key, this.value);
1089
}
1090
1091
public negate(): ContextKeyExpression {
1092
if (!this.negated) {
1093
this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this);
1094
}
1095
return this.negated;
1096
}
1097
}
1098
1099
export class ContextKeyNotExpr implements IContextKeyExpression {
1100
1101
public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1102
const constantValue = CONSTANT_VALUES.get(key);
1103
if (typeof constantValue === 'boolean') {
1104
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1105
}
1106
return new ContextKeyNotExpr(key, negated);
1107
}
1108
1109
public readonly type = ContextKeyExprType.Not;
1110
1111
private constructor(
1112
private readonly key: string,
1113
private negated: ContextKeyExpression | null
1114
) {
1115
}
1116
1117
public cmp(other: ContextKeyExpression): number {
1118
if (other.type !== this.type) {
1119
return this.type - other.type;
1120
}
1121
return cmp1(this.key, other.key);
1122
}
1123
1124
public equals(other: ContextKeyExpression): boolean {
1125
if (other.type === this.type) {
1126
return (this.key === other.key);
1127
}
1128
return false;
1129
}
1130
1131
public substituteConstants(): ContextKeyExpression | undefined {
1132
const constantValue = CONSTANT_VALUES.get(this.key);
1133
if (typeof constantValue === 'boolean') {
1134
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1135
}
1136
return this;
1137
}
1138
1139
public evaluate(context: IContext): boolean {
1140
return (!context.getValue(this.key));
1141
}
1142
1143
public serialize(): string {
1144
return `!${this.key}`;
1145
}
1146
1147
public keys(): string[] {
1148
return [this.key];
1149
}
1150
1151
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1152
return mapFnc.mapNot(this.key);
1153
}
1154
1155
public negate(): ContextKeyExpression {
1156
if (!this.negated) {
1157
this.negated = ContextKeyDefinedExpr.create(this.key, this);
1158
}
1159
return this.negated;
1160
}
1161
}
1162
1163
function withFloatOrStr<T extends ContextKeyExpression>(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr {
1164
if (typeof value === 'string') {
1165
const n = parseFloat(value);
1166
if (!isNaN(n)) {
1167
value = n;
1168
}
1169
}
1170
if (typeof value === 'string' || typeof value === 'number') {
1171
return callback(value);
1172
}
1173
return ContextKeyFalseExpr.INSTANCE;
1174
}
1175
1176
export class ContextKeyGreaterExpr implements IContextKeyExpression {
1177
1178
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1179
return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated));
1180
}
1181
1182
public readonly type = ContextKeyExprType.Greater;
1183
1184
private constructor(
1185
private readonly key: string,
1186
private readonly value: number | string,
1187
private negated: ContextKeyExpression | null
1188
) { }
1189
1190
public cmp(other: ContextKeyExpression): number {
1191
if (other.type !== this.type) {
1192
return this.type - other.type;
1193
}
1194
return cmp2(this.key, this.value, other.key, other.value);
1195
}
1196
1197
public equals(other: ContextKeyExpression): boolean {
1198
if (other.type === this.type) {
1199
return (this.key === other.key && this.value === other.value);
1200
}
1201
return false;
1202
}
1203
1204
public substituteConstants(): ContextKeyExpression | undefined {
1205
return this;
1206
}
1207
1208
public evaluate(context: IContext): boolean {
1209
if (typeof this.value === 'string') {
1210
return false;
1211
}
1212
return (parseFloat(<any>context.getValue(this.key)) > this.value);
1213
}
1214
1215
public serialize(): string {
1216
return `${this.key} > ${this.value}`;
1217
}
1218
1219
public keys(): string[] {
1220
return [this.key];
1221
}
1222
1223
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1224
return mapFnc.mapGreater(this.key, this.value);
1225
}
1226
1227
public negate(): ContextKeyExpression {
1228
if (!this.negated) {
1229
this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this);
1230
}
1231
return this.negated;
1232
}
1233
}
1234
1235
export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression {
1236
1237
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1238
return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated));
1239
}
1240
1241
public readonly type = ContextKeyExprType.GreaterEquals;
1242
1243
private constructor(
1244
private readonly key: string,
1245
private readonly value: number | string,
1246
private negated: ContextKeyExpression | null
1247
) { }
1248
1249
public cmp(other: ContextKeyExpression): number {
1250
if (other.type !== this.type) {
1251
return this.type - other.type;
1252
}
1253
return cmp2(this.key, this.value, other.key, other.value);
1254
}
1255
1256
public equals(other: ContextKeyExpression): boolean {
1257
if (other.type === this.type) {
1258
return (this.key === other.key && this.value === other.value);
1259
}
1260
return false;
1261
}
1262
1263
public substituteConstants(): ContextKeyExpression | undefined {
1264
return this;
1265
}
1266
1267
public evaluate(context: IContext): boolean {
1268
if (typeof this.value === 'string') {
1269
return false;
1270
}
1271
return (parseFloat(<any>context.getValue(this.key)) >= this.value);
1272
}
1273
1274
public serialize(): string {
1275
return `${this.key} >= ${this.value}`;
1276
}
1277
1278
public keys(): string[] {
1279
return [this.key];
1280
}
1281
1282
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1283
return mapFnc.mapGreaterEquals(this.key, this.value);
1284
}
1285
1286
public negate(): ContextKeyExpression {
1287
if (!this.negated) {
1288
this.negated = ContextKeySmallerExpr.create(this.key, this.value, this);
1289
}
1290
return this.negated;
1291
}
1292
}
1293
1294
export class ContextKeySmallerExpr implements IContextKeyExpression {
1295
1296
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1297
return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated));
1298
}
1299
1300
public readonly type = ContextKeyExprType.Smaller;
1301
1302
private constructor(
1303
private readonly key: string,
1304
private readonly value: number | string,
1305
private negated: ContextKeyExpression | null
1306
) {
1307
}
1308
1309
public cmp(other: ContextKeyExpression): number {
1310
if (other.type !== this.type) {
1311
return this.type - other.type;
1312
}
1313
return cmp2(this.key, this.value, other.key, other.value);
1314
}
1315
1316
public equals(other: ContextKeyExpression): boolean {
1317
if (other.type === this.type) {
1318
return (this.key === other.key && this.value === other.value);
1319
}
1320
return false;
1321
}
1322
1323
public substituteConstants(): ContextKeyExpression | undefined {
1324
return this;
1325
}
1326
1327
public evaluate(context: IContext): boolean {
1328
if (typeof this.value === 'string') {
1329
return false;
1330
}
1331
return (parseFloat(<any>context.getValue(this.key)) < this.value);
1332
}
1333
1334
public serialize(): string {
1335
return `${this.key} < ${this.value}`;
1336
}
1337
1338
public keys(): string[] {
1339
return [this.key];
1340
}
1341
1342
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1343
return mapFnc.mapSmaller(this.key, this.value);
1344
}
1345
1346
public negate(): ContextKeyExpression {
1347
if (!this.negated) {
1348
this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this);
1349
}
1350
return this.negated;
1351
}
1352
}
1353
1354
export class ContextKeySmallerEqualsExpr implements IContextKeyExpression {
1355
1356
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1357
return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated));
1358
}
1359
1360
public readonly type = ContextKeyExprType.SmallerEquals;
1361
1362
private constructor(
1363
private readonly key: string,
1364
private readonly value: number | string,
1365
private negated: ContextKeyExpression | null
1366
) {
1367
}
1368
1369
public cmp(other: ContextKeyExpression): number {
1370
if (other.type !== this.type) {
1371
return this.type - other.type;
1372
}
1373
return cmp2(this.key, this.value, other.key, other.value);
1374
}
1375
1376
public equals(other: ContextKeyExpression): boolean {
1377
if (other.type === this.type) {
1378
return (this.key === other.key && this.value === other.value);
1379
}
1380
return false;
1381
}
1382
1383
public substituteConstants(): ContextKeyExpression | undefined {
1384
return this;
1385
}
1386
1387
public evaluate(context: IContext): boolean {
1388
if (typeof this.value === 'string') {
1389
return false;
1390
}
1391
return (parseFloat(<any>context.getValue(this.key)) <= this.value);
1392
}
1393
1394
public serialize(): string {
1395
return `${this.key} <= ${this.value}`;
1396
}
1397
1398
public keys(): string[] {
1399
return [this.key];
1400
}
1401
1402
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1403
return mapFnc.mapSmallerEquals(this.key, this.value);
1404
}
1405
1406
public negate(): ContextKeyExpression {
1407
if (!this.negated) {
1408
this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this);
1409
}
1410
return this.negated;
1411
}
1412
}
1413
1414
export class ContextKeyRegexExpr implements IContextKeyExpression {
1415
1416
public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr {
1417
return new ContextKeyRegexExpr(key, regexp);
1418
}
1419
1420
public readonly type = ContextKeyExprType.Regex;
1421
private negated: ContextKeyExpression | null = null;
1422
1423
private constructor(
1424
private readonly key: string,
1425
private readonly regexp: RegExp | null
1426
) {
1427
//
1428
}
1429
1430
public cmp(other: ContextKeyExpression): number {
1431
if (other.type !== this.type) {
1432
return this.type - other.type;
1433
}
1434
if (this.key < other.key) {
1435
return -1;
1436
}
1437
if (this.key > other.key) {
1438
return 1;
1439
}
1440
const thisSource = this.regexp ? this.regexp.source : '';
1441
const otherSource = other.regexp ? other.regexp.source : '';
1442
if (thisSource < otherSource) {
1443
return -1;
1444
}
1445
if (thisSource > otherSource) {
1446
return 1;
1447
}
1448
return 0;
1449
}
1450
1451
public equals(other: ContextKeyExpression): boolean {
1452
if (other.type === this.type) {
1453
const thisSource = this.regexp ? this.regexp.source : '';
1454
const otherSource = other.regexp ? other.regexp.source : '';
1455
return (this.key === other.key && thisSource === otherSource);
1456
}
1457
return false;
1458
}
1459
1460
public substituteConstants(): ContextKeyExpression | undefined {
1461
return this;
1462
}
1463
1464
public evaluate(context: IContext): boolean {
1465
const value = context.getValue<any>(this.key);
1466
return this.regexp ? this.regexp.test(value) : false;
1467
}
1468
1469
public serialize(): string {
1470
const value = this.regexp
1471
? `/${this.regexp.source}/${this.regexp.flags}`
1472
: '/invalid/';
1473
return `${this.key} =~ ${value}`;
1474
}
1475
1476
public keys(): string[] {
1477
return [this.key];
1478
}
1479
1480
public map(mapFnc: IContextKeyExprMapper): ContextKeyRegexExpr {
1481
return mapFnc.mapRegex(this.key, this.regexp);
1482
}
1483
1484
public negate(): ContextKeyExpression {
1485
if (!this.negated) {
1486
this.negated = ContextKeyNotRegexExpr.create(this);
1487
}
1488
return this.negated;
1489
}
1490
}
1491
1492
export class ContextKeyNotRegexExpr implements IContextKeyExpression {
1493
1494
public static create(actual: ContextKeyRegexExpr): ContextKeyExpression {
1495
return new ContextKeyNotRegexExpr(actual);
1496
}
1497
1498
public readonly type = ContextKeyExprType.NotRegex;
1499
1500
private constructor(private readonly _actual: ContextKeyRegexExpr) {
1501
//
1502
}
1503
1504
public cmp(other: ContextKeyExpression): number {
1505
if (other.type !== this.type) {
1506
return this.type - other.type;
1507
}
1508
return this._actual.cmp(other._actual);
1509
}
1510
1511
public equals(other: ContextKeyExpression): boolean {
1512
if (other.type === this.type) {
1513
return this._actual.equals(other._actual);
1514
}
1515
return false;
1516
}
1517
1518
public substituteConstants(): ContextKeyExpression | undefined {
1519
return this;
1520
}
1521
1522
public evaluate(context: IContext): boolean {
1523
return !this._actual.evaluate(context);
1524
}
1525
1526
public serialize(): string {
1527
return `!(${this._actual.serialize()})`;
1528
}
1529
1530
public keys(): string[] {
1531
return this._actual.keys();
1532
}
1533
1534
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1535
return new ContextKeyNotRegexExpr(this._actual.map(mapFnc));
1536
}
1537
1538
public negate(): ContextKeyExpression {
1539
return this._actual;
1540
}
1541
}
1542
1543
/**
1544
* @returns the same instance if nothing changed.
1545
*/
1546
function eliminateConstantsInArray(arr: ContextKeyExpression[]): (ContextKeyExpression | undefined)[] {
1547
// Allocate array only if there is a difference
1548
let newArr: (ContextKeyExpression | undefined)[] | null = null;
1549
for (let i = 0, len = arr.length; i < len; i++) {
1550
const newExpr = arr[i].substituteConstants();
1551
1552
if (arr[i] !== newExpr) {
1553
// something has changed!
1554
1555
// allocate array on first difference
1556
if (newArr === null) {
1557
newArr = [];
1558
for (let j = 0; j < i; j++) {
1559
newArr[j] = arr[j];
1560
}
1561
}
1562
}
1563
1564
if (newArr !== null) {
1565
newArr[i] = newExpr;
1566
}
1567
}
1568
1569
if (newArr === null) {
1570
return arr;
1571
}
1572
return newArr;
1573
}
1574
1575
export class ContextKeyAndExpr implements IContextKeyExpression {
1576
1577
public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1578
return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1579
}
1580
1581
public readonly type = ContextKeyExprType.And;
1582
1583
private constructor(
1584
public readonly expr: ContextKeyExpression[],
1585
private negated: ContextKeyExpression | null
1586
) {
1587
}
1588
1589
public cmp(other: ContextKeyExpression): number {
1590
if (other.type !== this.type) {
1591
return this.type - other.type;
1592
}
1593
if (this.expr.length < other.expr.length) {
1594
return -1;
1595
}
1596
if (this.expr.length > other.expr.length) {
1597
return 1;
1598
}
1599
for (let i = 0, len = this.expr.length; i < len; i++) {
1600
const r = cmp(this.expr[i], other.expr[i]);
1601
if (r !== 0) {
1602
return r;
1603
}
1604
}
1605
return 0;
1606
}
1607
1608
public equals(other: ContextKeyExpression): boolean {
1609
if (other.type === this.type) {
1610
if (this.expr.length !== other.expr.length) {
1611
return false;
1612
}
1613
for (let i = 0, len = this.expr.length; i < len; i++) {
1614
if (!this.expr[i].equals(other.expr[i])) {
1615
return false;
1616
}
1617
}
1618
return true;
1619
}
1620
return false;
1621
}
1622
1623
public substituteConstants(): ContextKeyExpression | undefined {
1624
const exprArr = eliminateConstantsInArray(this.expr);
1625
if (exprArr === this.expr) {
1626
// no change
1627
return this;
1628
}
1629
return ContextKeyAndExpr.create(exprArr, this.negated, false);
1630
}
1631
1632
public evaluate(context: IContext): boolean {
1633
for (let i = 0, len = this.expr.length; i < len; i++) {
1634
if (!this.expr[i].evaluate(context)) {
1635
return false;
1636
}
1637
}
1638
return true;
1639
}
1640
1641
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1642
const expr: ContextKeyExpression[] = [];
1643
let hasTrue = false;
1644
1645
for (const e of arr) {
1646
if (!e) {
1647
continue;
1648
}
1649
1650
if (e.type === ContextKeyExprType.True) {
1651
// anything && true ==> anything
1652
hasTrue = true;
1653
continue;
1654
}
1655
1656
if (e.type === ContextKeyExprType.False) {
1657
// anything && false ==> false
1658
return ContextKeyFalseExpr.INSTANCE;
1659
}
1660
1661
if (e.type === ContextKeyExprType.And) {
1662
expr.push(...e.expr);
1663
continue;
1664
}
1665
1666
expr.push(e);
1667
}
1668
1669
if (expr.length === 0 && hasTrue) {
1670
return ContextKeyTrueExpr.INSTANCE;
1671
}
1672
1673
if (expr.length === 0) {
1674
return undefined;
1675
}
1676
1677
if (expr.length === 1) {
1678
return expr[0];
1679
}
1680
1681
expr.sort(cmp);
1682
1683
// eliminate duplicate terms
1684
for (let i = 1; i < expr.length; i++) {
1685
if (expr[i - 1].equals(expr[i])) {
1686
expr.splice(i, 1);
1687
i--;
1688
}
1689
}
1690
1691
if (expr.length === 1) {
1692
return expr[0];
1693
}
1694
1695
// We must distribute any OR expression because we don't support parens
1696
// OR extensions will be at the end (due to sorting rules)
1697
while (expr.length > 1) {
1698
const lastElement = expr[expr.length - 1];
1699
if (lastElement.type !== ContextKeyExprType.Or) {
1700
break;
1701
}
1702
// pop the last element
1703
expr.pop();
1704
1705
// pop the second to last element
1706
const secondToLastElement = expr.pop()!;
1707
1708
const isFinished = (expr.length === 0);
1709
1710
// distribute `lastElement` over `secondToLastElement`
1711
const resultElement = ContextKeyOrExpr.create(
1712
lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null, extraRedundantCheck)),
1713
null,
1714
isFinished
1715
);
1716
1717
if (resultElement) {
1718
expr.push(resultElement);
1719
expr.sort(cmp);
1720
}
1721
}
1722
1723
if (expr.length === 1) {
1724
return expr[0];
1725
}
1726
1727
// resolve false AND expressions
1728
if (extraRedundantCheck) {
1729
for (let i = 0; i < expr.length; i++) {
1730
for (let j = i + 1; j < expr.length; j++) {
1731
if (expr[i].negate().equals(expr[j])) {
1732
// A && !A case
1733
return ContextKeyFalseExpr.INSTANCE;
1734
}
1735
}
1736
}
1737
1738
if (expr.length === 1) {
1739
return expr[0];
1740
}
1741
}
1742
1743
return new ContextKeyAndExpr(expr, negated);
1744
}
1745
1746
public serialize(): string {
1747
return this.expr.map(e => e.serialize()).join(' && ');
1748
}
1749
1750
public keys(): string[] {
1751
const result: string[] = [];
1752
for (const expr of this.expr) {
1753
result.push(...expr.keys());
1754
}
1755
return result;
1756
}
1757
1758
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1759
return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1760
}
1761
1762
public negate(): ContextKeyExpression {
1763
if (!this.negated) {
1764
const result: ContextKeyExpression[] = [];
1765
for (const expr of this.expr) {
1766
result.push(expr.negate());
1767
}
1768
this.negated = ContextKeyOrExpr.create(result, this, true)!;
1769
}
1770
return this.negated;
1771
}
1772
}
1773
1774
export class ContextKeyOrExpr implements IContextKeyExpression {
1775
1776
public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1777
return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1778
}
1779
1780
public readonly type = ContextKeyExprType.Or;
1781
1782
private constructor(
1783
public readonly expr: ContextKeyExpression[],
1784
private negated: ContextKeyExpression | null
1785
) {
1786
}
1787
1788
public cmp(other: ContextKeyExpression): number {
1789
if (other.type !== this.type) {
1790
return this.type - other.type;
1791
}
1792
if (this.expr.length < other.expr.length) {
1793
return -1;
1794
}
1795
if (this.expr.length > other.expr.length) {
1796
return 1;
1797
}
1798
for (let i = 0, len = this.expr.length; i < len; i++) {
1799
const r = cmp(this.expr[i], other.expr[i]);
1800
if (r !== 0) {
1801
return r;
1802
}
1803
}
1804
return 0;
1805
}
1806
1807
public equals(other: ContextKeyExpression): boolean {
1808
if (other.type === this.type) {
1809
if (this.expr.length !== other.expr.length) {
1810
return false;
1811
}
1812
for (let i = 0, len = this.expr.length; i < len; i++) {
1813
if (!this.expr[i].equals(other.expr[i])) {
1814
return false;
1815
}
1816
}
1817
return true;
1818
}
1819
return false;
1820
}
1821
1822
public substituteConstants(): ContextKeyExpression | undefined {
1823
const exprArr = eliminateConstantsInArray(this.expr);
1824
if (exprArr === this.expr) {
1825
// no change
1826
return this;
1827
}
1828
return ContextKeyOrExpr.create(exprArr, this.negated, false);
1829
}
1830
1831
public evaluate(context: IContext): boolean {
1832
for (let i = 0, len = this.expr.length; i < len; i++) {
1833
if (this.expr[i].evaluate(context)) {
1834
return true;
1835
}
1836
}
1837
return false;
1838
}
1839
1840
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1841
let expr: ContextKeyExpression[] = [];
1842
let hasFalse = false;
1843
1844
if (arr) {
1845
for (let i = 0, len = arr.length; i < len; i++) {
1846
const e = arr[i];
1847
if (!e) {
1848
continue;
1849
}
1850
1851
if (e.type === ContextKeyExprType.False) {
1852
// anything || false ==> anything
1853
hasFalse = true;
1854
continue;
1855
}
1856
1857
if (e.type === ContextKeyExprType.True) {
1858
// anything || true ==> true
1859
return ContextKeyTrueExpr.INSTANCE;
1860
}
1861
1862
if (e.type === ContextKeyExprType.Or) {
1863
expr = expr.concat(e.expr);
1864
continue;
1865
}
1866
1867
expr.push(e);
1868
}
1869
1870
if (expr.length === 0 && hasFalse) {
1871
return ContextKeyFalseExpr.INSTANCE;
1872
}
1873
1874
expr.sort(cmp);
1875
}
1876
1877
if (expr.length === 0) {
1878
return undefined;
1879
}
1880
1881
if (expr.length === 1) {
1882
return expr[0];
1883
}
1884
1885
// eliminate duplicate terms
1886
for (let i = 1; i < expr.length; i++) {
1887
if (expr[i - 1].equals(expr[i])) {
1888
expr.splice(i, 1);
1889
i--;
1890
}
1891
}
1892
1893
if (expr.length === 1) {
1894
return expr[0];
1895
}
1896
1897
// resolve true OR expressions
1898
if (extraRedundantCheck) {
1899
for (let i = 0; i < expr.length; i++) {
1900
for (let j = i + 1; j < expr.length; j++) {
1901
if (expr[i].negate().equals(expr[j])) {
1902
// A || !A case
1903
return ContextKeyTrueExpr.INSTANCE;
1904
}
1905
}
1906
}
1907
1908
if (expr.length === 1) {
1909
return expr[0];
1910
}
1911
}
1912
1913
return new ContextKeyOrExpr(expr, negated);
1914
}
1915
1916
public serialize(): string {
1917
return this.expr.map(e => e.serialize()).join(' || ');
1918
}
1919
1920
public keys(): string[] {
1921
const result: string[] = [];
1922
for (const expr of this.expr) {
1923
result.push(...expr.keys());
1924
}
1925
return result;
1926
}
1927
1928
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1929
return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1930
}
1931
1932
public negate(): ContextKeyExpression {
1933
if (!this.negated) {
1934
const result: ContextKeyExpression[] = [];
1935
for (const expr of this.expr) {
1936
result.push(expr.negate());
1937
}
1938
1939
// We don't support parens, so here we distribute the AND over the OR terminals
1940
// We always take the first 2 AND pairs and distribute them
1941
while (result.length > 1) {
1942
const LEFT = result.shift()!;
1943
const RIGHT = result.shift()!;
1944
1945
const all: ContextKeyExpression[] = [];
1946
for (const left of getTerminals(LEFT)) {
1947
for (const right of getTerminals(RIGHT)) {
1948
all.push(ContextKeyAndExpr.create([left, right], null, false)!);
1949
}
1950
}
1951
1952
result.unshift(ContextKeyOrExpr.create(all, null, false)!);
1953
}
1954
1955
this.negated = ContextKeyOrExpr.create(result, this, true)!;
1956
}
1957
return this.negated;
1958
}
1959
}
1960
1961
export interface ContextKeyInfo {
1962
readonly key: string;
1963
readonly type?: string;
1964
readonly description?: string;
1965
}
1966
1967
export class RawContextKey<T extends ContextKeyValue> extends ContextKeyDefinedExpr {
1968
1969
private static _info: ContextKeyInfo[] = [];
1970
1971
static all(): IterableIterator<ContextKeyInfo> {
1972
return RawContextKey._info.values();
1973
}
1974
1975
private readonly _defaultValue: T | undefined;
1976
1977
constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string; description: string }) {
1978
super(key, null);
1979
this._defaultValue = defaultValue;
1980
1981
// collect all context keys into a central place
1982
if (typeof metaOrHide === 'object') {
1983
RawContextKey._info.push({ ...metaOrHide, key });
1984
} else if (metaOrHide !== true) {
1985
RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined });
1986
}
1987
}
1988
1989
public bindTo(target: IContextKeyService): IContextKey<T> {
1990
return target.createKey(this.key, this._defaultValue);
1991
}
1992
1993
public getValue(target: IContextKeyService): T | undefined {
1994
return target.getContextKeyValue<T>(this.key);
1995
}
1996
1997
public toNegated(): ContextKeyExpression {
1998
return this.negate();
1999
}
2000
2001
public isEqualTo(value: any): ContextKeyExpression {
2002
return ContextKeyEqualsExpr.create(this.key, value);
2003
}
2004
2005
public notEqualsTo(value: any): ContextKeyExpression {
2006
return ContextKeyNotEqualsExpr.create(this.key, value);
2007
}
2008
2009
public greater(value: any): ContextKeyExpression {
2010
return ContextKeyGreaterExpr.create(this.key, value);
2011
}
2012
}
2013
2014
export type ContextKeyValue = null | undefined | boolean | number | string
2015
| Array<null | undefined | boolean | number | string>
2016
| Record<string, null | undefined | boolean | number | string>;
2017
2018
export interface IContext {
2019
getValue<T extends ContextKeyValue = ContextKeyValue>(key: string): T | undefined;
2020
}
2021
2022
export interface IContextKey<T extends ContextKeyValue = ContextKeyValue> {
2023
set(value: T): void;
2024
reset(): void;
2025
get(): T | undefined;
2026
}
2027
2028
export interface IContextKeyServiceTarget {
2029
parentElement: IContextKeyServiceTarget | null;
2030
setAttribute(attr: string, value: string): void;
2031
removeAttribute(attr: string): void;
2032
hasAttribute(attr: string): boolean;
2033
getAttribute(attr: string): string | null;
2034
}
2035
2036
export const IContextKeyService = createDecorator<IContextKeyService>('contextKeyService');
2037
2038
export interface IReadableSet<T> {
2039
has(value: T): boolean;
2040
}
2041
2042
export interface IContextKeyChangeEvent {
2043
affectsSome(keys: IReadableSet<string>): boolean;
2044
allKeysContainedIn(keys: IReadableSet<string>): boolean;
2045
}
2046
2047
export type IScopedContextKeyService = IContextKeyService & IDisposable;
2048
2049
export interface IContextKeyService {
2050
readonly _serviceBrand: undefined;
2051
2052
onDidChangeContext: Event<IContextKeyChangeEvent>;
2053
bufferChangeEvents(callback: Function): void;
2054
2055
createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T>;
2056
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;
2057
getContextKeyValue<T>(key: string): T | undefined;
2058
2059
createScoped(target: IContextKeyServiceTarget): IScopedContextKeyService;
2060
createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;
2061
getContext(target: IContextKeyServiceTarget | null): IContext;
2062
2063
updateParent(parentContextKeyService: IContextKeyService): void;
2064
}
2065
2066
function cmp1(key1: string, key2: string): number {
2067
if (key1 < key2) {
2068
return -1;
2069
}
2070
if (key1 > key2) {
2071
return 1;
2072
}
2073
return 0;
2074
}
2075
2076
function cmp2(key1: string, value1: any, key2: string, value2: any): number {
2077
if (key1 < key2) {
2078
return -1;
2079
}
2080
if (key1 > key2) {
2081
return 1;
2082
}
2083
if (value1 < value2) {
2084
return -1;
2085
}
2086
if (value1 > value2) {
2087
return 1;
2088
}
2089
return 0;
2090
}
2091
2092
/**
2093
* Returns true if it is provable `p` implies `q`.
2094
*/
2095
export function implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean {
2096
2097
if (p.type === ContextKeyExprType.False || q.type === ContextKeyExprType.True) {
2098
// false implies anything
2099
// anything implies true
2100
return true;
2101
}
2102
2103
if (p.type === ContextKeyExprType.Or) {
2104
if (q.type === ContextKeyExprType.Or) {
2105
// `a || b || c` can only imply something like `a || b || c || d`
2106
return allElementsIncluded(p.expr, q.expr);
2107
}
2108
return false;
2109
}
2110
2111
if (q.type === ContextKeyExprType.Or) {
2112
for (const element of q.expr) {
2113
if (implies(p, element)) {
2114
return true;
2115
}
2116
}
2117
return false;
2118
}
2119
2120
if (p.type === ContextKeyExprType.And) {
2121
if (q.type === ContextKeyExprType.And) {
2122
// `a && b && c` implies `a && c`
2123
return allElementsIncluded(q.expr, p.expr);
2124
}
2125
for (const element of p.expr) {
2126
if (implies(element, q)) {
2127
return true;
2128
}
2129
}
2130
return false;
2131
}
2132
2133
return p.equals(q);
2134
}
2135
2136
/**
2137
* Returns true if all elements in `p` are also present in `q`.
2138
* The two arrays are assumed to be sorted
2139
*/
2140
function allElementsIncluded(p: ContextKeyExpression[], q: ContextKeyExpression[]): boolean {
2141
let pIndex = 0;
2142
let qIndex = 0;
2143
while (pIndex < p.length && qIndex < q.length) {
2144
const cmp = p[pIndex].cmp(q[qIndex]);
2145
2146
if (cmp < 0) {
2147
// an element from `p` is missing from `q`
2148
return false;
2149
} else if (cmp === 0) {
2150
pIndex++;
2151
qIndex++;
2152
} else {
2153
qIndex++;
2154
}
2155
}
2156
return (pIndex === p.length);
2157
}
2158
2159
function getTerminals(node: ContextKeyExpression) {
2160
if (node.type === ContextKeyExprType.Or) {
2161
return node.expr;
2162
}
2163
return [node];
2164
}
2165
2166