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

6
import * 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 16
import { Emitter, Event } from 'vs/base/common/event';
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';
R
Rob Lourens 已提交
26 27
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
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';
J
Johannes Rieken 已提交
36
import { IProgressService } from 'vs/platform/progress/common/progress';
37 38
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchErrorCode, VIEW_ID } from 'vs/workbench/services/search/common/search';
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, IResourceLabelsContainer } 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 } 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 62 63
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Memento } from 'vs/workbench/common/memento';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
E
Erich Gamma 已提交
64

65 66
const $ = dom.$;

67
export class SearchView extends ViewletPanel {
E
Erich Gamma 已提交
68

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

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

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

77
	private container: HTMLElement;
E
Erich Gamma 已提交
78
	private queryBuilder: QueryBuilder;
79
	private viewModel: SearchModel;
80
	private memento: Memento;
E
Erich Gamma 已提交
81

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

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

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

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

U
Ubuntu 已提交
118
	private currentSelectedFileMatch: FileMatch | undefined;
S
Sandeep Somavarapu 已提交
119

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

U
Ubuntu 已提交
124
	private searchWithoutFolderMessageElement: HTMLElement | undefined;
125

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

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

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

		this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
169 170 171
		this.memento = new Memento(this.id, storageService);
		this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE);
		this.globalMemento = this.memento.getMemento(StorageScope.GLOBAL);
E
Erich Gamma 已提交
172

B
Benjamin Pasero 已提交
173 174 175
		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()));
176
		this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory()));
177

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

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

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

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

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

206
	renderBody(parent: HTMLElement): void {
B
Benjamin Pasero 已提交
207
		this.viewModel = this._register(this.searchWorkbenchService.searchModel);
208
		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 323 324 325
	}

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

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

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

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

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

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

345
	protected updateActions(): void {
S
Sandeep Somavarapu 已提交
346 347
		for (const action of this.actions) {
			action.update();
348 349
			this.refreshAction.update();
			this.cancelAction.update();
S
Sandeep Somavarapu 已提交
350
		}
351
		super.updateActions();
S
Sandeep Somavarapu 已提交
352 353
	}

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

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

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

B
Benjamin Pasero 已提交
381
		if (showReplace) {
S
Sandeep Somavarapu 已提交
382 383 384
			this.searchWidget.toggleReplace(true);
		}

385
		this._register(this.searchWidget.onSearchSubmit(() => this.onQueryChanged()));
B
Benjamin Pasero 已提交
386
		this._register(this.searchWidget.onSearchCancel(() => this.cancelSearch()));
387
		this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.onQueryChanged(true)));
388

389 390 391
		this._register(this.searchWidget.onDidHeightChange(() => this.reLayout()));

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

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

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

407 408 409 410
		this.trackInputBox(this.searchWidget.searchInputFocusTracker);
		this.trackInputBox(this.searchWidget.replaceInputFocusTracker);
	}

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

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

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

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

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

461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
	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 已提交
477
	private createFolderIterator(folderMatch: BaseFolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator<ITreeElement<RenderableMatch>> {
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
		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 }));
	}

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

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

513
		const progressRunner = this.progressService.show(100);
514

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

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

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

543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
	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);
	}

607
	private clearMessage(): HTMLElement {
R
Rob Lourens 已提交
608
		this.searchWithoutFolderMessageElement = undefined;
609

610 611 612 613 614
		dom.clearNode(this.messagesElement);
		dom.show(this.messagesElement);
		dispose(this.messageDisposables);
		this.messageDisposables = [];

615
		return dom.append(this.messagesElement, $('.message'));
616 617
	}

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

622 623 624 625 626 627
		const identityProvider: IIdentityProvider<RenderableMatch> = {
			getId(element: RenderableMatch) {
				return element.id();
			}
		};

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

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

R
Rob Lourens 已提交
655
				this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned);
656 657
			}
		}));
S
Sandeep Somavarapu 已提交
658

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

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

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

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

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

R
Rob Lourens 已提交
704
	selectCurrentMatch(): void {
R
Rob Lourens 已提交
705
		const focused = this.tree.getFocus()[0];
706
		const fakeKeyboardEvent = getSelectionKeyboardEvent(undefined, false);
R
Rob Lourens 已提交
707
		this.tree.setSelection([focused], fakeKeyboardEvent);
R
Rob Lourens 已提交
708 709
	}

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

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

R
Rob Lourens 已提交
720
		let navigator = this.tree.navigate(selected);
R
Rob Lourens 已提交
721 722

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

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

			// Select the FileMatch's first child
736
			next = navigator.next();
737 738 739
		}

		// Reveal the newly selected element
740
		if (next) {
741
			this.tree.setFocus([next], getSelectionKeyboardEvent(undefined, false));
742
			this.tree.reveal(next);
743
			this.selectCurrentMatchEmitter.fire(undefined);
744
		}
745 746
	}

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

751
		let prev = navigator.previous();
752 753

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

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

		// Reveal the newly selected element
780
		if (prev) {
781
			this.tree.setFocus([prev], getSelectionKeyboardEvent(undefined, false));
R
Rob Lourens 已提交
782
			this.tree.reveal(prev);
783
			this.selectCurrentMatchEmitter.fire(undefined);
784
		}
785 786
	}

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

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

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

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

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

R
Rob Lourens 已提交
813
		return updatedText;
814 815
	}

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

		if (this.searchWidget.replaceInputHasFocus()) {
827
			this.moveFocusFromSearchOrReplace();
828 829 830 831
			return;
		}

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

		if (this.inputPatternExcludes.inputHasFocus()) {
838 839 840 841 842
			this.selectTreeIfNotSelected();
			return;
		}
	}

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

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

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

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

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

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

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

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

R
Rob Lourens 已提交
891
		const actionsPosition = this.configurationService.getValue<ISearchConfigurationProperties>('search').actionsPosition;
892 893
		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);
894

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

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

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

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

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

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

913 914
	protected layoutBody(height: number, width: number): void {
		this.size = new dom.Dimension(width, height);
B
Benjamin Pasero 已提交
915
		this.reLayout();
E
Erich Gamma 已提交
916 917
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
		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;
			}

1021
			return searchText;
E
Erich Gamma 已提交
1022
		}
1023

E
Erich Gamma 已提交
1024 1025 1026 1027 1028 1029 1030
		return null;
	}

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

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

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

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

1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
	searchReplace(query?: string, replace?: string, triggerSearch?: boolean, caseSensitive?: boolean, wholeWords?: boolean, regex?: boolean, filesToInclude?: string, filesToExclude?: string): void {
		if (caseSensitive !== undefined && caseSensitive !== null) {
			this.searchWidget.searchInput.setCaseSensitive(caseSensitive);
		}
		if (wholeWords !== undefined && wholeWords !== null) {
			this.searchWidget.searchInput.setWholeWords(wholeWords);
		}
		if (regex !== undefined && regex !== null) {
			this.searchWidget.searchInput.setRegex(regex);
		}
		if (filesToInclude !== undefined && filesToInclude !== null) {
			this.searchIncludePattern.setValue(String(filesToInclude));
		}
		if (filesToExclude !== undefined && filesToExclude !== null) {
			this.searchExcludePattern.setValue(String(filesToExclude));
		}
		if (query !== undefined && query !== null) {
			this.searchWidget.searchInput.setValue(query);
		}
		if (replace !== undefined && replace !== null) {
			this.searchWidget.replaceInput.value = replace;
		}

		if (triggerSearch !== undefined && triggerSearch !== null && triggerSearch) {
			this.onQueryChanged(true);
		}
	}

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

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

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

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

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

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

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

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

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

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

R
Rob Lourens 已提交
1159
	onQueryChanged(preserveFocus?: boolean): void {
1160 1161 1162 1163
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1164 1165 1166
		const excludePatternText = this.inputPatternExcludes.getValue().trim();
		const includePatternText = this.inputPatternIncludes.getValue().trim();
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
E
Erich Gamma 已提交
1167 1168 1169 1170 1171

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

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

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

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

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

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

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

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

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

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

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

			return undefined;
		});
E
Erich Gamma 已提交
1247 1248
	}

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

S
Sandeep Somavarapu 已提交
1254 1255
		this.viewModel.cancelSearch();

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

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

1264
		this.searchWidget.searchInput.clearMessage();
1265 1266 1267
		this.searching = true;
		setTimeout(() => {
			if (this.searching) {
1268
				this.updateActions();
1269 1270
			}
		}, 2000);
E
Erich Gamma 已提交
1271 1272
		this.showEmptyStage();

1273
		const onComplete = (completed?: ISearchComplete) => {
1274
			this.searching = false;
1275 1276

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		let visibleMatches = 0;

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

			// Search result tree update
1395 1396 1397
			const fileCount = this.viewModel.searchResult.fileCount();
			if (visibleMatches !== fileCount) {
				visibleMatches = fileCount;
R
Rob Lourens 已提交
1398
				this.refreshAndUpdateCount();
1399
			}
1400
			if (fileCount > 0) {
S
Sandeep Somavarapu 已提交
1401
				this.updateActions();
E
Erich Gamma 已提交
1402
			}
1403
		}, 100);
E
Erich Gamma 已提交
1404

S
Sandeep Somavarapu 已提交
1405
		this.searchWidget.setReplaceAllActionState(false);
1406

1407
		return this.viewModel.search(query)
1408
			.then(onComplete, onError);
E
Erich Gamma 已提交
1409 1410
	}

1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
	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')
				}
			]);
		}
	}

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

1450 1451 1452
		this.openSettings('.exclude');
	}

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

1466
	private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void {
1467
		const fileCount = this.viewModel.searchResult.fileCount();
1468 1469
		this.hasSearchResultsKey.set(fileCount > 0);

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

1485 1486 1487 1488 1489 1490 1491
	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);
1492
		} else {
1493
			return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
1494 1495 1496
		}
	}

1497 1498 1499 1500
	private showSearchWithoutFolderMessage(): void {
		this.searchWithoutFolderMessageElement = this.clearMessage();

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

		const openFolderLink = dom.append(textEl,
1504
			$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder")));
1505 1506 1507 1508 1509 1510

		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 已提交
1511
			this.actionRunner!.run(action).then(() => {
1512 1513 1514 1515
				action.dispose();
			}, err => {
				action.dispose();
				errors.onUnexpectedError(err);
1516
			});
1517
		}));
1518 1519
	}

E
Erich Gamma 已提交
1520
	private showEmptyStage(): void {
1521

E
Erich Gamma 已提交
1522
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1523 1524
		this.searchSubmitted = false;
		this.updateActions();
E
Erich Gamma 已提交
1525 1526

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

J
Johannes Rieken 已提交
1533
	private onFocus(lineMatch: any, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
E
Erich Gamma 已提交
1534
		if (!(lineMatch instanceof Match)) {
1535
			this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange();
1536
			return Promise.resolve(true);
E
Erich Gamma 已提交
1537 1538
		}

1539 1540
		const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
		return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ?
R
Rob Lourens 已提交
1541 1542
			this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) :
			this.open(lineMatch, preserveFocus, sideBySide, pinned);
1543 1544
	}

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

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

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

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

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

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

1617
		const matches = this.viewModel.searchResult.matches();
E
Erich Gamma 已提交
1618 1619 1620

		for (let i = 0, len = matches.length; i < len; i++) {
			if (e.contains(matches[i].resource(), FileChangeType.DELETED)) {
1621
				this.viewModel.searchResult.remove(matches[i]);
E
Erich Gamma 已提交
1622 1623 1624 1625
			}
		}
	}

R
Rob Lourens 已提交
1626
	getActions(): IAction[] {
1627 1628 1629 1630 1631 1632
		return [
			this.searching ?
				this.cancelAction :
				this.refreshAction,
			...this.actions
		];
1633 1634
	}

1635
	private clearHistory(): void {
1636 1637 1638 1639 1640
		this.searchWidget.clearHistory();
		this.inputPatternExcludes.clearHistory();
		this.inputPatternIncludes.clearHistory();
	}

1641
	public saveState(): void {
A
Amy Qiu 已提交
1642 1643 1644 1645
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1646
		const patternExcludes = this.inputPatternExcludes.getValue().trim();
A
Amy Qiu 已提交
1647
		const patternIncludes = this.inputPatternIncludes.getValue().trim();
1648
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
A
Amy Qiu 已提交
1649

B
Benjamin Pasero 已提交
1650 1651 1652 1653 1654 1655 1656
		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 已提交
1657

B
Benjamin Pasero 已提交
1658 1659
		const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
		this.viewletState['view.showReplace'] = isReplaceShown;
1660
		this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue();
B
Benjamin Pasero 已提交
1661

1662 1663
		const history: ISearchHistoryValues = Object.create(null);

1664
		const searchHistory = this.searchWidget.getSearchHistory();
1665 1666 1667 1668
		if (searchHistory && searchHistory.length) {
			history.search = searchHistory;
		}

1669
		const replaceHistory = this.searchWidget.getReplaceHistory();
1670 1671 1672 1673
		if (replaceHistory && replaceHistory.length) {
			history.replace = replaceHistory;
		}

1674
		const patternExcludesHistory = this.inputPatternExcludes.getHistory();
1675 1676 1677 1678
		if (patternExcludesHistory && patternExcludesHistory.length) {
			history.exclude = patternExcludesHistory;
		}

1679
		const patternIncludesHistory = this.inputPatternIncludes.getHistory();
1680 1681 1682
		if (patternIncludesHistory && patternIncludesHistory.length) {
			history.include = patternIncludesHistory;
		}
1683

1684
		this.searchHistoryService.save(history);
B
Benjamin Pasero 已提交
1685 1686

		super.saveState();
A
Amy Qiu 已提交
1687 1688
	}

1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699
	private _toDispose: IDisposable[] = [];
	protected _register<T extends IDisposable>(t: T): T {
		if (this.isDisposed) {
			console.warn('Registering disposable on object that has already been disposed.');
			t.dispose();
		} else {
			this._toDispose.push(t);
		}
		return t;
	}

R
Rob Lourens 已提交
1700
	dispose(): void {
E
Erich Gamma 已提交
1701
		this.isDisposed = true;
1702 1703
		this.saveState();
		this._toDispose = dispose(this._toDispose);
E
Erich Gamma 已提交
1704 1705
		super.dispose();
	}
1706 1707 1708
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
B
Benjamin Pasero 已提交
1709
	const matchHighlightColor = theme.getColor(editorFindMatchHighlight);
1710
	if (matchHighlightColor) {
I
isidor 已提交
1711
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { background-color: ${matchHighlightColor}; }`);
B
Benjamin Pasero 已提交
1712 1713 1714 1715
	}

	const diffInsertedColor = theme.getColor(diffInserted);
	if (diffInsertedColor) {
I
isidor 已提交
1716
		collector.addRule(`.monaco-workbench .search-view .replaceMatch { background-color: ${diffInsertedColor}; }`);
B
Benjamin Pasero 已提交
1717 1718 1719 1720
	}

	const diffRemovedColor = theme.getColor(diffRemoved);
	if (diffRemovedColor) {
I
isidor 已提交
1721
		collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { background-color: ${diffRemovedColor}; }`);
B
Benjamin Pasero 已提交
1722 1723 1724 1725
	}

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

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

1734 1735
	const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
	if (findMatchHighlightBorder) {
1736
		collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`);
1737
	}
1738 1739 1740

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