diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index cc14eea9d51..fb9ce79e05d 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -16,6 +16,11 @@ export interface IExpression { [pattern: string]: boolean | SiblingClause | any; } +export interface IRelativePattern { + base: string; + pattern: string; +} + export function getEmptyExpression(): IExpression { return Object.create(null); } @@ -28,6 +33,8 @@ export interface SiblingClause { when: string; } +const GLOBSTAR = '**'; +const GLOB_SPLIT = '/'; const PATH_REGEX = '[/\\\\]'; // any slash or backslash const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash const ALL_FORWARD_SLASHES = /\//g; @@ -103,10 +110,10 @@ function parseRegExp(pattern: string): string { let regEx = ''; // Split up into segments for each slash found - let segments = splitGlobAware(pattern, '/'); + let segments = splitGlobAware(pattern, GLOB_SPLIT); // Special case where we only have globstars - if (segments.every(s => s === '**')) { + if (segments.every(s => s === GLOBSTAR)) { regEx = '.*'; } @@ -116,7 +123,7 @@ function parseRegExp(pattern: string): string { segments.forEach((segment, index) => { // Globstar is special - if (segment === '**') { + if (segment === GLOBSTAR) { // if we have more than one globstar after another, just ignore it if (!previousSegmentWasGlobStar) { @@ -207,7 +214,7 @@ function parseRegExp(pattern: string): string { } // Tail: Add the slash we had split on if there is more to come and the next one is not a globstar - if (index < segments.length - 1 && segments[index + 1] !== '**') { + if (index < segments.length - 1 && segments[index + 1] !== GLOBSTAR) { regEx += PATH_REGEX; } @@ -264,11 +271,39 @@ const NULL = function (): string { return null; }; -function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPattern { - if (!pattern) { +function toAbsolutePattern(relativePattern: IRelativePattern | string): string { + + // Without a base URI, best we can do is add '**' to the pattern + if (typeof relativePattern === 'string') { + if (relativePattern.indexOf(GLOBSTAR) !== 0) { + relativePattern = GLOBSTAR + GLOB_SPLIT + strings.ltrim(relativePattern, GLOB_SPLIT); + } + + return relativePattern; + } + + // Guard against null/undefined + if (!relativePattern) { + return undefined; + } + + // With a base URI, we can append the path to the relative glob as prefix + return relativePattern.base + GLOB_SPLIT + strings.ltrim(relativePattern.pattern, GLOB_SPLIT); +} + +function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern { + if (!arg1) { return NULL; } + // Handle IRelativePattern + let pattern: string; + if (typeof arg1 !== 'string') { + pattern = toAbsolutePattern(arg1.pattern); + } else { + pattern = arg1; + } + // Whitespace trimming pattern = pattern.trim(); @@ -276,7 +311,7 @@ function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPatte const patternKey = `${pattern}_${!!options.trimForExclusions}`; let parsedPattern = CACHE.get(patternKey); if (parsedPattern) { - return parsedPattern; + return wrapRelativePattern(parsedPattern, pattern); } // Check for Trivias @@ -304,7 +339,21 @@ function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPatte // Cache CACHE.set(patternKey, parsedPattern); - return parsedPattern; + return wrapRelativePattern(parsedPattern, pattern); +} + +function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern): ParsedStringPattern { + if (typeof arg2 === 'string') { + return parsedPattern; + } + + return function (path, basename) { + if (!paths.isEqualOrParent(path, arg2.base)) { + return null; + } + + return parsedPattern(path, basename); + }; } function trimForExclusions(pattern: string, options: IGlobOptions): string { @@ -395,9 +444,9 @@ function toRegExp(pattern: string): ParsedStringPattern { * - simple brace expansion ({js,ts} => js or ts) * - character ranges (using [...]) */ -export function match(pattern: string, path: string): boolean; +export function match(pattern: string | IRelativePattern, path: string): boolean; export function match(expression: IExpression, path: string, siblingsFn?: () => string[]): string /* the matching pattern */; -export function match(arg1: string | IExpression, path: string, siblingsFn?: () => string[]): any { +export function match(arg1: string | IExpression | IRelativePattern, path: string, siblingsFn?: () => string[]): any { if (!arg1 || !path) { return false; } @@ -413,16 +462,16 @@ export function match(arg1: string | IExpression, path: string, siblingsFn?: () * - simple brace expansion ({js,ts} => js or ts) * - character ranges (using [...]) */ -export function parse(pattern: string, options?: IGlobOptions): ParsedPattern; +export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern; export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression; -export function parse(arg1: string | IExpression, options: IGlobOptions = {}): any { +export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): any { if (!arg1) { return FALSE; } // Glob with String - if (typeof arg1 === 'string') { - const parsedPattern = parsePattern(arg1, options); + if (typeof arg1 === 'string' || (arg1 as IRelativePattern).base) { + const parsedPattern = parsePattern(arg1 as string | IRelativePattern, options); if (parsedPattern === NULL) { return FALSE; } diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index 1e3183171bd..1c87c114c4a 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -884,4 +884,35 @@ suite('Glob', () => { // Later expressions take precedence assert.deepEqual(glob.mergeExpressions({ 'a': true, 'b': false, 'c': true }, { 'a': false, 'b': true }), { 'a': false, 'b': true, 'c': true }); }); + + test('relative pattern', function () { + let p: glob.IRelativePattern = { base: '/DNXConsoleApp', pattern: '**/*.cs' }; + + assert(glob.match(p, '/DNXConsoleApp/Program.cs')); + assert(glob.match(p, '/DNXConsoleApp/foo/Program.cs')); + assert(!glob.match(p, '/DNXConsoleApp/foo/Program.ts')); + assert(!glob.match(p, '/other/DNXConsoleApp/foo/Program.ts')); + + p = { base: 'C:\\DNXConsoleApp', pattern: '**/*.cs' }; + assert(glob.match(p, 'C:\\DNXConsoleApp\\Program.cs')); + assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); + assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.ts')); + assert(!glob.match(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts')); + + assert(glob.match(p, 'C:/DNXConsoleApp/Program.cs')); + assert(glob.match(p, 'C:/DNXConsoleApp/foo/Program.cs')); + assert(!glob.match(p, 'C:/DNXConsoleApp/foo/Program.ts')); + assert(!glob.match(p, 'C:/other/DNXConsoleApp/foo/Program.ts')); + + p = { base: 'C:/DNXConsoleApp', pattern: '**/*.cs' }; + assert(glob.match(p, 'C:\\DNXConsoleApp\\Program.cs')); + assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); + assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.ts')); + assert(!glob.match(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts')); + + assert(glob.match(p, 'C:/DNXConsoleApp/Program.cs')); + assert(glob.match(p, 'C:/DNXConsoleApp/foo/Program.cs')); + assert(!glob.match(p, 'C:/DNXConsoleApp/foo/Program.ts')); + assert(!glob.match(p, 'C:/other/DNXConsoleApp/foo/Program.ts')); + }); }); \ No newline at end of file diff --git a/src/vs/editor/common/modes/languageSelector.ts b/src/vs/editor/common/modes/languageSelector.ts index 335e08e8b2e..356f454ecf4 100644 --- a/src/vs/editor/common/modes/languageSelector.ts +++ b/src/vs/editor/common/modes/languageSelector.ts @@ -6,12 +6,12 @@ 'use strict'; import URI from 'vs/base/common/uri'; -import { match as matchGlobPattern } from 'vs/base/common/glob'; // TODO@Alex +import { match as matchGlobPattern, IRelativePattern } from 'vs/base/common/glob'; // TODO@Alex export interface LanguageFilter { language?: string; scheme?: string; - pattern?: string; + pattern?: string | IRelativePattern; } export type LanguageSelector = string | LanguageFilter | (string | LanguageFilter)[]; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 3ec9e869fd4..4b01c762c7e 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1658,6 +1658,31 @@ declare module 'vscode' { validateInput?(value: string): string | undefined | null; } + class RelativePattern { + + /** + * A base file path to which the pattern will be matched against relatively. + */ + base: string; + + /** + * A file glob pattern like `*.{ts,js}` that will be matched on file paths + * relative to the base path. + * + * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`, + * the file glob pattern will match on `index.js`. + */ + pattern: string; + + constructor(pattern: string, base: WorkspaceFolder | string) + } + + /** + * A file glob pattern to match file paths against. This can either be a glob pattern string + * (like `**∕*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + */ + export type GlobPattern = string | RelativePattern; + /** * A document filter denotes a document by different properties like * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of @@ -1679,9 +1704,9 @@ declare module 'vscode' { scheme?: string; /** - * A glob pattern, like `*.{ts,js}`. + * A [glob pattern](#GlobPattern) that is matched on the absolute path of the document. */ - pattern?: string; + pattern?: GlobPattern; } /** @@ -1693,7 +1718,6 @@ declare module 'vscode' { */ export type DocumentSelector = string | DocumentFilter | (string | DocumentFilter)[]; - /** * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves @@ -4999,30 +5023,34 @@ declare module 'vscode' { /** * Creates a file system watcher. * - * A glob pattern that filters the file events must be provided. Optionally, flags to ignore certain - * kinds of events can be provided. To stop listening to events the watcher must be disposed. + * A glob pattern that filters the file events on their absolute path must be provided. Optionally, + * flags to ignore certain kinds of events can be provided. To stop listening to events the watcher must be disposed. * * *Note* that only files within the current [workspace folders](#workspace.workspaceFolders) can be watched. * - * @param globPattern A glob pattern that is applied to the names of created, changed, and deleted files. + * @param globPattern A [glob pattern](#GlobPattern) that is applied to the absolute paths of created, changed, + * and deleted files. * @param ignoreCreateEvents Ignore when files have been created. * @param ignoreChangeEvents Ignore when files have been changed. * @param ignoreDeleteEvents Ignore when files have been deleted. * @return A new file system watcher instance. */ - export function createFileSystemWatcher(globPattern: string, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; + export function createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; /** - * Find files in the workspace. + * Find files in the workspace. Will return no results if no [workspace folders](#workspace.workspaceFolders) + * are opened. * * @sample `findFiles('**∕*.js', '**∕node_modules∕**', 10)` - * @param include A glob pattern that defines the files to search for. - * @param exclude A glob pattern that defines files and folders to exclude. + * @param include A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. + * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. */ - export function findFiles(include: string, exclude?: string, maxResults?: number, token?: CancellationToken): Thenable; + export function findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable; /** * Save all dirty files. diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index b53bba5f047..12777a683c8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -6,7 +6,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; -import { ISearchService, QueryType, ISearchQuery } from 'vs/platform/search/common/search'; +import { ISearchService, QueryType, ISearchQuery, IFolderQuery } from 'vs/platform/search/common/search'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -15,6 +15,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IExperimentService } from 'vs/platform/telemetry/common/experiments'; +import { IRelativePattern } from 'vs/base/common/glob'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -53,17 +54,25 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- search --- - $startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable { + $startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable { const workspace = this._contextService.getWorkspace(); if (!workspace.folders.length) { return undefined; } + + let folderQueries: IFolderQuery[]; + if (typeof include === 'string' || !include) { + folderQueries = workspace.folders.map(folder => ({ folder: folder.uri })); // absolute pattern: search across all folders + } else { + folderQueries = [{ folder: URI.file(include.base) }]; // relative pattern: search only in base folder + } + const query: ISearchQuery = { - folderQueries: workspace.folders.map(folder => ({ folder: folder.uri })), + folderQueries, type: QueryType.File, maxResults, - includePattern: { [include]: true }, - excludePattern: { [exclude]: true }, + includePattern: { [typeof include === 'string' ? include : !!include ? include.pattern : undefined]: true }, + excludePattern: { [typeof exclude === 'string' ? exclude : !!exclude ? exclude.pattern : undefined]: true }, useRipgrep: this._experimentService.getExperiments().ripgrepQuickSearch }; this._searchService.extendQuery(query); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index b6434280765..845f114f678 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -610,6 +610,7 @@ export function createApiFactory( TaskScope: extHostTypes.TaskScope, Task: extHostTypes.Task, ConfigurationTarget: extHostTypes.ConfigurationTarget, + RelativePattern: extHostTypes.RelativePattern, // TODO@JOH,remote FileChangeType: FileChangeType, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 92ed6c653db..1c193f868ba 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -47,6 +47,7 @@ import { ITreeItem } from 'vs/workbench/common/views'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { SerializedError } from 'vs/base/common/errors'; +import { IRelativePattern } from 'vs/base/common/glob'; import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { IStat, IFileChange } from 'vs/platform/files/common/files'; @@ -311,7 +312,7 @@ export interface MainThreadTelemetryShape extends IDisposable { } export interface MainThreadWorkspaceShape extends IDisposable { - $startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable; + $startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; } diff --git a/src/vs/workbench/api/node/extHostFileSystemEventService.ts b/src/vs/workbench/api/node/extHostFileSystemEventService.ts index 6038446ffc4..1c5aa2b1e28 100644 --- a/src/vs/workbench/api/node/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/node/extHostFileSystemEventService.ts @@ -6,7 +6,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { Disposable } from './extHostTypes'; -import { parse } from 'vs/base/common/glob'; +import { parse, IRelativePattern } from 'vs/base/common/glob'; import { Uri, FileSystemWatcher as _FileSystemWatcher } from 'vscode'; import { FileSystemEvents, ExtHostFileSystemEventServiceShape } from './extHost.protocol'; @@ -30,7 +30,7 @@ class FileSystemWatcher implements _FileSystemWatcher { return Boolean(this._config & 0b100); } - constructor(dispatcher: Event, globPattern: string, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { + constructor(dispatcher: Event, globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { this._config = 0; if (ignoreCreateEvents) { @@ -96,7 +96,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ constructor() { } - public createFileSystemWatcher(globPattern: string, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): _FileSystemWatcher { + public createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): _FileSystemWatcher { return new FileSystemWatcher(this._emitter.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 22d9b4cacf5..81addd2688b 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -535,4 +535,4 @@ export namespace ProgressLocation { } return undefined; } -} +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 1db1c7986a9..e6678bfaab4 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -10,6 +10,7 @@ import URI from 'vs/base/common/uri'; import { illegalArgument } from 'vs/base/common/errors'; import * as vscode from 'vscode'; import { isMarkdownString } from 'vs/base/common/htmlContent'; +import { IRelativePattern } from 'vs/base/common/glob'; export class Disposable { @@ -1445,3 +1446,13 @@ export enum ConfigurationTarget { WorkspaceFolder = 3 } + +export class RelativePattern implements IRelativePattern { + base: string; + pattern: string; + + constructor(pattern: string, base: vscode.WorkspaceFolder | string) { + this.pattern = pattern; + this.base = typeof base === 'string' ? base : base.uri.fsPath; + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 7018fd88c07..1d7fd1b940f 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -14,6 +14,7 @@ import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspace import * as vscode from 'vscode'; import { compare } from 'vs/base/common/strings'; import { TrieMap } from 'vs/base/common/map'; +import { IRelativePattern } from 'vs/base/common/glob'; class Workspace2 extends Workspace { @@ -156,7 +157,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { // --- search --- - findFiles(include: string, exclude: string, maxResults?: number, token?: vscode.CancellationToken): Thenable { + findFiles(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults?: number, token?: vscode.CancellationToken): Thenable { const requestId = ExtHostWorkspace._requestIdPool++; const result = this._proxy.$startSearch(include, exclude, maxResults, requestId); if (token) {