Path: blob/main/src/vs/workbench/api/test/node/extHostSearch.test.ts
5222 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { mapArrayOrNot } from '../../../../base/common/arrays.js';7import { timeout } from '../../../../base/common/async.js';8import { CancellationTokenSource } from '../../../../base/common/cancellation.js';9import { isCancellationError } from '../../../../base/common/errors.js';10import { revive } from '../../../../base/common/marshalling.js';11import { joinPath } from '../../../../base/common/resources.js';12import { URI, UriComponents } from '../../../../base/common/uri.js';13import * as pfs from '../../../../base/node/pfs.js';14import { mock } from '../../../../base/test/common/mock.js';15import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';16import { NullLogService } from '../../../../platform/log/common/log.js';17import { MainContext, MainThreadSearchShape } from '../../common/extHost.protocol.js';18import { ExtHostConfigProvider, IExtHostConfiguration } from '../../common/extHostConfiguration.js';19import { IExtHostInitDataService } from '../../common/extHostInitDataService.js';20import { Range } from '../../common/extHostTypes.js';21import { URITransformerService } from '../../common/extHostUriTransformerService.js';22import { NativeExtHostSearch } from '../../node/extHostSearch.js';23import { TestRPCProtocol } from '../common/testRPCProtocol.js';24import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from '../../../services/search/common/search.js';25import { TextSearchManager } from '../../../services/search/common/textSearchManager.js';26import { NativeTextSearchManager } from '../../../services/search/node/textSearchManager.js';27import type * as vscode from 'vscode';28import { AISearchKeyword } from '../../../services/search/common/searchExtTypes.js';2930let rpcProtocol: TestRPCProtocol;31let extHostSearch: NativeExtHostSearch;3233let mockMainThreadSearch: MockMainThreadSearch;34class MockMainThreadSearch implements MainThreadSearchShape {35lastHandle!: number;3637results: Array<UriComponents | IRawFileMatch2> = [];3839keywords: Array<AISearchKeyword> = [];4041$registerFileSearchProvider(handle: number, scheme: string): void {42this.lastHandle = handle;43}4445$registerTextSearchProvider(handle: number, scheme: string): void {46this.lastHandle = handle;47}4849$registerAITextSearchProvider(handle: number, scheme: string): void {50this.lastHandle = handle;51}5253$unregisterProvider(handle: number): void {54}5556$handleFileMatch(handle: number, session: number, data: UriComponents[]): void {57this.results.push(...data);58}5960$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void {61this.results.push(...data);62}6364$handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void {65this.keywords.push(data);66}6768$handleTelemetry(eventName: string, data: any): void {69}7071dispose() {72}73}7475let mockPFS: Partial<typeof pfs>;7677function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch {78return !!(<vscode.TextSearchMatch>data).preview;79}8081suite('ExtHostSearch', () => {82const disposables = ensureNoDisposablesAreLeakedInTestSuite();8384async function registerTestTextSearchProvider(provider: vscode.TextSearchProvider, scheme = 'file'): Promise<void> {85disposables.add(extHostSearch.registerTextSearchProviderOld(scheme, provider));86await rpcProtocol.sync();87}8889async function registerTestFileSearchProvider(provider: vscode.FileSearchProvider, scheme = 'file'): Promise<void> {90disposables.add(extHostSearch.registerFileSearchProviderOld(scheme, provider));91await rpcProtocol.sync();92}9394async function runFileSearch(query: IFileQuery, cancel = false): Promise<{ results: URI[]; stats: ISearchCompleteStats }> {95let stats: ISearchCompleteStats;96try {97const cancellation = new CancellationTokenSource();98const p = extHostSearch.$provideFileSearchResults(mockMainThreadSearch.lastHandle, 0, query, cancellation.token);99if (cancel) {100await timeout(0);101cancellation.cancel();102}103104stats = await p;105} catch (err) {106if (!isCancellationError(err)) {107await rpcProtocol.sync();108throw err;109}110}111112await rpcProtocol.sync();113return {114results: (<UriComponents[]>mockMainThreadSearch.results).map(r => URI.revive(r)),115stats: stats!116};117}118119async function runTextSearch(query: ITextQuery): Promise<{ results: IFileMatch[]; stats: ISearchCompleteStats }> {120let stats: ISearchCompleteStats;121try {122const cancellation = new CancellationTokenSource();123const p = extHostSearch.$provideTextSearchResults(mockMainThreadSearch.lastHandle, 0, query, cancellation.token);124125stats = await p;126} catch (err) {127if (!isCancellationError(err)) {128await rpcProtocol.sync();129throw err;130}131}132133await rpcProtocol.sync();134const results: IFileMatch[] = revive(<IRawFileMatch2[]>mockMainThreadSearch.results);135136return { results, stats: stats! };137}138139setup(() => {140rpcProtocol = new TestRPCProtocol();141142mockMainThreadSearch = new MockMainThreadSearch();143const logService = new NullLogService();144145rpcProtocol.set(MainContext.MainThreadSearch, mockMainThreadSearch);146147mockPFS = {};148extHostSearch = disposables.add(new class extends NativeExtHostSearch {149constructor() {150super(151rpcProtocol,152new class extends mock<IExtHostInitDataService>() { override remote = { isRemote: false, authority: undefined, connectionData: null }; },153new URITransformerService(null),154new class extends mock<IExtHostConfiguration>() {155override async getConfigProvider(): Promise<ExtHostConfigProvider> {156return {157onDidChangeConfiguration(_listener: (event: vscode.ConfigurationChangeEvent) => void) { },158getConfiguration(): vscode.WorkspaceConfiguration {159return {160get() { },161has() {162return false;163},164inspect() {165return undefined;166},167async update() { }168};169},170171} as ExtHostConfigProvider;172}173},174logService175);176// eslint-disable-next-line local/code-no-any-casts177this._pfs = mockPFS as any;178}179180protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider2): TextSearchManager {181return new NativeTextSearchManager(query, provider, this._pfs);182}183});184});185186teardown(() => {187return rpcProtocol.sync();188});189190const rootFolderA = URI.file('/foo/bar1');191const rootFolderB = URI.file('/foo/bar2');192const fancyScheme = 'fancy';193const fancySchemeFolderA = URI.from({ scheme: fancyScheme, path: '/project/folder1' });194195suite('File:', () => {196197function getSimpleQuery(filePattern = ''): IFileQuery {198return {199type: QueryType.File,200201filePattern,202folderQueries: [203{ folder: rootFolderA }204]205};206}207208function compareURIs(actual: URI[], expected: URI[]) {209const sortAndStringify = (arr: URI[]) => arr.sort().map(u => u.toString());210211assert.deepStrictEqual(212sortAndStringify(actual),213sortAndStringify(expected));214}215216test('no results', async () => {217await registerTestFileSearchProvider({218provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {219return Promise.resolve(null!);220}221});222223const { results, stats } = await runFileSearch(getSimpleQuery());224assert(!stats.limitHit);225assert(!results.length);226});227228test('simple results', async () => {229const reportedResults = [230joinPath(rootFolderA, 'file1.ts'),231joinPath(rootFolderA, 'file2.ts'),232joinPath(rootFolderA, 'subfolder/file3.ts')233];234235await registerTestFileSearchProvider({236provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {237return Promise.resolve(reportedResults);238}239});240241const { results, stats } = await runFileSearch(getSimpleQuery());242assert(!stats.limitHit);243assert.strictEqual(results.length, 3);244compareURIs(results, reportedResults);245});246247test('Search canceled', async () => {248let cancelRequested = false;249await registerTestFileSearchProvider({250provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {251252return new Promise((resolve, reject) => {253function onCancel() {254cancelRequested = true;255256resolve([joinPath(options.folder, 'file1.ts')]); // or reject or nothing?257}258259if (token.isCancellationRequested) {260onCancel();261} else {262disposables.add(token.onCancellationRequested(() => onCancel()));263}264});265}266});267268const { results } = await runFileSearch(getSimpleQuery(), true);269assert(cancelRequested);270assert(!results.length);271});272273test('session cancellation should work', async () => {274let numSessionCancelled = 0;275const disposables: (vscode.Disposable | undefined)[] = [];276await registerTestFileSearchProvider({277provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {278279disposables.push(options.session?.onCancellationRequested(() => {280numSessionCancelled++;281}));282283return Promise.resolve([]);284}285});286287288await runFileSearch({ ...getSimpleQuery(), cacheKey: '1' }, true);289await runFileSearch({ ...getSimpleQuery(), cacheKey: '2' }, true);290extHostSearch.$clearCache('1');291assert.strictEqual(numSessionCancelled, 1);292disposables.forEach(d => d?.dispose());293});294295test('provider returns null', async () => {296await registerTestFileSearchProvider({297provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {298return null!;299}300});301302try {303await runFileSearch(getSimpleQuery());304assert(false, 'Expected to fail');305} catch {306// Expected to throw307}308});309310test('all provider calls get global include/excludes', async () => {311await registerTestFileSearchProvider({312provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {313assert(options.excludes.length === 2 && options.includes.length === 2, 'Missing global include/excludes');314return Promise.resolve(null!);315}316});317318const query: ISearchQuery = {319type: QueryType.File,320321filePattern: '',322includePattern: {323'foo': true,324'bar': true325},326excludePattern: {327'something': true,328'else': true329},330folderQueries: [331{ folder: rootFolderA },332{ folder: rootFolderB }333]334};335336await runFileSearch(query);337});338339test('global/local include/excludes combined', async () => {340await registerTestFileSearchProvider({341provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {342if (options.folder.toString() === rootFolderA.toString()) {343assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);344assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);345} else {346assert.deepStrictEqual(options.includes.sort(), ['*.ts']);347assert.deepStrictEqual(options.excludes.sort(), ['*.js']);348}349350return Promise.resolve(null!);351}352});353354const query: ISearchQuery = {355type: QueryType.File,356357filePattern: '',358includePattern: {359'*.ts': true360},361excludePattern: {362'*.js': true363},364folderQueries: [365{366folder: rootFolderA,367includePattern: {368'foo': true369},370excludePattern: [{371pattern: {372'bar': true373}374}]375},376{ folder: rootFolderB }377]378};379380await runFileSearch(query);381});382383test('include/excludes resolved correctly', async () => {384await registerTestFileSearchProvider({385provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {386assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);387assert.deepStrictEqual(options.excludes.sort(), []);388389return Promise.resolve(null!);390}391});392393const query: ISearchQuery = {394type: QueryType.File,395396filePattern: '',397includePattern: {398'*.ts': true,399'*.jsx': false400},401excludePattern: {402'*.js': true,403'*.tsx': false404},405folderQueries: [406{407folder: rootFolderA,408includePattern: {409'*.jsx': true410},411excludePattern: [{412pattern: {413'*.js': false414}415}]416}417]418};419420await runFileSearch(query);421});422423test('basic sibling exclude clause', async () => {424const reportedResults = [425'file1.ts',426'file1.js',427];428429await registerTestFileSearchProvider({430provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {431return Promise.resolve(reportedResults432.map(relativePath => joinPath(options.folder, relativePath)));433}434});435436const query: ISearchQuery = {437type: QueryType.File,438439filePattern: '',440excludePattern: {441'*.js': {442when: '$(basename).ts'443}444},445folderQueries: [446{ folder: rootFolderA }447]448};449450const { results } = await runFileSearch(query);451compareURIs(452results,453[454joinPath(rootFolderA, 'file1.ts')455]);456});457458// https://github.com/microsoft/vscode-remotehub/issues/255459test('include, sibling exclude, and subfolder', async () => {460const reportedResults = [461'foo/file1.ts',462'foo/file1.js',463];464465await registerTestFileSearchProvider({466provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {467return Promise.resolve(reportedResults468.map(relativePath => joinPath(options.folder, relativePath)));469}470});471472const query: ISearchQuery = {473type: QueryType.File,474475filePattern: '',476includePattern: { '**/*.ts': true },477excludePattern: {478'*.js': {479when: '$(basename).ts'480}481},482folderQueries: [483{ folder: rootFolderA }484]485};486487const { results } = await runFileSearch(query);488compareURIs(489results,490[491joinPath(rootFolderA, 'foo/file1.ts')492]);493});494495test('multiroot sibling exclude clause', async () => {496497await registerTestFileSearchProvider({498provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {499let reportedResults: URI[];500if (options.folder.fsPath === rootFolderA.fsPath) {501reportedResults = [502'folder/fileA.scss',503'folder/fileA.css',504'folder/file2.css'505].map(relativePath => joinPath(rootFolderA, relativePath));506} else {507reportedResults = [508'fileB.ts',509'fileB.js',510'file3.js'511].map(relativePath => joinPath(rootFolderB, relativePath));512}513514return Promise.resolve(reportedResults);515}516});517518const query: ISearchQuery = {519type: QueryType.File,520521filePattern: '',522excludePattern: {523'*.js': {524when: '$(basename).ts'525},526'*.css': true527},528folderQueries: [529{530folder: rootFolderA,531excludePattern: [{532pattern: {533'folder/*.css': {534when: '$(basename).scss'535}536}537}]538},539{540folder: rootFolderB,541excludePattern: [{542pattern: {543'*.js': false544}545}]546}547]548};549550const { results } = await runFileSearch(query);551compareURIs(552results,553[554joinPath(rootFolderA, 'folder/fileA.scss'),555joinPath(rootFolderA, 'folder/file2.css'),556557joinPath(rootFolderB, 'fileB.ts'),558joinPath(rootFolderB, 'fileB.js'),559joinPath(rootFolderB, 'file3.js'),560]);561});562563test('max results = 1', async () => {564const reportedResults = [565joinPath(rootFolderA, 'file1.ts'),566joinPath(rootFolderA, 'file2.ts'),567joinPath(rootFolderA, 'file3.ts'),568];569570let wasCanceled = false;571await registerTestFileSearchProvider({572provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {573disposables.add(token.onCancellationRequested(() => wasCanceled = true));574575return Promise.resolve(reportedResults);576}577});578579const query: ISearchQuery = {580type: QueryType.File,581582filePattern: '',583maxResults: 1,584585folderQueries: [586{587folder: rootFolderA588}589]590};591592const { results, stats } = await runFileSearch(query);593assert(stats.limitHit, 'Expected to return limitHit');594assert.strictEqual(results.length, 1);595compareURIs(results, reportedResults.slice(0, 1));596assert(wasCanceled, 'Expected to be canceled when hitting limit');597});598599test('max results = 2', async () => {600const reportedResults = [601joinPath(rootFolderA, 'file1.ts'),602joinPath(rootFolderA, 'file2.ts'),603joinPath(rootFolderA, 'file3.ts'),604];605606let wasCanceled = false;607await registerTestFileSearchProvider({608provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {609disposables.add(token.onCancellationRequested(() => wasCanceled = true));610611return Promise.resolve(reportedResults);612}613});614615const query: ISearchQuery = {616type: QueryType.File,617618filePattern: '',619maxResults: 2,620621folderQueries: [622{623folder: rootFolderA624}625]626};627628const { results, stats } = await runFileSearch(query);629assert(stats.limitHit, 'Expected to return limitHit');630assert.strictEqual(results.length, 2);631compareURIs(results, reportedResults.slice(0, 2));632assert(wasCanceled, 'Expected to be canceled when hitting limit');633});634635test('provider returns maxResults exactly', async () => {636const reportedResults = [637joinPath(rootFolderA, 'file1.ts'),638joinPath(rootFolderA, 'file2.ts'),639];640641let wasCanceled = false;642await registerTestFileSearchProvider({643provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {644disposables.add(token.onCancellationRequested(() => wasCanceled = true));645646return Promise.resolve(reportedResults);647}648});649650const query: ISearchQuery = {651type: QueryType.File,652653filePattern: '',654maxResults: 2,655656folderQueries: [657{658folder: rootFolderA659}660]661};662663const { results, stats } = await runFileSearch(query);664assert(!stats.limitHit, 'Expected not to return limitHit');665assert.strictEqual(results.length, 2);666compareURIs(results, reportedResults);667assert(!wasCanceled, 'Expected not to be canceled when just reaching limit');668});669670test('multiroot max results', async () => {671let cancels = 0;672await registerTestFileSearchProvider({673async provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {674disposables.add(token.onCancellationRequested(() => cancels++));675676// Provice results async so it has a chance to invoke every provider677await new Promise(r => process.nextTick(r));678return [679'file1.ts',680'file2.ts',681'file3.ts',682].map(relativePath => joinPath(options.folder, relativePath));683}684});685686const query: ISearchQuery = {687type: QueryType.File,688689filePattern: '',690maxResults: 2,691692folderQueries: [693{694folder: rootFolderA695},696{697folder: rootFolderB698}699]700};701702const { results } = await runFileSearch(query);703assert.strictEqual(results.length, 2); // Don't care which 2 we got704assert.strictEqual(cancels, 2, 'Expected all invocations to be canceled when hitting limit');705});706707test('works with non-file schemes', async () => {708const reportedResults = [709joinPath(fancySchemeFolderA, 'file1.ts'),710joinPath(fancySchemeFolderA, 'file2.ts'),711joinPath(fancySchemeFolderA, 'subfolder/file3.ts'),712713];714715await registerTestFileSearchProvider({716provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {717return Promise.resolve(reportedResults);718}719}, fancyScheme);720721const query: ISearchQuery = {722type: QueryType.File,723filePattern: '',724folderQueries: [725{726folder: fancySchemeFolderA727}728]729};730731const { results } = await runFileSearch(query);732compareURIs(results, reportedResults);733});734test('if onlyFileScheme is set, do not call custom schemes', async () => {735let fancySchemeCalled = false;736await registerTestFileSearchProvider({737provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {738fancySchemeCalled = true;739return Promise.resolve([]);740}741}, fancyScheme);742743const query: ISearchQuery = {744type: QueryType.File,745filePattern: '',746folderQueries: []747};748749await runFileSearch(query);750assert(!fancySchemeCalled);751});752});753754suite('Text:', () => {755756function makePreview(text: string): vscode.TextSearchMatch['preview'] {757return {758matches: [new Range(0, 0, 0, text.length)],759text760};761}762763function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchMatch {764return {765preview: makePreview('foo'),766ranges: [new Range(0, 0, 0, 3)],767uri: joinPath(baseFolder, relativePath)768};769}770771function getSimpleQuery(queryText: string): ITextQuery {772return {773type: QueryType.Text,774contentPattern: getPattern(queryText),775776folderQueries: [777{ folder: rootFolderA }778]779};780}781782function getPattern(queryText: string): IPatternInfo {783return {784pattern: queryText785};786}787788function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) {789const actualTextSearchResults: vscode.TextSearchResult[] = [];790for (const fileMatch of actual) {791// Make relative792for (const lineResult of fileMatch.results!) {793if (resultIsMatch(lineResult)) {794actualTextSearchResults.push({795preview: {796text: lineResult.previewText,797matches: mapArrayOrNot(798lineResult.rangeLocations.map(r => r.preview),799m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn))800},801ranges: mapArrayOrNot(802lineResult.rangeLocations.map(r => r.source),803r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn),804),805uri: fileMatch.resource806});807} else {808actualTextSearchResults.push(<vscode.TextSearchContext>{809text: lineResult.text,810lineNumber: lineResult.lineNumber,811uri: fileMatch.resource812});813}814}815}816817const rangeToString = (r: vscode.Range) => `(${r.start.line}, ${r.start.character}), (${r.end.line}, ${r.end.character})`;818819const makeComparable = (results: vscode.TextSearchResult[]) => results820.sort((a, b) => {821const compareKeyA = a.uri.toString() + ': ' + (extensionResultIsMatch(a) ? a.preview.text : a.text);822const compareKeyB = b.uri.toString() + ': ' + (extensionResultIsMatch(b) ? b.preview.text : b.text);823return compareKeyB.localeCompare(compareKeyA);824})825.map(r => extensionResultIsMatch(r) ? {826uri: r.uri.toString(),827range: mapArrayOrNot(r.ranges, rangeToString),828preview: {829text: r.preview.text,830match: null // Don't care about this right now831}832} : {833uri: r.uri.toString(),834text: r.text,835lineNumber: r.lineNumber836});837838return assert.deepStrictEqual(839makeComparable(actualTextSearchResults),840makeComparable(expected));841}842843test('no results', async () => {844await registerTestTextSearchProvider({845provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {846return Promise.resolve(null!);847}848});849850const { results, stats } = await runTextSearch(getSimpleQuery('foo'));851assert(!stats.limitHit);852assert(!results.length);853});854855test('basic results', async () => {856const providedResults: vscode.TextSearchResult[] = [857makeTextResult(rootFolderA, 'file1.ts'),858makeTextResult(rootFolderA, 'file2.ts')859];860861await registerTestTextSearchProvider({862provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {863providedResults.forEach(r => progress.report(r));864return Promise.resolve(null!);865}866});867868const { results, stats } = await runTextSearch(getSimpleQuery('foo'));869assert(!stats.limitHit);870assertResults(results, providedResults);871});872873test('all provider calls get global include/excludes', async () => {874await registerTestTextSearchProvider({875provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {876assert.strictEqual(options.includes.length, 1);877assert.strictEqual(options.excludes.length, 1);878return Promise.resolve(null!);879}880});881882const query: ITextQuery = {883type: QueryType.Text,884contentPattern: getPattern('foo'),885886includePattern: {887'*.ts': true888},889890excludePattern: {891'*.js': true892},893894folderQueries: [895{ folder: rootFolderA },896{ folder: rootFolderB }897]898};899900await runTextSearch(query);901});902903test('global/local include/excludes combined', async () => {904await registerTestTextSearchProvider({905provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {906if (options.folder.toString() === rootFolderA.toString()) {907assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);908assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);909} else {910assert.deepStrictEqual(options.includes.sort(), ['*.ts']);911assert.deepStrictEqual(options.excludes.sort(), ['*.js']);912}913914return Promise.resolve(null!);915}916});917918const query: ITextQuery = {919type: QueryType.Text,920contentPattern: getPattern('foo'),921922includePattern: {923'*.ts': true924},925excludePattern: {926'*.js': true927},928folderQueries: [929{930folder: rootFolderA,931includePattern: {932'foo': true933},934excludePattern: [{935pattern: {936'bar': true937}938}]939},940{ folder: rootFolderB }941]942};943944await runTextSearch(query);945});946947test('include/excludes resolved correctly', async () => {948await registerTestTextSearchProvider({949provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {950assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);951assert.deepStrictEqual(options.excludes.sort(), []);952953return Promise.resolve(null!);954}955});956957const query: ISearchQuery = {958type: QueryType.Text,959contentPattern: getPattern('foo'),960961includePattern: {962'*.ts': true,963'*.jsx': false964},965excludePattern: {966'*.js': true,967'*.tsx': false968},969folderQueries: [970{971folder: rootFolderA,972includePattern: {973'*.jsx': true974},975excludePattern: [{976pattern: {977'*.js': false978}979}]980}981]982};983984await runTextSearch(query);985});986987test('provider fail', async () => {988await registerTestTextSearchProvider({989provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {990throw new Error('Provider fail');991}992});993994try {995await runTextSearch(getSimpleQuery('foo'));996assert(false, 'Expected to fail');997} catch {998// expected to fail999}1000});10011002test('basic sibling clause', async () => {1003// eslint-disable-next-line local/code-no-any-casts1004(mockPFS as any).Promises = {1005readdir: (_path: string): any => {1006if (_path === rootFolderA.fsPath) {1007return Promise.resolve([1008'file1.js',1009'file1.ts'1010]);1011} else {1012return Promise.reject(new Error('Wrong path'));1013}1014}1015};10161017const providedResults: vscode.TextSearchResult[] = [1018makeTextResult(rootFolderA, 'file1.js'),1019makeTextResult(rootFolderA, 'file1.ts')1020];10211022await registerTestTextSearchProvider({1023provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1024providedResults.forEach(r => progress.report(r));1025return Promise.resolve(null!);1026}1027});10281029const query: ISearchQuery = {1030type: QueryType.Text,1031contentPattern: getPattern('foo'),10321033excludePattern: {1034'*.js': {1035when: '$(basename).ts'1036}1037},10381039folderQueries: [1040{ folder: rootFolderA }1041]1042};10431044const { results } = await runTextSearch(query);1045assertResults(results, providedResults.slice(1));1046});10471048test('multiroot sibling clause', async () => {1049// eslint-disable-next-line local/code-no-any-casts1050(mockPFS as any).Promises = {1051readdir: (_path: string): any => {1052if (_path === joinPath(rootFolderA, 'folder').fsPath) {1053return Promise.resolve([1054'fileA.scss',1055'fileA.css',1056'file2.css'1057]);1058} else if (_path === rootFolderB.fsPath) {1059return Promise.resolve([1060'fileB.ts',1061'fileB.js',1062'file3.js'1063]);1064} else {1065return Promise.reject(new Error('Wrong path'));1066}1067}1068};10691070await registerTestTextSearchProvider({1071provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1072let reportedResults;1073if (options.folder.fsPath === rootFolderA.fsPath) {1074reportedResults = [1075makeTextResult(rootFolderA, 'folder/fileA.scss'),1076makeTextResult(rootFolderA, 'folder/fileA.css'),1077makeTextResult(rootFolderA, 'folder/file2.css')1078];1079} else {1080reportedResults = [1081makeTextResult(rootFolderB, 'fileB.ts'),1082makeTextResult(rootFolderB, 'fileB.js'),1083makeTextResult(rootFolderB, 'file3.js')1084];1085}10861087reportedResults.forEach(r => progress.report(r));1088return Promise.resolve(null!);1089}1090});10911092const query: ISearchQuery = {1093type: QueryType.Text,1094contentPattern: getPattern('foo'),10951096excludePattern: {1097'*.js': {1098when: '$(basename).ts'1099},1100'*.css': true1101},1102folderQueries: [1103{1104folder: rootFolderA,1105excludePattern: [{1106pattern: {1107'folder/*.css': {1108when: '$(basename).scss'1109}1110}1111}]1112},1113{1114folder: rootFolderB,1115excludePattern: [{1116pattern: {1117'*.js': false1118}1119}]1120}1121]1122};11231124const { results } = await runTextSearch(query);1125assertResults(results, [1126makeTextResult(rootFolderA, 'folder/fileA.scss'),1127makeTextResult(rootFolderA, 'folder/file2.css'),1128makeTextResult(rootFolderB, 'fileB.ts'),1129makeTextResult(rootFolderB, 'fileB.js'),1130makeTextResult(rootFolderB, 'file3.js')]);1131});11321133test('include pattern applied', async () => {1134const providedResults: vscode.TextSearchResult[] = [1135makeTextResult(rootFolderA, 'file1.js'),1136makeTextResult(rootFolderA, 'file1.ts')1137];11381139await registerTestTextSearchProvider({1140provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1141providedResults.forEach(r => progress.report(r));1142return Promise.resolve(null!);1143}1144});11451146const query: ISearchQuery = {1147type: QueryType.Text,1148contentPattern: getPattern('foo'),11491150includePattern: {1151'*.ts': true1152},11531154folderQueries: [1155{ folder: rootFolderA }1156]1157};11581159const { results } = await runTextSearch(query);1160assertResults(results, providedResults.slice(1));1161});11621163test('max results = 1', async () => {1164const providedResults: vscode.TextSearchResult[] = [1165makeTextResult(rootFolderA, 'file1.ts'),1166makeTextResult(rootFolderA, 'file2.ts')1167];11681169let wasCanceled = false;1170await registerTestTextSearchProvider({1171provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1172disposables.add(token.onCancellationRequested(() => wasCanceled = true));1173providedResults.forEach(r => progress.report(r));1174return Promise.resolve(null!);1175}1176});11771178const query: ISearchQuery = {1179type: QueryType.Text,1180contentPattern: getPattern('foo'),11811182maxResults: 1,11831184folderQueries: [1185{ folder: rootFolderA }1186]1187};11881189const { results, stats } = await runTextSearch(query);1190assert(stats.limitHit, 'Expected to return limitHit');1191assertResults(results, providedResults.slice(0, 1));1192assert(wasCanceled, 'Expected to be canceled');1193});11941195test('max results = 2', async () => {1196const providedResults: vscode.TextSearchResult[] = [1197makeTextResult(rootFolderA, 'file1.ts'),1198makeTextResult(rootFolderA, 'file2.ts'),1199makeTextResult(rootFolderA, 'file3.ts')1200];12011202let wasCanceled = false;1203await registerTestTextSearchProvider({1204provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1205disposables.add(token.onCancellationRequested(() => wasCanceled = true));1206providedResults.forEach(r => progress.report(r));1207return Promise.resolve(null!);1208}1209});12101211const query: ISearchQuery = {1212type: QueryType.Text,1213contentPattern: getPattern('foo'),12141215maxResults: 2,12161217folderQueries: [1218{ folder: rootFolderA }1219]1220};12211222const { results, stats } = await runTextSearch(query);1223assert(stats.limitHit, 'Expected to return limitHit');1224assertResults(results, providedResults.slice(0, 2));1225assert(wasCanceled, 'Expected to be canceled');1226});12271228test('provider returns maxResults exactly', async () => {1229const providedResults: vscode.TextSearchResult[] = [1230makeTextResult(rootFolderA, 'file1.ts'),1231makeTextResult(rootFolderA, 'file2.ts')1232];12331234let wasCanceled = false;1235await registerTestTextSearchProvider({1236provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1237disposables.add(token.onCancellationRequested(() => wasCanceled = true));1238providedResults.forEach(r => progress.report(r));1239return Promise.resolve(null!);1240}1241});12421243const query: ISearchQuery = {1244type: QueryType.Text,1245contentPattern: getPattern('foo'),12461247maxResults: 2,12481249folderQueries: [1250{ folder: rootFolderA }1251]1252};12531254const { results, stats } = await runTextSearch(query);1255assert(!stats.limitHit, 'Expected not to return limitHit');1256assertResults(results, providedResults);1257assert(!wasCanceled, 'Expected not to be canceled');1258});12591260test('provider returns early with limitHit', async () => {1261const providedResults: vscode.TextSearchResult[] = [1262makeTextResult(rootFolderA, 'file1.ts'),1263makeTextResult(rootFolderA, 'file2.ts'),1264makeTextResult(rootFolderA, 'file3.ts')1265];12661267await registerTestTextSearchProvider({1268provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1269providedResults.forEach(r => progress.report(r));1270return Promise.resolve({ limitHit: true });1271}1272});12731274const query: ISearchQuery = {1275type: QueryType.Text,1276contentPattern: getPattern('foo'),12771278maxResults: 1000,12791280folderQueries: [1281{ folder: rootFolderA }1282]1283};12841285const { results, stats } = await runTextSearch(query);1286assert(stats.limitHit, 'Expected to return limitHit');1287assertResults(results, providedResults);1288});12891290test('multiroot max results', async () => {1291let cancels = 0;1292await registerTestTextSearchProvider({1293async provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1294disposables.add(token.onCancellationRequested(() => cancels++));1295await new Promise(r => process.nextTick(r));1296[1297'file1.ts',1298'file2.ts',1299'file3.ts',1300].forEach(f => progress.report(makeTextResult(options.folder, f)));1301return null!;1302}1303});13041305const query: ISearchQuery = {1306type: QueryType.Text,1307contentPattern: getPattern('foo'),13081309maxResults: 2,13101311folderQueries: [1312{ folder: rootFolderA },1313{ folder: rootFolderB }1314]1315};13161317const { results } = await runTextSearch(query);1318assert.strictEqual(results.length, 2);1319assert.strictEqual(cancels, 2);1320});13211322test('works with non-file schemes', async () => {1323const providedResults: vscode.TextSearchResult[] = [1324makeTextResult(fancySchemeFolderA, 'file1.ts'),1325makeTextResult(fancySchemeFolderA, 'file2.ts'),1326makeTextResult(fancySchemeFolderA, 'file3.ts')1327];13281329await registerTestTextSearchProvider({1330provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1331providedResults.forEach(r => progress.report(r));1332return Promise.resolve(null!);1333}1334}, fancyScheme);13351336const query: ISearchQuery = {1337type: QueryType.Text,1338contentPattern: getPattern('foo'),13391340folderQueries: [1341{ folder: fancySchemeFolderA }1342]1343};13441345const { results } = await runTextSearch(query);1346assertResults(results, providedResults);1347});1348});1349});135013511352