@theia/search-in-workspace 1.65.0-next.6 → 1.66.0-next.0
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/lib/browser/components/search-in-workspace-textarea.d.ts.map +1 -1
- package/lib/browser/components/search-in-workspace-textarea.js +1 -2
- package/lib/browser/components/search-in-workspace-textarea.js.map +1 -1
- package/lib/browser/search-in-workspace-frontend-module.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-frontend-module.js +3 -2
- package/lib/browser/search-in-workspace-frontend-module.js.map +1 -1
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts +3 -2
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-result-tree-widget.js +18 -15
- package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
- package/lib/browser/search-in-workspace-service.d.ts +5 -5
- package/lib/browser/search-in-workspace-service.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-service.js +4 -4
- package/lib/browser/search-in-workspace-service.js.map +1 -1
- package/lib/browser/search-in-workspace-widget.d.ts +1 -1
- package/lib/browser/search-in-workspace-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-widget.js +2 -4
- package/lib/browser/search-in-workspace-widget.js.map +1 -1
- package/lib/browser-only/browser-only-search-in-workspace-service.d.ts +5 -0
- package/lib/browser-only/browser-only-search-in-workspace-service.d.ts.map +1 -0
- package/lib/browser-only/browser-only-search-in-workspace-service.js +40 -0
- package/lib/browser-only/browser-only-search-in-workspace-service.js.map +1 -0
- package/lib/browser-only/browser-search-in-workspace-server.d.ts +80 -0
- package/lib/browser-only/browser-search-in-workspace-server.d.ts.map +1 -0
- package/lib/browser-only/browser-search-in-workspace-server.js +378 -0
- package/lib/browser-only/browser-search-in-workspace-server.js.map +1 -0
- package/lib/browser-only/search-in-workspace-frontend-only-module.d.ts +4 -0
- package/lib/browser-only/search-in-workspace-frontend-only-module.d.ts.map +1 -0
- package/lib/browser-only/search-in-workspace-frontend-only-module.js +37 -0
- package/lib/browser-only/search-in-workspace-frontend-only-module.js.map +1 -0
- package/lib/{browser → common}/search-in-workspace-preferences.d.ts +2 -1
- package/lib/common/search-in-workspace-preferences.d.ts.map +1 -0
- package/lib/{browser → common}/search-in-workspace-preferences.js +4 -3
- package/lib/common/search-in-workspace-preferences.js.map +1 -0
- package/lib/node/search-in-workspace-backend-module.d.ts.map +1 -1
- package/lib/node/search-in-workspace-backend-module.js +2 -0
- package/lib/node/search-in-workspace-backend-module.js.map +1 -1
- package/package.json +13 -10
- package/src/browser/components/search-in-workspace-textarea.tsx +1 -2
- package/src/browser/search-in-workspace-frontend-module.ts +5 -4
- package/src/browser/search-in-workspace-result-tree-widget.tsx +30 -17
- package/src/browser/search-in-workspace-service.ts +8 -7
- package/src/browser/search-in-workspace-widget.tsx +4 -3
- package/src/browser-only/browser-only-search-in-workspace-service.ts +30 -0
- package/src/browser-only/browser-search-in-workspace-server.ts +471 -0
- package/src/browser-only/search-in-workspace-frontend-only-module.ts +35 -0
- package/src/{browser → common}/search-in-workspace-preferences.ts +3 -2
- package/src/node/search-in-workspace-backend-module.ts +2 -0
- package/lib/browser/search-in-workspace-preferences.d.ts.map +0 -1
- package/lib/browser/search-in-workspace-preferences.js.map +0 -1
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Maksim Kachurin 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
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
17
|
+
import type {
|
|
18
|
+
SearchInWorkspaceClient,
|
|
19
|
+
SearchInWorkspaceOptions,
|
|
20
|
+
SearchInWorkspaceResult,
|
|
21
|
+
SearchInWorkspaceServer,
|
|
22
|
+
SearchMatch
|
|
23
|
+
} from '../common/search-in-workspace-interface';
|
|
24
|
+
import { FileUri } from '@theia/core/lib/common/file-uri';
|
|
25
|
+
import { URI, ILogger } from '@theia/core';
|
|
26
|
+
import { FileService, TextFileOperationError, TextFileOperationResult } from '@theia/filesystem/lib/browser/file-service';
|
|
27
|
+
import { normalizeGlob, matchesPattern, createIgnoreMatcher, getIgnorePatterns } from '@theia/filesystem/lib/browser-only/file-search';
|
|
28
|
+
import { escapeRegExpCharacters } from '@theia/core/lib/common/strings';
|
|
29
|
+
import { BinarySize, type FileStatWithMetadata } from '@theia/filesystem/lib/common/files';
|
|
30
|
+
|
|
31
|
+
interface SearchController {
|
|
32
|
+
regex: RegExp;
|
|
33
|
+
searchPaths: URI[];
|
|
34
|
+
options: SearchInWorkspaceOptions;
|
|
35
|
+
isAborted: () => boolean;
|
|
36
|
+
abort: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const minimatchOpts = {
|
|
40
|
+
dot: true,
|
|
41
|
+
matchBase: true,
|
|
42
|
+
nocase: true
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
@injectable()
|
|
46
|
+
export class BrowserSearchInWorkspaceServer implements SearchInWorkspaceServer {
|
|
47
|
+
@inject(ILogger) @named('search-in-workspace')
|
|
48
|
+
protected readonly logger: ILogger;
|
|
49
|
+
|
|
50
|
+
@inject(FileService)
|
|
51
|
+
protected readonly fs: FileService;
|
|
52
|
+
|
|
53
|
+
private client: SearchInWorkspaceClient | undefined;
|
|
54
|
+
private ongoingSearches: Map<number, SearchController> = new Map();
|
|
55
|
+
private nextSearchId: number = 1;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the client for receiving search results
|
|
59
|
+
*/
|
|
60
|
+
setClient(client: SearchInWorkspaceClient | undefined): void {
|
|
61
|
+
this.client = client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initiates a search operation and returns a search ID.
|
|
66
|
+
* @param what - The search term or pattern
|
|
67
|
+
* @param rootUris - Array of root URIs to search in
|
|
68
|
+
* @param opts - Search options including filters and limits
|
|
69
|
+
* @returns Promise resolving to the search ID
|
|
70
|
+
*/
|
|
71
|
+
async search(what: string, rootUris: string[], opts: SearchInWorkspaceOptions = {}): Promise<number> {
|
|
72
|
+
const searchId = this.nextSearchId++;
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
|
|
75
|
+
const { regex, searchPaths, options } = await this.processSearchOptions(
|
|
76
|
+
what,
|
|
77
|
+
rootUris,
|
|
78
|
+
opts,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
this.ongoingSearches.set(searchId, {
|
|
82
|
+
regex,
|
|
83
|
+
searchPaths,
|
|
84
|
+
options,
|
|
85
|
+
isAborted: () => controller.signal.aborted,
|
|
86
|
+
abort: () => controller.abort()
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Start search asynchronously and return searchId immediately
|
|
90
|
+
this.doSearch(searchId).catch((error: Error) => {
|
|
91
|
+
const errorStr = `An error happened while searching (${error.message}).`;
|
|
92
|
+
|
|
93
|
+
this.client?.onDone(searchId, errorStr);
|
|
94
|
+
}).finally(() => {
|
|
95
|
+
this.ongoingSearches.delete(searchId);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return searchId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Cancels an ongoing search operation.
|
|
103
|
+
* @param searchId - The ID of the search to cancel
|
|
104
|
+
*/
|
|
105
|
+
cancel(searchId: number): Promise<void> {
|
|
106
|
+
const controller = this.ongoingSearches.get(searchId);
|
|
107
|
+
|
|
108
|
+
if (controller) {
|
|
109
|
+
this.ongoingSearches.delete(searchId);
|
|
110
|
+
|
|
111
|
+
controller.abort();
|
|
112
|
+
this.client?.onDone(searchId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Disposes the service by aborting all ongoing searches.
|
|
120
|
+
*/
|
|
121
|
+
dispose(): void {
|
|
122
|
+
this.ongoingSearches.forEach(controller => controller.abort());
|
|
123
|
+
this.ongoingSearches.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Internal method to perform the search.
|
|
128
|
+
* @param searchId - The ID of the search to perform.
|
|
129
|
+
* @returns A promise that resolves when the search is complete.
|
|
130
|
+
*/
|
|
131
|
+
private async doSearch(searchId: number): Promise<void> {
|
|
132
|
+
const ctx = this.ongoingSearches.get(searchId);
|
|
133
|
+
|
|
134
|
+
if (!ctx) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { regex, searchPaths, options, isAborted } = ctx;
|
|
139
|
+
|
|
140
|
+
const maxFileSize = options.maxFileSize ? BinarySize.parseSize(options.maxFileSize) : 20 * BinarySize.MB;
|
|
141
|
+
const matcher = createIgnoreMatcher();
|
|
142
|
+
|
|
143
|
+
let remaining = options.maxResults ?? Number.POSITIVE_INFINITY;
|
|
144
|
+
|
|
145
|
+
for (const root of searchPaths) {
|
|
146
|
+
if (isAborted()) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pathsStack: URI[] = [root];
|
|
151
|
+
let index = 0;
|
|
152
|
+
|
|
153
|
+
while (index < pathsStack.length && !isAborted() && remaining > 0) {
|
|
154
|
+
const current = pathsStack[index++];
|
|
155
|
+
const relPath = current.path.toString().replace(/^\/|^\.\//, '');
|
|
156
|
+
|
|
157
|
+
// Skip excluded paths
|
|
158
|
+
if (this.shouldExcludePath(current, options.exclude)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Skip ignored files unless explicitly included
|
|
163
|
+
if (!options.includeIgnored && relPath && matcher.ignores(relPath)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let stat: FileStatWithMetadata;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
stat = await this.fs.resolve(current, { resolveMetadata: true });
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Skip files not matching include patterns
|
|
176
|
+
if (stat.isFile && !this.shouldIncludePath(current, options.include)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Skip files exceeding size limit
|
|
181
|
+
if (stat.isFile && stat.size > maxFileSize) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Process directory contents
|
|
186
|
+
if (stat.isDirectory) {
|
|
187
|
+
if (Array.isArray(stat.children)) {
|
|
188
|
+
// Load ignore patterns from files
|
|
189
|
+
if (!options.includeIgnored) {
|
|
190
|
+
const patterns = await getIgnorePatterns(
|
|
191
|
+
current,
|
|
192
|
+
uri => this.fs.read(uri).then(content => content.value)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
matcher.add(patterns);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const child of stat.children) {
|
|
199
|
+
pathsStack.push(child.resource);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const matches = await this.searchFileByLines(current, regex, isAborted, {
|
|
208
|
+
autoGuessEncoding: true,
|
|
209
|
+
acceptTextOnly: true
|
|
210
|
+
}, remaining);
|
|
211
|
+
|
|
212
|
+
if (matches.length > 0) {
|
|
213
|
+
const result: SearchInWorkspaceResult = {
|
|
214
|
+
root: root.path.toString(),
|
|
215
|
+
fileUri: current.path.toString(),
|
|
216
|
+
matches
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
this.client?.onResult(searchId, result);
|
|
220
|
+
|
|
221
|
+
remaining -= matches.length;
|
|
222
|
+
if (remaining <= 0) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (err instanceof TextFileOperationError && err.textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.logger.error(`Error reading file ${current.path.toString()}: ${err.message}`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (remaining <= 0 || isAborted()) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.client?.onDone(searchId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Searches for matches within a file by processing it line by line.
|
|
246
|
+
* @param uri - The file URI to search
|
|
247
|
+
* @param re - The regex pattern to match
|
|
248
|
+
* @param isAborted - Function to check if search was aborted
|
|
249
|
+
* @param opts - File reading options
|
|
250
|
+
* @param limit - Maximum number of matches to return
|
|
251
|
+
* @returns Array of search matches found in the file
|
|
252
|
+
*/
|
|
253
|
+
private async searchFileByLines(
|
|
254
|
+
uri: URI,
|
|
255
|
+
re: RegExp,
|
|
256
|
+
isAborted: () => boolean,
|
|
257
|
+
opts: { autoGuessEncoding: boolean; acceptTextOnly: boolean },
|
|
258
|
+
limit: number
|
|
259
|
+
): Promise<SearchMatch[]> {
|
|
260
|
+
const { value: stream } = await this.fs.readStream(uri, opts);
|
|
261
|
+
|
|
262
|
+
let leftover = '';
|
|
263
|
+
let lineNo = 0;
|
|
264
|
+
const matches: SearchMatch[] = [];
|
|
265
|
+
|
|
266
|
+
await new Promise<void>((resolve, reject) => {
|
|
267
|
+
stream.on('data', chunk => {
|
|
268
|
+
if (isAborted()) {
|
|
269
|
+
stream.pause();
|
|
270
|
+
resolve();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const data = leftover + chunk;
|
|
275
|
+
const lines = data.split(/\r?\n/);
|
|
276
|
+
leftover = lines.pop() ?? '';
|
|
277
|
+
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
lineNo += 1; // 1-based
|
|
280
|
+
|
|
281
|
+
if (!line) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Reset regex lastIndex for global patterns
|
|
286
|
+
if (re.global) {
|
|
287
|
+
re.lastIndex = 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let m: RegExpExecArray | null;
|
|
291
|
+
|
|
292
|
+
while ((m = re.exec(line))) {
|
|
293
|
+
matches.push({
|
|
294
|
+
line: lineNo,
|
|
295
|
+
character: m.index + 1, // 1-based
|
|
296
|
+
length: m[0].length,
|
|
297
|
+
lineText: line
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (matches.length >= limit) {
|
|
301
|
+
resolve();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
stream.on('error', err => reject(err));
|
|
309
|
+
|
|
310
|
+
stream.on('end', () => {
|
|
311
|
+
if (leftover.length && matches.length < limit) {
|
|
312
|
+
lineNo += 1;
|
|
313
|
+
const line = leftover;
|
|
314
|
+
|
|
315
|
+
// Reset regex lastIndex for global patterns
|
|
316
|
+
if (re.global) {
|
|
317
|
+
re.lastIndex = 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let m: RegExpExecArray | null;
|
|
321
|
+
|
|
322
|
+
while ((m = re.exec(line))) {
|
|
323
|
+
matches.push({
|
|
324
|
+
line: lineNo,
|
|
325
|
+
character: m.index + 1,
|
|
326
|
+
length: m[0].length,
|
|
327
|
+
lineText: line
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (matches.length >= limit) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
resolve();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return matches;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Processes search options and returns clean paths and processed options.
|
|
345
|
+
* This method consolidates the path processing logic and matchWholeWord handling for better readability.
|
|
346
|
+
*/
|
|
347
|
+
private async processSearchOptions(_searchTerm: string, _searchPaths: string[], _options: SearchInWorkspaceOptions): Promise<{
|
|
348
|
+
regex: RegExp,
|
|
349
|
+
searchPaths: URI[],
|
|
350
|
+
options: SearchInWorkspaceOptions,
|
|
351
|
+
}> {
|
|
352
|
+
const options = { ..._options };
|
|
353
|
+
|
|
354
|
+
options.maxResults = typeof options.maxResults === 'number' && options.maxResults > 0 ? options.maxResults : Number.POSITIVE_INFINITY;
|
|
355
|
+
options.include = (options.include ?? []).map(glob => normalizeGlob(glob));
|
|
356
|
+
options.exclude = (options.exclude ?? []).map(glob => normalizeGlob(glob));
|
|
357
|
+
|
|
358
|
+
// If there are absolute paths in `include` we will remove them and use
|
|
359
|
+
// those as paths to search from
|
|
360
|
+
const paths = await this.extractSearchPathsFromIncludes(
|
|
361
|
+
_searchPaths.map(p => FileUri.fsPath(p)),
|
|
362
|
+
options.include
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Build regex with consideration of useRegExp/matchCase/matchWholeWord
|
|
366
|
+
const useRegExp = !!options.useRegExp;
|
|
367
|
+
const matchCase = !!options.matchCase;
|
|
368
|
+
const matchWholeWord = !!options.matchWholeWord;
|
|
369
|
+
|
|
370
|
+
const flags = 'g' + (matchCase ? '' : 'i') + 'u';
|
|
371
|
+
let source = useRegExp ? _searchTerm : escapeRegExpCharacters(_searchTerm);
|
|
372
|
+
|
|
373
|
+
// Unicode word boundaries: letters/numbers/underscore
|
|
374
|
+
if (matchWholeWord) {
|
|
375
|
+
const wbL = '(?<![\\p{L}\\p{N}_])';
|
|
376
|
+
const wbR = '(?![\\p{L}\\p{N}_])';
|
|
377
|
+
source = `${wbL}${source}${wbR}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const regex = new RegExp(source, flags);
|
|
381
|
+
|
|
382
|
+
const searchPaths = paths.map(p => URI.fromFilePath(p));
|
|
383
|
+
|
|
384
|
+
return { regex, searchPaths, options };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Checks if a path should be excluded based on exclude patterns.
|
|
389
|
+
* @param uri - The URI to check
|
|
390
|
+
* @param exclude - Array of exclude patterns
|
|
391
|
+
* @returns True if the path should be excluded
|
|
392
|
+
*/
|
|
393
|
+
protected shouldExcludePath(uri: URI, exclude: string[] | undefined): boolean {
|
|
394
|
+
if (!exclude?.length) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return matchesPattern(uri.path.toString(), exclude, minimatchOpts);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Checks if a path should be included based on include patterns.
|
|
403
|
+
* @param uri - The URI to check
|
|
404
|
+
* @param include - Array of include patterns
|
|
405
|
+
* @returns True if the path should be included
|
|
406
|
+
*/
|
|
407
|
+
private shouldIncludePath(uri: URI, include: string[] | undefined): boolean {
|
|
408
|
+
if (!include?.length) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return matchesPattern(uri.path.toString(), include, minimatchOpts);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The default search paths are set to be the root paths associated to a workspace
|
|
417
|
+
* however the search scope can be further refined with the include paths available in the search options.
|
|
418
|
+
* This method will replace the searching paths to the ones specified in the 'include' options but as long
|
|
419
|
+
* as the 'include' paths can be successfully validated as existing.
|
|
420
|
+
*
|
|
421
|
+
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
|
|
422
|
+
* derived from the include options which can be used to perform the search.
|
|
423
|
+
*
|
|
424
|
+
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
|
|
425
|
+
* provided as an equivalent search path instead.
|
|
426
|
+
*/
|
|
427
|
+
protected async extractSearchPathsFromIncludes(searchPaths: string[], include: string[]): Promise<string[]> {
|
|
428
|
+
if (!include) {
|
|
429
|
+
return searchPaths;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const resolvedPaths = new Set<string>();
|
|
433
|
+
const searchPathsUris = searchPaths.map(p => new URI(p));
|
|
434
|
+
|
|
435
|
+
for (const pattern of include) {
|
|
436
|
+
const [base, _] = getGlobBase(pattern);
|
|
437
|
+
const baseUri = new URI(base);
|
|
438
|
+
|
|
439
|
+
for (const rootUri of searchPathsUris) {
|
|
440
|
+
if (rootUri.isEqualOrParent(baseUri) && await this.fs.exists(baseUri)) {
|
|
441
|
+
resolvedPaths.add(baseUri.path.toString());
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return resolvedPaths.size ? Array.from(resolvedPaths) : searchPaths;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get the base + rest of a glob pattern.
|
|
452
|
+
*
|
|
453
|
+
* @param pattern - The glob pattern to get the base of (like 'workspace2/foo/*.md')
|
|
454
|
+
* @returns The base + rest of the glob pattern. (like ['workspace2/foo/', '*.md'])
|
|
455
|
+
*/
|
|
456
|
+
function getGlobBase(pattern: string): [string, string] {
|
|
457
|
+
const isAbsolute = pattern.startsWith('/');
|
|
458
|
+
const parts = pattern.replace(/^\//, '').split('/');
|
|
459
|
+
const magic = /[*?[\]{}]/;
|
|
460
|
+
|
|
461
|
+
const staticParts: string[] = [];
|
|
462
|
+
|
|
463
|
+
for (const part of parts) {
|
|
464
|
+
if (magic.test(part)) { break; }
|
|
465
|
+
staticParts.push(part);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const base = (isAbsolute ? '/' : '') + staticParts.join('/');
|
|
469
|
+
|
|
470
|
+
return [base, pattern.substring(base.length)];
|
|
471
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Maksim Kachurin 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 { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
|
+
import { SearchInWorkspaceServer } from '../common/search-in-workspace-interface';
|
|
19
|
+
import { BrowserSearchInWorkspaceServer } from './browser-search-in-workspace-server';
|
|
20
|
+
import { SearchInWorkspaceService } from '../browser/search-in-workspace-service';
|
|
21
|
+
import { BrowserOnlySearchInWorkspaceService } from './browser-only-search-in-workspace-service';
|
|
22
|
+
|
|
23
|
+
export default new ContainerModule((bind, _unbind, isBound, rebind) => {
|
|
24
|
+
if (isBound(SearchInWorkspaceServer)) {
|
|
25
|
+
rebind(SearchInWorkspaceServer).to(BrowserSearchInWorkspaceServer).inSingletonScope();
|
|
26
|
+
} else {
|
|
27
|
+
bind(SearchInWorkspaceServer).to(BrowserSearchInWorkspaceServer).inSingletonScope();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isBound(SearchInWorkspaceService)) {
|
|
31
|
+
rebind(SearchInWorkspaceService).to(BrowserOnlySearchInWorkspaceService).inSingletonScope();
|
|
32
|
+
} else {
|
|
33
|
+
bind(SearchInWorkspaceService).to(BrowserOnlySearchInWorkspaceService).inSingletonScope();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { nls } from '@theia/core/lib/common/nls';
|
|
18
|
-
import {
|
|
18
|
+
import { PreferenceProxy, PreferenceScope, PreferenceService, createPreferenceProxy } from '@theia/core/lib/common/preferences';
|
|
19
19
|
import { interfaces } from '@theia/core/shared/inversify';
|
|
20
|
+
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
|
20
21
|
|
|
21
22
|
export const searchInWorkspacePreferencesSchema: PreferenceSchema = {
|
|
22
|
-
|
|
23
|
+
scope: PreferenceScope.Folder,
|
|
23
24
|
properties: {
|
|
24
25
|
'search.lineNumbers': {
|
|
25
26
|
description: nls.localizeByDefault('Controls whether to show line numbers for search results.'),
|
|
@@ -19,6 +19,7 @@ import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common'
|
|
|
19
19
|
import { SearchInWorkspaceServer, SearchInWorkspaceClient, SIW_WS_PATH } from '../common/search-in-workspace-interface';
|
|
20
20
|
import { RipgrepSearchInWorkspaceServer, RgPath } from './ripgrep-search-in-workspace-server';
|
|
21
21
|
import { rgPath } from '@vscode/ripgrep';
|
|
22
|
+
import { bindSearchInWorkspacePreferences } from '../common/search-in-workspace-preferences';
|
|
22
23
|
|
|
23
24
|
export default new ContainerModule(bind => {
|
|
24
25
|
bind(SearchInWorkspaceServer).to(RipgrepSearchInWorkspaceServer);
|
|
@@ -30,4 +31,5 @@ export default new ContainerModule(bind => {
|
|
|
30
31
|
return server;
|
|
31
32
|
}));
|
|
32
33
|
bind(RgPath).toConstantValue(rgPath);
|
|
34
|
+
bindSearchInWorkspacePreferences(bind);
|
|
33
35
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-in-workspace-preferences.d.ts","sourceRoot":"","sources":["../../src/browser/search-in-workspace-preferences.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAiD,MAAM,qCAAqC,CAAC;AAC1J,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE1D,eAAO,MAAM,kCAAkC,EAAE,gBA+ChD,CAAC;AAEF,qBAAa,8BAA8B;IACvC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,mCAAmC,EAAE,MAAM,CAAC;IAC5C,mCAAmC,EAAE,OAAO,CAAC;IAC7C,kBAAkB,EAAE,OAAO,CAAC;IAC5B,uBAAuB,EAAE,OAAO,CAAC;CACpC;AAED,eAAO,MAAM,uCAAuC,eAAoD,CAAC;AACzG,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AACnF,MAAM,MAAM,4BAA4B,GAAG,eAAe,CAAC,8BAA8B,CAAC,CAAC;AAE3F,wBAAgB,kCAAkC,CAAC,WAAW,EAAE,iBAAiB,EAAE,MAAM,GAAE,gBAAqD,GAAG,4BAA4B,CAE9K;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,GAAG,IAAI,CAQ5E"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-in-workspace-preferences.js","sourceRoot":"","sources":["../../src/browser/search-in-workspace-preferences.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,0CAA0C;AAC1C,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;AAEhF,oDAAiD;AACjD,qEAA0J;AAG7I,QAAA,kCAAkC,GAAqB;IAChE,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACR,oBAAoB,EAAE;YAClB,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,2DAA2D,CAAC;YAC/F,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,SAAS;SAClB;QACD,wBAAwB,EAAE;YACtB,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,oEAAoE,CAAC;YACxG,OAAO,EAAE,MAAM;YACf,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,cAAc,CAAC;SACnD;QACD,iCAAiC,EAAE;YAC/B,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,2FAA2F,CAAC;YAC/H,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,SAAS;SAClB;QACD,qBAAqB,EAAE;YACnB,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,+BAA+B,CAAC;YACnE,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,SAAS;SAClB;QACD,mCAAmC,EAAE;YACjC,mCAAmC;YACnC,mBAAmB,EAAE,SAAG,CAAC,iBAAiB,CAAC,wJAAwJ,EAAE,yBAAyB,CAAC;YAC/N,OAAO,EAAE,GAAG;YACZ,IAAI,EAAE,QAAQ;SACjB;QACD,mCAAmC,EAAE;YACjC,WAAW,EAAE,SAAG,CAAC,QAAQ,CAAC,sDAAsD,EAAE,yCAAyC,CAAC;YAC5H,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,SAAS;SAClB;QACD,kBAAkB,EAAE;YAChB,mCAAmC;YACnC,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,gGAAgG,CAAC;YACpI,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,SAAS;SAClB;QACD,uBAAuB,EAAE;YACrB,WAAW,EAAE,SAAG,CAAC,iBAAiB,CAAC,sDAAsD,CAAC;YAC1F,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,SAAS;SAClB;KACJ;CACJ,CAAC;AAEF,MAAa,8BAA8B;CAQ1C;AARD,wEAQC;AAEY,QAAA,uCAAuC,GAAG,MAAM,CAAC,yCAAyC,CAAC,CAAC;AAC5F,QAAA,4BAA4B,GAAG,MAAM,CAAC,8BAA8B,CAAC,CAAC;AAGnF,SAAgB,kCAAkC,CAAC,WAA8B,EAAE,SAA2B,0CAAkC;IAC5I,OAAO,IAAA,mCAAqB,EAAC,WAAW,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC;AAFD,gFAEC;AAED,SAAgB,gCAAgC,CAAC,IAAqB;IAClE,IAAI,CAAC,oCAA4B,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE;QACpD,MAAM,WAAW,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAoB,+BAAiB,CAAC,CAAC;QAC5E,MAAM,YAAY,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAyB,+CAAuC,CAAC,CAAC;QACxG,OAAO,kCAAkC,CAAC,WAAW,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC;IACtB,IAAI,CAAC,+CAAuC,CAAC,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,0CAAkC,EAAE,CAAC,CAAC;IAC9G,IAAI,CAAC,oCAAsB,CAAC,CAAC,SAAS,CAAC,+CAAuC,CAAC,CAAC;AACpF,CAAC;AARD,4EAQC"}
|