Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce IRelativePattern and adopt in DocumentFilter/FileWatcher/FileSearch #34695

Merged
merged 15 commits into from
Sep 22, 2017
77 changes: 63 additions & 14 deletions src/vs/base/common/glob.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,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 @@ -1610,9 +1635,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 @@ -1624,7 +1649,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 @@ -4911,30 +4935,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,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
Original file line number Diff line number Diff line change
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