提交 d4c2e7d8 编写于 作者: M Matej Urbas

file search: include workspace folder in filter

"Go to File..." search can now filter files based on the workspace folder name.

This feature is activated only when the workspace contains more than one folder.

This is particularly useful when your workspace contains multiple files with the same name, each of them in another workspace folder.

A common example is the `README.md` file. Say you want to find a `README.md` file from a particular folder, say `my-folder`. Before this change you'd have to press the `Down` button a few times before you could get to the file.

With this change you'd instead search for `mfREADME.md`. The desired readme file should now appear closer to the top of the file search popup.
上级 89cafb24
......@@ -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;
......
......@@ -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<boolean> {
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,
......
......@@ -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 {
......
......@@ -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 <ITextQuery>{
...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 <IFileQuery>{
...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<uri> {
private commonQuery(folderResources: IWorkspaceFolderData[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps<uri> {
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<uri> = {
......@@ -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<ISearchConfiguration>({ resource: folder });
const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder.uri });
const settingExcludes = this.getExcludesForFolder(folderConfig, options);
const excludePattern: glob.IExpression = {
...(settingExcludes || {}),
......@@ -436,7 +437,8 @@ export class QueryBuilder {
};
return <IFolderQuery>{
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,
......
......@@ -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);
......
......@@ -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<U extends UriComponents = URI> {
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<T> {
......@@ -486,6 +499,11 @@ export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg i
return !!(<ISerializedFileMatch>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[];
......
......@@ -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<IRawFileMatch> {
......
......@@ -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;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册