searchView.ts 66.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 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, Severity } 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, SearchErrorCode, 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, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult, BaseFolderMatch } from 'vs/workbench/contrib/search/common/searchModel';
54
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
55
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
56
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
57
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
58
import { relativePath } from 'vs/base/common/resources';
59
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
60 61
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
62
import { Memento, MementoObject } from 'vs/workbench/common/memento';
63
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
E
Erich Gamma 已提交
64

65 66
const $ = dom.$;

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

73
export class SearchView extends ViewletPanel {
E
Erich Gamma 已提交
74

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

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

E
Erich Gamma 已提交
81
	private isDisposed: boolean;
82

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

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

101
	private state: SearchUIState;
S
Sandeep Somavarapu 已提交
102

103
	private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction> = [];
104 105
	private cancelAction: CancelSearchAction;
	private refreshAction: RefreshAction;
R
Rob Lourens 已提交
106
	private contextMenu: IMenu;
107

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

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

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

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

J
Johannes Rieken 已提交
130
	private currentSearchQ = Promise.resolve();
131

132
	constructor(
133
		options: IViewletPanelOptions,
134 135
		@IFileService private readonly fileService: IFileService,
		@IEditorService private readonly editorService: IEditorService,
136
		@IProgressService private readonly progressService: IProgressService,
137 138 139 140
		@INotificationService private readonly notificationService: INotificationService,
		@IDialogService private readonly dialogService: IDialogService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
141
		@IConfigurationService configurationService: IConfigurationService,
142 143 144 145 146 147
		@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,
148
		@IThemeService protected themeService: IThemeService,
149 150
		@ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService,
		@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
151
		@IContextMenuService contextMenuService: IContextMenuService,
152
		@IMenuService private readonly menuService: IMenuService,
153 154 155
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
		@IKeybindingService keybindingService: IKeybindingService,
		@IStorageService storageService: IStorageService,
E
Erich Gamma 已提交
156
	) {
S
Sandeep Somavarapu 已提交
157
		super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService);
E
Erich Gamma 已提交
158

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

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

B
Benjamin Pasero 已提交
178 179 180
		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()));
181
		this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory()));
182

183
		this.delayedRefresh = this._register(new Delayer<void>(250));
184

185 186 187 188 189 190 191 192 193 194
		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 已提交
195 196
	}

R
Rob Lourens 已提交
197
	get searchResult(): SearchResult {
R
Rob Lourens 已提交
198 199 200
		return this.viewModel && this.viewModel.searchResult;
	}

201
	private onDidChangeWorkbenchState(): void {
202 203
		if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) {
			dom.hide(this.searchWithoutFolderMessageElement);
204 205 206
		}
	}

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

210
		this.searchWidgetsContainerElement = dom.append(this.container, $('.search-widgets-container'));
211
		this.createSearchWidget(this.searchWidgetsContainerElement);
212

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

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

225 226 227
		// 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 已提交
228

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

236 237 238 239 240 241 242
			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 已提交
243

244 245 246 247 248 249 250 251 252
			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 已提交
253

254 255 256 257 258
		// 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));
259

260 261 262 263
		this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, {
			ariaLabel: nls.localize('label.includes', 'Search Include Patterns'),
			history: patternIncludesHistory,
		}));
264

265
		this.inputPatternIncludes.setValue(patternIncludes);
266

267
		this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true));
268 269 270 271 272 273 274 275 276 277 278
		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,
		}));
279

280 281
		this.inputPatternExcludes.setValue(patternExclusions);
		this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles);
282

283
		this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true));
284 285 286
		this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget
		this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused);

287
		this.messagesElement = dom.append(this.container, $('.messages'));
288
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
289
			this.showSearchWithoutFolderMessage();
290
		}
291

292
		this.createSearchResultsView(this.container);
293

294
		if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '' || queryDetailsExpanded !== '' || !useExcludesAndIgnoreFiles) {
295
			this.toggleQueryDetails(true, true, true);
296 297
		}

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

300
		this._register(this.searchWidget.searchInput.onInput(() => this.updateActions()));
301
		this._register(this.searchWidget.replaceInput.onDidChange(() => this.updateActions()));
302

S
Sandeep Somavarapu 已提交
303 304
		this._register(this.onDidFocus(() => this.viewletFocused.set(true)));
		this._register(this.onDidBlur(() => this.viewletFocused.set(false)));
305

306
		this._register(this.onDidChangeBodyVisibility(visible => this.onVisibilityChanged(visible)));
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
	}

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

R
Rob Lourens 已提交
325
	get searchAndReplaceWidget(): SearchWidget {
326 327 328
		return this.searchWidget;
	}

R
Rob Lourens 已提交
329
	get searchIncludePattern(): PatternInputWidget {
330 331 332
		return this.inputPatternIncludes;
	}

R
Rob Lourens 已提交
333
	get searchExcludePattern(): PatternInputWidget {
334 335 336
		return this.inputPatternExcludes;
	}

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

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

348
		super.updateActions();
S
Sandeep Somavarapu 已提交
349 350
	}

351
	private isScreenReaderOptimized() {
352
		const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
353 354 355 356
		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;
				}

452 453 454 455 456 457
				if (element instanceof SearchResult) {
					this.tree.setChildren(null, this.createIterator(element, collapseResults));
				} else {
					this.tree.setChildren(element, this.createIterator(element, collapseResults));
					this.tree.rerender(element);
				}
R
Rob Lourens 已提交
458
			});
459 460 461
		}
	}

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
	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 };
		});
	}

U
Ubuntu 已提交
478
	private createFolderIterator(folderMatch: BaseFolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
		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 }));
	}

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

509
	private replaceAll(): void {
510
		if (this.viewModel.searchResult.count() === 0) {
S
Sandeep Somavarapu 已提交
511 512 513
			return;
		}

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

B
Benjamin Pasero 已提交
519 520 521 522 523 524 525 526
		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);
		});

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

534
		this.dialogService.confirm(confirmation).then(res => {
535
			if (res.confirmed) {
536
				this.searchWidget.setReplaceAllActionState(false);
B
Benjamin Pasero 已提交
537 538
				this.viewModel.searchResult.replaceAll(progressReporter).then(() => {
					progressComplete();
539
					const messageEl = this.clearMessage();
540
					dom.append(messageEl, $('p', undefined, afterReplaceAllMessage));
541
				}, (error) => {
B
Benjamin Pasero 已提交
542
					progressComplete();
543
					errors.isPromiseCanceledError(error);
544
					this.notificationService.error(error);
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 604 605 606 607 608 609 610 611 612 613
	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);
	}

614
	private clearMessage(): HTMLElement {
R
Rob Lourens 已提交
615
		this.searchWithoutFolderMessageElement = undefined;
616

617 618 619 620 621
		dom.clearNode(this.messagesElement);
		dom.show(this.messagesElement);
		dispose(this.messageDisposables);
		this.messageDisposables = [];

622
		return dom.append(this.messagesElement, $('.message'));
623 624
	}

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

629 630 631 632 633 634
		const identityProvider: IIdentityProvider<RenderableMatch> = {
			getId(element: RenderableMatch) {
				return element.id();
			}
		};

635
		this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }));
636
		this.tree = this._register(this.instantiationService.createInstance(WorkbenchObjectTree,
R
Rob Lourens 已提交
637 638 639
			this.resultsElement,
			delegate,
			[
B
Benjamin Pasero 已提交
640 641
				this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)),
				this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)),
642
				this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)),
R
Rob Lourens 已提交
643
			],
644
			{
645
				identityProvider,
646
				accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider, this.viewModel),
R
Rob Lourens 已提交
647 648
				dnd: this.instantiationService.createInstance(SearchDND),
				multipleSelectionSupport: false
B
Benjamin Pasero 已提交
649
			}));
R
Rob Lourens 已提交
650
		this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
R
Rob Lourens 已提交
651

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

R
Rob Lourens 已提交
662
				this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned);
663 664
			}
		}));
S
Sandeep Somavarapu 已提交
665

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

M
Matt Bierner 已提交
688
	private onContextMenu(e: ITreeContextMenuEvent<RenderableMatch | null>): void {
R
Rob Lourens 已提交
689 690 691 692 693 694 695
		if (!this.contextMenu) {
			this.contextMenu = this._register(this.menuService.createMenu(MenuId.SearchContext, this.contextKeyService));
		}

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

696 697 698
		const actions: IAction[] = [];
		const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);

R
Rob Lourens 已提交
699 700
		this.contextMenuService.showContextMenu({
			getAnchor: () => e.anchor,
701 702 703
			getActions: () => actions,
			getActionsContext: () => e.element,
			onHide: () => dispose(actionsDisposable)
R
Rob Lourens 已提交
704 705 706
		});
	}

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

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

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

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

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

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

		// Reveal the newly selected element
737
		if (next) {
738
			this.tree.setFocus([next], getSelectionKeyboardEvent(undefined, false));
739 740
			this.tree.reveal(next);
		}
741 742
	}

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

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

		// Expand and go past FileMatch nodes
750 751
		if (!(prev instanceof Match)) {
			prev = navigator.previous();
R
Rob Lourens 已提交
752
			if (!prev) {
R
Rob Lourens 已提交
753
				// Wrap around
R
Rob Lourens 已提交
754 755 756 757
				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 已提交
758
				this.tree.expand(prev);
M
Matt Bierner 已提交
759
				let tmp: RenderableMatch | null;
R
Rob Lourens 已提交
760 761 762 763 764
				while (tmp = navigator.next()) {
					prev = tmp;
				}
			}

765 766 767
			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 已提交
768 769 770
				const nextItem = navigator.next();
				this.tree.expand(prev);
				navigator = this.tree.navigate(nextItem); // recreate navigator because modifying the tree can invalidate it
771
				prev = navigator.previous();
772 773 774 775
			}
		}

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

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

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

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

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

803
				this.searchWidget.searchInput.setValue(selectedText);
804
				updatedText = true;
805
			}
806
		}
R
Rob Lourens 已提交
807

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

R
Rob Lourens 已提交
930
	clearSearchResults(): void {
931
		this.viewModel.searchResult.clear();
E
Erich Gamma 已提交
932
		this.showEmptyStage();
933
		if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
934
			this.showSearchWithoutFolderMessage();
935
		}
936
		this.searchWidget.clear();
S
Sandeep Somavarapu 已提交
937
		this.viewModel.cancelSearch();
938
		this.updateActions();
E
Erich Gamma 已提交
939 940
	}

R
Rob Lourens 已提交
941
	cancelSearch(): boolean {
S
Sandeep Somavarapu 已提交
942
		if (this.viewModel.cancelSearch()) {
943
			this.searchWidget.focus();
944 945 946 947 948
			return true;
		}
		return false;
	}

949
	private selectTreeIfNotSelected(): void {
R
Rob Lourens 已提交
950
		if (this.tree.getNode(null)) {
951
			this.tree.domFocus();
952
			const selection = this.tree.getSelection();
E
Erich Gamma 已提交
953 954 955 956 957 958
			if (selection.length === 0) {
				this.tree.focusNext();
			}
		}
	}

M
Matt Bierner 已提交
959
	private getSearchTextFromEditor(allowUnselectedWord: boolean): string | null {
B
Benjamin Pasero 已提交
960
		if (!this.editorService.activeEditor) {
E
Erich Gamma 已提交
961 962 963
			return null;
		}

R
Rob Lourens 已提交
964 965 966 967
		if (dom.isAncestor(document.activeElement, this.getContainer())) {
			return null;
		}

968 969 970 971
		let activeTextEditorWidget = this.editorService.activeTextEditorWidget;
		if (isDiffEditor(activeTextEditorWidget)) {
			if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) {
				activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor();
972
			} else {
973
				activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
974 975 976
			}
		}

M
Matt Bierner 已提交
977
		if (!isCodeEditor(activeTextEditorWidget) || !activeTextEditorWidget.hasModel()) {
E
Erich Gamma 已提交
978 979 980
			return null;
		}

981
		const range = activeTextEditorWidget.getSelection();
R
Rob Lourens 已提交
982 983 984 985
		if (!range) {
			return null;
		}

R
Rob Lourens 已提交
986
		if (range.isEmpty() && !this.searchWidget.searchInput.getValue() && allowUnselectedWord) {
987
			const wordAtPosition = activeTextEditorWidget.getModel().getWordAtPosition(range.getStartPosition());
R
Rob Lourens 已提交
988 989 990 991 992
			if (wordAtPosition) {
				return wordAtPosition.word;
			}
		}

993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011
		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;
			}

1012
			return searchText;
E
Erich Gamma 已提交
1013
		}
1014

E
Erich Gamma 已提交
1015 1016 1017 1018 1019 1020 1021
		return null;
	}

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

R
Rob Lourens 已提交
1022
	toggleCaseSensitive(): void {
S
Sandeep Somavarapu 已提交
1023
		this.searchWidget.searchInput.setCaseSensitive(!this.searchWidget.searchInput.getCaseSensitive());
1024
		this.onQueryChanged(true);
S
Sandeep Somavarapu 已提交
1025 1026
	}

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

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

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

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

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

E
Erich Gamma 已提交
1094
		if (!skipLayout && this.size) {
1095
			this.layout(this.size.height);
E
Erich Gamma 已提交
1096 1097 1098
		}
	}

U
Ubuntu 已提交
1099
	searchInFolders(resources?: URI[]): void {
1100
		const folderPaths: string[] = [];
1101
		const workspace = this.contextService.getWorkspace();
1102 1103 1104

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

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

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

1138
		if (!folderPaths.length || folderPaths.some(folderPath => folderPath === '.')) {
S
Sandeep Somavarapu 已提交
1139 1140 1141 1142 1143
			this.inputPatternIncludes.setValue('');
			this.searchWidget.focus();
			return;
		}

1144
		// Show 'files to include' box
E
Erich Gamma 已提交
1145
		if (!this.showsFileTypes()) {
1146
			this.toggleQueryDetails(true, true);
E
Erich Gamma 已提交
1147
		}
B
Benjamin Pasero 已提交
1148

1149
		this.inputPatternIncludes.setValue(folderPaths.join(', '));
B
Benjamin Pasero 已提交
1150
		this.searchWidget.focus(false);
E
Erich Gamma 已提交
1151 1152
	}

R
Rob Lourens 已提交
1153
	onQueryChanged(preserveFocus?: boolean): void {
1154 1155 1156 1157
		if (!this.searchWidget.searchInput.inputBox.isInputValid()) {
			return;
		}

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

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

1170
		const content: IPatternInfo = {
E
Erich Gamma 已提交
1171 1172 1173
			pattern: contentPattern,
			isRegExp: isRegex,
			isCaseSensitive: isCaseSensitive,
R
Rob Lourens 已提交
1174
			isWordMatch: isWholeWords
E
Erich Gamma 已提交
1175 1176
		};

1177 1178
		const excludePattern = this.inputPatternExcludes.getValue();
		const includePattern = this.inputPatternIncludes.getValue();
E
Erich Gamma 已提交
1179

R
Rob Lourens 已提交
1180 1181 1182
		// 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 已提交
1183
		const charsPerLine = content.isRegExp ? 10000 :
1184
			250;
R
Rob Lourens 已提交
1185

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

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

1208
		let query: ITextQuery;
1209
		try {
1210
			query = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
1211 1212
		} catch (err) {
			onQueryValidationError(err);
1213
			return;
1214
		}
1215

1216
		this.validateQuery(query).then(() => {
1217
			this.onQueryTriggered(query, options, excludePatternText, includePatternText);
1218

1219 1220 1221
			if (!preserveFocus) {
				this.searchWidget.focus(false); // focus back to input field
			}
1222
		}, onQueryValidationError);
1223 1224
	}

J
Johannes Rieken 已提交
1225
	private validateQuery(query: ITextQuery): Promise<void> {
1226 1227 1228
		// Validate folderQueries
		const folderQueriesExistP =
			query.folderQueries.map(fq => {
B
Benjamin Pasero 已提交
1229
				return this.fileService.exists(fq.folder);
1230 1231
			});

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

			return undefined;
		});
E
Erich Gamma 已提交
1245 1246
	}

1247
	private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): void {
R
Rob Lourens 已提交
1248
		this.searchWidget.searchInput.onSearchSubmit();
1249
		this.inputPatternExcludes.onSearchSubmit();
1250 1251
		this.inputPatternIncludes.onSearchSubmit();

S
Sandeep Somavarapu 已提交
1252 1253
		this.viewModel.cancelSearch();

1254 1255
		this.currentSearchQ = this.currentSearchQ
			.then(() => this.doSearch(query, options, excludePatternText, includePatternText))
J
Johannes Rieken 已提交
1256
			.then(() => undefined, () => undefined);
1257 1258 1259
	}

	private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable<void> {
1260 1261 1262 1263
		let progressComplete: () => void;
		this.progressService.withProgress({ location: VIEWLET_ID }, _progress => {
			return new Promise(resolve => progressComplete = resolve);
		});
E
Erich Gamma 已提交
1264

1265
		this.searchWidget.searchInput.clearMessage();
1266
		this.state = SearchUIState.Searching;
E
Erich Gamma 已提交
1267 1268
		this.showEmptyStage();

1269 1270 1271 1272 1273
		const slowTimer = setTimeout(() => {
			this.state = SearchUIState.SlowSearch;
			this.updateActions();
		}, 2000);

1274
		const onComplete = (completed?: ISearchComplete) => {
1275 1276
			clearTimeout(slowTimer);
			this.state = SearchUIState.Idle;
1277 1278

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

1281
			// Do final render, then expand if just 1 file with less than 50 matches
R
Rob Lourens 已提交
1282
			this.onSearchResultsChanged();
1283 1284 1285

			const collapseResults = this.configurationService.getValue<ISearchConfigurationProperties>('search').collapseResults;
			if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) {
R
Rob Lourens 已提交
1286 1287 1288
				const onlyMatch = this.viewModel.searchResult.matches()[0];
				if (onlyMatch.count() < 50) {
					this.tree.expand(onlyMatch);
1289
				}
R
Rob Lourens 已提交
1290
			}
1291

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

R
Rob Lourens 已提交
1294
			this.updateActions();
1295
			const hasResults = !this.viewModel.searchResult.isEmpty();
E
Erich Gamma 已提交
1296

R
Rob Lourens 已提交
1297 1298 1299 1300 1301 1302
			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 已提交
1303

R
Rob Lourens 已提交
1304
			if (!hasResults) {
1305 1306
				const hasExcludes = !!excludePatternText;
				const hasIncludes = !!includePatternText;
R
Rob Lourens 已提交
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317
				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 已提交
1318
					message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
R
Rob Lourens 已提交
1319
				}
1320

R
Rob Lourens 已提交
1321 1322
				// Indicate as status to ARIA
				aria.status(message);
E
Erich Gamma 已提交
1323

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

R
Rob Lourens 已提交
1327 1328 1329 1330 1331 1332 1333 1334 1335 1336
				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);
1337

R
Rob Lourens 已提交
1338 1339
						this.inputPatternExcludes.setValue('');
						this.inputPatternIncludes.setValue('');
1340

R
Rob Lourens 已提交
1341 1342
						this.onQueryChanged();
					}));
1343
				} else {
R
Rob Lourens 已提交
1344 1345 1346 1347 1348 1349
					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, ' - '));
1350

R
Rob Lourens 已提交
1351 1352
					const learnMoreLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.learnMore', "Learn More")));
					this.addClickEvents(learnMoreLink, this.onLearnMore);
1353
				}
R
Rob Lourens 已提交
1354 1355 1356 1357

				if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
					this.showSearchWithoutFolderMessage();
				}
1358
				this.reLayout();
R
Rob Lourens 已提交
1359 1360 1361 1362 1363 1364
			} 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 已提交
1365 1366
		};

1367
		const onError = (e: any) => {
1368 1369
			clearTimeout(slowTimer);
			this.state = SearchUIState.Idle;
1370
			if (errors.isPromiseCanceledError(e)) {
U
Ubuntu 已提交
1371
				return onComplete(undefined);
1372
			} else {
1373
				this.updateActions();
1374
				progressComplete();
1375
				this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR });
1376
				this.viewModel.searchResult.clear();
1377

1378
				if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
1379
					this.showPcre2Hint();
1380
				}
1381 1382

				return Promise.resolve();
E
Erich Gamma 已提交
1383 1384 1385 1386 1387
			}
		};

		let visibleMatches = 0;

1388 1389
		let updatedActionsForFileCount = false;

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

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

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

S
Sandeep Somavarapu 已提交
1410
		this.searchWidget.setReplaceAllActionState(false);
1411

1412
		return this.viewModel.search(query)
1413
			.then(onComplete, onError);
E
Erich Gamma 已提交
1414 1415
	}

1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432
	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')
				}
			]);
		}
	}

1433 1434
	private addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => {
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.CLICK, handler));
1435
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.KEY_DOWN, e => {
1436
			const event = new StandardKeyboardEvent(e);
1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454
			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);

1455 1456 1457
		this.openSettings('.exclude');
	}

U
Ubuntu 已提交
1458
	private openSettings(query: string): Promise<IEditor | null> {
1459 1460
		const options: ISettingsEditorOptions = { query };
		return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ?
1461 1462 1463 1464 1465 1466 1467 1468 1469 1470
			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');
	}

1471
	private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void {
1472
		const fileCount = this.viewModel.searchResult.fileCount();
1473 1474
		this.hasSearchResultsKey.set(fileCount > 0);

1475
		const msgWasHidden = this.messagesElement.style.display === 'none';
1476
		if (fileCount > 0) {
1477
			const messageEl = this.clearMessage();
1478 1479 1480 1481 1482 1483
			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 已提交
1484
			this.reLayout();
1485
		} else if (!msgWasHidden) {
1486
			dom.hide(this.messagesElement);
1487 1488 1489
		}
	}

1490 1491 1492 1493 1494 1495 1496
	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);
1497
		} else {
1498
			return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
1499 1500 1501
		}
	}

1502 1503 1504 1505
	private showSearchWithoutFolderMessage(): void {
		this.searchWithoutFolderMessageElement = this.clearMessage();

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

		const openFolderLink = dom.append(textEl,
1509
			$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder")));
1510 1511 1512 1513 1514 1515

		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);
U
Ubuntu 已提交
1516
			this.actionRunner!.run(action).then(() => {
1517 1518 1519 1520
				action.dispose();
			}, err => {
				action.dispose();
				errors.onUnexpectedError(err);
1521
			});
1522
		}));
1523 1524
	}

E
Erich Gamma 已提交
1525 1526
	private showEmptyStage(): void {
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1527
		this.updateActions();
E
Erich Gamma 已提交
1528 1529

		// clean up ui
S
Sandeep Somavarapu 已提交
1530
		// this.replaceService.disposeAllReplacePreviews();
1531
		dom.hide(this.messagesElement);
1532
		dom.show(this.resultsElement);
U
Ubuntu 已提交
1533
		this.currentSelectedFileMatch = undefined;
E
Erich Gamma 已提交
1534 1535
	}

1536
	private onFocus(lineMatch: Match, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
1537 1538
		const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
		return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ?
R
Rob Lourens 已提交
1539 1540
			this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) :
			this.open(lineMatch, preserveFocus, sideBySide, pinned);
1541 1542
	}

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

R
Rob Lourens 已提交
1564
			if (editor) {
U
Ubuntu 已提交
1565
				this.editorGroupsService.activateGroup(editor.group!);
R
Rob Lourens 已提交
1566
			}
S
Sandeep Somavarapu 已提交
1567
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
1568 1569
	}

1570
	private getSelectionFrom(element: FileMatchOrMatch): any {
1571
		let match: Match | null = null;
1572
		if (element instanceof Match) {
J
Johannes Rieken 已提交
1573
			match = element;
1574 1575
		}
		if (element instanceof FileMatch && element.count() > 0) {
J
Johannes Rieken 已提交
1576
			match = element.matches()[element.matches().length - 1];
1577 1578
		}
		if (match) {
1579
			const range = match.range();
S
Sandeep Somavarapu 已提交
1580
			if (this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) {
1581
				const replaceString = match.replaceString;
1582 1583
				return {
					startLineNumber: range.startLineNumber,
S
Sandeep Somavarapu 已提交
1584
					startColumn: range.startColumn,
1585
					endLineNumber: range.startLineNumber,
S
Sandeep Somavarapu 已提交
1586
					endColumn: range.startColumn + replaceString.length
1587 1588 1589 1590
				};
			}
			return range;
		}
R
Rob Lourens 已提交
1591
		return undefined;
1592 1593
	}

B
Benjamin Pasero 已提交
1594
	private onUntitledDidChangeDirty(resource: URI): void {
E
Erich Gamma 已提交
1595 1596 1597 1598
		if (!this.viewModel) {
			return;
		}

1599
		// remove search results from this resource as it got disposed
B
Benjamin Pasero 已提交
1600
		if (!this.untitledEditorService.isDirty(resource)) {
1601
			const matches = this.viewModel.searchResult.matches();
1602
			for (let i = 0, len = matches.length; i < len; i++) {
1603
				if (resource.toString() === matches[i].resource.toString()) {
1604 1605
					this.viewModel.searchResult.remove(matches[i]);
				}
E
Erich Gamma 已提交
1606 1607 1608 1609 1610
			}
		}
	}

	private onFilesChanged(e: FileChangesEvent): void {
B
Benjamin Pasero 已提交
1611
		if (!this.viewModel || !e.gotDeleted()) {
E
Erich Gamma 已提交
1612 1613 1614
			return;
		}

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

1617 1618
		const changedMatches = matches.filter(m => e.contains(m.resource, FileChangeType.DELETED));
		this.viewModel.searchResult.remove(changedMatches);
E
Erich Gamma 已提交
1619 1620
	}

R
Rob Lourens 已提交
1621
	getActions(): IAction[] {
1622
		return [
1623
			this.state === SearchUIState.SlowSearch ?
1624 1625 1626 1627
				this.cancelAction :
				this.refreshAction,
			...this.actions
		];
1628 1629
	}

1630
	private clearHistory(): void {
1631 1632 1633 1634 1635
		this.searchWidget.clearHistory();
		this.inputPatternExcludes.clearHistory();
		this.inputPatternIncludes.clearHistory();
	}

1636
	public saveState(): void {
A
Amy Qiu 已提交
1637 1638 1639 1640
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1641
		const patternExcludes = this.inputPatternExcludes.getValue().trim();
A
Amy Qiu 已提交
1642
		const patternIncludes = this.inputPatternIncludes.getValue().trim();
1643
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
A
Amy Qiu 已提交
1644

B
Benjamin Pasero 已提交
1645 1646 1647 1648 1649 1650 1651
		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 已提交
1652

B
Benjamin Pasero 已提交
1653 1654
		const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
		this.viewletState['view.showReplace'] = isReplaceShown;
1655
		this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue();
B
Benjamin Pasero 已提交
1656

1657 1658
		const history: ISearchHistoryValues = Object.create(null);

1659
		const searchHistory = this.searchWidget.getSearchHistory();
1660 1661 1662 1663
		if (searchHistory && searchHistory.length) {
			history.search = searchHistory;
		}

1664
		const replaceHistory = this.searchWidget.getReplaceHistory();
1665 1666 1667 1668
		if (replaceHistory && replaceHistory.length) {
			history.replace = replaceHistory;
		}

1669
		const patternExcludesHistory = this.inputPatternExcludes.getHistory();
1670 1671 1672 1673
		if (patternExcludesHistory && patternExcludesHistory.length) {
			history.exclude = patternExcludesHistory;
		}

1674
		const patternIncludesHistory = this.inputPatternIncludes.getHistory();
1675 1676 1677
		if (patternIncludesHistory && patternIncludesHistory.length) {
			history.include = patternIncludesHistory;
		}
1678

1679
		this.searchHistoryService.save(history);
B
Benjamin Pasero 已提交
1680 1681

		super.saveState();
A
Amy Qiu 已提交
1682 1683
	}

R
Rob Lourens 已提交
1684
	dispose(): void {
E
Erich Gamma 已提交
1685
		this.isDisposed = true;
1686
		this.saveState();
E
Erich Gamma 已提交
1687 1688
		super.dispose();
	}
1689 1690 1691
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
B
Benjamin Pasero 已提交
1692
	const matchHighlightColor = theme.getColor(editorFindMatchHighlight);
1693
	if (matchHighlightColor) {
I
isidor 已提交
1694
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { background-color: ${matchHighlightColor}; }`);
B
Benjamin Pasero 已提交
1695 1696 1697 1698
	}

	const diffInsertedColor = theme.getColor(diffInserted);
	if (diffInsertedColor) {
I
isidor 已提交
1699
		collector.addRule(`.monaco-workbench .search-view .replaceMatch { background-color: ${diffInsertedColor}; }`);
B
Benjamin Pasero 已提交
1700 1701 1702 1703
	}

	const diffRemovedColor = theme.getColor(diffRemoved);
	if (diffRemovedColor) {
I
isidor 已提交
1704
		collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { background-color: ${diffRemovedColor}; }`);
B
Benjamin Pasero 已提交
1705 1706 1707 1708
	}

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

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

1717 1718
	const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
	if (findMatchHighlightBorder) {
1719
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`);
1720
	}
1721 1722 1723

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