Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow search provider results to be outside of folder #227256

Merged
merged 8 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/vs/workbench/api/test/node/extHostSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,25 @@ suite('ExtHostSearch', () => {
const { results } = await runFileSearch(query);
compareURIs(results, reportedResults);
});

test('Works when provider returns files that are not in the original workspace', async () => {
const reportedResults = [
joinPath(rootFolderB, 'file1.ts'),
joinPath(rootFolderA, 'file2.ts'),
joinPath(rootFolderA, 'subfolder/file3.ts')
];

await registerTestFileSearchProvider({
provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<URI[]> {
return Promise.resolve(reportedResults);
}
});

const { results, stats } = await runFileSearch(getSimpleQuery());
assert(!stats.limitHit);
assert.strictEqual(results.length, 3);
compareURIs(results, reportedResults);
});
});

suite('Text:', () => {
Expand Down Expand Up @@ -1296,5 +1315,24 @@ suite('ExtHostSearch', () => {
const { results } = await runTextSearch(query);
assertResults(results, providedResults);
});


test('Works when provider returns files that are not in the original workspace', async () => {
const providedResults: vscode.TextSearchResult[] = [
makeTextResult(rootFolderB, 'file1.ts'),
makeTextResult(rootFolderA, 'file2.ts')
];

await registerTestTextSearchProvider({
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
providedResults.forEach(r => progress.report(r));
return Promise.resolve(null!);
}
});

const { results, stats } = await runTextSearch(getSimpleQuery('foo'));
assert(!stats.limitHit);
assertResults(results, providedResults);
});
});
});
10 changes: 5 additions & 5 deletions src/vs/workbench/services/search/common/fileSearchManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search';
import { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes';
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { Disposable } from 'vs/base/common/lifecycle';
import { OldFileSearchProviderConverter } from 'vs/workbench/services/search/common/searchExtConversionTypes';
import { ResourceMap } from 'vs/base/common/map';

interface IInternalFileMatch {
base: URI;
Expand Down Expand Up @@ -126,7 +126,7 @@ class FileSearchEngine {
};


const folderMappings: TernarySearchTree<URI, FolderQueryInfo> = TernarySearchTree.forUris<FolderQueryInfo>();
const folderMappings = new ResourceMap<FolderQueryInfo>();
fqs.forEach(fq => {
const queryTester = new QueryGlobTester(this.config, fq);
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
Expand All @@ -153,9 +153,9 @@ class FileSearchEngine {


if (results) {
results.forEach(result => {

const fqFolderInfo = folderMappings.findSubstr(result)!;
results.forEach(resultInfo => {
const result = resultInfo.result;
const fqFolderInfo = folderMappings.get(resultInfo.folder)!;
const relativePath = path.posix.relative(fqFolderInfo.folder.path, result.path);

if (fqFolderInfo.noSiblingsClauses) {
Expand Down
41 changes: 28 additions & 13 deletions src/vs/workbench/services/search/common/searchExtConversionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { IProgress } from 'vs/platform/progress/common/progress';
import { DEFAULT_TEXT_SEARCH_PREVIEW_OPTIONS } from 'vs/workbench/services/search/common/search';
import { Range, FileSearchProviderNew, FileSearchProviderOptions, ProviderResult, TextSearchCompleteNew, TextSearchContextNew, TextSearchMatchNew, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew, AITextSearchProviderNew, TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes';
import { Range, FileSearchProviderNew, FileSearchProviderOptions, ProviderResult, TextSearchCompleteNew, TextSearchContextNew, TextSearchMatchNew, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew, AITextSearchProviderNew, TextSearchCompleteMessage, SearchResultFromFolder } from 'vs/workbench/services/search/common/searchExtTypes';

// old types that are retained for backward compatibility
// TODO: delete this when search apis are adopted by all first-party extensions
Expand Down Expand Up @@ -470,13 +470,22 @@ function newToOldFileProviderOptions(options: FileSearchProviderOptions): FileSe
export class OldFileSearchProviderConverter implements FileSearchProviderNew {
constructor(private provider: FileSearchProvider) { }

provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<URI[]> {
const getResult = async () => {
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<SearchResultFromFolder<URI>[]> {
const getResult: () => Promise<{
uris: URI[] | null | undefined;
folder: URI;
}[]> = async () => {
const newOpts = newToOldFileProviderOptions(options);
return Promise.all(newOpts.map(
o => this.provider.provideFileSearchResults({ pattern }, o, token)));
async o => ({ uris: await this.provider.provideFileSearchResults({ pattern }, o, token), folder: o.folder })));
};
return getResult().then(e => coalesce(e).flat());
return getResult().then(rawResult =>
coalesce(rawResult.flatMap(e => (
e.uris?.map(uri => (
{ result: uri, folder: e.folder }
)))
))
);
}
}

Expand Down Expand Up @@ -528,19 +537,22 @@ export function oldToNewTextSearchResult(result: TextSearchResult): TextSearchRe
export class OldTextSearchProviderConverter implements TextSearchProviderNew {
constructor(private provider: TextSearchProvider) { }

provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew> {
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew> {

const progressShim = (oldResult: TextSearchResult) => {
const progressShim = (oldResult: TextSearchResult, folder: URI) => {
if (!validateProviderResult(oldResult)) {
return;
}
progress.report(oldToNewTextSearchResult(oldResult));
progress.report({
folder,
result: oldToNewTextSearchResult(oldResult)
});
};

const getResult = async () => {
return coalesce(await Promise.all(
newToOldTextProviderOptions(options).map(
o => this.provider.provideTextSearchResults(query, o, { report: (e) => progressShim(e) }, token))))
o => this.provider.provideTextSearchResults(query, o, { report: (e) => progressShim(e, o.folder) }, token))))
.reduce(
(prev, cur) => ({ limitHit: prev.limitHit || cur.limitHit }),
{ limitHit: false }
Expand All @@ -559,18 +571,21 @@ export class OldTextSearchProviderConverter implements TextSearchProviderNew {
export class OldAITextSearchProviderConverter implements AITextSearchProviderNew {
constructor(private provider: AITextSearchProvider) { }

provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew> {
const progressShim = (oldResult: TextSearchResult) => {
provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew> {
const progressShim = (oldResult: TextSearchResult, folder: URI) => {
if (!validateProviderResult(oldResult)) {
return;
}
progress.report(oldToNewTextSearchResult(oldResult));
progress.report({
folder,
result: oldToNewTextSearchResult(oldResult)
});
};

const getResult = async () => {
return coalesce(await Promise.all(
newToOldTextProviderOptions(options).map(
o => this.provider.provideAITextSearchResults(query, o, { report: (e) => progressShim(e) }, token))))
o => this.provider.provideAITextSearchResults(query, o, { report: (e) => progressShim(e, o.folder) }, token))))
.reduce(
(prev, cur) => ({ limitHit: prev.limitHit || cur.limitHit }),
{ limitHit: false }
Expand Down
49 changes: 11 additions & 38 deletions src/vs/workbench/services/search/common/searchExtTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,41 +320,6 @@ export class TextSearchContextNew {
*/
export type TextSearchResultNew = TextSearchMatchNew | TextSearchContextNew;


/**
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions.
*
* A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for
* all files that match the user's query.
*
* The FileSearchProvider will be invoked on every keypress in quickaccess. When `workspace.findFiles` is called, it will be invoked with an empty query string,
* and in that case, every file in the folder should be returned.
*/
export interface FileSearchProviderNew {
/**
* Provide the set of files that match a certain file path pattern.
* @param query The parameters for this query.
* @param options A set of options to consider while searching files.
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<URI[]>;
}

/**
* A TextSearchProvider provides search results for text results inside files in the workspace.
*/
export interface TextSearchProviderNew {
/**
* Provide results that match the given text pattern.
* @param query The parameters for this query.
* @param options A set of options to consider while searching.
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}

/**
* Information collected when text search is complete.
*/
Expand Down Expand Up @@ -417,7 +382,7 @@ export interface FileSearchProviderNew {
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<URI[]>;
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<SearchResultFromFolder<URI>[]>;
}

/**
Expand All @@ -431,7 +396,7 @@ export interface TextSearchProviderNew {
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}

/**
Expand Down Expand Up @@ -537,5 +502,13 @@ export interface AITextSearchProviderNew {
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}

/**
* A wrapper for a search result that indicates the original workspace folder that this result was found for.
*/
export type SearchResultFromFolder<T> = {
result: T;
folder: URI;
};
12 changes: 6 additions & 6 deletions src/vs/workbench/services/search/common/textSearchManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import { isThenable } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { URI } from 'vs/base/common/uri';
import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, excludeToGlobPattern, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider, ISearchRange, DEFAULT_TEXT_SEARCH_PREVIEW_OPTIONS } from 'vs/workbench/services/search/common/search';
import { AITextSearchProviderNew, TextSearchCompleteNew, TextSearchMatchNew, TextSearchProviderFolderOptions, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes';
import { AITextSearchProviderNew, SearchResultFromFolder, TextSearchCompleteNew, TextSearchMatchNew, TextSearchProviderFolderOptions, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes';

export interface IFileUtils {
readdir: (resource: URI) => Promise<string[]>;
Expand Down Expand Up @@ -122,20 +122,20 @@ export class TextSearchManager {
}

private async doSearch(folderQueries: IFolderQuery<URI>[], onResult: (result: TextSearchResultNew, folderIdx: number) => void, token: CancellationToken): Promise<TextSearchCompleteNew | null | undefined> {
const folderMappings: TernarySearchTree<URI, FolderQueryInfo> = TernarySearchTree.forUris<FolderQueryInfo>();
const folderMappings: ResourceMap<FolderQueryInfo> = new ResourceMap<FolderQueryInfo>();
folderQueries.forEach((fq, i) => {
const queryTester = new QueryGlobTester(this.query, fq);
folderMappings.set(fq.folder, { queryTester, folder: fq.folder, folderIdx: i });
});

const testingPs: Promise<void>[] = [];
const progress = {
report: (result: TextSearchResultNew) => {

report: (resultInfo: SearchResultFromFolder<TextSearchResultNew>) => {
const result = resultInfo.result;
if (result.uri === undefined) {
throw Error('Text search result URI is undefined. Please check provider implementation.');
}
const folderQuery = folderMappings.findSubstr(result.uri)!;
const folderQuery = folderMappings.get(resultInfo.folder)!;
const hasSibling = folderQuery.folder.scheme === Schemas.file ?
hasSiblingPromiseFn(() => {
return this.fileUtils.readdir(resources.dirname(result.uri));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ declare module 'vscode' {
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: Progress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: Progress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}

export namespace workspace {
Expand Down
3 changes: 2 additions & 1 deletion src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ declare module 'vscode' {
* @param pattern The search pattern to match against file paths.
* @param options A set of options to consider while searching files.
* @param token A cancellation token.
* @returns A list of matching URIs, where each URI is associated with a workspace folder.
*/
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<Uri[]>;
provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult<SearchResultFromFolder<Uri>[]>;
}

export namespace workspace {
Expand Down
10 changes: 9 additions & 1 deletion src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ declare module 'vscode' {
*/
export type TextSearchResultNew = TextSearchMatchNew | TextSearchContextNew;

/**
* A wrapper for a search result that indicates the original workspace folder that this result was found for.
*/
export type SearchResultFromFolder<T> = {
result: T;
folder: Uri;
};

/**
* A TextSearchProvider provides search results for text results inside files in the workspace.
*/
Expand All @@ -257,7 +265,7 @@ declare module 'vscode' {
* These results can be direct matches, or context that surrounds matches.
* @param token A cancellation token.
*/
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress<SearchResultFromFolder<TextSearchResultNew>>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}

export namespace workspace {
Expand Down