Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/node/extHostSearch.test.ts
5222 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 assert from 'assert';
7
import { mapArrayOrNot } from '../../../../base/common/arrays.js';
8
import { timeout } from '../../../../base/common/async.js';
9
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
10
import { isCancellationError } from '../../../../base/common/errors.js';
11
import { revive } from '../../../../base/common/marshalling.js';
12
import { joinPath } from '../../../../base/common/resources.js';
13
import { URI, UriComponents } from '../../../../base/common/uri.js';
14
import * as pfs from '../../../../base/node/pfs.js';
15
import { mock } from '../../../../base/test/common/mock.js';
16
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
17
import { NullLogService } from '../../../../platform/log/common/log.js';
18
import { MainContext, MainThreadSearchShape } from '../../common/extHost.protocol.js';
19
import { ExtHostConfigProvider, IExtHostConfiguration } from '../../common/extHostConfiguration.js';
20
import { IExtHostInitDataService } from '../../common/extHostInitDataService.js';
21
import { Range } from '../../common/extHostTypes.js';
22
import { URITransformerService } from '../../common/extHostUriTransformerService.js';
23
import { NativeExtHostSearch } from '../../node/extHostSearch.js';
24
import { TestRPCProtocol } from '../common/testRPCProtocol.js';
25
import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from '../../../services/search/common/search.js';
26
import { TextSearchManager } from '../../../services/search/common/textSearchManager.js';
27
import { NativeTextSearchManager } from '../../../services/search/node/textSearchManager.js';
28
import type * as vscode from 'vscode';
29
import { AISearchKeyword } from '../../../services/search/common/searchExtTypes.js';
30
31
let rpcProtocol: TestRPCProtocol;
32
let extHostSearch: NativeExtHostSearch;
33
34
let mockMainThreadSearch: MockMainThreadSearch;
35
class MockMainThreadSearch implements MainThreadSearchShape {
36
lastHandle!: number;
37
38
results: Array<UriComponents | IRawFileMatch2> = [];
39
40
keywords: Array<AISearchKeyword> = [];
41
42
$registerFileSearchProvider(handle: number, scheme: string): void {
43
this.lastHandle = handle;
44
}
45
46
$registerTextSearchProvider(handle: number, scheme: string): void {
47
this.lastHandle = handle;
48
}
49
50
$registerAITextSearchProvider(handle: number, scheme: string): void {
51
this.lastHandle = handle;
52
}
53
54
$unregisterProvider(handle: number): void {
55
}
56
57
$handleFileMatch(handle: number, session: number, data: UriComponents[]): void {
58
this.results.push(...data);
59
}
60
61
$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void {
62
this.results.push(...data);
63
}
64
65
$handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void {
66
this.keywords.push(data);
67
}
68
69
$handleTelemetry(eventName: string, data: any): void {
70
}
71
72
dispose() {
73
}
74
}
75
76
let mockPFS: Partial<typeof pfs>;
77
78
function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch {
79
return !!(<vscode.TextSearchMatch>data).preview;
80
}
81
82
suite('ExtHostSearch', () => {
83
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
84
85
async function registerTestTextSearchProvider(provider: vscode.TextSearchProvider, scheme = 'file'): Promise<void> {
86
disposables.add(extHostSearch.registerTextSearchProviderOld(scheme, provider));
87
await rpcProtocol.sync();
88
}
89
90
async function registerTestFileSearchProvider(provider: vscode.FileSearchProvider, scheme = 'file'): Promise<void> {
91
disposables.add(extHostSearch.registerFileSearchProviderOld(scheme, provider));
92
await rpcProtocol.sync();
93
}
94
95
async function runFileSearch(query: IFileQuery, cancel = false): Promise<{ results: URI[]; stats: ISearchCompleteStats }> {
96
let stats: ISearchCompleteStats;
97
try {
98
const cancellation = new CancellationTokenSource();
99
const p = extHostSearch.$provideFileSearchResults(mockMainThreadSearch.lastHandle, 0, query, cancellation.token);
100
if (cancel) {
101
await timeout(0);
102
cancellation.cancel();
103
}
104
105
stats = await p;
106
} catch (err) {
107
if (!isCancellationError(err)) {
108
await rpcProtocol.sync();
109
throw err;
110
}
111
}
112
113
await rpcProtocol.sync();
114
return {
115
results: (<UriComponents[]>mockMainThreadSearch.results).map(r => URI.revive(r)),
116
stats: stats!
117
};
118
}
119
120
async function runTextSearch(query: ITextQuery): Promise<{ results: IFileMatch[]; stats: ISearchCompleteStats }> {
121
let stats: ISearchCompleteStats;
122
try {
123
const cancellation = new CancellationTokenSource();
124
const p = extHostSearch.$provideTextSearchResults(mockMainThreadSearch.lastHandle, 0, query, cancellation.token);
125
126
stats = await p;
127
} catch (err) {
128
if (!isCancellationError(err)) {
129
await rpcProtocol.sync();
130
throw err;
131
}
132
}
133
134
await rpcProtocol.sync();
135
const results: IFileMatch[] = revive(<IRawFileMatch2[]>mockMainThreadSearch.results);
136
137
return { results, stats: stats! };
138
}
139
140
setup(() => {
141
rpcProtocol = new TestRPCProtocol();
142
143
mockMainThreadSearch = new MockMainThreadSearch();
144
const logService = new NullLogService();
145
146
rpcProtocol.set(MainContext.MainThreadSearch, mockMainThreadSearch);
147
148
mockPFS = {};
149
extHostSearch = disposables.add(new class extends NativeExtHostSearch {
150
constructor() {
151
super(
152
rpcProtocol,
153
new class extends mock<IExtHostInitDataService>() { override remote = { isRemote: false, authority: undefined, connectionData: null }; },
154
new URITransformerService(null),
155
new class extends mock<IExtHostConfiguration>() {
156
override async getConfigProvider(): Promise<ExtHostConfigProvider> {
157
return {
158
onDidChangeConfiguration(_listener: (event: vscode.ConfigurationChangeEvent) => void) { },
159
getConfiguration(): vscode.WorkspaceConfiguration {
160
return {
161
get() { },
162
has() {
163
return false;
164
},
165
inspect() {
166
return undefined;
167
},
168
async update() { }
169
};
170
},
171
172
} as ExtHostConfigProvider;
173
}
174
},
175
logService
176
);
177
// eslint-disable-next-line local/code-no-any-casts
178
this._pfs = mockPFS as any;
179
}
180
181
protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider2): TextSearchManager {
182
return new NativeTextSearchManager(query, provider, this._pfs);
183
}
184
});
185
});
186
187
teardown(() => {
188
return rpcProtocol.sync();
189
});
190
191
const rootFolderA = URI.file('/foo/bar1');
192
const rootFolderB = URI.file('/foo/bar2');
193
const fancyScheme = 'fancy';
194
const fancySchemeFolderA = URI.from({ scheme: fancyScheme, path: '/project/folder1' });
195
196
suite('File:', () => {
197
198
function getSimpleQuery(filePattern = ''): IFileQuery {
199
return {
200
type: QueryType.File,
201
202
filePattern,
203
folderQueries: [
204
{ folder: rootFolderA }
205
]
206
};
207
}
208
209
function compareURIs(actual: URI[], expected: URI[]) {
210
const sortAndStringify = (arr: URI[]) => arr.sort().map(u => u.toString());
211
212
assert.deepStrictEqual(
213
sortAndStringify(actual),
214
sortAndStringify(expected));
215
}
216
217
test('no results', async () => {
218
await registerTestFileSearchProvider({
219
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
220
return Promise.resolve(null!);
221
}
222
});
223
224
const { results, stats } = await runFileSearch(getSimpleQuery());
225
assert(!stats.limitHit);
226
assert(!results.length);
227
});
228
229
test('simple results', async () => {
230
const reportedResults = [
231
joinPath(rootFolderA, 'file1.ts'),
232
joinPath(rootFolderA, 'file2.ts'),
233
joinPath(rootFolderA, 'subfolder/file3.ts')
234
];
235
236
await registerTestFileSearchProvider({
237
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
238
return Promise.resolve(reportedResults);
239
}
240
});
241
242
const { results, stats } = await runFileSearch(getSimpleQuery());
243
assert(!stats.limitHit);
244
assert.strictEqual(results.length, 3);
245
compareURIs(results, reportedResults);
246
});
247
248
test('Search canceled', async () => {
249
let cancelRequested = false;
250
await registerTestFileSearchProvider({
251
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
252
253
return new Promise((resolve, reject) => {
254
function onCancel() {
255
cancelRequested = true;
256
257
resolve([joinPath(options.folder, 'file1.ts')]); // or reject or nothing?
258
}
259
260
if (token.isCancellationRequested) {
261
onCancel();
262
} else {
263
disposables.add(token.onCancellationRequested(() => onCancel()));
264
}
265
});
266
}
267
});
268
269
const { results } = await runFileSearch(getSimpleQuery(), true);
270
assert(cancelRequested);
271
assert(!results.length);
272
});
273
274
test('session cancellation should work', async () => {
275
let numSessionCancelled = 0;
276
const disposables: (vscode.Disposable | undefined)[] = [];
277
await registerTestFileSearchProvider({
278
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
279
280
disposables.push(options.session?.onCancellationRequested(() => {
281
numSessionCancelled++;
282
}));
283
284
return Promise.resolve([]);
285
}
286
});
287
288
289
await runFileSearch({ ...getSimpleQuery(), cacheKey: '1' }, true);
290
await runFileSearch({ ...getSimpleQuery(), cacheKey: '2' }, true);
291
extHostSearch.$clearCache('1');
292
assert.strictEqual(numSessionCancelled, 1);
293
disposables.forEach(d => d?.dispose());
294
});
295
296
test('provider returns null', async () => {
297
await registerTestFileSearchProvider({
298
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
299
return null!;
300
}
301
});
302
303
try {
304
await runFileSearch(getSimpleQuery());
305
assert(false, 'Expected to fail');
306
} catch {
307
// Expected to throw
308
}
309
});
310
311
test('all provider calls get global include/excludes', async () => {
312
await registerTestFileSearchProvider({
313
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
314
assert(options.excludes.length === 2 && options.includes.length === 2, 'Missing global include/excludes');
315
return Promise.resolve(null!);
316
}
317
});
318
319
const query: ISearchQuery = {
320
type: QueryType.File,
321
322
filePattern: '',
323
includePattern: {
324
'foo': true,
325
'bar': true
326
},
327
excludePattern: {
328
'something': true,
329
'else': true
330
},
331
folderQueries: [
332
{ folder: rootFolderA },
333
{ folder: rootFolderB }
334
]
335
};
336
337
await runFileSearch(query);
338
});
339
340
test('global/local include/excludes combined', async () => {
341
await registerTestFileSearchProvider({
342
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
343
if (options.folder.toString() === rootFolderA.toString()) {
344
assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);
345
assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);
346
} else {
347
assert.deepStrictEqual(options.includes.sort(), ['*.ts']);
348
assert.deepStrictEqual(options.excludes.sort(), ['*.js']);
349
}
350
351
return Promise.resolve(null!);
352
}
353
});
354
355
const query: ISearchQuery = {
356
type: QueryType.File,
357
358
filePattern: '',
359
includePattern: {
360
'*.ts': true
361
},
362
excludePattern: {
363
'*.js': true
364
},
365
folderQueries: [
366
{
367
folder: rootFolderA,
368
includePattern: {
369
'foo': true
370
},
371
excludePattern: [{
372
pattern: {
373
'bar': true
374
}
375
}]
376
},
377
{ folder: rootFolderB }
378
]
379
};
380
381
await runFileSearch(query);
382
});
383
384
test('include/excludes resolved correctly', async () => {
385
await registerTestFileSearchProvider({
386
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
387
assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);
388
assert.deepStrictEqual(options.excludes.sort(), []);
389
390
return Promise.resolve(null!);
391
}
392
});
393
394
const query: ISearchQuery = {
395
type: QueryType.File,
396
397
filePattern: '',
398
includePattern: {
399
'*.ts': true,
400
'*.jsx': false
401
},
402
excludePattern: {
403
'*.js': true,
404
'*.tsx': false
405
},
406
folderQueries: [
407
{
408
folder: rootFolderA,
409
includePattern: {
410
'*.jsx': true
411
},
412
excludePattern: [{
413
pattern: {
414
'*.js': false
415
}
416
}]
417
}
418
]
419
};
420
421
await runFileSearch(query);
422
});
423
424
test('basic sibling exclude clause', async () => {
425
const reportedResults = [
426
'file1.ts',
427
'file1.js',
428
];
429
430
await registerTestFileSearchProvider({
431
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
432
return Promise.resolve(reportedResults
433
.map(relativePath => joinPath(options.folder, relativePath)));
434
}
435
});
436
437
const query: ISearchQuery = {
438
type: QueryType.File,
439
440
filePattern: '',
441
excludePattern: {
442
'*.js': {
443
when: '$(basename).ts'
444
}
445
},
446
folderQueries: [
447
{ folder: rootFolderA }
448
]
449
};
450
451
const { results } = await runFileSearch(query);
452
compareURIs(
453
results,
454
[
455
joinPath(rootFolderA, 'file1.ts')
456
]);
457
});
458
459
// https://github.com/microsoft/vscode-remotehub/issues/255
460
test('include, sibling exclude, and subfolder', async () => {
461
const reportedResults = [
462
'foo/file1.ts',
463
'foo/file1.js',
464
];
465
466
await registerTestFileSearchProvider({
467
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
468
return Promise.resolve(reportedResults
469
.map(relativePath => joinPath(options.folder, relativePath)));
470
}
471
});
472
473
const query: ISearchQuery = {
474
type: QueryType.File,
475
476
filePattern: '',
477
includePattern: { '**/*.ts': true },
478
excludePattern: {
479
'*.js': {
480
when: '$(basename).ts'
481
}
482
},
483
folderQueries: [
484
{ folder: rootFolderA }
485
]
486
};
487
488
const { results } = await runFileSearch(query);
489
compareURIs(
490
results,
491
[
492
joinPath(rootFolderA, 'foo/file1.ts')
493
]);
494
});
495
496
test('multiroot sibling exclude clause', async () => {
497
498
await registerTestFileSearchProvider({
499
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
500
let reportedResults: URI[];
501
if (options.folder.fsPath === rootFolderA.fsPath) {
502
reportedResults = [
503
'folder/fileA.scss',
504
'folder/fileA.css',
505
'folder/file2.css'
506
].map(relativePath => joinPath(rootFolderA, relativePath));
507
} else {
508
reportedResults = [
509
'fileB.ts',
510
'fileB.js',
511
'file3.js'
512
].map(relativePath => joinPath(rootFolderB, relativePath));
513
}
514
515
return Promise.resolve(reportedResults);
516
}
517
});
518
519
const query: ISearchQuery = {
520
type: QueryType.File,
521
522
filePattern: '',
523
excludePattern: {
524
'*.js': {
525
when: '$(basename).ts'
526
},
527
'*.css': true
528
},
529
folderQueries: [
530
{
531
folder: rootFolderA,
532
excludePattern: [{
533
pattern: {
534
'folder/*.css': {
535
when: '$(basename).scss'
536
}
537
}
538
}]
539
},
540
{
541
folder: rootFolderB,
542
excludePattern: [{
543
pattern: {
544
'*.js': false
545
}
546
}]
547
}
548
]
549
};
550
551
const { results } = await runFileSearch(query);
552
compareURIs(
553
results,
554
[
555
joinPath(rootFolderA, 'folder/fileA.scss'),
556
joinPath(rootFolderA, 'folder/file2.css'),
557
558
joinPath(rootFolderB, 'fileB.ts'),
559
joinPath(rootFolderB, 'fileB.js'),
560
joinPath(rootFolderB, 'file3.js'),
561
]);
562
});
563
564
test('max results = 1', async () => {
565
const reportedResults = [
566
joinPath(rootFolderA, 'file1.ts'),
567
joinPath(rootFolderA, 'file2.ts'),
568
joinPath(rootFolderA, 'file3.ts'),
569
];
570
571
let wasCanceled = false;
572
await registerTestFileSearchProvider({
573
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
574
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
575
576
return Promise.resolve(reportedResults);
577
}
578
});
579
580
const query: ISearchQuery = {
581
type: QueryType.File,
582
583
filePattern: '',
584
maxResults: 1,
585
586
folderQueries: [
587
{
588
folder: rootFolderA
589
}
590
]
591
};
592
593
const { results, stats } = await runFileSearch(query);
594
assert(stats.limitHit, 'Expected to return limitHit');
595
assert.strictEqual(results.length, 1);
596
compareURIs(results, reportedResults.slice(0, 1));
597
assert(wasCanceled, 'Expected to be canceled when hitting limit');
598
});
599
600
test('max results = 2', async () => {
601
const reportedResults = [
602
joinPath(rootFolderA, 'file1.ts'),
603
joinPath(rootFolderA, 'file2.ts'),
604
joinPath(rootFolderA, 'file3.ts'),
605
];
606
607
let wasCanceled = false;
608
await registerTestFileSearchProvider({
609
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
610
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
611
612
return Promise.resolve(reportedResults);
613
}
614
});
615
616
const query: ISearchQuery = {
617
type: QueryType.File,
618
619
filePattern: '',
620
maxResults: 2,
621
622
folderQueries: [
623
{
624
folder: rootFolderA
625
}
626
]
627
};
628
629
const { results, stats } = await runFileSearch(query);
630
assert(stats.limitHit, 'Expected to return limitHit');
631
assert.strictEqual(results.length, 2);
632
compareURIs(results, reportedResults.slice(0, 2));
633
assert(wasCanceled, 'Expected to be canceled when hitting limit');
634
});
635
636
test('provider returns maxResults exactly', async () => {
637
const reportedResults = [
638
joinPath(rootFolderA, 'file1.ts'),
639
joinPath(rootFolderA, 'file2.ts'),
640
];
641
642
let wasCanceled = false;
643
await registerTestFileSearchProvider({
644
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
645
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
646
647
return Promise.resolve(reportedResults);
648
}
649
});
650
651
const query: ISearchQuery = {
652
type: QueryType.File,
653
654
filePattern: '',
655
maxResults: 2,
656
657
folderQueries: [
658
{
659
folder: rootFolderA
660
}
661
]
662
};
663
664
const { results, stats } = await runFileSearch(query);
665
assert(!stats.limitHit, 'Expected not to return limitHit');
666
assert.strictEqual(results.length, 2);
667
compareURIs(results, reportedResults);
668
assert(!wasCanceled, 'Expected not to be canceled when just reaching limit');
669
});
670
671
test('multiroot max results', async () => {
672
let cancels = 0;
673
await registerTestFileSearchProvider({
674
async provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
675
disposables.add(token.onCancellationRequested(() => cancels++));
676
677
// Provice results async so it has a chance to invoke every provider
678
await new Promise(r => process.nextTick(r));
679
return [
680
'file1.ts',
681
'file2.ts',
682
'file3.ts',
683
].map(relativePath => joinPath(options.folder, relativePath));
684
}
685
});
686
687
const query: ISearchQuery = {
688
type: QueryType.File,
689
690
filePattern: '',
691
maxResults: 2,
692
693
folderQueries: [
694
{
695
folder: rootFolderA
696
},
697
{
698
folder: rootFolderB
699
}
700
]
701
};
702
703
const { results } = await runFileSearch(query);
704
assert.strictEqual(results.length, 2); // Don't care which 2 we got
705
assert.strictEqual(cancels, 2, 'Expected all invocations to be canceled when hitting limit');
706
});
707
708
test('works with non-file schemes', async () => {
709
const reportedResults = [
710
joinPath(fancySchemeFolderA, 'file1.ts'),
711
joinPath(fancySchemeFolderA, 'file2.ts'),
712
joinPath(fancySchemeFolderA, 'subfolder/file3.ts'),
713
714
];
715
716
await registerTestFileSearchProvider({
717
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
718
return Promise.resolve(reportedResults);
719
}
720
}, fancyScheme);
721
722
const query: ISearchQuery = {
723
type: QueryType.File,
724
filePattern: '',
725
folderQueries: [
726
{
727
folder: fancySchemeFolderA
728
}
729
]
730
};
731
732
const { results } = await runFileSearch(query);
733
compareURIs(results, reportedResults);
734
});
735
test('if onlyFileScheme is set, do not call custom schemes', async () => {
736
let fancySchemeCalled = false;
737
await registerTestFileSearchProvider({
738
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
739
fancySchemeCalled = true;
740
return Promise.resolve([]);
741
}
742
}, fancyScheme);
743
744
const query: ISearchQuery = {
745
type: QueryType.File,
746
filePattern: '',
747
folderQueries: []
748
};
749
750
await runFileSearch(query);
751
assert(!fancySchemeCalled);
752
});
753
});
754
755
suite('Text:', () => {
756
757
function makePreview(text: string): vscode.TextSearchMatch['preview'] {
758
return {
759
matches: [new Range(0, 0, 0, text.length)],
760
text
761
};
762
}
763
764
function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchMatch {
765
return {
766
preview: makePreview('foo'),
767
ranges: [new Range(0, 0, 0, 3)],
768
uri: joinPath(baseFolder, relativePath)
769
};
770
}
771
772
function getSimpleQuery(queryText: string): ITextQuery {
773
return {
774
type: QueryType.Text,
775
contentPattern: getPattern(queryText),
776
777
folderQueries: [
778
{ folder: rootFolderA }
779
]
780
};
781
}
782
783
function getPattern(queryText: string): IPatternInfo {
784
return {
785
pattern: queryText
786
};
787
}
788
789
function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) {
790
const actualTextSearchResults: vscode.TextSearchResult[] = [];
791
for (const fileMatch of actual) {
792
// Make relative
793
for (const lineResult of fileMatch.results!) {
794
if (resultIsMatch(lineResult)) {
795
actualTextSearchResults.push({
796
preview: {
797
text: lineResult.previewText,
798
matches: mapArrayOrNot(
799
lineResult.rangeLocations.map(r => r.preview),
800
m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn))
801
},
802
ranges: mapArrayOrNot(
803
lineResult.rangeLocations.map(r => r.source),
804
r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn),
805
),
806
uri: fileMatch.resource
807
});
808
} else {
809
actualTextSearchResults.push(<vscode.TextSearchContext>{
810
text: lineResult.text,
811
lineNumber: lineResult.lineNumber,
812
uri: fileMatch.resource
813
});
814
}
815
}
816
}
817
818
const rangeToString = (r: vscode.Range) => `(${r.start.line}, ${r.start.character}), (${r.end.line}, ${r.end.character})`;
819
820
const makeComparable = (results: vscode.TextSearchResult[]) => results
821
.sort((a, b) => {
822
const compareKeyA = a.uri.toString() + ': ' + (extensionResultIsMatch(a) ? a.preview.text : a.text);
823
const compareKeyB = b.uri.toString() + ': ' + (extensionResultIsMatch(b) ? b.preview.text : b.text);
824
return compareKeyB.localeCompare(compareKeyA);
825
})
826
.map(r => extensionResultIsMatch(r) ? {
827
uri: r.uri.toString(),
828
range: mapArrayOrNot(r.ranges, rangeToString),
829
preview: {
830
text: r.preview.text,
831
match: null // Don't care about this right now
832
}
833
} : {
834
uri: r.uri.toString(),
835
text: r.text,
836
lineNumber: r.lineNumber
837
});
838
839
return assert.deepStrictEqual(
840
makeComparable(actualTextSearchResults),
841
makeComparable(expected));
842
}
843
844
test('no results', async () => {
845
await registerTestTextSearchProvider({
846
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
847
return Promise.resolve(null!);
848
}
849
});
850
851
const { results, stats } = await runTextSearch(getSimpleQuery('foo'));
852
assert(!stats.limitHit);
853
assert(!results.length);
854
});
855
856
test('basic results', async () => {
857
const providedResults: vscode.TextSearchResult[] = [
858
makeTextResult(rootFolderA, 'file1.ts'),
859
makeTextResult(rootFolderA, 'file2.ts')
860
];
861
862
await registerTestTextSearchProvider({
863
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
864
providedResults.forEach(r => progress.report(r));
865
return Promise.resolve(null!);
866
}
867
});
868
869
const { results, stats } = await runTextSearch(getSimpleQuery('foo'));
870
assert(!stats.limitHit);
871
assertResults(results, providedResults);
872
});
873
874
test('all provider calls get global include/excludes', async () => {
875
await registerTestTextSearchProvider({
876
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
877
assert.strictEqual(options.includes.length, 1);
878
assert.strictEqual(options.excludes.length, 1);
879
return Promise.resolve(null!);
880
}
881
});
882
883
const query: ITextQuery = {
884
type: QueryType.Text,
885
contentPattern: getPattern('foo'),
886
887
includePattern: {
888
'*.ts': true
889
},
890
891
excludePattern: {
892
'*.js': true
893
},
894
895
folderQueries: [
896
{ folder: rootFolderA },
897
{ folder: rootFolderB }
898
]
899
};
900
901
await runTextSearch(query);
902
});
903
904
test('global/local include/excludes combined', async () => {
905
await registerTestTextSearchProvider({
906
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
907
if (options.folder.toString() === rootFolderA.toString()) {
908
assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);
909
assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);
910
} else {
911
assert.deepStrictEqual(options.includes.sort(), ['*.ts']);
912
assert.deepStrictEqual(options.excludes.sort(), ['*.js']);
913
}
914
915
return Promise.resolve(null!);
916
}
917
});
918
919
const query: ITextQuery = {
920
type: QueryType.Text,
921
contentPattern: getPattern('foo'),
922
923
includePattern: {
924
'*.ts': true
925
},
926
excludePattern: {
927
'*.js': true
928
},
929
folderQueries: [
930
{
931
folder: rootFolderA,
932
includePattern: {
933
'foo': true
934
},
935
excludePattern: [{
936
pattern: {
937
'bar': true
938
}
939
}]
940
},
941
{ folder: rootFolderB }
942
]
943
};
944
945
await runTextSearch(query);
946
});
947
948
test('include/excludes resolved correctly', async () => {
949
await registerTestTextSearchProvider({
950
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
951
assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);
952
assert.deepStrictEqual(options.excludes.sort(), []);
953
954
return Promise.resolve(null!);
955
}
956
});
957
958
const query: ISearchQuery = {
959
type: QueryType.Text,
960
contentPattern: getPattern('foo'),
961
962
includePattern: {
963
'*.ts': true,
964
'*.jsx': false
965
},
966
excludePattern: {
967
'*.js': true,
968
'*.tsx': false
969
},
970
folderQueries: [
971
{
972
folder: rootFolderA,
973
includePattern: {
974
'*.jsx': true
975
},
976
excludePattern: [{
977
pattern: {
978
'*.js': false
979
}
980
}]
981
}
982
]
983
};
984
985
await runTextSearch(query);
986
});
987
988
test('provider fail', async () => {
989
await registerTestTextSearchProvider({
990
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
991
throw new Error('Provider fail');
992
}
993
});
994
995
try {
996
await runTextSearch(getSimpleQuery('foo'));
997
assert(false, 'Expected to fail');
998
} catch {
999
// expected to fail
1000
}
1001
});
1002
1003
test('basic sibling clause', async () => {
1004
// eslint-disable-next-line local/code-no-any-casts
1005
(mockPFS as any).Promises = {
1006
readdir: (_path: string): any => {
1007
if (_path === rootFolderA.fsPath) {
1008
return Promise.resolve([
1009
'file1.js',
1010
'file1.ts'
1011
]);
1012
} else {
1013
return Promise.reject(new Error('Wrong path'));
1014
}
1015
}
1016
};
1017
1018
const providedResults: vscode.TextSearchResult[] = [
1019
makeTextResult(rootFolderA, 'file1.js'),
1020
makeTextResult(rootFolderA, 'file1.ts')
1021
];
1022
1023
await registerTestTextSearchProvider({
1024
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1025
providedResults.forEach(r => progress.report(r));
1026
return Promise.resolve(null!);
1027
}
1028
});
1029
1030
const query: ISearchQuery = {
1031
type: QueryType.Text,
1032
contentPattern: getPattern('foo'),
1033
1034
excludePattern: {
1035
'*.js': {
1036
when: '$(basename).ts'
1037
}
1038
},
1039
1040
folderQueries: [
1041
{ folder: rootFolderA }
1042
]
1043
};
1044
1045
const { results } = await runTextSearch(query);
1046
assertResults(results, providedResults.slice(1));
1047
});
1048
1049
test('multiroot sibling clause', async () => {
1050
// eslint-disable-next-line local/code-no-any-casts
1051
(mockPFS as any).Promises = {
1052
readdir: (_path: string): any => {
1053
if (_path === joinPath(rootFolderA, 'folder').fsPath) {
1054
return Promise.resolve([
1055
'fileA.scss',
1056
'fileA.css',
1057
'file2.css'
1058
]);
1059
} else if (_path === rootFolderB.fsPath) {
1060
return Promise.resolve([
1061
'fileB.ts',
1062
'fileB.js',
1063
'file3.js'
1064
]);
1065
} else {
1066
return Promise.reject(new Error('Wrong path'));
1067
}
1068
}
1069
};
1070
1071
await registerTestTextSearchProvider({
1072
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1073
let reportedResults;
1074
if (options.folder.fsPath === rootFolderA.fsPath) {
1075
reportedResults = [
1076
makeTextResult(rootFolderA, 'folder/fileA.scss'),
1077
makeTextResult(rootFolderA, 'folder/fileA.css'),
1078
makeTextResult(rootFolderA, 'folder/file2.css')
1079
];
1080
} else {
1081
reportedResults = [
1082
makeTextResult(rootFolderB, 'fileB.ts'),
1083
makeTextResult(rootFolderB, 'fileB.js'),
1084
makeTextResult(rootFolderB, 'file3.js')
1085
];
1086
}
1087
1088
reportedResults.forEach(r => progress.report(r));
1089
return Promise.resolve(null!);
1090
}
1091
});
1092
1093
const query: ISearchQuery = {
1094
type: QueryType.Text,
1095
contentPattern: getPattern('foo'),
1096
1097
excludePattern: {
1098
'*.js': {
1099
when: '$(basename).ts'
1100
},
1101
'*.css': true
1102
},
1103
folderQueries: [
1104
{
1105
folder: rootFolderA,
1106
excludePattern: [{
1107
pattern: {
1108
'folder/*.css': {
1109
when: '$(basename).scss'
1110
}
1111
}
1112
}]
1113
},
1114
{
1115
folder: rootFolderB,
1116
excludePattern: [{
1117
pattern: {
1118
'*.js': false
1119
}
1120
}]
1121
}
1122
]
1123
};
1124
1125
const { results } = await runTextSearch(query);
1126
assertResults(results, [
1127
makeTextResult(rootFolderA, 'folder/fileA.scss'),
1128
makeTextResult(rootFolderA, 'folder/file2.css'),
1129
makeTextResult(rootFolderB, 'fileB.ts'),
1130
makeTextResult(rootFolderB, 'fileB.js'),
1131
makeTextResult(rootFolderB, 'file3.js')]);
1132
});
1133
1134
test('include pattern applied', async () => {
1135
const providedResults: vscode.TextSearchResult[] = [
1136
makeTextResult(rootFolderA, 'file1.js'),
1137
makeTextResult(rootFolderA, 'file1.ts')
1138
];
1139
1140
await registerTestTextSearchProvider({
1141
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1142
providedResults.forEach(r => progress.report(r));
1143
return Promise.resolve(null!);
1144
}
1145
});
1146
1147
const query: ISearchQuery = {
1148
type: QueryType.Text,
1149
contentPattern: getPattern('foo'),
1150
1151
includePattern: {
1152
'*.ts': true
1153
},
1154
1155
folderQueries: [
1156
{ folder: rootFolderA }
1157
]
1158
};
1159
1160
const { results } = await runTextSearch(query);
1161
assertResults(results, providedResults.slice(1));
1162
});
1163
1164
test('max results = 1', async () => {
1165
const providedResults: vscode.TextSearchResult[] = [
1166
makeTextResult(rootFolderA, 'file1.ts'),
1167
makeTextResult(rootFolderA, 'file2.ts')
1168
];
1169
1170
let wasCanceled = false;
1171
await registerTestTextSearchProvider({
1172
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1173
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
1174
providedResults.forEach(r => progress.report(r));
1175
return Promise.resolve(null!);
1176
}
1177
});
1178
1179
const query: ISearchQuery = {
1180
type: QueryType.Text,
1181
contentPattern: getPattern('foo'),
1182
1183
maxResults: 1,
1184
1185
folderQueries: [
1186
{ folder: rootFolderA }
1187
]
1188
};
1189
1190
const { results, stats } = await runTextSearch(query);
1191
assert(stats.limitHit, 'Expected to return limitHit');
1192
assertResults(results, providedResults.slice(0, 1));
1193
assert(wasCanceled, 'Expected to be canceled');
1194
});
1195
1196
test('max results = 2', async () => {
1197
const providedResults: vscode.TextSearchResult[] = [
1198
makeTextResult(rootFolderA, 'file1.ts'),
1199
makeTextResult(rootFolderA, 'file2.ts'),
1200
makeTextResult(rootFolderA, 'file3.ts')
1201
];
1202
1203
let wasCanceled = false;
1204
await registerTestTextSearchProvider({
1205
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1206
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
1207
providedResults.forEach(r => progress.report(r));
1208
return Promise.resolve(null!);
1209
}
1210
});
1211
1212
const query: ISearchQuery = {
1213
type: QueryType.Text,
1214
contentPattern: getPattern('foo'),
1215
1216
maxResults: 2,
1217
1218
folderQueries: [
1219
{ folder: rootFolderA }
1220
]
1221
};
1222
1223
const { results, stats } = await runTextSearch(query);
1224
assert(stats.limitHit, 'Expected to return limitHit');
1225
assertResults(results, providedResults.slice(0, 2));
1226
assert(wasCanceled, 'Expected to be canceled');
1227
});
1228
1229
test('provider returns maxResults exactly', async () => {
1230
const providedResults: vscode.TextSearchResult[] = [
1231
makeTextResult(rootFolderA, 'file1.ts'),
1232
makeTextResult(rootFolderA, 'file2.ts')
1233
];
1234
1235
let wasCanceled = false;
1236
await registerTestTextSearchProvider({
1237
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1238
disposables.add(token.onCancellationRequested(() => wasCanceled = true));
1239
providedResults.forEach(r => progress.report(r));
1240
return Promise.resolve(null!);
1241
}
1242
});
1243
1244
const query: ISearchQuery = {
1245
type: QueryType.Text,
1246
contentPattern: getPattern('foo'),
1247
1248
maxResults: 2,
1249
1250
folderQueries: [
1251
{ folder: rootFolderA }
1252
]
1253
};
1254
1255
const { results, stats } = await runTextSearch(query);
1256
assert(!stats.limitHit, 'Expected not to return limitHit');
1257
assertResults(results, providedResults);
1258
assert(!wasCanceled, 'Expected not to be canceled');
1259
});
1260
1261
test('provider returns early with limitHit', async () => {
1262
const providedResults: vscode.TextSearchResult[] = [
1263
makeTextResult(rootFolderA, 'file1.ts'),
1264
makeTextResult(rootFolderA, 'file2.ts'),
1265
makeTextResult(rootFolderA, 'file3.ts')
1266
];
1267
1268
await registerTestTextSearchProvider({
1269
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1270
providedResults.forEach(r => progress.report(r));
1271
return Promise.resolve({ limitHit: true });
1272
}
1273
});
1274
1275
const query: ISearchQuery = {
1276
type: QueryType.Text,
1277
contentPattern: getPattern('foo'),
1278
1279
maxResults: 1000,
1280
1281
folderQueries: [
1282
{ folder: rootFolderA }
1283
]
1284
};
1285
1286
const { results, stats } = await runTextSearch(query);
1287
assert(stats.limitHit, 'Expected to return limitHit');
1288
assertResults(results, providedResults);
1289
});
1290
1291
test('multiroot max results', async () => {
1292
let cancels = 0;
1293
await registerTestTextSearchProvider({
1294
async provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1295
disposables.add(token.onCancellationRequested(() => cancels++));
1296
await new Promise(r => process.nextTick(r));
1297
[
1298
'file1.ts',
1299
'file2.ts',
1300
'file3.ts',
1301
].forEach(f => progress.report(makeTextResult(options.folder, f)));
1302
return null!;
1303
}
1304
});
1305
1306
const query: ISearchQuery = {
1307
type: QueryType.Text,
1308
contentPattern: getPattern('foo'),
1309
1310
maxResults: 2,
1311
1312
folderQueries: [
1313
{ folder: rootFolderA },
1314
{ folder: rootFolderB }
1315
]
1316
};
1317
1318
const { results } = await runTextSearch(query);
1319
assert.strictEqual(results.length, 2);
1320
assert.strictEqual(cancels, 2);
1321
});
1322
1323
test('works with non-file schemes', async () => {
1324
const providedResults: vscode.TextSearchResult[] = [
1325
makeTextResult(fancySchemeFolderA, 'file1.ts'),
1326
makeTextResult(fancySchemeFolderA, 'file2.ts'),
1327
makeTextResult(fancySchemeFolderA, 'file3.ts')
1328
];
1329
1330
await registerTestTextSearchProvider({
1331
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
1332
providedResults.forEach(r => progress.report(r));
1333
return Promise.resolve(null!);
1334
}
1335
}, fancyScheme);
1336
1337
const query: ISearchQuery = {
1338
type: QueryType.Text,
1339
contentPattern: getPattern('foo'),
1340
1341
folderQueries: [
1342
{ folder: fancySchemeFolderA }
1343
]
1344
};
1345
1346
const { results } = await runTextSearch(query);
1347
assertResults(results, providedResults);
1348
});
1349
});
1350
});
1351
1352