diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 55438ee410a3a39462471303349e0bbb1b2df4a7..3dd44ffc0df8a7b3e47fb2010a83b8b50e006961 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -23,7 +23,7 @@ export interface ISearchService { export interface IQueryOptions { rootResources?: uri[]; - filePatterns?: IPatternInfo[]; + filePattern?: string; excludePattern?: glob.IExpression; includePattern?: glob.IExpression; maxResults?: number; diff --git a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts index 8b04c1fcc1c88d877f87a7bb432b39903f27f4b7..706f85652609a268f012e6376f609728861083b5 100644 --- a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts +++ b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts @@ -50,7 +50,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { this.openSymbolHandler.setStandalone(false); this.openFileHandler.setStandalone(false); - this.resultsToSearchCache = {}; + this.resultsToSearchCache = Object.create(null); this.delayer = new ThrottledDelayer(OpenAnythingHandler.SEARCH_DELAY); } @@ -201,7 +201,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { // Find cache entries by prefix of search value let cachedEntries: QuickOpenEntry[]; for (let previousSearch in this.resultsToSearchCache) { - if (this.resultsToSearchCache.hasOwnProperty(previousSearch) && searchValue.indexOf(previousSearch) === 0) { + if (searchValue.indexOf(previousSearch) === 0) { cachedEntries = this.resultsToSearchCache[previousSearch]; break; } @@ -257,7 +257,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { this.isClosed = true; // Clear Cache - this.resultsToSearchCache = {}; + this.resultsToSearchCache = Object.create(null); // Propagate this.openSymbolHandler.onClose(canceled); diff --git a/src/vs/workbench/parts/search/browser/openFileHandler.ts b/src/vs/workbench/parts/search/browser/openFileHandler.ts index 9c3287e747794b9f02d546206b141106c1ad1740..92e718dee109bb1448b2ea21adf8bf2c8441927b 100644 --- a/src/vs/workbench/parts/search/browser/openFileHandler.ts +++ b/src/vs/workbench/parts/search/browser/openFileHandler.ts @@ -138,7 +138,7 @@ export class OpenFileHandler extends QuickOpenHandler { rootResources.push(this.contextService.getWorkspace().resource); } - let query: IQueryOptions = { filePatterns: [{ pattern: searchValue }], rootResources: rootResources }; + let query: IQueryOptions = { filePattern: searchValue, rootResources: rootResources }; return this.queryBuilder.file(query).then((query) => { this.pendingSearch = this.searchService.search(query); diff --git a/src/vs/workbench/parts/search/common/searchQuery.ts b/src/vs/workbench/parts/search/common/searchQuery.ts index 1ecc167b1dbc1fa3ee672ddb8cd5885b52ed342b..898d4b4b1b27f58f1649ce0821e82b45cf6c5392 100644 --- a/src/vs/workbench/parts/search/common/searchQuery.ts +++ b/src/vs/workbench/parts/search/common/searchQuery.ts @@ -58,7 +58,7 @@ export class QueryBuilder { return { type: type, rootResources: options.rootResources, - filePatterns: options.filePatterns || [], + filePattern: options.filePattern, excludePattern: options.excludePattern, includePattern: options.includePattern, maxResults: options.maxResults, diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index bcafef13d50978c8b188b7624bb172a0f86a2f43..39cce1d526f42589f55cfa3ee1b6e219dbbc7159 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -9,6 +9,7 @@ import fs = require('fs'); import paths = require('path'); import types = require('vs/base/common/types'); +import filters = require('vs/base/common/filters'); import arrays = require('vs/base/common/arrays'); import strings = require('vs/base/common/strings'); import glob = require('vs/base/common/glob'); @@ -18,159 +19,12 @@ import extfs = require('vs/base/node/extfs'); import flow = require('vs/base/node/flow'); import {ISerializedFileMatch, IRawSearch, ISearchEngine} from 'vs/workbench/services/search/node/rawSearchService'; -export class CamelCaseExp implements IExpression { - private pattern: string; - - constructor(pattern: string) { - this.pattern = pattern.toLowerCase(); - } - - public test(value: string): boolean { - if (value.length === 0) { - return false; - } - - let pattern = this.pattern.toLowerCase(); - let result: boolean; - let i = 0; - - while (i < value.length && !(result = matches(pattern, value, 0, i))) { - i = nextAnchor(value, i + 1); - } - - return result; - } -} - -function isUpper(c: string): boolean { - let code = c.charCodeAt(0); - - return 65 <= code && code <= 90; -} - -function isNumber(c: string): boolean { - let code = c.charCodeAt(0); - - return 48 <= code && code <= 57; -} - -function nextAnchor(value: string, start: number): number { - let c: string; - for (let i = start; i < value.length; i++) { - c = value[i]; - if (isUpper(c) || isNumber(c)) { - return i; - } - } - - return value.length; -} - -function matches(pattern: string, value: string, patternIndex: number, valueIndex: number): boolean { - if (patternIndex === pattern.length) { - return true; - } - - if (valueIndex === value.length) { - return false; - } - - if (pattern[patternIndex] !== value[valueIndex].toLowerCase()) { - return false; - } - - let nextUpperIndex = valueIndex + 1; - let result = matches(pattern, value, patternIndex + 1, valueIndex + 1); - while (!result && (nextUpperIndex = nextAnchor(value, nextUpperIndex)) < value.length) { - result = matches(pattern, value, patternIndex + 1, nextUpperIndex); - nextUpperIndex++; - } - - return result; -} - -function isCamelCasePattern(pattern: string): boolean { - return (/^\w[\w.]*$/).test(pattern); -} - -export interface IExpression { - test: (value: string) => boolean; -} - -export class FilePatterns { - - private static DOT = '.'.charCodeAt(0); - - private expressions: IExpression[]; - - constructor(expressions: IPatternInfo[]) { - this.expressions = []; - - for (let i = 0; i < expressions.length; i++) { - let expression = expressions[i]; - let exp: IExpression; - - // Match all - if (!expression.pattern) { - exp = { test: () => true }; - } - - // RegExp - else if (expression.isRegExp) { - try { - exp = new RegExp(expression.pattern, 'i'); - } catch (e) { - if (e instanceof SyntaxError) { - exp = { test: () => true }; - } else { - throw e; - } - } - } - - // Camelcase - else if (isCamelCasePattern(expression.pattern)) { - exp = new CamelCaseExp(expression.pattern); - } - - // String - else { - if (expression.pattern.charCodeAt(0) === FilePatterns.DOT) { - expression.pattern = '*' + expression.pattern; // convert a . to a *. query - } - - // escape to regular expressions - expression.pattern = strings.anchorPattern(strings.convertSimple2RegExpPattern(expression.pattern), true, false); - - exp = new RegExp(expression.pattern, 'i'); - } - - this.expressions.push(exp); - } - } - - public static hasPatterns(expressions: IPatternInfo[]): boolean { - return expressions && expressions.length > 0 && expressions.some(e => !!e.pattern); - } - - public test(value: string): IExpression { - for (let i = 0; i < this.expressions.length; i++) { - let exp = this.expressions[i]; - if (exp.test(value)) { - return exp; - } - } - - return null; - } -} - export class FileWalker { private static ENOTDIR = 'ENOTDIR'; private config: IRawSearch; - private patterns: FilePatterns; + private filePattern: string; private excludePattern: glob.IExpression; private includePattern: glob.IExpression; private maxResults: number; @@ -182,7 +36,7 @@ export class FileWalker { constructor(config: IRawSearch) { this.config = config; - this.patterns = FilePatterns.hasPatterns(config.filePatterns) && new FilePatterns(config.filePatterns); + this.filePattern = config.filePattern; this.excludePattern = config.excludePattern; this.includePattern = config.includePattern; this.maxResults = config.maxResults || null; @@ -227,7 +81,7 @@ export class FileWalker { } // Check for match on file pattern and include pattern - if ((!this.patterns || this.patterns.test(paths.basename(absolutePath))) && (!this.includePattern || glob.match(this.includePattern, absolutePath))) { + if (this.isFilePatternMatch(paths.basename(absolutePath)) && (!this.includePattern || glob.match(this.includePattern, absolutePath))) { this.resultCount++; if (this.maxResults && this.resultCount > this.maxResults) { @@ -264,7 +118,7 @@ export class FileWalker { // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway let siblings = files; - if (this.config.filePatterns && this.config.filePatterns.length === 1 && this.config.filePatterns[0].pattern === file) { + if (this.config.filePattern === file) { siblings = []; } @@ -302,7 +156,7 @@ export class FileWalker { if ((error).code === FileWalker.ENOTDIR && !this.isCanceled && !this.isLimitHit) { // Check for match on file pattern and include pattern - if ((!this.patterns || this.patterns.test(file)) && (!this.includePattern || glob.match(this.includePattern, relativeFilePath, children))) { + if (this.isFilePatternMatch(file) && (!this.includePattern || glob.match(this.includePattern, relativeFilePath, children))) { this.resultCount++; if (this.maxResults && this.resultCount > this.maxResults) { @@ -329,6 +183,19 @@ export class FileWalker { }); } + private isFilePatternMatch(path: string): boolean { + + // Check for search pattern + if (this.filePattern) { + const res = filters.matchesFuzzy(this.filePattern, path); + + return !!res && res.length > 0; + } + + // No patterns means we match all + return true; + } + private realPathLink(path: string, clb: (error: Error, realpath?: string) => void): void { return fs.lstat(path, (error, lstat) => { if (error) { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 2cd19b162e3a89b0c0971ddc1326967a5947a862..35a1571bcb8e49c1d5b4c364eed592b9023f6749 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -18,7 +18,7 @@ import {Engine as TextSearchEngine} from 'vs/workbench/services/search/node/text export interface IRawSearch { rootPaths: string[]; - filePatterns: IPatternInfo[]; + filePattern?: string; excludePattern?: glob.IExpression; includePattern?: glob.IExpression; contentPattern?: IPatternInfo; @@ -52,21 +52,17 @@ export interface ISerializedSearchProgressItem extends ISerializedFileMatch, IPr export class SearchService implements IRawSearchService { public fileSearch(config: IRawSearch): PPromise { - config.filePatterns = config.filePatterns && config.filePatterns.length > 0 ? config.filePatterns : [{ pattern: '' /* All files */ }]; - let engine = new FileSearchEngine(config); return this.doSearch(engine); } public textSearch(config: IRawSearch): PPromise { - config.filePatterns = config.filePatterns && config.filePatterns.length > 0 ? config.filePatterns : [{ pattern: '' /* All files */ }]; - let engine = new TextSearchEngine(config, new FileWalker({ rootPaths: config.rootPaths, includePattern: config.includePattern, excludePattern: config.excludePattern, - filePatterns: config.filePatterns + filePattern: config.filePattern })); return this.doSearch(engine); diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index b235858242d21257b0b4f2060cd55a37001be7fc..3a84c6fb5d26c7b57686bb548ac3e817a4010b96 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -8,8 +8,9 @@ import {PPromise} from 'vs/base/common/winjs.base'; import uri from 'vs/base/common/uri'; import glob = require('vs/base/common/glob'); import objects = require('vs/base/common/objects'); +import filters = require('vs/base/common/filters'); import strings = require('vs/base/common/strings'); -import { Client } from 'vs/base/node/service.cp'; +import {Client} from 'vs/base/node/service.cp'; import {IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, ISearchConfiguration, ISearchService} from 'vs/platform/search/common/search'; import {IUntitledEditorService} from 'vs/workbench/services/untitled/browser/untitledEditorService'; import {IModelService} from 'vs/editor/common/services/modelService'; @@ -123,7 +124,7 @@ export class SearchService implements ISearchService { return; } - if (!this.matches(resource, query.filePatterns.map((p) => this.patternToRegExp(p.pattern)), query.includePattern, query.excludePattern)) { + if (!this.matches(resource, query.filePattern, query.includePattern, query.excludePattern)) { return; // respect user filters } @@ -145,27 +146,17 @@ export class SearchService implements ISearchService { return localResults; } - private patternToRegExp(pattern: string): RegExp { - if (pattern[0] === '.') { - pattern = '*' + pattern; // convert a . to a *. query - } - - // escape to regular expressions - pattern = strings.anchorPattern(strings.convertSimple2RegExpPattern(pattern), true, false); - - return new RegExp(pattern, 'i'); - } - - private matches(resource: uri, filePatterns: RegExp[], includePattern: glob.IExpression, excludePattern: glob.IExpression): boolean { + private matches(resource: uri, filePattern: string, includePattern: glob.IExpression, excludePattern: glob.IExpression): boolean { let workspaceRelativePath = this.contextService.toWorkspaceRelativePath(resource); - // file patterns - if (filePatterns && filePatterns.length) { + // file pattern + if (filePattern) { if (resource.scheme !== 'file') { return false; // if we match on file pattern, we have to ignore non file resources } - if (!filePatterns.some((pattern) => pattern.test(resource.fsPath))) { + const res = filters.matchesFuzzy(filePattern, resource.fsPath); + if (!res || res.length === 0) { return false; } } @@ -229,7 +220,7 @@ class DiskSearch { let rawSearch: IRawSearch = { rootPaths: rootResources.map(r => r.fsPath), - filePatterns: query.filePatterns, + filePattern: query.filePattern, excludePattern: query.excludePattern, includePattern: query.includePattern, maxResults: query.maxResults diff --git a/src/vs/workbench/services/search/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts index 56a7b8ad59537e1649cc392acb9ff7e1f3c99f3f..15b69ad41d88cfb183cbbbe4bbc38e098b461dad 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -32,7 +32,7 @@ suite('Search', () => { test('Files: *.js', function(done: () => void) { let engine = new FileSearchEngine({ rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.js' }] + filePattern: '*.js' }); let count = 0; @@ -50,7 +50,7 @@ suite('Search', () => { test('Files: *.js (Files as roots)', function(done: () => void) { let engine = new FileSearchEngine({ rootPaths: [require.toUrl('./fixtures/examples/company.js'), require.toUrl('./fixtures/examples/small.js')], - filePatterns: [{ pattern: '*.js' }] + filePattern: '*.js' }); let count = 0; @@ -68,7 +68,7 @@ suite('Search', () => { test('Files: NPE (CamelCase)', function(done: () => void) { let engine = new FileSearchEngine({ rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: 'NullPE' }] + filePattern: 'NullPE' }); let count = 0; @@ -86,7 +86,7 @@ suite('Search', () => { test('Files: *.*', function(done: () => void) { let engine = new FileSearchEngine({ rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', isCaseSensitive: true }] + filePattern: '*.*' }); let count = 0; @@ -104,7 +104,7 @@ suite('Search', () => { test('Files: *.as', function(done: () => void) { let engine = new FileSearchEngine({ rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.as' }] + filePattern: '*.as' }); let count = 0; @@ -123,7 +123,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.js', modifiers: 'i' }], + filePattern: '*.js', contentPattern: { pattern: 'GameOfLife', modifiers: 'i' } }; @@ -144,7 +144,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.js', modifiers: 'i' }], + filePattern: '*.js', contentPattern: { pattern: 'Game.?fL\\w?fe', isRegExp: true } }; @@ -165,7 +165,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.js', modifiers: 'i' }], + filePattern: '*.js', contentPattern: { pattern: 'GameOfLife', isWordMatch: true, isCaseSensitive: true } }; @@ -186,7 +186,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.css', modifiers: 'i' }], + filePattern: '*.css', contentPattern: { pattern: 'Helvetica', modifiers: 'i' } }; @@ -207,7 +207,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'e', modifiers: 'i' } }; @@ -228,7 +228,7 @@ suite('Search', () => { let c = 0; let config:any = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'e', modifiers: 'i' }, excludePattern: { '**/examples': true } }; @@ -250,7 +250,7 @@ suite('Search', () => { let c = 0; let config:any = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'e', modifiers: 'i' }, includePattern: { '**/examples/**': true } }; @@ -272,7 +272,7 @@ suite('Search', () => { let c = 0; let config:any = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'e', modifiers: 'i' }, includePattern: { '**/examples/**': true }, excludePattern: { '**/examples/small.js': true } @@ -295,7 +295,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'a', modifiers: 'i' }, maxResults: 520 }; @@ -317,7 +317,7 @@ suite('Search', () => { let c = 0; let config = { rootPaths: [require.toUrl('./fixtures')], - filePatterns: [{ pattern: '*.*', modifiers: 'i' }], + filePattern: '*.*', contentPattern: { pattern: 'ahsogehtdas', modifiers: 'i' } };