src/vs/workbench/services/search/node/fileSearch.ts

770 LOC · 156 covered · 614 uncovered · 31 ranges · 60 concepts · 1 introducers · 32 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/workbench/api/common/extHostSearch.ts · 232 LOCcommon/extHostSearch.tssrc/vs/workbench/api/common/extHostUriTransformerService.ts · 37 LOCcommon/extHostUriTransfo…src/vs/workbench/api/node/extHostSearch.ts · 167 LOCnode/extHostSearch.tssrc/vs/workbench/api/test/common/testRPCProtocol.ts · 173 LOCcommon/testRPCProtocol.t…src/vs/workbench/services/search/common/fileSearchManager.ts · 429 LOCcommon/fileSearchManager…src/vs/workbench/services/search/node/rawSearchService.ts · 449 LOCnode/rawSearchService.tssrc/vs/workbench/services/search/node/ripgrepSearchProvider.ts · 73 LOCnode/ripgrepSearchProvid…src/vs/workbench/services/search/node/textSearchAdapter.ts · 61 LOCnode/textSearchAdapter.t…glob.ts ×1 · 4 introduced LOCglob.ts ×1extHostSearch.test|title=ExtHostSearch File: multiroot sibling exclude clause|occurrence=1 · 0 introduced LOCextHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: basic sibling clause|occurrence=1 · 0 introduced LOCextHostSearch.test|title…search.ts ×3 · 13 introduced LOCsearch.ts ×3extHostSearch.test|title=ExtHostSearch File: include, sibling exclude, and subfolder|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×2 · 4 introduced LOCfileSearchManager.ts ×2extHostSearch.test|title=ExtHostSearch File: basic sibling exclude clause|occurrence=1 · 0 introduced LOCextHostSearch.test|title…search.ts ×3 · 13 introduced LOCsearch.ts ×3fileSearchManager.ts ×9 · 57 introduced LOCfileSearchManager.ts ×9search.ts ×1 · 1 introduced LOCsearch.ts ×1textSearchManager.ts ×1 · 2 introduced LOCtextSearchManager.ts ×1extHostSearch.test|title=ExtHostSearch Text: max results = 2|occurrence=1 · 0 introduced LOCextHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: max results = 1|occurrence=1 · 0 introduced LOCextHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: provider returns maxResults exactly|occurrence=1 · 0 introduced LOCextHostSearch.test|title…textSearchManager.ts ×2 · 8 introduced LOCtextSearchManager.ts ×2extHostSearch.test|title=ExtHostSearch Text: basic results|occurrence=1, extHostSearch.test|title=ExtHostSearch Text: provider returns early with limitHit|occurrence=1 · 0 introduced LOCextHostSearch.test|title…excludes combined|occurrence=1 · 0 introduced LOCexcludes combined|occurr…textSearchManager.ts ×1 · 1 introduced LOCtextSearchManager.ts ×1extHostSearch.test|title=ExtHostSearch File: multiroot max results|occurrence=1 · 0 introduced LOCextHostSearch.test|title…textSearchManager.ts ×1 · 3 introduced LOCtextSearchManager.ts ×1search.ts ×1 · 7 introduced LOCsearch.ts ×1search.ts ×1 · 2 introduced LOCsearch.ts ×1excludes resolved correctly|occurrence=1 · 0 introduced LOCexcludes resolved correc…fileSearchManager.ts ×1 · 3 introduced LOCfileSearchManager.ts ×1searchExtConversionTypes.ts ×10 · 60 introduced LOCsearchExtConversionTypes…extHostSearch.test|title=ExtHostSearch File: provider returns maxResults exactly|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×1 · 2 introduced LOCfileSearchManager.ts ×1excludes combined|occurrence=1 · 0 introduced LOCexcludes combined|occurr…extHostSearch.test|title=ExtHostSearch File: simple results|occurrence=1 · 0 introduced LOCextHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: works with non-file schemes|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×1 · 5 introduced LOCfileSearchManager.ts ×1fileSearchManager.ts ×10 · 32 introduced LOCfileSearchManager.ts ×10excludes|occurrence=1 · 0 introduced LOCexcludes|occurrence=1excludes|occurrence=1 · 0 introduced LOCexcludes|occurrence=1excludes resolved correctly|occurrence=1 · 0 introduced LOCexcludes resolved correc…textSearchManager.ts ×1 · 2 introduced LOCtextSearchManager.ts ×1fileSearchManager.ts ×2 · 3 introduced LOCfileSearchManager.ts ×2search.ts ×2 · 2 introduced LOCsearch.ts ×2fileSearchManager.ts ×4 · 25 introduced LOCfileSearchManager.ts ×4fileSearchManager.ts ×1 · 4 introduced LOCfileSearchManager.ts ×1search.ts ×2 · 7 introduced LOCsearch.ts ×2search.ts ×1 · 4 introduced LOCsearch.ts ×1extHostSearch.test|title=ExtHostSearch File: no results|occurrence=1, extHostSearch.test|title=ExtHostSearch File: provider returns null|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×3 · 15 introduced LOCfileSearchManager.ts ×3search.ts ×2 · 2 introduced LOCsearch.ts ×2extHostSearch.test|title=ExtHostSearch Text: no results|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×1 · 5 introduced LOCfileSearchManager.ts ×1search.ts ×1 · 3 introduced LOCsearch.ts ×1fileSearchManager.ts ×5 · 42 introduced LOCfileSearchManager.ts ×5searchExtConversionTypes.ts ×2 · 9 introduced LOCsearchExtConversionTypes…textSearchManager.ts ×1 · 3 introduced LOCtextSearchManager.ts ×1search.ts ×1 · 2 introduced LOCsearch.ts ×1search.ts ×1 · 2 introduced LOCsearch.ts ×1searchExtConversionTypes.ts ×6 · 60 introduced LOCsearchExtConversionTypes…extHostSearch.ts ×1 · 3 introduced LOCextHostSearch.ts ×1extHostSearch.test|title=ExtHostSearch File: if onlyFileScheme is set, do not call custom schemes|occurrence=1 · 0 introduced LOCextHostSearch.test|title…fileSearchManager.ts ×5 · 19 introduced LOCfileSearchManager.ts ×5fileSearchManager.ts ×1 · 2 introduced LOCfileSearchManager.ts ×1fileSearchManager.ts ×20 · 133 introduced LOCfileSearchManager.ts ×20fileSearch.ts ×31 · 624 introduced LOCfileSearch.ts ×31extHostSearch.test|title=ExtHostSearch File: Search canceled|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: Search canceled|occurrence=1extHostSearch.test|title…excludes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: all provider calls get global include/excludes|occurrence=1excludes|occurrence=1extHostSearch.test|title=ExtHostSearch File: basic sibling exclude clause|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: basic sibling exclude clause|occurrence=1extHostSearch.test|title…excludes combined|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: global/local include/excludes combined|occurrence=1excludes combined|occurr…extHostSearch.test|title=ExtHostSearch File: if onlyFileScheme is set, do not call custom schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: if onlyFileScheme is set, do not call custom schemes|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: include, sibling exclude, and subfolder|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: include, sibling exclude, and subfolder|occurrence=1extHostSearch.test|title…excludes resolved correctly|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: include/excludes resolved correctly|occurrence=1excludes resolved correc…extHostSearch.test|title=ExtHostSearch File: max results = 1|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: max results = 1|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: max results = 2|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: max results = 2|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: multiroot max results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: multiroot max results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: multiroot sibling exclude clause|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: multiroot sibling exclude clause|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: no results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: no results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: provider returns maxResults exactly|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: provider returns maxResults exactly|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: provider returns null|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: provider returns null|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: session cancellation should work|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: session cancellation should work|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: simple results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: simple results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch File: works with non-file schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch File: works with non-file schemes|occurrence=1extHostSearch.test|title…excludes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: all provider calls get global include/excludes|occurrence=1excludes|occurrence=1extHostSearch.test|title=ExtHostSearch Text: basic results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: basic results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: basic sibling clause|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: basic sibling clause|occurrence=1extHostSearch.test|title…excludes combined|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: global/local include/excludes combined|occurrence=1excludes combined|occurr…extHostSearch.test|title=ExtHostSearch Text: include pattern applied|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: include pattern applied|occurrence=1extHostSearch.test|title…excludes resolved correctly|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: include/excludes resolved correctly|occurrence=1excludes resolved correc…extHostSearch.test|title=ExtHostSearch Text: max results = 1|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: max results = 1|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: max results = 2|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: max results = 2|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: multiroot max results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: multiroot max results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: multiroot sibling clause|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: multiroot sibling clause|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: no results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: no results|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: provider fail|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: provider fail|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: provider returns early with limitHit|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: provider returns early with limitHit|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: provider returns maxResults exactly|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: provider returns maxResults exactly|occurrence=1extHostSearch.test|title…extHostSearch.test|title=ExtHostSearch Text: works with non-file schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostSearch.test|title=ExtHostSearch Text: works with non-file schemes|occurrence=1extHostSearch.test|title…Focused file · src/vs/workbench/services/search/node/fileSearch.ts · 770 LOCnode/fileSearch.ts

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- fileSearch.ts ×31
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as childProcess from 'child_process';
7 > import * as fs from 'fs';
8 > import * as path from '../../../../base/common/path.js';
9 > import { Readable } from 'stream';
10 > import { StringDecoder } from 'string_decoder';
11 > import * as arrays from '../../../../base/common/arrays.js';
12 > import { toErrorMessage } from '../../../../base/common/errorMessage.js';
13 > import * as glob from '../../../../base/common/glob.js';
14 > import * as normalization from '../../../../base/common/normalization.js';
15 > import { isEqualOrParent } from '../../../../base/common/extpath.js';
16 > import * as platform from '../../../../base/common/platform.js';
17 > import { StopWatch } from '../../../../base/common/stopwatch.js';
18 > import * as strings from '../../../../base/common/strings.js';
19 > import * as types from '../../../../base/common/types.js';
20 > import { URI } from '../../../../base/common/uri.js';
21 > import { Promises } from '../../../../base/node/pfs.js';
22 > import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch, hasSiblingFn } from '../common/search.js';
23 > import { spawnRipgrepCmd } from './ripgrepFileSearch.js';
24 > import { prepareQuery } from '../../../../base/common/fuzzyScorer.js';
25 >
26 > interface IDirectoryEntry extends IRawFileMatch {
27 > base: string;
28 > basename: string;
29 > }
30 >
31 > interface IDirectoryTree {
32 > rootEntries: IDirectoryEntry[];
33 > pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
34 > }
35 >
36 > const killCmds = new Set<() => void>();
37 > process.on('exit', () => {
38 > killCmds.forEach(cmd => cmd());
39 > });
40 >
41 > export class FileWalker {
42 > private config: IFileQuery;
43 > private filePattern: string;
44 > private normalizedFilePatternLowercase: string | null = null;
45 > private includePattern: glob.ParsedExpression | undefined;
46 > private maxResults: number | null;
47 > private exists: boolean;
48 > private maxFilesize: number | null = null;
49 > private isLimitHit: boolean;
50 > private resultCount: number;
51 > private isCanceled = false;
52 > private fileWalkSW: StopWatch | null = null;
53 > private directoriesWalked: number;
54 > private filesWalked: number;
55 > private errors: string[];
56 > private cmdSW: StopWatch | null = null;
57 > private cmdResultCount: number = 0;
58 >
59 > private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
60 > private globalExcludePattern: glob.ParsedExpression | undefined;
61 >
62 > private walkedPaths: { [path: string]: boolean };
63 >
64 > constructor(config: IFileQuery) {
65 this.config = config;
66 this.filePattern = config.filePattern || '';
67 const globOptions = config.ignoreGlobCase ? { ignoreCase: true } : undefined;
68 this.includePattern = config.includePattern && glob.parse(config.includePattern, globOptions);
69 this.maxResults = config.maxResults || null;
70 this.exists = !!config.exists;
71 this.walkedPaths = Object.create(null);
72 this.resultCount = 0;
73 this.isLimitHit = false;
74 this.directoriesWalked = 0;
75 this.filesWalked = 0;
76 this.errors = [];
77
78 if (this.filePattern) {
79 this.normalizedFilePatternLowercase = config.shouldGlobMatchFilePattern ? null : prepareQuery(this.filePattern).normalizedLowercase;
80 }
81
82 this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern, globOptions);
83 this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
84
85 config.folderQueries.forEach(folderQuery => {
86 const folderExcludeExpression: glob.IExpression = {}; // todo: consider exclude baseURI
87
88 folderQuery.excludePattern?.forEach(excludePattern => {
89 Object.assign(folderExcludeExpression, excludePattern.pattern || {}, this.config.excludePattern || {});
90 });
91
92 if (!folderQuery.excludePattern?.length) {
93 Object.assign(folderExcludeExpression, this.config.excludePattern || {});
94 }
95
96 // Add excludes for other root folders
97 const fqPath = folderQuery.folder.fsPath;
98 config.folderQueries
99 .map(rootFolderQuery => rootFolderQuery.folder.fsPath)
100 .filter(rootFolder => rootFolder !== fqPath)
101 .forEach(otherRootFolder => {
102 // Exclude nested root folders
103 if (isEqualOrParent(otherRootFolder, fqPath, config.ignoreGlobCase)) {
104 folderExcludeExpression[path.relative(fqPath, otherRootFolder)] = true;
105 }
106 });
107
108 this.folderExcludePatterns.set(fqPath, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, fqPath, config.ignoreGlobCase));
109 });
110 }
112 > cancel(): void {
113 this.isCanceled = true;
114 killCmds.forEach(cmd => cmd());
115 }
117 > walk(folderQueries: IFolderQuery[], extraFiles: URI[], numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void {
118 this.fileWalkSW = StopWatch.create(false);
119
120 // Support that the file pattern is a full path to a file that exists
121 if (this.isCanceled) {
122 return done(null, this.isLimitHit);
123 }
124
125 // For each extra file
126 extraFiles.forEach(extraFilePath => {
127 const basename = path.basename(extraFilePath.fsPath);
128 if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath.fsPath, basename)) {
129 return; // excluded
130 }
131
132 // File: Check for match on file pattern and include pattern
133 this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */, searchPath: undefined });
134 });
135
136 this.cmdSW = StopWatch.create(false);
137
138 // For each root folder
139 this.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
140 this.call(this.cmdTraversal, this, folderQuery, numThreads, onResult, onMessage, (err?: Error) => {
141 if (err) {
142 const errorMessage = toErrorMessage(err);
143 console.error(errorMessage);
144 this.errors.push(errorMessage);
145 rootFolderDone(err, undefined);
146 } else {
147 rootFolderDone(null, undefined);
148 }
149 });
150 }, (errors, _result) => {
151 this.fileWalkSW!.stop();
152 const err = errors ? arrays.coalesce(errors)[0] : null;
153 done(err, this.isLimitHit);
154 });
155 }
157 > private parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
158 const results = new Array(list.length);
159 const errors = new Array<Error | null>(list.length);
160 let didErrorOccur = false;
161 let doneCount = 0;
162
163 if (list.length === 0) {
164 return callback(null, []);
165 }
166
167 list.forEach((item, index) => {
168 fn(item, (error, result) => {
169 if (error) {
170 didErrorOccur = true;
171 results[index] = null;
172 errors[index] = error;
173 } else {
174 results[index] = result;
175 errors[index] = null;
176 }
177
178 if (++doneCount === list.length) {
179 return callback(didErrorOccur ? errors : null, results);
180 }
181 });
182 });
183 }
185 > private call<F extends Function>(fun: F, that: any, ...args: any[]): void {
186 try {
187 fun.apply(that, args);
188 } catch (e) {
189 args[args.length - 1](e);
190 }
191 }
193 > private async cmdTraversal(folderQuery: IFolderQuery, numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): Promise<void> {
194 const rootFolder = folderQuery.folder.fsPath;
195 const isMac = platform.isMacintosh;
196
197 const killCmd = () => cmd && cmd.kill();
198 killCmds.add(killCmd);
199
200 let done = (err?: Error) => {
201 killCmds.delete(killCmd);
202 done = () => { };
203 cb(err);
204 };
205 let leftover = '';
206 const tree = this.initDirectoryTree();
207
208 let ripgrep;
209 try {
210 ripgrep = await spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression, numThreads);
211 } catch (err) {
212 done(err instanceof Error ? err : new Error(String(err)));
213 return;
214 }
215 const cmd = ripgrep.cmd;
216 const noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length;
217
218 const escapedArgs = ripgrep.rgArgs.args
219 .map(arg => arg.match(/^-/) ? arg : `'${arg}'`)
220 .join(' ');
221
222 let rgCmd = `${ripgrep.rgDiskPath} ${escapedArgs}\n - cwd: ${ripgrep.cwd}`;
223 if (ripgrep.rgArgs.siblingClauses) {
224 rgCmd += `\n - Sibling clauses: ${JSON.stringify(ripgrep.rgArgs.siblingClauses)}`;
225 }
226 onMessage({ message: rgCmd });
227
228 this.cmdResultCount = 0;
229 this.collectStdout(cmd, 'utf8', onMessage, (err: Error | null, stdout?: string, last?: boolean) => {
230 if (err) {
231 done(err);
232 return;
233 }
234 if (this.isLimitHit) {
235 done();
236 return;
237 }
238
239 // Mac: uses NFD unicode form on disk, but we want NFC
240 const normalized = leftover + (isMac ? normalization.normalizeNFC(stdout || '') : stdout);
241 const relativeFiles = normalized.split('\n');
242
243 if (last) {
244 const n = relativeFiles.length;
245 relativeFiles[n - 1] = relativeFiles[n - 1].trim();
246 if (!relativeFiles[n - 1]) {
247 relativeFiles.pop();
248 }
249 } else {
250 leftover = relativeFiles.pop() || '';
251 }
252
253 if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
254 done(new Error('Splitting up files failed'));
255 return;
256 }
257
258 this.cmdResultCount += relativeFiles.length;
259
260 if (noSiblingsClauses) {
261 for (const relativePath of relativeFiles) {
262 this.matchFile(onResult, { base: rootFolder, relativePath, searchPath: this.getSearchPath(folderQuery, relativePath) });
263 if (this.isLimitHit) {
264 killCmd();
265 break;
266 }
267 }
268 if (last || this.isLimitHit) {
269 done();
270 }
271
272 return;
273 }
274
275 // TODO: Optimize siblings clauses with ripgrep here.
276 this.addDirectoryEntries(folderQuery, tree, rootFolder, relativeFiles, onResult);
277
278 if (last) {
279 this.matchDirectoryTree(tree, rootFolder, onResult);
280 done();
281 }
282 });
283 }
285 > /**
286 > * Public for testing.
287 > */
288 > spawnFindCmd(folderQuery: IFolderQuery) {
289 const excludePattern = this.folderExcludePatterns.get(folderQuery.folder.fsPath)!;
290 const basenames = excludePattern.getBasenameTerms();
291 const pathTerms = excludePattern.getPathTerms();
292 const args = ['-L', '.'];
293 if (basenames.length || pathTerms.length) {
294 args.push('-not', '(', '(');
295 for (const basename of basenames) {
296 args.push('-name', basename);
297 args.push('-o');
298 }
299 for (const path of pathTerms) {
300 args.push('-path', path);
301 args.push('-o');
302 }
303 args.pop();
304 args.push(')', '-prune', ')');
305 }
306 args.push('-type', 'f');
307 return childProcess.spawn('find', args, { cwd: folderQuery.folder.fsPath });
308 }
310 > /**
311 > * Public for testing.
312 > */
313 > readStdout(cmd: childProcess.ChildProcess, encoding: BufferEncoding, cb: (err: Error | null, stdout?: string) => void): void {
314 let all = '';
315 this.collectStdout(cmd, encoding, () => { }, (err: Error | null, stdout?: string, last?: boolean) => {
316 if (err) {
317 cb(err);
318 return;
319 }
320
321 all += stdout;
322 if (last) {
323 cb(null, all);
324 }
325 });
326 }
328 > private collectStdout(cmd: childProcess.ChildProcess, encoding: BufferEncoding, onMessage: (message: IProgressMessage) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void {
329 let onData = (err: Error | null, stdout?: string, last?: boolean) => {
330 if (err || last) {
331 onData = () => { };
332
333 this.cmdSW?.stop();
334 }
335 cb(err, stdout, last);
336 };
337
338 let gotData = false;
339 if (cmd.stdout) {
340 // Should be non-null, but #38195
341 this.forwardData(cmd.stdout, encoding, onData);
342 cmd.stdout.once('data', () => gotData = true);
343 } else {
344 onMessage({ message: 'stdout is null' });
345 }
346
347 let stderr: Buffer[];
348 if (cmd.stderr) {
349 // Should be non-null, but #38195
350 stderr = this.collectData(cmd.stderr);
351 } else {
352 onMessage({ message: 'stderr is null' });
353 }
354
355 cmd.on('error', (err: Error) => {
356 onData(err);
357 });
358
359 cmd.on('close', (code: number) => {
360 // ripgrep returns code=1 when no results are found
361 let stderrText: string;
362 if (!gotData && (stderrText = this.decodeData(stderr, encoding)) && rgErrorMsgForDisplay(stderrText)) {
363 onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
364 } else {
365 if (this.exists && code === 0) {
366 this.isLimitHit = true;
367 }
368 onData(null, '', true);
369 }
370 });
371 }
373 > private forwardData(stream: Readable, encoding: BufferEncoding, cb: (err: Error | null, stdout?: string) => void): StringDecoder {
374 const decoder = new StringDecoder(encoding);
375 stream.on('data', (data: Buffer) => {
376 cb(null, decoder.write(data));
377 });
378 return decoder;
379 }
381 > private collectData(stream: Readable): Buffer[] {
382 const buffers: Buffer[] = [];
383 stream.on('data', (data: Buffer) => {
384 buffers.push(data);
385 });
386 return buffers;
387 }
389 > private decodeData(buffers: Buffer[], encoding: BufferEncoding): string {
390 const decoder = new StringDecoder(encoding);
391 return buffers.map(buffer => decoder.write(buffer)).join('');
392 }
394 > private initDirectoryTree(): IDirectoryTree {
395 const tree: IDirectoryTree = {
396 rootEntries: [],
397 pathToEntries: Object.create(null)
398 };
399 tree.pathToEntries['.'] = tree.rootEntries;
400 return tree;
401 }
403 > private addDirectoryEntries(folderQuery: IFolderQuery, { pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
404 // Support relative paths to files from a root resource (ignores excludes)
405 const filePatternMatch = this.filePattern && relativeFiles.find(f => strings.equals(f, this.filePattern, this.config.ignoreGlobCase));
406 if (filePatternMatch) {
407 this.matchFile(onResult, {
408 base,
409 relativePath: filePatternMatch,
410 searchPath: this.getSearchPath(folderQuery, filePatternMatch)
411 });
412 }
413
414 const add = (relativePath: string) => {
415 const basename = path.basename(relativePath);
416 const dirname = path.dirname(relativePath);
417 let entries = pathToEntries[dirname];
418 if (!entries) {
419 entries = pathToEntries[dirname] = [];
420 add(dirname);
421 }
422 entries.push({
423 base,
424 relativePath,
425 basename,
426 searchPath: this.getSearchPath(folderQuery, relativePath),
427 });
428 };
429 relativeFiles.forEach(add);
430 }
432 > private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
433 const self = this;
434 const excludePattern = this.folderExcludePatterns.get(rootFolder)!;
435 const filePattern = this.filePattern;
436 const ignoreGlobCase = this.config.ignoreGlobCase;
437 function matchDirectory(entries: IDirectoryEntry[]) {
438 self.directoriesWalked++;
439 const hasSibling = hasSiblingFn(() => entries.map(entry => entry.basename));
440 for (let i = 0, n = entries.length; i < n; i++) {
441 const entry = entries[i];
442 const { relativePath, basename } = entry;
443
444 // Check exclude pattern
445 // If the user searches for the exact file name, we adjust the glob matching
446 // to ignore filtering by siblings because the user seems to know what they
447 // are searching for and we want to include the result in that case anyway
448 if (excludePattern.test(relativePath, basename, !strings.equals(filePattern, basename, ignoreGlobCase) ? hasSibling : undefined)) {
449 continue;
450 }
451
452 const sub = pathToEntries[relativePath];
453 if (sub) {
454 matchDirectory(sub);
455 } else {
456 self.filesWalked++;
457 if (strings.equals(relativePath, filePattern, ignoreGlobCase)) {
458 continue; // ignore file if its path matches with the file pattern because that is already matched above
459 }
460
461 self.matchFile(onResult, entry);
462 }
463
464 if (self.isLimitHit) {
465 break;
466 }
467 }
468 }
469 matchDirectory(rootEntries);
470 }
472 > getStats(): ISearchEngineStats {
473 return {
474 cmdTime: this.cmdSW!.elapsed(),
475 fileWalkTime: this.fileWalkSW!.elapsed(),
476 directoriesWalked: this.directoriesWalked,
477 filesWalked: this.filesWalked,
478 cmdResultCount: this.cmdResultCount
479 };
480 }
482 > private doWalk(folderQuery: IFolderQuery, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error?: Error) => void): void {
483 const rootFolder = folderQuery.folder;
484
485 // Execute tasks on each file in parallel to optimize throughput
486 const hasSibling = hasSiblingFn(() => files);
487 this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
488
489 // Check canceled
490 if (this.isCanceled || this.isLimitHit) {
491 return clb(null);
492 }
493
494 // Check exclude pattern
495 // If the user searches for the exact file name, we adjust the glob matching
496 // to ignore filtering by siblings because the user seems to know what they
497 // are searching for and we want to include the result in that case anyway
498 const currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
499 if (this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.test(currentRelativePath, file, !strings.equals(this.config.filePattern, file, this.config.ignoreGlobCase) ? hasSibling : undefined)) {
500 return clb(null);
501 }
502
503 // Use lstat to detect links
504 const currentAbsolutePath = [rootFolder.fsPath, currentRelativePath].join(path.sep);
505 fs.lstat(currentAbsolutePath, (error, lstat) => {
506 if (error || this.isCanceled || this.isLimitHit) {
507 return clb(null);
508 }
509
510 // If the path is a link, we must instead use fs.stat() to find out if the
511 // link is a directory or not because lstat will always return the stat of
512 // the link which is always a file.
513 this.statLinkIfNeeded(currentAbsolutePath, lstat, (error, stat) => {
514 if (error || this.isCanceled || this.isLimitHit) {
515 return clb(null);
516 }
517
518 // Directory: Follow directories
519 if (stat.isDirectory()) {
520 this.directoriesWalked++;
521
522 // to really prevent loops with links we need to resolve the real path of them
523 return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
524 if (error || this.isCanceled || this.isLimitHit) {
525 return clb(null);
526 }
527
528 realpath = realpath || '';
529 if (this.walkedPaths[realpath]) {
530 return clb(null); // escape when there are cycles (can happen with symlinks)
531 }
532
533 this.walkedPaths[realpath] = true; // remember as walked
534
535 // Continue walking
536 return Promises.readdir(currentAbsolutePath).then(children => {
537 if (this.isCanceled || this.isLimitHit) {
538 return clb(null);
539 }
540
541 this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err || null));
542 }, error => {
543 clb(null);
544 });
545 });
546 }
547
548 // File: Check for match on file pattern and include pattern
549 else {
550 this.filesWalked++;
551 if (strings.equals(currentRelativePath, this.filePattern, this.config.ignoreGlobCase)) {
552 return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
553 }
554
555 if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
556 return clb(null, undefined); // ignore file if max file size is hit
557 }
558
559 this.matchFile(onResult, {
560 base: rootFolder.fsPath,
561 relativePath: currentRelativePath,
562 searchPath: this.getSearchPath(folderQuery, currentRelativePath),
563 });
564 }
565
566 // Unwind
567 return clb(null, undefined);
568 });
569 });
570 }, (error: Array<Error | null> | null): void => {
571 const filteredErrors = error ? arrays.coalesce(error) : error; // find any error by removing null values first
572 return done(filteredErrors && filteredErrors.length > 0 ? filteredErrors[0] : undefined);
573 });
574 }
576 > private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
577 if (this.isFileMatch(candidate) && (!this.includePattern || this.includePattern(candidate.relativePath, path.basename(candidate.relativePath)))) {
578 this.resultCount++;
579
580 if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) {
581 this.isLimitHit = true;
582 }
583
584 if (!this.isLimitHit) {
585 onResult(candidate);
586 }
587 }
588 }
590 > private isFileMatch(candidate: IRawFileMatch): boolean {
591 // Check for search pattern
592 if (this.filePattern) {
593 if (this.filePattern === '*') {
594 return true; // support the all-matching wildcard
595 }
596
597 if (this.normalizedFilePatternLowercase) {
598 return isFilePatternMatch(candidate, this.normalizedFilePatternLowercase);
599 } else if (this.filePattern) {
600 return isFilePatternMatch(candidate, this.filePattern, false, this.config.ignoreGlobCase);
601 }
602 }
603
604 // No patterns means we match all
605 return true;
606 }
608 > private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error | null, stat: fs.Stats) => void): void {
609 if (lstat.isSymbolicLink()) {
610 return fs.stat(path, clb); // stat the target the link points to
611 }
612
613 return clb(null, lstat); // not a link, so the stat is already ok for us
614 }
616 > private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error | null, realpath?: string) => void): void {
617 if (lstat.isSymbolicLink()) {
618 return fs.realpath(path, (error, realpath) => {
619 if (error) {
620 return clb(error);
621 }
622
623 return clb(null, realpath);
624 });
625 }
626
627 return clb(null, path);
628 }
630 > /**
631 > * If we're searching for files in multiple workspace folders, then better prepend the
632 > * name of the workspace folder to the path of the file. This way we'll be able to
633 > * better filter files that are all on the top of a workspace folder and have all the
634 > * same name. A typical example are `package.json` or `README.md` files.
635 > */
636 > private getSearchPath(folderQuery: IFolderQuery, relativePath: string): string {
637 if (folderQuery.folderName) {
638 return path.join(folderQuery.folderName, relativePath);
639 }
640 return relativePath;
641 }
643 >
644 > export class Engine implements ISearchEngine<IRawFileMatch> {
645 > private folderQueries: IFolderQuery[];
646 > private extraFiles: URI[];
647 > private walker: FileWalker;
648 > private numThreads?: number;
649 >
650 > constructor(config: IFileQuery, numThreads?: number) {
651 this.folderQueries = config.folderQueries;
652 this.extraFiles = config.extraFileResources || [];
653 this.numThreads = numThreads;
654
655 this.walker = new FileWalker(config);
656 }
658 > search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void): void {
659 this.walker.walk(this.folderQueries, this.extraFiles, this.numThreads, onResult, onProgress, (err: Error | null, isLimitHit: boolean) => {
660 done(err, {
661 limitHit: isLimitHit,
662 stats: this.walker.getStats(),
663 messages: [],
664 });
665 });
666 }
668 > cancel(): void {
669 this.walker.cancel();
670 }
672 >
673 > /**
674 > * This class exists to provide one interface on top of two ParsedExpressions, one for absolute expressions and one for relative expressions.
675 > * The absolute and relative expressions don't "have" to be kept separate, but this keeps us from having to path.join every single
676 > * file searched, it's only used for a text search with a searchPath
677 > */
678 > class AbsoluteAndRelativeParsedExpression {
679 > private absoluteParsedExpr: glob.ParsedExpression | undefined;
680 > private relativeParsedExpr: glob.ParsedExpression | undefined;
681 >
682 > constructor(public expression: glob.IExpression, private root: string, private ignoreCase?: boolean) {
683 this.init(expression);
684 }
686 > /**
687 > * Split the IExpression into its absolute and relative components, and glob.parse them separately.
688 > */
689 > private init(expr: glob.IExpression): void {
690 let absoluteGlobExpr: glob.IExpression | undefined;
691 let relativeGlobExpr: glob.IExpression | undefined;
692 Object.keys(expr)
693 .filter(key => expr[key])
694 .forEach(key => {
695 if (path.isAbsolute(key)) {
696 absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
697 absoluteGlobExpr[key] = expr[key];
698 } else {
699 relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
700 relativeGlobExpr[key] = expr[key];
701 }
702 });
703
704 const globOptions = { trimForExclusions: true, ignoreCase: this.ignoreCase };
705 this.absoluteParsedExpr = absoluteGlobExpr && glob.parse(absoluteGlobExpr, globOptions);
706 this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, globOptions);
707 }
709 > test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | Promise<string | null> | undefined | null {
710 return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) ||
711 (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling));
712 }
714 > getBasenameTerms(): string[] {
715 const basenameTerms: string[] = [];
716 if (this.absoluteParsedExpr) {
717 basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
718 }
719
720 if (this.relativeParsedExpr) {
721 basenameTerms.push(...glob.getBasenameTerms(this.relativeParsedExpr));
722 }
723
724 return basenameTerms;
725 }
727 > getPathTerms(): string[] {
728 const pathTerms: string[] = [];
729 if (this.absoluteParsedExpr) {
730 pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
731 }
732
733 if (this.relativeParsedExpr) {
734 pathTerms.push(...glob.getPathTerms(this.relativeParsedExpr));
735 }
736
737 return pathTerms;
738 }
740 >
741 function rgErrorMsgForDisplay(msg: string): string | undefined {
742 const lines = msg.trim().split('\n');
743 const firstLine = lines[0].trim();
744
745 if (firstLine.startsWith('Error parsing regex')) {
746 return firstLine;
747 }
748
749 if (firstLine.startsWith('regex parse error')) {
750 return strings.uppercaseFirstLetter(lines[lines.length - 1].trim());
751 }
752
753 if (firstLine.startsWith('error parsing glob') ||
754 firstLine.startsWith('unsupported encoding')) {
755 // Uppercase first letter
756 return firstLine.charAt(0).toUpperCase() + firstLine.substr(1);
757 }
758
759 if (firstLine === `Literal '\\n' not allowed.`) {
760 // I won't localize this because none of the Ripgrep error messages are localized
761 return `Literal '\\n' currently not supported`;
762 }
763
764 if (firstLine.startsWith('Literal ')) {
765 // Other unsupported chars
766 return firstLine;
767 }
768
769 return undefined;
770 }