From 902daddaf8c4b8f74c1d97b18c49873742b7a33f Mon Sep 17 00:00:00 2001 From: seantan22 Date: Fri, 22 Jan 2021 11:35:10 -0500 Subject: [PATCH] Add Whitespace Query Support to Quick File Open What it Does Fixes [#8747](https://github.com/eclipse-theia/theia/issues/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 --- packages/debug/src/browser/style/index.css | 2 +- .../src/node/file-search-service-impl.spec.ts | 31 +++++++++++++++++++ .../src/node/file-search-service-impl.ts | 21 +++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index 8431a3b075f3e..08b40239996f9 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -203,7 +203,7 @@ /** Editor **/ .monaco-editor .theia-debug-breakpoint-hint { - background: url('breakpoint-hint.svg') center center no-repeat; + background: url('breakpoint-hint.svg') center center no-repeat; } .theia-debug-breakpoint-icon { diff --git a/packages/file-search/src/node/file-search-service-impl.spec.ts b/packages/file-search/src/node/file-search-service-impl.spec.ts index cc2f34ccb7477..af56ecae1584f 100644 --- a/packages/file-search/src/node/file-search-service-impl.spec.ts +++ b/packages/file-search/src/node/file-search-service-impl.spec.ts @@ -195,4 +195,35 @@ describe('search-service', function (): void { }); }); + describe('search with whitespaces', () => { + it('should support file searches with whitespaces', async () => { + const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString(); + 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 rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString(); + 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 rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString(); + 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); + }); + }); + }); diff --git a/packages/file-search/src/node/file-search-service-impl.ts b/packages/file-search/src/node/file-search-service-impl.ts index da28c10890c96..209d3fbddca6d 100644 --- a/packages/file-search/src/node/file-search-service-impl.ts +++ b/packages/file-search/src/node/file-search-service-impl.ts @@ -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();