searchWidget.ts 25.7 KB
Newer Older
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';
7
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
S
Sandeep Somavarapu 已提交
8
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
9
import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button';
10
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
J
jeanp413 已提交
11
import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput';
12
import { IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
13 14 15 16
import { Widget } from 'vs/base/browser/ui/widget';
import { Action } from 'vs/base/common/actions';
import { Delayer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
17
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
18
import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/findModel';
19
import * as nls from 'vs/nls';
20 21
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
22 23 24 25
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
26
import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
J
Jackson Kearl 已提交
27
import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
28
import { IThemeService } from 'vs/platform/theme/common/themeService';
J
jeanp413 已提交
29
import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget';
30
import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
31
import * as Constants from 'vs/workbench/contrib/search/common/constants';
I
isidor 已提交
32
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
33
import { isMacintosh } from 'vs/base/common/platform';
34
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
35
import { IViewsService } from 'vs/workbench/common/views';
M
Martin Aeschlimann 已提交
36
import { searchReplaceAllIcon, searchHideReplaceIcon, searchShowContextIcon, searchShowReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
37

38 39
/** Specified in searchview.css */
export const SingleLineInputHeight = 24;
R
romainHainaut 已提交
40

41
export interface ISearchWidgetOptions {
J
Johannes Rieken 已提交
42
	value?: string;
43
	replaceValue?: string;
J
Johannes Rieken 已提交
44 45 46
	isRegex?: boolean;
	isCaseSensitive?: boolean;
	isWholeWords?: boolean;
47 48
	searchHistory?: string[];
	replaceHistory?: string[];
49
	preserveCase?: boolean;
50 51
	_hideReplaceToggle?: boolean; // TODO: Search Editor's replace experience
	showContextToggle?: boolean;
52 53
}

54 55
class ReplaceAllAction extends Action {

R
Rob Lourens 已提交
56
	static readonly ID: string = 'search.action.replaceAll';
57

58
	constructor(private _searchWidget: SearchWidget) {
M
Martin Aeschlimann 已提交
59
		super(ReplaceAllAction.ID, '', searchReplaceAllIcon.classNames, false);
60 61 62
	}

	set searchWidget(searchWidget: SearchWidget) {
J
Johannes Rieken 已提交
63
		this._searchWidget = searchWidget;
64 65
	}

66
	run(): Promise<any> {
67 68 69
		if (this._searchWidget) {
			return this._searchWidget.triggerReplaceAll();
		}
70
		return Promise.resolve(null);
71 72 73
	}
}

74 75
const ctrlKeyMod = (isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);

J
jeanp413 已提交
76 77
function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
	const isMultiline = !!value.match(/\n/);
78
	if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionStart > 0) {
J
jeanp413 已提交
79 80 81 82 83 84 85
		event.stopPropagation();
		return;
	}
}

function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
	const isMultiline = !!value.match(/\n/);
86
	if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionEnd < textarea.value.length) {
J
jeanp413 已提交
87 88 89 90 91
		event.stopPropagation();
		return;
	}
}

92
export class SearchWidget extends Widget {
93
	private static readonly INPUT_MAX_HEIGHT = 134;
94

95 96
	private static readonly REPLACE_ALL_DISABLED_LABEL = nls.localize('search.action.replaceAll.disabled.label', "Replace All (Submit Search to Enable)");
	private static readonly REPLACE_ALL_ENABLED_LABEL = (keyBindingService2: IKeybindingService): string => {
97
		const kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID);
B
Benjamin Pasero 已提交
98
		return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb, keyBindingService2);
99
	};
S
Sandeep Somavarapu 已提交
100

101
	domNode!: HTMLElement;
102

103 104
	searchInput!: FindInput;
	searchInputFocusTracker!: dom.IFocusTracker;
105
	private searchInputBoxFocused: IContextKey<boolean>;
106

107
	private replaceContainer!: HTMLElement;
J
jeanp413 已提交
108 109 110
	replaceInput!: ReplaceInput;
	replaceInputFocusTracker!: dom.IFocusTracker;
	private replaceInputBoxFocused: IContextKey<boolean>;
111 112
	private toggleReplaceButton!: Button;
	private replaceAllAction!: ReplaceAllAction;
A
Alex Dima 已提交
113
	private replaceActive: IContextKey<boolean>;
114
	private replaceActionBar!: ActionBar;
S
Sandeep Somavarapu 已提交
115
	private _replaceHistoryDelayer: Delayer<void>;
116
	private ignoreGlobalFindBufferOnNextFocus = false;
117
	private previousGlobalFindBufferValue: string | null = null;
118

119 120
	private _onSearchSubmit = this._register(new Emitter<{ triggeredOnType: boolean, delay: number }>());
	readonly onSearchSubmit: Event<{ triggeredOnType: boolean, delay: number }> = this._onSearchSubmit.event;
121

122 123
	private _onSearchCancel = this._register(new Emitter<{ focus: boolean }>());
	readonly onSearchCancel: Event<{ focus: boolean }> = this._onSearchCancel.event;
124 125

	private _onReplaceToggled = this._register(new Emitter<void>());
R
Rob Lourens 已提交
126
	readonly onReplaceToggled: Event<void> = this._onReplaceToggled.event;
127

S
Sandeep Somavarapu 已提交
128
	private _onReplaceStateChange = this._register(new Emitter<boolean>());
R
Rob Lourens 已提交
129
	readonly onReplaceStateChange: Event<boolean> = this._onReplaceStateChange.event;
130

131 132 133
	private _onPreserveCaseChange = this._register(new Emitter<boolean>());
	readonly onPreserveCaseChange: Event<boolean> = this._onPreserveCaseChange.event;

U
Ubuntu 已提交
134 135
	private _onReplaceValueChanged = this._register(new Emitter<void>());
	readonly onReplaceValueChanged: Event<void> = this._onReplaceValueChanged.event;
136 137

	private _onReplaceAll = this._register(new Emitter<void>());
R
Rob Lourens 已提交
138
	readonly onReplaceAll: Event<void> = this._onReplaceAll.event;
139

S
Sandeep Somavarapu 已提交
140
	private _onBlur = this._register(new Emitter<void>());
R
Rob Lourens 已提交
141
	readonly onBlur: Event<void> = this._onBlur.event;
S
Sandeep Somavarapu 已提交
142

143
	private _onDidHeightChange = this._register(new Emitter<void>());
R
Rob Lourens 已提交
144
	readonly onDidHeightChange: Event<void> = this._onDidHeightChange.event;
145

146 147 148 149 150
	private readonly _onDidToggleContext = new Emitter<void>();
	readonly onDidToggleContext: Event<void> = this._onDidToggleContext.event;

	private showContextCheckbox!: Checkbox;
	private contextLinesInput!: InputBox;
151

152
	constructor(
153
		container: HTMLElement,
154
		options: ISearchWidgetOptions,
155 156 157 158 159
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
		@IKeybindingService private readonly keyBindingService: IKeybindingService,
		@IClipboardService private readonly clipboardServce: IClipboardService,
160 161
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService
162
	) {
163
		super();
S
Sandeep Somavarapu 已提交
164 165 166
		this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService);
		this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
		this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService);
167

S
Sandeep Somavarapu 已提交
168
		this._replaceHistoryDelayer = new Delayer<void>(500);
169

170
		this.render(container, options);
171 172 173 174 175 176

		this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('editor.accessibilitySupport')) {
				this.updateAccessibilitySupport();
			}
		});
I
isidor 已提交
177
		this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.updateAccessibilitySupport());
178
		this.updateAccessibilitySupport();
179 180
	}

R
Rob Lourens 已提交
181
	focus(select: boolean = true, focusReplace: boolean = false, suppressGlobalSearchBuffer = false): void {
182 183
		this.ignoreGlobalFindBufferOnNextFocus = suppressGlobalSearchBuffer;

184 185 186 187 188 189 190 191 192 193 194 195 196
		if (focusReplace && this.isReplaceShown()) {
			this.replaceInput.focus();
			if (select) {
				this.replaceInput.select();
			}
		} else {
			this.searchInput.focus();
			if (select) {
				this.searchInput.select();
			}
		}
	}

R
Rob Lourens 已提交
197
	setWidth(width: number) {
198
		this.searchInput.inputBox.layout();
J
Johannes Rieken 已提交
199
		this.replaceInput.width = width - 28;
J
jeanp413 已提交
200
		this.replaceInput.inputBox.layout();
201 202
	}

R
Rob Lourens 已提交
203
	clear() {
204
		this.searchInput.clear();
J
jeanp413 已提交
205
		this.replaceInput.setValue('');
S
Sandeep Somavarapu 已提交
206
		this.setReplaceAllActionState(false);
207 208
	}

R
Rob Lourens 已提交
209
	isReplaceShown(): boolean {
210
		return !this.replaceContainer.classList.contains('disabled');
211 212
	}

S
Sandeep Somavarapu 已提交
213
	isReplaceActive(): boolean {
214
		return !!this.replaceActive.get();
S
Sandeep Somavarapu 已提交
215 216
	}

R
Rob Lourens 已提交
217
	getReplaceValue(): string {
J
jeanp413 已提交
218
		return this.replaceInput.getValue();
219 220
	}

R
Rob Lourens 已提交
221
	toggleReplace(show?: boolean): void {
R
Rob Lourens 已提交
222
		if (show === undefined || show !== this.isReplaceShown()) {
223
			this.onToggleReplaceButton();
S
Sandeep Somavarapu 已提交
224 225 226
		}
	}

R
Rob Lourens 已提交
227
	getSearchHistory(): string[] {
228
		return this.searchInput.inputBox.getHistory();
229 230
	}

R
Rob Lourens 已提交
231
	getReplaceHistory(): string[] {
J
jeanp413 已提交
232
		return this.replaceInput.inputBox.getHistory();
233 234
	}

R
Rob Lourens 已提交
235
	clearHistory(): void {
236
		this.searchInput.inputBox.clearHistory();
237 238
	}

R
Rob Lourens 已提交
239
	showNextSearchTerm() {
240
		this.searchInput.inputBox.showNextValue();
241 242
	}

R
Rob Lourens 已提交
243
	showPreviousSearchTerm() {
244
		this.searchInput.inputBox.showPreviousValue();
245 246
	}

R
Rob Lourens 已提交
247
	showNextReplaceTerm() {
J
jeanp413 已提交
248
		this.replaceInput.inputBox.showNextValue();
249 250
	}

R
Rob Lourens 已提交
251
	showPreviousReplaceTerm() {
J
jeanp413 已提交
252
		this.replaceInput.inputBox.showPreviousValue();
253 254
	}

R
Rob Lourens 已提交
255
	searchInputHasFocus(): boolean {
M
Matt Bierner 已提交
256
		return !!this.searchInputBoxFocused.get();
257 258
	}

R
Rob Lourens 已提交
259
	replaceInputHasFocus(): boolean {
J
jeanp413 已提交
260
		return this.replaceInput.inputBox.hasFocus();
261 262
	}

R
Rob Lourens 已提交
263
	focusReplaceAllAction(): void {
S
Sandeep Somavarapu 已提交
264 265 266
		this.replaceActionBar.focus(true);
	}

R
Rob Lourens 已提交
267
	focusRegexAction(): void {
S
Sandeep Somavarapu 已提交
268 269 270
		this.searchInput.focusOnRegex();
	}

271 272 273 274
	private render(container: HTMLElement, options: ISearchWidgetOptions): void {
		this.domNode = dom.append(container, dom.$('.search-widget'));
		this.domNode.style.position = 'relative';

275 276 277
		if (!options._hideReplaceToggle) {
			this.renderToggleReplaceButton(this.domNode);
		}
278 279

		this.renderSearchInput(this.domNode, options);
280
		this.renderReplaceInput(this.domNode, options);
281 282
	}

283
	private updateAccessibilitySupport(): void {
I
isidor 已提交
284
		this.searchInput.setFocusInputOnOptionClick(!this.accessibilityService.isScreenReaderOptimized());
285 286
	}

287
	private renderToggleReplaceButton(parent: HTMLElement): void {
288
		const opts: IButtonOptions = {
M
Matt Bierner 已提交
289 290 291 292
			buttonBackground: undefined,
			buttonBorder: undefined,
			buttonForeground: undefined,
			buttonHoverBackground: undefined
293 294
		};
		this.toggleReplaceButton = this._register(new Button(parent, opts));
295
		this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false');
M
Martin Aeschlimann 已提交
296
		dom.addClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames);
R
Rob Lourens 已提交
297
		this.toggleReplaceButton.icon = 'toggle-replace-button';
J
Joao Moreno 已提交
298 299
		// TODO@joh need to dispose this listener eventually
		this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton());
300
		this.toggleReplaceButton.element.title = nls.localize('search.replace.toggle.button.title', "Toggle Replace");
301 302 303
	}

	private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
304
		const inputOptions: IFindInputOptions = {
305
			label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
306
			validation: (value: string) => this.validateSearchInput(value),
S
Sandeep Somavarapu 已提交
307
			placeholder: nls.localize('search.placeHolder', "Search"),
S
Sandeep Somavarapu 已提交
308 309 310
			appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keyBindingService),
			appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService),
			appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService),
311
			history: options.searchHistory,
312
			flexibleHeight: true,
313
			flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT
314 315
		};

316
		const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box'));
317
		this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, true));
J
jeanp413 已提交
318
		this._register(attachFindReplaceInputBoxStyler(this.searchInput, this.themeService));
S
Sandeep Somavarapu 已提交
319
		this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
320 321 322 323
		this.searchInput.setValue(options.value || '');
		this.searchInput.setRegex(!!options.isRegex);
		this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
		this.searchInput.setWholeWords(!!options.isWholeWords);
324 325 326 327
		this._register(this.searchInput.onCaseSensitiveKeyDown((keyboardEvent: IKeyboardEvent) => this.onCaseSensitiveKeyDown(keyboardEvent)));
		this._register(this.searchInput.onRegexKeyDown((keyboardEvent: IKeyboardEvent) => this.onRegexKeyDown(keyboardEvent)));
		this._register(this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged()));
		this._register(this.searchInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire()));
328

329
		this._register(this.onReplaceValueChanged(() => {
J
jeanp413 已提交
330
			this._replaceHistoryDelayer.trigger(() => this.replaceInput.inputBox.addToHistory());
331 332
		}));

S
Sandeep Somavarapu 已提交
333
		this.searchInputFocusTracker = this._register(dom.trackFocus(this.searchInput.inputBox.inputElement));
334
		this._register(this.searchInputFocusTracker.onDidFocus(async () => {
335 336
			this.searchInputBoxFocused.set(true);

R
Rob Lourens 已提交
337
			const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
338
			if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) {
339
				const globalBufferText = await this.clipboardServce.readFindText();
340
				if (this.previousGlobalFindBufferValue !== globalBufferText) {
S
Sandeep Somavarapu 已提交
341
					this.searchInput.inputBox.addToHistory();
342 343 344 345 346 347 348 349 350
					this.searchInput.setValue(globalBufferText);
					this.searchInput.select();
				}

				this.previousGlobalFindBufferValue = globalBufferText;
			}

			this.ignoreGlobalFindBufferOnNextFocus = false;
		}));
351
		this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
352 353


354
		this.showContextCheckbox = new Checkbox({ isChecked: false, title: nls.localize('showContext', "Show Context"), icon: searchShowContextIcon });
355
		this._register(this.showContextCheckbox.onChange(() => this.onContextLinesChanged()));
356 357 358 359

		if (options.showContextToggle) {
			this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' });
			dom.addClass(this.contextLinesInput.element, 'context-lines-input');
360
			this.contextLinesInput.value = '' + (this.configurationService.getValue<ISearchConfigurationProperties>('search').searchEditor.defaultNumberOfContextLines ?? 1);
361
			this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged()));
J
Jackson Kearl 已提交
362
			this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService));
363 364 365 366
			dom.append(searchInputContainer, this.showContextCheckbox.domNode);
		}
	}

367 368 369 370 371 372 373 374 375 376 377
	private onContextLinesChanged() {
		dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked);
		this._onDidToggleContext.fire();

		if (this.contextLinesInput.value.includes('-')) {
			this.contextLinesInput.value = '0';
		}

		this._onDidToggleContext.fire();
	}

378 379 380 381 382 383 384 385
	public setContextLines(lines: number) {
		if (!this.contextLinesInput) { return; }
		if (lines === 0) {
			this.showContextCheckbox.checked = false;
		} else {
			this.showContextCheckbox.checked = true;
			this.contextLinesInput.value = '' + lines;
		}
386
		dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked);
387 388
	}

389
	private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
J
Joao Moreno 已提交
390
		this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
391 392
		const replaceBox = dom.append(this.replaceContainer, dom.$('.replace-input'));

J
jeanp413 已提交
393 394
		this.replaceInput = this._register(new ContextScopedReplaceInput(replaceBox, this.contextViewService, {
			label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
395
			placeholder: nls.localize('search.replace.placeHolder', "Replace"),
396
			appendPreserveCaseLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.TogglePreserveCaseId), this.keyBindingService),
J
jeanp413 已提交
397
			history: options.replaceHistory,
398 399
			flexibleHeight: true,
			flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT
J
jeanp413 已提交
400
		}, this.contextKeyService, true));
401

J
jeanp413 已提交
402
		this._register(this.replaceInput.onDidOptionChange(viaKeyboard => {
403
			if (!viaKeyboard) {
J
jeanp413 已提交
404
				this._onPreserveCaseChange.fire(this.replaceInput.getPreserveCase());
405 406 407
			}
		}));

J
jeanp413 已提交
408 409 410 411 412
		this._register(attachFindReplaceInputBoxStyler(this.replaceInput, this.themeService));
		this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent));
		this.replaceInput.setValue(options.replaceValue || '');
		this._register(this.replaceInput.inputBox.onDidChange(() => this._onReplaceValueChanged.fire()));
		this._register(this.replaceInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire()));
S
Sandeep Somavarapu 已提交
413

414
		this.replaceAllAction = new ReplaceAllAction(this);
S
Sandeep Somavarapu 已提交
415 416 417
		this.replaceAllAction.label = SearchWidget.REPLACE_ALL_DISABLED_LABEL;
		this.replaceActionBar = this._register(new ActionBar(this.replaceContainer));
		this.replaceActionBar.push([this.replaceAllAction], { icon: true, label: false });
S
Sandeep Somavarapu 已提交
418
		this.onkeydown(this.replaceActionBar.domNode, (keyboardEvent) => this.onReplaceActionbarKeyDown(keyboardEvent));
419

J
jeanp413 已提交
420
		this.replaceInputFocusTracker = this._register(dom.trackFocus(this.replaceInput.inputBox.inputElement));
421 422
		this._register(this.replaceInputFocusTracker.onDidFocus(() => this.replaceInputBoxFocused.set(true)));
		this._register(this.replaceInputFocusTracker.onDidBlur(() => this.replaceInputBoxFocused.set(false)));
423
		this._register(this.replaceInput.onPreserveCaseKeyDown((keyboardEvent: IKeyboardEvent) => this.onPreserveCaseKeyDown(keyboardEvent)));
424 425
	}

426
	triggerReplaceAll(): Promise<any> {
427
		this._onReplaceAll.fire();
428
		return Promise.resolve(null);
429 430
	}

J
Johannes Rieken 已提交
431
	private onToggleReplaceButton(): void {
S
Sandeep Somavarapu 已提交
432
		dom.toggleClass(this.replaceContainer, 'disabled');
M
Martin Aeschlimann 已提交
433 434 435 436 437 438 439
		if (this.isReplaceShown()) {
			dom.removeClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames);
			dom.addClasses(this.toggleReplaceButton.element, searchShowReplaceIcon.classNames);
		} else {
			dom.removeClasses(this.toggleReplaceButton.element, searchShowReplaceIcon.classNames);
			dom.addClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames);
		}
440
		this.toggleReplaceButton.element.setAttribute('aria-expanded', this.isReplaceShown() ? 'true' : 'false');
441
		this.updateReplaceActiveState();
442
		this._onReplaceToggled.fire();
443 444
	}

445
	setValue(value: string) {
446
		this.searchInput.setValue(value);
447 448
	}

R
Rob Lourens 已提交
449
	setReplaceAllActionState(enabled: boolean): void {
450
		if (this.replaceAllAction.enabled !== enabled) {
J
Johannes Rieken 已提交
451
			this.replaceAllAction.enabled = enabled;
S
Sandeep Somavarapu 已提交
452
			this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
453 454 455 456 457
			this.updateReplaceActiveState();
		}
	}

	private updateReplaceActiveState(): void {
458 459
		const currentState = this.isReplaceActive();
		const newState = this.isReplaceShown() && this.replaceAllAction.enabled;
460
		if (currentState !== newState) {
S
Sandeep Somavarapu 已提交
461 462
			this.replaceActive.set(newState);
			this._onReplaceStateChange.fire(newState);
J
jeanp413 已提交
463
			this.replaceInput.inputBox.layout();
464
		}
465 466
	}

M
Matt Bierner 已提交
467
	private validateSearchInput(value: string): IMessage | null {
468 469 470 471 472 473 474
		if (value.length === 0) {
			return null;
		}
		if (!this.searchInput.getRegex()) {
			return null;
		}
		try {
475
			new RegExp(value, 'u');
476 477 478
		} catch (e) {
			return { content: e.message };
		}
479 480

		return null;
481 482
	}

S
Sandeep Somavarapu 已提交
483
	private onSearchInputChanged(): void {
484
		this.searchInput.clearMessage();
S
Sandeep Somavarapu 已提交
485
		this.setReplaceAllActionState(false);
486 487

		if (this.searchConfiguration.searchOnType) {
J
Jackson Kearl 已提交
488 489 490 491
			if (this.searchInput.getRegex()) {
				try {
					const regex = new RegExp(this.searchInput.getValue(), 'ug');
					const matchienessHeuristic = `
492 493 494 495 496 497 498 499
								~!@#$%^&*()_+
								\`1234567890-=
								qwertyuiop[]\\
								QWERTYUIOP{}|
								asdfghjkl;'
								ASDFGHJKL:"
								zxcvbnm,./
								ZXCVBNM<>? `.match(regex)?.length ?? 0;
500

J
Jackson Kearl 已提交
501 502 503 504 505 506 507 508
					const delayMultiplier =
						matchienessHeuristic < 50 ? 1 :
							matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w`
								10; // only things matching empty string

					this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier);
				} catch {
					// pass
509
				}
J
Jackson Kearl 已提交
510 511
			} else {
				this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod);
512
			}
513
		}
S
Sandeep Somavarapu 已提交
514 515
	}

S
Sandeep Somavarapu 已提交
516
	private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
517 518 519 520 521
		if (keyboardEvent.equals(ctrlKeyMod | KeyCode.Enter)) {
			this.searchInput.inputBox.insertAtCursor('\n');
			keyboardEvent.preventDefault();
		}

S
Sandeep Somavarapu 已提交
522
		if (keyboardEvent.equals(KeyCode.Enter)) {
523
			this.searchInput.onSearchSubmit();
S
Sandeep Somavarapu 已提交
524 525 526 527 528
			this.submitSearch();
			keyboardEvent.preventDefault();
		}

		else if (keyboardEvent.equals(KeyCode.Escape)) {
529
			this._onSearchCancel.fire({ focus: true });
S
Sandeep Somavarapu 已提交
530 531 532 533 534 535 536 537 538 539 540
			keyboardEvent.preventDefault();
		}

		else if (keyboardEvent.equals(KeyCode.Tab)) {
			if (this.isReplaceShown()) {
				this.replaceInput.focus();
			} else {
				this.searchInput.focusOnCaseSensitive();
			}
			keyboardEvent.preventDefault();
		}
541 542

		else if (keyboardEvent.equals(KeyCode.UpArrow)) {
J
jeanp413 已提交
543
			stopPropagationForMultiLineUpwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
544 545 546
		}

		else if (keyboardEvent.equals(KeyCode.DownArrow)) {
J
jeanp413 已提交
547
			stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
548
		}
S
Sandeep Somavarapu 已提交
549 550 551 552 553 554 555 556 557 558 559 560
	}

	private onCaseSensitiveKeyDown(keyboardEvent: IKeyboardEvent) {
		if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
			if (this.isReplaceShown()) {
				this.replaceInput.focus();
				keyboardEvent.preventDefault();
			}
		}
	}

	private onRegexKeyDown(keyboardEvent: IKeyboardEvent) {
561 562 563 564 565 566 567 568 569
		if (keyboardEvent.equals(KeyCode.Tab)) {
			if (this.isReplaceShown()) {
				this.replaceInput.focusOnPreserve();
				keyboardEvent.preventDefault();
			}
		}
	}

	private onPreserveCaseKeyDown(keyboardEvent: IKeyboardEvent) {
S
Sandeep Somavarapu 已提交
570 571 572 573 574 575 576
		if (keyboardEvent.equals(KeyCode.Tab)) {
			if (this.isReplaceActive()) {
				this.focusReplaceAllAction();
			} else {
				this._onBlur.fire();
			}
			keyboardEvent.preventDefault();
577
		}
578 579 580 581
		else if (KeyMod.Shift | KeyCode.Tab) {
			this.focusRegexAction();
			keyboardEvent.preventDefault();
		}
582 583
	}

S
Sandeep Somavarapu 已提交
584
	private onReplaceInputKeyDown(keyboardEvent: IKeyboardEvent) {
585
		if (keyboardEvent.equals(ctrlKeyMod | KeyCode.Enter)) {
J
jeanp413 已提交
586
			this.replaceInput.inputBox.insertAtCursor('\n');
587 588 589
			keyboardEvent.preventDefault();
		}

S
Sandeep Somavarapu 已提交
590 591 592 593 594 595 596 597 598 599 600 601 602
		if (keyboardEvent.equals(KeyCode.Enter)) {
			this.submitSearch();
			keyboardEvent.preventDefault();
		}

		else if (keyboardEvent.equals(KeyCode.Tab)) {
			this.searchInput.focusOnCaseSensitive();
			keyboardEvent.preventDefault();
		}

		else if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
			this.searchInput.focus();
			keyboardEvent.preventDefault();
603
		}
604 605

		else if (keyboardEvent.equals(KeyCode.UpArrow)) {
J
jeanp413 已提交
606
			stopPropagationForMultiLineUpwards(keyboardEvent, this.replaceInput.getValue(), this.replaceInput.domNode.querySelector('textarea'));
607 608 609
		}

		else if (keyboardEvent.equals(KeyCode.DownArrow)) {
J
jeanp413 已提交
610
			stopPropagationForMultiLineDownwards(keyboardEvent, this.replaceInput.getValue(), this.replaceInput.domNode.querySelector('textarea'));
611
		}
612 613
	}

S
Sandeep Somavarapu 已提交
614 615 616 617 618 619 620
	private onReplaceActionbarKeyDown(keyboardEvent: IKeyboardEvent) {
		if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
			this.focusRegexAction();
			keyboardEvent.preventDefault();
		}
	}

621
	private async submitSearch(triggeredOnType = false, delay: number = 0): Promise<void> {
R
Rob Lourens 已提交
622 623 624 625 626
		this.searchInput.validate();
		if (!this.searchInput.inputBox.isInputValid()) {
			return;
		}

627
		const value = this.searchInput.getValue();
R
Rob Lourens 已提交
628
		const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
629
		if (value && useGlobalFindBuffer) {
630
			await this.clipboardServce.writeFindText(value);
631
		}
632
		this._onSearchSubmit.fire({ triggeredOnType, delay });
633 634
	}

635
	getContextLines() {
636 637 638
		return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0;
	}

639 640 641 642 643 644 645
	modifyContextLines(increase: boolean) {
		const current = +this.contextLinesInput.value;
		const modified = current + (increase ? 1 : -1);
		this.showContextCheckbox.checked = modified !== 0;
		this.contextLinesInput.value = '' + modified;
	}

646 647 648 649 650
	toggleContextLines() {
		this.showContextCheckbox.checked = !this.showContextCheckbox.checked;
		this.onContextLinesChanged();
	}

R
Rob Lourens 已提交
651
	dispose(): void {
652
		this.setReplaceAllActionState(false);
653 654
		super.dispose();
	}
R
Rob Lourens 已提交
655 656 657 658

	private get searchConfiguration(): ISearchConfigurationProperties {
		return this.configurationService.getValue<ISearchConfigurationProperties>('search');
	}
659 660 661
}

export function registerContributions() {
J
Johannes Rieken 已提交
662 663
	KeybindingsRegistry.registerCommandAndKeybindingRule({
		id: ReplaceAllAction.ID,
664
		weight: KeybindingWeight.WorkbenchContrib,
I
isidor 已提交
665
		when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE),
666 667
		primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter,
		handler: accessor => {
668 669 670 671 672 673
			const viewsService = accessor.get(IViewsService);
			if (isSearchViewFocused(viewsService)) {
				const searchView = getSearchView(viewsService);
				if (searchView) {
					new ReplaceAllAction(searchView.searchAndReplaceWidget).run();
				}
674 675 676
			}
		}
	});
A
Alex Dima 已提交
677
}