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
	) {
157
		super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService);
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 172

		this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
173 174 175
		this.memento = new Memento(this.id, storageService);
		this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE);
		this.globalMemento = this.memento.getMemento(StorageScope.GLOBAL);
E
Erich Gamma 已提交
176

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

	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 }));
B
Benjamin Pasero 已提交
636
		this.tree = this._register(<WorkbenchObjectTree<RenderableMatch, any>>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 900
		const searchResultContainerSize = this.size.height -
			messagesSize -
901
			dom.getTotalHeight(this.searchWidgetsContainerElement);
902

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

905
		this.tree.layout(searchResultContainerSize, 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
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1158 1159 1160
		const excludePatternText = this.inputPatternExcludes.getValue().trim();
		const includePatternText = this.inputPatternIncludes.getValue().trim();
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
E
Erich Gamma 已提交
1161 1162 1163 1164 1165

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

1166
		const content: IPatternInfo = {
E
Erich Gamma 已提交
1167 1168 1169
			pattern: contentPattern,
			isRegExp: isRegex,
			isCaseSensitive: isCaseSensitive,
R
Rob Lourens 已提交
1170
			isWordMatch: isWholeWords
E
Erich Gamma 已提交
1171 1172
		};

1173 1174
		const excludePattern = this.inputPatternExcludes.getValue();
		const includePattern = this.inputPatternIncludes.getValue();
E
Erich Gamma 已提交
1175

R
Rob Lourens 已提交
1176 1177 1178
		// 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 已提交
1179
		const charsPerLine = content.isRegExp ? 10000 :
1180
			250;
R
Rob Lourens 已提交
1181

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

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

1204
		let query: ITextQuery;
1205
		try {
1206
			query = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
1207 1208
		} catch (err) {
			onQueryValidationError(err);
1209
			return;
1210
		}
1211

1212
		this.validateQuery(query).then(() => {
1213
			this.onQueryTriggered(query, options, excludePatternText, includePatternText);
1214

1215 1216 1217
			if (!preserveFocus) {
				this.searchWidget.focus(false); // focus back to input field
			}
1218
		}, onQueryValidationError);
1219 1220
	}

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

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

			return undefined;
		});
E
Erich Gamma 已提交
1241 1242
	}

1243
	private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): void {
R
Rob Lourens 已提交
1244
		this.searchWidget.searchInput.onSearchSubmit();
1245
		this.inputPatternExcludes.onSearchSubmit();
1246 1247
		this.inputPatternIncludes.onSearchSubmit();

S
Sandeep Somavarapu 已提交
1248 1249
		this.viewModel.cancelSearch();

1250 1251
		this.currentSearchQ = this.currentSearchQ
			.then(() => this.doSearch(query, options, excludePatternText, includePatternText))
J
Johannes Rieken 已提交
1252
			.then(() => undefined, () => undefined);
1253 1254 1255
	}

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

1261
		this.searchWidget.searchInput.clearMessage();
1262
		this.state = SearchUIState.Searching;
E
Erich Gamma 已提交
1263 1264
		this.showEmptyStage();

1265 1266 1267 1268 1269
		const slowTimer = setTimeout(() => {
			this.state = SearchUIState.SlowSearch;
			this.updateActions();
		}, 2000);

1270
		const onComplete = (completed?: ISearchComplete) => {
1271 1272
			clearTimeout(slowTimer);
			this.state = SearchUIState.Idle;
1273 1274

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

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

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

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

R
Rob Lourens 已提交
1290
			this.updateActions();
1291
			const hasResults = !this.viewModel.searchResult.isEmpty();
E
Erich Gamma 已提交
1292

R
Rob Lourens 已提交
1293 1294 1295 1296 1297 1298
			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 已提交
1299

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

R
Rob Lourens 已提交
1317 1318
				// Indicate as status to ARIA
				aria.status(message);
E
Erich Gamma 已提交
1319

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

R
Rob Lourens 已提交
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332
				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);
1333

R
Rob Lourens 已提交
1334 1335
						this.inputPatternExcludes.setValue('');
						this.inputPatternIncludes.setValue('');
1336

R
Rob Lourens 已提交
1337 1338
						this.onQueryChanged();
					}));
1339
				} else {
R
Rob Lourens 已提交
1340 1341 1342 1343 1344 1345
					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, ' - '));
1346

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

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

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

1374
				if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
1375
					this.showPcre2Hint();
1376
				}
1377 1378

				return Promise.resolve();
E
Erich Gamma 已提交
1379 1380 1381 1382 1383
			}
		};

		let visibleMatches = 0;

1384 1385
		let updatedActionsForFileCount = false;

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

			// Search result tree update
1394 1395 1396
			const fileCount = this.viewModel.searchResult.fileCount();
			if (visibleMatches !== fileCount) {
				visibleMatches = fileCount;
R
Rob Lourens 已提交
1397
				this.refreshAndUpdateCount();
1398
			}
1399 1400 1401

			if (fileCount > 0 && !updatedActionsForFileCount) {
				updatedActionsForFileCount = true;
S
Sandeep Somavarapu 已提交
1402
				this.updateActions();
E
Erich Gamma 已提交
1403
			}
1404
		}, 100);
E
Erich Gamma 已提交
1405

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

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

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

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

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

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

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

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

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

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

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

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

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

E
Erich Gamma 已提交
1521 1522
	private showEmptyStage(): void {
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1523
		this.updateActions();
E
Erich Gamma 已提交
1524 1525

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

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

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

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

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

B
Benjamin Pasero 已提交
1590
	private onUntitledDidChangeDirty(resource: URI): void {
E
Erich Gamma 已提交
1591 1592 1593 1594
		if (!this.viewModel) {
			return;
		}

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

	private onFilesChanged(e: FileChangesEvent): void {
B
Benjamin Pasero 已提交
1607
		if (!this.viewModel || !e.gotDeleted()) {
E
Erich Gamma 已提交
1608 1609 1610
			return;
		}

1611
		const matches = this.viewModel.searchResult.matches();
E
Erich Gamma 已提交
1612 1613 1614

		for (let i = 0, len = matches.length; i < len; i++) {
			if (e.contains(matches[i].resource(), FileChangeType.DELETED)) {
1615
				this.viewModel.searchResult.remove(matches[i]);
E
Erich Gamma 已提交
1616 1617 1618 1619
			}
		}
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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