Skip to content

Commit

Permalink
Add Whitespace Query Support to Quick File Open
Browse files Browse the repository at this point in the history
What it Does

Fixes [#8747](#8747)

- Allows whitespaces to be included in a `quick file open` search query.
- Performs both an exact and fuzzy search on the query with whitespaces.
- Adds tests for whitespace queries (considers fuzzy matching and search
  term order).

How to Test

1. `ctrl + p` to `quick file open`
2. Search for a file with a query that includes whitespaces (eg. `readme core`)
3. Observe that whitespaces do not affect the search results

Alternatively, run `@theia/file-search` tests.

Signed-off-by: seantan22 <sean.a.tan@ericsson.com>
  • Loading branch information
seantan22 committed Jan 27, 2021
1 parent 47be972 commit 23ca2a3
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 11 deletions.
43 changes: 35 additions & 8 deletions packages/file-search/src/browser/quick-file-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,22 +167,34 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
return;
}
const fileSearchResultItems: QuickOpenItem[] = [];
const pathSearchResultItems: QuickOpenItem[] = [];

if (results.length <= 0) {
acceptor([this.toNoResultsItem()]);
return;
}

for (const fileUri of results) {
if (!alreadyCollected.has(fileUri)) {
const item = this.toItem(fileUri);
fileSearchResultItems.push(item);
alreadyCollected.add(fileUri);
this.updateAlreadyCollected(fileSearchResultItems, results, alreadyCollected);

// Create a copy of the file search results
const exactMatches = fileSearchResultItems.slice();

// Filter out exact filename matches & add to `exactMatches`
const filenames = // TODO: how to get filenames? extract them from paths?
for (const filename of filenames) {
const uriString = filename.uri.toString();
const patternExists = filename.toLocaleLowerCase().indexOf(lookFor) !== -1; // exact match
if (!alreadyCollected.has(uriString) && patternExists) {
const item = this.toItem(filename.uri);
exactMatches.push(item);
alreadyCollected.add(uriString);
}
}

// Create a copy of the file search results and sort.
const sortedResults = fileSearchResultItems.slice();
this.updateAlreadyCollected(pathSearchResultItems, results, alreadyCollected);

// Create a copy of the remaining file search results and sort.
const sortedResults = pathSearchResultItems.slice();
sortedResults.sort((a, b) => this.compareItems(a, b));

// Extract the first element, and re-add it to the array with the group label.
Expand All @@ -193,7 +205,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
sortedResults.unshift(item);
}
// Return the recently used items, followed by the search results.
acceptor([...recentlyUsedItems, ...sortedResults]);
acceptor([...recentlyUsedItems, ...exactMatches, ...sortedResults]);
};

this.fileSearchService.find(lookFor, {
Expand All @@ -212,6 +224,16 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
}
}

private updateAlreadyCollected(searchResultItems: QuickOpenItem[], results: string[], collectedSet: Set<string>): void {
for (const fileUri of results) {
if (!collectedSet.has(fileUri)) {
const item = this.toItem(fileUri);
searchResultItems.push(item);
collectedSet.add(fileUri);
}
}
}

protected getRunFunction(uri: URI): (mode: QuickOpenMode) => boolean {
return (mode: QuickOpenMode) => {
if (mode !== QuickOpenMode.OPEN) {
Expand Down Expand Up @@ -254,6 +276,11 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
* @returns the score.
*/
function score(str: string): number {

// TODO: Check exact matches


// Check fuzzy matches
const match = fuzzy.match(query, str);
// eslint-disable-next-line no-null/no-null
return (match === null) ? 0 : match.score;
Expand Down
30 changes: 30 additions & 0 deletions packages/file-search/src/node/file-search-service-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,34 @@ describe('search-service', function (): void {
});
});

describe('search with whitespaces', () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString();

it('should support file searches with whitespaces', async () => {
const matches = await service.find('foo sub', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });

expect(matches).to.be.length(2);
expect(matches[0].endsWith('subdir1/sub-bar/foo.txt'));
expect(matches[1].endsWith('subdir1/sub2/foo.txt'));
});

it('should support fuzzy file searches with whitespaces', async () => {
const matchesExact = await service.find('foo sbd2', { rootUris: [rootUri], fuzzyMatch: false, useGitIgnore: true, limit: 200 });
const matchesFuzzy = await service.find('foo sbd2', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });

expect(matchesExact).to.be.length(0);
expect(matchesFuzzy).to.be.length(1);
expect(matchesFuzzy[0].endsWith('subdir1/sub2/foo.txt'));
});

it('should support file searches with whitespaces regardless of order', async () => {
const matchesA = await service.find('foo sub', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
const matchesB = await service.find('sub foo', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });

expect(matchesA).to.not.be.empty;
expect(matchesB).to.not.be.empty;
expect(matchesA).to.deep.eq(matchesB);
});
});

});
21 changes: 18 additions & 3 deletions packages/file-search/src/node/file-search-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,39 @@ export class FileSearchServiceImpl implements FileSearchService {
searchPattern = searchPattern.replace(/\//g, '\\');
}

const WHITESPACE_QUERY_SEPARATOR = ' ';
const stringPattern = searchPattern.toLocaleLowerCase();
const stringPatterns = stringPattern.split(WHITESPACE_QUERY_SEPARATOR);

await Promise.all(Object.keys(roots).map(async root => {
try {
const rootUri = new URI(root);
const rootPath = FileUri.fsPath(rootUri);
const rootOptions = roots[root];

await this.doFind(rootUri, rootOptions, candidate => {

// Convert OS-native candidate path to a file URI string
const fileUri = FileUri.create(path.resolve(rootPath, candidate)).toString();

// Skip results that have already been matched.
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
return;
}
if (!searchPattern || searchPattern === '*' || candidate.toLocaleLowerCase().indexOf(stringPattern) !== -1) {

// Determine if the candidate matches any of the patterns exactly or fuzzy
const patternExists = stringPatterns.every(pattern => candidate.toLocaleLowerCase().indexOf(pattern) !== -1);
if (patternExists) {
exactMatches.add(fileUri);
} else if (!searchPattern || searchPattern === '*') {
exactMatches.add(fileUri);
} else if (opts.fuzzyMatch && fuzzy.test(searchPattern, candidate)) {
fuzzyMatches.add(fileUri);
} else {
const fuzzyPatternExists = stringPatterns.every(pattern => fuzzy.test(pattern, candidate));
if (opts.fuzzyMatch && fuzzyPatternExists) {
fuzzyMatches.add(fileUri);
}
}

// Preemptively terminate the search when the list of exact matches reaches the limit.
if (exactMatches.size === opts.limit) {
cancellationSource.cancel();
Expand Down

0 comments on commit 23ca2a3

Please sign in to comment.