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