src/vs/workbench/services/search/common/search.ts

880 LOC · 770 covered · 110 uncovered · 98 ranges · 798 concepts · 31 introducers · 437 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.

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 > /*--------------------------------------------------------------------------------------------- search.ts ×34
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 { mapArrayOrNot } from '../../../../base/common/arrays.js';
7 > import { CancellationToken } from '../../../../base/common/cancellation.js';
8 > import * as glob from '../../../../base/common/glob.js';
9 > import { IDisposable } from '../../../../base/common/lifecycle.js';
10 > import * as objects from '../../../../base/common/objects.js';
11 > import * as extpath from '../../../../base/common/extpath.js';
12 > import { fuzzyContains, getNLines } from '../../../../base/common/strings.js';
13 > import { URI, UriComponents } from '../../../../base/common/uri.js';
14 > import { IFilesConfiguration } from '../../../../platform/files/common/files.js';
15 > import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
16 > import { ITelemetryData } from '../../../../platform/telemetry/common/telemetry.js';
17 > import { Event } from '../../../../base/common/event.js';
18 > import * as paths from '../../../../base/common/path.js';
19 > import { isCancellationError } from '../../../../base/common/errors.js';
20 > import { AISearchKeyword, GlobPattern, TextSearchCompleteMessageType } from './searchExtTypes.js';
21 > import { isThenable } from '../../../../base/common/async.js';
22 > import { ResourceSet } from '../../../../base/common/map.js';
23 >
24 > export { TextSearchCompleteMessageType };
25 >
26 > export const VIEWLET_ID = 'workbench.view.search';
27 > export const PANEL_ID = 'workbench.panel.search';
28 > export const VIEW_ID = 'workbench.view.search';
29 > export const SEARCH_RESULT_LANGUAGE_ID = 'search-result';
30 >
31 > export const SEARCH_EXCLUDE_CONFIG = 'search.exclude';
32 > export const DEFAULT_MAX_SEARCH_RESULTS = 20000;
33 >
34 > // Warning: this pattern is used in the search editor to detect offsets. If you
35 > // change this, also change the search-result built-in extension
36 > const SEARCH_ELIDED_PREFIX = '⟪ ';
37 > const SEARCH_ELIDED_SUFFIX = ' characters skipped ⟫';
38 > const SEARCH_ELIDED_MIN_LEN = (SEARCH_ELIDED_PREFIX.length + SEARCH_ELIDED_SUFFIX.length + 5) * 2;
39 >
40 > export const ISearchService = createDecorator<ISearchService>('searchService');
41 >
42 > /**
43 > * A service that enables to search for files or with in files.
44 > */
45 > export interface ISearchService {
46 > readonly _serviceBrand: undefined;
47 > textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete>;
48 > aiTextSearch(query: IAITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete>;
49 > getAIName(): Promise<string | undefined>;
50 > textSearchSplitSyncAsync(query: ITextQuery, token?: CancellationToken | undefined, onProgress?: ((result: ISearchProgressItem) => void) | undefined, notebookFilesToIgnore?: ResourceSet, asyncNotebookFilesToIgnore?: Promise<ResourceSet>): { syncResults: ISearchComplete; asyncResults: Promise<ISearchComplete> };
51 > fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
52 > schemeHasFileSearchProvider(scheme: string): boolean;
53 > clearCache(cacheKey: string): Promise<void>;
54 > registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable;
55 > }
56 >
57 > /**
58 > * TODO@roblou - split text from file search entirely, or share code in a more natural way.
59 > */
60 > export const enum SearchProviderType {
61 > file,
62 > text,
63 > aiText
64 > }
65 >
66 > export interface ISearchResultProvider {
67 > getAIName(): Promise<string | undefined>;
68 > textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete>;
69 > fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
70 > clearCache(cacheKey: string): Promise<void>;
71 > }
72 >
73 >
74 > export interface ExcludeGlobPattern<U extends UriComponents = URI> {
75 > folder?: U;
76 > pattern: glob.IExpression;
77 > }
78 >
79 > export interface IFolderQuery<U extends UriComponents = URI> {
80 > folder: U;
81 > folderName?: string;
82 > excludePattern?: ExcludeGlobPattern<U>[];
83 > includePattern?: glob.IExpression;
84 > ignoreGlobCase?: boolean;
85 > fileEncoding?: string;
86 > disregardIgnoreFiles?: boolean;
87 > disregardGlobalIgnoreFiles?: boolean;
88 > disregardParentIgnoreFiles?: boolean;
89 > ignoreSymlinks?: boolean;
90 > }
91 >
92 > export interface ICommonQueryProps<U extends UriComponents> {
93 > /** For telemetry - indicates what is triggering the source */
94 > _reason?: string;
95 >
96 > folderQueries: IFolderQuery<U>[];
97 > // The include pattern for files that gets passed into ripgrep.
98 > // Note that this will override any ignore files if applicable.
99 > includePattern?: glob.IExpression;
100 > excludePattern?: glob.IExpression;
101 > ignoreGlobCase?: boolean;
102 > extraFileResources?: U[];
103 >
104 > onlyOpenEditors?: boolean;
105 >
106 > maxResults?: number;
107 > usingSearchPaths?: boolean;
108 > onlyFileScheme?: boolean;
109 > }
110 >
111 > export interface IFileQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
112 > type: QueryType.File;
113 > filePattern?: string;
114 >
115 > // when walking through the tree to find the result, don't use the filePattern to fuzzy match.
116 > // Instead, should use glob matching.
117 > shouldGlobMatchFilePattern?: boolean;
118 >
119 > /**
120 > * If true no results will be returned. Instead `limitHit` will indicate if at least one result exists or not.
121 > * Currently does not work with queries including a 'siblings clause'.
122 > */
123 > exists?: boolean;
124 > sortByScore?: boolean;
125 > cacheKey?: string;
126 > }
127 >
128 > export interface ITextQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
129 > type: QueryType.Text;
130 > contentPattern: IPatternInfo;
131 >
132 > previewOptions?: ITextSearchPreviewOptions;
133 > maxFileSize?: number;
134 > surroundingContext?: number;
135 >
136 > userDisabledExcludesAndIgnoreFiles?: boolean;
137 > }
138 >
139 > export interface IAITextQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
140 > type: QueryType.aiText;
141 > contentPattern: string;
142 >
143 > previewOptions?: ITextSearchPreviewOptions;
144 > maxFileSize?: number;
145 > surroundingContext?: number;
146 >
147 > userDisabledExcludesAndIgnoreFiles?: boolean;
148 > }
149 >
150 > export type IFileQuery = IFileQueryProps<URI>;
151 > export type IRawFileQuery = IFileQueryProps<UriComponents>;
152 > export type ITextQuery = ITextQueryProps<URI>;
153 > export type IRawTextQuery = ITextQueryProps<UriComponents>;
154 > export type IAITextQuery = IAITextQueryProps<URI>;
155 > export type IRawAITextQuery = IAITextQueryProps<UriComponents>;
156 >
157 > export type IRawQuery = IRawTextQuery | IRawFileQuery | IRawAITextQuery;
158 > export type ISearchQuery = ITextQuery | IFileQuery | IAITextQuery;
159 > export type ITextSearchQuery = ITextQuery | IAITextQuery;
160 >
161 > export const enum QueryType {
162 > File = 1,
163 > Text = 2,
164 > aiText = 3
165 > }
166 >
167 > /* __GDPR__FRAGMENT__
168 > "IPatternInfo" : {
169 > "isRegExp": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
170 > "isWordMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
171 > "wordSeparators": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
172 > "isMultiline": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
173 > "isCaseSensitive": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
174 > "isSmartCase": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
175 > }
176 > */
177 > export interface IPatternInfo {
178 > pattern: string;
179 > isRegExp?: boolean;
180 > isWordMatch?: boolean;
181 > wordSeparators?: string;
182 > isMultiline?: boolean;
183 > isUnicode?: boolean;
184 > isCaseSensitive?: boolean;
185 > notebookInfo?: INotebookPatternInfo;
186 > }
187 >
188 > export interface INotebookPatternInfo {
189 > isInNotebookMarkdownInput?: boolean;
190 > isInNotebookMarkdownPreview?: boolean;
191 > isInNotebookCellInput?: boolean;
192 > isInNotebookCellOutput?: boolean;
193 > }
194 >
195 > export interface IFileMatch<U extends UriComponents = URI> {
196 > resource: U;
197 > results?: ITextSearchResult<U>[];
198 > }
199 >
200 > export type IRawFileMatch2 = IFileMatch<UriComponents>;
201 >
202 > export interface ITextSearchPreviewOptions {
203 > matchLines: number;
204 > charsPerLine: number;
205 > }
206 >
207 > export interface ISearchRange {
208 > readonly startLineNumber: number;
209 > readonly startColumn: number;
210 > readonly endLineNumber: number;
211 > readonly endColumn: number;
212 > }
213 >
214 > export interface ITextSearchMatch<U extends UriComponents = URI> {
215 > uri?: U;
216 > rangeLocations: SearchRangeSetPairing[];
217 > previewText: string;
218 > webviewIndex?: number;
219 > cellFragment?: string;
220 > }
221 >
222 > export interface ITextSearchContext<U extends UriComponents = URI> {
223 > uri?: U;
224 > text: string;
225 > lineNumber: number;
226 > }
227 >
228 > export type ITextSearchResult<U extends UriComponents = URI> = ITextSearchMatch<U> | ITextSearchContext<U>;
229 >
230 > export function resultIsMatch(result: ITextSearchResult): result is ITextSearchMatch {
231 > return !!(<ITextSearchMatch>result).rangeLocations && !!(<ITextSearchMatch>result).previewText; search.ts ×1
232 > }
234 > export interface IProgressMessage {
235 > message: string;
236 > }
237 >
238 > export type ISearchProgressItem = IFileMatch | IProgressMessage | AISearchKeyword;
239 >
240 > export function isFileMatch(p: ISearchProgressItem): p is IFileMatch {
241 return !!(<IFileMatch>p).resource;
242 }
244 > export function isAIKeyword(p: ISearchProgressItem): p is AISearchKeyword {
245 return !!(<AISearchKeyword>p).keyword;
246 }
248 > export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProgressItem): p is IProgressMessage {
249 return !!(p as IProgressMessage).message;
250 }
252 > export interface ITextSearchCompleteMessage {
253 > text: string;
254 > type: TextSearchCompleteMessageType;
255 > trusted?: boolean;
256 > }
257 >
258 > export interface ISearchCompleteStats {
259 > limitHit?: boolean;
260 > messages: ITextSearchCompleteMessage[];
261 > stats?: IFileSearchStats | ITextSearchStats;
262 > }
263 >
264 > export interface ISearchComplete extends ISearchCompleteStats {
265 > results: IFileMatch[];
266 > exit?: SearchCompletionExitCode;
267 > aiKeywords?: AISearchKeyword[];
268 > }
269 >
270 > export const enum SearchCompletionExitCode {
271 > Normal,
272 > NewSearchStarted
273 > }
274 >
275 > export interface ITextSearchStats {
276 > type: 'textSearchProvider' | 'searchProcess' | 'aiTextSearchProvider';
277 > }
278 >
279 > export interface IFileSearchStats {
280 > fromCache: boolean;
281 > detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats;
282 >
283 > resultCount: number;
284 > type: 'fileSearchProvider' | 'searchProcess';
285 > sortingTime?: number;
286 > }
287 >
288 > export interface ICachedSearchStats {
289 > cacheWasResolved: boolean;
290 > cacheLookupTime: number;
291 > cacheFilterTime: number;
292 > cacheEntryCount: number;
293 > }
294 >
295 > export interface ISearchEngineStats {
296 > fileWalkTime: number;
297 > directoriesWalked: number;
298 > filesWalked: number;
299 > cmdTime: number;
300 > cmdResultCount?: number;
301 > }
302 >
303 > export interface IFileSearchProviderStats {
304 > providerTime: number;
305 > postProcessTime: number;
306 > }
307 >
308 > export class FileMatch implements IFileMatch {
309 > results: ITextSearchResult[] = [];
310 > constructor(public resource: URI) {
311 // empty
312 }
313 > } search.ts ×34
314 >
315 > export interface SearchRangeSetPairing {
316 > source: ISearchRange;
317 > preview: ISearchRange;
318 > }
319 >
320 > export class TextSearchMatch implements ITextSearchMatch {
321 > rangeLocations: SearchRangeSetPairing[] = [];
322 > previewText: string;
323 > webviewIndex?: number;
324 >
325 > constructor(text: string, ranges: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions, webviewIndex?: number) {
326 > this.webviewIndex = webviewIndex; search.ts ×4
327 >
328 > // Trim preview if this is one match and a single-line match with a preview requested.
329 > // Otherwise send the full text, like for replace or for showing multiple previews.
330 > // TODO this is fishy.
331 > const rangesArr = Array.isArray(ranges) ? ranges : [ranges];
332 >
333 > if (previewOptions && previewOptions.matchLines === 1 && isSingleLineRangeList(rangesArr)) {
334 > // 1 line preview requested search.ts ×3
335 > text = getNLines(text, previewOptions.matchLines);
336 >
337 > let result = '';
338 > let shift = 0;
339 > let lastEnd = 0;
340 > const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
341 > for (const range of rangesArr) {
342 > const previewStart = Math.max(range.startColumn - leadingChars, 0);
343 > const previewEnd = range.startColumn + previewOptions.charsPerLine;
344 > if (previewStart > lastEnd + leadingChars + SEARCH_ELIDED_MIN_LEN) {
345 > const elision = SEARCH_ELIDED_PREFIX + (previewStart - lastEnd) + SEARCH_ELIDED_SUFFIX; search.ts ×1
346 > result += elision + text.slice(previewStart, previewEnd);
347 > shift += previewStart - (lastEnd + elision.length);
348 > } else { search.ts ×3
349 > result += text.slice(lastEnd, previewEnd);
350 > }
351 >
352 > lastEnd = previewEnd;
353 > this.rangeLocations.push({
354 > source: range,
355 > preview: new OneLineRange(0, range.startColumn - shift, range.endColumn - shift)
356 > });
357 >
358 > }
359 >
360 > this.previewText = result;
361 > } else { search.ts ×4
362 > const firstMatchLine = Array.isArray(ranges) ? ranges[0].startLineNumber : ranges.startLineNumber; search.ts ×1
363 >
364 > const rangeLocs = mapArrayOrNot(ranges, r => ({
365 > preview: new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn),
366 > source: r
367 > }));
368 >
369 > this.rangeLocations = Array.isArray(rangeLocs) ? rangeLocs : [rangeLocs];
370 > this.previewText = text;
371 > }
372 > } search.ts ×4
373 > } search.ts ×34
374 >
375 > function isSingleLineRangeList(ranges: ISearchRange[]): boolean { search.ts ×2
376 > const line = ranges[0].startLineNumber;
377 > for (const r of ranges) {
378 > if (r.startLineNumber !== line || r.endLineNumber !== line) {
379 > return false; search.ts ×1
380 > }
381 > } search.ts ×2
383 > return true;
384 > }
386 > export class SearchRange implements ISearchRange {
387 > startLineNumber: number;
388 > startColumn: number;
389 > endLineNumber: number;
390 > endColumn: number;
391 >
392 > constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
393 > this.startLineNumber = startLineNumber; search.ts ×4
394 > this.startColumn = startColumn;
395 > this.endLineNumber = endLineNumber;
396 > this.endColumn = endColumn;
397 > }
398 > } search.ts ×34
399 >
400 > export class OneLineRange extends SearchRange {
401 > constructor(lineNumber: number, startColumn: number, endColumn: number) {
402 > super(lineNumber, startColumn, lineNumber, endColumn); search.ts ×1
403 > }
404 > } search.ts ×34
405 >
406 > export const enum ViewMode {
407 > List = 'list',
408 > Tree = 'tree'
409 > }
410 >
411 > export const enum SearchSortOrder {
412 > Default = 'default',
413 > FileNames = 'fileNames',
414 > Type = 'type',
415 > Modified = 'modified',
416 > CountDescending = 'countDescending',
417 > CountAscending = 'countAscending'
418 > }
419 >
420 > export const enum SemanticSearchBehavior {
421 > Auto = 'auto',
422 > Manual = 'manual',
423 > RunOnEmpty = 'runOnEmpty',
424 > }
425 >
426 > export interface ISearchConfigurationProperties {
427 > exclude: glob.IExpression;
428 > /**
429 > * Use ignore file for file search.
430 > */
431 > useIgnoreFiles: boolean;
432 > useGlobalIgnoreFiles: boolean;
433 > useParentIgnoreFiles: boolean;
434 > followSymlinks: boolean;
435 > smartCase: boolean;
436 > globalFindClipboard: boolean;
437 > useReplacePreview: boolean;
438 > showLineNumbers: boolean;
439 > actionsPosition: 'auto' | 'right';
440 > maxResults: number | null;
441 > collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
442 > searchOnType: boolean;
443 > seedOnFocus: boolean;
444 > seedWithNearestWord: boolean;
445 > searchOnTypeDebouncePeriod: number;
446 > mode: 'view' | 'reuseEditor' | 'newEditor';
447 > searchEditor: {
448 > doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide';
449 > singleClickBehaviour: 'default' | 'peekDefinition';
450 > reusePriorSearchConfiguration: boolean;
451 > defaultNumberOfContextLines: number | null;
452 > focusResultsOnSearch: boolean;
453 > experimental: {};
454 > };
455 > sortOrder: SearchSortOrder;
456 > decorations: {
457 > colors: boolean;
458 > badges: boolean;
459 > };
460 > quickAccess: {
461 > preserveInput: boolean;
462 > };
463 > defaultViewMode: ViewMode;
464 > experimental: {
465 > closedNotebookRichContentResults: boolean;
466 > };
467 > searchView: {
468 > semanticSearchBehavior: string;
469 > keywordSuggestions: boolean;
470 > };
471 > }
472 >
473 > export interface ISearchConfiguration extends IFilesConfiguration {
474 > search?: ISearchConfigurationProperties;
475 > editor: {
476 > wordSeparators: string;
477 > };
478 > }
479 >
480 > export function getExcludes(configuration: ISearchConfiguration, includeSearchExcludes = true): glob.IExpression | undefined {
481 > const fileExcludes = configuration && configuration.files && configuration.files.exclude; promptFilesLocator.ts ×2
482 > const searchExcludes = includeSearchExcludes && configuration && configuration.search && configuration.search.exclude;
483 >
484 > if (!fileExcludes && !searchExcludes) {
485 > return undefined;
486 > }
487
488 > if (!fileExcludes || !searchExcludes) { promptFilesLocator.ts ×2
489 return fileExcludes || searchExcludes || undefined;
490 }
491
492 let allExcludes: glob.IExpression = Object.create(null);
493 // clone the config as it could be frozen
494 allExcludes = objects.mixin(allExcludes, objects.deepClone(fileExcludes));
495 allExcludes = objects.mixin(allExcludes, objects.deepClone(searchExcludes), true);
496
497 return allExcludes;
498 }
500 > export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string): boolean {
501 const globOptions = queryProps.ignoreGlobCase ? { ignoreCase: true } : undefined;
502 if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath, globOptions)) {
503 return false;
504 }
505
506 if (queryProps.includePattern || queryProps.usingSearchPaths) {
507 if (queryProps.includePattern && glob.match(queryProps.includePattern, fsPath, globOptions)) {
508 return true;
509 }
510
511 // If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present
512 if (queryProps.usingSearchPaths) {
513 return !!queryProps.folderQueries && queryProps.folderQueries.some(fq => {
514 const searchPath = fq.folder.fsPath;
515 if (extpath.isEqualOrParent(fsPath, searchPath, queryProps.ignoreGlobCase)) {
516 const relPath = paths.relative(searchPath, fsPath);
517 return !fq.includePattern || !!glob.match(fq.includePattern, relPath, globOptions);
518 } else {
519 return false;
520 }
521 });
522 }
523
524 return false;
525 }
526
527 return true;
528 }
530 > export enum SearchErrorCode {
531 > unknownEncoding = 1,
532 > regexParseError,
533 > globParseError,
534 > invalidLiteral,
535 > rgProcessError,
536 > other,
537 > canceled
538 > }
539 >
540 > export class SearchError extends Error {
541 > constructor(message: string, readonly code?: SearchErrorCode) {
542 super(message);
543 }
544 > } search.ts ×34
545 >
546 > export function deserializeSearchError(error: Error): SearchError {
547 const errorMsg = error.message;
548
549 if (isCancellationError(error)) {
550 return new SearchError(errorMsg, SearchErrorCode.canceled);
551 }
552
553 try {
554 const details = JSON.parse(errorMsg);
555 return new SearchError(details.message, details.code);
556 } catch (e) {
557 return new SearchError(errorMsg, SearchErrorCode.other);
558 }
559 }
561 > export function serializeSearchError(searchError: SearchError): Error {
562 const details = { message: searchError.message, code: searchError.code };
563 return new Error(JSON.stringify(details));
564 }
565 > export interface ITelemetryEvent { search.ts ×34
566 > eventName: string;
567 > data: ITelemetryData;
568 > }
569 >
570 > export interface IRawSearchService {
571 > fileSearch(search: IRawFileQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
572 > textSearch(search: IRawTextQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
573 > clearCache(cacheKey: string): Promise<void>;
574 > }
575 >
576 > export interface IRawFileMatch {
577 > base?: string;
578 > /**
579 > * The path of the file relative to the containing `base` folder.
580 > * This path is exactly as it appears on the filesystem.
581 > */
582 > relativePath: string;
583 > /**
584 > * This path is transformed for search purposes. For example, this could be
585 > * the `relativePath` with the workspace folder name prepended. This way the
586 > * search algorithm would also match against the name of the containing folder.
587 > *
588 > * If not given, the search algorithm should use `relativePath`.
589 > */
590 > searchPath: string | undefined;
591 > }
592 >
593 > export interface ISearchEngine<T> {
594 > search: (onResult: (matches: T) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void) => void;
595 > cancel: () => void;
596 > }
597 >
598 > export interface ISerializedSearchSuccess {
599 > type: 'success';
600 > limitHit: boolean;
601 > messages: ITextSearchCompleteMessage[];
602 > stats?: IFileSearchStats | ITextSearchStats;
603 > }
604 >
605 > export interface ISearchEngineSuccess {
606 > limitHit: boolean;
607 > messages: ITextSearchCompleteMessage[];
608 > stats: ISearchEngineStats;
609 > }
610 >
611 > export interface ISerializedSearchError {
612 > type: 'error';
613 > error: {
614 > message: string;
615 > stack: string;
616 > };
617 > }
618 >
619 > export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError;
620 >
621 > export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete {
622 // eslint-disable-next-line local/code-no-any-casts
623 if ((arg as any).type === 'error') {
624 return true;
625 // eslint-disable-next-line local/code-no-any-casts
626 } else if ((arg as any).type === 'success') {
627 return true;
628 } else {
629 return false;
630 }
631 }
633 > export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess {
634 return arg.type === 'success';
635 }
637 > export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg is ISerializedFileMatch {
638 return !!(<ISerializedFileMatch>arg).path;
639 }
641 > const filePatternIgnoreCaseOptions = { ignoreCase: true };
642 >
643 > export function isFilePatternMatch(candidate: IRawFileMatch, filePatternToUse: string, fuzzy = true, ignoreCase?: boolean): boolean {
644 > const pathToMatch = candidate.searchPath ? candidate.searchPath : candidate.relativePath; search.ts ×2
645 > return fuzzy ?
646 fuzzyContains(pathToMatch, filePatternToUse) :
647 > glob.match(filePatternToUse, pathToMatch, ignoreCase ? filePatternIgnoreCaseOptions : undefined); search.ts ×2
648 > }
650 > export interface ISerializedFileMatch {
651 > path: string;
652 > results?: ITextSearchResult[];
653 > numMatches?: number;
654 > }
655 >
656 > // Type of the possible values for progress calls from the engine
657 > export type ISerializedSearchProgressItem = ISerializedFileMatch | ISerializedFileMatch[] | IProgressMessage;
658 > export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgressMessage;
659 >
660 >
661 > export class SerializableFileMatch implements ISerializedFileMatch {
662 > path: string;
663 > results: ITextSearchMatch[];
664 >
665 > constructor(path: string) {
666 this.path = path;
667 this.results = [];
668 }
670 > addMatch(match: ITextSearchMatch): void {
671 this.results.push(match);
672 }
674 > serialize(): ISerializedFileMatch {
675 return {
676 path: this.path,
677 results: this.results,
678 numMatches: this.results.length
679 };
680 }
681 > } search.ts ×34
682 >
683 > /**
684 > * Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
685 > */
686 > export function resolvePatternsForProvider(globalPattern: glob.IExpression | undefined, folderPattern: glob.IExpression | undefined): string[] {
687 > const merged = { search.ts ×9
688 > ...(globalPattern || {}),
689 > ...(folderPattern || {})
690 > };
691 >
692 > return Object.keys(merged)
693 > .filter(key => {
694 > const value = merged[key]; search.ts ×1
695 > return typeof value === 'boolean' && value;
696 > }); search.ts ×9
697 > }
699 > export class QueryGlobTester {
700 >
701 > private _excludeExpression: glob.IExpression[]; // TODO: evaluate globs based on baseURI of pattern
702 > private _parsedExcludeExpression: glob.ParsedExpression[];
703 >
704 > private _parsedIncludeExpression: glob.ParsedExpression | null = null;
705 >
706 > constructor(config: ISearchQuery, folderQuery: IFolderQuery) {
707 > const globOptions = config.ignoreGlobCase || folderQuery.ignoreGlobCase ? { ignoreCase: true } : undefined; search.ts ×9
708 >
709 > // todo: try to incorporate folderQuery.excludePattern.folder if available
710 > this._excludeExpression = folderQuery.excludePattern?.map(excludePattern => {
711 > return { search.ts ×1
712 > ...(config.excludePattern || {}),
713 > ...(excludePattern.pattern || {})
714 > } satisfies glob.IExpression;
715 > }) ?? []; search.ts ×9
716 >
717 > if (this._excludeExpression.length === 0) {
718 > // even if there are no folderQueries, we want to observe the global excludes search.ts ×1
719 > this._excludeExpression = [config.excludePattern || {}];
720 > }
722 > this._parsedExcludeExpression = this._excludeExpression.map(e => glob.parse(e, globOptions));
723 >
724 > // Empty includeExpression means include nothing, so no {} shortcuts
725 > let includeExpression: glob.IExpression | undefined = config.includePattern;
726 > if (folderQuery.includePattern) {
727 > if (includeExpression) { search.ts ×2
728 > includeExpression = {
729 > ...includeExpression,
730 > ...folderQuery.includePattern
731 > };
732 > } else {
733 includeExpression = folderQuery.includePattern;
734 }
735 > } search.ts ×2
737 > if (includeExpression) {
738 > this._parsedIncludeExpression = glob.parse(includeExpression, globOptions); search.ts ×1
739 > }
740 > } search.ts ×9
742 > private _evalParsedExcludeExpression(testPath: string, basename: string | undefined, hasSibling?: (name: string) => boolean): string | null {
743 > // todo: less hacky way of evaluating sync vs async sibling clauses fileSearchManager.ts ×9
744 > let result: string | null = null;
745 >
746 > for (const folderExclude of this._parsedExcludeExpression) {
747 >
748 > // find first non-null result
749 > const evaluation = folderExclude(testPath, basename, hasSibling);
750 >
751 > if (typeof evaluation === 'string') {
752 > result = evaluation; search.ts ×3
753 > break;
754 > }
756 > return result;
757 > }
759 >
760 > matchesExcludesSync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean {
761 > if (this._parsedExcludeExpression && this._evalParsedExcludeExpression(testPath, basename, hasSibling)) { fileSearchManager.ts ×9
762 > return true; search.ts ×3
763 > }
765 > return false;
766 > }
768 > /**
769 > * Guaranteed sync - siblingsFn should not return a promise.
770 > */
771 > includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean {
772 if (this._parsedExcludeExpression && this._evalParsedExcludeExpression(testPath, basename, hasSibling)) {
773 return false;
774 }
775
776 if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) {
777 return false;
778 }
779
780 return true;
781 }
783 > /**
784 > * Evaluating the exclude expression is only async if it includes sibling clauses. As an optimization, avoid doing anything with Promises
785 > * unless the expression is async.
786 > */
787 > includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): Promise<boolean> | boolean {
789 > const isIncluded = () => {
790 > return this._parsedIncludeExpression ?
791 > !!(this._parsedIncludeExpression(testPath, basename, hasSibling)) : search.ts ×1
792 > true; search.ts ×1
794 >
795 > return Promise.all(this._parsedExcludeExpression.map(e => {
796 > const excluded = e(testPath, basename, hasSibling);
797 > if (isThenable(excluded)) {
798 > return excluded.then(excluded => { search.ts ×3
799 > if (excluded) {
800 > return false;
801 > }
802 > glob.ts ×1
803 > return isIncluded();
804 > }); search.ts ×3
805 > }
807 > return isIncluded();
808 >
809 > })).then(e => e.some(e => !!e));
810 >
811 >
812 > }
814 > hasSiblingExcludeClauses(): boolean {
815 > return this._excludeExpression.reduce((prev, curr) => hasSiblingClauses(curr) || prev, false); fileSearchManager.ts ×5
816 > }
817 > } search.ts ×34
818 >
819 > function hasSiblingClauses(pattern: glob.IExpression): boolean { fileSearchManager.ts ×5
820 > for (const key in pattern) {
821 > if (typeof pattern[key] !== 'boolean') { search.ts ×2
822 > return true; fileSearchManager.ts ×9
823 > }
824 > } search.ts ×2
826 > return false;
827 > }
829 > export function hasSiblingPromiseFn(siblingsFn?: () => Promise<string[]>) {
830 > if (!siblingsFn) { search.ts ×3
831 return undefined;
832 }
834 > let siblings: Promise<Record<string, true>>;
835 > return (name: string) => {
836 > if (!siblings) { search.ts ×3
837 > siblings = (siblingsFn() || Promise.resolve([]))
838 > .then(list => list ? listToMap(list) : {});
839 > }
840 > return siblings.then(map => !!map[name]);
841 > };
842 > } search.ts ×3
844 > export function hasSiblingFn(siblingsFn?: () => string[]) {
845 > if (!siblingsFn) { fileSearchManager.ts ×3
846 return undefined;
847 }
849 > let siblings: Record<string, true>;
850 > return (name: string) => {
851 > if (!siblings) { search.ts ×3
852 > const list = siblingsFn();
853 > siblings = list ? listToMap(list) : {};
854 > }
855 > return !!siblings[name];
856 > };
859 > function listToMap(list: string[]) { search.ts ×1
860 > const map: Record<string, true> = {};
861 > for (const key of list) {
862 > map[key] = true;
863 > }
864 > return map;
865 > }
867 > export function excludeToGlobPattern(excludesForFolder: { baseUri?: URI | undefined; patterns: string[] }[]): GlobPattern[] {
868 > return excludesForFolder.flatMap(exclude => exclude.patterns.map(pattern => { search.ts ×9
869 > return exclude.baseUri ? search.ts ×2
870 {
871 baseUri: exclude.baseUri,
872 pattern: pattern
873 > } : pattern; search.ts ×2
874 > })); search.ts ×9
875 > }
877 > export const DEFAULT_TEXT_SEARCH_PREVIEW_OPTIONS = {
878 > matchLines: 100,
879 > charsPerLine: 10000
880 > };