Skip to content

Commit

Permalink
Move search paths resolving methods to a separate module
Browse files Browse the repository at this point in the history
  • Loading branch information
alvsan09 committed Apr 22, 2021
1 parent 1adc8c1 commit 17b9d4a
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/**
* Checks if the format of a given path represents a relative path within the base directory
*/
export function isRelativeToBaseDirectory(filePath: string): boolean {
const normalizedPath = filePath.replace(/\\/g, '/');
return normalizedPath.startsWith('./');
}

/**
* Push an item to an existing string array only if the item is not already included.
* If the given array is undefined it creates a new one with the given item as the first entry.
*/
export function pushIfNotIncluded(containerArray: string[] | undefined, item: string): string[] {
if (!containerArray) {
return [item];
}

if (!containerArray.includes(item)) {
containerArray.push(item);
}

return containerArray;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as temp from 'temp';
import * as fs from 'fs';
import { expect } from 'chai';
import { rgPath as realRgPath } from 'vscode-ripgrep';
import { resolveSearchPathsFromIncludes } from './ripgrep-search-paths-resolver';

// Allow creating temporary files, but remove them when we are done.
const track = temp.track();
Expand Down Expand Up @@ -952,8 +953,15 @@ describe('#resolvePatternToPathMap', function (): void {
this.timeout(10000);
it('should not resolve paths from a not absolute / relative pattern', function (): void {
const pattern = 'carrots';
const resultMap = ripgrepServer['resolvePatternToPathsMap']([pattern], [rootDirA]);
expect(resultMap.size).equal(0);
const options = { include: [pattern] };
const searchPaths = resolveSearchPathsFromIncludes([rootDirA], options);
// Same root directory
expect(searchPaths.length).equal(1);
expect(searchPaths[0]).equal(rootDirA);

// Pattern is unchanged
expect(options.include.length).equal(1);
expect(options.include[0]).equals(pattern);
});

it('should resolve pattern to path for relative filename', function (): void {
Expand Down Expand Up @@ -1051,6 +1059,9 @@ describe('#patternToGlobCLIArguments', function (): void {
});

function checkResolvedPathForPattern(pattern: string, expectedPath: string): void {
const resultMap: Map<string, string[]> = ripgrepServer['resolvePatternToPathsMap']([pattern], [rootDirA]);
expect(resultMap.get(pattern)![0]).equal(expectedPath);
const options = {include: [pattern]};
const searchPaths = resolveSearchPathsFromIncludes([rootDirA], options);
expect(searchPaths.length).equal(1);
expect(options.include.length).equals(0);
expect(searchPaths[0]).equal(expectedPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import { FileUri } from '@theia/core/lib/node/file-uri';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface';
import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { isRelativeToBaseDirectory, pushIfNotIncluded } from './ripgrep-search-in-workspace-helper';
import { resolveSearchPathsFromIncludes } from './ripgrep-search-paths-resolver';

export const RgPath = Symbol('RgPath');

Expand All @@ -40,7 +41,8 @@ function bytesOrTextToString(obj: IRgBytesOrText): string {

/**
* Joins the given root and pattern to form an absolute path
* as long as the pattern is in relative form e.g. './foo'.
* as long as the pattern is in relative form.
* E.g. './foo' becomes '${root}/foo'
*/
function relativeToAbsolutePattern(root: string, pattern: string): string {
if (!isRelativeToBaseDirectory(pattern)) {
Expand All @@ -50,65 +52,6 @@ function relativeToAbsolutePattern(root: string, pattern: string): string {
return path.join(root, pattern);
}

function isRelativeToBaseDirectory(filePath: string): boolean {
const normalizedPath = filePath.replace(/\\/g, '/');
return normalizedPath.startsWith('./');
}

/**
* Attempts to build a valid absolute file or directory from the given pattern and root folder.
* e.g. /a/b/c/foo/** to /a/b/c/foo, or './foo/**' to '${root}/foo'.
*
* @returns the valid path if found existing in the file system.
*/
function resolveIncludeFolderFromGlob(root: string, pattern: string): string | undefined {
const patternBase = stripGlobSuffix(pattern);

if (!path.isAbsolute(patternBase) && !isRelativeToBaseDirectory(patternBase)) {
// The include pattern is not referring to a single file / folder, i.e. not to be converted
// to include folder.
return undefined;
}

const targetPath = path.isAbsolute(patternBase) ? patternBase : path.join(root, patternBase);

if (fs.existsSync(targetPath)) {
return targetPath;
}

return undefined;
}

/**
* Removes a glob suffix from a given pattern (e.g. /a/b/c/**)
* to a directory path (/a/b/c).
*
* @returns the path without the glob suffix,
* else returns the original pattern.
*/
function stripGlobSuffix(pattern: string): string {
const pathParsed = path.parse(pattern);
const suffix = pathParsed.base;

return suffix === '**' ? pathParsed.dir : pattern;
}

/**
* Push an item to an existing string array only if the item is not already included.
* If the given array is undefined it creates a new one with the given item as the first entry.
*/
function pushIfNotIncluded(containerArray: string[] | undefined, item: string): string[] {
if (!containerArray) {
return [item];
}

if (!containerArray.includes(item)) {
containerArray.push(item);
}

return containerArray;
}

type IRgMessage = IRgMatch | IRgBegin | IRgEnd;

interface IRgMatch {
Expand Down Expand Up @@ -260,7 +203,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
// we'll use to parse the lines.
const searchId = this.nextSearchId++;
const rootPaths = rootUris.map(root => FileUri.fsPath(root));
const searchPaths: string[] = this.resolveSearchPathsFromIncludes(rootPaths, opts);
const searchPaths: string[] = resolveSearchPathsFromIncludes(rootPaths, opts);
this.includesExcludesToAbsolute(searchPaths, opts);
const rgArgs = this.getArgs(opts);
// if we use matchWholeWord we use regExp internally,
Expand Down Expand Up @@ -441,55 +384,6 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
});
}

/**
* The default search paths are set to be the root paths associated to a workspace
* however the search scope can be further refined with the include paths available in the search options.
* This method will replace the searching paths to the ones specified in the 'include' options but as long
* as the 'include' paths can be successfully validated as existing.
*
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
* derived from the include options which can be used to perform the search.
*
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
* provided as an equivalent search path instead.
*/
protected resolveSearchPathsFromIncludes(rootPaths: string[], opts: SearchInWorkspaceOptions | undefined): string[] {
if (!opts || !opts.include) {
return rootPaths;
}

const includesAsPaths = this.resolvePatternToPathsMap(opts.include, rootPaths);
const patternPaths = Array.from(includesAsPaths.keys());

// Remove file patterns that were successfully translated to search paths.
opts.include = opts.include.filter(item => !patternPaths.includes(item));

return includesAsPaths.size > 0 ? [].concat.apply([], Array.from(includesAsPaths.values())) : rootPaths;
}

/**
* Attempts to resolve valid file paths from a given list of patterns.
* The given search paths are used to try resolving relative path patterns to an absolute path.
* The resulting map will include all patterns associated to its equivalent file paths.
* The given patterns that are not successfully mapped to paths are not included.
*/
protected resolvePatternToPathsMap(patterns: string[], searchPaths: string[]): Map<string, string[]> {
const patternToPathMap = new Map<string, string[]>();

patterns.forEach(pattern => {
searchPaths.forEach(root => {
const foundPath = resolveIncludeFolderFromGlob(root, pattern);

if (foundPath) {
const pathArray = patternToPathMap.get(pattern);
patternToPathMap.set(pattern, pushIfNotIncluded(pathArray, foundPath));
}
});
});

return patternToPathMap;
}

/**
* Transforms relative patterns to absolute paths, one for each given search path.
*/
Expand Down
106 changes: 106 additions & 0 deletions packages/search-in-workspace/src/node/ripgrep-search-paths-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { isRelativeToBaseDirectory, pushIfNotIncluded } from './ripgrep-search-in-workspace-helper';

/**
* The default search paths are set to be the root paths associated to a workspace
* however the search scope can be further refined with the include paths available in the search options.
* This method will replace the searching paths to the ones specified in the 'include' options but as long
* as the 'include' paths can be successfully validated as existing.
*
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
* derived from the include options which can be used to perform the search.
*
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
* provided as an equivalent search path instead.
*/
export function resolveSearchPathsFromIncludes(rootPaths: string[], opts: SearchInWorkspaceOptions | undefined): string[] {
if (!opts || !opts.include) {
return rootPaths;
}

const includesAsPaths = resolvePatternToPathsMap(opts.include, rootPaths);
const patternPaths = Array.from(includesAsPaths.keys());

// Remove file patterns that were successfully translated to search paths.
opts.include = opts.include.filter(item => !patternPaths.includes(item));

return includesAsPaths.size > 0 ? [].concat.apply([], Array.from(includesAsPaths.values())) : rootPaths;
}

/**
* Attempts to resolve valid file paths from a given list of patterns.
* The given search paths are used to try resolving relative path patterns to an absolute path.
* The resulting map will include all patterns associated to its equivalent file paths.
* The given patterns that are not successfully mapped to paths are not included.
*/
function resolvePatternToPathsMap(patterns: string[], searchPaths: string[]): Map<string, string[]> {
const patternToPathMap = new Map<string, string[]>();

patterns.forEach(pattern => {
searchPaths.forEach(root => {
const foundPath = resolveIncludeFolderFromGlob(root, pattern);

if (foundPath) {
const pathArray = patternToPathMap.get(pattern);
patternToPathMap.set(pattern, pushIfNotIncluded(pathArray, foundPath));
}
});
});

return patternToPathMap;
}

/**
* Attempts to build a valid absolute file or directory from the given pattern and root folder.
* e.g. /a/b/c/foo/** to /a/b/c/foo, or './foo/**' to '${root}/foo'.
*
* @returns the valid path if found existing in the file system.
*/
function resolveIncludeFolderFromGlob(root: string, pattern: string): string | undefined {
const patternBase = stripGlobSuffix(pattern);

if (!path.isAbsolute(patternBase) && !isRelativeToBaseDirectory(patternBase)) {
// The include pattern is not referring to a single file / folder, i.e. not to be converted
// to include folder.
return undefined;
}

const targetPath = path.isAbsolute(patternBase) ? patternBase : path.join(root, patternBase);

if (fs.existsSync(targetPath)) {
return targetPath;
}

return undefined;
}

/**
* Removes a glob suffix from a given pattern (e.g. /a/b/c/**)
* to a directory path (/a/b/c).
*
* @returns the path without the glob suffix,
* else returns the original pattern.
*/
function stripGlobSuffix(pattern: string): string {
const pathParsed = path.parse(pattern);
const suffix = pathParsed.base;

return suffix === '**' ? pathParsed.dir : pattern;
}

0 comments on commit 17b9d4a

Please sign in to comment.