searchView.ts 66.3 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 dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
7
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
8
import * as aria from 'vs/base/browser/ui/aria/aria';
9
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
R
Rob Lourens 已提交
10 11
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree';
12 13 14
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 已提交
15
import { Event } from 'vs/base/common/event';
R
Rob Lourens 已提交
16
import { Iterator } from 'vs/base/common/iterator';
S
Sandeep Somavarapu 已提交
17
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
18
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
19 20
import * as env from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
21
import { URI } from 'vs/base/common/uri';
22
import 'vs/css!./media/searchview';
23 24 25
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import * as nls from 'vs/nls';
26
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
R
Rob Lourens 已提交
27
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
J
Johannes Rieken 已提交
28
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
29
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
R
Rob Lourens 已提交
30
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
31 32
import { IConfirmation, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
33
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
34
import { TreeResourceNavigator2, WorkbenchObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService';
35
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
36
import { IProgressService, IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
37
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, VIEWLET_ID } from 'vs/workbench/services/search/common/search';
38
import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService';
39
import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
40 41 42
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';
43
import { ResourceLabels } from 'vs/workbench/browser/labels';
44
import { IEditor } from 'vs/workbench/common/editor';
45
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
46
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions';
47 48 49 50 51 52
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';
53
import { FileMatch, FileMatchOrMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult, FolderMatch, FolderMatchWithResource } from 'vs/workbench/contrib/search/common/searchModel';
54
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
55
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
56
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
57
import { relativePath } from 'vs/base/common/resources';
58
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
59 60
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
61
import { Memento, MementoObject } from 'vs/workbench/common/memento';
62
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
63
import { IOpenerService } from 'vs/platform/opener/common/opener';
E
Erich Gamma 已提交
64

65 66
const $ = dom.$;

67 68 69 70 71 72
enum SearchUIState {
	Idle,
	Searching,
	SlowSearch
}

J
Jackson Kearl 已提交
73
const SEARCH_CANCELLED_MESSAGE = nls.localize('searchCanceled', "Search was canceled before any results could be found - ");
74
export class SearchView extends ViewletPanel {
E
Erich Gamma 已提交
75

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

78
	private static readonly WIDE_CLASS_NAME = 'wide';
R
Rob Lourens 已提交
79
	private static readonly WIDE_VIEW_SIZE = 1000;
80
	private static readonly ACTIONS_RIGHT_CLASS_NAME = 'actions-right';
81

82
	private isDisposed = false;
83

84
	private container!: HTMLElement;
E
Erich Gamma 已提交
85
	private queryBuilder: QueryBuilder;
86
	private viewModel: SearchModel;
87
	private memento: Memento;
E
Erich Gamma 已提交
88

A
Alex Dima 已提交
89
	private viewletVisible: IContextKey<boolean>;
S
Sandeep Somavarapu 已提交
90
	private viewletFocused: IContextKey<boolean>;
91 92
	private inputBoxFocused: IContextKey<boolean>;
	private inputPatternIncludesFocused: IContextKey<boolean>;
93
	private inputPatternExclusionsFocused: IContextKey<boolean>;
94 95
	private firstMatchFocused: IContextKey<boolean>;
	private fileMatchOrMatchFocused: IContextKey<boolean>;
96
	private fileMatchOrFolderMatchFocus: IContextKey<boolean>;
97
	private fileMatchOrFolderMatchWithResourceFocus: IContextKey<boolean>;
98
	private fileMatchFocused: IContextKey<boolean>;
99
	private folderMatchFocused: IContextKey<boolean>;
100
	private matchFocused: IContextKey<boolean>;
101
	private hasSearchResultsKey: IContextKey<boolean>;
R
Rob Lourens 已提交
102

103
	private state: SearchUIState = SearchUIState.Idle;
S
Sandeep Somavarapu 已提交
104

105
	private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction> = [];
106 107
	private cancelAction: CancelSearchAction;
	private refreshAction: RefreshAction;
108
	private contextMenu: IMenu | null = null;
109

110 111
	private tree!: WorkbenchObjectTree<RenderableMatch>;
	private treeLabels!: ResourceLabels;
112
	private viewletState: MementoObject;
113
	private messagesElement!: HTMLElement;
114
	private messageDisposables: IDisposable[] = [];
115 116 117 118 119 120 121 122
	private searchWidgetsContainerElement!: HTMLElement;
	private searchWidget!: SearchWidget;
	private size!: dom.Dimension;
	private queryDetails!: HTMLElement;
	private toggleQueryDetailsButton!: HTMLElement;
	private inputPatternExcludes!: ExcludePatternInputWidget;
	private inputPatternIncludes!: PatternInputWidget;
	private resultsElement!: HTMLElement;
E
Erich Gamma 已提交
123

U
Ubuntu 已提交
124
	private currentSelectedFileMatch: FileMatch | undefined;
S
Sandeep Somavarapu 已提交
125

S
Sandeep Somavarapu 已提交
126
	private delayedRefresh: Delayer<void>;
127
	private changedWhileHidden: boolean = false;
R
Rob Lourens 已提交
128

U
Ubuntu 已提交
129
	private searchWithoutFolderMessageElement: HTMLElement | undefined;
130

J
Johannes Rieken 已提交
131
	private currentSearchQ = Promise.resolve();
132
	private addToSearchHistoryDelayer: Delayer<void>;
133

134
	constructor(
135
		options: IViewletPanelOptions,
136 137
		@IFileService private readonly fileService: IFileService,
		@IEditorService private readonly editorService: IEditorService,
138
		@IProgressService private readonly progressService: IProgressService,
139 140 141 142
		@INotificationService private readonly notificationService: INotificationService,
		@IDialogService private readonly dialogService: IDialogService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
143
		@IConfigurationService configurationService: IConfigurationService,
144 145 146 147
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@ISearchWorkbenchService private readonly searchWorkbenchService: ISearchWorkbenchService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
		@IReplaceService private readonly replaceService: IReplaceService,
148
		@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
149
		@IPreferencesService private readonly preferencesService: IPreferencesService,
150
		@IThemeService protected themeService: IThemeService,
151
		@ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService,
152
		@IContextMenuService contextMenuService: IContextMenuService,
153
		@IMenuService private readonly menuService: IMenuService,
154 155 156
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
		@IKeybindingService keybindingService: IKeybindingService,
		@IStorageService storageService: IStorageService,
157
		@IOpenerService private readonly openerService: IOpenerService
E
Erich Gamma 已提交
158
	) {
S
Sandeep Somavarapu 已提交
159
		super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService);
E
Erich Gamma 已提交
160

I
isidor 已提交
161
		this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService);
S
Sandeep Somavarapu 已提交
162
		this.viewletFocused = Constants.SearchViewFocusedKey.bindTo(contextKeyService);
163 164
		this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
		this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService);
165
		this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService);
166 167
		this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService);
		this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService);
168
		this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService);
169
		this.fileMatchOrFolderMatchWithResourceFocus = Constants.FileMatchOrFolderMatchWithResourceFocusKey.bindTo(contextKeyService);
170
		this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService);
171
		this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService);
172
		this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService);
173
		this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService);
E
Erich Gamma 已提交
174

R
Rob Lourens 已提交
175
		this.viewModel = this._register(this.searchWorkbenchService.searchModel);
E
Erich Gamma 已提交
176
		this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
177 178
		this.memento = new Memento(this.id, storageService);
		this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE);
E
Erich Gamma 已提交
179

B
Benjamin Pasero 已提交
180
		this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e)));
181
		this._register(this.untitledTextEditorService.onDidDisposeModel(e => this.onUntitledDidDispose(e)));
B
Benjamin Pasero 已提交
182
		this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
183
		this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory()));
184

185
		this.delayedRefresh = this._register(new Delayer<void>(250));
186

187 188
		this.addToSearchHistoryDelayer = this._register(new Delayer<void>(500));

189 190 191 192 193 194 195 196 197 198
		this.actions = [
			this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)),
			this._register(this.instantiationService.createInstance(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL))
		];
		this.refreshAction = this._register(this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL));
		this.cancelAction = this._register(this.instantiationService.createInstance(CancelSearchAction, CancelSearchAction.ID, CancelSearchAction.LABEL));
	}

	getContainer(): HTMLElement {
		return this.container;
E
Erich Gamma 已提交
199 200
	}

R
Rob Lourens 已提交
201
	get searchResult(): SearchResult {
R
Rob Lourens 已提交
202 203 204
		return this.viewModel && this.viewModel.searchResult;
	}

205
	private onDidChangeWorkbenchState(): void {
206 207
		if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) {
			dom.hide(this.searchWithoutFolderMessageElement);
208 209 210
		}
	}

211 212
	renderBody(parent: HTMLElement): void {
		this.container = dom.append(parent, dom.$('.search-view'));
E
Erich Gamma 已提交
213

214
		this.searchWidgetsContainerElement = dom.append(this.container, $('.search-widgets-container'));
215
		this.createSearchWidget(this.searchWidgetsContainerElement);
216

217
		const history = this.searchHistoryService.load();
B
Benjamin Pasero 已提交
218 219
		const filePatterns = this.viewletState['query.filePatterns'] || '';
		const patternExclusions = this.viewletState['query.folderExclusions'] || '';
R
Rob Lourens 已提交
220
		const patternExclusionsHistory: string[] = history.exclude || [];
B
Benjamin Pasero 已提交
221
		const patternIncludes = this.viewletState['query.folderIncludes'] || '';
R
Rob Lourens 已提交
222
		const patternIncludesHistory: string[] = history.include || [];
B
Benjamin Pasero 已提交
223 224 225
		const queryDetailsExpanded = this.viewletState['query.queryDetailsExpanded'] || '';
		const useExcludesAndIgnoreFiles = typeof this.viewletState['query.useExcludesAndIgnoreFiles'] === 'boolean' ?
			this.viewletState['query.useExcludesAndIgnoreFiles'] : true;
226

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

229 230
		// Toggle query details button
		this.toggleQueryDetailsButton = dom.append(this.queryDetails,
231
			$('.more.codicon.codicon-ellipsis', { tabindex: 0, role: 'button', title: nls.localize('moreSearch', "Toggle Search Details") }));
E
Erich Gamma 已提交
232

233 234
		this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => {
			dom.EventHelper.stop(e);
235
			this.toggleQueryDetails(!this.isScreenReaderOptimized());
236 237 238
		}));
		this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
			const event = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
239

240 241 242 243 244 245 246
			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 已提交
247

248 249 250 251
			if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
				if (this.searchWidget.isReplaceActive()) {
					this.searchWidget.focusReplaceAllAction();
				} else {
S
skprabhanjan 已提交
252
					this.searchWidget.isReplaceShown() ? this.searchWidget.replaceInput.focusOnPreserve() : this.searchWidget.focusRegexAction();
253 254 255 256
				}
				dom.EventHelper.stop(e);
			}
		}));
E
Erich Gamma 已提交
257

258 259 260 261 262
		// 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));
263

264 265 266 267
		this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, {
			ariaLabel: nls.localize('label.includes', 'Search Include Patterns'),
			history: patternIncludesHistory,
		}));
268

269
		this.inputPatternIncludes.setValue(patternIncludes);
270

271
		this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true));
272
		this.inputPatternIncludes.onCancel(() => this.cancelSearch(false));
273 274 275 276 277 278 279 280 281 282
		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,
		}));
283

284 285
		this.inputPatternExcludes.setValue(patternExclusions);
		this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles);
286

287
		this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true));
288
		this.inputPatternExcludes.onCancel(() => this.cancelSearch(false));
289
		this.inputPatternExcludes.onChangeIgnoreBox(() => this.onQueryChanged(true));
290 291
		this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused);

292
		this.messagesElement = dom.append(this.container, $('.messages'));
293
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
294
			this.showSearchWithoutFolderMessage();
295
		}
296

297
		this.createSearchResultsView(this.container);
298

299
		if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '' || queryDetailsExpanded !== '' || !useExcludesAndIgnoreFiles) {
300
			this.toggleQueryDetails(true, true, true);
301 302
		}

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

305
		this._register(this.searchWidget.searchInput.onInput(() => this.updateActions()));
J
jeanp413 已提交
306
		this._register(this.searchWidget.replaceInput.onInput(() => this.updateActions()));
307

S
Sandeep Somavarapu 已提交
308 309
		this._register(this.onDidFocus(() => this.viewletFocused.set(true)));
		this._register(this.onDidBlur(() => this.viewletFocused.set(false)));
310

311
		this._register(this.onDidChangeBodyVisibility(visible => this.onVisibilityChanged(visible)));
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
	}

	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);
		}
328 329
	}

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

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

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

342 343 344
	/**
	 * Warning: a bit expensive due to updating the view title
	 */
345
	protected updateActions(): void {
S
Sandeep Somavarapu 已提交
346 347 348
		for (const action of this.actions) {
			action.update();
		}
349 350 351 352

		this.refreshAction.update();
		this.cancelAction.update();

353
		super.updateActions();
S
Sandeep Somavarapu 已提交
354 355
	}

356
	private isScreenReaderOptimized() {
357
		const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
358 359 360 361
		const config = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
		return config === 'on' || (config === 'auto' && detected);
	}

362
	private createSearchWidget(container: HTMLElement): void {
363
		const contentPattern = this.viewletState['query.contentPattern'] || '';
364
		const replaceText = this.viewletState['query.replaceText'] || '';
365 366 367
		const isRegex = this.viewletState['query.regex'] === true;
		const isWholeWords = this.viewletState['query.wholeWords'] === true;
		const isCaseSensitive = this.viewletState['query.caseSensitive'] === true;
368
		const history = this.searchHistoryService.load();
369 370 371
		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;
372
		const preserveCase = this.viewletState['query.preserveCase'] === true;
E
Erich Gamma 已提交
373

374
		this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, <ISearchWidgetOptions>{
375
			value: contentPattern,
376
			replaceValue: replaceText,
377 378
			isRegex: isRegex,
			isCaseSensitive: isCaseSensitive,
379
			isWholeWords: isWholeWords,
380
			searchHistory: searchHistory,
381 382
			replaceHistory: replaceHistory,
			preserveCase: preserveCase
B
Benjamin Pasero 已提交
383
		}));
S
Sandeep Somavarapu 已提交
384

B
Benjamin Pasero 已提交
385
		if (showReplace) {
S
Sandeep Somavarapu 已提交
386 387 388
			this.searchWidget.toggleReplace(true);
		}

389
		this._register(this.searchWidget.onSearchSubmit(() => this.onQueryChanged()));
390
		this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus)));
391
		this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.onQueryChanged(true)));
392

393 394 395
		this._register(this.searchWidget.onDidHeightChange(() => this.reLayout()));

		this._register(this.searchWidget.onReplaceToggled(() => this.reLayout()));
B
Benjamin Pasero 已提交
396
		this._register(this.searchWidget.onReplaceStateChange((state) => {
J
Johannes Rieken 已提交
397
			this.viewModel.replaceActive = state;
R
Rob Lourens 已提交
398
			this.refreshTree();
S
Sandeep Somavarapu 已提交
399
		}));
400 401 402 403 404 405

		this._register(this.searchWidget.onPreserveCaseChange((state) => {
			this.viewModel.preserveCase = state;
			this.refreshTree();
		}));

B
Benjamin Pasero 已提交
406
		this._register(this.searchWidget.onReplaceValueChanged((value) => {
J
Johannes Rieken 已提交
407
			this.viewModel.replaceString = this.searchWidget.getReplaceValue();
R
Rob Lourens 已提交
408
			this.delayedRefresh.trigger(() => this.refreshTree());
S
Sandeep Somavarapu 已提交
409
		}));
410

B
Benjamin Pasero 已提交
411
		this._register(this.searchWidget.onBlur(() => {
S
Sandeep Somavarapu 已提交
412 413 414
			this.toggleQueryDetailsButton.focus();
		}));

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

417 418 419 420
		this.trackInputBox(this.searchWidget.searchInputFocusTracker);
		this.trackInputBox(this.searchWidget.replaceInputFocusTracker);
	}

421
	private trackInputBox(inputFocusTracker: dom.IFocusTracker, contextKey?: IContextKey<boolean>): void {
B
Benjamin Pasero 已提交
422
		this._register(inputFocusTracker.onDidFocus(() => {
423
			this.inputBoxFocused.set(true);
424 425 426
			if (contextKey) {
				contextKey.set(true);
			}
427
		}));
B
Benjamin Pasero 已提交
428
		this._register(inputFocusTracker.onDidBlur(() => {
429
			this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus()
430
				|| this.searchWidget.replaceInputHasFocus()
431 432
				|| this.inputPatternIncludes.inputHasFocus()
				|| this.inputPatternExcludes.inputHasFocus());
433 434 435
			if (contextKey) {
				contextKey.set(false);
			}
436
		}));
S
Sandeep Somavarapu 已提交
437 438
	}

R
Rob Lourens 已提交
439
	private onSearchResultsChanged(event?: IChangeEvent): void {
440 441 442 443 444 445 446
		if (this.isVisible()) {
			return this.refreshAndUpdateCount(event);
		} else {
			this.changedWhileHidden = true;
		}
	}

R
Rob Lourens 已提交
447
	private refreshAndUpdateCount(event?: IChangeEvent): void {
R
Rob Lourens 已提交
448
		this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty());
449
		this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles);
R
Rob Lourens 已提交
450
		return this.refreshTree(event);
451 452
	}

R
Rob Lourens 已提交
453
	refreshTree(event?: IChangeEvent): void {
454
		const collapseResults = this.configurationService.getValue<ISearchConfigurationProperties>('search').collapseResults;
455
		if (!event || event.added || event.removed) {
456
			// Refresh whole tree
457
			this.tree.setChildren(null, this.createResultIterator(collapseResults));
458
		} else {
459
			// FileMatch modified, refresh those elements
R
Rob Lourens 已提交
460
			event.elements.forEach(element => {
461 462
				this.tree.setChildren(element, this.createIterator(element, collapseResults));
				this.tree.rerender(element);
R
Rob Lourens 已提交
463
			});
464 465 466
		}
	}

467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
	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 };
		});
	}

483
	private createFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
		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 }));
	}

508
	private createIterator(match: FolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
509
		return match instanceof SearchResult ? this.createResultIterator(collapseResults) :
510
			match instanceof FolderMatch ? this.createFolderIterator(match, collapseResults) :
511 512 513
				this.createFileIterator(match);
	}

514
	private replaceAll(): void {
515
		if (this.viewModel.searchResult.count() === 0) {
S
Sandeep Somavarapu 已提交
516 517 518
			return;
		}

519 520 521 522
		const occurrences = this.viewModel.searchResult.count();
		const fileCount = this.viewModel.searchResult.fileCount();
		const replaceValue = this.searchWidget.getReplaceValue() || '';
		const afterReplaceAllMessage = this.buildAfterReplaceAllMessage(occurrences, fileCount, replaceValue);
523

B
Benjamin Pasero 已提交
524 525 526 527 528 529 530 531
		let progressComplete: () => void;
		let progressReporter: IProgress<IProgressStep>;
		this.progressService.withProgress({ location: VIEWLET_ID, delay: 100, total: occurrences }, p => {
			progressReporter = p;

			return new Promise(resolve => progressComplete = resolve);
		});

532
		const confirmation: IConfirmation = {
533
			title: nls.localize('replaceAll.confirmation.title', "Replace All"),
534
			message: this.buildReplaceAllConfirmationMessage(occurrences, fileCount, replaceValue),
535
			primaryButton: nls.localize('replaceAll.confirm.button', "&&Replace"),
B
Benjamin Pasero 已提交
536
			type: 'question'
537 538
		};

539
		this.dialogService.confirm(confirmation).then(res => {
540
			if (res.confirmed) {
541
				this.searchWidget.setReplaceAllActionState(false);
B
Benjamin Pasero 已提交
542 543
				this.viewModel.searchResult.replaceAll(progressReporter).then(() => {
					progressComplete();
544
					const messageEl = this.clearMessage();
545
					dom.append(messageEl, $('p', undefined, afterReplaceAllMessage));
546
				}, (error) => {
B
Benjamin Pasero 已提交
547
					progressComplete();
548
					errors.isPromiseCanceledError(error);
549
					this.notificationService.error(error);
550 551 552
				});
			}
		});
553 554
	}

555 556 557 558 559 560 561
	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);
				}

562
				return nls.localize('removeAll.occurrence.file.message', "Replaced {0} occurrence across {1} file.", occurrences, fileCount);
563 564 565 566 567 568 569 570 571 572 573 574 575 576
			}

			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);
			}

577
			return nls.localize('removeAll.occurrences.file.message', "Replaced {0} occurrences across {1} file.", occurrences, fileCount);
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
		}

		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);
				}

594
				return nls.localize('replaceAll.occurrence.file.confirmation.message', "Replace {0} occurrence across {1} file?", occurrences, fileCount);
595 596 597 598 599 600 601 602 603 604 605 606 607 608
			}

			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);
			}

609
			return nls.localize('replaceAll.occurrences.file.confirmation.message', "Replace {0} occurrences across {1} file?", occurrences, fileCount);
610 611 612 613 614 615 616 617 618
		}

		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);
	}

619
	private clearMessage(): HTMLElement {
R
Rob Lourens 已提交
620
		this.searchWithoutFolderMessageElement = undefined;
621

622 623 624 625 626
		dom.clearNode(this.messagesElement);
		dom.show(this.messagesElement);
		dispose(this.messageDisposables);
		this.messageDisposables = [];

627
		return dom.append(this.messagesElement, $('.message'));
628 629
	}

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

634 635 636 637 638 639
		const identityProvider: IIdentityProvider<RenderableMatch> = {
			getId(element: RenderableMatch) {
				return element.id();
			}
		};

640
		this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }));
641
		this.tree = this._register(this.instantiationService.createInstance(WorkbenchObjectTree,
J
Joao Moreno 已提交
642
			'SearchView',
R
Rob Lourens 已提交
643 644 645
			this.resultsElement,
			delegate,
			[
B
Benjamin Pasero 已提交
646 647
				this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)),
				this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)),
648
				this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)),
R
Rob Lourens 已提交
649
			],
650
			{
651
				identityProvider,
652
				accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider, this.viewModel),
R
Rob Lourens 已提交
653 654
				dnd: this.instantiationService.createInstance(SearchDND),
				multipleSelectionSupport: false
B
Benjamin Pasero 已提交
655
			}));
R
Rob Lourens 已提交
656
		this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
R
Rob Lourens 已提交
657

R
Rob Lourens 已提交
658
		const resourceNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: true, openOnSelection: false }));
J
Joao Moreno 已提交
659
		this._register(Event.debounce(resourceNavigator.onDidOpenResource, (last, event) => event, 75, true)(options => {
660
			if (options.element instanceof Match) {
661
				const selectedMatch: Match = options.element;
662 663
				if (this.currentSelectedFileMatch) {
					this.currentSelectedFileMatch.setSelectedMatch(null);
664
				}
665 666
				this.currentSelectedFileMatch = selectedMatch.parent();
				this.currentSelectedFileMatch.setSelectedMatch(selectedMatch);
R
Rob Lourens 已提交
667

R
Rob Lourens 已提交
668
				this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned);
669 670
			}
		}));
S
Sandeep Somavarapu 已提交
671

J
Joao Moreno 已提交
672
		this._register(Event.any<any>(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => {
673
			if (this.tree.isDOMFocused()) {
R
Rob Lourens 已提交
674 675
				const focus = this.tree.getFocus()[0];
				this.firstMatchFocused.set(this.tree.navigate().first() === focus);
676 677 678 679 680
				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);
681
				this.fileMatchOrFolderMatchWithResourceFocus.set(focus instanceof FileMatch || focus instanceof FolderMatchWithResource);
682 683 684 685 686 687 688 689 690 691
			}
		}));

		this._register(this.tree.onDidBlur(e => {
			this.firstMatchFocused.reset();
			this.fileMatchOrMatchFocused.reset();
			this.fileMatchFocused.reset();
			this.folderMatchFocused.reset();
			this.matchFocused.reset();
			this.fileMatchOrFolderMatchFocus.reset();
692
			this.fileMatchOrFolderMatchWithResourceFocus.reset();
693
		}));
E
Erich Gamma 已提交
694 695
	}

M
Matt Bierner 已提交
696
	private onContextMenu(e: ITreeContextMenuEvent<RenderableMatch | null>): void {
R
Rob Lourens 已提交
697 698 699 700 701 702 703
		if (!this.contextMenu) {
			this.contextMenu = this._register(this.menuService.createMenu(MenuId.SearchContext, this.contextKeyService));
		}

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

704 705 706
		const actions: IAction[] = [];
		const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);

R
Rob Lourens 已提交
707 708
		this.contextMenuService.showContextMenu({
			getAnchor: () => e.anchor,
709 710 711
			getActions: () => actions,
			getActionsContext: () => e.element,
			onHide: () => dispose(actionsDisposable)
R
Rob Lourens 已提交
712 713 714
		});
	}

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

R
Rob Lourens 已提交
718
		// Expand the initial selected node, if needed
J
jeanp413 已提交
719
		if (selected && !(selected instanceof Match)) {
R
Rob Lourens 已提交
720 721
			if (this.tree.isCollapsed(selected)) {
				this.tree.expand(selected);
R
Rob Lourens 已提交
722 723 724
			}
		}

R
Rob Lourens 已提交
725
		let navigator = this.tree.navigate(selected);
R
Rob Lourens 已提交
726 727

		let next = navigator.next();
728
		if (!next) {
R
Rob Lourens 已提交
729
			next = navigator.first();
730 731
		}

J
jeanp413 已提交
732
		// Expand until first child is a Match
R
Rob Lourens 已提交
733
		while (!(next instanceof Match)) {
R
Rob Lourens 已提交
734 735
			if (this.tree.isCollapsed(next)) {
				this.tree.expand(next);
736 737
			}

J
jeanp413 已提交
738
			// Select the first child
739
			next = navigator.next();
740 741 742
		}

		// Reveal the newly selected element
743
		if (next) {
744 745 746
			if (next === selected) {
				this.tree.setFocus([]);
			}
747
			this.tree.setFocus([next], getSelectionKeyboardEvent(undefined, false));
748 749
			this.tree.reveal(next);
		}
750 751
	}

R
Rob Lourens 已提交
752
	selectPreviousMatch(): void {
M
Matt Bierner 已提交
753
		const [selected] = this.tree.getSelection();
R
Rob Lourens 已提交
754
		let navigator = this.tree.navigate(selected);
755

756
		let prev = navigator.previous();
757

J
jeanp413 已提交
758 759 760 761
		// Select previous until find a Match or a collapsed item
		while (!prev || (!(prev instanceof Match) && !this.tree.isCollapsed(prev))) {
			prev = prev ? navigator.previous() : navigator.last();
		}
R
Rob Lourens 已提交
762

J
jeanp413 已提交
763 764 765 766 767 768
		// Expand until last child is a Match
		while (!(prev instanceof Match)) {
			const nextItem = navigator.next();
			this.tree.expand(prev);
			navigator = this.tree.navigate(nextItem); // recreate navigator because modifying the tree can invalidate it
			prev = nextItem ? navigator.previous() : navigator.last(); // select last child
769 770 771
		}

		// Reveal the newly selected element
772
		if (prev) {
773 774 775
			if (prev === selected) {
				this.tree.setFocus([]);
			}
776
			this.tree.setFocus([prev], getSelectionKeyboardEvent(undefined, false));
R
Rob Lourens 已提交
777
			this.tree.reveal(prev);
778
		}
779 780
	}

R
Rob Lourens 已提交
781
	moveFocusToResults(): void {
Y
Yogesh 已提交
782 783 784
		this.tree.domFocus();
	}

R
Rob Lourens 已提交
785
	focus(): void {
E
Erich Gamma 已提交
786 787
		super.focus();

R
Rob Lourens 已提交
788 789 790 791
		const updatedText = this.updateTextFromSelection();
		this.searchWidget.focus(undefined, undefined, updatedText);
	}

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

R
Rob Lourens 已提交
807
		return updatedText;
808 809
	}

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

		if (this.searchWidget.replaceInputHasFocus()) {
821
			this.moveFocusFromSearchOrReplace();
822 823 824 825
			return;
		}

		if (this.inputPatternIncludes.inputHasFocus()) {
826 827 828 829 830 831
			this.inputPatternExcludes.focus();
			this.inputPatternExcludes.select();
			return;
		}

		if (this.inputPatternExcludes.inputHasFocus()) {
832 833 834 835 836
			this.selectTreeIfNotSelected();
			return;
		}
	}

837 838
	private moveFocusFromSearchOrReplace() {
		if (this.showsFileTypes()) {
839
			this.toggleQueryDetails(true, this.showsFileTypes());
840 841 842 843 844
		} else {
			this.selectTreeIfNotSelected();
		}
	}

R
Rob Lourens 已提交
845
	focusPreviousInputBox(): void {
846 847 848 849 850 851 852 853 854 855 856 857 858 859
		if (this.searchWidget.searchInputHasFocus()) {
			return;
		}

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

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

860 861 862 863 864 865
		if (this.inputPatternExcludes.inputHasFocus()) {
			this.inputPatternIncludes.focus();
			this.inputPatternIncludes.select();
			return;
		}

S
Sandeep Somavarapu 已提交
866 867 868 869
		if (this.tree.isDOMFocused()) {
			this.moveFocusFromResults();
			return;
		}
870 871
	}

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

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

R
Rob Lourens 已提交
885
		const actionsPosition = this.configurationService.getValue<ISearchConfigurationProperties>('search').actionsPosition;
886 887
		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);
888

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

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

894 895 896 897
		const messagesSize = this.messagesElement.style.display === 'none' ?
			0 :
			dom.getTotalHeight(this.messagesElement);

898
		const searchResultContainerHeight = this.size.height -
899
			messagesSize -
900
			dom.getTotalHeight(this.searchWidgetsContainerElement);
901

902
		this.resultsElement.style.height = searchResultContainerHeight + 'px';
E
Erich Gamma 已提交
903

904
		this.tree.layout(searchResultContainerHeight, this.size.width);
E
Erich Gamma 已提交
905 906
	}

907 908
	protected layoutBody(height: number, width: number): void {
		this.size = new dom.Dimension(width, height);
B
Benjamin Pasero 已提交
909
		this.reLayout();
E
Erich Gamma 已提交
910 911
	}

R
Rob Lourens 已提交
912
	getControl() {
E
Erich Gamma 已提交
913 914 915
		return this.tree;
	}

916 917
	isSlowSearch(): boolean {
		return this.state === SearchUIState.SlowSearch;
918 919
	}

R
Rob Lourens 已提交
920
	allSearchFieldsClear(): boolean {
921
		return this.searchWidget.getReplaceValue() === '' &&
922
			this.searchWidget.searchInput.getValue() === '';
923 924
	}

R
Rob Lourens 已提交
925
	hasSearchResults(): boolean {
S
Sandeep Somavarapu 已提交
926 927 928
		return !this.viewModel.searchResult.isEmpty();
	}

929
	hasSearchPattern(): boolean {
930
		return this.searchWidget && this.searchWidget.searchInput.getValue().length > 0;
931 932
	}

R
Rob Lourens 已提交
933
	clearSearchResults(): void {
934
		this.viewModel.searchResult.clear();
J
Jackson Kearl 已提交
935
		this.showEmptyStage(true);
936
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
937
			this.showSearchWithoutFolderMessage();
938
		}
939
		this.searchWidget.clear();
S
Sandeep Somavarapu 已提交
940
		this.viewModel.cancelSearch();
941
		this.updateActions();
R
Rob Lourens 已提交
942 943

		aria.status(nls.localize('ariaSearchResultsClearStatus', "The search results have been cleared"));
E
Erich Gamma 已提交
944 945
	}

946
	cancelSearch(focus: boolean = true): boolean {
S
Sandeep Somavarapu 已提交
947
		if (this.viewModel.cancelSearch()) {
948
			if (focus) { this.searchWidget.focus(); }
949 950 951 952 953
			return true;
		}
		return false;
	}

954
	private selectTreeIfNotSelected(): void {
R
Rob Lourens 已提交
955
		if (this.tree.getNode(null)) {
956
			this.tree.domFocus();
957
			const selection = this.tree.getSelection();
E
Erich Gamma 已提交
958 959 960 961 962 963
			if (selection.length === 0) {
				this.tree.focusNext();
			}
		}
	}

M
Matt Bierner 已提交
964
	private getSearchTextFromEditor(allowUnselectedWord: boolean): string | null {
B
Benjamin Pasero 已提交
965
		if (!this.editorService.activeEditor) {
E
Erich Gamma 已提交
966 967 968
			return null;
		}

R
Rob Lourens 已提交
969 970 971 972
		if (dom.isAncestor(document.activeElement, this.getContainer())) {
			return null;
		}

973 974 975 976
		let activeTextEditorWidget = this.editorService.activeTextEditorWidget;
		if (isDiffEditor(activeTextEditorWidget)) {
			if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) {
				activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor();
977
			} else {
978
				activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
979 980 981
			}
		}

M
Matt Bierner 已提交
982
		if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) {
E
Erich Gamma 已提交
983 984 985
			return null;
		}

986
		const range = activeTextEditorWidget.getSelection();
R
Rob Lourens 已提交
987 988 989 990
		if (!range) {
			return null;
		}

R
Rob Lourens 已提交
991
		if (range.isEmpty() && !this.searchWidget.searchInput.getValue() && allowUnselectedWord) {
992
			const wordAtPosition = activeTextEditorWidget.getModel().getWordAtPosition(range.getStartPosition());
R
Rob Lourens 已提交
993 994 995 996 997
			if (wordAtPosition) {
				return wordAtPosition.word;
			}
		}

998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
		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;
			}

1017
			return searchText;
E
Erich Gamma 已提交
1018
		}
1019

E
Erich Gamma 已提交
1020 1021 1022 1023 1024 1025 1026
		return null;
	}

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

R
Rob Lourens 已提交
1027
	toggleCaseSensitive(): void {
S
Sandeep Somavarapu 已提交
1028
		this.searchWidget.searchInput.setCaseSensitive(!this.searchWidget.searchInput.getCaseSensitive());
1029
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1030 1031
	}

R
Rob Lourens 已提交
1032
	toggleWholeWords(): void {
S
Sandeep Somavarapu 已提交
1033
		this.searchWidget.searchInput.setWholeWords(!this.searchWidget.searchInput.getWholeWords());
1034
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1035 1036
	}

R
Rob Lourens 已提交
1037
	toggleRegex(): void {
S
Sandeep Somavarapu 已提交
1038
		this.searchWidget.searchInput.setRegex(!this.searchWidget.searchInput.getRegex());
1039
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1040 1041
	}

1042 1043 1044
	setSearchParameters(args: IFindInFilesArgs = {}): void {
		if (typeof args.isCaseSensitive === 'boolean') {
			this.searchWidget.searchInput.setCaseSensitive(args.isCaseSensitive);
1045
		}
1046 1047
		if (typeof args.matchWholeWord === 'boolean') {
			this.searchWidget.searchInput.setWholeWords(args.matchWholeWord);
1048
		}
1049 1050
		if (typeof args.isRegex === 'boolean') {
			this.searchWidget.searchInput.setRegex(args.isRegex);
1051
		}
1052 1053
		if (typeof args.filesToInclude === 'string') {
			this.searchIncludePattern.setValue(String(args.filesToInclude));
1054
		}
1055 1056
		if (typeof args.filesToExclude === 'string') {
			this.searchExcludePattern.setValue(String(args.filesToExclude));
1057
		}
1058 1059
		if (typeof args.query === 'string') {
			this.searchWidget.searchInput.setValue(args.query);
1060
		}
1061
		if (typeof args.replace === 'string') {
J
jeanp413 已提交
1062
			this.searchWidget.replaceInput.setValue(args.replace);
J
jwikman 已提交
1063
		} else {
J
jeanp413 已提交
1064 1065
			if (this.searchWidget.replaceInput.getValue() !== '') {
				this.searchWidget.replaceInput.setValue('');
J
jwikman 已提交
1066
			}
1067
		}
1068
		if (typeof args.triggerSearch === 'boolean' && args.triggerSearch) {
1069 1070 1071 1072
			this.onQueryChanged(true);
		}
	}

R
Rob Lourens 已提交
1073
	toggleQueryDetails(moveFocus = true, show?: boolean, skipLayout?: boolean, reverse?: boolean): void {
1074
		const cls = 'more';
E
Erich Gamma 已提交
1075
		show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show);
B
Benjamin Pasero 已提交
1076
		this.viewletState['query.queryDetailsExpanded'] = show;
E
Erich Gamma 已提交
1077 1078 1079
		skipLayout = Boolean(skipLayout);

		if (show) {
1080
			this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true');
E
Erich Gamma 已提交
1081
			dom.addClass(this.queryDetails, cls);
1082
			if (moveFocus) {
1083 1084 1085 1086 1087 1088 1089
				if (reverse) {
					this.inputPatternExcludes.focus();
					this.inputPatternExcludes.select();
				} else {
					this.inputPatternIncludes.focus();
					this.inputPatternIncludes.select();
				}
1090
			}
E
Erich Gamma 已提交
1091
		} else {
1092
			this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false');
E
Erich Gamma 已提交
1093
			dom.removeClass(this.queryDetails, cls);
1094
			if (moveFocus) {
1095
				this.searchWidget.focus();
1096
			}
E
Erich Gamma 已提交
1097
		}
B
Benjamin Pasero 已提交
1098

E
Erich Gamma 已提交
1099
		if (!skipLayout && this.size) {
1100
			this.layout(this.size.height);
E
Erich Gamma 已提交
1101 1102 1103
		}
	}

U
Ubuntu 已提交
1104
	searchInFolders(resources?: URI[]): void {
1105
		const folderPaths: string[] = [];
1106
		const workspace = this.contextService.getWorkspace();
1107 1108 1109

		if (resources) {
			resources.forEach(resource => {
1110
				let folderPath: string | undefined;
1111 1112
				if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
					// Show relative path from the root for single-root mode
1113
					folderPath = relativePath(workspace.folders[0].uri, resource); // always uses forward slashes
1114 1115 1116 1117 1118 1119
					if (folderPath && folderPath !== '.') {
						folderPath = './' + folderPath;
					}
				} else {
					const owningFolder = this.contextService.getWorkspaceFolder(resource);
					if (owningFolder) {
1120
						const owningRootName = owningFolder.name;
1121 1122

						// If this root is the only one with its basename, use a relative ./ path. If there is another, use an absolute path
1123
						const isUniqueFolder = workspace.folders.filter(folder => folder.name === owningRootName).length === 1;
1124
						if (isUniqueFolder) {
1125 1126
							const relPath = relativePath(owningFolder.uri, resource); // always uses forward slashes
							if (relPath === '') {
1127 1128
								folderPath = `./${owningFolder.name}`;
							} else {
R
Rob Lourens 已提交
1129
								folderPath = `./${owningFolder.name}/${relPath}`;
1130
							}
1131
						} else {
1132
							folderPath = resource.fsPath; // TODO rob: handle on-file URIs
1133
						}
1134 1135
					}
				}
1136 1137 1138 1139 1140

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

1143
		if (!folderPaths.length || folderPaths.some(folderPath => folderPath === '.')) {
S
Sandeep Somavarapu 已提交
1144 1145 1146 1147 1148
			this.inputPatternIncludes.setValue('');
			this.searchWidget.focus();
			return;
		}

1149
		// Show 'files to include' box
E
Erich Gamma 已提交
1150
		if (!this.showsFileTypes()) {
1151
			this.toggleQueryDetails(true, true);
E
Erich Gamma 已提交
1152
		}
B
Benjamin Pasero 已提交
1153

1154
		this.inputPatternIncludes.setValue(folderPaths.join(', '));
B
Benjamin Pasero 已提交
1155
		this.searchWidget.focus(false);
E
Erich Gamma 已提交
1156 1157
	}

R
Rob Lourens 已提交
1158
	onQueryChanged(preserveFocus?: boolean): void {
1159 1160 1161 1162
		if (!this.searchWidget.searchInput.inputBox.isInputValid()) {
			return;
		}

1163 1164 1165 1166
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1167 1168 1169
		const excludePatternText = this.inputPatternExcludes.getValue().trim();
		const includePatternText = this.inputPatternIncludes.getValue().trim();
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
E
Erich Gamma 已提交
1170 1171

		if (contentPattern.length === 0) {
1172
			this.clearSearchResults();
J
Jackson Kearl 已提交
1173
			this.clearMessage();
E
Erich Gamma 已提交
1174 1175 1176
			return;
		}

1177
		const content: IPatternInfo = {
E
Erich Gamma 已提交
1178 1179 1180
			pattern: contentPattern,
			isRegExp: isRegex,
			isCaseSensitive: isCaseSensitive,
R
Rob Lourens 已提交
1181
			isWordMatch: isWholeWords
E
Erich Gamma 已提交
1182 1183
		};

1184 1185
		const excludePattern = this.inputPatternExcludes.getValue();
		const includePattern = this.inputPatternIncludes.getValue();
E
Erich Gamma 已提交
1186

R
Rob Lourens 已提交
1187 1188 1189
		// 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 已提交
1190
		const charsPerLine = content.isRegExp ? 10000 :
1191
			250;
R
Rob Lourens 已提交
1192

1193
		const options: ITextQueryBuilderOptions = {
R
Rob Lourens 已提交
1194
			_reason: 'searchView',
R
Rob Lourens 已提交
1195
			extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
1196
			maxResults: SearchView.MAX_TEXT_RESULTS,
R
Rob Lourens 已提交
1197 1198
			disregardIgnoreFiles: !useExcludesAndIgnoreFiles || undefined,
			disregardExcludeSettings: !useExcludesAndIgnoreFiles || undefined,
R
Rob Lourens 已提交
1199
			excludePattern,
1200 1201
			includePattern,
			previewOptions: {
R
Rob Lourens 已提交
1202 1203
				matchLines: 1,
				charsPerLine
R
Rob Lourens 已提交
1204
			},
1205 1206
			isSmartCase: this.configurationService.getValue<ISearchConfiguration>().search.smartCase,
			expandPatterns: true
E
Erich Gamma 已提交
1207
		};
S
Sandeep Somavarapu 已提交
1208
		const folderResources = this.contextService.getWorkspace().folders;
1209 1210 1211 1212 1213 1214

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

1215
		let query: ITextQuery;
1216
		try {
1217
			query = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
1218 1219
		} catch (err) {
			onQueryValidationError(err);
1220
			return;
1221
		}
1222

1223
		this.validateQuery(query).then(() => {
1224
			this.onQueryTriggered(query, options, excludePatternText, includePatternText);
1225

1226 1227 1228
			if (!preserveFocus) {
				this.searchWidget.focus(false); // focus back to input field
			}
1229
		}, onQueryValidationError);
1230 1231
	}

J
Johannes Rieken 已提交
1232
	private validateQuery(query: ITextQuery): Promise<void> {
1233 1234 1235
		// Validate folderQueries
		const folderQueriesExistP =
			query.folderQueries.map(fq => {
B
Benjamin Pasero 已提交
1236
				return this.fileService.exists(fq.folder);
1237 1238
			});

1239
		return Promise.all(folderQueriesExistP).then(existResults => {
R
Rob Lourens 已提交
1240 1241
			// If no folders exist, show an error message about the first one
			const existingFolderQueries = query.folderQueries.filter((folderQuery, i) => existResults[i]);
1242
			if (!query.folderQueries.length || existingFolderQueries.length) {
R
Rob Lourens 已提交
1243 1244 1245
				query.folderQueries = existingFolderQueries;
			} else {
				const nonExistantPath = query.folderQueries[0].folder.fsPath;
1246
				const searchPathNotFoundError = nls.localize('searchPathNotFoundError', "Search path not found: {0}", nonExistantPath);
1247
				return Promise.reject(new Error(searchPathNotFoundError));
1248 1249 1250 1251
			}

			return undefined;
		});
E
Erich Gamma 已提交
1252 1253
	}

1254
	private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): void {
1255
		this.addToSearchHistoryDelayer.trigger(() => this.searchWidget.searchInput.onSearchSubmit());
1256
		this.inputPatternExcludes.onSearchSubmit();
1257 1258
		this.inputPatternIncludes.onSearchSubmit();

S
Sandeep Somavarapu 已提交
1259 1260
		this.viewModel.cancelSearch();

1261 1262
		this.currentSearchQ = this.currentSearchQ
			.then(() => this.doSearch(query, options, excludePatternText, includePatternText))
J
Johannes Rieken 已提交
1263
			.then(() => undefined, () => undefined);
1264 1265 1266
	}

	private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable<void> {
1267
		let progressComplete: () => void;
1268
		this.progressService.withProgress({ location: VIEWLET_ID, delay: 300 }, _progress => {
1269 1270
			return new Promise(resolve => progressComplete = resolve);
		});
E
Erich Gamma 已提交
1271

1272
		this.searchWidget.searchInput.clearMessage();
1273
		this.state = SearchUIState.Searching;
E
Erich Gamma 已提交
1274 1275
		this.showEmptyStage();

1276 1277 1278 1279 1280
		const slowTimer = setTimeout(() => {
			this.state = SearchUIState.SlowSearch;
			this.updateActions();
		}, 2000);

1281
		const onComplete = (completed?: ISearchComplete) => {
1282 1283
			clearTimeout(slowTimer);
			this.state = SearchUIState.Idle;
1284 1285

			// Complete up to 100% as needed
1286
			progressComplete();
E
Erich Gamma 已提交
1287

1288
			// Do final render, then expand if just 1 file with less than 50 matches
R
Rob Lourens 已提交
1289
			this.onSearchResultsChanged();
1290 1291 1292

			const collapseResults = this.configurationService.getValue<ISearchConfigurationProperties>('search').collapseResults;
			if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) {
R
Rob Lourens 已提交
1293 1294 1295
				const onlyMatch = this.viewModel.searchResult.matches()[0];
				if (onlyMatch.count() < 50) {
					this.tree.expand(onlyMatch);
1296
				}
R
Rob Lourens 已提交
1297
			}
1298

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

R
Rob Lourens 已提交
1301
			this.updateActions();
1302
			const hasResults = !this.viewModel.searchResult.isEmpty();
E
Erich Gamma 已提交
1303

R
Rob Lourens 已提交
1304 1305 1306 1307 1308 1309
			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 已提交
1310

R
Rob Lourens 已提交
1311
			if (!hasResults) {
1312 1313
				const hasExcludes = !!excludePatternText;
				const hasIncludes = !!includePatternText;
R
Rob Lourens 已提交
1314 1315 1316
				let message: string;

				if (!completed) {
J
Jackson Kearl 已提交
1317
					message = SEARCH_CANCELLED_MESSAGE;
R
Rob Lourens 已提交
1318 1319 1320 1321 1322 1323 1324
				} 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 已提交
1325
					message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
R
Rob Lourens 已提交
1326
				}
1327

R
Rob Lourens 已提交
1328 1329
				// Indicate as status to ARIA
				aria.status(message);
E
Erich Gamma 已提交
1330

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

R
Rob Lourens 已提交
1334 1335 1336 1337 1338 1339 1340 1341 1342 1343
				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);
1344

R
Rob Lourens 已提交
1345 1346
						this.inputPatternExcludes.setValue('');
						this.inputPatternIncludes.setValue('');
1347

R
Rob Lourens 已提交
1348 1349
						this.onQueryChanged();
					}));
1350
				} else {
R
Rob Lourens 已提交
1351 1352 1353 1354 1355 1356
					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, ' - '));
1357

R
Rob Lourens 已提交
1358 1359
					const learnMoreLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.learnMore', "Learn More")));
					this.addClickEvents(learnMoreLink, this.onLearnMore);
1360
				}
R
Rob Lourens 已提交
1361 1362 1363 1364

				if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
					this.showSearchWithoutFolderMessage();
				}
1365
				this.reLayout();
R
Rob Lourens 已提交
1366 1367 1368 1369 1370 1371
			} 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 已提交
1372 1373
		};

1374
		const onError = (e: any) => {
1375 1376
			clearTimeout(slowTimer);
			this.state = SearchUIState.Idle;
1377
			if (errors.isPromiseCanceledError(e)) {
U
Ubuntu 已提交
1378
				return onComplete(undefined);
1379
			} else {
1380
				this.updateActions();
1381
				progressComplete();
1382
				this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR });
1383
				this.viewModel.searchResult.clear();
1384

1385
				return Promise.resolve();
E
Erich Gamma 已提交
1386 1387 1388 1389 1390
			}
		};

		let visibleMatches = 0;

1391 1392
		let updatedActionsForFileCount = false;

E
Erich Gamma 已提交
1393
		// Handle UI updates in an interval to show frequent progress and results
1394
		const uiRefreshHandle: any = setInterval(() => {
1395
			if (this.state === SearchUIState.Idle) {
E
Erich Gamma 已提交
1396 1397 1398 1399 1400
				window.clearInterval(uiRefreshHandle);
				return;
			}

			// Search result tree update
1401 1402 1403
			const fileCount = this.viewModel.searchResult.fileCount();
			if (visibleMatches !== fileCount) {
				visibleMatches = fileCount;
R
Rob Lourens 已提交
1404
				this.refreshAndUpdateCount();
1405
			}
1406 1407 1408

			if (fileCount > 0 && !updatedActionsForFileCount) {
				updatedActionsForFileCount = true;
S
Sandeep Somavarapu 已提交
1409
				this.updateActions();
E
Erich Gamma 已提交
1410
			}
1411
		}, 100);
E
Erich Gamma 已提交
1412

S
Sandeep Somavarapu 已提交
1413
		this.searchWidget.setReplaceAllActionState(false);
1414

1415
		return this.viewModel.search(query)
1416
			.then(onComplete, onError);
E
Erich Gamma 已提交
1417 1418
	}

1419 1420
	private addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => {
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.CLICK, handler));
1421
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.KEY_DOWN, e => {
1422
			const event = new StandardKeyboardEvent(e);
1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440
			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);

1441 1442 1443
		this.openSettings('.exclude');
	}

1444
	private openSettings(query: string): Promise<IEditor | undefined> {
1445 1446
		const options: ISettingsEditorOptions = { query };
		return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ?
1447 1448 1449 1450 1451 1452 1453
			this.preferencesService.openWorkspaceSettings(undefined, options) :
			this.preferencesService.openGlobalSettings(undefined, options);
	}

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

1454
		this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977'));
1455 1456
	}

1457
	private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void {
1458
		const fileCount = this.viewModel.searchResult.fileCount();
1459 1460
		this.hasSearchResultsKey.set(fileCount > 0);

1461
		const msgWasHidden = this.messagesElement.style.display === 'none';
1462
		if (fileCount > 0) {
1463
			const messageEl = this.clearMessage();
1464 1465 1466 1467 1468 1469
			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 已提交
1470
			this.reLayout();
1471
		} else if (!msgWasHidden) {
1472
			dom.hide(this.messagesElement);
1473 1474 1475
		}
	}

1476 1477 1478 1479 1480 1481 1482
	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);
1483
		} else {
1484
			return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
1485 1486 1487
		}
	}

1488 1489 1490 1491
	private showSearchWithoutFolderMessage(): void {
		this.searchWithoutFolderMessageElement = this.clearMessage();

		const textEl = dom.append(this.searchWithoutFolderMessageElement,
R
Rob Lourens 已提交
1492
			$('p', undefined, nls.localize('searchWithoutFolder', "You have not opened or specified a folder. Only open files are currently searched - ")));
1493 1494

		const openFolderLink = dom.append(textEl,
1495
			$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder")));
1496 1497 1498 1499

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

R
Rob Lourens 已提交
1500 1501 1502 1503
			const action = env.isMacintosh ?
				this.instantiationService.createInstance(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL) :
				this.instantiationService.createInstance(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL);

U
Ubuntu 已提交
1504
			this.actionRunner!.run(action).then(() => {
1505 1506 1507 1508
				action.dispose();
			}, err => {
				action.dispose();
				errors.onUnexpectedError(err);
1509
			});
1510
		}));
1511 1512
	}

J
Jackson Kearl 已提交
1513
	private showEmptyStage(forceHideMessages = false): void {
E
Erich Gamma 已提交
1514
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1515
		this.updateActions();
E
Erich Gamma 已提交
1516

J
Jackson Kearl 已提交
1517 1518
		const showingCancelled = (this.messagesElement.firstChild?.textContent?.indexOf(SEARCH_CANCELLED_MESSAGE) ?? -1) > -1;

E
Erich Gamma 已提交
1519
		// clean up ui
S
Sandeep Somavarapu 已提交
1520
		// this.replaceService.disposeAllReplacePreviews();
J
Jackson Kearl 已提交
1521 1522 1523 1524 1525
		if (showingCancelled || forceHideMessages || !this.configurationService.getValue<ISearchConfiguration>().search.searchOnType) {
			// when in search to type, don't preemptively hide, as it causes flickering and shifting of the live results
			dom.hide(this.messagesElement);
		}

1526
		dom.show(this.resultsElement);
U
Ubuntu 已提交
1527
		this.currentSelectedFileMatch = undefined;
E
Erich Gamma 已提交
1528 1529
	}

1530
	private onFocus(lineMatch: Match, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
1531 1532
		const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
		return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ?
R
Rob Lourens 已提交
1533 1534
			this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) :
			this.open(lineMatch, preserveFocus, sideBySide, pinned);
1535 1536
	}

U
Ubuntu 已提交
1537
	open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<void> {
R
Rob Lourens 已提交
1538
		const selection = this.getSelectionFrom(element);
1539
		const resource = element instanceof Match ? element.parent().resource : (<FileMatch>element).resource;
E
Erich Gamma 已提交
1540
		return this.editorService.openEditor({
1541
			resource: resource,
E
Erich Gamma 已提交
1542
			options: {
1543 1544
				preserveFocus,
				pinned,
S
Sandeep Somavarapu 已提交
1545
				selection,
1546
				revealIfVisible: true
E
Erich Gamma 已提交
1547
			}
B
Benjamin Pasero 已提交
1548
		}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
S
Sandeep Somavarapu 已提交
1549
			if (editor && element instanceof Match && preserveFocus) {
1550
				this.viewModel.searchResult.rangeHighlightDecorations.highlightRange(
U
Ubuntu 已提交
1551
					(<ICodeEditor>editor.getControl()).getModel()!,
1552 1553
					element.range()
				);
S
Sandeep Somavarapu 已提交
1554
			} else {
1555
				this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange();
S
Sandeep Somavarapu 已提交
1556 1557
			}
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
1558 1559
	}

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

1584
	private onUntitledDidDispose(resource: URI): void {
E
Erich Gamma 已提交
1585 1586 1587 1588
		if (!this.viewModel) {
			return;
		}

1589
		// remove search results from this resource as it got disposed
1590 1591 1592 1593
		const matches = this.viewModel.searchResult.matches();
		for (let i = 0, len = matches.length; i < len; i++) {
			if (resource.toString() === matches[i].resource.toString()) {
				this.viewModel.searchResult.remove(matches[i]);
E
Erich Gamma 已提交
1594 1595 1596 1597 1598
			}
		}
	}

	private onFilesChanged(e: FileChangesEvent): void {
B
Benjamin Pasero 已提交
1599
		if (!this.viewModel || !e.gotDeleted()) {
E
Erich Gamma 已提交
1600 1601 1602
			return;
		}

1603
		const matches = this.viewModel.searchResult.matches();
E
Erich Gamma 已提交
1604

1605 1606
		const changedMatches = matches.filter(m => e.contains(m.resource, FileChangeType.DELETED));
		this.viewModel.searchResult.remove(changedMatches);
E
Erich Gamma 已提交
1607 1608
	}

R
Rob Lourens 已提交
1609
	getActions(): IAction[] {
1610
		return [
1611
			this.state === SearchUIState.SlowSearch ?
1612 1613 1614 1615
				this.cancelAction :
				this.refreshAction,
			...this.actions
		];
1616 1617
	}

1618
	private clearHistory(): void {
1619 1620 1621 1622 1623
		this.searchWidget.clearHistory();
		this.inputPatternExcludes.clearHistory();
		this.inputPatternIncludes.clearHistory();
	}

1624
	public saveState(): void {
A
Amy Qiu 已提交
1625 1626 1627 1628
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1629
		const patternExcludes = this.inputPatternExcludes.getValue().trim();
A
Amy Qiu 已提交
1630
		const patternIncludes = this.inputPatternIncludes.getValue().trim();
1631
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
1632
		const preserveCase = this.viewModel.preserveCase;
A
Amy Qiu 已提交
1633

B
Benjamin Pasero 已提交
1634 1635 1636 1637 1638 1639 1640
		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;
1641
		this.viewletState['query.preserveCase'] = preserveCase;
A
Amy Qiu 已提交
1642

B
Benjamin Pasero 已提交
1643 1644
		const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
		this.viewletState['view.showReplace'] = isReplaceShown;
1645
		this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue();
B
Benjamin Pasero 已提交
1646

1647 1648
		const history: ISearchHistoryValues = Object.create(null);

1649
		const searchHistory = this.searchWidget.getSearchHistory();
1650 1651 1652 1653
		if (searchHistory && searchHistory.length) {
			history.search = searchHistory;
		}

1654
		const replaceHistory = this.searchWidget.getReplaceHistory();
1655 1656 1657 1658
		if (replaceHistory && replaceHistory.length) {
			history.replace = replaceHistory;
		}

1659
		const patternExcludesHistory = this.inputPatternExcludes.getHistory();
1660 1661 1662 1663
		if (patternExcludesHistory && patternExcludesHistory.length) {
			history.exclude = patternExcludesHistory;
		}

1664
		const patternIncludesHistory = this.inputPatternIncludes.getHistory();
1665 1666 1667
		if (patternIncludesHistory && patternIncludesHistory.length) {
			history.include = patternIncludesHistory;
		}
1668

1669
		this.searchHistoryService.save(history);
B
Benjamin Pasero 已提交
1670 1671

		super.saveState();
A
Amy Qiu 已提交
1672 1673
	}

R
Rob Lourens 已提交
1674
	dispose(): void {
E
Erich Gamma 已提交
1675
		this.isDisposed = true;
1676
		this.saveState();
E
Erich Gamma 已提交
1677 1678
		super.dispose();
	}
1679 1680 1681
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
B
Benjamin Pasero 已提交
1682
	const matchHighlightColor = theme.getColor(editorFindMatchHighlight);
1683
	if (matchHighlightColor) {
I
isidor 已提交
1684
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { background-color: ${matchHighlightColor}; }`);
B
Benjamin Pasero 已提交
1685 1686 1687 1688
	}

	const diffInsertedColor = theme.getColor(diffInserted);
	if (diffInsertedColor) {
I
isidor 已提交
1689
		collector.addRule(`.monaco-workbench .search-view .replaceMatch { background-color: ${diffInsertedColor}; }`);
B
Benjamin Pasero 已提交
1690 1691 1692 1693
	}

	const diffRemovedColor = theme.getColor(diffRemoved);
	if (diffRemovedColor) {
I
isidor 已提交
1694
		collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { background-color: ${diffRemovedColor}; }`);
B
Benjamin Pasero 已提交
1695 1696 1697 1698
	}

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

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

1707 1708
	const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
	if (findMatchHighlightBorder) {
1709
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`);
1710
	}
1711 1712 1713

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