searchView.ts 65.0 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 * as browser from 'vs/base/browser/browser';
7
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
8
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
9
import * as aria from 'vs/base/browser/ui/aria/aria';
10
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
R
Rob Lourens 已提交
11 12
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree';
13 14 15
import { IAction } from 'vs/base/common/actions';
import { Delayer } from 'vs/base/common/async';
import * as errors from 'vs/base/common/errors';
R
Rob Lourens 已提交
16 17
import { Emitter, Event } from 'vs/base/common/event';
import { Iterator } from 'vs/base/common/iterator';
S
Sandeep Somavarapu 已提交
18
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
19
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
20 21
import * as env from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
22
import { URI } from 'vs/base/common/uri';
23
import 'vs/css!./media/searchview';
24 25 26
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import * as nls from 'vs/nls';
R
Rob Lourens 已提交
27 28
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
J
Johannes Rieken 已提交
29
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
30
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
R
Rob Lourens 已提交
31
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
32 33
import { IConfirmation, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
34
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
R
Rob Lourens 已提交
35
import { TreeResourceNavigator2, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
36
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
J
Johannes Rieken 已提交
37
import { IProgressService } from 'vs/platform/progress/common/progress';
38
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchHistoryService, ISearchHistoryValues, ITextQuery, SearchErrorCode, VIEW_ID } from 'vs/workbench/services/search/common/search';
B
Benjamin Pasero 已提交
39
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
J
Johannes Rieken 已提交
40
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
41
import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
42 43 44
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
45
import { ResourceLabels } from 'vs/workbench/browser/labels';
46
import { Viewlet } from 'vs/workbench/browser/viewlet';
47
import { IEditor } from 'vs/workbench/common/editor';
48 49
import { IPanel } from 'vs/workbench/common/panel';
import { IViewlet } from 'vs/workbench/common/viewlet';
50 51 52 53 54 55 56 57
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, getKeyboardEventForEditorOpen, RefreshAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, SearchDND } from 'vs/workbench/contrib/search/browser/searchResultsView';
import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
58
import { FileMatch, FileMatchOrMatch, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult, BaseFolderMatch } from 'vs/workbench/contrib/search/common/searchModel';
59
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
60
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
S
Sandeep Somavarapu 已提交
61
import { IPartService } from 'vs/workbench/services/part/common/partService';
62
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
63
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
64
import { relativePath } from 'vs/base/common/resources';
E
Erich Gamma 已提交
65

66 67
const $ = dom.$;

68
export class SearchView extends Viewlet implements IViewlet, IPanel {
E
Erich Gamma 已提交
69

R
Rob Lourens 已提交
70
	private static readonly MAX_TEXT_RESULTS = 10000;
S
Sandeep Somavarapu 已提交
71

72
	private static readonly WIDE_CLASS_NAME = 'wide';
R
Rob Lourens 已提交
73
	private static readonly WIDE_VIEW_SIZE = 1000;
74
	private static readonly ACTIONS_RIGHT_CLASS_NAME = 'actions-right';
75

E
Erich Gamma 已提交
76
	private isDisposed: boolean;
77

E
Erich Gamma 已提交
78
	private queryBuilder: QueryBuilder;
79
	private viewModel: SearchModel;
E
Erich Gamma 已提交
80

A
Alex Dima 已提交
81
	private viewletVisible: IContextKey<boolean>;
S
Sandeep Somavarapu 已提交
82
	private viewletFocused: IContextKey<boolean>;
83 84
	private inputBoxFocused: IContextKey<boolean>;
	private inputPatternIncludesFocused: IContextKey<boolean>;
85
	private inputPatternExclusionsFocused: IContextKey<boolean>;
86 87
	private firstMatchFocused: IContextKey<boolean>;
	private fileMatchOrMatchFocused: IContextKey<boolean>;
88
	private fileMatchOrFolderMatchFocus: IContextKey<boolean>;
89
	private fileMatchFocused: IContextKey<boolean>;
90
	private folderMatchFocused: IContextKey<boolean>;
91
	private matchFocused: IContextKey<boolean>;
92
	private hasSearchResultsKey: IContextKey<boolean>;
R
Rob Lourens 已提交
93

S
Sandeep Somavarapu 已提交
94
	private searchSubmitted: boolean;
95
	private searching: boolean;
S
Sandeep Somavarapu 已提交
96

97
	private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction> = [];
98 99
	private cancelAction: CancelSearchAction;
	private refreshAction: RefreshAction;
R
Rob Lourens 已提交
100
	private contextMenu: IMenu;
101

R
Rob Lourens 已提交
102
	private tree: WorkbenchObjectTree<RenderableMatch>;
B
Benjamin Pasero 已提交
103
	private treeLabels: ResourceLabels;
B
Benjamin Pasero 已提交
104
	private viewletState: object;
105
	private globalMemento: object;
106 107
	private messagesElement: HTMLElement;
	private messageDisposables: IDisposable[] = [];
108
	private searchWidgetsContainerElement: HTMLElement;
109
	private searchWidget: SearchWidget;
110
	private size: dom.Dimension;
E
Erich Gamma 已提交
111
	private queryDetails: HTMLElement;
S
Sandeep Somavarapu 已提交
112
	private toggleQueryDetailsButton: HTMLElement;
113 114
	private inputPatternExcludes: ExcludePatternInputWidget;
	private inputPatternIncludes: PatternInputWidget;
115
	private resultsElement: HTMLElement;
E
Erich Gamma 已提交
116

S
Sandeep Somavarapu 已提交
117 118
	private currentSelectedFileMatch: FileMatch;

M
Matt Bierner 已提交
119
	private readonly selectCurrentMatchEmitter: Emitter<string | undefined>;
S
Sandeep Somavarapu 已提交
120
	private delayedRefresh: Delayer<void>;
121
	private changedWhileHidden: boolean;
R
Rob Lourens 已提交
122

123
	private searchWithoutFolderMessageElement: HTMLElement;
124

J
Johannes Rieken 已提交
125
	private currentSearchQ = Promise.resolve();
126

127
	constructor(
S
Sandeep Somavarapu 已提交
128
		@IPartService partService: IPartService,
129
		@ITelemetryService telemetryService: ITelemetryService,
130 131 132 133 134
		@IFileService private readonly fileService: IFileService,
		@IEditorService private readonly editorService: IEditorService,
		@IProgressService private readonly progressService: IProgressService,
		@INotificationService private readonly notificationService: INotificationService,
		@IDialogService private readonly dialogService: IDialogService,
B
Benjamin Pasero 已提交
135
		@IStorageService storageService: IStorageService,
136 137
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
138
		@IConfigurationService configurationService: IConfigurationService,
139 140 141 142 143 144
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@ISearchWorkbenchService private readonly searchWorkbenchService: ISearchWorkbenchService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
		@IReplaceService private readonly replaceService: IReplaceService,
		@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
		@IPreferencesService private readonly preferencesService: IPreferencesService,
145
		@IThemeService protected themeService: IThemeService,
146 147 148
		@ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService,
		@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
		@IContextMenuService private readonly contextMenuService: IContextMenuService,
149
		@IMenuService private readonly menuService: IMenuService
E
Erich Gamma 已提交
150
	) {
151
		super(VIEW_ID, configurationService, partService, telemetryService, themeService, storageService);
E
Erich Gamma 已提交
152

I
isidor 已提交
153
		this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService);
S
Sandeep Somavarapu 已提交
154
		this.viewletFocused = Constants.SearchViewFocusedKey.bindTo(contextKeyService);
155 156
		this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
		this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService);
157
		this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService);
158 159
		this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService);
		this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService);
160
		this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService);
161
		this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService);
162
		this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService);
163
		this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService);
164
		this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService);
E
Erich Gamma 已提交
165 166

		this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
B
Benjamin Pasero 已提交
167
		this.viewletState = this.getMemento(StorageScope.WORKSPACE);
168
		this.globalMemento = this.getMemento(StorageScope.GLOBAL);
E
Erich Gamma 已提交
169

B
Benjamin Pasero 已提交
170 171 172
		this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e)));
		this._register(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e)));
		this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
173
		this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory()));
174

175
		this.selectCurrentMatchEmitter = this._register(new Emitter<string>());
J
Joao Moreno 已提交
176
		this._register(Event.debounce(this.selectCurrentMatchEmitter.event, (l, e) => e, 100, /*leading=*/true)
177
			(() => this.selectCurrentMatch()));
R
Rob Lourens 已提交
178

179
		this.delayedRefresh = this._register(new Delayer<void>(250));
180

E
Erich Gamma 已提交
181 182
	}

R
Rob Lourens 已提交
183
	get searchResult(): SearchResult {
R
Rob Lourens 已提交
184 185 186
		return this.viewModel && this.viewModel.searchResult;
	}

187
	private onDidChangeWorkbenchState(): void {
188 189
		if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) {
			dom.hide(this.searchWithoutFolderMessageElement);
190 191 192
		}
	}

R
Rob Lourens 已提交
193
	create(parent: HTMLElement): void {
E
Erich Gamma 已提交
194 195
		super.create(parent);

B
Benjamin Pasero 已提交
196
		this.viewModel = this._register(this.searchWorkbenchService.searchModel);
197
		dom.addClass(parent, 'search-view');
E
Erich Gamma 已提交
198

199
		this.searchWidgetsContainerElement = dom.append(parent, $('.search-widgets-container'));
200
		this.createSearchWidget(this.searchWidgetsContainerElement);
201

202
		const history = this.searchHistoryService.load();
B
Benjamin Pasero 已提交
203 204
		const filePatterns = this.viewletState['query.filePatterns'] || '';
		const patternExclusions = this.viewletState['query.folderExclusions'] || '';
R
Rob Lourens 已提交
205
		const patternExclusionsHistory: string[] = history.exclude || [];
B
Benjamin Pasero 已提交
206
		const patternIncludes = this.viewletState['query.folderIncludes'] || '';
R
Rob Lourens 已提交
207
		const patternIncludesHistory: string[] = history.include || [];
B
Benjamin Pasero 已提交
208 209 210
		const queryDetailsExpanded = this.viewletState['query.queryDetailsExpanded'] || '';
		const useExcludesAndIgnoreFiles = typeof this.viewletState['query.useExcludesAndIgnoreFiles'] === 'boolean' ?
			this.viewletState['query.useExcludesAndIgnoreFiles'] : true;
211

212
		this.queryDetails = dom.append(this.searchWidgetsContainerElement, $('.query-details'));
S
Sandeep Somavarapu 已提交
213

214 215 216
		// Toggle query details button
		this.toggleQueryDetailsButton = dom.append(this.queryDetails,
			$('.more', { tabindex: 0, role: 'button', title: nls.localize('moreSearch', "Toggle Search Details") }));
E
Erich Gamma 已提交
217

218 219
		this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => {
			dom.EventHelper.stop(e);
220
			this.toggleQueryDetails(!this.isScreenReaderOptimized());
221 222 223
		}));
		this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
			const event = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
224

225 226 227 228 229 230 231
			if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
				dom.EventHelper.stop(e);
				this.toggleQueryDetails(false);
			}
		}));
		this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
			const event = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
232

233 234 235 236 237 238 239 240 241
			if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
				if (this.searchWidget.isReplaceActive()) {
					this.searchWidget.focusReplaceAllAction();
				} else {
					this.searchWidget.focusRegexAction();
				}
				dom.EventHelper.stop(e);
			}
		}));
E
Erich Gamma 已提交
242

243 244 245 246 247
		// folder includes list
		const folderIncludesList = dom.append(this.queryDetails,
			$('.file-types.includes'));
		const filesToIncludeTitle = nls.localize('searchScope.includes', "files to include");
		dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle));
248

249 250 251 252
		this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, {
			ariaLabel: nls.localize('label.includes', 'Search Include Patterns'),
			history: patternIncludesHistory,
		}));
253

254
		this.inputPatternIncludes.setValue(patternIncludes);
255

256
		this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true));
257 258 259 260 261 262 263 264 265 266 267
		this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget
		this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused);

		// excludes list
		const excludesList = dom.append(this.queryDetails, $('.file-types.excludes'));
		const excludesTitle = nls.localize('searchScope.excludes', "files to exclude");
		dom.append(excludesList, $('h4', undefined, excludesTitle));
		this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, {
			ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns'),
			history: patternExclusionsHistory,
		}));
268

269 270
		this.inputPatternExcludes.setValue(patternExclusions);
		this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles);
271

272
		this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true));
273 274 275
		this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget
		this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused);

276
		this.messagesElement = dom.append(parent, $('.messages'));
277
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
278
			this.showSearchWithoutFolderMessage();
279
		}
280

281
		this.createSearchResultsView(parent);
282

S
Sandeep Somavarapu 已提交
283
		this.actions = [
284 285
			this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)),
			this._register(this.instantiationService.createInstance(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL))
S
Sandeep Somavarapu 已提交
286
		];
287 288
		this.refreshAction = this._register(this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL));
		this.cancelAction = this._register(this.instantiationService.createInstance(CancelSearchAction, CancelSearchAction.ID, CancelSearchAction.LABEL));
289

290
		if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '' || queryDetailsExpanded !== '' || !useExcludesAndIgnoreFiles) {
291
			this.toggleQueryDetails(true, true, true);
292 293
		}

B
Benjamin Pasero 已提交
294
		this._register(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event)));
S
Sandeep Somavarapu 已提交
295

296
		this._register(this.searchWidget.searchInput.onInput(() => this.updateActions()));
297
		this._register(this.searchWidget.replaceInput.onDidChange(() => this.updateActions()));
298 299
		this._register(this.searchIncludePattern.inputBox.onDidChange(() => this.updateActions()));
		this._register(this.searchExcludePattern.inputBox.onDidChange(() => this.updateActions()));
300

S
Sandeep Somavarapu 已提交
301 302
		this._register(this.onDidFocus(() => this.viewletFocused.set(true)));
		this._register(this.onDidBlur(() => this.viewletFocused.set(false)));
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323

		this._register(this.onDidChangeVisibility(visible => this.onVisibilityChanged(visible)));
	}

	private onVisibilityChanged(visible: boolean): void {
		this.viewletVisible.set(visible);
		if (visible) {
			if (this.changedWhileHidden) {
				// Render if results changed while viewlet was hidden - #37818
				this.refreshAndUpdateCount();
				this.changedWhileHidden = false;
			}
		}

		// Enable highlights if there are searchresults
		if (this.viewModel) {
			this.viewModel.searchResult.toggleHighlights(visible);
		}

		// Open focused element from results in case the editor area is otherwise empty
		if (visible && !this.editorService.activeEditor) {
324
			const focus = this.tree.getFocus();
325 326 327 328
			if (focus) {
				this.onFocus(focus, true);
			}
		}
329 330
	}

R
Rob Lourens 已提交
331
	get searchAndReplaceWidget(): SearchWidget {
332 333 334
		return this.searchWidget;
	}

R
Rob Lourens 已提交
335
	get searchIncludePattern(): PatternInputWidget {
336 337 338
		return this.inputPatternIncludes;
	}

R
Rob Lourens 已提交
339
	get searchExcludePattern(): PatternInputWidget {
340 341 342
		return this.inputPatternExcludes;
	}

S
Sandeep Somavarapu 已提交
343 344 345
	private updateActions(): void {
		for (const action of this.actions) {
			action.update();
346 347
			this.refreshAction.update();
			this.cancelAction.update();
S
Sandeep Somavarapu 已提交
348 349 350
		}
	}

351 352 353 354 355 356
	private isScreenReaderOptimized() {
		const detected = browser.getAccessibilitySupport() === env.AccessibilitySupport.Enabled;
		const config = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
		return config === 'on' || (config === 'auto' && detected);
	}

357
	private createSearchWidget(container: HTMLElement): void {
358
		const contentPattern = this.viewletState['query.contentPattern'] || '';
359
		const replaceText = this.viewletState['query.replaceText'] || '';
360 361 362
		const isRegex = this.viewletState['query.regex'] === true;
		const isWholeWords = this.viewletState['query.wholeWords'] === true;
		const isCaseSensitive = this.viewletState['query.caseSensitive'] === true;
363
		const history = this.searchHistoryService.load();
364 365 366
		const searchHistory = history.search || this.viewletState['query.searchHistory'] || [];
		const replaceHistory = history.replace || this.viewletState['query.replaceHistory'] || [];
		const showReplace = typeof this.viewletState['view.showReplace'] === 'boolean' ? this.viewletState['view.showReplace'] : true;
E
Erich Gamma 已提交
367

368
		this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, <ISearchWidgetOptions>{
369
			value: contentPattern,
370
			replaceValue: replaceText,
371 372
			isRegex: isRegex,
			isCaseSensitive: isCaseSensitive,
373
			isWholeWords: isWholeWords,
374
			searchHistory: searchHistory,
375
			replaceHistory: replaceHistory
B
Benjamin Pasero 已提交
376
		}));
S
Sandeep Somavarapu 已提交
377

B
Benjamin Pasero 已提交
378
		if (showReplace) {
S
Sandeep Somavarapu 已提交
379 380 381
			this.searchWidget.toggleReplace(true);
		}

382
		this._register(this.searchWidget.onSearchSubmit(() => this.onQueryChanged()));
B
Benjamin Pasero 已提交
383
		this._register(this.searchWidget.onSearchCancel(() => this.cancelSearch()));
384
		this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.onQueryChanged(true)));
385

386 387 388
		this._register(this.searchWidget.onDidHeightChange(() => this.reLayout()));

		this._register(this.searchWidget.onReplaceToggled(() => this.reLayout()));
B
Benjamin Pasero 已提交
389
		this._register(this.searchWidget.onReplaceStateChange((state) => {
J
Johannes Rieken 已提交
390
			this.viewModel.replaceActive = state;
R
Rob Lourens 已提交
391
			this.refreshTree();
S
Sandeep Somavarapu 已提交
392
		}));
B
Benjamin Pasero 已提交
393
		this._register(this.searchWidget.onReplaceValueChanged((value) => {
J
Johannes Rieken 已提交
394
			this.viewModel.replaceString = this.searchWidget.getReplaceValue();
R
Rob Lourens 已提交
395
			this.delayedRefresh.trigger(() => this.refreshTree());
S
Sandeep Somavarapu 已提交
396
		}));
397

B
Benjamin Pasero 已提交
398
		this._register(this.searchWidget.onBlur(() => {
S
Sandeep Somavarapu 已提交
399 400 401
			this.toggleQueryDetailsButton.focus();
		}));

B
Benjamin Pasero 已提交
402
		this._register(this.searchWidget.onReplaceAll(() => this.replaceAll()));
403

404 405 406 407
		this.trackInputBox(this.searchWidget.searchInputFocusTracker);
		this.trackInputBox(this.searchWidget.replaceInputFocusTracker);
	}

408
	private trackInputBox(inputFocusTracker: dom.IFocusTracker, contextKey?: IContextKey<boolean>): void {
B
Benjamin Pasero 已提交
409
		this._register(inputFocusTracker.onDidFocus(() => {
410
			this.inputBoxFocused.set(true);
411 412 413
			if (contextKey) {
				contextKey.set(true);
			}
414
		}));
B
Benjamin Pasero 已提交
415
		this._register(inputFocusTracker.onDidBlur(() => {
416
			this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus()
417
				|| this.searchWidget.replaceInputHasFocus()
418 419
				|| this.inputPatternIncludes.inputHasFocus()
				|| this.inputPatternExcludes.inputHasFocus());
420 421 422
			if (contextKey) {
				contextKey.set(false);
			}
423
		}));
S
Sandeep Somavarapu 已提交
424 425
	}

R
Rob Lourens 已提交
426
	private onSearchResultsChanged(event?: IChangeEvent): void {
427 428 429 430 431 432 433
		if (this.isVisible()) {
			return this.refreshAndUpdateCount(event);
		} else {
			this.changedWhileHidden = true;
		}
	}

R
Rob Lourens 已提交
434
	private refreshAndUpdateCount(event?: IChangeEvent): void {
R
Rob Lourens 已提交
435 436 437
		this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty());
		this.updateSearchResultCount(this.viewModel.searchResult.query.userDisabledExcludesAndIgnoreFiles);
		return this.refreshTree(event);
438 439
	}

R
Rob Lourens 已提交
440
	refreshTree(event?: IChangeEvent): void {
441
		const collapseResults = this.configurationService.getValue<ISearchConfigurationProperties>('search').collapseResults;
442
		if (!event || event.added || event.removed) {
443
			this.tree.setChildren(null, this.createResultIterator(collapseResults));
444
		} else {
R
Rob Lourens 已提交
445
			event.elements.forEach(element => {
446
				if (element instanceof BaseFolderMatch) {
447
					// The folder may or may not be in the tree. Refresh the whole thing.
448
					this.tree.setChildren(null, this.createResultIterator(collapseResults));
449 450 451
					return;
				}

R
Rob Lourens 已提交
452
				const root = element instanceof SearchResult ? null : element;
453
				this.tree.setChildren(root, this.createIterator(element, collapseResults));
R
Rob Lourens 已提交
454
			});
455 456 457
		}
	}

458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
	private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
		const folderMatches = this.searchResult.folderMatches()
			.filter(fm => !fm.isEmpty())
			.sort(searchMatchComparer);

		if (folderMatches.length === 1) {
			return this.createFolderIterator(folderMatches[0], collapseResults);
		}

		const foldersIt = Iterator.fromArray(folderMatches);
		return Iterator.map(foldersIt, folderMatch => {
			const children = this.createFolderIterator(folderMatch, collapseResults);
			return <ITreeElement<RenderableMatch>>{ element: folderMatch, children };
		});
	}

	private createFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
		const filesIt = Iterator.fromArray(
			folderMatch.matches()
				.sort(searchMatchComparer));

		return Iterator.map(filesIt, fileMatch => {
			const children = this.createFileIterator(fileMatch);

			let nodeExists = true;
			try { this.tree.getNode(fileMatch); } catch (e) { nodeExists = false; }

			const collapsed = nodeExists ? undefined :
				(collapseResults === 'alwaysCollapse' || (fileMatch.matches().length > 10 && collapseResults !== 'alwaysExpand'));

			return <ITreeElement<RenderableMatch>>{ element: fileMatch, children, collapsed };
		});
	}

	private createFileIterator(fileMatch: FileMatch): Iterator<ITreeElement<RenderableMatch>> {
		const matchesIt = Iterator.from(
			fileMatch.matches()
				.sort(searchMatchComparer));
		return Iterator.map(matchesIt, r => (<ITreeElement<RenderableMatch>>{ element: r }));
	}

499
	private createIterator(match: BaseFolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
500
		return match instanceof SearchResult ? this.createResultIterator(collapseResults) :
501
			match instanceof BaseFolderMatch ? this.createFolderIterator(match, collapseResults) :
502 503 504
				this.createFileIterator(match);
	}

505
	private replaceAll(): void {
506
		if (this.viewModel.searchResult.count() === 0) {
S
Sandeep Somavarapu 已提交
507 508 509
			return;
		}

510
		const progressRunner = this.progressService.show(100);
511

512 513 514 515
		const occurrences = this.viewModel.searchResult.count();
		const fileCount = this.viewModel.searchResult.fileCount();
		const replaceValue = this.searchWidget.getReplaceValue() || '';
		const afterReplaceAllMessage = this.buildAfterReplaceAllMessage(occurrences, fileCount, replaceValue);
516

517
		const confirmation: IConfirmation = {
518
			title: nls.localize('replaceAll.confirmation.title', "Replace All"),
519
			message: this.buildReplaceAllConfirmationMessage(occurrences, fileCount, replaceValue),
520
			primaryButton: nls.localize('replaceAll.confirm.button', "&&Replace"),
B
Benjamin Pasero 已提交
521
			type: 'question'
522 523
		};

524
		this.dialogService.confirm(confirmation).then(res => {
525
			if (res.confirmed) {
526 527 528
				this.searchWidget.setReplaceAllActionState(false);
				this.viewModel.searchResult.replaceAll(progressRunner).then(() => {
					progressRunner.done();
529
					const messageEl = this.clearMessage();
530
					dom.append(messageEl, $('p', undefined, afterReplaceAllMessage));
531 532 533
				}, (error) => {
					progressRunner.done();
					errors.isPromiseCanceledError(error);
534
					this.notificationService.error(error);
535 536 537
				});
			}
		});
538 539
	}

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
	private buildAfterReplaceAllMessage(occurrences: number, fileCount: number, replaceValue?: string) {
		if (occurrences === 1) {
			if (fileCount === 1) {
				if (replaceValue) {
					return nls.localize('replaceAll.occurrence.file.message', "Replaced {0} occurrence across {1} file with '{2}'.", occurrences, fileCount, replaceValue);
				}

				return nls.localize('removeAll.occurrence.file.message', "Replaced {0} occurrence across {1} file'.", occurrences, fileCount);
			}

			if (replaceValue) {
				return nls.localize('replaceAll.occurrence.files.message', "Replaced {0} occurrence across {1} files with '{2}'.", occurrences, fileCount, replaceValue);
			}

			return nls.localize('removeAll.occurrence.files.message', "Replaced {0} occurrence across {1} files.", occurrences, fileCount);
		}

		if (fileCount === 1) {
			if (replaceValue) {
				return nls.localize('replaceAll.occurrences.file.message', "Replaced {0} occurrences across {1} file with '{2}'.", occurrences, fileCount, replaceValue);
			}

			return nls.localize('removeAll.occurrences.file.message', "Replaced {0} occurrences across {1} file'.", occurrences, fileCount);
		}

		if (replaceValue) {
			return nls.localize('replaceAll.occurrences.files.message', "Replaced {0} occurrences across {1} files with '{2}'.", occurrences, fileCount, replaceValue);
		}

		return nls.localize('removeAll.occurrences.files.message', "Replaced {0} occurrences across {1} files.", occurrences, fileCount);
	}

	private buildReplaceAllConfirmationMessage(occurrences: number, fileCount: number, replaceValue?: string) {
		if (occurrences === 1) {
			if (fileCount === 1) {
				if (replaceValue) {
					return nls.localize('removeAll.occurrence.file.confirmation.message', "Replace {0} occurrence across {1} file with '{2}'?", occurrences, fileCount, replaceValue);
				}

				return nls.localize('replaceAll.occurrence.file.confirmation.message', "Replace {0} occurrence across {1} file'?", occurrences, fileCount);
			}

			if (replaceValue) {
				return nls.localize('removeAll.occurrence.files.confirmation.message', "Replace {0} occurrence across {1} files with '{2}'?", occurrences, fileCount, replaceValue);
			}

			return nls.localize('replaceAll.occurrence.files.confirmation.message', "Replace {0} occurrence across {1} files?", occurrences, fileCount);
		}

		if (fileCount === 1) {
			if (replaceValue) {
				return nls.localize('removeAll.occurrences.file.confirmation.message', "Replace {0} occurrences across {1} file with '{2}'?", occurrences, fileCount, replaceValue);
			}

			return nls.localize('replaceAll.occurrences.file.confirmation.message', "Replace {0} occurrences across {1} file'?", occurrences, fileCount);
		}

		if (replaceValue) {
			return nls.localize('removeAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files with '{2}'?", occurrences, fileCount, replaceValue);
		}

		return nls.localize('replaceAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files?", occurrences, fileCount);
	}

604
	private clearMessage(): HTMLElement {
R
Rob Lourens 已提交
605
		this.searchWithoutFolderMessageElement = undefined;
606

607 608 609 610 611
		dom.clearNode(this.messagesElement);
		dom.show(this.messagesElement);
		dispose(this.messageDisposables);
		this.messageDisposables = [];

612
		return dom.append(this.messagesElement, $('.message'));
613 614
	}

615 616
	private createSearchResultsView(container: HTMLElement): void {
		this.resultsElement = dom.append(container, $('.results.show-file-icons'));
R
Rob Lourens 已提交
617 618
		const delegate = this.instantiationService.createInstance(SearchDelegate);

619 620 621 622 623 624
		const identityProvider: IIdentityProvider<RenderableMatch> = {
			getId(element: RenderableMatch) {
				return element.id();
			}
		};

625
		this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
B
Benjamin Pasero 已提交
626
		this.tree = this._register(<WorkbenchObjectTree<RenderableMatch, any>>this.instantiationService.createInstance(WorkbenchObjectTree,
R
Rob Lourens 已提交
627 628 629
			this.resultsElement,
			delegate,
			[
B
Benjamin Pasero 已提交
630 631
				this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)),
				this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)),
632
				this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)),
R
Rob Lourens 已提交
633
			],
634
			{
635
				identityProvider,
636 637
				accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider, this.viewModel),
				dnd: this.instantiationService.createInstance(SearchDND)
B
Benjamin Pasero 已提交
638
			}));
R
Rob Lourens 已提交
639
		this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
R
Rob Lourens 已提交
640 641

		const resourceNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: true }));
J
Joao Moreno 已提交
642
		this._register(Event.debounce(resourceNavigator.onDidOpenResource, (last, event) => event, 75, true)(options => {
643
			if (options.element instanceof Match) {
644
				const selectedMatch: Match = options.element;
645 646
				if (this.currentSelectedFileMatch) {
					this.currentSelectedFileMatch.setSelectedMatch(null);
647
				}
648 649
				this.currentSelectedFileMatch = selectedMatch.parent();
				this.currentSelectedFileMatch.setSelectedMatch(selectedMatch);
R
Rob Lourens 已提交
650

R
Rob Lourens 已提交
651
				this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned);
652 653
			}
		}));
S
Sandeep Somavarapu 已提交
654

J
Joao Moreno 已提交
655
		this._register(Event.any<any>(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => {
656
			if (this.tree.isDOMFocused()) {
R
Rob Lourens 已提交
657 658
				const focus = this.tree.getFocus()[0];
				this.firstMatchFocused.set(this.tree.navigate().first() === focus);
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
				this.fileMatchOrMatchFocused.set(!!focus);
				this.fileMatchFocused.set(focus instanceof FileMatch);
				this.folderMatchFocused.set(focus instanceof FolderMatch);
				this.matchFocused.set(focus instanceof Match);
				this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch);
			}
		}));

		this._register(this.tree.onDidBlur(e => {
			this.firstMatchFocused.reset();
			this.fileMatchOrMatchFocused.reset();
			this.fileMatchFocused.reset();
			this.folderMatchFocused.reset();
			this.matchFocused.reset();
			this.fileMatchOrFolderMatchFocus.reset();
		}));
E
Erich Gamma 已提交
675 676
	}

R
Rob Lourens 已提交
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
	private onContextMenu(e: ITreeContextMenuEvent<RenderableMatch>): void {
		if (!e.element) {
			return;
		}

		if (!this.contextMenu) {
			this.contextMenu = this._register(this.menuService.createMenu(MenuId.SearchContext, this.contextKeyService));
		}

		e.browserEvent.preventDefault();
		e.browserEvent.stopPropagation();

		this.contextMenuService.showContextMenu({
			getAnchor: () => e.anchor,
			getActions: () => {
				const actions: IAction[] = [];
				fillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);
				return actions;
			},
			getActionsContext: () => e.element
		});
	}

R
Rob Lourens 已提交
700
	selectCurrentMatch(): void {
R
Rob Lourens 已提交
701
		const focused = this.tree.getFocus()[0];
R
Rob Lourens 已提交
702 703
		const fakeKeyboardEvent = getKeyboardEventForEditorOpen({ preserveFocus: false });
		this.tree.setSelection([focused], fakeKeyboardEvent);
R
Rob Lourens 已提交
704 705
	}

R
Rob Lourens 已提交
706
	selectNextMatch(): void {
M
Matt Bierner 已提交
707
		const [selected] = this.tree.getSelection();
708

R
Rob Lourens 已提交
709 710
		// Expand the initial selected node, if needed
		if (selected instanceof FileMatch) {
R
Rob Lourens 已提交
711 712
			if (this.tree.isCollapsed(selected)) {
				this.tree.expand(selected);
R
Rob Lourens 已提交
713 714 715
			}
		}

R
Rob Lourens 已提交
716
		let navigator = this.tree.navigate(selected);
R
Rob Lourens 已提交
717 718

		let next = navigator.next();
719
		if (!next) {
R
Rob Lourens 已提交
720
			// Reached the end - get a new navigator from the root.
R
Rob Lourens 已提交
721
			navigator = this.tree.navigate();
R
Rob Lourens 已提交
722
			next = navigator.first();
723 724 725
		}

		// Expand and go past FileMatch nodes
R
Rob Lourens 已提交
726
		while (!(next instanceof Match)) {
R
Rob Lourens 已提交
727 728
			if (this.tree.isCollapsed(next)) {
				this.tree.expand(next);
729 730 731
			}

			// Select the FileMatch's first child
732
			next = navigator.next();
733 734 735
		}

		// Reveal the newly selected element
736
		if (next) {
R
Rob Lourens 已提交
737
			this.tree.setFocus([next]);
R
Rob Lourens 已提交
738
			this.tree.setSelection([next]);
739
			this.tree.reveal(next);
740
			this.selectCurrentMatchEmitter.fire(undefined);
741
		}
742 743
	}

R
Rob Lourens 已提交
744
	selectPreviousMatch(): void {
M
Matt Bierner 已提交
745
		const [selected] = this.tree.getSelection();
R
Rob Lourens 已提交
746
		let navigator = this.tree.navigate(selected);
747

748
		let prev = navigator.previous();
749 750

		// Expand and go past FileMatch nodes
751 752
		if (!(prev instanceof Match)) {
			prev = navigator.previous();
R
Rob Lourens 已提交
753
			if (!prev) {
R
Rob Lourens 已提交
754
				// Wrap around
R
Rob Lourens 已提交
755 756 757 758
				prev = navigator.last();

				// This is complicated because .last will set the navigator to the last FileMatch,
				// so expand it and FF to its last child
R
Rob Lourens 已提交
759
				this.tree.expand(prev);
M
Matt Bierner 已提交
760
				let tmp: RenderableMatch | null;
R
Rob Lourens 已提交
761 762 763 764 765
				while (tmp = navigator.next()) {
					prev = tmp;
				}
			}

766 767 768
			if (!(prev instanceof Match)) {
				// There is a second non-Match result, which must be a collapsed FileMatch.
				// Expand it then select its last child.
R
Rob Lourens 已提交
769 770 771
				const nextItem = navigator.next();
				this.tree.expand(prev);
				navigator = this.tree.navigate(nextItem); // recreate navigator because modifying the tree can invalidate it
772
				prev = navigator.previous();
773 774 775 776
			}
		}

		// Reveal the newly selected element
777
		if (prev) {
R
Rob Lourens 已提交
778
			this.tree.setFocus([prev]);
R
Rob Lourens 已提交
779
			this.tree.setSelection([prev]);
R
Rob Lourens 已提交
780
			this.tree.reveal(prev);
781
			this.selectCurrentMatchEmitter.fire(undefined);
782
		}
783 784
	}

R
Rob Lourens 已提交
785
	moveFocusToResults(): void {
Y
Yogesh 已提交
786 787 788
		this.tree.domFocus();
	}

R
Rob Lourens 已提交
789
	focus(): void {
E
Erich Gamma 已提交
790 791
		super.focus();

R
Rob Lourens 已提交
792 793 794 795
		const updatedText = this.updateTextFromSelection();
		this.searchWidget.focus(undefined, undefined, updatedText);
	}

R
Rob Lourens 已提交
796
	updateTextFromSelection(allowUnselectedWord = true): boolean {
797
		let updatedText = false;
798 799
		const seedSearchStringFromSelection = this.configurationService.getValue<IEditorOptions>('editor').find.seedSearchStringFromSelection;
		if (seedSearchStringFromSelection) {
R
Rob Lourens 已提交
800
			let selectedText = this.getSearchTextFromEditor(allowUnselectedWord);
801
			if (selectedText) {
802 803 804 805
				if (this.searchWidget.searchInput.getRegex()) {
					selectedText = strings.escapeRegExpCharacters(selectedText);
				}

806
				this.searchWidget.searchInput.setValue(selectedText);
807
				updatedText = true;
808
			}
809
		}
R
Rob Lourens 已提交
810

R
Rob Lourens 已提交
811
		return updatedText;
812 813
	}

R
Rob Lourens 已提交
814
	focusNextInputBox(): void {
815
		if (this.searchWidget.searchInputHasFocus()) {
816 817 818 819 820
			if (this.searchWidget.isReplaceShown()) {
				this.searchWidget.focus(true, true);
			} else {
				this.moveFocusFromSearchOrReplace();
			}
821 822 823 824
			return;
		}

		if (this.searchWidget.replaceInputHasFocus()) {
825
			this.moveFocusFromSearchOrReplace();
826 827 828 829
			return;
		}

		if (this.inputPatternIncludes.inputHasFocus()) {
830 831 832 833 834 835
			this.inputPatternExcludes.focus();
			this.inputPatternExcludes.select();
			return;
		}

		if (this.inputPatternExcludes.inputHasFocus()) {
836 837 838 839 840
			this.selectTreeIfNotSelected();
			return;
		}
	}

841 842
	private moveFocusFromSearchOrReplace() {
		if (this.showsFileTypes()) {
843
			this.toggleQueryDetails(true, this.showsFileTypes());
844 845 846 847 848
		} else {
			this.selectTreeIfNotSelected();
		}
	}

R
Rob Lourens 已提交
849
	focusPreviousInputBox(): void {
850 851 852 853 854 855 856 857 858 859 860 861 862 863
		if (this.searchWidget.searchInputHasFocus()) {
			return;
		}

		if (this.searchWidget.replaceInputHasFocus()) {
			this.searchWidget.focus(true);
			return;
		}

		if (this.inputPatternIncludes.inputHasFocus()) {
			this.searchWidget.focus(true, true);
			return;
		}

864 865 866 867 868 869
		if (this.inputPatternExcludes.inputHasFocus()) {
			this.inputPatternIncludes.focus();
			this.inputPatternIncludes.select();
			return;
		}

S
Sandeep Somavarapu 已提交
870 871 872 873
		if (this.tree.isDOMFocused()) {
			this.moveFocusFromResults();
			return;
		}
874 875
	}

S
Sandeep Somavarapu 已提交
876
	private moveFocusFromResults(): void {
877
		if (this.showsFileTypes()) {
878
			this.toggleQueryDetails(true, true, false, true);
879 880 881
		} else {
			this.searchWidget.focus(true, true);
		}
E
Erich Gamma 已提交
882 883 884 885 886 887 888
	}

	private reLayout(): void {
		if (this.isDisposed) {
			return;
		}

R
Rob Lourens 已提交
889
		const actionsPosition = this.configurationService.getValue<ISearchConfigurationProperties>('search').actionsPosition;
890 891
		dom.toggleClass(this.getContainer(), SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right');
		dom.toggleClass(this.getContainer(), SearchView.WIDE_CLASS_NAME, this.size.width >= SearchView.WIDE_VIEW_SIZE);
892

893
		this.searchWidget.setWidth(this.size.width - 28 /* container margin */);
E
Erich Gamma 已提交
894

895
		this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */);
S
Sandeep Somavarapu 已提交
896
		this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */);
E
Erich Gamma 已提交
897

898 899 900 901
		const messagesSize = this.messagesElement.style.display === 'none' ?
			0 :
			dom.getTotalHeight(this.messagesElement);

902 903
		const searchResultContainerSize = this.size.height -
			messagesSize -
904
			dom.getTotalHeight(this.searchWidgetsContainerElement);
905

906
		this.resultsElement.style.height = searchResultContainerSize + 'px';
E
Erich Gamma 已提交
907

908
		this.tree.layout(searchResultContainerSize, this.size.width);
E
Erich Gamma 已提交
909 910
	}

R
Rob Lourens 已提交
911
	layout(dimension: dom.Dimension): void {
E
Erich Gamma 已提交
912
		this.size = dimension;
B
Benjamin Pasero 已提交
913
		this.reLayout();
E
Erich Gamma 已提交
914 915
	}

R
Rob Lourens 已提交
916
	getControl() {
E
Erich Gamma 已提交
917 918 919
		return this.tree;
	}

R
Rob Lourens 已提交
920
	isSearchSubmitted(): boolean {
S
Sandeep Somavarapu 已提交
921 922 923
		return this.searchSubmitted;
	}

R
Rob Lourens 已提交
924
	isSearching(): boolean {
925 926 927
		return this.searching;
	}

R
Rob Lourens 已提交
928
	allSearchFieldsClear(): boolean {
929 930 931 932 933 934
		return this.searchWidget.getReplaceValue() === '' &&
			this.searchWidget.searchInput.getValue() === '' &&
			this.searchIncludePattern.getValue() === '' &&
			this.searchExcludePattern.getValue() === '';
	}

R
Rob Lourens 已提交
935
	hasSearchResults(): boolean {
S
Sandeep Somavarapu 已提交
936 937 938
		return !this.viewModel.searchResult.isEmpty();
	}

R
Rob Lourens 已提交
939
	clearSearchResults(): void {
940
		this.viewModel.searchResult.clear();
E
Erich Gamma 已提交
941
		this.showEmptyStage();
942
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
943
			this.showSearchWithoutFolderMessage();
944
		}
945
		this.searchWidget.clear();
946 947
		this.searchIncludePattern.setValue('');
		this.searchExcludePattern.setValue('');
S
Sandeep Somavarapu 已提交
948
		this.viewModel.cancelSearch();
949
		this.updateActions();
E
Erich Gamma 已提交
950 951
	}

R
Rob Lourens 已提交
952
	cancelSearch(): boolean {
S
Sandeep Somavarapu 已提交
953
		if (this.viewModel.cancelSearch()) {
954
			this.searchWidget.focus();
955 956 957 958 959
			return true;
		}
		return false;
	}

960
	private selectTreeIfNotSelected(): void {
R
Rob Lourens 已提交
961
		if (this.tree.getNode(null)) {
962
			this.tree.domFocus();
963
			const selection = this.tree.getSelection();
E
Erich Gamma 已提交
964 965 966 967 968 969
			if (selection.length === 0) {
				this.tree.focusNext();
			}
		}
	}

M
Matt Bierner 已提交
970
	private getSearchTextFromEditor(allowUnselectedWord: boolean): string | null {
B
Benjamin Pasero 已提交
971
		if (!this.editorService.activeEditor) {
E
Erich Gamma 已提交
972 973 974
			return null;
		}

R
Rob Lourens 已提交
975 976 977 978
		if (dom.isAncestor(document.activeElement, this.getContainer())) {
			return null;
		}

979 980 981 982
		let activeTextEditorWidget = this.editorService.activeTextEditorWidget;
		if (isDiffEditor(activeTextEditorWidget)) {
			if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) {
				activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor();
983
			} else {
984
				activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
985 986 987
			}
		}

M
Matt Bierner 已提交
988
		if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) {
E
Erich Gamma 已提交
989 990 991
			return null;
		}

992
		const range = activeTextEditorWidget.getSelection();
R
Rob Lourens 已提交
993 994 995 996
		if (!range) {
			return null;
		}

R
Rob Lourens 已提交
997
		if (range.isEmpty() && !this.searchWidget.searchInput.getValue() && allowUnselectedWord) {
998
			const wordAtPosition = activeTextEditorWidget.getModel().getWordAtPosition(range.getStartPosition());
R
Rob Lourens 已提交
999 1000 1001 1002 1003
			if (wordAtPosition) {
				return wordAtPosition.word;
			}
		}

1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
		if (!range.isEmpty()) {
			let searchText = '';
			for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {
				let lineText = activeTextEditorWidget.getModel().getLineContent(i);
				if (i === range.endLineNumber) {
					lineText = lineText.substring(0, range.endColumn - 1);
				}

				if (i === range.startLineNumber) {
					lineText = lineText.substring(range.startColumn - 1);
				}

				if (i !== range.startLineNumber) {
					lineText = '\n' + lineText;
				}

				searchText += lineText;
			}

1023
			return searchText;
E
Erich Gamma 已提交
1024
		}
1025

E
Erich Gamma 已提交
1026 1027 1028 1029 1030 1031 1032
		return null;
	}

	private showsFileTypes(): boolean {
		return dom.hasClass(this.queryDetails, 'more');
	}

R
Rob Lourens 已提交
1033
	toggleCaseSensitive(): void {
S
Sandeep Somavarapu 已提交
1034
		this.searchWidget.searchInput.setCaseSensitive(!this.searchWidget.searchInput.getCaseSensitive());
1035
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1036 1037
	}

R
Rob Lourens 已提交
1038
	toggleWholeWords(): void {
S
Sandeep Somavarapu 已提交
1039
		this.searchWidget.searchInput.setWholeWords(!this.searchWidget.searchInput.getWholeWords());
1040
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1041 1042
	}

R
Rob Lourens 已提交
1043
	toggleRegex(): void {
S
Sandeep Somavarapu 已提交
1044
		this.searchWidget.searchInput.setRegex(!this.searchWidget.searchInput.getRegex());
1045
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1046 1047
	}

R
Rob Lourens 已提交
1048
	toggleQueryDetails(moveFocus = true, show?: boolean, skipLayout?: boolean, reverse?: boolean): void {
1049
		const cls = 'more';
E
Erich Gamma 已提交
1050
		show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show);
B
Benjamin Pasero 已提交
1051
		this.viewletState['query.queryDetailsExpanded'] = show;
E
Erich Gamma 已提交
1052 1053 1054
		skipLayout = Boolean(skipLayout);

		if (show) {
1055
			this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true');
E
Erich Gamma 已提交
1056
			dom.addClass(this.queryDetails, cls);
1057
			if (moveFocus) {
1058 1059 1060 1061 1062 1063 1064
				if (reverse) {
					this.inputPatternExcludes.focus();
					this.inputPatternExcludes.select();
				} else {
					this.inputPatternIncludes.focus();
					this.inputPatternIncludes.select();
				}
1065
			}
E
Erich Gamma 已提交
1066
		} else {
1067
			this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false');
E
Erich Gamma 已提交
1068
			dom.removeClass(this.queryDetails, cls);
1069
			if (moveFocus) {
1070
				this.searchWidget.focus();
1071
			}
E
Erich Gamma 已提交
1072
		}
B
Benjamin Pasero 已提交
1073

E
Erich Gamma 已提交
1074 1075 1076 1077 1078
		if (!skipLayout && this.size) {
			this.layout(this.size);
		}
	}

1079
	searchInFolders(resources: URI[]): void {
1080
		const folderPaths: string[] = [];
1081
		const workspace = this.contextService.getWorkspace();
1082 1083 1084

		if (resources) {
			resources.forEach(resource => {
1085
				let folderPath: string | undefined;
1086 1087
				if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
					// Show relative path from the root for single-root mode
1088
					folderPath = relativePath(workspace.folders[0].uri, resource); // always uses forward slashes
1089 1090 1091 1092 1093 1094
					if (folderPath && folderPath !== '.') {
						folderPath = './' + folderPath;
					}
				} else {
					const owningFolder = this.contextService.getWorkspaceFolder(resource);
					if (owningFolder) {
1095
						const owningRootName = owningFolder.name;
1096 1097

						// If this root is the only one with its basename, use a relative ./ path. If there is another, use an absolute path
1098
						const isUniqueFolder = workspace.folders.filter(folder => folder.name === owningRootName).length === 1;
1099
						if (isUniqueFolder) {
1100 1101
							const relPath = relativePath(owningFolder.uri, resource); // always uses forward slashes
							if (relPath === '') {
1102 1103 1104 1105
								folderPath = `./${owningFolder.name}`;
							} else {
								folderPath = `./${owningFolder.name}/${relativePath}`;
							}
1106
						} else {
1107
							folderPath = resource.fsPath; // TODO rob: handle on-file URIs
1108
						}
1109 1110
					}
				}
1111 1112 1113 1114 1115

				if (folderPath) {
					folderPaths.push(folderPath);
				}
			});
S
Sandeep Somavarapu 已提交
1116 1117
		}

1118
		if (!folderPaths.length || folderPaths.some(folderPath => folderPath === '.')) {
S
Sandeep Somavarapu 已提交
1119 1120 1121 1122 1123
			this.inputPatternIncludes.setValue('');
			this.searchWidget.focus();
			return;
		}

1124
		// Show 'files to include' box
E
Erich Gamma 已提交
1125
		if (!this.showsFileTypes()) {
1126
			this.toggleQueryDetails(true, true);
E
Erich Gamma 已提交
1127
		}
B
Benjamin Pasero 已提交
1128

1129
		this.inputPatternIncludes.setValue(folderPaths.join(', '));
B
Benjamin Pasero 已提交
1130
		this.searchWidget.focus(false);
E
Erich Gamma 已提交
1131 1132
	}

R
Rob Lourens 已提交
1133
	onQueryChanged(preserveFocus?: boolean): void {
1134 1135 1136 1137
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1138 1139 1140
		const excludePatternText = this.inputPatternExcludes.getValue().trim();
		const includePatternText = this.inputPatternIncludes.getValue().trim();
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
E
Erich Gamma 已提交
1141 1142 1143 1144 1145

		if (contentPattern.length === 0) {
			return;
		}

1146
		const content: IPatternInfo = {
E
Erich Gamma 已提交
1147 1148 1149
			pattern: contentPattern,
			isRegExp: isRegex,
			isCaseSensitive: isCaseSensitive,
R
Rob Lourens 已提交
1150
			isWordMatch: isWholeWords
E
Erich Gamma 已提交
1151 1152
		};

1153 1154
		const excludePattern = this.inputPatternExcludes.getValue();
		const includePattern = this.inputPatternIncludes.getValue();
E
Erich Gamma 已提交
1155

R
Rob Lourens 已提交
1156 1157 1158
		// Need the full match line to correctly calculate replace text, if this is a search/replace with regex group references ($1, $2, ...).
		// 10000 chars is enough to avoid sending huge amounts of text around, if you do a replace with a longer match, it may or may not resolve the group refs correctly.
		// https://github.com/Microsoft/vscode/issues/58374
R
Rob Lourens 已提交
1159
		const charsPerLine = content.isRegExp ? 10000 :
1160
			250;
R
Rob Lourens 已提交
1161

1162
		const options: ITextQueryBuilderOptions = {
R
Rob Lourens 已提交
1163
			_reason: 'searchView',
1164
			extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService),
1165
			maxResults: SearchView.MAX_TEXT_RESULTS,
R
Rob Lourens 已提交
1166 1167
			disregardIgnoreFiles: !useExcludesAndIgnoreFiles || undefined,
			disregardExcludeSettings: !useExcludesAndIgnoreFiles || undefined,
R
Rob Lourens 已提交
1168
			excludePattern,
1169 1170
			includePattern,
			previewOptions: {
R
Rob Lourens 已提交
1171 1172
				matchLines: 1,
				charsPerLine
R
Rob Lourens 已提交
1173
			},
1174 1175
			isSmartCase: this.configurationService.getValue<ISearchConfiguration>().search.smartCase,
			expandPatterns: true
E
Erich Gamma 已提交
1176
		};
S
Sandeep Somavarapu 已提交
1177
		const folderResources = this.contextService.getWorkspace().folders;
1178 1179 1180 1181 1182 1183

		const onQueryValidationError = (err: Error) => {
			this.searchWidget.searchInput.showMessage({ content: err.message, type: MessageType.ERROR });
			this.viewModel.searchResult.clear();
		};

1184
		let query: ITextQuery;
1185
		try {
1186
			query = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
1187 1188
		} catch (err) {
			onQueryValidationError(err);
1189
			return;
1190
		}
1191

1192
		this.validateQuery(query).then(() => {
1193
			this.onQueryTriggered(query, options, excludePatternText, includePatternText);
1194

1195 1196 1197
			if (!preserveFocus) {
				this.searchWidget.focus(false); // focus back to input field
			}
1198
		}, onQueryValidationError);
1199 1200
	}

J
Johannes Rieken 已提交
1201
	private validateQuery(query: ITextQuery): Promise<void> {
1202 1203 1204 1205 1206 1207
		// Validate folderQueries
		const folderQueriesExistP =
			query.folderQueries.map(fq => {
				return this.fileService.existsFile(fq.folder);
			});

1208
		return Promise.resolve(folderQueriesExistP).then(existResults => {
R
Rob Lourens 已提交
1209 1210
			// If no folders exist, show an error message about the first one
			const existingFolderQueries = query.folderQueries.filter((folderQuery, i) => existResults[i]);
1211
			if (!query.folderQueries.length || existingFolderQueries.length) {
R
Rob Lourens 已提交
1212 1213 1214
				query.folderQueries = existingFolderQueries;
			} else {
				const nonExistantPath = query.folderQueries[0].folder.fsPath;
1215
				const searchPathNotFoundError = nls.localize('searchPathNotFoundError', "Search path not found: {0}", nonExistantPath);
1216
				return Promise.reject(new Error(searchPathNotFoundError));
1217 1218 1219 1220
			}

			return undefined;
		});
E
Erich Gamma 已提交
1221 1222
	}

1223
	private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): void {
R
Rob Lourens 已提交
1224
		this.searchWidget.searchInput.onSearchSubmit();
1225
		this.inputPatternExcludes.onSearchSubmit();
1226 1227
		this.inputPatternIncludes.onSearchSubmit();

S
Sandeep Somavarapu 已提交
1228 1229
		this.viewModel.cancelSearch();

1230 1231
		this.currentSearchQ = this.currentSearchQ
			.then(() => this.doSearch(query, options, excludePatternText, includePatternText))
J
Johannes Rieken 已提交
1232
			.then(() => undefined, () => undefined);
1233 1234 1235
	}

	private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable<void> {
1236
		const progressRunner = this.progressService.show(/*infinite=*/true);
E
Erich Gamma 已提交
1237

1238
		this.searchWidget.searchInput.clearMessage();
1239 1240 1241
		this.searching = true;
		setTimeout(() => {
			if (this.searching) {
1242 1243
				this.updateActions();
				this.updateTitleArea();
1244 1245
			}
		}, 2000);
E
Erich Gamma 已提交
1246 1247
		this.showEmptyStage();

1248
		const onComplete = (completed?: ISearchComplete) => {
1249
			this.searching = false;
1250 1251

			// Complete up to 100% as needed
1252
			progressRunner.done();
E
Erich Gamma 已提交
1253

1254
			// Do final render, then expand if just 1 file with less than 50 matches
R
Rob Lourens 已提交
1255
			this.onSearchResultsChanged();
1256 1257 1258

			const collapseResults = this.configurationService.getValue<ISearchConfigurationProperties>('search').collapseResults;
			if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) {
R
Rob Lourens 已提交
1259 1260 1261
				const onlyMatch = this.viewModel.searchResult.matches()[0];
				if (onlyMatch.count() < 50) {
					this.tree.expand(onlyMatch);
1262
				}
R
Rob Lourens 已提交
1263
			}
1264

R
Rob Lourens 已提交
1265
			this.viewModel.replaceString = this.searchWidget.getReplaceValue();
E
Erich Gamma 已提交
1266

R
Rob Lourens 已提交
1267 1268 1269
			this.searchSubmitted = true;
			this.updateActions();
			this.updateTitleArea();
1270
			const hasResults = !this.viewModel.searchResult.isEmpty();
E
Erich Gamma 已提交
1271

R
Rob Lourens 已提交
1272 1273 1274 1275 1276 1277
			if (completed && completed.limitHit) {
				this.searchWidget.searchInput.showMessage({
					content: nls.localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results."),
					type: MessageType.WARNING
				});
			}
E
Erich Gamma 已提交
1278

R
Rob Lourens 已提交
1279
			if (!hasResults) {
1280 1281
				const hasExcludes = !!excludePatternText;
				const hasIncludes = !!includePatternText;
R
Rob Lourens 已提交
1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292
				let message: string;

				if (!completed) {
					message = nls.localize('searchCanceled', "Search was canceled before any results could be found - ");
				} else if (hasIncludes && hasExcludes) {
					message = nls.localize('noResultsIncludesExcludes', "No results found in '{0}' excluding '{1}' - ", includePatternText, excludePatternText);
				} else if (hasIncludes) {
					message = nls.localize('noResultsIncludes', "No results found in '{0}' - ", includePatternText);
				} else if (hasExcludes) {
					message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText);
				} else {
R
Rob Lourens 已提交
1293
					message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
R
Rob Lourens 已提交
1294
				}
1295

R
Rob Lourens 已提交
1296 1297
				// Indicate as status to ARIA
				aria.status(message);
E
Erich Gamma 已提交
1298

R
Rob Lourens 已提交
1299
				dom.hide(this.resultsElement);
E
Erich Gamma 已提交
1300

R
Rob Lourens 已提交
1301 1302
				const messageEl = this.clearMessage();
				const p = dom.append(messageEl, $('p', undefined, message));
E
Erich Gamma 已提交
1303

R
Rob Lourens 已提交
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313
				if (!completed) {
					const searchAgainLink = dom.append(p, $('a.pointer.prominent', undefined, nls.localize('rerunSearch.message', "Search again")));
					this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => {
						dom.EventHelper.stop(e, false);
						this.onQueryChanged();
					}));
				} else if (hasIncludes || hasExcludes) {
					const searchAgainLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('rerunSearchInAll.message', "Search again in all files")));
					this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => {
						dom.EventHelper.stop(e, false);
1314

R
Rob Lourens 已提交
1315 1316
						this.inputPatternExcludes.setValue('');
						this.inputPatternIncludes.setValue('');
1317

R
Rob Lourens 已提交
1318 1319
						this.onQueryChanged();
					}));
1320
				} else {
R
Rob Lourens 已提交
1321 1322 1323 1324 1325 1326
					const openSettingsLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.message', "Open Settings")));
					this.addClickEvents(openSettingsLink, this.onOpenSettings);
				}

				if (completed) {
					dom.append(p, $('span', undefined, ' - '));
1327

R
Rob Lourens 已提交
1328 1329
					const learnMoreLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.learnMore', "Learn More")));
					this.addClickEvents(learnMoreLink, this.onLearnMore);
1330
				}
R
Rob Lourens 已提交
1331 1332 1333 1334 1335 1336 1337 1338 1339 1340

				if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
					this.showSearchWithoutFolderMessage();
				}
			} else {
				this.viewModel.searchResult.toggleHighlights(this.isVisible()); // show highlights

				// Indicate final search result count for ARIA
				aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(), this.viewModel.searchResult.fileCount()));
			}
E
Erich Gamma 已提交
1341 1342
		};

1343
		const onError = (e: any) => {
1344
			if (errors.isPromiseCanceledError(e)) {
1345
				return onComplete(null);
1346
			} else {
1347
				this.searching = false;
1348 1349
				this.updateActions();
				this.updateTitleArea();
1350
				progressRunner.done();
1351
				this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR });
1352
				this.viewModel.searchResult.clear();
1353

1354
				if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
1355
					this.showPcre2Hint();
1356
				}
1357 1358

				return Promise.resolve();
E
Erich Gamma 已提交
1359 1360 1361 1362 1363 1364
			}
		};

		let visibleMatches = 0;

		// Handle UI updates in an interval to show frequent progress and results
1365
		const uiRefreshHandle: any = setInterval(() => {
1366
			if (!this.searching) {
E
Erich Gamma 已提交
1367 1368 1369 1370 1371
				window.clearInterval(uiRefreshHandle);
				return;
			}

			// Search result tree update
1372 1373 1374
			const fileCount = this.viewModel.searchResult.fileCount();
			if (visibleMatches !== fileCount) {
				visibleMatches = fileCount;
R
Rob Lourens 已提交
1375
				this.refreshAndUpdateCount();
1376
			}
1377
			if (fileCount > 0) {
S
Sandeep Somavarapu 已提交
1378
				this.updateActions();
E
Erich Gamma 已提交
1379
			}
1380
		}, 100);
E
Erich Gamma 已提交
1381

S
Sandeep Somavarapu 已提交
1382
		this.searchWidget.setReplaceAllActionState(false);
1383

1384
		return this.viewModel.search(query)
1385
			.then(onComplete, onError);
E
Erich Gamma 已提交
1386 1387
	}

1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404
	private showPcre2Hint(): void {
		if (!this.globalMemento['disablePcre2Hint']) {
			// If the regex parsed in JS but not rg, it likely uses features that are supported in JS and PCRE2 but not Rust
			this.notificationService.prompt(Severity.Info, nls.localize('rgRegexError', "You can enable \"search.usePCRE2\" to enable some extra regex features like lookbehind and backreferences."), [
				{
					label: nls.localize('neverAgain', "Don't Show Again"),
					run: () => this.globalMemento['disablePcre2Hint'] = true,
					isSecondary: true
				},
				{
					label: nls.localize('otherEncodingWarning.openSettingsLabel', "Open Settings"),
					run: () => this.openSettings('search.usePCRE2')
				}
			]);
		}
	}

1405 1406
	private addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => {
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.CLICK, handler));
1407
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.KEY_DOWN, e => {
1408
			const event = new StandardKeyboardEvent(e);
1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426
			let eventHandled = true;

			if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
				handler(e);
			} else {
				eventHandled = false;
			}

			if (eventHandled) {
				event.preventDefault();
				event.stopPropagation();
			}
		}));
	}

	private onOpenSettings = (e: dom.EventLike): void => {
		dom.EventHelper.stop(e, false);

1427 1428 1429
		this.openSettings('.exclude');
	}

J
Johannes Rieken 已提交
1430
	private openSettings(query: string): Promise<IEditor> {
1431 1432
		const options: ISettingsEditorOptions = { query };
		return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ?
1433 1434 1435 1436 1437 1438 1439 1440 1441 1442
			this.preferencesService.openWorkspaceSettings(undefined, options) :
			this.preferencesService.openGlobalSettings(undefined, options);
	}

	private onLearnMore = (e: MouseEvent): void => {
		dom.EventHelper.stop(e, false);

		window.open('https://go.microsoft.com/fwlink/?linkid=853977');
	}

1443
	private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void {
1444
		const fileCount = this.viewModel.searchResult.fileCount();
1445 1446
		this.hasSearchResultsKey.set(fileCount > 0);

1447
		const msgWasHidden = this.messagesElement.style.display === 'none';
1448
		if (fileCount > 0) {
1449
			const messageEl = this.clearMessage();
1450 1451 1452 1453 1454 1455
			let resultMsg = this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount);
			if (disregardExcludesAndIgnores) {
				resultMsg += nls.localize('useIgnoresAndExcludesDisabled', " - exclude settings and ignore files are disabled");
			}

			dom.append(messageEl, $('p', undefined, resultMsg));
R
Rob Lourens 已提交
1456
			this.reLayout();
1457
		} else if (!msgWasHidden) {
1458
			dom.hide(this.messagesElement);
1459 1460 1461
		}
	}

1462 1463 1464 1465 1466 1467 1468
	private buildResultCountMessage(resultCount: number, fileCount: number): string {
		if (resultCount === 1 && fileCount === 1) {
			return nls.localize('search.file.result', "{0} result in {1} file", resultCount, fileCount);
		} else if (resultCount === 1) {
			return nls.localize('search.files.result', "{0} result in {1} files", resultCount, fileCount);
		} else if (fileCount === 1) {
			return nls.localize('search.file.results', "{0} results in {1} file", resultCount, fileCount);
1469
		} else {
1470
			return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
1471 1472 1473
		}
	}

1474 1475 1476 1477
	private showSearchWithoutFolderMessage(): void {
		this.searchWithoutFolderMessageElement = this.clearMessage();

		const textEl = dom.append(this.searchWithoutFolderMessageElement,
1478
			$('p', undefined, nls.localize('searchWithoutFolder', "You have not yet opened a folder. Only open files are currently searched - ")));
1479 1480

		const openFolderLink = dom.append(textEl,
1481
			$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder")));
1482 1483 1484 1485 1486 1487

		this.messageDisposables.push(dom.addDisposableListener(openFolderLink, dom.EventType.CLICK, (e: MouseEvent) => {
			dom.EventHelper.stop(e, false);

			const actionClass = env.isMacintosh ? OpenFileFolderAction : OpenFolderAction;
			const action = this.instantiationService.createInstance<string, string, IAction>(actionClass, actionClass.ID, actionClass.LABEL);
1488
			this.actionRunner.run(action).then(() => {
1489 1490 1491 1492
				action.dispose();
			}, err => {
				action.dispose();
				errors.onUnexpectedError(err);
1493
			});
1494
		}));
1495 1496
	}

E
Erich Gamma 已提交
1497
	private showEmptyStage(): void {
1498

E
Erich Gamma 已提交
1499
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1500 1501
		this.searchSubmitted = false;
		this.updateActions();
E
Erich Gamma 已提交
1502 1503

		// clean up ui
S
Sandeep Somavarapu 已提交
1504
		// this.replaceService.disposeAllReplacePreviews();
1505
		dom.hide(this.messagesElement);
1506
		dom.show(this.resultsElement);
S
Sandeep Somavarapu 已提交
1507
		this.currentSelectedFileMatch = null;
E
Erich Gamma 已提交
1508 1509
	}

J
Johannes Rieken 已提交
1510
	private onFocus(lineMatch: any, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
E
Erich Gamma 已提交
1511
		if (!(lineMatch instanceof Match)) {
1512
			this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange();
1513
			return Promise.resolve(true);
E
Erich Gamma 已提交
1514 1515
		}

1516 1517
		const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
		return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ?
R
Rob Lourens 已提交
1518 1519
			this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) :
			this.open(lineMatch, preserveFocus, sideBySide, pinned);
1520 1521
	}

R
Rob Lourens 已提交
1522
	open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
R
Rob Lourens 已提交
1523 1524
		const selection = this.getSelectionFrom(element);
		const resource = element instanceof Match ? element.parent().resource() : (<FileMatch>element).resource();
E
Erich Gamma 已提交
1525
		return this.editorService.openEditor({
1526
			resource: resource,
E
Erich Gamma 已提交
1527
			options: {
1528 1529
				preserveFocus,
				pinned,
S
Sandeep Somavarapu 已提交
1530
				selection,
1531
				revealIfVisible: true
E
Erich Gamma 已提交
1532
			}
B
Benjamin Pasero 已提交
1533
		}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
S
Sandeep Somavarapu 已提交
1534
			if (editor && element instanceof Match && preserveFocus) {
1535 1536 1537 1538
				this.viewModel.searchResult.rangeHighlightDecorations.highlightRange(
					(<ICodeEditor>editor.getControl()).getModel(),
					element.range()
				);
S
Sandeep Somavarapu 已提交
1539
			} else {
1540
				this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange();
S
Sandeep Somavarapu 已提交
1541
			}
R
Rob Lourens 已提交
1542

R
Rob Lourens 已提交
1543 1544 1545
			if (editor) {
				return this.editorGroupsService.activateGroup(editor.group);
			} else {
1546
				return Promise.resolve(null);
R
Rob Lourens 已提交
1547
			}
S
Sandeep Somavarapu 已提交
1548
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
1549 1550
	}

1551
	private getSelectionFrom(element: FileMatchOrMatch): any {
1552
		let match: Match | null = null;
1553
		if (element instanceof Match) {
J
Johannes Rieken 已提交
1554
			match = element;
1555 1556
		}
		if (element instanceof FileMatch && element.count() > 0) {
J
Johannes Rieken 已提交
1557
			match = element.matches()[element.matches().length - 1];
1558 1559
		}
		if (match) {
1560
			const range = match.range();
S
Sandeep Somavarapu 已提交
1561
			if (this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) {
1562
				const replaceString = match.replaceString;
1563 1564
				return {
					startLineNumber: range.startLineNumber,
S
Sandeep Somavarapu 已提交
1565
					startColumn: range.startColumn,
1566
					endLineNumber: range.startLineNumber,
S
Sandeep Somavarapu 已提交
1567
					endColumn: range.startColumn + replaceString.length
1568 1569 1570 1571
				};
			}
			return range;
		}
R
Rob Lourens 已提交
1572
		return undefined;
1573 1574
	}

B
Benjamin Pasero 已提交
1575
	private onUntitledDidChangeDirty(resource: URI): void {
E
Erich Gamma 已提交
1576 1577 1578 1579
		if (!this.viewModel) {
			return;
		}

1580
		// remove search results from this resource as it got disposed
B
Benjamin Pasero 已提交
1581
		if (!this.untitledEditorService.isDirty(resource)) {
1582
			const matches = this.viewModel.searchResult.matches();
1583
			for (let i = 0, len = matches.length; i < len; i++) {
B
Benjamin Pasero 已提交
1584
				if (resource.toString() === matches[i].resource().toString()) {
1585 1586
					this.viewModel.searchResult.remove(matches[i]);
				}
E
Erich Gamma 已提交
1587 1588 1589 1590 1591
			}
		}
	}

	private onFilesChanged(e: FileChangesEvent): void {
B
Benjamin Pasero 已提交
1592
		if (!this.viewModel || !e.gotDeleted()) {
E
Erich Gamma 已提交
1593 1594 1595
			return;
		}

1596
		const matches = this.viewModel.searchResult.matches();
E
Erich Gamma 已提交
1597 1598 1599

		for (let i = 0, len = matches.length; i < len; i++) {
			if (e.contains(matches[i].resource(), FileChangeType.DELETED)) {
1600
				this.viewModel.searchResult.remove(matches[i]);
E
Erich Gamma 已提交
1601 1602 1603 1604
			}
		}
	}

R
Rob Lourens 已提交
1605
	getActions(): IAction[] {
1606 1607 1608 1609 1610 1611
		return [
			this.searching ?
				this.cancelAction :
				this.refreshAction,
			...this.actions
		];
1612 1613
	}

1614
	private clearHistory(): void {
1615 1616 1617 1618 1619
		this.searchWidget.clearHistory();
		this.inputPatternExcludes.clearHistory();
		this.inputPatternIncludes.clearHistory();
	}

B
Benjamin Pasero 已提交
1620
	protected saveState(): void {
A
Amy Qiu 已提交
1621 1622 1623 1624
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1625
		const patternExcludes = this.inputPatternExcludes.getValue().trim();
A
Amy Qiu 已提交
1626
		const patternIncludes = this.inputPatternIncludes.getValue().trim();
1627
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
A
Amy Qiu 已提交
1628

B
Benjamin Pasero 已提交
1629 1630 1631 1632 1633 1634 1635
		this.viewletState['query.contentPattern'] = contentPattern;
		this.viewletState['query.regex'] = isRegex;
		this.viewletState['query.wholeWords'] = isWholeWords;
		this.viewletState['query.caseSensitive'] = isCaseSensitive;
		this.viewletState['query.folderExclusions'] = patternExcludes;
		this.viewletState['query.folderIncludes'] = patternIncludes;
		this.viewletState['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles;
A
Amy Qiu 已提交
1636

B
Benjamin Pasero 已提交
1637 1638
		const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
		this.viewletState['view.showReplace'] = isReplaceShown;
1639
		this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue();
B
Benjamin Pasero 已提交
1640

1641 1642
		const history: ISearchHistoryValues = Object.create(null);

1643
		const searchHistory = this.searchWidget.getSearchHistory();
1644 1645 1646 1647
		if (searchHistory && searchHistory.length) {
			history.search = searchHistory;
		}

1648
		const replaceHistory = this.searchWidget.getReplaceHistory();
1649 1650 1651 1652
		if (replaceHistory && replaceHistory.length) {
			history.replace = replaceHistory;
		}

1653
		const patternExcludesHistory = this.inputPatternExcludes.getHistory();
1654 1655 1656 1657
		if (patternExcludesHistory && patternExcludesHistory.length) {
			history.exclude = patternExcludesHistory;
		}

1658
		const patternIncludesHistory = this.inputPatternIncludes.getHistory();
1659 1660 1661
		if (patternIncludesHistory && patternIncludesHistory.length) {
			history.include = patternIncludesHistory;
		}
1662

1663
		this.searchHistoryService.save(history);
B
Benjamin Pasero 已提交
1664 1665

		super.saveState();
A
Amy Qiu 已提交
1666 1667
	}

R
Rob Lourens 已提交
1668
	dispose(): void {
E
Erich Gamma 已提交
1669 1670 1671 1672
		this.isDisposed = true;

		super.dispose();
	}
1673 1674 1675
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
B
Benjamin Pasero 已提交
1676
	const matchHighlightColor = theme.getColor(editorFindMatchHighlight);
1677
	if (matchHighlightColor) {
I
isidor 已提交
1678
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { background-color: ${matchHighlightColor}; }`);
B
Benjamin Pasero 已提交
1679 1680 1681 1682
	}

	const diffInsertedColor = theme.getColor(diffInserted);
	if (diffInsertedColor) {
I
isidor 已提交
1683
		collector.addRule(`.monaco-workbench .search-view .replaceMatch { background-color: ${diffInsertedColor}; }`);
B
Benjamin Pasero 已提交
1684 1685 1686 1687
	}

	const diffRemovedColor = theme.getColor(diffRemoved);
	if (diffRemovedColor) {
I
isidor 已提交
1688
		collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { background-color: ${diffRemovedColor}; }`);
B
Benjamin Pasero 已提交
1689 1690 1691 1692
	}

	const diffInsertedOutlineColor = theme.getColor(diffInsertedOutline);
	if (diffInsertedOutlineColor) {
1693
		collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffInsertedOutlineColor}; }`);
B
Benjamin Pasero 已提交
1694 1695 1696 1697
	}

	const diffRemovedOutlineColor = theme.getColor(diffRemovedOutline);
	if (diffRemovedOutlineColor) {
1698
		collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffRemovedOutlineColor}; }`);
B
Benjamin Pasero 已提交
1699 1700
	}

1701 1702
	const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
	if (findMatchHighlightBorder) {
1703
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`);
1704
	}
1705 1706 1707

	const outlineSelectionColor = theme.getColor(listActiveSelectionForeground);
	if (outlineSelectionColor) {
1708
		collector.addRule(`.monaco-workbench .search-view .monaco-list.element-focused .monaco-list-row.focused.selected:not(.highlighted) .action-label:focus { outline-color: ${outlineSelectionColor} }`);
1709
	}
1710
});