Path: blob/main/src/vs/workbench/api/test/node/extHostSearch.test.ts
3296 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);176this._pfs = mockPFS as any;177}178179protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider2): TextSearchManager {180return new NativeTextSearchManager(query, provider, this._pfs);181}182});183});184185teardown(() => {186return rpcProtocol.sync();187});188189const rootFolderA = URI.file('/foo/bar1');190const rootFolderB = URI.file('/foo/bar2');191const fancyScheme = 'fancy';192const fancySchemeFolderA = URI.from({ scheme: fancyScheme, path: '/project/folder1' });193194suite('File:', () => {195196function getSimpleQuery(filePattern = ''): IFileQuery {197return {198type: QueryType.File,199200filePattern,201folderQueries: [202{ folder: rootFolderA }203]204};205}206207function compareURIs(actual: URI[], expected: URI[]) {208const sortAndStringify = (arr: URI[]) => arr.sort().map(u => u.toString());209210assert.deepStrictEqual(211sortAndStringify(actual),212sortAndStringify(expected));213}214215test('no results', async () => {216await registerTestFileSearchProvider({217provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {218return Promise.resolve(null!);219}220});221222const { results, stats } = await runFileSearch(getSimpleQuery());223assert(!stats.limitHit);224assert(!results.length);225});226227test('simple results', async () => {228const reportedResults = [229joinPath(rootFolderA, 'file1.ts'),230joinPath(rootFolderA, 'file2.ts'),231joinPath(rootFolderA, 'subfolder/file3.ts')232];233234await registerTestFileSearchProvider({235provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {236return Promise.resolve(reportedResults);237}238});239240const { results, stats } = await runFileSearch(getSimpleQuery());241assert(!stats.limitHit);242assert.strictEqual(results.length, 3);243compareURIs(results, reportedResults);244});245246test('Search canceled', async () => {247let cancelRequested = false;248await registerTestFileSearchProvider({249provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {250251return new Promise((resolve, reject) => {252function onCancel() {253cancelRequested = true;254255resolve([joinPath(options.folder, 'file1.ts')]); // or reject or nothing?256}257258if (token.isCancellationRequested) {259onCancel();260} else {261disposables.add(token.onCancellationRequested(() => onCancel()));262}263});264}265});266267const { results } = await runFileSearch(getSimpleQuery(), true);268assert(cancelRequested);269assert(!results.length);270});271272test('session cancellation should work', async () => {273let numSessionCancelled = 0;274const disposables: (vscode.Disposable | undefined)[] = [];275await registerTestFileSearchProvider({276provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {277278disposables.push(options.session?.onCancellationRequested(() => {279numSessionCancelled++;280}));281282return Promise.resolve([]);283}284});285286287await runFileSearch({ ...getSimpleQuery(), cacheKey: '1' }, true);288await runFileSearch({ ...getSimpleQuery(), cacheKey: '2' }, true);289extHostSearch.$clearCache('1');290assert.strictEqual(numSessionCancelled, 1);291disposables.forEach(d => d?.dispose());292});293294test('provider returns null', async () => {295await registerTestFileSearchProvider({296provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {297return null!;298}299});300301try {302await runFileSearch(getSimpleQuery());303assert(false, 'Expected to fail');304} catch {305// Expected to throw306}307});308309test('all provider calls get global include/excludes', async () => {310await registerTestFileSearchProvider({311provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {312assert(options.excludes.length === 2 && options.includes.length === 2, 'Missing global include/excludes');313return Promise.resolve(null!);314}315});316317const query: ISearchQuery = {318type: QueryType.File,319320filePattern: '',321includePattern: {322'foo': true,323'bar': true324},325excludePattern: {326'something': true,327'else': true328},329folderQueries: [330{ folder: rootFolderA },331{ folder: rootFolderB }332]333};334335await runFileSearch(query);336});337338test('global/local include/excludes combined', async () => {339await registerTestFileSearchProvider({340provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {341if (options.folder.toString() === rootFolderA.toString()) {342assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);343assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);344} else {345assert.deepStrictEqual(options.includes.sort(), ['*.ts']);346assert.deepStrictEqual(options.excludes.sort(), ['*.js']);347}348349return Promise.resolve(null!);350}351});352353const query: ISearchQuery = {354type: QueryType.File,355356filePattern: '',357includePattern: {358'*.ts': true359},360excludePattern: {361'*.js': true362},363folderQueries: [364{365folder: rootFolderA,366includePattern: {367'foo': true368},369excludePattern: [{370pattern: {371'bar': true372}373}]374},375{ folder: rootFolderB }376]377};378379await runFileSearch(query);380});381382test('include/excludes resolved correctly', async () => {383await registerTestFileSearchProvider({384provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {385assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);386assert.deepStrictEqual(options.excludes.sort(), []);387388return Promise.resolve(null!);389}390});391392const query: ISearchQuery = {393type: QueryType.File,394395filePattern: '',396includePattern: {397'*.ts': true,398'*.jsx': false399},400excludePattern: {401'*.js': true,402'*.tsx': false403},404folderQueries: [405{406folder: rootFolderA,407includePattern: {408'*.jsx': true409},410excludePattern: [{411pattern: {412'*.js': false413}414}]415}416]417};418419await runFileSearch(query);420});421422test('basic sibling exclude clause', async () => {423const reportedResults = [424'file1.ts',425'file1.js',426];427428await registerTestFileSearchProvider({429provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {430return Promise.resolve(reportedResults431.map(relativePath => joinPath(options.folder, relativePath)));432}433});434435const query: ISearchQuery = {436type: QueryType.File,437438filePattern: '',439excludePattern: {440'*.js': {441when: '$(basename).ts'442}443},444folderQueries: [445{ folder: rootFolderA }446]447};448449const { results } = await runFileSearch(query);450compareURIs(451results,452[453joinPath(rootFolderA, 'file1.ts')454]);455});456457// https://github.com/microsoft/vscode-remotehub/issues/255458test('include, sibling exclude, and subfolder', async () => {459const reportedResults = [460'foo/file1.ts',461'foo/file1.js',462];463464await registerTestFileSearchProvider({465provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {466return Promise.resolve(reportedResults467.map(relativePath => joinPath(options.folder, relativePath)));468}469});470471const query: ISearchQuery = {472type: QueryType.File,473474filePattern: '',475includePattern: { '**/*.ts': true },476excludePattern: {477'*.js': {478when: '$(basename).ts'479}480},481folderQueries: [482{ folder: rootFolderA }483]484};485486const { results } = await runFileSearch(query);487compareURIs(488results,489[490joinPath(rootFolderA, 'foo/file1.ts')491]);492});493494test('multiroot sibling exclude clause', async () => {495496await registerTestFileSearchProvider({497provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {498let reportedResults: URI[];499if (options.folder.fsPath === rootFolderA.fsPath) {500reportedResults = [501'folder/fileA.scss',502'folder/fileA.css',503'folder/file2.css'504].map(relativePath => joinPath(rootFolderA, relativePath));505} else {506reportedResults = [507'fileB.ts',508'fileB.js',509'file3.js'510].map(relativePath => joinPath(rootFolderB, relativePath));511}512513return Promise.resolve(reportedResults);514}515});516517const query: ISearchQuery = {518type: QueryType.File,519520filePattern: '',521excludePattern: {522'*.js': {523when: '$(basename).ts'524},525'*.css': true526},527folderQueries: [528{529folder: rootFolderA,530excludePattern: [{531pattern: {532'folder/*.css': {533when: '$(basename).scss'534}535}536}]537},538{539folder: rootFolderB,540excludePattern: [{541pattern: {542'*.js': false543}544}]545}546]547};548549const { results } = await runFileSearch(query);550compareURIs(551results,552[553joinPath(rootFolderA, 'folder/fileA.scss'),554joinPath(rootFolderA, 'folder/file2.css'),555556joinPath(rootFolderB, 'fileB.ts'),557joinPath(rootFolderB, 'fileB.js'),558joinPath(rootFolderB, 'file3.js'),559]);560});561562test('max results = 1', async () => {563const reportedResults = [564joinPath(rootFolderA, 'file1.ts'),565joinPath(rootFolderA, 'file2.ts'),566joinPath(rootFolderA, 'file3.ts'),567];568569let wasCanceled = false;570await registerTestFileSearchProvider({571provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {572disposables.add(token.onCancellationRequested(() => wasCanceled = true));573574return Promise.resolve(reportedResults);575}576});577578const query: ISearchQuery = {579type: QueryType.File,580581filePattern: '',582maxResults: 1,583584folderQueries: [585{586folder: rootFolderA587}588]589};590591const { results, stats } = await runFileSearch(query);592assert(stats.limitHit, 'Expected to return limitHit');593assert.strictEqual(results.length, 1);594compareURIs(results, reportedResults.slice(0, 1));595assert(wasCanceled, 'Expected to be canceled when hitting limit');596});597598test('max results = 2', async () => {599const reportedResults = [600joinPath(rootFolderA, 'file1.ts'),601joinPath(rootFolderA, 'file2.ts'),602joinPath(rootFolderA, 'file3.ts'),603];604605let wasCanceled = false;606await registerTestFileSearchProvider({607provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {608disposables.add(token.onCancellationRequested(() => wasCanceled = true));609610return Promise.resolve(reportedResults);611}612});613614const query: ISearchQuery = {615type: QueryType.File,616617filePattern: '',618maxResults: 2,619620folderQueries: [621{622folder: rootFolderA623}624]625};626627const { results, stats } = await runFileSearch(query);628assert(stats.limitHit, 'Expected to return limitHit');629assert.strictEqual(results.length, 2);630compareURIs(results, reportedResults.slice(0, 2));631assert(wasCanceled, 'Expected to be canceled when hitting limit');632});633634test('provider returns maxResults exactly', async () => {635const reportedResults = [636joinPath(rootFolderA, 'file1.ts'),637joinPath(rootFolderA, 'file2.ts'),638];639640let wasCanceled = false;641await registerTestFileSearchProvider({642provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {643disposables.add(token.onCancellationRequested(() => wasCanceled = true));644645return Promise.resolve(reportedResults);646}647});648649const query: ISearchQuery = {650type: QueryType.File,651652filePattern: '',653maxResults: 2,654655folderQueries: [656{657folder: rootFolderA658}659]660};661662const { results, stats } = await runFileSearch(query);663assert(!stats.limitHit, 'Expected not to return limitHit');664assert.strictEqual(results.length, 2);665compareURIs(results, reportedResults);666assert(!wasCanceled, 'Expected not to be canceled when just reaching limit');667});668669test('multiroot max results', async () => {670let cancels = 0;671await registerTestFileSearchProvider({672async provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {673disposables.add(token.onCancellationRequested(() => cancels++));674675// Provice results async so it has a chance to invoke every provider676await new Promise(r => process.nextTick(r));677return [678'file1.ts',679'file2.ts',680'file3.ts',681].map(relativePath => joinPath(options.folder, relativePath));682}683});684685const query: ISearchQuery = {686type: QueryType.File,687688filePattern: '',689maxResults: 2,690691folderQueries: [692{693folder: rootFolderA694},695{696folder: rootFolderB697}698]699};700701const { results } = await runFileSearch(query);702assert.strictEqual(results.length, 2); // Don't care which 2 we got703assert.strictEqual(cancels, 2, 'Expected all invocations to be canceled when hitting limit');704});705706test('works with non-file schemes', async () => {707const reportedResults = [708joinPath(fancySchemeFolderA, 'file1.ts'),709joinPath(fancySchemeFolderA, 'file2.ts'),710joinPath(fancySchemeFolderA, 'subfolder/file3.ts'),711712];713714await registerTestFileSearchProvider({715provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {716return Promise.resolve(reportedResults);717}718}, fancyScheme);719720const query: ISearchQuery = {721type: QueryType.File,722filePattern: '',723folderQueries: [724{725folder: fancySchemeFolderA726}727]728};729730const { results } = await runFileSearch(query);731compareURIs(results, reportedResults);732});733test('if onlyFileScheme is set, do not call custom schemes', async () => {734let fancySchemeCalled = false;735await registerTestFileSearchProvider({736provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {737fancySchemeCalled = true;738return Promise.resolve([]);739}740}, fancyScheme);741742const query: ISearchQuery = {743type: QueryType.File,744filePattern: '',745folderQueries: []746};747748await runFileSearch(query);749assert(!fancySchemeCalled);750});751});752753suite('Text:', () => {754755function makePreview(text: string): vscode.TextSearchMatch['preview'] {756return {757matches: [new Range(0, 0, 0, text.length)],758text759};760}761762function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchMatch {763return {764preview: makePreview('foo'),765ranges: [new Range(0, 0, 0, 3)],766uri: joinPath(baseFolder, relativePath)767};768}769770function getSimpleQuery(queryText: string): ITextQuery {771return {772type: QueryType.Text,773contentPattern: getPattern(queryText),774775folderQueries: [776{ folder: rootFolderA }777]778};779}780781function getPattern(queryText: string): IPatternInfo {782return {783pattern: queryText784};785}786787function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) {788const actualTextSearchResults: vscode.TextSearchResult[] = [];789for (const fileMatch of actual) {790// Make relative791for (const lineResult of fileMatch.results!) {792if (resultIsMatch(lineResult)) {793actualTextSearchResults.push({794preview: {795text: lineResult.previewText,796matches: mapArrayOrNot(797lineResult.rangeLocations.map(r => r.preview),798m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn))799},800ranges: mapArrayOrNot(801lineResult.rangeLocations.map(r => r.source),802r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn),803),804uri: fileMatch.resource805});806} else {807actualTextSearchResults.push(<vscode.TextSearchContext>{808text: lineResult.text,809lineNumber: lineResult.lineNumber,810uri: fileMatch.resource811});812}813}814}815816const rangeToString = (r: vscode.Range) => `(${r.start.line}, ${r.start.character}), (${r.end.line}, ${r.end.character})`;817818const makeComparable = (results: vscode.TextSearchResult[]) => results819.sort((a, b) => {820const compareKeyA = a.uri.toString() + ': ' + (extensionResultIsMatch(a) ? a.preview.text : a.text);821const compareKeyB = b.uri.toString() + ': ' + (extensionResultIsMatch(b) ? b.preview.text : b.text);822return compareKeyB.localeCompare(compareKeyA);823})824.map(r => extensionResultIsMatch(r) ? {825uri: r.uri.toString(),826range: mapArrayOrNot(r.ranges, rangeToString),827preview: {828text: r.preview.text,829match: null // Don't care about this right now830}831} : {832uri: r.uri.toString(),833text: r.text,834lineNumber: r.lineNumber835});836837return assert.deepStrictEqual(838makeComparable(actualTextSearchResults),839makeComparable(expected));840}841842test('no results', async () => {843await registerTestTextSearchProvider({844provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {845return Promise.resolve(null!);846}847});848849const { results, stats } = await runTextSearch(getSimpleQuery('foo'));850assert(!stats.limitHit);851assert(!results.length);852});853854test('basic results', async () => {855const providedResults: vscode.TextSearchResult[] = [856makeTextResult(rootFolderA, 'file1.ts'),857makeTextResult(rootFolderA, 'file2.ts')858];859860await registerTestTextSearchProvider({861provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {862providedResults.forEach(r => progress.report(r));863return Promise.resolve(null!);864}865});866867const { results, stats } = await runTextSearch(getSimpleQuery('foo'));868assert(!stats.limitHit);869assertResults(results, providedResults);870});871872test('all provider calls get global include/excludes', async () => {873await registerTestTextSearchProvider({874provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {875assert.strictEqual(options.includes.length, 1);876assert.strictEqual(options.excludes.length, 1);877return Promise.resolve(null!);878}879});880881const query: ITextQuery = {882type: QueryType.Text,883contentPattern: getPattern('foo'),884885includePattern: {886'*.ts': true887},888889excludePattern: {890'*.js': true891},892893folderQueries: [894{ folder: rootFolderA },895{ folder: rootFolderB }896]897};898899await runTextSearch(query);900});901902test('global/local include/excludes combined', async () => {903await registerTestTextSearchProvider({904provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {905if (options.folder.toString() === rootFolderA.toString()) {906assert.deepStrictEqual(options.includes.sort(), ['*.ts', 'foo']);907assert.deepStrictEqual(options.excludes.sort(), ['*.js', 'bar']);908} else {909assert.deepStrictEqual(options.includes.sort(), ['*.ts']);910assert.deepStrictEqual(options.excludes.sort(), ['*.js']);911}912913return Promise.resolve(null!);914}915});916917const query: ITextQuery = {918type: QueryType.Text,919contentPattern: getPattern('foo'),920921includePattern: {922'*.ts': true923},924excludePattern: {925'*.js': true926},927folderQueries: [928{929folder: rootFolderA,930includePattern: {931'foo': true932},933excludePattern: [{934pattern: {935'bar': true936}937}]938},939{ folder: rootFolderB }940]941};942943await runTextSearch(query);944});945946test('include/excludes resolved correctly', async () => {947await registerTestTextSearchProvider({948provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {949assert.deepStrictEqual(options.includes.sort(), ['*.jsx', '*.ts']);950assert.deepStrictEqual(options.excludes.sort(), []);951952return Promise.resolve(null!);953}954});955956const query: ISearchQuery = {957type: QueryType.Text,958contentPattern: getPattern('foo'),959960includePattern: {961'*.ts': true,962'*.jsx': false963},964excludePattern: {965'*.js': true,966'*.tsx': false967},968folderQueries: [969{970folder: rootFolderA,971includePattern: {972'*.jsx': true973},974excludePattern: [{975pattern: {976'*.js': false977}978}]979}980]981};982983await runTextSearch(query);984});985986test('provider fail', async () => {987await registerTestTextSearchProvider({988provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {989throw new Error('Provider fail');990}991});992993try {994await runTextSearch(getSimpleQuery('foo'));995assert(false, 'Expected to fail');996} catch {997// expected to fail998}999});10001001test('basic sibling clause', async () => {1002(mockPFS as any).Promises = {1003readdir: (_path: string): any => {1004if (_path === rootFolderA.fsPath) {1005return Promise.resolve([1006'file1.js',1007'file1.ts'1008]);1009} else {1010return Promise.reject(new Error('Wrong path'));1011}1012}1013};10141015const providedResults: vscode.TextSearchResult[] = [1016makeTextResult(rootFolderA, 'file1.js'),1017makeTextResult(rootFolderA, 'file1.ts')1018];10191020await registerTestTextSearchProvider({1021provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1022providedResults.forEach(r => progress.report(r));1023return Promise.resolve(null!);1024}1025});10261027const query: ISearchQuery = {1028type: QueryType.Text,1029contentPattern: getPattern('foo'),10301031excludePattern: {1032'*.js': {1033when: '$(basename).ts'1034}1035},10361037folderQueries: [1038{ folder: rootFolderA }1039]1040};10411042const { results } = await runTextSearch(query);1043assertResults(results, providedResults.slice(1));1044});10451046test('multiroot sibling clause', async () => {1047(mockPFS as any).Promises = {1048readdir: (_path: string): any => {1049if (_path === joinPath(rootFolderA, 'folder').fsPath) {1050return Promise.resolve([1051'fileA.scss',1052'fileA.css',1053'file2.css'1054]);1055} else if (_path === rootFolderB.fsPath) {1056return Promise.resolve([1057'fileB.ts',1058'fileB.js',1059'file3.js'1060]);1061} else {1062return Promise.reject(new Error('Wrong path'));1063}1064}1065};10661067await registerTestTextSearchProvider({1068provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1069let reportedResults;1070if (options.folder.fsPath === rootFolderA.fsPath) {1071reportedResults = [1072makeTextResult(rootFolderA, 'folder/fileA.scss'),1073makeTextResult(rootFolderA, 'folder/fileA.css'),1074makeTextResult(rootFolderA, 'folder/file2.css')1075];1076} else {1077reportedResults = [1078makeTextResult(rootFolderB, 'fileB.ts'),1079makeTextResult(rootFolderB, 'fileB.js'),1080makeTextResult(rootFolderB, 'file3.js')1081];1082}10831084reportedResults.forEach(r => progress.report(r));1085return Promise.resolve(null!);1086}1087});10881089const query: ISearchQuery = {1090type: QueryType.Text,1091contentPattern: getPattern('foo'),10921093excludePattern: {1094'*.js': {1095when: '$(basename).ts'1096},1097'*.css': true1098},1099folderQueries: [1100{1101folder: rootFolderA,1102excludePattern: [{1103pattern: {1104'folder/*.css': {1105when: '$(basename).scss'1106}1107}1108}]1109},1110{1111folder: rootFolderB,1112excludePattern: [{1113pattern: {1114'*.js': false1115}1116}]1117}1118]1119};11201121const { results } = await runTextSearch(query);1122assertResults(results, [1123makeTextResult(rootFolderA, 'folder/fileA.scss'),1124makeTextResult(rootFolderA, 'folder/file2.css'),1125makeTextResult(rootFolderB, 'fileB.ts'),1126makeTextResult(rootFolderB, 'fileB.js'),1127makeTextResult(rootFolderB, 'file3.js')]);1128});11291130test('include pattern applied', async () => {1131const providedResults: vscode.TextSearchResult[] = [1132makeTextResult(rootFolderA, 'file1.js'),1133makeTextResult(rootFolderA, 'file1.ts')1134];11351136await registerTestTextSearchProvider({1137provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1138providedResults.forEach(r => progress.report(r));1139return Promise.resolve(null!);1140}1141});11421143const query: ISearchQuery = {1144type: QueryType.Text,1145contentPattern: getPattern('foo'),11461147includePattern: {1148'*.ts': true1149},11501151folderQueries: [1152{ folder: rootFolderA }1153]1154};11551156const { results } = await runTextSearch(query);1157assertResults(results, providedResults.slice(1));1158});11591160test('max results = 1', async () => {1161const providedResults: vscode.TextSearchResult[] = [1162makeTextResult(rootFolderA, 'file1.ts'),1163makeTextResult(rootFolderA, 'file2.ts')1164];11651166let wasCanceled = false;1167await registerTestTextSearchProvider({1168provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1169disposables.add(token.onCancellationRequested(() => wasCanceled = true));1170providedResults.forEach(r => progress.report(r));1171return Promise.resolve(null!);1172}1173});11741175const query: ISearchQuery = {1176type: QueryType.Text,1177contentPattern: getPattern('foo'),11781179maxResults: 1,11801181folderQueries: [1182{ folder: rootFolderA }1183]1184};11851186const { results, stats } = await runTextSearch(query);1187assert(stats.limitHit, 'Expected to return limitHit');1188assertResults(results, providedResults.slice(0, 1));1189assert(wasCanceled, 'Expected to be canceled');1190});11911192test('max results = 2', async () => {1193const providedResults: vscode.TextSearchResult[] = [1194makeTextResult(rootFolderA, 'file1.ts'),1195makeTextResult(rootFolderA, 'file2.ts'),1196makeTextResult(rootFolderA, 'file3.ts')1197];11981199let wasCanceled = false;1200await registerTestTextSearchProvider({1201provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1202disposables.add(token.onCancellationRequested(() => wasCanceled = true));1203providedResults.forEach(r => progress.report(r));1204return Promise.resolve(null!);1205}1206});12071208const query: ISearchQuery = {1209type: QueryType.Text,1210contentPattern: getPattern('foo'),12111212maxResults: 2,12131214folderQueries: [1215{ folder: rootFolderA }1216]1217};12181219const { results, stats } = await runTextSearch(query);1220assert(stats.limitHit, 'Expected to return limitHit');1221assertResults(results, providedResults.slice(0, 2));1222assert(wasCanceled, 'Expected to be canceled');1223});12241225test('provider returns maxResults exactly', async () => {1226const providedResults: vscode.TextSearchResult[] = [1227makeTextResult(rootFolderA, 'file1.ts'),1228makeTextResult(rootFolderA, 'file2.ts')1229];12301231let wasCanceled = false;1232await registerTestTextSearchProvider({1233provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1234disposables.add(token.onCancellationRequested(() => wasCanceled = true));1235providedResults.forEach(r => progress.report(r));1236return Promise.resolve(null!);1237}1238});12391240const query: ISearchQuery = {1241type: QueryType.Text,1242contentPattern: getPattern('foo'),12431244maxResults: 2,12451246folderQueries: [1247{ folder: rootFolderA }1248]1249};12501251const { results, stats } = await runTextSearch(query);1252assert(!stats.limitHit, 'Expected not to return limitHit');1253assertResults(results, providedResults);1254assert(!wasCanceled, 'Expected not to be canceled');1255});12561257test('provider returns early with limitHit', async () => {1258const providedResults: vscode.TextSearchResult[] = [1259makeTextResult(rootFolderA, 'file1.ts'),1260makeTextResult(rootFolderA, 'file2.ts'),1261makeTextResult(rootFolderA, 'file3.ts')1262];12631264await registerTestTextSearchProvider({1265provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1266providedResults.forEach(r => progress.report(r));1267return Promise.resolve({ limitHit: true });1268}1269});12701271const query: ISearchQuery = {1272type: QueryType.Text,1273contentPattern: getPattern('foo'),12741275maxResults: 1000,12761277folderQueries: [1278{ folder: rootFolderA }1279]1280};12811282const { results, stats } = await runTextSearch(query);1283assert(stats.limitHit, 'Expected to return limitHit');1284assertResults(results, providedResults);1285});12861287test('multiroot max results', async () => {1288let cancels = 0;1289await registerTestTextSearchProvider({1290async provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1291disposables.add(token.onCancellationRequested(() => cancels++));1292await new Promise(r => process.nextTick(r));1293[1294'file1.ts',1295'file2.ts',1296'file3.ts',1297].forEach(f => progress.report(makeTextResult(options.folder, f)));1298return null!;1299}1300});13011302const query: ISearchQuery = {1303type: QueryType.Text,1304contentPattern: getPattern('foo'),13051306maxResults: 2,13071308folderQueries: [1309{ folder: rootFolderA },1310{ folder: rootFolderB }1311]1312};13131314const { results } = await runTextSearch(query);1315assert.strictEqual(results.length, 2);1316assert.strictEqual(cancels, 2);1317});13181319test('works with non-file schemes', async () => {1320const providedResults: vscode.TextSearchResult[] = [1321makeTextResult(fancySchemeFolderA, 'file1.ts'),1322makeTextResult(fancySchemeFolderA, 'file2.ts'),1323makeTextResult(fancySchemeFolderA, 'file3.ts')1324];13251326await registerTestTextSearchProvider({1327provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {1328providedResults.forEach(r => progress.report(r));1329return Promise.resolve(null!);1330}1331}, fancyScheme);13321333const query: ISearchQuery = {1334type: QueryType.Text,1335contentPattern: getPattern('foo'),13361337folderQueries: [1338{ folder: fancySchemeFolderA }1339]1340};13411342const { results } = await runTextSearch(query);1343assertResults(results, providedResults);1344});1345});1346});134713481349