diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 7e31738058d..6ccb03fa620 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -115,7 +115,7 @@ export interface IWorkspaceFolderData { /** * The name of this workspace folder. Defaults to - * the basename its [uri-path](#Uri.path) + * the basename of its [uri-path](#Uri.path) */ readonly name: string; diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 7d67852018a..bf9dc7b1088 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -12,7 +12,7 @@ import { isNative } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -138,7 +138,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } const query = this._queryBuilder.file( - includeFolder ? [includeFolder] : workspace.folders.map(f => f.uri), + includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders, { maxResults: withNullAsUndefined(maxResults), disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined, @@ -190,7 +190,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { $checkExists(folders: UriComponents[], includes: string[], token: CancellationToken): Promise { const queryBuilder = this._instantiationService.createInstance(QueryBuilder); - const query = queryBuilder.file(folders.map(folder => URI.revive(folder)), { + const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), { _reason: 'checkExists', includePattern: includes.join(', '), expandPatterns: true, diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index e3c5e565472..f4a5f62c36a 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -167,7 +167,11 @@ export class OpenFileHandler extends QuickOpenHandler { } else { - complete = await this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); + let fileQuery = this.queryBuilder.file( + this.contextService.getWorkspace().folders, + queryOptions + ); + complete = await this.searchService.fileSearch(fileQuery, token); } const results: QuickOpenEntry[] = []; @@ -238,10 +242,7 @@ export class OpenFileHandler extends QuickOpenHandler { sortByScore: true, }; - const folderResources = this.contextService.getWorkspace().folders.map(folder => folder.uri); - const query = this.queryBuilder.file(folderResources, options); - - return query; + return this.queryBuilder.file(this.contextService.getWorkspace().folders, options); } get isCacheLoaded(): boolean { diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index ee91d6edcf2..92c69e2691d 100644 --- a/src/vs/workbench/contrib/search/common/queryBuilder.ts +++ b/src/vs/workbench/contrib/search/common/queryBuilder.ts @@ -16,7 +16,7 @@ import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, toWorkspaceFolder, IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/workbench/services/search/common/search'; import { Schemas } from 'vs/base/common/network'; @@ -94,7 +94,7 @@ export class QueryBuilder { return !folderConfig.search.useRipgrep; }); - const commonQuery = this.commonQuery(folderResources, options); + const commonQuery = this.commonQuery(folderResources?.map(toWorkspaceFolder), options); return { ...commonQuery, type: QueryType.Text, @@ -134,8 +134,8 @@ export class QueryBuilder { return newPattern; } - file(folderResources: uri[] | undefined, options: IFileQueryBuilderOptions = {}): IFileQuery { - const commonQuery = this.commonQuery(folderResources, options); + file(folders: IWorkspaceFolderData[], options: IFileQueryBuilderOptions = {}): IFileQuery { + const commonQuery = this.commonQuery(folders, options); return { ...commonQuery, type: QueryType.File, @@ -144,11 +144,11 @@ export class QueryBuilder { : options.filePattern, exists: options.exists, sortByScore: options.sortByScore, - cacheKey: options.cacheKey + cacheKey: options.cacheKey, }; } - private commonQuery(folderResources: uri[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { + private commonQuery(folderResources: IWorkspaceFolderData[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { let includeSearchPathsInfo: ISearchPathsInfo = {}; if (options.includePattern) { const includePattern = normalizeSlashes(options.includePattern); @@ -166,9 +166,10 @@ export class QueryBuilder { } // Build folderQueries from searchPaths, if given, otherwise folderResources + const includeFolderName = folderResources.length > 1; const folderQueries = (includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length ? includeSearchPathsInfo.searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath, options, excludeSearchPathsInfo)) : - folderResources.map(uri => this.getFolderQueryForRoot(uri, options, excludeSearchPathsInfo))) + folderResources.map(folder => this.getFolderQueryForRoot(folder, options, excludeSearchPathsInfo, includeFolderName))) .filter(query => !!query) as IFolderQuery[]; const queryProps: ICommonQueryProps = { @@ -403,7 +404,7 @@ export class QueryBuilder { } private getFolderQueryForSearchPath(searchPath: ISearchPathPattern, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null { - const rootConfig = this.getFolderQueryForRoot(searchPath.searchPath, options, searchPathExcludes); + const rootConfig = this.getFolderQueryForRoot(toWorkspaceFolder(searchPath.searchPath), options, searchPathExcludes, false); if (!rootConfig) { return null; } @@ -416,10 +417,10 @@ export class QueryBuilder { }; } - private getFolderQueryForRoot(folder: uri, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null { + private getFolderQueryForRoot(folder: IWorkspaceFolderData, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo, includeFolderName: boolean): IFolderQuery | null { let thisFolderExcludeSearchPathPattern: glob.IExpression | undefined; if (searchPathExcludes.searchPaths) { - const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder))[0]; + const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder.uri))[0]; if (thisFolderExcludeSearchPath && !thisFolderExcludeSearchPath.pattern) { // entire folder is excluded return null; @@ -428,7 +429,7 @@ export class QueryBuilder { } } - const folderConfig = this.configurationService.getValue({ resource: folder }); + const folderConfig = this.configurationService.getValue({ resource: folder.uri }); const settingExcludes = this.getExcludesForFolder(folderConfig, options); const excludePattern: glob.IExpression = { ...(settingExcludes || {}), @@ -436,7 +437,8 @@ export class QueryBuilder { }; return { - folder, + folder: folder.uri, + folderName: includeFolderName ? folder.name : undefined, excludePattern: Object.keys(excludePattern).length > 0 ? excludePattern : undefined, fileEncoding: folderConfig.files && folderConfig.files.encoding, disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search.useIgnoreFiles, diff --git a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts index 26f6e129337..621cf435ac4 100644 --- a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts @@ -25,6 +25,7 @@ suite('QueryBuilder', () => { const PATTERN_INFO: IPatternInfo = { pattern: 'a' }; const ROOT_1 = fixPath('/foo/root1'); const ROOT_1_URI = getUri(ROOT_1); + const ROOT_1_NAMED_FOLDER = toWorkspaceFolder(ROOT_1_URI); const WS_CONFIG_PATH = getUri('/bar/test.code-workspace'); // location of the workspace file (not important except that it is a file URI) let instantiationService: TestInstantiationService; @@ -89,7 +90,10 @@ suite('QueryBuilder', () => { test('does not split glob pattern when expandPatterns disabled', () => { assertEqualQueries( - queryBuilder.file([ROOT_1_URI], { includePattern: '**/foo, **/bar' }), + queryBuilder.file( + [ROOT_1_NAMED_FOLDER], + { includePattern: '**/foo, **/bar' }, + ), { folderQueries: [{ folder: ROOT_1_URI @@ -362,7 +366,7 @@ suite('QueryBuilder', () => { const content = 'content'; assertEqualQueries( queryBuilder.file( - undefined, + [], { filePattern: ` ${content} ` } ), { @@ -902,10 +906,13 @@ suite('QueryBuilder', () => { suite('file', () => { test('simple file query', () => { const cacheKey = 'asdf'; - const query = queryBuilder.file([ROOT_1_URI], { - cacheKey, - sortByScore: true - }); + const query = queryBuilder.file( + [ROOT_1_NAMED_FOLDER], + { + cacheKey, + sortByScore: true + }, + ); assert.equal(query.folderQueries.length, 1); assert.equal(query.cacheKey, cacheKey); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index e646364a885..24b80d78385 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -9,7 +9,7 @@ import * as glob from 'vs/base/common/glob'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as extpath from 'vs/base/common/extpath'; -import { getNLines } from 'vs/base/common/strings'; +import { fuzzyContains, getNLines } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -50,6 +50,7 @@ export interface ISearchResultProvider { export interface IFolderQuery { folder: U; + folderName?: string; excludePattern?: glob.IExpression; includePattern?: glob.IExpression; fileEncoding?: string; @@ -437,9 +438,21 @@ export interface IRawSearchService { export interface IRawFileMatch { base?: string; + /** + * The path of the file relative to the containing `base` folder. + * This path is exactly as it appears on the filesystem. + */ relativePath: string; basename: string; size?: number; + /** + * This path is transformed for search purposes. For example, this could be + * the `relativePath` with the workspace folder name prepended. This way the + * search algorithm would also match against the name of the containing folder. + * + * If not given, the search algorithm should use `relativePath`. + */ + searchPath?: string; } export interface ISearchEngine { @@ -486,6 +499,11 @@ export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg i return !!(arg).path; } +export function isFilePatternMatch(candidate: IRawFileMatch, normalizedFilePatternLowercase: string): boolean { + const pathToMatch = candidate.searchPath ? candidate.searchPath : candidate.relativePath; + return fuzzyContains(pathToMatch, normalizedFilePatternLowercase); +} + export interface ISerializedFileMatch { path: string; results?: ITextSearchResult[]; diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index e4d0dbbc75b..a3be0c1ad46 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -20,7 +20,7 @@ import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { readdir } from 'vs/base/node/pfs'; -import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search'; +import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; @@ -247,7 +247,7 @@ export class FileWalker { if (noSiblingsClauses) { for (const relativePath of relativeFiles) { const basename = path.basename(relativePath); - this.matchFile(onResult, { base: rootFolder, relativePath, basename }); + this.matchFile(onResult, { base: rootFolder, relativePath, searchPath: this.getSearchPath(folderQuery, relativePath), basename }); if (this.isLimitHit) { killCmd(); break; @@ -540,7 +540,13 @@ export class FileWalker { return clb(null, undefined); // ignore file if max file size is hit } - this.matchFile(onResult, { base: rootFolder.fsPath, relativePath: currentRelativePath, basename: file, size: stat.size }); + this.matchFile(onResult, { + base: rootFolder.fsPath, + relativePath: currentRelativePath, + searchPath: this.getSearchPath(folderQuery, currentRelativePath), + basename: file, + size: stat.size, + }); } // Unwind @@ -554,7 +560,7 @@ export class FileWalker { } private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void { - if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { + if (this.isFileMatch(candidate) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { this.resultCount++; if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) { @@ -567,8 +573,7 @@ export class FileWalker { } } - private isFilePatternMatch(path: string): boolean { - + private isFileMatch(candidate: IRawFileMatch): boolean { // Check for search pattern if (this.filePattern) { if (this.filePattern === '*') { @@ -576,7 +581,7 @@ export class FileWalker { } if (this.normalizedFilePatternLowercase) { - return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); + return isFilePatternMatch(candidate, this.normalizedFilePatternLowercase); } } @@ -605,6 +610,19 @@ export class FileWalker { return clb(null, path); } + + /** + * If we're searching for files in multiple workspace folders, then better prepend the + * name of the workspace folder to the path of the file. This way we'll be able to + * better filter files that are all on the top of a workspace folder and have all the + * same name. A typical example are `package.json` or `README.md` files. + */ + private getSearchPath(folderQuery: IFolderQuery, relativePath: string): string { + if (folderQuery.folderName) { + return path.join(folderQuery.folderName, relativePath); + } + return relativePath; + } } export class Engine implements ISearchEngine { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 336d4cd0350..92bb48351e0 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -17,7 +17,7 @@ import * as strings from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { MAX_FILE_SIZE } from 'vs/base/node/pfs'; -import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search'; +import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; @@ -316,7 +316,7 @@ export class SearchService implements IRawSearchService { for (const entry of cachedEntries) { // Check if this entry is a match for the search value - if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { + if (!isFilePatternMatch(entry, normalizedSearchValueLowercase)) { continue; }