Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts
5310 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 * as sinon from 'sinon';
7
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { sep } from '../../../../../base/common/path.js';
9
import { isWindows } from '../../../../../base/common/platform.js';
10
import { extUriBiasedIgnorePathCase } from '../../../../../base/common/resources.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { mock } from '../../../../../base/test/common/mock.js';
13
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
14
import { Selection } from '../../../../common/core/selection.js';
15
import { TextModel } from '../../../../common/model/textModel.js';
16
import { SnippetParser, Variable, VariableResolver } from '../../browser/snippetParser.js';
17
import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from '../../browser/snippetVariables.js';
18
import { createTextModel } from '../../../../test/common/testTextModel.js';
19
import { ILabelService } from '../../../../../platform/label/common/label.js';
20
import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';
21
import { Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js';
22
import { toWorkspaceFolders } from '../../../../../platform/workspaces/common/workspaces.js';
23
24
suite('Snippet Variables Resolver', function () {
25
26
27
const labelService = new class extends mock<ILabelService>() {
28
override getUriLabel(uri: URI) {
29
return uri.fsPath;
30
}
31
};
32
33
let model: TextModel;
34
let resolver: VariableResolver;
35
36
setup(function () {
37
model = createTextModel([
38
'this is line one',
39
'this is line two',
40
' this is line three'
41
].join('\n'), undefined, undefined, URI.parse('file:///foo/files/text.txt'));
42
43
resolver = new CompositeSnippetVariableResolver([
44
new ModelBasedVariableResolver(labelService, model),
45
new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0, undefined),
46
]);
47
});
48
49
teardown(function () {
50
model.dispose();
51
});
52
53
ensureNoDisposablesAreLeakedInTestSuite();
54
55
56
function assertVariableResolve(resolver: VariableResolver, varName: string, expected?: string) {
57
const snippet = new SnippetParser().parse(`$${varName}`);
58
const variable = <Variable>snippet.children[0];
59
variable.resolve(resolver);
60
if (variable.children.length === 0) {
61
assert.strictEqual(undefined, expected);
62
} else {
63
assert.strictEqual(variable.toString(), expected);
64
}
65
}
66
67
test('editor variables, basics', function () {
68
assertVariableResolve(resolver, 'TM_FILENAME', 'text.txt');
69
assertVariableResolve(resolver, 'something', undefined);
70
});
71
72
test('editor variables, file/dir', function () {
73
74
const disposables = new DisposableStore();
75
76
assertVariableResolve(resolver, 'TM_FILENAME', 'text.txt');
77
if (!isWindows) {
78
assertVariableResolve(resolver, 'TM_DIRECTORY', '/foo/files');
79
assertVariableResolve(resolver, 'TM_DIRECTORY_BASE', 'files');
80
assertVariableResolve(resolver, 'TM_FILEPATH', '/foo/files/text.txt');
81
}
82
83
resolver = new ModelBasedVariableResolver(
84
labelService,
85
disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))
86
);
87
assertVariableResolve(resolver, 'TM_FILENAME', 'ghi');
88
if (!isWindows) {
89
assertVariableResolve(resolver, 'TM_DIRECTORY', '/abc/def');
90
assertVariableResolve(resolver, 'TM_DIRECTORY_BASE', 'def');
91
assertVariableResolve(resolver, 'TM_FILEPATH', '/abc/def/ghi');
92
}
93
94
resolver = new ModelBasedVariableResolver(
95
labelService,
96
disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:fff.ts')))
97
);
98
assertVariableResolve(resolver, 'TM_DIRECTORY', '');
99
assertVariableResolve(resolver, 'TM_DIRECTORY_BASE', '');
100
assertVariableResolve(resolver, 'TM_FILEPATH', 'fff.ts');
101
102
disposables.dispose();
103
});
104
105
test('Path delimiters in code snippet variables aren\'t specific to remote OS #76840', function () {
106
107
const labelService = new class extends mock<ILabelService>() {
108
override getUriLabel(uri: URI) {
109
return uri.fsPath.replace(/\/|\\/g, '|');
110
}
111
};
112
113
const model = createTextModel([].join('\n'), undefined, undefined, URI.parse('foo:///foo/files/text.txt'));
114
115
const resolver = new CompositeSnippetVariableResolver([new ModelBasedVariableResolver(labelService, model)]);
116
117
assertVariableResolve(resolver, 'TM_FILEPATH', '|foo|files|text.txt');
118
119
model.dispose();
120
});
121
122
test('editor variables, selection', function () {
123
124
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0, undefined);
125
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');
126
assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two');
127
assertVariableResolve(resolver, 'TM_LINE_INDEX', '1');
128
assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2');
129
assertVariableResolve(resolver, 'CURSOR_INDEX', '0');
130
assertVariableResolve(resolver, 'CURSOR_NUMBER', '1');
131
132
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 4, undefined);
133
assertVariableResolve(resolver, 'CURSOR_INDEX', '4');
134
assertVariableResolve(resolver, 'CURSOR_NUMBER', '5');
135
136
resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0, undefined);
137
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');
138
assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one');
139
assertVariableResolve(resolver, 'TM_LINE_INDEX', '0');
140
assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1');
141
142
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0, undefined);
143
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined);
144
145
assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this');
146
147
resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0, undefined);
148
assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined);
149
150
});
151
152
test('TextmateSnippet, resolve variable', function () {
153
const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true);
154
assert.strictEqual(snippet.toString(), '""');
155
snippet.resolveVariables(resolver);
156
assert.strictEqual(snippet.toString(), '"this"');
157
158
});
159
160
test('TextmateSnippet, resolve variable with default', function () {
161
const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true);
162
assert.strictEqual(snippet.toString(), '"foo"');
163
snippet.resolveVariables(resolver);
164
assert.strictEqual(snippet.toString(), '"this"');
165
});
166
167
test('More useful environment variables for snippets, #32737', function () {
168
169
const disposables = new DisposableStore();
170
171
assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'text');
172
173
resolver = new ModelBasedVariableResolver(
174
labelService,
175
disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))
176
);
177
assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'ghi');
178
179
resolver = new ModelBasedVariableResolver(
180
labelService,
181
disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:.git')))
182
);
183
assertVariableResolve(resolver, 'TM_FILENAME_BASE', '.git');
184
185
resolver = new ModelBasedVariableResolver(
186
labelService,
187
disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:foo.')))
188
);
189
assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'foo');
190
191
disposables.dispose();
192
});
193
194
195
function assertVariableResolve2(input: string, expected: string, varValue?: string) {
196
const snippet = new SnippetParser().parse(input)
197
.resolveVariables({ resolve(variable) { return varValue || variable.name; } });
198
199
const actual = snippet.toString();
200
assert.strictEqual(actual, expected);
201
}
202
203
test('Variable Snippet Transform', function () {
204
205
const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true);
206
snippet.resolveVariables(resolver);
207
assert.strictEqual(snippet.toString(), 'name=text');
208
209
assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var');
210
assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t');
211
assertVariableResolve2('${Foo/(.*)/${1:+Bar}/img}', 'Bar');
212
213
//https://github.com/microsoft/vscode/issues/33162
214
assertVariableResolve2('export default class ${TM_FILENAME/(\\w+)\\.js/$1/g}', 'export default class FooFile', 'FooFile.js');
215
216
assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/g}', 'FARbarFARbar'); // global
217
assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/}', 'FARbarfoobar'); // first match
218
assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', 'foobarfoobar'); // no match, no else
219
// assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', ''); // no match
220
221
assertVariableResolve2('${foobarfoobar/(foo)/${2:+FAR}/g}', 'barbar'); // bad group reference
222
});
223
224
test('Snippet transforms do not handle regex with alternatives or optional matches, #36089', function () {
225
226
assertVariableResolve2(
227
'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',
228
'MyClass',
229
'my-class.js'
230
);
231
232
// no hyphens
233
assertVariableResolve2(
234
'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',
235
'Myclass',
236
'myclass.js'
237
);
238
239
// none matching suffix
240
assertVariableResolve2(
241
'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',
242
'Myclass.foo',
243
'myclass.foo'
244
);
245
246
// more than one hyphen
247
assertVariableResolve2(
248
'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',
249
'ThisIsAFile',
250
'this-is-a-file.js'
251
);
252
253
// KEBAB CASE
254
assertVariableResolve2(
255
'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',
256
'capital-case',
257
'CapitalCase'
258
);
259
260
assertVariableResolve2(
261
'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',
262
'capital-case-more',
263
'CapitalCaseMore'
264
);
265
});
266
267
test('Add variable to insert value from clipboard to a snippet #40153', function () {
268
269
assertVariableResolve(new ClipboardBasedVariableResolver(() => undefined, 1, 0, true), 'CLIPBOARD', undefined);
270
271
assertVariableResolve(new ClipboardBasedVariableResolver(() => null!, 1, 0, true), 'CLIPBOARD', undefined);
272
273
assertVariableResolve(new ClipboardBasedVariableResolver(() => '', 1, 0, true), 'CLIPBOARD', undefined);
274
275
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'CLIPBOARD', 'foo');
276
277
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'foo', undefined);
278
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'cLIPBOARD', undefined);
279
});
280
281
test('Add variable to insert value from clipboard to a snippet #40153, 2', function () {
282
283
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1', 1, 2, true), 'CLIPBOARD', 'line1');
284
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2\nline3', 1, 2, true), 'CLIPBOARD', 'line1\nline2\nline3');
285
286
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 1, 2, true), 'CLIPBOARD', 'line2');
287
resolver = new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true);
288
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true), 'CLIPBOARD', 'line1');
289
290
assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, false), 'CLIPBOARD', 'line1\nline2');
291
});
292
293
294
function assertVariableResolve3(resolver: VariableResolver, varName: string) {
295
const snippet = new SnippetParser().parse(`$${varName}`);
296
const variable = <Variable>snippet.children[0];
297
298
assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`);
299
}
300
301
test('Add time variables for snippets #41631, #43140', function () {
302
303
const resolver = new TimeBasedVariableResolver;
304
305
assertVariableResolve3(resolver, 'CURRENT_YEAR');
306
assertVariableResolve3(resolver, 'CURRENT_YEAR_SHORT');
307
assertVariableResolve3(resolver, 'CURRENT_MONTH');
308
assertVariableResolve3(resolver, 'CURRENT_DATE');
309
assertVariableResolve3(resolver, 'CURRENT_HOUR');
310
assertVariableResolve3(resolver, 'CURRENT_MINUTE');
311
assertVariableResolve3(resolver, 'CURRENT_SECOND');
312
assertVariableResolve3(resolver, 'CURRENT_DAY_NAME');
313
assertVariableResolve3(resolver, 'CURRENT_DAY_NAME_SHORT');
314
assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME');
315
assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME_SHORT');
316
assertVariableResolve3(resolver, 'CURRENT_SECONDS_UNIX');
317
assertVariableResolve3(resolver, 'CURRENT_TIMEZONE_OFFSET');
318
});
319
320
test('Time-based snippet variables resolve to the same values even as time progresses', async function () {
321
const snippetText = `
322
$CURRENT_YEAR
323
$CURRENT_YEAR_SHORT
324
$CURRENT_MONTH
325
$CURRENT_DATE
326
$CURRENT_HOUR
327
$CURRENT_MINUTE
328
$CURRENT_SECOND
329
$CURRENT_DAY_NAME
330
$CURRENT_DAY_NAME_SHORT
331
$CURRENT_MONTH_NAME
332
$CURRENT_MONTH_NAME_SHORT
333
$CURRENT_SECONDS_UNIX
334
$CURRENT_TIMEZONE_OFFSET
335
`;
336
337
const clock = sinon.useFakeTimers();
338
try {
339
const resolver = new TimeBasedVariableResolver;
340
341
const firstResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);
342
clock.tick((365 * 24 * 3600 * 1000) + (24 * 3600 * 1000) + (3661 * 1000)); // 1 year + 1 day + 1 hour + 1 minute + 1 second
343
const secondResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);
344
345
assert.strictEqual(firstResolve.toString(), secondResolve.toString(), `Time-based snippet variables resolved differently`);
346
} finally {
347
clock.restore();
348
}
349
});
350
351
test('creating snippet - format-condition doesn\'t work #53617', function () {
352
353
const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true);
354
snippet.resolveVariables({ resolve() { return '10'; } });
355
assert.strictEqual(snippet.toString(), 'It is line 10');
356
357
snippet.resolveVariables({ resolve() { return '11'; } });
358
assert.strictEqual(snippet.toString(), 'It is not line 10');
359
});
360
361
test('Add workspace name and folder variables for snippets #68261', function () {
362
363
let workspace: IWorkspace;
364
const workspaceService = new class implements IWorkspaceContextService {
365
declare readonly _serviceBrand: undefined;
366
_throw = () => { throw new Error(); };
367
onDidChangeWorkbenchState = this._throw;
368
onDidChangeWorkspaceName = this._throw;
369
onWillChangeWorkspaceFolders = this._throw;
370
onDidChangeWorkspaceFolders = this._throw;
371
getCompleteWorkspace = this._throw;
372
getWorkspace(): IWorkspace { return workspace; }
373
getWorkbenchState = this._throw;
374
getWorkspaceFolder = this._throw;
375
isCurrentWorkspace = this._throw;
376
isInsideWorkspace = this._throw;
377
};
378
379
const resolver = new WorkspaceBasedVariableResolver(workspaceService);
380
381
// empty workspace
382
workspace = new Workspace('');
383
assertVariableResolve(resolver, 'WORKSPACE_NAME', undefined);
384
assertVariableResolve(resolver, 'WORKSPACE_FOLDER', undefined);
385
386
// single folder workspace without config
387
workspace = new Workspace('', [toWorkspaceFolder(URI.file('/folderName'))]);
388
assertVariableResolve(resolver, 'WORKSPACE_NAME', 'folderName');
389
if (!isWindows) {
390
assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/folderName');
391
}
392
393
// workspace with config
394
const workspaceConfigPath = URI.file('testWorkspace.code-workspace');
395
workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath);
396
assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace');
397
if (!isWindows) {
398
assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/');
399
}
400
});
401
402
test('Add RELATIVE_FILEPATH snippet variable #114208', function () {
403
404
let resolver: VariableResolver;
405
406
// Mock a label service (only coded for file uris)
407
const workspaceLabelService = ((rootPath: string): ILabelService => {
408
const labelService = new class extends mock<ILabelService>() {
409
override getUriLabel(uri: URI, options: { relative?: boolean } = {}) {
410
const rootFsPath = URI.file(rootPath).fsPath + sep;
411
const fsPath = uri.fsPath;
412
if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) {
413
return fsPath.substring(rootFsPath.length);
414
}
415
return fsPath;
416
}
417
};
418
return labelService;
419
});
420
421
const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt'));
422
423
// empty workspace
424
resolver = new ModelBasedVariableResolver(
425
workspaceLabelService(''),
426
model
427
);
428
429
if (!isWindows) {
430
assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt');
431
} else {
432
assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt');
433
}
434
435
// single folder workspace
436
resolver = new ModelBasedVariableResolver(
437
workspaceLabelService('/foo'),
438
model
439
);
440
if (!isWindows) {
441
assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt');
442
} else {
443
assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt');
444
}
445
446
model.dispose();
447
});
448
});
449
450