Skip to content

Commit

Permalink
Introduce GlobPattern and adopt in DocumentFilter/FileWatcher/FileSea…
Browse files Browse the repository at this point in the history
…rch (#34695)

* introduce IRelativePattern and use in extension API

* 💄

* docs

* introduce RelativePattern

* support RelativePattern also for file watcher

* also make findFiles support RelativePattern

* less type conversion

* add GlobPattern type and remove readonly

* make base a string

* fix setter access to RelativePattern

* fix npe when exclude is undefined

* fix findFiles: pattern seems to be matched against workspace always

* 💄

* clarify glob pattern matching
  • Loading branch information
bpasero committed Sep 22, 2017
1 parent 9e05d4b commit 3e9fa59
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 38 deletions.
77 changes: 63 additions & 14 deletions src/vs/base/common/glob.ts
Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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 = '.*';
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -264,19 +271,47 @@ 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();

// Check cache
const patternKey = `${pattern}_${!!options.trimForExclusions}`;
let parsedPattern = CACHE.get(patternKey);
if (parsedPattern) {
return parsedPattern;
return wrapRelativePattern(parsedPattern, pattern);
}

// Check for Trivias
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
31 changes: 31 additions & 0 deletions src/vs/base/test/node/glob.test.ts
Expand Up @@ -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'));
});
});
4 changes: 2 additions & 2 deletions src/vs/editor/common/modes/languageSelector.ts
Expand Up @@ -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)[];
Expand Down
50 changes: 39 additions & 11 deletions src/vs/vscode.d.ts
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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<Uri[]>;
export function findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable<Uri[]>;

/**
* Save all dirty files.
Expand Down
19 changes: 14 additions & 5 deletions src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -53,17 +54,25 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {

// --- search ---

$startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable<URI[]> {
$startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable<URI[]> {
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);
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/node/extHost.api.impl.ts
Expand Up @@ -610,6 +610,7 @@ export function createApiFactory(
TaskScope: extHostTypes.TaskScope,
Task: extHostTypes.Task,
ConfigurationTarget: extHostTypes.ConfigurationTarget,
RelativePattern: extHostTypes.RelativePattern,

// TODO@JOH,remote
FileChangeType: <any>FileChangeType,
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/api/node/extHost.protocol.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -311,7 +312,7 @@ export interface MainThreadTelemetryShape extends IDisposable {
}

export interface MainThreadWorkspaceShape extends IDisposable {
$startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable<URI[]>;
$startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable<URI[]>;
$cancelSearch(requestId: number): Thenable<boolean>;
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
}
Expand Down

0 comments on commit 3e9fa59

Please sign in to comment.