Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.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
import assert from 'assert';
6
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
7
import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from '../../browser/snippetParser.js';
8
9
suite('SnippetParser', () => {
10
11
ensureNoDisposablesAreLeakedInTestSuite();
12
13
test('Scanner', () => {
14
15
const scanner = new Scanner();
16
assert.strictEqual(scanner.next().type, TokenType.EOF);
17
18
scanner.text('abc');
19
assert.strictEqual(scanner.next().type, TokenType.VariableName);
20
assert.strictEqual(scanner.next().type, TokenType.EOF);
21
22
scanner.text('{{abc}}');
23
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
24
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
25
assert.strictEqual(scanner.next().type, TokenType.VariableName);
26
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
27
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
28
assert.strictEqual(scanner.next().type, TokenType.EOF);
29
30
scanner.text('abc() ');
31
assert.strictEqual(scanner.next().type, TokenType.VariableName);
32
assert.strictEqual(scanner.next().type, TokenType.Format);
33
assert.strictEqual(scanner.next().type, TokenType.EOF);
34
35
scanner.text('abc 123');
36
assert.strictEqual(scanner.next().type, TokenType.VariableName);
37
assert.strictEqual(scanner.next().type, TokenType.Format);
38
assert.strictEqual(scanner.next().type, TokenType.Int);
39
assert.strictEqual(scanner.next().type, TokenType.EOF);
40
41
scanner.text('$foo');
42
assert.strictEqual(scanner.next().type, TokenType.Dollar);
43
assert.strictEqual(scanner.next().type, TokenType.VariableName);
44
assert.strictEqual(scanner.next().type, TokenType.EOF);
45
46
scanner.text('$foo_bar');
47
assert.strictEqual(scanner.next().type, TokenType.Dollar);
48
assert.strictEqual(scanner.next().type, TokenType.VariableName);
49
assert.strictEqual(scanner.next().type, TokenType.EOF);
50
51
scanner.text('$foo-bar');
52
assert.strictEqual(scanner.next().type, TokenType.Dollar);
53
assert.strictEqual(scanner.next().type, TokenType.VariableName);
54
assert.strictEqual(scanner.next().type, TokenType.Dash);
55
assert.strictEqual(scanner.next().type, TokenType.VariableName);
56
assert.strictEqual(scanner.next().type, TokenType.EOF);
57
58
scanner.text('${foo}');
59
assert.strictEqual(scanner.next().type, TokenType.Dollar);
60
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
61
assert.strictEqual(scanner.next().type, TokenType.VariableName);
62
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
63
assert.strictEqual(scanner.next().type, TokenType.EOF);
64
65
scanner.text('${1223:foo}');
66
assert.strictEqual(scanner.next().type, TokenType.Dollar);
67
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
68
assert.strictEqual(scanner.next().type, TokenType.Int);
69
assert.strictEqual(scanner.next().type, TokenType.Colon);
70
assert.strictEqual(scanner.next().type, TokenType.VariableName);
71
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
72
assert.strictEqual(scanner.next().type, TokenType.EOF);
73
74
scanner.text('\\${}');
75
assert.strictEqual(scanner.next().type, TokenType.Backslash);
76
assert.strictEqual(scanner.next().type, TokenType.Dollar);
77
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
78
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
79
80
scanner.text('${foo/regex/format/option}');
81
assert.strictEqual(scanner.next().type, TokenType.Dollar);
82
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
83
assert.strictEqual(scanner.next().type, TokenType.VariableName);
84
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
85
assert.strictEqual(scanner.next().type, TokenType.VariableName);
86
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
87
assert.strictEqual(scanner.next().type, TokenType.VariableName);
88
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
89
assert.strictEqual(scanner.next().type, TokenType.VariableName);
90
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
91
assert.strictEqual(scanner.next().type, TokenType.EOF);
92
});
93
94
function assertText(value: string, expected: string) {
95
const actual = SnippetParser.asInsertText(value);
96
assert.strictEqual(actual, expected);
97
}
98
99
function assertMarker(input: TextmateSnippet | Marker[] | string, ...ctors: Function[]) {
100
let marker: Marker[];
101
if (input instanceof TextmateSnippet) {
102
marker = [...input.children];
103
} else if (typeof input === 'string') {
104
const p = new SnippetParser();
105
marker = p.parse(input).children;
106
} else {
107
marker = [...input];
108
}
109
while (marker.length > 0) {
110
const m = marker.pop();
111
const ctor = ctors.pop()!;
112
assert.ok(m instanceof ctor);
113
}
114
assert.strictEqual(marker.length, ctors.length);
115
assert.strictEqual(marker.length, 0);
116
}
117
118
function assertTextAndMarker(value: string, escaped: string, ...ctors: Function[]) {
119
assertText(value, escaped);
120
assertMarker(value, ...ctors);
121
}
122
123
function assertEscaped(value: string, expected: string) {
124
const actual = SnippetParser.escape(value);
125
assert.strictEqual(actual, expected);
126
}
127
128
test('Parser, escaped', function () {
129
assertEscaped('foo$0', 'foo\\$0');
130
assertEscaped('foo\\$0', 'foo\\\\\\$0');
131
assertEscaped('f$1oo$0', 'f\\$1oo\\$0');
132
assertEscaped('${1:foo}$0', '\\${1:foo\\}\\$0');
133
assertEscaped('$', '\\$');
134
});
135
136
test('Parser, text', () => {
137
assertText('$', '$');
138
assertText('\\\\$', '\\$');
139
assertText('{', '{');
140
assertText('\\}', '}');
141
assertText('\\abc', '\\abc');
142
assertText('foo${f:\\}}bar', 'foo}bar');
143
assertText('\\{', '\\{');
144
assertText('I need \\\\\\$', 'I need \\$');
145
assertText('\\', '\\');
146
assertText('\\{{', '\\{{');
147
assertText('{{', '{{');
148
assertText('{{dd', '{{dd');
149
assertText('}}', '}}');
150
assertText('ff}}', 'ff}}');
151
152
assertText('farboo', 'farboo');
153
assertText('far{{}}boo', 'far{{}}boo');
154
assertText('far{{123}}boo', 'far{{123}}boo');
155
assertText('far\\{{123}}boo', 'far\\{{123}}boo');
156
assertText('far{{id:bern}}boo', 'far{{id:bern}}boo');
157
assertText('far{{id:bern {{basel}}}}boo', 'far{{id:bern {{basel}}}}boo');
158
assertText('far{{id:bern {{id:basel}}}}boo', 'far{{id:bern {{id:basel}}}}boo');
159
assertText('far{{id:bern {{id2:basel}}}}boo', 'far{{id:bern {{id2:basel}}}}boo');
160
});
161
162
163
test('Parser, TM text', () => {
164
assertTextAndMarker('foo${1:bar}}', 'foobar}', Text, Placeholder, Text);
165
assertTextAndMarker('foo${1:bar}${2:foo}}', 'foobarfoo}', Text, Placeholder, Placeholder, Text);
166
167
assertTextAndMarker('foo${1:bar\\}${2:foo}}', 'foobar}foo', Text, Placeholder);
168
169
const [, placeholder] = new SnippetParser().parse('foo${1:bar\\}${2:foo}}').children;
170
const { children } = (<Placeholder>placeholder);
171
172
assert.strictEqual((<Placeholder>placeholder).index, 1);
173
assert.ok(children[0] instanceof Text);
174
assert.strictEqual(children[0].toString(), 'bar}');
175
assert.ok(children[1] instanceof Placeholder);
176
assert.strictEqual(children[1].toString(), 'foo');
177
});
178
179
test('Parser, placeholder', () => {
180
assertTextAndMarker('farboo', 'farboo', Text);
181
assertTextAndMarker('far{{}}boo', 'far{{}}boo', Text);
182
assertTextAndMarker('far{{123}}boo', 'far{{123}}boo', Text);
183
assertTextAndMarker('far\\{{123}}boo', 'far\\{{123}}boo', Text);
184
});
185
186
test('Parser, literal code', () => {
187
assertTextAndMarker('far`123`boo', 'far`123`boo', Text);
188
assertTextAndMarker('far\\`123\\`boo', 'far\\`123\\`boo', Text);
189
});
190
191
test('Parser, variables/tabstop', () => {
192
assertTextAndMarker('$far-boo', '-boo', Variable, Text);
193
assertTextAndMarker('\\$far-boo', '$far-boo', Text);
194
assertTextAndMarker('far$farboo', 'far', Text, Variable);
195
assertTextAndMarker('far${farboo}', 'far', Text, Variable);
196
assertTextAndMarker('$123', '', Placeholder);
197
assertTextAndMarker('$farboo', '', Variable);
198
assertTextAndMarker('$far12boo', '', Variable);
199
assertTextAndMarker('000_${far}_000', '000__000', Text, Variable, Text);
200
assertTextAndMarker('FFF_${TM_SELECTED_TEXT}_FFF$0', 'FFF__FFF', Text, Variable, Text, Placeholder);
201
});
202
203
test('Parser, variables/placeholder with defaults', () => {
204
assertTextAndMarker('${name:value}', 'value', Variable);
205
assertTextAndMarker('${1:value}', 'value', Placeholder);
206
assertTextAndMarker('${1:bar${2:foo}bar}', 'barfoobar', Placeholder);
207
208
assertTextAndMarker('${name:value', '${name:value', Text);
209
assertTextAndMarker('${1:bar${2:foobar}', '${1:barfoobar', Text, Placeholder);
210
});
211
212
test('Parser, variable transforms', function () {
213
assertTextAndMarker('${foo///}', '', Variable);
214
assertTextAndMarker('${foo/regex/format/gmi}', '', Variable);
215
assertTextAndMarker('${foo/([A-Z][a-z])/format/}', '', Variable);
216
217
// invalid regex
218
assertTextAndMarker('${foo/([A-Z][a-z])/format/GMI}', '${foo/([A-Z][a-z])/format/GMI}', Text);
219
assertTextAndMarker('${foo/([A-Z][a-z])/format/funky}', '${foo/([A-Z][a-z])/format/funky}', Text);
220
assertTextAndMarker('${foo/([A-Z][a-z]/format/}', '${foo/([A-Z][a-z]/format/}', Text);
221
222
// tricky regex
223
assertTextAndMarker('${foo/m\\/atch/$1/i}', '', Variable);
224
assertMarker('${foo/regex\/format/options}', Text);
225
226
// incomplete
227
assertTextAndMarker('${foo///', '${foo///', Text);
228
assertTextAndMarker('${foo/regex/format/options', '${foo/regex/format/options', Text);
229
230
// format string
231
assertMarker('${foo/.*/${0:fooo}/i}', Variable);
232
assertMarker('${foo/.*/${1}/i}', Variable);
233
assertMarker('${foo/.*/$1/i}', Variable);
234
assertMarker('${foo/.*/This-$1-encloses/i}', Variable);
235
assertMarker('${foo/.*/complex${1:else}/i}', Variable);
236
assertMarker('${foo/.*/complex${1:-else}/i}', Variable);
237
assertMarker('${foo/.*/complex${1:+if}/i}', Variable);
238
assertMarker('${foo/.*/complex${1:?if:else}/i}', Variable);
239
assertMarker('${foo/.*/complex${1:/upcase}/i}', Variable);
240
241
});
242
243
test('Parser, placeholder transforms', function () {
244
assertTextAndMarker('${1///}', '', Placeholder);
245
assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder);
246
assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder);
247
248
// tricky regex
249
assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder);
250
assertMarker('${1/regex\/format/options}', Text);
251
252
// incomplete
253
assertTextAndMarker('${1///', '${1///', Text);
254
assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text);
255
});
256
257
test('No way to escape forward slash in snippet regex #36715', function () {
258
assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable);
259
});
260
261
test('No way to escape forward slash in snippet format section #37562', function () {
262
assertMarker('${TM_SELECTED_TEXT/a/\\/$1/g}', Variable);
263
assertMarker('${TM_SELECTED_TEXT/a/in\\/$1ner/g}', Variable);
264
assertMarker('${TM_SELECTED_TEXT/a/end\\//g}', Variable);
265
});
266
267
test('Parser, placeholder with choice', () => {
268
269
assertTextAndMarker('${1|one,two,three|}', 'one', Placeholder);
270
assertTextAndMarker('${1|one|}', 'one', Placeholder);
271
assertTextAndMarker('${1|one1,two2|}', 'one1', Placeholder);
272
assertTextAndMarker('${1|one1\\,two2|}', 'one1,two2', Placeholder);
273
assertTextAndMarker('${1|one1\\|two2|}', 'one1|two2', Placeholder);
274
assertTextAndMarker('${1|one1\\atwo2|}', 'one1\\atwo2', Placeholder);
275
assertTextAndMarker('${1|one,two,three,|}', '${1|one,two,three,|}', Text);
276
assertTextAndMarker('${1|one,', '${1|one,', Text);
277
278
const snippet = new SnippetParser().parse('${1|one,two,three|}');
279
const expected: ((m: Marker) => boolean)[] = [
280
m => m instanceof Placeholder,
281
m => m instanceof Choice && m.options.length === 3 && m.options.every(x => x instanceof Text),
282
];
283
snippet.walk(marker => {
284
assert.ok(expected.shift()!(marker));
285
return true;
286
});
287
});
288
289
test('Snippet choices: unable to escape comma and pipe, #31521', function () {
290
assertTextAndMarker('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(not, not);', Text, Placeholder, Text);
291
});
292
293
test('Marker, toTextmateString()', function () {
294
295
function assertTextsnippetString(input: string, expected: string): void {
296
const snippet = new SnippetParser().parse(input);
297
const actual = snippet.toTextmateString();
298
assert.strictEqual(actual, expected);
299
}
300
301
assertTextsnippetString('$1', '$1');
302
assertTextsnippetString('\\$1', '\\$1');
303
assertTextsnippetString('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});');
304
assertTextsnippetString('console.log(${1|not\\, not, \\| five, 5, 1 23|});', 'console.log(${1|not\\, not, \\| five, 5, 1 23|});');
305
assertTextsnippetString('${1|cho\\,ices,wi\\|th,esc\\\\aping,chall\\\\\\,enges|}', '${1|cho\\,ices,wi\\|th,esc\\\\aping,chall\\\\\\,enges|}');
306
assertTextsnippetString('this is text', 'this is text');
307
assertTextsnippetString('this ${1:is ${2:nested with $var}}', 'this ${1:is ${2:nested with ${var}}}');
308
assertTextsnippetString('this ${1:is ${2:nested with $var}}}', 'this ${1:is ${2:nested with ${var}}}\\}');
309
});
310
311
test('Marker, toTextmateString() <-> identity', function () {
312
313
function assertIdent(input: string): void {
314
// full loop: (1) parse input, (2) generate textmate string, (3) parse, (4) ensure both trees are equal
315
const snippet = new SnippetParser().parse(input);
316
const input2 = snippet.toTextmateString();
317
const snippet2 = new SnippetParser().parse(input2);
318
319
function checkCheckChildren(marker1: Marker, marker2: Marker) {
320
assert.ok(marker1 instanceof Object.getPrototypeOf(marker2).constructor);
321
assert.ok(marker2 instanceof Object.getPrototypeOf(marker1).constructor);
322
323
assert.strictEqual(marker1.children.length, marker2.children.length);
324
assert.strictEqual(marker1.toString(), marker2.toString());
325
326
for (let i = 0; i < marker1.children.length; i++) {
327
checkCheckChildren(marker1.children[i], marker2.children[i]);
328
}
329
}
330
331
checkCheckChildren(snippet, snippet2);
332
}
333
334
assertIdent('$1');
335
assertIdent('\\$1');
336
assertIdent('console.log(${1|not\\, not, five, 5, 1 23|});');
337
assertIdent('console.log(${1|not\\, not, \\| five, 5, 1 23|});');
338
assertIdent('this is text');
339
assertIdent('this ${1:is ${2:nested with $var}}');
340
assertIdent('this ${1:is ${2:nested with $var}}}');
341
assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');
342
});
343
344
test('Parser, choise marker', () => {
345
const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');
346
347
assert.strictEqual(placeholders.length, 1);
348
assert.ok(placeholders[0].choice instanceof Choice);
349
assert.ok(placeholders[0].children[0] instanceof Choice);
350
assert.strictEqual((<Choice>placeholders[0].children[0]).options.length, 3);
351
352
assertText('${1|one,two,three|}', 'one');
353
assertText('\\${1|one,two,three|}', '${1|one,two,three|}');
354
assertText('${1\\|one,two,three|}', '${1\\|one,two,three|}');
355
assertText('${1||}', '${1||}');
356
});
357
358
test('Backslash character escape in choice tabstop doesn\'t work #58494', function () {
359
360
const { placeholders } = new SnippetParser().parse('${1|\\,,},$,\\|,\\\\|}');
361
assert.strictEqual(placeholders.length, 1);
362
assert.ok(placeholders[0].choice instanceof Choice);
363
});
364
365
test('Parser, only textmate', () => {
366
const p = new SnippetParser();
367
assertMarker(p.parse('far{{}}boo'), Text);
368
assertMarker(p.parse('far{{123}}boo'), Text);
369
assertMarker(p.parse('far\\{{123}}boo'), Text);
370
371
assertMarker(p.parse('far$0boo'), Text, Placeholder, Text);
372
assertMarker(p.parse('far${123}boo'), Text, Placeholder, Text);
373
assertMarker(p.parse('far\\${123}boo'), Text);
374
});
375
376
test('Parser, real world', () => {
377
let marker = new SnippetParser().parse('console.warn(${1: $TM_SELECTED_TEXT })').children;
378
379
assert.strictEqual(marker[0].toString(), 'console.warn(');
380
assert.ok(marker[1] instanceof Placeholder);
381
assert.strictEqual(marker[2].toString(), ')');
382
383
const placeholder = <Placeholder>marker[1];
384
assert.strictEqual(placeholder.index, 1);
385
assert.strictEqual(placeholder.children.length, 3);
386
assert.ok(placeholder.children[0] instanceof Text);
387
assert.ok(placeholder.children[1] instanceof Variable);
388
assert.ok(placeholder.children[2] instanceof Text);
389
assert.strictEqual(placeholder.children[0].toString(), ' ');
390
assert.strictEqual(placeholder.children[1].toString(), '');
391
assert.strictEqual(placeholder.children[2].toString(), ' ');
392
393
const nestedVariable = <Variable>placeholder.children[1];
394
assert.strictEqual(nestedVariable.name, 'TM_SELECTED_TEXT');
395
assert.strictEqual(nestedVariable.children.length, 0);
396
397
marker = new SnippetParser().parse('$TM_SELECTED_TEXT').children;
398
assert.strictEqual(marker.length, 1);
399
assert.ok(marker[0] instanceof Variable);
400
});
401
402
test('Parser, transform example', () => {
403
const { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0');
404
405
//${1:name}
406
assert.ok(children[0] instanceof Placeholder);
407
assert.strictEqual(children[0].children.length, 1);
408
assert.strictEqual(children[0].children[0].toString(), 'name');
409
assert.strictEqual((<Placeholder>children[0]).transform, undefined);
410
411
// :
412
assert.ok(children[1] instanceof Text);
413
assert.strictEqual(children[1].toString(), ' : ');
414
415
//${2:type}
416
assert.ok(children[2] instanceof Placeholder);
417
assert.strictEqual(children[2].children.length, 1);
418
assert.strictEqual(children[2].children[0].toString(), 'type');
419
420
//${3/\\s:=(.*)/${1:+ :=}${1}/}
421
assert.ok(children[3] instanceof Placeholder);
422
assert.strictEqual(children[3].children.length, 0);
423
assert.notStrictEqual((<Placeholder>children[3]).transform, undefined);
424
const transform = (<Placeholder>children[3]).transform!;
425
assert.deepStrictEqual(transform.regexp, /\s:=(.*)/);
426
assert.strictEqual(transform.children.length, 2);
427
assert.ok(transform.children[0] instanceof FormatString);
428
assert.strictEqual((<FormatString>transform.children[0]).index, 1);
429
assert.strictEqual((<FormatString>transform.children[0]).ifValue, ' :=');
430
assert.ok(transform.children[1] instanceof FormatString);
431
assert.strictEqual((<FormatString>transform.children[1]).index, 1);
432
assert.ok(children[4] instanceof Text);
433
assert.strictEqual(children[4].toString(), ';\n');
434
435
});
436
437
// TODO @jrieken making this strictEqul causes circular json conversion errors
438
test('Parser, default placeholder values', () => {
439
440
assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder);
441
442
const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err}`, error:$1').children;
443
444
assert.strictEqual((<Placeholder>p1).index, 1);
445
assert.strictEqual((<Placeholder>p1).children.length, 1);
446
assert.strictEqual((<Text>(<Placeholder>p1).children[0]).toString(), 'err');
447
448
assert.strictEqual((<Placeholder>p2).index, 1);
449
assert.strictEqual((<Placeholder>p2).children.length, 1);
450
assert.strictEqual((<Text>(<Placeholder>p2).children[0]).toString(), 'err');
451
});
452
453
// TODO @jrieken making this strictEqul causes circular json conversion errors
454
test('Parser, default placeholder values and one transform', () => {
455
456
assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder);
457
458
const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children;
459
460
assert.strictEqual((<Placeholder>p3).index, 1);
461
assert.strictEqual((<Placeholder>p3).children.length, 1);
462
assert.strictEqual((<Text>(<Placeholder>p3).children[0]).toString(), 'err');
463
assert.strictEqual((<Placeholder>p3).transform, undefined);
464
465
assert.strictEqual((<Placeholder>p4).index, 1);
466
assert.strictEqual((<Placeholder>p4).children.length, 1);
467
assert.strictEqual((<Text>(<Placeholder>p4).children[0]).toString(), 'err');
468
assert.notStrictEqual((<Placeholder>p4).transform, undefined);
469
});
470
471
test('Repeated snippet placeholder should always inherit, #31040', function () {
472
assertText('${1:foo}-abc-$1', 'foo-abc-foo');
473
assertText('${1:foo}-abc-${1}', 'foo-abc-foo');
474
assertText('${1:foo}-abc-${1:bar}', 'foo-abc-foo');
475
assertText('${1}-abc-${1:foo}', 'foo-abc-foo');
476
});
477
478
test('backspace esapce in TM only, #16212', () => {
479
const actual = SnippetParser.asInsertText('Foo \\\\${abc}bar');
480
assert.strictEqual(actual, 'Foo \\bar');
481
});
482
483
test('colon as variable/placeholder value, #16717', () => {
484
let actual = SnippetParser.asInsertText('${TM_SELECTED_TEXT:foo:bar}');
485
assert.strictEqual(actual, 'foo:bar');
486
487
actual = SnippetParser.asInsertText('${1:foo:bar}');
488
assert.strictEqual(actual, 'foo:bar');
489
});
490
491
test('incomplete placeholder', () => {
492
assertTextAndMarker('${1:}', '', Placeholder);
493
});
494
495
test('marker#len', () => {
496
497
function assertLen(template: string, ...lengths: number[]): void {
498
const snippet = new SnippetParser().parse(template, true);
499
snippet.walk(m => {
500
const expected = lengths.shift();
501
assert.strictEqual(m.len(), expected);
502
return true;
503
});
504
assert.strictEqual(lengths.length, 0);
505
}
506
507
assertLen('text$0', 4, 0);
508
assertLen('$1text$0', 0, 4, 0);
509
assertLen('te$1xt$0', 2, 0, 2, 0);
510
assertLen('errorContext: `${1:err}`, error: $0', 15, 0, 3, 10, 0);
511
assertLen('errorContext: `${1:err}`, error: $1$0', 15, 0, 3, 10, 0, 3, 0);
512
assertLen('$TM_SELECTED_TEXT$0', 0, 0);
513
assertLen('${TM_SELECTED_TEXT:def}$0', 0, 3, 0);
514
});
515
516
test('parser, parent node', function () {
517
let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
518
519
assert.strictEqual(snippet.placeholders.length, 3);
520
let [first, second] = snippet.placeholders;
521
assert.strictEqual(first.index, 1);
522
assert.strictEqual(second.index, 2);
523
assert.ok(second.parent === first);
524
assert.ok(first.parent === snippet);
525
526
snippet = new SnippetParser().parse('${VAR:default${1:value}}$0', true);
527
assert.strictEqual(snippet.placeholders.length, 2);
528
[first] = snippet.placeholders;
529
assert.strictEqual(first.index, 1);
530
531
assert.ok(snippet.children[0] instanceof Variable);
532
assert.ok(first.parent === snippet.children[0]);
533
});
534
535
test('TextmateSnippet#enclosingPlaceholders', () => {
536
const snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
537
const [first, second] = snippet.placeholders;
538
539
assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []);
540
assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]);
541
});
542
543
test('TextmateSnippet#offset', () => {
544
let snippet = new SnippetParser().parse('te$1xt', true);
545
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
546
assert.strictEqual(snippet.offset(snippet.children[1]), 2);
547
assert.strictEqual(snippet.offset(snippet.children[2]), 2);
548
549
snippet = new SnippetParser().parse('${TM_SELECTED_TEXT:def}', true);
550
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
551
assert.strictEqual(snippet.offset((<Variable>snippet.children[0]).children[0]), 0);
552
553
// forgein marker
554
assert.strictEqual(snippet.offset(new Text('foo')), -1);
555
});
556
557
test('TextmateSnippet#placeholder', () => {
558
let snippet = new SnippetParser().parse('te$1xt$0', true);
559
let placeholders = snippet.placeholders;
560
assert.strictEqual(placeholders.length, 2);
561
562
snippet = new SnippetParser().parse('te$1xt$1$0', true);
563
placeholders = snippet.placeholders;
564
assert.strictEqual(placeholders.length, 3);
565
566
567
snippet = new SnippetParser().parse('te$1xt$2$0', true);
568
placeholders = snippet.placeholders;
569
assert.strictEqual(placeholders.length, 3);
570
571
snippet = new SnippetParser().parse('${1:bar${2:foo}bar}$0', true);
572
placeholders = snippet.placeholders;
573
assert.strictEqual(placeholders.length, 3);
574
});
575
576
test('TextmateSnippet#replace 1/2', function () {
577
const snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
578
579
assert.strictEqual(snippet.placeholders.length, 3);
580
const [, second] = snippet.placeholders;
581
assert.strictEqual(second.index, 2);
582
583
const enclosing = snippet.enclosingPlaceholders(second);
584
assert.strictEqual(enclosing.length, 1);
585
assert.strictEqual(enclosing[0].index, 1);
586
587
const nested = new SnippetParser().parse('ddd$1eee$0', true);
588
snippet.replace(second, nested.children);
589
590
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
591
assert.strictEqual(snippet.placeholders.length, 4);
592
assert.strictEqual(snippet.placeholders[0].index, 1);
593
assert.strictEqual(snippet.placeholders[1].index, 1);
594
assert.strictEqual(snippet.placeholders[2].index, 0);
595
assert.strictEqual(snippet.placeholders[3].index, 0);
596
597
const newEnclosing = snippet.enclosingPlaceholders(snippet.placeholders[1]);
598
assert.ok(newEnclosing[0] === snippet.placeholders[0]);
599
assert.strictEqual(newEnclosing.length, 1);
600
assert.strictEqual(newEnclosing[0].index, 1);
601
});
602
603
test('TextmateSnippet#replace 2/2', function () {
604
const snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
605
606
assert.strictEqual(snippet.placeholders.length, 3);
607
const [, second] = snippet.placeholders;
608
assert.strictEqual(second.index, 2);
609
610
const nested = new SnippetParser().parse('dddeee$0', true);
611
snippet.replace(second, nested.children);
612
613
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
614
assert.strictEqual(snippet.placeholders.length, 3);
615
});
616
617
test('Snippet order for placeholders, #28185', function () {
618
619
const _10 = new Placeholder(10);
620
const _2 = new Placeholder(2);
621
622
assert.strictEqual(Placeholder.compareByIndex(_10, _2), 1);
623
});
624
625
test('Maximum call stack size exceeded, #28983', function () {
626
new SnippetParser().parse('${1:${foo:${1}}}');
627
});
628
629
test('Snippet can freeze the editor, #30407', function () {
630
631
const seen = new Set<Marker>();
632
633
seen.clear();
634
new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend').walk(marker => {
635
assert.ok(!seen.has(marker));
636
seen.add(marker);
637
return true;
638
});
639
640
seen.clear();
641
new SnippetParser().parse('${1:${FOO:abc$1def}}').walk(marker => {
642
assert.ok(!seen.has(marker));
643
seen.add(marker);
644
return true;
645
});
646
});
647
648
test('Snippets: make parser ignore `${0|choice|}`, #31599', function () {
649
assertTextAndMarker('${0|foo,bar|}', '${0|foo,bar|}', Text);
650
assertTextAndMarker('${1|foo,bar|}', 'foo', Placeholder);
651
});
652
653
654
test('Transform -> FormatString#resolve', function () {
655
656
// shorthand functions
657
assert.strictEqual(new FormatString(1, 'upcase').resolve('foo'), 'FOO');
658
assert.strictEqual(new FormatString(1, 'downcase').resolve('FOO'), 'foo');
659
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar');
660
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat');
661
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo');
662
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo');
663
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('snake_AndPascalCase'), 'SnakeAndPascalCase');
664
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('kebab-AndPascalCase'), 'KebabAndPascalCase');
665
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('_justPascalCase'), 'JustPascalCase');
666
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo');
667
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo');
668
assert.strictEqual(new FormatString(1, 'camelcase').resolve('snake_AndCamelCase'), 'snakeAndCamelCase');
669
assert.strictEqual(new FormatString(1, 'camelcase').resolve('kebab-AndCamelCase'), 'kebabAndCamelCase');
670
assert.strictEqual(new FormatString(1, 'camelcase').resolve('_JustCamelCase'), 'justCamelCase');
671
assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input');
672
673
// if
674
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(undefined), '');
675
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(''), '');
676
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve('bar'), 'foo');
677
678
// else
679
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(undefined), 'foo');
680
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(''), 'foo');
681
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve('bar'), 'bar');
682
683
// if-else
684
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(undefined), 'foo');
685
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(''), 'foo');
686
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar');
687
});
688
689
test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function () {
690
const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"');
691
assert.strictEqual(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"');
692
});
693
694
test('[BUG] HTML attribute suggestions: Snippet session does not have end-position set, #33147', function () {
695
696
const { placeholders } = new SnippetParser().parse('src="$1"', true);
697
const [first, second] = placeholders;
698
699
assert.strictEqual(placeholders.length, 2);
700
assert.strictEqual(first.index, 1);
701
assert.strictEqual(second.index, 0);
702
703
});
704
705
test('Snippet optional transforms are not applied correctly when reusing the same variable, #37702', function () {
706
707
const transform = new Transform();
708
transform.appendChild(new FormatString(1, 'upcase'));
709
transform.appendChild(new FormatString(2, 'upcase'));
710
transform.regexp = /^(.)|-(.)/g;
711
712
assert.strictEqual(transform.resolve('my-file-name'), 'MyFileName');
713
714
const clone = transform.clone();
715
assert.strictEqual(clone.resolve('my-file-name'), 'MyFileName');
716
});
717
718
test('problem with snippets regex #40570', function () {
719
720
const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}');
721
assertMarker(snippet, Variable);
722
});
723
724
test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function () {
725
const transform = new Transform();
726
transform.appendChild(new Text('bar'));
727
transform.regexp = new RegExp('foo', 'gi');
728
assert.strictEqual(transform.toTextmateString(), '/foo/bar/ig');
729
});
730
731
test('Snippet parser freeze #53144', function () {
732
const snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}');
733
assertMarker(snippet, Placeholder);
734
});
735
736
test('snippets variable not resolved in JSON proposal #52931', function () {
737
assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder);
738
});
739
740
test('Mirroring sequence of nested placeholders not selected properly on backjumping #58736', function () {
741
const snippet = new SnippetParser().parse('${3:nest1 ${1:nest2 ${2:nest3}}} $3');
742
assert.strictEqual(snippet.children.length, 3);
743
assert.ok(snippet.children[0] instanceof Placeholder);
744
assert.ok(snippet.children[1] instanceof Text);
745
assert.ok(snippet.children[2] instanceof Placeholder);
746
747
function assertParent(marker: Marker) {
748
marker.children.forEach(assertParent);
749
if (!(marker instanceof Placeholder)) {
750
return;
751
}
752
let found = false;
753
let m: Marker = marker;
754
while (m && !found) {
755
if (m.parent === snippet) {
756
found = true;
757
}
758
m = m.parent;
759
}
760
assert.ok(found);
761
}
762
const [, , clone] = snippet.children;
763
assertParent(clone);
764
});
765
766
test('Backspace can\'t be escaped in snippet variable transforms #65412', function () {
767
768
const snippet = new SnippetParser().parse('namespace ${TM_DIRECTORY/[\\/]/\\\\/g};');
769
assertMarker(snippet, Text, Variable, Text);
770
});
771
772
test('Snippet cannot escape closing bracket inside conditional insertion variable replacement #78883', function () {
773
774
const snippet = new SnippetParser().parse('${TM_DIRECTORY/(.+)/${1:+import { hello \\} from world}/}');
775
const variable = <Variable>snippet.children[0];
776
assert.strictEqual(snippet.children.length, 1);
777
assert.ok(variable instanceof Variable);
778
assert.ok(variable.transform);
779
assert.strictEqual(variable.transform.children.length, 1);
780
assert.ok(variable.transform.children[0] instanceof FormatString);
781
assert.strictEqual((<FormatString>variable.transform.children[0]).ifValue, 'import { hello } from world');
782
assert.strictEqual((<FormatString>variable.transform.children[0]).elseValue, undefined);
783
});
784
785
test('Snippet escape backslashes inside conditional insertion variable replacement #80394', function () {
786
787
const snippet = new SnippetParser().parse('${CURRENT_YEAR/(.+)/${1:+\\\\}/}');
788
const variable = <Variable>snippet.children[0];
789
assert.strictEqual(snippet.children.length, 1);
790
assert.ok(variable instanceof Variable);
791
assert.ok(variable.transform);
792
assert.strictEqual(variable.transform.children.length, 1);
793
assert.ok(variable.transform.children[0] instanceof FormatString);
794
assert.strictEqual((<FormatString>variable.transform.children[0]).ifValue, '\\');
795
assert.strictEqual((<FormatString>variable.transform.children[0]).elseValue, undefined);
796
});
797
798
test('Snippet placeholder empty right after expansion #152553', function () {
799
800
const snippet = new SnippetParser().parse('${1:prog}: ${2:$1.cc} - $2');
801
const actual = snippet.toString();
802
assert.strictEqual(actual, 'prog: prog.cc - prog.cc');
803
804
const snippet2 = new SnippetParser().parse('${1:prog}: ${3:${2:$1.cc}.33} - $2 $3');
805
const actual2 = snippet2.toString();
806
assert.strictEqual(actual2, 'prog: prog.cc.33 - prog.cc prog.cc.33');
807
808
// cyclic references of placeholders
809
const snippet3 = new SnippetParser().parse('${1:$2.one} <> ${2:$1.two}');
810
const actual3 = snippet3.toString();
811
assert.strictEqual(actual3, '.two.one.two.one <> .one.two.one.two');
812
});
813
814
test('Snippet choices are incorrectly escaped/applied #180132', function () {
815
assertTextAndMarker('${1|aaa$aaa|}bbb\\$bbb', 'aaa$aaabbb$bbb', Placeholder, Text);
816
});
817
});
818
819