searchModel.ts 40.6 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { RunOnceScheduler } from 'vs/base/common/async';
7
import { CancellationTokenSource } from 'vs/base/common/cancellation';
8
import * as errors from 'vs/base/common/errors';
J
Joao Moreno 已提交
9
import { Emitter, Event } from 'vs/base/common/event';
10
import { getBaseLabel } from 'vs/base/common/labels';
11
import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
12
import { ResourceMap, TernarySearchTree } from 'vs/base/common/map';
13
import { lcut } from 'vs/base/common/strings';
14
import { URI } from 'vs/base/common/uri';
15
import { Range } from 'vs/editor/common/core/range';
16
import { FindMatch, IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model';
17
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
18
import { IModelService } from 'vs/editor/common/services/modelService';
19
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
B
Benjamin Pasero 已提交
20
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
21
import { ReplacePattern } from 'vs/workbench/services/search/common/replace';
J
Jackson Kearl 已提交
22
import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange, ITextSearchContext, ITextSearchResult, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search';
23
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
24
import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry';
25
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
26
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
27
import { editorMatchesToTextSearchResults, addContextToEditorMatches } from 'vs/workbench/services/search/common/searchHelpers';
28
import { withNullAsUndefined } from 'vs/base/common/types';
29
import { memoize } from 'vs/base/common/decorators';
30
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
31 32
import { compareFileNames, compareFileExtensions, comparePaths } from 'vs/base/common/comparers';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
E
Erich Gamma 已提交
33 34 35

export class Match {

R
Rob Lourens 已提交
36 37
	private static readonly MAX_PREVIEW_CHARS = 250;

E
Erich Gamma 已提交
38
	private _id: string;
39
	private _range: Range;
R
Rob Lourens 已提交
40 41
	private _oneLinePreviewText: string;
	private _rangeInPreviewText: ISearchRange;
E
Erich Gamma 已提交
42

R
Rob Lourens 已提交
43 44 45 46 47 48 49 50 51
	// For replace
	private _fullPreviewRange: ISearchRange;

	constructor(private _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) {
		this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber];
		const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ?
			_fullPreviewRange.endColumn :
			this._oneLinePreviewText.length;
		this._rangeInPreviewText = new OneLineRange(1, _fullPreviewRange.startColumn + 1, adjustedEndCol + 1);
52

53
		this._range = new Range(
R
Rob Lourens 已提交
54 55 56 57 58 59
			_documentRange.startLineNumber + 1,
			_documentRange.startColumn + 1,
			_documentRange.endLineNumber + 1,
			_documentRange.endColumn + 1);

		this._fullPreviewRange = _fullPreviewRange;
60 61

		this._id = this._parent.id() + '>' + this._range + this.getMatchString();
E
Erich Gamma 已提交
62 63
	}

R
Rob Lourens 已提交
64
	id(): string {
E
Erich Gamma 已提交
65 66 67
		return this._id;
	}

R
Rob Lourens 已提交
68
	parent(): FileMatch {
E
Erich Gamma 已提交
69 70 71
		return this._parent;
	}

R
Rob Lourens 已提交
72
	text(): string {
R
Rob Lourens 已提交
73
		return this._oneLinePreviewText;
E
Erich Gamma 已提交
74 75
	}

R
Rob Lourens 已提交
76
	range(): Range {
E
Erich Gamma 已提交
77 78 79
		return this._range;
	}

80
	@memoize
R
Rob Lourens 已提交
81
	preview(): { before: string; inside: string; after: string; } {
R
Rob Lourens 已提交
82
		let before = this._oneLinePreviewText.substring(0, this._rangeInPreviewText.startColumn - 1),
S
Sandeep Somavarapu 已提交
83
			inside = this.getMatchString(),
R
Rob Lourens 已提交
84
			after = this._oneLinePreviewText.substring(this._rangeInPreviewText.endColumn - 1);
E
Erich Gamma 已提交
85

86
		before = lcut(before, 26);
87
		before = before.trimLeft();
88

R
Rob Lourens 已提交
89 90 91 92 93
		let charsRemaining = Match.MAX_PREVIEW_CHARS - before.length;
		inside = inside.substr(0, charsRemaining);
		charsRemaining -= inside.length;
		after = after.substr(0, charsRemaining);

E
Erich Gamma 已提交
94 95 96 97 98 99
		return {
			before,
			inside,
			after,
		};
	}
S
Sandeep Somavarapu 已提交
100

R
Rob Lourens 已提交
101
	get replaceString(): string {
R
Rob Lourens 已提交
102
		const searchModel = this.parent().parent().searchModel;
R
Rob Lourens 已提交
103 104 105
		if (!searchModel.replacePattern) {
			throw new Error('searchModel.replacePattern must be set before accessing replaceString');
		}
R
Rob Lourens 已提交
106

107
		const fullMatchText = this.fullMatchText();
108
		let replaceString = searchModel.replacePattern.getReplaceString(fullMatchText, searchModel.preserveCase);
S
Sandeep Somavarapu 已提交
109

S
Sandeep Somavarapu 已提交
110 111
		// If match string is not matching then regex pattern has a lookahead expression
		if (replaceString === null) {
R
Rob Lourens 已提交
112 113
			const fullMatchTextWithSurroundingContent = this.fullMatchText(true);
			replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithSurroundingContent, searchModel.preserveCase);
R
Rob Lourens 已提交
114 115 116

			// Search/find normalize line endings - check whether \r prevents regex from matching
			if (replaceString === null) {
R
Rob Lourens 已提交
117
				const fullMatchTextWithoutCR = fullMatchTextWithSurroundingContent.replace(/\r\n/g, '\n');
118
				replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithoutCR, searchModel.preserveCase);
R
Rob Lourens 已提交
119
			}
S
Sandeep Somavarapu 已提交
120
		}
S
Sandeep Somavarapu 已提交
121 122 123 124 125 126

		// Match string is still not matching. Could be unsupported matches (multi-line).
		if (replaceString === null) {
			replaceString = searchModel.replacePattern.pattern;
		}

S
Sandeep Somavarapu 已提交
127
		return replaceString;
S
Sandeep Somavarapu 已提交
128 129
	}

R
Rob Lourens 已提交
130
	fullMatchText(includeSurrounding = false): string {
R
Rob Lourens 已提交
131
		let thisMatchPreviewLines: string[];
R
Rob Lourens 已提交
132 133
		if (includeSurrounding) {
			thisMatchPreviewLines = this._fullPreviewLines;
R
Rob Lourens 已提交
134 135 136
		} else {
			thisMatchPreviewLines = this._fullPreviewLines.slice(this._fullPreviewRange.startLineNumber, this._fullPreviewRange.endLineNumber + 1);
			thisMatchPreviewLines[thisMatchPreviewLines.length - 1] = thisMatchPreviewLines[thisMatchPreviewLines.length - 1].slice(0, this._fullPreviewRange.endColumn);
R
Rob Lourens 已提交
137
			thisMatchPreviewLines[0] = thisMatchPreviewLines[0].slice(this._fullPreviewRange.startColumn);
R
Rob Lourens 已提交
138 139 140 141 142
		}

		return thisMatchPreviewLines.join('\n');
	}

J
Jackson Kearl 已提交
143 144 145 146 147 148 149 150 151
	rangeInPreview() {
		// convert to editor's base 1 positions.
		return {
			...this._fullPreviewRange,
			startColumn: this._fullPreviewRange.startColumn + 1,
			endColumn: this._fullPreviewRange.endColumn + 1
		};
	}

152 153 154 155
	fullPreviewLines(): string[] {
		return this._fullPreviewLines.slice(this._fullPreviewRange.startLineNumber, this._fullPreviewRange.endLineNumber + 1);
	}

R
Rob Lourens 已提交
156
	getMatchString(): string {
R
Rob Lourens 已提交
157
		return this._oneLinePreviewText.substring(this._rangeInPreviewText.startColumn - 1, this._rangeInPreviewText.endColumn - 1);
S
Sandeep Somavarapu 已提交
158
	}
E
Erich Gamma 已提交
159 160
}

161
export class FileMatch extends Disposable implements IFileMatch {
162

163
	private static readonly _CURRENT_FIND_MATCH = ModelDecorationOptions.register({
164
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
165
		zIndex: 13,
166 167
		className: 'currentFindMatch',
		overviewRuler: {
168
			color: themeColorFromId(overviewRulerFindMatchForeground),
169
			position: OverviewRulerLane.Center
170 171 172 173
		},
		minimap: {
			color: themeColorFromId(minimapFindMatch),
			position: MinimapPosition.Inline
174 175 176
		}
	});

177
	private static readonly _FIND_MATCH = ModelDecorationOptions.register({
178 179 180
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'findMatch',
		overviewRuler: {
181
			color: themeColorFromId(overviewRulerFindMatchForeground),
182
			position: OverviewRulerLane.Center
183 184 185 186
		},
		minimap: {
			color: themeColorFromId(minimapFindMatch),
			position: MinimapPosition.Inline
187 188 189 190 191
		}
	});

	private static getDecorationOption(selected: boolean): ModelDecorationOptions {
		return (selected ? FileMatch._CURRENT_FIND_MATCH : FileMatch._FIND_MATCH);
S
Sandeep Somavarapu 已提交
192
	}
193

194 195
	private _onChange = this._register(new Emitter<{ didRemove?: boolean; forceUpdateModel?: boolean }>());
	readonly onChange: Event<{ didRemove?: boolean; forceUpdateModel?: boolean }> = this._onChange.event;
196

S
Sandeep Somavarapu 已提交
197
	private _onDispose = this._register(new Emitter<void>());
R
Rob Lourens 已提交
198
	readonly onDispose: Event<void> = this._onDispose.event;
E
Erich Gamma 已提交
199 200

	private _resource: URI;
201
	private _fileStat?: IFileStatWithMetadata;
202 203
	private _model: ITextModel | null = null;
	private _modelListener: IDisposable | null = null;
204
	private _matches: Map<string, Match>;
S
Sandeep Somavarapu 已提交
205
	private _removedMatches: Set<string>;
206
	private _selectedMatch: Match | null = null;
207 208 209

	private _updateScheduler: RunOnceScheduler;
	private _modelDecorations: string[] = [];
E
Erich Gamma 已提交
210

211 212 213 214 215
	private _context: Map<number, string> = new Map();
	public get context(): Map<number, string> {
		return new Map(this._context);
	}

R
Rob Lourens 已提交
216
	constructor(private _query: IPatternInfo, private _previewOptions: ITextSearchPreviewOptions | undefined, private _maxResults: number | undefined, private _parent: FolderMatch, private rawMatch: IFileMatch,
217
		@IModelService private readonly modelService: IModelService, @IReplaceService private readonly replaceService: IReplaceService
R
Rob Lourens 已提交
218
	) {
219
		super();
220
		this._resource = this.rawMatch.resource;
221
		this._matches = new Map<string, Match>();
S
Sandeep Somavarapu 已提交
222
		this._removedMatches = new Set<string>();
S
Sandeep Somavarapu 已提交
223
		this._updateScheduler = new RunOnceScheduler(this.updateMatchesForModel.bind(this), 250);
224 225

		this.createMatches();
E
Erich Gamma 已提交
226 227
	}

228
	private createMatches(): void {
229
		const model = this.modelService.getModel(this._resource);
230 231
		if (model) {
			this.bindModel(model);
S
Sandeep Somavarapu 已提交
232
			this.updateMatchesForModel();
233
		} else {
R
Rob Lourens 已提交
234
			this.rawMatch.results!
235 236 237 238 239
				.filter(resultIsMatch)
				.forEach(rawMatch => {
					textSearchResultToMatches(rawMatch, this)
						.forEach(m => this.add(m));
				});
240 241

			this.addContext(this.rawMatch.results);
242 243 244
		}
	}

R
Rob Lourens 已提交
245
	bindModel(model: ITextModel): void {
S
Sandeep Somavarapu 已提交
246
		this._model = model;
A
Alex Dima 已提交
247
		this._modelListener = this._model.onDidChangeContent(() => {
248 249 250 251 252 253 254 255
			this._updateScheduler.schedule();
		});
		this._model.onWillDispose(() => this.onModelWillDispose());
		this.updateHighlights();
	}

	private onModelWillDispose(): void {
		// Update matches because model might have some dirty changes
S
Sandeep Somavarapu 已提交
256
		this.updateMatchesForModel();
257 258 259 260 261 262 263
		this.unbindModel();
	}

	private unbindModel(): void {
		if (this._model) {
			this._updateScheduler.cancel();
			this._model.deltaDecorations(this._modelDecorations, []);
S
Sandeep Somavarapu 已提交
264
			this._model = null;
265
			this._modelListener!.dispose();
266 267 268
		}
	}

S
Sandeep Somavarapu 已提交
269
	private updateMatchesForModel(): void {
270 271 272 273 274
		// this is called from a timeout and might fire
		// after the model has been disposed
		if (!this._model) {
			return;
		}
275
		this._matches = new Map<string, Match>();
R
Rob Lourens 已提交
276 277

		const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null;
278
		const matches = this._model
R
Rob Lourens 已提交
279
			.findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults);
280

S
Sandeep Somavarapu 已提交
281
		this.updateMatches(matches, true);
S
Sandeep Somavarapu 已提交
282 283
	}

S
Sandeep Somavarapu 已提交
284
	private updatesMatchesForLineAfterReplace(lineNumber: number, modelChange: boolean): void {
R
Rob Lourens 已提交
285 286 287 288
		if (!this._model) {
			return;
		}

S
Sandeep Somavarapu 已提交
289 290 291 292 293 294
		const range = {
			startLineNumber: lineNumber,
			startColumn: this._model.getLineMinColumn(lineNumber),
			endLineNumber: lineNumber,
			endColumn: this._model.getLineMaxColumn(lineNumber)
		};
295
		const oldMatches = Array.from(this._matches.values()).filter(match => match.range().startLineNumber === lineNumber);
S
Sandeep Somavarapu 已提交
296 297
		oldMatches.forEach(match => this._matches.delete(match.id()));

R
Rob Lourens 已提交
298 299
		const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null;
		const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults);
S
Sandeep Somavarapu 已提交
300
		this.updateMatches(matches, modelChange);
S
Sandeep Somavarapu 已提交
301 302
	}

R
Rob Lourens 已提交
303 304 305 306 307
	private updateMatches(matches: FindMatch[], modelChange: boolean): void {
		if (!this._model) {
			return;
		}

308 309
		const textSearchResults = editorMatchesToTextSearchResults(matches, this._model, this._previewOptions);
		textSearchResults.forEach(textSearchResult => {
R
Rob Lourens 已提交
310 311 312 313 314 315
			textSearchResultToMatches(textSearchResult, this).forEach(match => {
				if (!this._removedMatches.has(match.id())) {
					this.add(match);
					if (this.isMatchSelected(match)) {
						this._selectedMatch = match;
					}
S
Sandeep Somavarapu 已提交
316
				}
R
Rob Lourens 已提交
317
			});
318 319
		});

320 321 322 323 324
		this.addContext(
			addContextToEditorMatches(textSearchResults, this._model, this.parent().parent().query!)
				.filter((result => !resultIsMatch(result)) as ((a: any) => a is ITextSearchContext))
				.map(context => ({ ...context, lineNumber: context.lineNumber + 1 })));

325
		this._onChange.fire({ forceUpdateModel: modelChange });
326 327 328
		this.updateHighlights();
	}

R
Rob Lourens 已提交
329
	updateHighlights(): void {
330 331 332 333 334
		if (!this._model) {
			return;
		}

		if (this.parent().showHighlights) {
S
Sandeep Somavarapu 已提交
335
			this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, this.matches().map(match => <IModelDeltaDecoration>{
336
				range: match.range(),
S
Sandeep Somavarapu 已提交
337
				options: FileMatch.getDecorationOption(this.isMatchSelected(match))
338 339 340 341
			}));
		} else {
			this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, []);
		}
E
Erich Gamma 已提交
342 343
	}

R
Rob Lourens 已提交
344
	id(): string {
345
		return this.resource.toString();
E
Erich Gamma 已提交
346 347
	}

348
	parent(): FolderMatch {
E
Erich Gamma 已提交
349 350 351
		return this._parent;
	}

R
Rob Lourens 已提交
352
	matches(): Match[] {
353
		return Array.from(this._matches.values());
E
Erich Gamma 已提交
354 355
	}

356
	remove(match: Match): void {
S
Sandeep Somavarapu 已提交
357
		this.removeMatch(match);
S
Sandeep Somavarapu 已提交
358
		this._removedMatches.add(match.id());
359
		this._onChange.fire({ didRemove: true });
E
Erich Gamma 已提交
360 361
	}

362 363 364 365 366 367
	private replaceQ = Promise.resolve();
	async replace(toReplace: Match): Promise<void> {
		return this.replaceQ = this.replaceQ.finally(async () => {
			await this.replaceService.replace(toReplace);
			this.updatesMatchesForLineAfterReplace(toReplace.range().startLineNumber, false);
		});
E
Erich Gamma 已提交
368 369
	}

R
Rob Lourens 已提交
370
	setSelectedMatch(match: Match | null): void {
S
Sandeep Somavarapu 已提交
371 372 373 374 375 376 377 378
		if (match) {
			if (!this._matches.has(match.id())) {
				return;
			}
			if (this.isMatchSelected(match)) {
				return;
			}
		}
R
Rob Lourens 已提交
379

S
Sandeep Somavarapu 已提交
380 381 382 383
		this._selectedMatch = match;
		this.updateHighlights();
	}

R
Rob Lourens 已提交
384
	getSelectedMatch(): Match | null {
S
Sandeep Somavarapu 已提交
385 386 387
		return this._selectedMatch;
	}

R
Rob Lourens 已提交
388
	isMatchSelected(match: Match): boolean {
R
Rob Lourens 已提交
389
		return !!this._selectedMatch && this._selectedMatch.id() === match.id();
S
Sandeep Somavarapu 已提交
390 391
	}

R
Rob Lourens 已提交
392
	count(): number {
S
Sandeep Somavarapu 已提交
393
		return this.matches().length;
E
Erich Gamma 已提交
394 395
	}

396
	get resource(): URI {
E
Erich Gamma 已提交
397 398 399
		return this._resource;
	}

R
Rob Lourens 已提交
400
	name(): string {
401
		return getBaseLabel(this.resource);
E
Erich Gamma 已提交
402 403
	}

404 405 406 407 408 409 410 411
	addContext(results: ITextSearchResult[] | undefined) {
		if (!results) { return; }

		results
			.filter((result => !resultIsMatch(result)) as ((a: any) => a is ITextSearchContext))
			.forEach(context => this._context.set(context.lineNumber, context.text));
	}

R
Rob Lourens 已提交
412
	add(match: Match, trigger?: boolean) {
413 414
		this._matches.set(match.id(), match);
		if (trigger) {
415
			this._onChange.fire({ forceUpdateModel: true });
416 417
		}
	}
S
Sandeep Somavarapu 已提交
418 419 420 421 422 423 424 425 426 427

	private removeMatch(match: Match) {
		this._matches.delete(match.id());
		if (this.isMatchSelected(match)) {
			this.setSelectedMatch(null);
		} else {
			this.updateHighlights();
		}
	}

428 429 430 431 432 433 434 435 436 437 438 439
	async resolveFileStat(fileService: IFileService): Promise<void> {
		this._fileStat = await fileService.resolve(this.resource, { resolveMetadata: true });
	}

	public get fileStat(): IFileStatWithMetadata | undefined {
		return this._fileStat;
	}

	public set fileStat(stat: IFileStatWithMetadata | undefined) {
		this._fileStat = stat;
	}

R
Rob Lourens 已提交
440
	dispose(): void {
S
Sandeep Somavarapu 已提交
441 442 443 444 445
		this.setSelectedMatch(null);
		this.unbindModel();
		this._onDispose.fire();
		super.dispose();
	}
446
}
447

448
export interface IChangeEvent {
449
	elements: FileMatch[];
450 451 452 453
	added?: boolean;
	removed?: boolean;
}

454
export class FolderMatch extends Disposable {
E
Erich Gamma 已提交
455

S
Sandeep Somavarapu 已提交
456
	private _onChange = this._register(new Emitter<IChangeEvent>());
R
Rob Lourens 已提交
457
	readonly onChange: Event<IChangeEvent> = this._onChange.event;
E
Erich Gamma 已提交
458

459
	private _onDispose = this._register(new Emitter<void>());
R
Rob Lourens 已提交
460
	readonly onDispose: Event<void> = this._onDispose.event;
461

462 463
	private _fileMatches: ResourceMap<FileMatch>;
	private _unDisposedFileMatches: ResourceMap<FileMatch>;
464
	private _replacingAll: boolean = false;
S
Sandeep Somavarapu 已提交
465

R
Rob Lourens 已提交
466
	constructor(protected _resource: URI | null, private _id: string, private _index: number, private _query: ITextQuery, private _parent: SearchResult, private _searchModel: SearchModel,
467 468
		@IReplaceService private readonly replaceService: IReplaceService,
		@IInstantiationService private readonly instantiationService: IInstantiationService
469
	) {
470
		super();
471 472
		this._fileMatches = new ResourceMap<FileMatch>();
		this._unDisposedFileMatches = new ResourceMap<FileMatch>();
S
Sandeep Somavarapu 已提交
473 474
	}

R
Rob Lourens 已提交
475
	get searchModel(): SearchModel {
476
		return this._searchModel;
E
Erich Gamma 已提交
477 478
	}

R
Rob Lourens 已提交
479
	get showHighlights(): boolean {
480 481 482
		return this._parent.showHighlights;
	}

R
Rob Lourens 已提交
483
	set replacingAll(b: boolean) {
484 485 486
		this._replacingAll = b;
	}

R
Rob Lourens 已提交
487
	id(): string {
488
		return this._id;
489 490
	}

491
	get resource(): URI | null {
492 493 494
		return this._resource;
	}

R
Rob Lourens 已提交
495
	index(): number {
496 497 498
		return this._index;
	}

R
Rob Lourens 已提交
499
	name(): string {
500
		return getBaseLabel(withNullAsUndefined(this.resource)) || '';
501 502
	}

R
Rob Lourens 已提交
503
	parent(): SearchResult {
504 505 506
		return this._parent;
	}

R
Rob Lourens 已提交
507
	bindModel(model: ITextModel): void {
508 509 510 511 512 513
		const fileMatch = this._fileMatches.get(model.uri);
		if (fileMatch) {
			fileMatch.bindModel(model);
		}
	}

R
Rob Lourens 已提交
514
	add(raw: IFileMatch[], silent: boolean): void {
515 516
		const added: FileMatch[] = [];
		const updated: FileMatch[] = [];
R
Rob Lourens 已提交
517
		raw.forEach(rawFileMatch => {
518 519
			const existingFileMatch = this._fileMatches.get(rawFileMatch.resource);
			if (existingFileMatch) {
520
				rawFileMatch
R
Rob Lourens 已提交
521
					.results!
522 523 524 525 526
					.filter(resultIsMatch)
					.forEach(m => {
						textSearchResultToMatches(m, existingFileMatch)
							.forEach(m => existingFileMatch.add(m));
					});
527
				updated.push(existingFileMatch);
528 529

				existingFileMatch.addContext(rawFileMatch.results);
530 531 532 533
			} else {
				const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch);
				this.doAdd(fileMatch);
				added.push(fileMatch);
534
				const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove));
535
				fileMatch.onDispose(() => disposable.dispose());
536 537
			}
		});
538 539 540 541

		const elements = [...added, ...updated];
		if (!silent && elements.length) {
			this._onChange.fire({ elements, added: !!added.length });
E
Erich Gamma 已提交
542 543 544
		}
	}

545
	clear(): void {
546
		const changed: FileMatch[] = this.matches();
S
Sandeep Somavarapu 已提交
547
		this.disposeMatches();
548
		this._onChange.fire({ elements: changed, removed: true });
549
	}
E
Erich Gamma 已提交
550

551 552
	remove(matches: FileMatch | FileMatch[]): void {
		this.doRemove(matches);
553 554
	}

R
Rob Lourens 已提交
555
	replace(match: FileMatch): Promise<any> {
S
Sandeep Somavarapu 已提交
556
		return this.replaceService.replace([match]).then(() => {
S
Sandeep Somavarapu 已提交
557
			this.doRemove(match);
558
		});
559
	}
560

R
Rob Lourens 已提交
561
	replaceAll(): Promise<any> {
562
		const matches = this.matches();
563
		return this.replaceService.replace(matches).then(() => this.doRemove(matches));
564 565
	}

R
Rob Lourens 已提交
566
	matches(): FileMatch[] {
567
		return [...this._fileMatches.values()];
568 569
	}

R
Rob Lourens 已提交
570
	isEmpty(): boolean {
571 572 573
		return this.fileCount() === 0;
	}

R
Rob Lourens 已提交
574
	fileCount(): number {
575 576 577
		return this._fileMatches.size;
	}

R
Rob Lourens 已提交
578
	count(): number {
579 580 581
		return this.matches().reduce<number>((prev, match) => prev + match.count(), 0);
	}

582
	private onFileChange(fileMatch: FileMatch, removed = false): void {
583
		let added = false;
584
		if (!this._fileMatches.has(fileMatch.resource)) {
585 586 587 588 589 590 591 592
			this.doAdd(fileMatch);
			added = true;
		}
		if (fileMatch.count() === 0) {
			this.doRemove(fileMatch, false, false);
			added = false;
			removed = true;
		}
593
		if (!this._replacingAll) {
594
			this._onChange.fire({ elements: [fileMatch], added: added, removed: removed });
595 596 597 598
		}
	}

	private doAdd(fileMatch: FileMatch): void {
599 600 601
		this._fileMatches.set(fileMatch.resource, fileMatch);
		if (this._unDisposedFileMatches.has(fileMatch.resource)) {
			this._unDisposedFileMatches.delete(fileMatch.resource);
602 603 604
		}
	}

605
	private doRemove(fileMatches: FileMatch | FileMatch[], dispose: boolean = true, trigger: boolean = true): void {
606 607 608 609
		if (!Array.isArray(fileMatches)) {
			fileMatches = [fileMatches];
		}

R
Rob Lourens 已提交
610
		for (const match of fileMatches as FileMatch[]) {
611 612 613 614 615 616
			this._fileMatches.delete(match.resource);
			if (dispose) {
				match.dispose();
			} else {
				this._unDisposedFileMatches.set(match.resource, match);
			}
617 618 619
		}

		if (trigger) {
620
			this._onChange.fire({ elements: fileMatches, removed: true });
621 622 623 624
		}
	}

	private disposeMatches(): void {
625 626
		[...this._fileMatches.values()].forEach((fileMatch: FileMatch) => fileMatch.dispose());
		[...this._unDisposedFileMatches.values()].forEach((fileMatch: FileMatch) => fileMatch.dispose());
627 628 629 630
		this._fileMatches.clear();
		this._unDisposedFileMatches.clear();
	}

R
Rob Lourens 已提交
631
	dispose(): void {
632 633 634 635 636 637
		this.disposeMatches();
		this._onDispose.fire();
		super.dispose();
	}
}

R
Rob Lourens 已提交
638 639 640 641
/**
 * BaseFolderMatch => optional resource ("other files" node)
 * FolderMatch => required resource (normal folder node)
 */
642
export class FolderMatchWithResource extends FolderMatch {
R
Rob Lourens 已提交
643 644 645 646 647 648 649
	constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, _searchModel: SearchModel,
		@IReplaceService replaceService: IReplaceService,
		@IInstantiationService instantiationService: IInstantiationService
	) {
		super(_resource, _id, _index, _query, _parent, _searchModel, replaceService, instantiationService);
	}

650
	get resource(): URI {
R
Rob Lourens 已提交
651 652 653 654
		return this._resource!;
	}
}

R
Rob Lourens 已提交
655 656 657 658
/**
 * Compares instances of the same match type. Different match types should not be siblings
 * and their sort order is undefined.
 */
659
export function searchMatchComparer(elementA: RenderableMatch, elementB: RenderableMatch, sortOrder: SearchSortOrder = SearchSortOrder.Default): number {
660
	if (elementA instanceof FolderMatch && elementB instanceof FolderMatch) {
R
Rob Lourens 已提交
661 662 663 664
		return elementA.index() - elementB.index();
	}

	if (elementA instanceof FileMatch && elementB instanceof FileMatch) {
665
		switch (sortOrder) {
666
			case SearchSortOrder.CountDescending:
667
				return elementB.count() - elementA.count();
668
			case SearchSortOrder.CountAscending:
669
				return elementA.count() - elementB.count();
670
			case SearchSortOrder.Type:
671
				return compareFileExtensions(elementA.name(), elementB.name());
672
			case SearchSortOrder.FileNames:
673
				return compareFileNames(elementA.name(), elementB.name());
674
			case SearchSortOrder.Modified:
675 676 677 678 679 680 681 682 683
				const fileStatA = elementA.fileStat;
				const fileStatB = elementB.fileStat;
				if (fileStatA && fileStatB) {
					return fileStatB.mtime - fileStatA.mtime;
				}
			// Fall through otherwise
			default:
				return comparePaths(elementA.resource.fsPath, elementB.resource.fsPath) || compareFileNames(elementA.name(), elementB.name());
		}
R
Rob Lourens 已提交
684 685 686 687 688 689
	}

	if (elementA instanceof Match && elementB instanceof Match) {
		return Range.compareRangesUsingStarts(elementA.range(), elementB.range());
	}

R
Rob Lourens 已提交
690
	return 0;
R
Rob Lourens 已提交
691 692
}

693 694 695
export class SearchResult extends Disposable {

	private _onChange = this._register(new Emitter<IChangeEvent>());
R
Rob Lourens 已提交
696
	readonly onChange: Event<IChangeEvent> = this._onChange.event;
697

698
	private _folderMatches: FolderMatchWithResource[] = [];
699
	private _otherFilesMatch: FolderMatch | null = null;
700
	private _folderMatchesMap: TernarySearchTree<URI, FolderMatchWithResource> = TernarySearchTree.forUris<FolderMatchWithResource>();
701 702
	private _showHighlights: boolean = false;
	private _query: ITextQuery | null = null;
703 704

	private _rangeHighlightDecorations: RangeHighlightDecorations;
J
Jackson Kearl 已提交
705
	private disposePastResults: () => void = () => { };
706

707
	private _isDirty = false;
708

709 710
	constructor(
		private _searchModel: SearchModel,
711 712 713
		@IReplaceService private readonly replaceService: IReplaceService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
714 715
		@IModelService private readonly modelService: IModelService,
	) {
716 717
		super();
		this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations);
718 719

		this._register(this.modelService.onModelAdded(model => this.onModelAdded(model)));
720 721

		this._register(this.onChange(e => {
722 723
			if (e.removed) {
				this._isDirty = !this.isEmpty();
724 725 726 727
			}
		}));
	}

728 729
	get isDirty(): boolean {
		return this._isDirty;
730 731
	}

732
	get query(): ITextQuery | null {
R
Rob Lourens 已提交
733 734 735
		return this._query;
	}

736
	set query(query: ITextQuery | null) {
J
Jackson Kearl 已提交
737 738
		// When updating the query we could change the roots, so keep a reference to them to clean up when we trigger `disposePastResults`
		const oldFolderMatches = this.folderMatches();
739
		new Promise<void>(resolve => this.disposePastResults = resolve)
J
Jackson Kearl 已提交
740
			.then(() => oldFolderMatches.forEach(match => match.clear()))
741
			.then(() => oldFolderMatches.forEach(match => match.dispose()))
742
			.then(() => this._isDirty = false);
J
Jackson Kearl 已提交
743 744

		this._rangeHighlightDecorations.removeHighlightRange();
745
		this._folderMatchesMap = TernarySearchTree.forUris<FolderMatchWithResource>();
J
Jackson Kearl 已提交
746

747 748 749 750 751
		if (!query) {
			return;
		}

		this._folderMatches = (query && query.folderQueries || [])
752
			.map(fq => fq.folder)
753
			.map((resource, index) => this.createFolderMatchWithResource(resource, resource.toString(), index, query));
R
Rob Lourens 已提交
754

755
		this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource, fm));
R
Rob Lourens 已提交
756
		this._otherFilesMatch = this.createOtherFilesFolderMatch('otherFiles', this._folderMatches.length + 1, query);
757

R
Rob Lourens 已提交
758
		this._query = query;
759 760
	}

761
	private onModelAdded(model: ITextModel): void {
762
		const folderMatch = this._folderMatchesMap.findSubstr(model.uri);
763 764 765 766 767
		if (folderMatch) {
			folderMatch.bindModel(model);
		}
	}

768 769
	private createFolderMatchWithResource(resource: URI, id: string, index: number, query: ITextQuery): FolderMatchWithResource {
		return <FolderMatchWithResource>this._createBaseFolderMatch(FolderMatchWithResource, resource, id, index, query);
R
Rob Lourens 已提交
770 771
	}

772 773
	private createOtherFilesFolderMatch(id: string, index: number, query: ITextQuery): FolderMatch {
		return this._createBaseFolderMatch(FolderMatch, null, id, index, query);
R
Rob Lourens 已提交
774 775
	}

776
	private _createBaseFolderMatch(folderMatchClass: typeof FolderMatch | typeof FolderMatchWithResource, resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch {
R
Rob Lourens 已提交
777
		const folderMatch = this.instantiationService.createInstance(folderMatchClass, resource, id, index, query, this, this._searchModel);
778 779 780
		const disposable = folderMatch.onChange((event) => this._onChange.fire(event));
		folderMatch.onDispose(() => disposable.dispose());
		return folderMatch;
781 782
	}

R
Rob Lourens 已提交
783
	get searchModel(): SearchModel {
784 785 786
		return this._searchModel;
	}

R
Rob Lourens 已提交
787
	add(allRaw: IFileMatch[], silent: boolean = false): void {
788
		// Split up raw into a list per folder so we can do a batch add per folder.
R
Rob Lourens 已提交
789

790 791
		const { byFolder, other } = this.groupFilesByFolder(allRaw);
		byFolder.forEach(raw => {
792 793 794
			if (!raw.length) {
				return;
			}
795 796

			const folderMatch = this.getFolderMatch(raw[0].resource);
R
Rob Lourens 已提交
797 798 799
			if (folderMatch) {
				folderMatch.add(raw, silent);
			}
800
		});
801

J
Jackson Kearl 已提交
802 803
		this._otherFilesMatch?.add(other, silent);
		this.disposePastResults();
804 805
	}

R
Rob Lourens 已提交
806
	clear(): void {
807
		this.folderMatches().forEach((folderMatch) => folderMatch.clear());
808
		this.disposeMatches();
809 810
		this._folderMatches = [];
		this._otherFilesMatch = null;
811 812
	}

813
	remove(matches: FileMatch | FolderMatch | (FileMatch | FolderMatch)[]): void {
814 815 816 817
		if (!Array.isArray(matches)) {
			matches = [matches];
		}

818 819
		matches.forEach(m => {
			if (m instanceof FolderMatch) {
820
				m.clear();
821 822 823
			}
		});

824
		const fileMatches: FileMatch[] = matches.filter(m => m instanceof FileMatch) as FileMatch[];
825

826
		const { byFolder, other } = this.groupFilesByFolder(fileMatches);
827 828 829 830 831
		byFolder.forEach(matches => {
			if (!matches.length) {
				return;
			}

832
			this.getFolderMatch(matches[0].resource).remove(<FileMatch[]>matches);
833 834 835
		});

		if (other.length) {
836
			this.getFolderMatch(other[0].resource).remove(<FileMatch[]>other);
837
		}
838 839
	}

R
Rob Lourens 已提交
840
	replace(match: FileMatch): Promise<any> {
841
		return this.getFolderMatch(match.resource).replace(match);
842 843
	}

B
Benjamin Pasero 已提交
844
	replaceAll(progress: IProgress<IProgressStep>): Promise<any> {
845
		this.replacingAll = true;
J
Joao Moreno 已提交
846

B
Benjamin Pasero 已提交
847
		const promise = this.replaceService.replace(this.matches(), progress);
J
Joao Moreno 已提交
848
		const onDone = Event.stopwatch(Event.fromPromise(promise));
K
kieferrm 已提交
849
		/* __GDPR__
K
kieferrm 已提交
850
			"replaceAll.started" : {
K
kieferrm 已提交
851
				"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
K
kieferrm 已提交
852 853
			}
		*/
J
Joao Moreno 已提交
854 855 856
		onDone(duration => this.telemetryService.publicLog('replaceAll.started', { duration }));

		return promise.then(() => {
857
			this.replacingAll = false;
858 859
			this.clear();
		}, () => {
860
			this.replacingAll = false;
861 862
		});
	}
E
Erich Gamma 已提交
863

864
	folderMatches(): FolderMatch[] {
865
		return this._otherFilesMatch ?
R
Rob Lourens 已提交
866 867 868 869 870 871 872
			[
				...this._folderMatches,
				this._otherFilesMatch
			] :
			[
				...this._folderMatches
			];
873 874
	}

R
Rob Lourens 已提交
875
	matches(): FileMatch[] {
876
		const matches: FileMatch[][] = [];
R
Rob Lourens 已提交
877
		this.folderMatches().forEach(folderMatch => {
878 879
			matches.push(folderMatch.matches());
		});
R
Rob Lourens 已提交
880 881

		return (<FileMatch[]>[]).concat(...matches);
E
Erich Gamma 已提交
882 883
	}

R
Rob Lourens 已提交
884
	isEmpty(): boolean {
885
		return this.folderMatches().every((folderMatch) => folderMatch.isEmpty());
886
	}
E
Erich Gamma 已提交
887

R
Rob Lourens 已提交
888
	fileCount(): number {
889
		return this.folderMatches().reduce<number>((prev, match) => prev + match.fileCount(), 0);
890
	}
E
Erich Gamma 已提交
891

R
Rob Lourens 已提交
892
	count(): number {
893 894 895
		return this.matches().reduce<number>((prev, match) => prev + match.count(), 0);
	}

R
Rob Lourens 已提交
896
	get showHighlights(): boolean {
897
		return this._showHighlights;
E
Erich Gamma 已提交
898 899
	}

R
Rob Lourens 已提交
900
	toggleHighlights(value: boolean): void {
901 902 903 904
		if (this._showHighlights === value) {
			return;
		}
		this._showHighlights = value;
905
		let selectedMatch: Match | null = null;
906 907
		this.matches().forEach((fileMatch: FileMatch) => {
			fileMatch.updateHighlights();
S
Sandeep Somavarapu 已提交
908 909 910
			if (!selectedMatch) {
				selectedMatch = fileMatch.getSelectedMatch();
			}
911
		});
S
Sandeep Somavarapu 已提交
912
		if (this._showHighlights && selectedMatch) {
R
Rob Lourens 已提交
913
			// TS?
914
			this._rangeHighlightDecorations.highlightRange(
915
				(<Match>selectedMatch).parent().resource,
R
Rob Lourens 已提交
916
				(<Match>selectedMatch).range()
917
			);
S
Sandeep Somavarapu 已提交
918
		} else {
919
			this._rangeHighlightDecorations.removeHighlightRange();
S
Sandeep Somavarapu 已提交
920 921 922
		}
	}

R
Rob Lourens 已提交
923
	get rangeHighlightDecorations(): RangeHighlightDecorations {
S
Sandeep Somavarapu 已提交
924
		return this._rangeHighlightDecorations;
E
Erich Gamma 已提交
925
	}
926

927
	private getFolderMatch(resource: URI): FolderMatch {
928
		const folderMatch = this._folderMatchesMap.findSubstr(resource);
929
		return folderMatch ? folderMatch : this._otherFilesMatch!;
S
Sandeep Somavarapu 已提交
930 931
	}

932
	private set replacingAll(running: boolean) {
933
		this.folderMatches().forEach((folderMatch) => {
934 935
			folderMatch.replacingAll = running;
		});
S
Sandeep Somavarapu 已提交
936 937
	}

938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
	private groupFilesByFolder(fileMatches: IFileMatch[]): { byFolder: ResourceMap<IFileMatch[]>, other: IFileMatch[] } {
		const rawPerFolder = new ResourceMap<IFileMatch[]>();
		const otherFileMatches: IFileMatch[] = [];
		this._folderMatches.forEach(fm => rawPerFolder.set(fm.resource, []));

		fileMatches.forEach(rawFileMatch => {
			const folderMatch = this.getFolderMatch(rawFileMatch.resource);
			if (!folderMatch) {
				// foldermatch was previously removed by user or disposed for some reason
				return;
			}

			const resource = folderMatch.resource;
			if (resource) {
				rawPerFolder.get(resource)!.push(rawFileMatch);
			} else {
				otherFileMatches.push(rawFileMatch);
			}
		});

		return {
			byFolder: rawPerFolder,
			other: otherFileMatches
		};
	}

S
Sandeep Somavarapu 已提交
964
	private disposeMatches(): void {
965
		this.folderMatches().forEach(folderMatch => folderMatch.dispose());
966
		this._folderMatches = [];
967
		this._folderMatchesMap = TernarySearchTree.forUris<FolderMatchWithResource>();
968
		this._rangeHighlightDecorations.removeHighlightRange();
S
Sandeep Somavarapu 已提交
969 970
	}

R
Rob Lourens 已提交
971
	dispose(): void {
J
Jackson Kearl 已提交
972
		this.disposePastResults();
S
Sandeep Somavarapu 已提交
973
		this.disposeMatches();
S
Sandeep Somavarapu 已提交
974
		this._rangeHighlightDecorations.dispose();
S
Sandeep Somavarapu 已提交
975 976
		super.dispose();
	}
E
Erich Gamma 已提交
977 978
}

979
export class SearchModel extends Disposable {
E
Erich Gamma 已提交
980

981
	private _searchResult: SearchResult;
982
	private _searchQuery: ITextQuery | null = null;
S
Sandeep Somavarapu 已提交
983
	private _replaceActive: boolean = false;
984 985
	private _replaceString: string | null = null;
	private _replacePattern: ReplacePattern | null = null;
986
	private _preserveCase: boolean = false;
J
Jackson Kearl 已提交
987 988
	private _startStreamDelay: Promise<void> = Promise.resolve();
	private _resultQueue: IFileMatch[] = [];
E
Erich Gamma 已提交
989

M
Matt Bierner 已提交
990
	private readonly _onReplaceTermChanged: Emitter<void> = this._register(new Emitter<void>());
R
Rob Lourens 已提交
991
	readonly onReplaceTermChanged: Event<void> = this._onReplaceTermChanged.event;
S
Sandeep Somavarapu 已提交
992

993
	private currentCancelTokenSource: CancellationTokenSource | null = null;
J
Jackson Kearl 已提交
994
	private searchCancelledForNewSearch: boolean = false;
E
Erich Gamma 已提交
995

R
Rob Lourens 已提交
996
	constructor(
997 998
		@ISearchService private readonly searchService: ISearchService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
999
		@IConfigurationService private readonly configurationService: IConfigurationService,
1000
		@IInstantiationService private readonly instantiationService: IInstantiationService
R
Rob Lourens 已提交
1001
	) {
E
Erich Gamma 已提交
1002
		super();
S
Sandeep Somavarapu 已提交
1003
		this._searchResult = this.instantiationService.createInstance(SearchResult, this);
E
Erich Gamma 已提交
1004 1005
	}

R
Rob Lourens 已提交
1006
	isReplaceActive(): boolean {
S
Sandeep Somavarapu 已提交
1007
		return this._replaceActive;
S
Sandeep Somavarapu 已提交
1008 1009
	}

R
Rob Lourens 已提交
1010
	set replaceActive(replaceActive: boolean) {
S
Sandeep Somavarapu 已提交
1011
		this._replaceActive = replaceActive;
1012 1013
	}

R
Rob Lourens 已提交
1014
	get replacePattern(): ReplacePattern | null {
S
Sandeep Somavarapu 已提交
1015
		return this._replacePattern;
1016 1017
	}

R
Rob Lourens 已提交
1018
	get replaceString(): string {
R
Rob Lourens 已提交
1019
		return this._replaceString || '';
S
Sandeep Somavarapu 已提交
1020 1021
	}

1022 1023 1024 1025 1026 1027 1028 1029
	set preserveCase(value: boolean) {
		this._preserveCase = value;
	}

	get preserveCase(): boolean {
		return this._preserveCase;
	}

R
Rob Lourens 已提交
1030
	set replaceString(replaceString: string) {
S
Sandeep Somavarapu 已提交
1031
		this._replaceString = replaceString;
S
Sandeep Somavarapu 已提交
1032
		if (this._searchQuery) {
S
Sandeep Somavarapu 已提交
1033
			this._replacePattern = new ReplacePattern(replaceString, this._searchQuery.contentPattern);
S
Sandeep Somavarapu 已提交
1034
		}
S
Sandeep Somavarapu 已提交
1035
		this._onReplaceTermChanged.fire();
1036 1037
	}

R
Rob Lourens 已提交
1038
	get searchResult(): SearchResult {
1039
		return this._searchResult;
1040 1041
	}

R
Rob Lourens 已提交
1042
	search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> {
J
Jackson Kearl 已提交
1043
		this.cancelSearch(true);
1044

1045
		this._searchQuery = query;
J
Jackson Kearl 已提交
1046 1047 1048 1049
		if (!this.searchConfig.searchOnType) {
			this.searchResult.clear();
		}

1050
		this._searchResult.query = this._searchQuery;
1051 1052

		const progressEmitter = new Emitter<void>();
R
Rob Lourens 已提交
1053
		this._replacePattern = new ReplacePattern(this.replaceString, this._searchQuery.contentPattern);
S
Sandeep Somavarapu 已提交
1054

J
Jackson Kearl 已提交
1055
		// In search on type case, delay the streaming of results just a bit, so that we don't flash the only "local results" fast path
1056
		this._startStreamDelay = new Promise(resolve => setTimeout(resolve, this.searchConfig.searchOnType ? 150 : 0));
J
Jackson Kearl 已提交
1057

1058
		const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource();
1059
		const currentRequest = this.searchService.textSearch(this._searchQuery, this.currentCancelTokenSource.token, p => {
1060 1061 1062 1063 1064 1065 1066 1067
			progressEmitter.fire();
			this.onSearchProgress(p);

			if (onProgress) {
				onProgress(p);
			}
		});

1068 1069 1070
		const dispose = () => tokenSource.dispose();
		currentRequest.then(dispose, dispose);

J
Joao Moreno 已提交
1071 1072 1073
		const onDone = Event.fromPromise(currentRequest);
		const onFirstRender = Event.any<any>(onDone, progressEmitter.event);
		const onFirstRenderStopwatch = Event.stopwatch(onFirstRender);
K
kieferrm 已提交
1074
		/* __GDPR__
K
kieferrm 已提交
1075
			"searchResultsFirstRender" : {
K
kieferrm 已提交
1076
				"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
K
kieferrm 已提交
1077 1078
			}
		*/
J
Joao Moreno 已提交
1079 1080
		onFirstRenderStopwatch(duration => this.telemetryService.publicLog('searchResultsFirstRender', { duration }));

1081
		const start = Date.now();
1082
		currentRequest.then(
1083
			value => this.onSearchCompleted(value, Date.now() - start),
1084
			e => this.onSearchError(e, Date.now() - start));
E
Erich Gamma 已提交
1085

R
Rob Lourens 已提交
1086 1087 1088 1089 1090 1091 1092 1093
		return currentRequest.finally(() => {
			/* __GDPR__
				"searchResultsFinished" : {
					"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
			this.telemetryService.publicLog('searchResultsFinished', { duration: Date.now() - start });
		});
E
Erich Gamma 已提交
1094 1095
	}

R
Rob Lourens 已提交
1096 1097 1098 1099 1100
	private onSearchCompleted(completed: ISearchComplete | null, duration: number): ISearchComplete | null {
		if (!this._searchQuery) {
			throw new Error('onSearchCompleted must be called after a search is started');
		}

J
Jackson Kearl 已提交
1101 1102 1103
		this._searchResult.add(this._resultQueue);
		this._resultQueue = [];

1104
		const options: IPatternInfo = Object.assign({}, this._searchQuery.contentPattern);
R
#96022  
Rob Lourens 已提交
1105
		delete (options as any).pattern;
1106

R
Rob Lourens 已提交
1107
		const stats = completed && completed.stats as ITextSearchStats;
1108

1109 1110 1111 1112 1113 1114
		const fileSchemeOnly = this._searchQuery.folderQueries.every(fq => fq.folder.scheme === 'file');
		const otherSchemeOnly = this._searchQuery.folderQueries.every(fq => fq.folder.scheme !== 'file');
		const scheme = fileSchemeOnly ? 'file' :
			otherSchemeOnly ? 'other' :
				'mixed';

K
kieferrm 已提交
1115
		/* __GDPR__
K
kieferrm 已提交
1116
			"searchResultsShown" : {
K
kieferrm 已提交
1117 1118
				"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
				"fileCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
K
kieferrm 已提交
1119
				"options": { "${inline}": [ "${IPatternInfo}" ] },
K
kieferrm 已提交
1120
				"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1121
				"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
1122 1123
				"scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
				"searchOnTypeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
K
kieferrm 已提交
1124 1125
			}
		*/
1126 1127 1128 1129
		this.telemetryService.publicLog('searchResultsShown', {
			count: this._searchResult.count(),
			fileCount: this._searchResult.fileCount(),
			options,
1130
			duration,
1131
			type: stats && stats.type,
1132
			scheme,
J
Jackson Kearl 已提交
1133
			searchOnTypeEnabled: this.searchConfig.searchOnType
1134
		});
1135
		return completed;
E
Erich Gamma 已提交
1136 1137
	}

1138
	private onSearchError(e: any, duration: number): void {
1139
		if (errors.isPromiseCanceledError(e)) {
J
Jackson Kearl 已提交
1140 1141 1142 1143 1144 1145
			this.onSearchCompleted(
				this.searchCancelledForNewSearch
					? { exit: SearchCompletionExitCode.NewSearchStarted, results: [] }
					: null,
				duration);
			this.searchCancelledForNewSearch = false;
1146
		}
E
Erich Gamma 已提交
1147 1148
	}

J
Jackson Kearl 已提交
1149
	private async onSearchProgress(p: ISearchProgressItem) {
R
Rob Lourens 已提交
1150
		if ((<IFileMatch>p).resource) {
J
Jackson Kearl 已提交
1151 1152 1153 1154 1155 1156
			this._resultQueue.push(<IFileMatch>p);
			await this._startStreamDelay;
			if (this._resultQueue.length) {
				this._searchResult.add(this._resultQueue, true);
				this._resultQueue = [];
			}
E
Erich Gamma 已提交
1157
		}
1158
	}
E
Erich Gamma 已提交
1159

J
Jackson Kearl 已提交
1160 1161 1162 1163
	private get searchConfig() {
		return this.configurationService.getValue<ISearchConfigurationProperties>('search');
	}

J
Jackson Kearl 已提交
1164
	cancelSearch(cancelledForNewSearch = false): boolean {
1165
		if (this.currentCancelTokenSource) {
J
Jackson Kearl 已提交
1166
			this.searchCancelledForNewSearch = cancelledForNewSearch;
1167
			this.currentCancelTokenSource.cancel();
S
Sandeep Somavarapu 已提交
1168
			return true;
E
Erich Gamma 已提交
1169
		}
S
Sandeep Somavarapu 已提交
1170
		return false;
E
Erich Gamma 已提交
1171 1172
	}

R
Rob Lourens 已提交
1173
	dispose(): void {
1174 1175
		this.cancelSearch();
		this.searchResult.dispose();
E
Erich Gamma 已提交
1176 1177
		super.dispose();
	}
1178 1179
}

S
Sandeep Somavarapu 已提交
1180 1181
export type FileMatchOrMatch = FileMatch | Match;

1182
export type RenderableMatch = FolderMatch | FolderMatchWithResource | FileMatch | Match;
1183

S
Sandeep Somavarapu 已提交
1184 1185
export class SearchWorkbenchService implements ISearchWorkbenchService {

1186
	declare readonly _serviceBrand: undefined;
1187
	private _searchModel: SearchModel | null = null;
S
Sandeep Somavarapu 已提交
1188

1189
	constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
S
Sandeep Somavarapu 已提交
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
	}

	get searchModel(): SearchModel {
		if (!this._searchModel) {
			this._searchModel = this.instantiationService.createInstance(SearchModel);
		}
		return this._searchModel;
	}
}

export const ISearchWorkbenchService = createDecorator<ISearchWorkbenchService>('searchWorkbenchService');

export interface ISearchWorkbenchService {
1203
	readonly _serviceBrand: undefined;
S
Sandeep Somavarapu 已提交
1204 1205 1206

	readonly searchModel: SearchModel;
}
1207 1208 1209 1210 1211 1212 1213

/**
 * Can add a range highlight decoration to a model.
 * It will automatically remove it when the model has its decorations changed.
 */
export class RangeHighlightDecorations implements IDisposable {

1214 1215
	private _decorationId: string | null = null;
	private _model: ITextModel | null = null;
1216
	private readonly _modelDisposables = new DisposableStore();
1217 1218 1219

	constructor(
		@IModelService private readonly _modelService: IModelService
1220 1221
	) {
	}
1222

R
Rob Lourens 已提交
1223
	removeHighlightRange() {
1224 1225 1226 1227 1228 1229
		if (this._model && this._decorationId) {
			this._model.deltaDecorations([this._decorationId], []);
		}
		this._decorationId = null;
	}

R
Rob Lourens 已提交
1230
	highlightRange(resource: URI | ITextModel, range: Range, ownerId: number = 0): void {
R
Rob Lourens 已提交
1231
		let model: ITextModel | null;
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242
		if (URI.isUri(resource)) {
			model = this._modelService.getModel(resource);
		} else {
			model = resource;
		}

		if (model) {
			this.doHighlightRange(model, range);
		}
	}

A
Alex Dima 已提交
1243
	private doHighlightRange(model: ITextModel, range: Range) {
1244 1245 1246 1247 1248
		this.removeHighlightRange();
		this._decorationId = model.deltaDecorations([], [{ range: range, options: RangeHighlightDecorations._RANGE_HIGHLIGHT_DECORATION }])[0];
		this.setModel(model);
	}

A
Alex Dima 已提交
1249
	private setModel(model: ITextModel) {
1250
		if (this._model !== model) {
1251
			this.clearModelListeners();
1252
			this._model = model;
1253 1254
			this._modelDisposables.add(this._model.onDidChangeDecorations((e) => {
				this.clearModelListeners();
1255 1256 1257
				this.removeHighlightRange();
				this._model = null;
			}));
1258 1259
			this._modelDisposables.add(this._model.onWillDispose(() => {
				this.clearModelListeners();
1260 1261 1262 1263 1264 1265
				this.removeHighlightRange();
				this._model = null;
			}));
		}
	}

1266 1267
	private clearModelListeners() {
		this._modelDisposables.clear();
1268 1269
	}

R
Rob Lourens 已提交
1270
	dispose() {
1271 1272
		if (this._model) {
			this.removeHighlightRange();
1273
			this._modelDisposables.dispose();
1274 1275 1276 1277
			this._model = null;
		}
	}

1278
	private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({
1279 1280 1281 1282 1283
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'rangeHighlight',
		isWholeLine: true
	});
}
R
Rob Lourens 已提交
1284

1285
function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] {
R
Rob Lourens 已提交
1286
	const previewLines = rawMatch.preview.text.split('\n');
1287 1288
	if (Array.isArray(rawMatch.ranges)) {
		return rawMatch.ranges.map((r, i) => {
R
Rob Lourens 已提交
1289
			const previewRange: ISearchRange = (<ISearchRange[]>rawMatch.preview.matches)[i];
R
Rob Lourens 已提交
1290
			return new Match(fileMatch, previewLines, previewRange, r);
1291 1292
		});
	} else {
1293
		const previewRange = <ISearchRange>rawMatch.preview.matches;
1294
		const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges);
1295 1296 1297
		return [match];
	}
}