1
>
/*---------------------------------------------------------------------------------------------
contextkey.ts
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
}
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
}
191
>
get parsingErrors(): Readonly<ParsingError[]> {
192
return this._parsingErrors;
193
}
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 === '') {