@theia/search-in-workspace 1.45.0 → 1.46.0-next.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -40
- package/lib/browser/components/search-in-workspace-input.d.ts +39 -39
- package/lib/browser/components/search-in-workspace-input.js +123 -123
- package/lib/browser/components/search-in-workspace-textarea.d.ts +39 -39
- package/lib/browser/components/search-in-workspace-textarea.js +130 -130
- package/lib/browser/search-in-workspace-context-key-service.d.ts +23 -23
- package/lib/browser/search-in-workspace-context-key-service.js +90 -90
- package/lib/browser/search-in-workspace-factory.d.ts +10 -10
- package/lib/browser/search-in-workspace-factory.js +68 -68
- package/lib/browser/search-in-workspace-frontend-contribution.d.ts +57 -55
- package/lib/browser/search-in-workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-frontend-contribution.js +516 -482
- package/lib/browser/search-in-workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/search-in-workspace-frontend-module.d.ts +6 -6
- package/lib/browser/search-in-workspace-frontend-module.js +71 -71
- package/lib/browser/search-in-workspace-label-provider.d.ts +9 -9
- package/lib/browser/search-in-workspace-label-provider.js +57 -57
- package/lib/browser/search-in-workspace-preferences.d.ts +17 -17
- package/lib/browser/search-in-workspace-preferences.js +87 -87
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts +259 -255
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-result-tree-widget.js +1172 -1099
- package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
- package/lib/browser/search-in-workspace-service.d.ts +35 -35
- package/lib/browser/search-in-workspace-service.js +158 -158
- package/lib/browser/search-in-workspace-widget.d.ts +121 -121
- package/lib/browser/search-in-workspace-widget.js +629 -629
- package/lib/browser/search-layout-migrations.d.ts +5 -5
- package/lib/browser/search-layout-migrations.js +64 -64
- package/lib/common/search-in-workspace-interface.d.ts +116 -116
- package/lib/common/search-in-workspace-interface.js +35 -35
- package/lib/node/ripgrep-search-in-workspace-server.d.ts +94 -94
- package/lib/node/ripgrep-search-in-workspace-server.js +430 -430
- package/lib/node/ripgrep-search-in-workspace-server.js.map +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.d.ts +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js +899 -899
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js.map +1 -1
- package/lib/node/search-in-workspace-backend-module.d.ts +3 -3
- package/lib/node/search-in-workspace-backend-module.js +32 -32
- package/package.json +9 -9
- package/src/browser/components/search-in-workspace-input.tsx +139 -139
- package/src/browser/components/search-in-workspace-textarea.tsx +153 -153
- package/src/browser/search-in-workspace-context-key-service.ts +93 -93
- package/src/browser/search-in-workspace-factory.ts +59 -59
- package/src/browser/search-in-workspace-frontend-contribution.ts +510 -474
- package/src/browser/search-in-workspace-frontend-module.ts +83 -83
- package/src/browser/search-in-workspace-label-provider.ts +48 -48
- package/src/browser/search-in-workspace-preferences.ts +96 -96
- package/src/browser/search-in-workspace-result-tree-widget.tsx +1318 -1245
- package/src/browser/search-in-workspace-service.ts +152 -152
- package/src/browser/search-in-workspace-widget.tsx +727 -727
- package/src/browser/search-layout-migrations.ts +53 -53
- package/src/browser/styles/index.css +400 -400
- package/src/browser/styles/search.svg +6 -6
- package/src/common/search-in-workspace-interface.ts +153 -153
- package/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +1073 -1073
- package/src/node/ripgrep-search-in-workspace-server.ts +490 -490
- package/src/node/search-in-workspace-backend-module.ts +33 -33
|
@@ -1,490 +1,490 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2017-2021 Ericsson and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import * as fs from '@theia/core/shared/fs-extra';
|
|
18
|
-
import * as path from 'path';
|
|
19
|
-
import { ILogger } from '@theia/core';
|
|
20
|
-
import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node';
|
|
21
|
-
import { FileUri } from '@theia/core/lib/
|
|
22
|
-
import URI from '@theia/core/lib/common/uri';
|
|
23
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
24
|
-
import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface';
|
|
25
|
-
|
|
26
|
-
export const RgPath = Symbol('RgPath');
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Typing for ripgrep's arbitrary data object:
|
|
30
|
-
*
|
|
31
|
-
* https://docs.rs/grep-printer/0.1.0/grep_printer/struct.JSON.html#object-arbitrary-data
|
|
32
|
-
*/
|
|
33
|
-
export type IRgBytesOrText = { bytes: string } | { text: string };
|
|
34
|
-
|
|
35
|
-
function bytesOrTextToString(obj: IRgBytesOrText): string {
|
|
36
|
-
return 'bytes' in obj ?
|
|
37
|
-
Buffer.from(obj.bytes, 'base64').toString() :
|
|
38
|
-
obj.text;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type IRgMessage = IRgMatch | IRgBegin | IRgEnd;
|
|
42
|
-
|
|
43
|
-
interface IRgMatch {
|
|
44
|
-
type: 'match';
|
|
45
|
-
data: {
|
|
46
|
-
path: IRgBytesOrText;
|
|
47
|
-
lines: IRgBytesOrText;
|
|
48
|
-
line_number: number;
|
|
49
|
-
absolute_offset: number;
|
|
50
|
-
submatches: IRgSubmatch[];
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface IRgSubmatch {
|
|
55
|
-
match: IRgBytesOrText;
|
|
56
|
-
start: number;
|
|
57
|
-
end: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface IRgBegin {
|
|
61
|
-
type: 'begin';
|
|
62
|
-
data: {
|
|
63
|
-
path: IRgBytesOrText;
|
|
64
|
-
lines: string;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface IRgEnd {
|
|
69
|
-
type: 'end';
|
|
70
|
-
data: {
|
|
71
|
-
path: IRgBytesOrText;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
@injectable()
|
|
76
|
-
export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
|
|
77
|
-
|
|
78
|
-
// List of ongoing searches, maps search id to a the started rg process.
|
|
79
|
-
private ongoingSearches: Map<number, RawProcess> = new Map();
|
|
80
|
-
|
|
81
|
-
// Each incoming search is given a unique id, returned to the client. This is the next id we will assigned.
|
|
82
|
-
private nextSearchId: number = 1;
|
|
83
|
-
|
|
84
|
-
private client: SearchInWorkspaceClient | undefined;
|
|
85
|
-
|
|
86
|
-
@inject(RgPath)
|
|
87
|
-
protected readonly rgPath: string;
|
|
88
|
-
|
|
89
|
-
constructor(
|
|
90
|
-
@inject(ILogger) protected readonly logger: ILogger,
|
|
91
|
-
@inject(RawProcessFactory) protected readonly rawProcessFactory: RawProcessFactory,
|
|
92
|
-
) { }
|
|
93
|
-
|
|
94
|
-
setClient(client: SearchInWorkspaceClient | undefined): void {
|
|
95
|
-
this.client = client;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
protected getArgs(options?: SearchInWorkspaceOptions): string[] {
|
|
99
|
-
const args = new Set<string>();
|
|
100
|
-
|
|
101
|
-
args.add('--hidden');
|
|
102
|
-
args.add('--json');
|
|
103
|
-
|
|
104
|
-
if (options?.multiline) {
|
|
105
|
-
args.add('--multiline');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (options?.matchCase) {
|
|
109
|
-
args.add('--case-sensitive');
|
|
110
|
-
} else {
|
|
111
|
-
args.add('--ignore-case');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (options?.includeIgnored) {
|
|
115
|
-
args.add('--no-ignore');
|
|
116
|
-
}
|
|
117
|
-
if (options?.maxFileSize) {
|
|
118
|
-
args.add('--max-filesize=' + options.maxFileSize.trim());
|
|
119
|
-
} else {
|
|
120
|
-
args.add('--max-filesize=20M');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (options?.include) {
|
|
124
|
-
this.addGlobArgs(args, options.include, false);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (options?.exclude) {
|
|
128
|
-
this.addGlobArgs(args, options.exclude, true);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (options?.followSymlinks) {
|
|
132
|
-
args.add('--follow');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (options?.useRegExp || options?.matchWholeWord) {
|
|
136
|
-
args.add('--regexp');
|
|
137
|
-
} else {
|
|
138
|
-
args.add('--fixed-strings');
|
|
139
|
-
args.add('--');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return Array.from(args);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Add glob patterns to ripgrep's arguments
|
|
147
|
-
* @param args ripgrep set of arguments
|
|
148
|
-
* @param patterns patterns to include as globs
|
|
149
|
-
* @param exclude whether to negate the glob pattern or not
|
|
150
|
-
*/
|
|
151
|
-
protected addGlobArgs(args: Set<string>, patterns: string[], exclude: boolean = false): void {
|
|
152
|
-
const sanitizedPatterns = patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
|
153
|
-
for (let pattern of sanitizedPatterns) {
|
|
154
|
-
// make sure the pattern always starts with `**/`
|
|
155
|
-
if (pattern.startsWith('/')) {
|
|
156
|
-
pattern = '**' + pattern;
|
|
157
|
-
} else if (!pattern.startsWith('**/')) {
|
|
158
|
-
pattern = '**/' + pattern;
|
|
159
|
-
}
|
|
160
|
-
// add the exclusion prefix
|
|
161
|
-
if (exclude) {
|
|
162
|
-
pattern = '!' + pattern;
|
|
163
|
-
}
|
|
164
|
-
args.add(`--glob=${pattern}`);
|
|
165
|
-
// add a generic glob cli argument entry to include files inside a given directory
|
|
166
|
-
if (!pattern.endsWith('*')) {
|
|
167
|
-
// ensure the new pattern ends with `/*`
|
|
168
|
-
pattern += pattern.endsWith('/') ? '*' : '/*';
|
|
169
|
-
args.add(`--glob=${pattern}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Transforms relative patterns to absolute paths, one for each given search path.
|
|
176
|
-
* The resulting paths are not validated in the file system as the pattern keeps glob information.
|
|
177
|
-
*
|
|
178
|
-
* @returns The resulting list may be larger than the received patterns as a relative pattern may
|
|
179
|
-
* resolve to multiple absolute patterns up to the number of search paths.
|
|
180
|
-
*/
|
|
181
|
-
protected replaceRelativeToAbsolute(roots: string[], patterns: string[] = []): string[] {
|
|
182
|
-
const expandedPatterns = new Set<string>();
|
|
183
|
-
for (const pattern of patterns) {
|
|
184
|
-
if (this.isPatternRelative(pattern)) {
|
|
185
|
-
// create new patterns using the absolute form for each root
|
|
186
|
-
for (const root of roots) {
|
|
187
|
-
expandedPatterns.add(path.resolve(root, pattern));
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
expandedPatterns.add(pattern);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return Array.from(expandedPatterns);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Tests if the pattern is relative and should/can be made absolute.
|
|
198
|
-
*/
|
|
199
|
-
protected isPatternRelative(pattern: string): boolean {
|
|
200
|
-
return pattern.replace(/\\/g, '/').startsWith('./');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* By default, sets the search directories for the string WHAT to the provided ROOTURIS directories
|
|
205
|
-
* and returns the assigned search id.
|
|
206
|
-
*
|
|
207
|
-
* The include / exclude (options in SearchInWorkspaceOptions) are lists of patterns for files to
|
|
208
|
-
* include / exclude during search (glob characters are allowed).
|
|
209
|
-
*
|
|
210
|
-
* include patterns successfully recognized as absolute paths will override the default search and set
|
|
211
|
-
* the search directories to the ones provided as includes.
|
|
212
|
-
* Relative paths are allowed, the application will attempt to translate them to valid absolute paths
|
|
213
|
-
* based on the applicable search directories.
|
|
214
|
-
*/
|
|
215
|
-
async search(what: string, rootUris: string[], options: SearchInWorkspaceOptions = {}): Promise<number> {
|
|
216
|
-
// Start the rg process. Use --vimgrep to get one result per
|
|
217
|
-
// line, --color=always to get color control characters that
|
|
218
|
-
// we'll use to parse the lines.
|
|
219
|
-
const searchId = this.nextSearchId++;
|
|
220
|
-
const rootPaths = rootUris.map(root => FileUri.fsPath(root));
|
|
221
|
-
// If there are absolute paths in `include` we will remove them and use
|
|
222
|
-
// those as paths to search from.
|
|
223
|
-
const searchPaths = await this.extractSearchPathsFromIncludes(rootPaths, options);
|
|
224
|
-
options.include = this.replaceRelativeToAbsolute(searchPaths, options.include);
|
|
225
|
-
options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude);
|
|
226
|
-
const rgArgs = this.getArgs(options);
|
|
227
|
-
// If we use matchWholeWord we use regExp internally, so we need
|
|
228
|
-
// to escape regexp characters if we actually not set regexp true in UI.
|
|
229
|
-
if (options?.matchWholeWord && !options.useRegExp) {
|
|
230
|
-
what = what.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&');
|
|
231
|
-
if (!/\B/.test(what.charAt(0))) {
|
|
232
|
-
what = '\\b' + what;
|
|
233
|
-
}
|
|
234
|
-
if (!/\B/.test(what.charAt(what.length - 1))) {
|
|
235
|
-
what = what + '\\b';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const args = [...rgArgs, what, ...searchPaths];
|
|
240
|
-
const processOptions: RawProcessOptions = {
|
|
241
|
-
command: this.rgPath,
|
|
242
|
-
args
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
// TODO: Use child_process directly instead of rawProcessFactory?
|
|
246
|
-
const rgProcess: RawProcess = this.rawProcessFactory(processOptions);
|
|
247
|
-
this.ongoingSearches.set(searchId, rgProcess);
|
|
248
|
-
|
|
249
|
-
rgProcess.onError(error => {
|
|
250
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
251
|
-
let errorCode = (error as any).code;
|
|
252
|
-
|
|
253
|
-
// Try to provide somewhat clearer error messages, if possible.
|
|
254
|
-
if (errorCode === 'ENOENT') {
|
|
255
|
-
errorCode = 'could not find the ripgrep (rg) binary';
|
|
256
|
-
} else if (errorCode === 'EACCES') {
|
|
257
|
-
errorCode = 'could not execute the ripgrep (rg) binary';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const errorStr = `An error happened while searching (${errorCode}).`;
|
|
261
|
-
this.wrapUpSearch(searchId, errorStr);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Running counter of results.
|
|
265
|
-
let numResults = 0;
|
|
266
|
-
|
|
267
|
-
// Buffer to accumulate incoming output.
|
|
268
|
-
let databuf: string = '';
|
|
269
|
-
|
|
270
|
-
let currentSearchResult: SearchInWorkspaceResult | undefined;
|
|
271
|
-
|
|
272
|
-
rgProcess.outputStream.on('data', (chunk: Buffer) => {
|
|
273
|
-
// We might have already reached the max number of
|
|
274
|
-
// results, sent a TERM signal to rg, but we still get
|
|
275
|
-
// the data that was already output in the mean time.
|
|
276
|
-
// It's not necessary to return early here (the check
|
|
277
|
-
// for maxResults below would avoid sending extra
|
|
278
|
-
// results), but it avoids doing unnecessary work.
|
|
279
|
-
if (options?.maxResults && numResults >= options.maxResults) {
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
databuf += chunk;
|
|
284
|
-
|
|
285
|
-
while (1) {
|
|
286
|
-
// Check if we have a complete line.
|
|
287
|
-
const eolIdx = databuf.indexOf('\n');
|
|
288
|
-
if (eolIdx < 0) {
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Get and remove the line from the data buffer.
|
|
293
|
-
const lineBuf = databuf.slice(0, eolIdx);
|
|
294
|
-
databuf = databuf.slice(eolIdx + 1);
|
|
295
|
-
|
|
296
|
-
const obj = JSON.parse(lineBuf) as IRgMessage;
|
|
297
|
-
if (obj.type === 'begin') {
|
|
298
|
-
const file = bytesOrTextToString(obj.data.path);
|
|
299
|
-
if (file) {
|
|
300
|
-
currentSearchResult = {
|
|
301
|
-
fileUri: FileUri.create(file).toString(),
|
|
302
|
-
root: this.getRoot(file, rootUris).toString(),
|
|
303
|
-
matches: []
|
|
304
|
-
};
|
|
305
|
-
} else {
|
|
306
|
-
this.logger.error('Begin message without path. ' + JSON.stringify(obj));
|
|
307
|
-
}
|
|
308
|
-
} else if (obj.type === 'end') {
|
|
309
|
-
if (currentSearchResult && this.client) {
|
|
310
|
-
this.client.onResult(searchId, currentSearchResult);
|
|
311
|
-
}
|
|
312
|
-
currentSearchResult = undefined;
|
|
313
|
-
} else if (obj.type === 'match') {
|
|
314
|
-
if (!currentSearchResult) {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
const data = obj.data;
|
|
318
|
-
const file = bytesOrTextToString(data.path);
|
|
319
|
-
const line = data.line_number;
|
|
320
|
-
const lineText = bytesOrTextToString(data.lines);
|
|
321
|
-
|
|
322
|
-
if (file === undefined || lineText === undefined) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const lineInBytes = Buffer.from(lineText);
|
|
327
|
-
|
|
328
|
-
for (const submatch of data.submatches) {
|
|
329
|
-
const startOffset = lineInBytes.slice(0, submatch.start).toString().length;
|
|
330
|
-
const match = bytesOrTextToString(submatch.match);
|
|
331
|
-
let lineInfo: string | LinePreview = lineText.trimRight();
|
|
332
|
-
if (lineInfo.length > 300) {
|
|
333
|
-
const prefixLength = 25;
|
|
334
|
-
const start = Math.max(startOffset - prefixLength, 0);
|
|
335
|
-
const length = prefixLength + match.length + 70;
|
|
336
|
-
let prefix = '';
|
|
337
|
-
if (start >= prefixLength) {
|
|
338
|
-
prefix = '...';
|
|
339
|
-
}
|
|
340
|
-
const character = (start < prefixLength ? start : prefixLength) + prefix.length + 1;
|
|
341
|
-
lineInfo = <LinePreview>{
|
|
342
|
-
text: prefix + lineInfo.substring(start, start + length),
|
|
343
|
-
character
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
currentSearchResult.matches.push({
|
|
347
|
-
line,
|
|
348
|
-
character: startOffset + 1,
|
|
349
|
-
length: match.length,
|
|
350
|
-
lineText: lineInfo
|
|
351
|
-
});
|
|
352
|
-
numResults++;
|
|
353
|
-
|
|
354
|
-
// Did we reach the maximum number of results?
|
|
355
|
-
if (options?.maxResults && numResults >= options.maxResults) {
|
|
356
|
-
rgProcess.kill();
|
|
357
|
-
if (currentSearchResult && this.client) {
|
|
358
|
-
this.client.onResult(searchId, currentSearchResult);
|
|
359
|
-
}
|
|
360
|
-
currentSearchResult = undefined;
|
|
361
|
-
this.wrapUpSearch(searchId);
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
rgProcess.outputStream.on('end', () => {
|
|
370
|
-
// If we reached maxResults, we should have already
|
|
371
|
-
// wrapped up the search. Returning early avoids
|
|
372
|
-
// logging a warning message in wrapUpSearch.
|
|
373
|
-
if (options?.maxResults && numResults >= options.maxResults) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
this.wrapUpSearch(searchId);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
return searchId;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* The default search paths are set to be the root paths associated to a workspace
|
|
385
|
-
* however the search scope can be further refined with the include paths available in the search options.
|
|
386
|
-
* This method will replace the searching paths to the ones specified in the 'include' options but as long
|
|
387
|
-
* as the 'include' paths can be successfully validated as existing.
|
|
388
|
-
*
|
|
389
|
-
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
|
|
390
|
-
* derived from the include options which can be used to perform the search.
|
|
391
|
-
*
|
|
392
|
-
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
|
|
393
|
-
* provided as an equivalent search path instead.
|
|
394
|
-
*/
|
|
395
|
-
protected async extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): Promise<string[]> {
|
|
396
|
-
if (!options.include) {
|
|
397
|
-
return rootPaths;
|
|
398
|
-
}
|
|
399
|
-
const resolvedPaths = new Set<string>();
|
|
400
|
-
const include: string[] = [];
|
|
401
|
-
for (const pattern of options.include) {
|
|
402
|
-
let keep = true;
|
|
403
|
-
for (const root of rootPaths) {
|
|
404
|
-
const absolutePath = await this.getAbsolutePathFromPattern(root, pattern);
|
|
405
|
-
// undefined means the pattern cannot be converted into an absolute path
|
|
406
|
-
if (absolutePath) {
|
|
407
|
-
resolvedPaths.add(absolutePath);
|
|
408
|
-
keep = false;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (keep) {
|
|
412
|
-
include.push(pattern);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
options.include = include;
|
|
416
|
-
return resolvedPaths.size > 0
|
|
417
|
-
? Array.from(resolvedPaths)
|
|
418
|
-
: rootPaths;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Transform include/exclude option patterns from relative patterns to absolute patterns.
|
|
423
|
-
* E.g. './abc/foo.*' to '${root}/abc/foo.*', the transformation does not validate the
|
|
424
|
-
* pattern against the file system as glob suffixes remain.
|
|
425
|
-
*
|
|
426
|
-
* @returns undefined if the pattern cannot be converted into an absolute path.
|
|
427
|
-
*/
|
|
428
|
-
protected async getAbsolutePathFromPattern(root: string, pattern: string): Promise<string | undefined> {
|
|
429
|
-
pattern = pattern.replace(/\\/g, '/');
|
|
430
|
-
// The pattern is not referring to a single file or folder, i.e. not to be converted
|
|
431
|
-
if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) {
|
|
432
|
-
return undefined;
|
|
433
|
-
}
|
|
434
|
-
// remove the `/**` suffix if present
|
|
435
|
-
if (pattern.endsWith('/**')) {
|
|
436
|
-
pattern = pattern.substring(0, pattern.length - 3);
|
|
437
|
-
}
|
|
438
|
-
// if `pattern` is absolute then `root` will be ignored by `path.resolve()`
|
|
439
|
-
const targetPath = path.resolve(root, pattern);
|
|
440
|
-
if (await fs.pathExists(targetPath)) {
|
|
441
|
-
return targetPath;
|
|
442
|
-
}
|
|
443
|
-
return undefined;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Returns the root folder uri that a file belongs to.
|
|
448
|
-
* In case that a file belongs to more than one root folders, returns the root folder that is closest to the file.
|
|
449
|
-
* If the file is not from the current workspace, returns empty string.
|
|
450
|
-
* @param filePath string path of the file
|
|
451
|
-
* @param rootUris string URIs of the root folders in the current workspace
|
|
452
|
-
*/
|
|
453
|
-
private getRoot(filePath: string, rootUris: string[]): URI {
|
|
454
|
-
const roots = rootUris.filter(root => new URI(root).withScheme('file').isEqualOrParent(FileUri.create(filePath).withScheme('file')));
|
|
455
|
-
if (roots.length > 0) {
|
|
456
|
-
return FileUri.create(FileUri.fsPath(roots.sort((r1, r2) => r2.length - r1.length)[0]));
|
|
457
|
-
}
|
|
458
|
-
return new URI();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Cancel an ongoing search. Trying to cancel a search that doesn't exist isn't an
|
|
462
|
-
// error, otherwise we'd have to deal with race conditions, where a client cancels a
|
|
463
|
-
// search that finishes normally at the same time.
|
|
464
|
-
cancel(searchId: number): Promise<void> {
|
|
465
|
-
const process = this.ongoingSearches.get(searchId);
|
|
466
|
-
if (process) {
|
|
467
|
-
process.kill();
|
|
468
|
-
this.wrapUpSearch(searchId);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return Promise.resolve();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Send onDone to the client and clean up what we know about search searchId.
|
|
475
|
-
private wrapUpSearch(searchId: number, error?: string): void {
|
|
476
|
-
if (this.ongoingSearches.delete(searchId)) {
|
|
477
|
-
if (this.client) {
|
|
478
|
-
this.logger.debug('Sending onDone for ' + searchId, error);
|
|
479
|
-
this.client.onDone(searchId, error);
|
|
480
|
-
} else {
|
|
481
|
-
this.logger.debug('Wrapping up search ' + searchId + ' but no client');
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
this.logger.debug("Trying to wrap up a search we don't know about " + searchId);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
dispose(): void {
|
|
489
|
-
}
|
|
490
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2017-2021 Ericsson and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as fs from '@theia/core/shared/fs-extra';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { ILogger } from '@theia/core';
|
|
20
|
+
import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node';
|
|
21
|
+
import { FileUri } from '@theia/core/lib/common/file-uri';
|
|
22
|
+
import URI from '@theia/core/lib/common/uri';
|
|
23
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
24
|
+
import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface';
|
|
25
|
+
|
|
26
|
+
export const RgPath = Symbol('RgPath');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Typing for ripgrep's arbitrary data object:
|
|
30
|
+
*
|
|
31
|
+
* https://docs.rs/grep-printer/0.1.0/grep_printer/struct.JSON.html#object-arbitrary-data
|
|
32
|
+
*/
|
|
33
|
+
export type IRgBytesOrText = { bytes: string } | { text: string };
|
|
34
|
+
|
|
35
|
+
function bytesOrTextToString(obj: IRgBytesOrText): string {
|
|
36
|
+
return 'bytes' in obj ?
|
|
37
|
+
Buffer.from(obj.bytes, 'base64').toString() :
|
|
38
|
+
obj.text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type IRgMessage = IRgMatch | IRgBegin | IRgEnd;
|
|
42
|
+
|
|
43
|
+
interface IRgMatch {
|
|
44
|
+
type: 'match';
|
|
45
|
+
data: {
|
|
46
|
+
path: IRgBytesOrText;
|
|
47
|
+
lines: IRgBytesOrText;
|
|
48
|
+
line_number: number;
|
|
49
|
+
absolute_offset: number;
|
|
50
|
+
submatches: IRgSubmatch[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IRgSubmatch {
|
|
55
|
+
match: IRgBytesOrText;
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface IRgBegin {
|
|
61
|
+
type: 'begin';
|
|
62
|
+
data: {
|
|
63
|
+
path: IRgBytesOrText;
|
|
64
|
+
lines: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface IRgEnd {
|
|
69
|
+
type: 'end';
|
|
70
|
+
data: {
|
|
71
|
+
path: IRgBytesOrText;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@injectable()
|
|
76
|
+
export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
|
|
77
|
+
|
|
78
|
+
// List of ongoing searches, maps search id to a the started rg process.
|
|
79
|
+
private ongoingSearches: Map<number, RawProcess> = new Map();
|
|
80
|
+
|
|
81
|
+
// Each incoming search is given a unique id, returned to the client. This is the next id we will assigned.
|
|
82
|
+
private nextSearchId: number = 1;
|
|
83
|
+
|
|
84
|
+
private client: SearchInWorkspaceClient | undefined;
|
|
85
|
+
|
|
86
|
+
@inject(RgPath)
|
|
87
|
+
protected readonly rgPath: string;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
@inject(ILogger) protected readonly logger: ILogger,
|
|
91
|
+
@inject(RawProcessFactory) protected readonly rawProcessFactory: RawProcessFactory,
|
|
92
|
+
) { }
|
|
93
|
+
|
|
94
|
+
setClient(client: SearchInWorkspaceClient | undefined): void {
|
|
95
|
+
this.client = client;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected getArgs(options?: SearchInWorkspaceOptions): string[] {
|
|
99
|
+
const args = new Set<string>();
|
|
100
|
+
|
|
101
|
+
args.add('--hidden');
|
|
102
|
+
args.add('--json');
|
|
103
|
+
|
|
104
|
+
if (options?.multiline) {
|
|
105
|
+
args.add('--multiline');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options?.matchCase) {
|
|
109
|
+
args.add('--case-sensitive');
|
|
110
|
+
} else {
|
|
111
|
+
args.add('--ignore-case');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options?.includeIgnored) {
|
|
115
|
+
args.add('--no-ignore');
|
|
116
|
+
}
|
|
117
|
+
if (options?.maxFileSize) {
|
|
118
|
+
args.add('--max-filesize=' + options.maxFileSize.trim());
|
|
119
|
+
} else {
|
|
120
|
+
args.add('--max-filesize=20M');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options?.include) {
|
|
124
|
+
this.addGlobArgs(args, options.include, false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options?.exclude) {
|
|
128
|
+
this.addGlobArgs(args, options.exclude, true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options?.followSymlinks) {
|
|
132
|
+
args.add('--follow');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options?.useRegExp || options?.matchWholeWord) {
|
|
136
|
+
args.add('--regexp');
|
|
137
|
+
} else {
|
|
138
|
+
args.add('--fixed-strings');
|
|
139
|
+
args.add('--');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Array.from(args);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Add glob patterns to ripgrep's arguments
|
|
147
|
+
* @param args ripgrep set of arguments
|
|
148
|
+
* @param patterns patterns to include as globs
|
|
149
|
+
* @param exclude whether to negate the glob pattern or not
|
|
150
|
+
*/
|
|
151
|
+
protected addGlobArgs(args: Set<string>, patterns: string[], exclude: boolean = false): void {
|
|
152
|
+
const sanitizedPatterns = patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
|
153
|
+
for (let pattern of sanitizedPatterns) {
|
|
154
|
+
// make sure the pattern always starts with `**/`
|
|
155
|
+
if (pattern.startsWith('/')) {
|
|
156
|
+
pattern = '**' + pattern;
|
|
157
|
+
} else if (!pattern.startsWith('**/')) {
|
|
158
|
+
pattern = '**/' + pattern;
|
|
159
|
+
}
|
|
160
|
+
// add the exclusion prefix
|
|
161
|
+
if (exclude) {
|
|
162
|
+
pattern = '!' + pattern;
|
|
163
|
+
}
|
|
164
|
+
args.add(`--glob=${pattern}`);
|
|
165
|
+
// add a generic glob cli argument entry to include files inside a given directory
|
|
166
|
+
if (!pattern.endsWith('*')) {
|
|
167
|
+
// ensure the new pattern ends with `/*`
|
|
168
|
+
pattern += pattern.endsWith('/') ? '*' : '/*';
|
|
169
|
+
args.add(`--glob=${pattern}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Transforms relative patterns to absolute paths, one for each given search path.
|
|
176
|
+
* The resulting paths are not validated in the file system as the pattern keeps glob information.
|
|
177
|
+
*
|
|
178
|
+
* @returns The resulting list may be larger than the received patterns as a relative pattern may
|
|
179
|
+
* resolve to multiple absolute patterns up to the number of search paths.
|
|
180
|
+
*/
|
|
181
|
+
protected replaceRelativeToAbsolute(roots: string[], patterns: string[] = []): string[] {
|
|
182
|
+
const expandedPatterns = new Set<string>();
|
|
183
|
+
for (const pattern of patterns) {
|
|
184
|
+
if (this.isPatternRelative(pattern)) {
|
|
185
|
+
// create new patterns using the absolute form for each root
|
|
186
|
+
for (const root of roots) {
|
|
187
|
+
expandedPatterns.add(path.resolve(root, pattern));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
expandedPatterns.add(pattern);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return Array.from(expandedPatterns);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Tests if the pattern is relative and should/can be made absolute.
|
|
198
|
+
*/
|
|
199
|
+
protected isPatternRelative(pattern: string): boolean {
|
|
200
|
+
return pattern.replace(/\\/g, '/').startsWith('./');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* By default, sets the search directories for the string WHAT to the provided ROOTURIS directories
|
|
205
|
+
* and returns the assigned search id.
|
|
206
|
+
*
|
|
207
|
+
* The include / exclude (options in SearchInWorkspaceOptions) are lists of patterns for files to
|
|
208
|
+
* include / exclude during search (glob characters are allowed).
|
|
209
|
+
*
|
|
210
|
+
* include patterns successfully recognized as absolute paths will override the default search and set
|
|
211
|
+
* the search directories to the ones provided as includes.
|
|
212
|
+
* Relative paths are allowed, the application will attempt to translate them to valid absolute paths
|
|
213
|
+
* based on the applicable search directories.
|
|
214
|
+
*/
|
|
215
|
+
async search(what: string, rootUris: string[], options: SearchInWorkspaceOptions = {}): Promise<number> {
|
|
216
|
+
// Start the rg process. Use --vimgrep to get one result per
|
|
217
|
+
// line, --color=always to get color control characters that
|
|
218
|
+
// we'll use to parse the lines.
|
|
219
|
+
const searchId = this.nextSearchId++;
|
|
220
|
+
const rootPaths = rootUris.map(root => FileUri.fsPath(root));
|
|
221
|
+
// If there are absolute paths in `include` we will remove them and use
|
|
222
|
+
// those as paths to search from.
|
|
223
|
+
const searchPaths = await this.extractSearchPathsFromIncludes(rootPaths, options);
|
|
224
|
+
options.include = this.replaceRelativeToAbsolute(searchPaths, options.include);
|
|
225
|
+
options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude);
|
|
226
|
+
const rgArgs = this.getArgs(options);
|
|
227
|
+
// If we use matchWholeWord we use regExp internally, so we need
|
|
228
|
+
// to escape regexp characters if we actually not set regexp true in UI.
|
|
229
|
+
if (options?.matchWholeWord && !options.useRegExp) {
|
|
230
|
+
what = what.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&');
|
|
231
|
+
if (!/\B/.test(what.charAt(0))) {
|
|
232
|
+
what = '\\b' + what;
|
|
233
|
+
}
|
|
234
|
+
if (!/\B/.test(what.charAt(what.length - 1))) {
|
|
235
|
+
what = what + '\\b';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const args = [...rgArgs, what, ...searchPaths];
|
|
240
|
+
const processOptions: RawProcessOptions = {
|
|
241
|
+
command: this.rgPath,
|
|
242
|
+
args
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// TODO: Use child_process directly instead of rawProcessFactory?
|
|
246
|
+
const rgProcess: RawProcess = this.rawProcessFactory(processOptions);
|
|
247
|
+
this.ongoingSearches.set(searchId, rgProcess);
|
|
248
|
+
|
|
249
|
+
rgProcess.onError(error => {
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
251
|
+
let errorCode = (error as any).code;
|
|
252
|
+
|
|
253
|
+
// Try to provide somewhat clearer error messages, if possible.
|
|
254
|
+
if (errorCode === 'ENOENT') {
|
|
255
|
+
errorCode = 'could not find the ripgrep (rg) binary';
|
|
256
|
+
} else if (errorCode === 'EACCES') {
|
|
257
|
+
errorCode = 'could not execute the ripgrep (rg) binary';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const errorStr = `An error happened while searching (${errorCode}).`;
|
|
261
|
+
this.wrapUpSearch(searchId, errorStr);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Running counter of results.
|
|
265
|
+
let numResults = 0;
|
|
266
|
+
|
|
267
|
+
// Buffer to accumulate incoming output.
|
|
268
|
+
let databuf: string = '';
|
|
269
|
+
|
|
270
|
+
let currentSearchResult: SearchInWorkspaceResult | undefined;
|
|
271
|
+
|
|
272
|
+
rgProcess.outputStream.on('data', (chunk: Buffer) => {
|
|
273
|
+
// We might have already reached the max number of
|
|
274
|
+
// results, sent a TERM signal to rg, but we still get
|
|
275
|
+
// the data that was already output in the mean time.
|
|
276
|
+
// It's not necessary to return early here (the check
|
|
277
|
+
// for maxResults below would avoid sending extra
|
|
278
|
+
// results), but it avoids doing unnecessary work.
|
|
279
|
+
if (options?.maxResults && numResults >= options.maxResults) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
databuf += chunk;
|
|
284
|
+
|
|
285
|
+
while (1) {
|
|
286
|
+
// Check if we have a complete line.
|
|
287
|
+
const eolIdx = databuf.indexOf('\n');
|
|
288
|
+
if (eolIdx < 0) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get and remove the line from the data buffer.
|
|
293
|
+
const lineBuf = databuf.slice(0, eolIdx);
|
|
294
|
+
databuf = databuf.slice(eolIdx + 1);
|
|
295
|
+
|
|
296
|
+
const obj = JSON.parse(lineBuf) as IRgMessage;
|
|
297
|
+
if (obj.type === 'begin') {
|
|
298
|
+
const file = bytesOrTextToString(obj.data.path);
|
|
299
|
+
if (file) {
|
|
300
|
+
currentSearchResult = {
|
|
301
|
+
fileUri: FileUri.create(file).toString(),
|
|
302
|
+
root: this.getRoot(file, rootUris).toString(),
|
|
303
|
+
matches: []
|
|
304
|
+
};
|
|
305
|
+
} else {
|
|
306
|
+
this.logger.error('Begin message without path. ' + JSON.stringify(obj));
|
|
307
|
+
}
|
|
308
|
+
} else if (obj.type === 'end') {
|
|
309
|
+
if (currentSearchResult && this.client) {
|
|
310
|
+
this.client.onResult(searchId, currentSearchResult);
|
|
311
|
+
}
|
|
312
|
+
currentSearchResult = undefined;
|
|
313
|
+
} else if (obj.type === 'match') {
|
|
314
|
+
if (!currentSearchResult) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const data = obj.data;
|
|
318
|
+
const file = bytesOrTextToString(data.path);
|
|
319
|
+
const line = data.line_number;
|
|
320
|
+
const lineText = bytesOrTextToString(data.lines);
|
|
321
|
+
|
|
322
|
+
if (file === undefined || lineText === undefined) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const lineInBytes = Buffer.from(lineText);
|
|
327
|
+
|
|
328
|
+
for (const submatch of data.submatches) {
|
|
329
|
+
const startOffset = lineInBytes.slice(0, submatch.start).toString().length;
|
|
330
|
+
const match = bytesOrTextToString(submatch.match);
|
|
331
|
+
let lineInfo: string | LinePreview = lineText.trimRight();
|
|
332
|
+
if (lineInfo.length > 300) {
|
|
333
|
+
const prefixLength = 25;
|
|
334
|
+
const start = Math.max(startOffset - prefixLength, 0);
|
|
335
|
+
const length = prefixLength + match.length + 70;
|
|
336
|
+
let prefix = '';
|
|
337
|
+
if (start >= prefixLength) {
|
|
338
|
+
prefix = '...';
|
|
339
|
+
}
|
|
340
|
+
const character = (start < prefixLength ? start : prefixLength) + prefix.length + 1;
|
|
341
|
+
lineInfo = <LinePreview>{
|
|
342
|
+
text: prefix + lineInfo.substring(start, start + length),
|
|
343
|
+
character
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
currentSearchResult.matches.push({
|
|
347
|
+
line,
|
|
348
|
+
character: startOffset + 1,
|
|
349
|
+
length: match.length,
|
|
350
|
+
lineText: lineInfo
|
|
351
|
+
});
|
|
352
|
+
numResults++;
|
|
353
|
+
|
|
354
|
+
// Did we reach the maximum number of results?
|
|
355
|
+
if (options?.maxResults && numResults >= options.maxResults) {
|
|
356
|
+
rgProcess.kill();
|
|
357
|
+
if (currentSearchResult && this.client) {
|
|
358
|
+
this.client.onResult(searchId, currentSearchResult);
|
|
359
|
+
}
|
|
360
|
+
currentSearchResult = undefined;
|
|
361
|
+
this.wrapUpSearch(searchId);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
rgProcess.outputStream.on('end', () => {
|
|
370
|
+
// If we reached maxResults, we should have already
|
|
371
|
+
// wrapped up the search. Returning early avoids
|
|
372
|
+
// logging a warning message in wrapUpSearch.
|
|
373
|
+
if (options?.maxResults && numResults >= options.maxResults) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.wrapUpSearch(searchId);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return searchId;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The default search paths are set to be the root paths associated to a workspace
|
|
385
|
+
* however the search scope can be further refined with the include paths available in the search options.
|
|
386
|
+
* This method will replace the searching paths to the ones specified in the 'include' options but as long
|
|
387
|
+
* as the 'include' paths can be successfully validated as existing.
|
|
388
|
+
*
|
|
389
|
+
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
|
|
390
|
+
* derived from the include options which can be used to perform the search.
|
|
391
|
+
*
|
|
392
|
+
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
|
|
393
|
+
* provided as an equivalent search path instead.
|
|
394
|
+
*/
|
|
395
|
+
protected async extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): Promise<string[]> {
|
|
396
|
+
if (!options.include) {
|
|
397
|
+
return rootPaths;
|
|
398
|
+
}
|
|
399
|
+
const resolvedPaths = new Set<string>();
|
|
400
|
+
const include: string[] = [];
|
|
401
|
+
for (const pattern of options.include) {
|
|
402
|
+
let keep = true;
|
|
403
|
+
for (const root of rootPaths) {
|
|
404
|
+
const absolutePath = await this.getAbsolutePathFromPattern(root, pattern);
|
|
405
|
+
// undefined means the pattern cannot be converted into an absolute path
|
|
406
|
+
if (absolutePath) {
|
|
407
|
+
resolvedPaths.add(absolutePath);
|
|
408
|
+
keep = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (keep) {
|
|
412
|
+
include.push(pattern);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
options.include = include;
|
|
416
|
+
return resolvedPaths.size > 0
|
|
417
|
+
? Array.from(resolvedPaths)
|
|
418
|
+
: rootPaths;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Transform include/exclude option patterns from relative patterns to absolute patterns.
|
|
423
|
+
* E.g. './abc/foo.*' to '${root}/abc/foo.*', the transformation does not validate the
|
|
424
|
+
* pattern against the file system as glob suffixes remain.
|
|
425
|
+
*
|
|
426
|
+
* @returns undefined if the pattern cannot be converted into an absolute path.
|
|
427
|
+
*/
|
|
428
|
+
protected async getAbsolutePathFromPattern(root: string, pattern: string): Promise<string | undefined> {
|
|
429
|
+
pattern = pattern.replace(/\\/g, '/');
|
|
430
|
+
// The pattern is not referring to a single file or folder, i.e. not to be converted
|
|
431
|
+
if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
// remove the `/**` suffix if present
|
|
435
|
+
if (pattern.endsWith('/**')) {
|
|
436
|
+
pattern = pattern.substring(0, pattern.length - 3);
|
|
437
|
+
}
|
|
438
|
+
// if `pattern` is absolute then `root` will be ignored by `path.resolve()`
|
|
439
|
+
const targetPath = path.resolve(root, pattern);
|
|
440
|
+
if (await fs.pathExists(targetPath)) {
|
|
441
|
+
return targetPath;
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Returns the root folder uri that a file belongs to.
|
|
448
|
+
* In case that a file belongs to more than one root folders, returns the root folder that is closest to the file.
|
|
449
|
+
* If the file is not from the current workspace, returns empty string.
|
|
450
|
+
* @param filePath string path of the file
|
|
451
|
+
* @param rootUris string URIs of the root folders in the current workspace
|
|
452
|
+
*/
|
|
453
|
+
private getRoot(filePath: string, rootUris: string[]): URI {
|
|
454
|
+
const roots = rootUris.filter(root => new URI(root).withScheme('file').isEqualOrParent(FileUri.create(filePath).withScheme('file')));
|
|
455
|
+
if (roots.length > 0) {
|
|
456
|
+
return FileUri.create(FileUri.fsPath(roots.sort((r1, r2) => r2.length - r1.length)[0]));
|
|
457
|
+
}
|
|
458
|
+
return new URI();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Cancel an ongoing search. Trying to cancel a search that doesn't exist isn't an
|
|
462
|
+
// error, otherwise we'd have to deal with race conditions, where a client cancels a
|
|
463
|
+
// search that finishes normally at the same time.
|
|
464
|
+
cancel(searchId: number): Promise<void> {
|
|
465
|
+
const process = this.ongoingSearches.get(searchId);
|
|
466
|
+
if (process) {
|
|
467
|
+
process.kill();
|
|
468
|
+
this.wrapUpSearch(searchId);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return Promise.resolve();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Send onDone to the client and clean up what we know about search searchId.
|
|
475
|
+
private wrapUpSearch(searchId: number, error?: string): void {
|
|
476
|
+
if (this.ongoingSearches.delete(searchId)) {
|
|
477
|
+
if (this.client) {
|
|
478
|
+
this.logger.debug('Sending onDone for ' + searchId, error);
|
|
479
|
+
this.client.onDone(searchId, error);
|
|
480
|
+
} else {
|
|
481
|
+
this.logger.debug('Wrapping up search ' + searchId + ' but no client');
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
this.logger.debug("Trying to wrap up a search we don't know about " + searchId);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
dispose(): void {
|
|
489
|
+
}
|
|
490
|
+
}
|