searchView.ts 65.5 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
	}

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

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

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

U
Ubuntu 已提交
1077
	searchInFolders(resources?: URI[]): void {
1078
		const folderPaths: string[] = [];
1079
		const workspace = this.contextService.getWorkspace();
1080 1081 1082

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

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

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

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

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

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

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

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

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

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

R
Rob Lourens 已提交
1154 1155 1156
		// 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 已提交
1157
		const charsPerLine = content.isRegExp ? 10000 :
1158
			250;
R
Rob Lourens 已提交
1159

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

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

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

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

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

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

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

			return undefined;
		});
E
Erich Gamma 已提交
1219 1220
	}

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

S
Sandeep Somavarapu 已提交
1226 1227
		this.viewModel.cancelSearch();

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

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

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

1245
		const onComplete = (completed?: ISearchComplete) => {
1246
			this.searching = false;
1247 1248

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

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

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

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

R
Rob Lourens 已提交
1264 1265
			this.searchSubmitted = true;
			this.updateActions();
1266
			const hasResults = !this.viewModel.searchResult.isEmpty();
E
Erich Gamma 已提交
1267

R
Rob Lourens 已提交
1268 1269 1270 1271 1272 1273
			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 已提交
1274

R
Rob Lourens 已提交
1275
			if (!hasResults) {
1276 1277
				const hasExcludes = !!excludePatternText;
				const hasIncludes = !!includePatternText;
R
Rob Lourens 已提交
1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288
				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 已提交
1289
					message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
R
Rob Lourens 已提交
1290
				}
1291

R
Rob Lourens 已提交
1292 1293
				// Indicate as status to ARIA
				aria.status(message);
E
Erich Gamma 已提交
1294

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

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

R
Rob Lourens 已提交
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
				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);
1310

R
Rob Lourens 已提交
1311 1312
						this.inputPatternExcludes.setValue('');
						this.inputPatternIncludes.setValue('');
1313

R
Rob Lourens 已提交
1314 1315
						this.onQueryChanged();
					}));
1316
				} else {
R
Rob Lourens 已提交
1317 1318 1319 1320 1321 1322
					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, ' - '));
1323

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

				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 已提交
1337 1338
		};

1339
		const onError = (e: any) => {
1340
			if (errors.isPromiseCanceledError(e)) {
U
Ubuntu 已提交
1341
				return onComplete(undefined);
1342
			} else {
1343
				this.searching = false;
1344
				this.updateActions();
1345
				progressRunner.done();
1346
				this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR });
1347
				this.viewModel.searchResult.clear();
1348

1349
				if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
1350
					this.showPcre2Hint();
1351
				}
1352 1353

				return Promise.resolve();
E
Erich Gamma 已提交
1354 1355 1356 1357 1358 1359
			}
		};

		let visibleMatches = 0;

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

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

S
Sandeep Somavarapu 已提交
1377
		this.searchWidget.setReplaceAllActionState(false);
1378

1379
		return this.viewModel.search(query)
1380
			.then(onComplete, onError);
E
Erich Gamma 已提交
1381 1382
	}

1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
	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')
				}
			]);
		}
	}

1400 1401
	private addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => {
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.CLICK, handler));
1402
		this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.KEY_DOWN, e => {
1403
			const event = new StandardKeyboardEvent(e);
1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421
			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);

1422 1423 1424
		this.openSettings('.exclude');
	}

U
Ubuntu 已提交
1425
	private openSettings(query: string): Promise<IEditor | null> {
1426 1427
		const options: ISettingsEditorOptions = { query };
		return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ?
1428 1429 1430 1431 1432 1433 1434 1435 1436 1437
			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');
	}

1438
	private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void {
1439
		const fileCount = this.viewModel.searchResult.fileCount();
1440 1441
		this.hasSearchResultsKey.set(fileCount > 0);

1442
		const msgWasHidden = this.messagesElement.style.display === 'none';
1443
		if (fileCount > 0) {
1444
			const messageEl = this.clearMessage();
1445 1446 1447 1448 1449 1450
			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 已提交
1451
			this.reLayout();
1452
		} else if (!msgWasHidden) {
1453
			dom.hide(this.messagesElement);
1454 1455 1456
		}
	}

1457 1458 1459 1460 1461 1462 1463
	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);
1464
		} else {
1465
			return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
1466 1467 1468
		}
	}

1469 1470 1471 1472
	private showSearchWithoutFolderMessage(): void {
		this.searchWithoutFolderMessageElement = this.clearMessage();

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

		const openFolderLink = dom.append(textEl,
1476
			$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder")));
1477 1478 1479 1480 1481 1482

		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 已提交
1483
			this.actionRunner!.run(action).then(() => {
1484 1485 1486 1487
				action.dispose();
			}, err => {
				action.dispose();
				errors.onUnexpectedError(err);
1488
			});
1489
		}));
1490 1491
	}

E
Erich Gamma 已提交
1492
	private showEmptyStage(): void {
1493

E
Erich Gamma 已提交
1494
		// disable 'result'-actions
S
Sandeep Somavarapu 已提交
1495 1496
		this.searchSubmitted = false;
		this.updateActions();
E
Erich Gamma 已提交
1497 1498

		// clean up ui
S
Sandeep Somavarapu 已提交
1499
		// this.replaceService.disposeAllReplacePreviews();
1500
		dom.hide(this.messagesElement);
1501
		dom.show(this.resultsElement);
U
Ubuntu 已提交
1502
		this.currentSelectedFileMatch = undefined;
E
Erich Gamma 已提交
1503 1504
	}

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

1511 1512
		const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
		return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ?
R
Rob Lourens 已提交
1513 1514
			this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) :
			this.open(lineMatch, preserveFocus, sideBySide, pinned);
1515 1516
	}

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

R
Rob Lourens 已提交
1538
			if (editor) {
U
Ubuntu 已提交
1539
				this.editorGroupsService.activateGroup(editor.group!);
R
Rob Lourens 已提交
1540
			}
S
Sandeep Somavarapu 已提交
1541
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
1542 1543
	}

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

B
Benjamin Pasero 已提交
1568
	private onUntitledDidChangeDirty(resource: URI): void {
E
Erich Gamma 已提交
1569 1570 1571 1572
		if (!this.viewModel) {
			return;
		}

1573
		// remove search results from this resource as it got disposed
B
Benjamin Pasero 已提交
1574
		if (!this.untitledEditorService.isDirty(resource)) {
1575
			const matches = this.viewModel.searchResult.matches();
1576
			for (let i = 0, len = matches.length; i < len; i++) {
B
Benjamin Pasero 已提交
1577
				if (resource.toString() === matches[i].resource().toString()) {
1578 1579
					this.viewModel.searchResult.remove(matches[i]);
				}
E
Erich Gamma 已提交
1580 1581 1582 1583 1584
			}
		}
	}

	private onFilesChanged(e: FileChangesEvent): void {
B
Benjamin Pasero 已提交
1585
		if (!this.viewModel || !e.gotDeleted()) {
E
Erich Gamma 已提交
1586 1587 1588
			return;
		}

1589
		const matches = this.viewModel.searchResult.matches();
E
Erich Gamma 已提交
1590 1591 1592

		for (let i = 0, len = matches.length; i < len; i++) {
			if (e.contains(matches[i].resource(), FileChangeType.DELETED)) {
1593
				this.viewModel.searchResult.remove(matches[i]);
E
Erich Gamma 已提交
1594 1595 1596 1597
			}
		}
	}

R
Rob Lourens 已提交
1598
	getActions(): IAction[] {
1599 1600 1601 1602 1603 1604
		return [
			this.searching ?
				this.cancelAction :
				this.refreshAction,
			...this.actions
		];
1605 1606
	}

1607
	private clearHistory(): void {
1608 1609 1610 1611 1612
		this.searchWidget.clearHistory();
		this.inputPatternExcludes.clearHistory();
		this.inputPatternIncludes.clearHistory();
	}

1613
	public saveState(): void {
A
Amy Qiu 已提交
1614 1615 1616 1617
		const isRegex = this.searchWidget.searchInput.getRegex();
		const isWholeWords = this.searchWidget.searchInput.getWholeWords();
		const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
		const contentPattern = this.searchWidget.searchInput.getValue();
1618
		const patternExcludes = this.inputPatternExcludes.getValue().trim();
A
Amy Qiu 已提交
1619
		const patternIncludes = this.inputPatternIncludes.getValue().trim();
1620
		const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
A
Amy Qiu 已提交
1621

B
Benjamin Pasero 已提交
1622 1623 1624 1625 1626 1627 1628
		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 已提交
1629

B
Benjamin Pasero 已提交
1630 1631
		const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
		this.viewletState['view.showReplace'] = isReplaceShown;
1632
		this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue();
B
Benjamin Pasero 已提交
1633

1634 1635
		const history: ISearchHistoryValues = Object.create(null);

1636
		const searchHistory = this.searchWidget.getSearchHistory();
1637 1638 1639 1640
		if (searchHistory && searchHistory.length) {
			history.search = searchHistory;
		}

1641
		const replaceHistory = this.searchWidget.getReplaceHistory();
1642 1643 1644 1645
		if (replaceHistory && replaceHistory.length) {
			history.replace = replaceHistory;
		}

1646
		const patternExcludesHistory = this.inputPatternExcludes.getHistory();
1647 1648 1649 1650
		if (patternExcludesHistory && patternExcludesHistory.length) {
			history.exclude = patternExcludesHistory;
		}

1651
		const patternIncludesHistory = this.inputPatternIncludes.getHistory();
1652 1653 1654
		if (patternIncludesHistory && patternIncludesHistory.length) {
			history.include = patternIncludesHistory;
		}
1655

1656
		this.searchHistoryService.save(history);
B
Benjamin Pasero 已提交
1657 1658

		super.saveState();
A
Amy Qiu 已提交
1659 1660
	}

1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671
	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 已提交
1672
	dispose(): void {
E
Erich Gamma 已提交
1673
		this.isDisposed = true;
1674 1675
		this.saveState();
		this._toDispose = dispose(this._toDispose);
E
Erich Gamma 已提交
1676 1677
		super.dispose();
	}
1678 1679 1680
}

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

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

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

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

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

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

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