searchWidget.ts 25.5 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 31
import { appendKeyBindingLabel, isSearchViewFocused } from 'vs/workbench/contrib/search/browser/searchActions';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
32 33 34
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
35
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
36
import { isMacintosh } from 'vs/base/common/platform';
37
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
38

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

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

55 56
class ReplaceAllAction extends Action {

57
	private static fgInstance: ReplaceAllAction | null = null;
R
Rob Lourens 已提交
58
	static readonly ID: string = 'search.action.replaceAll';
59

J
Johannes Rieken 已提交
60
	static get INSTANCE(): ReplaceAllAction {
61
		if (ReplaceAllAction.fgInstance === null) {
J
Johannes Rieken 已提交
62
			ReplaceAllAction.fgInstance = new ReplaceAllAction();
63 64 65 66
		}
		return ReplaceAllAction.fgInstance;
	}

67
	private _searchWidget: SearchWidget | null = null;
68 69

	constructor() {
70
		super(ReplaceAllAction.ID, '', 'codicon-replace-all', false);
71 72 73
	}

	set searchWidget(searchWidget: SearchWidget) {
J
Johannes Rieken 已提交
74
		this._searchWidget = searchWidget;
75 76
	}

77
	run(): Promise<any> {
78 79 80
		if (this._searchWidget) {
			return this._searchWidget.triggerReplaceAll();
		}
81
		return Promise.resolve(null);
82 83 84
	}
}

85 86
const ctrlKeyMod = (isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);

J
jeanp413 已提交
87 88
function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
	const isMultiline = !!value.match(/\n/);
89
	if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionStart > 0) {
J
jeanp413 已提交
90 91 92 93 94 95 96
		event.stopPropagation();
		return;
	}
}

function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
	const isMultiline = !!value.match(/\n/);
97
	if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionEnd < textarea.value.length) {
J
jeanp413 已提交
98 99 100 101 102
		event.stopPropagation();
		return;
	}
}

103 104
export class SearchWidget extends Widget {

105 106
	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 => {
107
		const kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID);
B
Benjamin Pasero 已提交
108
		return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb, keyBindingService2);
109
	};
S
Sandeep Somavarapu 已提交
110

111
	domNode!: HTMLElement;
112

113 114
	searchInput!: FindInput;
	searchInputFocusTracker!: dom.IFocusTracker;
115
	private searchInputBoxFocused: IContextKey<boolean>;
116

117
	private replaceContainer!: HTMLElement;
J
jeanp413 已提交
118 119 120
	replaceInput!: ReplaceInput;
	replaceInputFocusTracker!: dom.IFocusTracker;
	private replaceInputBoxFocused: IContextKey<boolean>;
121 122
	private toggleReplaceButton!: Button;
	private replaceAllAction!: ReplaceAllAction;
A
Alex Dima 已提交
123
	private replaceActive: IContextKey<boolean>;
124
	private replaceActionBar!: ActionBar;
S
Sandeep Somavarapu 已提交
125
	private _replaceHistoryDelayer: Delayer<void>;
126
	private _searchDelayer: Delayer<void>;
127
	private ignoreGlobalFindBufferOnNextFocus = false;
128
	private previousGlobalFindBufferValue: string | null = null;
129

130 131
	private _onSearchSubmit = this._register(new Emitter<boolean>());
	readonly onSearchSubmit: Event<boolean /* triggeredOnType */> = this._onSearchSubmit.event;
132

133 134
	private _onSearchCancel = this._register(new Emitter<{ focus: boolean }>());
	readonly onSearchCancel: Event<{ focus: boolean }> = this._onSearchCancel.event;
135 136

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

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

142 143 144
	private _onPreserveCaseChange = this._register(new Emitter<boolean>());
	readonly onPreserveCaseChange: Event<boolean> = this._onPreserveCaseChange.event;

U
Ubuntu 已提交
145 146
	private _onReplaceValueChanged = this._register(new Emitter<void>());
	readonly onReplaceValueChanged: Event<void> = this._onReplaceValueChanged.event;
147 148

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

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

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

157 158 159
	private readonly _onDidToggleContext = new Emitter<void>();
	readonly onDidToggleContext: Event<void> = this._onDidToggleContext.event;

160
	private temporarilySkipSearchOnChange = false;
161 162
	private showContextCheckbox!: Checkbox;
	private contextLinesInput!: InputBox;
163

164
	constructor(
165
		container: HTMLElement,
166
		options: ISearchWidgetOptions,
167 168 169 170 171
		@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,
172 173
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService
174
	) {
175
		super();
S
Sandeep Somavarapu 已提交
176 177 178
		this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService);
		this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
		this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService);
179

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

182
		this._searchDelayer = this._register(new Delayer<void>(this.searchConfiguration.searchOnTypeDebouncePeriod));
183
		this.render(container, options);
184 185 186 187 188 189

		this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('editor.accessibilitySupport')) {
				this.updateAccessibilitySupport();
			}
		});
190
		this.accessibilityService.onDidChangeAccessibilitySupport(() => this.updateAccessibilitySupport());
191
		this.updateAccessibilitySupport();
192 193
	}

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

197 198 199 200 201 202 203 204 205 206 207 208 209
		if (focusReplace && this.isReplaceShown()) {
			this.replaceInput.focus();
			if (select) {
				this.replaceInput.select();
			}
		} else {
			this.searchInput.focus();
			if (select) {
				this.searchInput.select();
			}
		}
	}

R
Rob Lourens 已提交
210
	setWidth(width: number) {
211
		this.searchInput.inputBox.layout();
J
Johannes Rieken 已提交
212
		this.replaceInput.width = width - 28;
J
jeanp413 已提交
213
		this.replaceInput.inputBox.layout();
214 215
	}

R
Rob Lourens 已提交
216
	clear() {
217
		this.searchInput.clear();
J
jeanp413 已提交
218
		this.replaceInput.setValue('');
S
Sandeep Somavarapu 已提交
219
		this.setReplaceAllActionState(false);
220 221
	}

R
Rob Lourens 已提交
222
	isReplaceShown(): boolean {
S
Sandeep Somavarapu 已提交
223
		return !dom.hasClass(this.replaceContainer, 'disabled');
224 225
	}

S
Sandeep Somavarapu 已提交
226
	isReplaceActive(): boolean {
227
		return !!this.replaceActive.get();
S
Sandeep Somavarapu 已提交
228 229
	}

R
Rob Lourens 已提交
230
	getReplaceValue(): string {
J
jeanp413 已提交
231
		return this.replaceInput.getValue();
232 233
	}

R
Rob Lourens 已提交
234
	toggleReplace(show?: boolean): void {
R
Rob Lourens 已提交
235
		if (show === undefined || show !== this.isReplaceShown()) {
236
			this.onToggleReplaceButton();
S
Sandeep Somavarapu 已提交
237 238 239
		}
	}

R
Rob Lourens 已提交
240
	getSearchHistory(): string[] {
241
		return this.searchInput.inputBox.getHistory();
242 243
	}

R
Rob Lourens 已提交
244
	getReplaceHistory(): string[] {
J
jeanp413 已提交
245
		return this.replaceInput.inputBox.getHistory();
246 247
	}

R
Rob Lourens 已提交
248
	clearHistory(): void {
249
		this.searchInput.inputBox.clearHistory();
250 251
	}

R
Rob Lourens 已提交
252
	showNextSearchTerm() {
253
		this.searchInput.inputBox.showNextValue();
254 255
	}

R
Rob Lourens 已提交
256
	showPreviousSearchTerm() {
257
		this.searchInput.inputBox.showPreviousValue();
258 259
	}

R
Rob Lourens 已提交
260
	showNextReplaceTerm() {
J
jeanp413 已提交
261
		this.replaceInput.inputBox.showNextValue();
262 263
	}

R
Rob Lourens 已提交
264
	showPreviousReplaceTerm() {
J
jeanp413 已提交
265
		this.replaceInput.inputBox.showPreviousValue();
266 267
	}

R
Rob Lourens 已提交
268
	searchInputHasFocus(): boolean {
M
Matt Bierner 已提交
269
		return !!this.searchInputBoxFocused.get();
270 271
	}

R
Rob Lourens 已提交
272
	replaceInputHasFocus(): boolean {
J
jeanp413 已提交
273
		return this.replaceInput.inputBox.hasFocus();
274 275
	}

R
Rob Lourens 已提交
276
	focusReplaceAllAction(): void {
S
Sandeep Somavarapu 已提交
277 278 279
		this.replaceActionBar.focus(true);
	}

R
Rob Lourens 已提交
280
	focusRegexAction(): void {
S
Sandeep Somavarapu 已提交
281 282 283
		this.searchInput.focusOnRegex();
	}

284 285 286 287
	private render(container: HTMLElement, options: ISearchWidgetOptions): void {
		this.domNode = dom.append(container, dom.$('.search-widget'));
		this.domNode.style.position = 'relative';

288 289 290
		if (!options._hideReplaceToggle) {
			this.renderToggleReplaceButton(this.domNode);
		}
291 292

		this.renderSearchInput(this.domNode, options);
293
		this.renderReplaceInput(this.domNode, options);
294 295
	}

296
	private isScreenReaderOptimized() {
297
		const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
298 299 300 301
		const config = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
		return config === 'on' || (config === 'auto' && detected);
	}

302
	private updateAccessibilitySupport(): void {
303
		this.searchInput.setFocusInputOnOptionClick(!this.isScreenReaderOptimized());
304 305
	}

306
	private renderToggleReplaceButton(parent: HTMLElement): void {
307
		const opts: IButtonOptions = {
M
Matt Bierner 已提交
308 309 310 311
			buttonBackground: undefined,
			buttonBorder: undefined,
			buttonForeground: undefined,
			buttonHoverBackground: undefined
312 313
		};
		this.toggleReplaceButton = this._register(new Button(parent, opts));
314
		this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false');
315 316
		this.toggleReplaceButton.element.classList.add('codicon');
		this.toggleReplaceButton.element.classList.add('codicon-chevron-right');
R
Rob Lourens 已提交
317
		this.toggleReplaceButton.icon = 'toggle-replace-button';
J
Joao Moreno 已提交
318 319
		// TODO@joh need to dispose this listener eventually
		this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton());
320
		this.toggleReplaceButton.element.title = nls.localize('search.replace.toggle.button.title', "Toggle Replace");
321 322 323
	}

	private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
324
		const inputOptions: IFindInputOptions = {
325
			label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
326
			validation: (value: string) => this.validateSearchInput(value),
S
Sandeep Somavarapu 已提交
327
			placeholder: nls.localize('search.placeHolder', "Search"),
S
Sandeep Somavarapu 已提交
328 329 330
			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),
331 332
			history: options.searchHistory,
			flexibleHeight: true
333 334
		};

335
		const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box'));
336
		this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, true));
J
jeanp413 已提交
337
		this._register(attachFindReplaceInputBoxStyler(this.searchInput, this.themeService));
S
Sandeep Somavarapu 已提交
338
		this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
339 340 341 342
		this.searchInput.setValue(options.value || '');
		this.searchInput.setRegex(!!options.isRegex);
		this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
		this.searchInput.setWholeWords(!!options.isWholeWords);
343 344 345 346
		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()));
347

348
		this._register(this.onReplaceValueChanged(() => {
J
jeanp413 已提交
349
			this._replaceHistoryDelayer.trigger(() => this.replaceInput.inputBox.addToHistory());
350 351
		}));

S
Sandeep Somavarapu 已提交
352
		this.searchInputFocusTracker = this._register(dom.trackFocus(this.searchInput.inputBox.inputElement));
353 354 355
		this._register(this.searchInputFocusTracker.onDidFocus(() => {
			this.searchInputBoxFocused.set(true);

R
Rob Lourens 已提交
356
			const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
357 358 359
			if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) {
				const globalBufferText = this.clipboardServce.readFindText();
				if (this.previousGlobalFindBufferValue !== globalBufferText) {
S
Sandeep Somavarapu 已提交
360
					this.searchInput.inputBox.addToHistory();
361 362 363 364 365 366 367 368 369
					this.searchInput.setValue(globalBufferText);
					this.searchInput.select();
				}

				this.previousGlobalFindBufferValue = globalBufferText;
			}

			this.ignoreGlobalFindBufferOnNextFocus = false;
		}));
370
		this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386


		this.showContextCheckbox = new Checkbox({ isChecked: false, title: nls.localize('showContext', "Show Context"), actionClassName: 'codicon-list-selection' });
		this._register(this.showContextCheckbox.onChange(() => {
			dom.toggleClass(parent, 'show-context', this.showContextCheckbox.checked);
			this._onDidToggleContext.fire();
		}));

		if (options.showContextToggle) {
			this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' });
			dom.addClass(this.contextLinesInput.element, 'context-lines-input');
			this.contextLinesInput.value = '2';
			this._register(this.contextLinesInput.onDidChange(() => {
				if (this.contextLinesInput.value.includes('-')) {
					this.contextLinesInput.value = '0';
				}
387
				this._onDidToggleContext.fire();
388
			}));
J
Jackson Kearl 已提交
389
			this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService));
390 391 392 393 394 395 396 397 398 399 400 401
			dom.append(searchInputContainer, this.showContextCheckbox.domNode);
		}
	}

	public setContextLines(lines: number) {
		if (!this.contextLinesInput) { return; }
		if (lines === 0) {
			this.showContextCheckbox.checked = false;
		} else {
			this.showContextCheckbox.checked = true;
			this.contextLinesInput.value = '' + lines;
		}
402
		dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked);
403 404
	}

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

J
jeanp413 已提交
409 410
		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'),
411
			placeholder: nls.localize('search.replace.placeHolder', "Replace"),
J
jeanp413 已提交
412
			history: options.replaceHistory,
413
			flexibleHeight: true
J
jeanp413 已提交
414
		}, this.contextKeyService, true));
415

J
jeanp413 已提交
416
		this._register(this.replaceInput.onDidOptionChange(viaKeyboard => {
417
			if (!viaKeyboard) {
J
jeanp413 已提交
418
				this._onPreserveCaseChange.fire(this.replaceInput.getPreserveCase());
419 420 421
			}
		}));

J
jeanp413 已提交
422 423 424 425 426
		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 已提交
427 428

		this.replaceAllAction = ReplaceAllAction.INSTANCE;
J
Johannes Rieken 已提交
429
		this.replaceAllAction.searchWidget = this;
S
Sandeep Somavarapu 已提交
430 431 432
		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 已提交
433
		this.onkeydown(this.replaceActionBar.domNode, (keyboardEvent) => this.onReplaceActionbarKeyDown(keyboardEvent));
434

J
jeanp413 已提交
435
		this.replaceInputFocusTracker = this._register(dom.trackFocus(this.replaceInput.inputBox.inputElement));
436 437
		this._register(this.replaceInputFocusTracker.onDidFocus(() => this.replaceInputBoxFocused.set(true)));
		this._register(this.replaceInputFocusTracker.onDidBlur(() => this.replaceInputBoxFocused.set(false)));
438
		this._register(this.replaceInput.onPreserveCaseKeyDown((keyboardEvent: IKeyboardEvent) => this.onPreserveCaseKeyDown(keyboardEvent)));
439 440
	}

441
	triggerReplaceAll(): Promise<any> {
442
		this._onReplaceAll.fire();
443
		return Promise.resolve(null);
444 445
	}

J
Johannes Rieken 已提交
446
	private onToggleReplaceButton(): void {
S
Sandeep Somavarapu 已提交
447
		dom.toggleClass(this.replaceContainer, 'disabled');
448 449
		dom.toggleClass(this.toggleReplaceButton.element, 'codicon-chevron-right');
		dom.toggleClass(this.toggleReplaceButton.element, 'codicon-chevron-down');
450
		this.toggleReplaceButton.element.setAttribute('aria-expanded', this.isReplaceShown() ? 'true' : 'false');
451
		this.updateReplaceActiveState();
452
		this._onReplaceToggled.fire();
453 454
	}

455 456
	setValue(value: string, skipSearchOnChange: boolean) {
		this.temporarilySkipSearchOnChange = skipSearchOnChange || this.temporarilySkipSearchOnChange;
457
		this.searchInput.setValue(value);
458 459
	}

R
Rob Lourens 已提交
460
	setReplaceAllActionState(enabled: boolean): void {
461
		if (this.replaceAllAction.enabled !== enabled) {
J
Johannes Rieken 已提交
462
			this.replaceAllAction.enabled = enabled;
S
Sandeep Somavarapu 已提交
463
			this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
464 465 466 467 468
			this.updateReplaceActiveState();
		}
	}

	private updateReplaceActiveState(): void {
469 470
		const currentState = this.isReplaceActive();
		const newState = this.isReplaceShown() && this.replaceAllAction.enabled;
471
		if (currentState !== newState) {
S
Sandeep Somavarapu 已提交
472 473
			this.replaceActive.set(newState);
			this._onReplaceStateChange.fire(newState);
J
jeanp413 已提交
474
			this.replaceInput.inputBox.layout();
475
		}
476 477
	}

M
Matt Bierner 已提交
478
	private validateSearchInput(value: string): IMessage | null {
479 480 481 482 483 484 485
		if (value.length === 0) {
			return null;
		}
		if (!this.searchInput.getRegex()) {
			return null;
		}
		try {
486
			new RegExp(value, 'u');
487 488 489
		} catch (e) {
			return { content: e.message };
		}
490 491

		return null;
492 493
	}

S
Sandeep Somavarapu 已提交
494
	private onSearchInputChanged(): void {
495
		this.searchInput.clearMessage();
S
Sandeep Somavarapu 已提交
496
		this.setReplaceAllActionState(false);
497 498

		if (this.searchConfiguration.searchOnType) {
499 500 501 502
			if (this.temporarilySkipSearchOnChange) {
				this.temporarilySkipSearchOnChange = false;
			} else {
				this._onSearchCancel.fire({ focus: false });
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
				if (this.searchInput.getRegex()) {
					try {
						const regex = new RegExp(this.searchInput.getValue(), 'ug');
						const matchienessHeuristic = `
~!@#$%^&*()_+
\`1234567890-=
qwertyuiop[]\\
QWERTYUIOP{}|
asdfghjkl;'
ASDFGHJKL:"
zxcvbnm,./
ZXCVBNM<>? 	`.match(regex)?.length ?? 0;

						const delayMultiplier =
							matchienessHeuristic < 50 ? 1 :
								matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w`
									10; // only things matching empty string

						this._searchDelayer.trigger((() => this.submitSearch(true)), this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier);
					} catch {
						// pass
					}
				} else {
					this._searchDelayer.trigger((() => this.submitSearch(true)), this.searchConfiguration.searchOnTypeDebouncePeriod);
				}
528
			}
529
		}
S
Sandeep Somavarapu 已提交
530 531
	}

S
Sandeep Somavarapu 已提交
532
	private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
533 534 535 536 537
		if (keyboardEvent.equals(ctrlKeyMod | KeyCode.Enter)) {
			this.searchInput.inputBox.insertAtCursor('\n');
			keyboardEvent.preventDefault();
		}

S
Sandeep Somavarapu 已提交
538
		if (keyboardEvent.equals(KeyCode.Enter)) {
539
			this.searchInput.onSearchSubmit();
S
Sandeep Somavarapu 已提交
540 541 542 543 544
			this.submitSearch();
			keyboardEvent.preventDefault();
		}

		else if (keyboardEvent.equals(KeyCode.Escape)) {
545
			this._onSearchCancel.fire({ focus: true });
S
Sandeep Somavarapu 已提交
546 547 548 549 550 551 552 553 554 555 556
			keyboardEvent.preventDefault();
		}

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

		else if (keyboardEvent.equals(KeyCode.UpArrow)) {
J
jeanp413 已提交
559
			stopPropagationForMultiLineUpwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
560 561 562
		}

		else if (keyboardEvent.equals(KeyCode.DownArrow)) {
J
jeanp413 已提交
563
			stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
564
		}
S
Sandeep Somavarapu 已提交
565 566 567 568 569 570 571 572 573 574 575 576
	}

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

	private onRegexKeyDown(keyboardEvent: IKeyboardEvent) {
577 578 579 580 581 582 583 584 585
		if (keyboardEvent.equals(KeyCode.Tab)) {
			if (this.isReplaceShown()) {
				this.replaceInput.focusOnPreserve();
				keyboardEvent.preventDefault();
			}
		}
	}

	private onPreserveCaseKeyDown(keyboardEvent: IKeyboardEvent) {
S
Sandeep Somavarapu 已提交
586 587 588 589 590 591 592
		if (keyboardEvent.equals(KeyCode.Tab)) {
			if (this.isReplaceActive()) {
				this.focusReplaceAllAction();
			} else {
				this._onBlur.fire();
			}
			keyboardEvent.preventDefault();
593
		}
594 595 596 597
		else if (KeyMod.Shift | KeyCode.Tab) {
			this.focusRegexAction();
			keyboardEvent.preventDefault();
		}
598 599
	}

S
Sandeep Somavarapu 已提交
600
	private onReplaceInputKeyDown(keyboardEvent: IKeyboardEvent) {
601
		if (keyboardEvent.equals(ctrlKeyMod | KeyCode.Enter)) {
J
jeanp413 已提交
602
			this.replaceInput.inputBox.insertAtCursor('\n');
603 604 605
			keyboardEvent.preventDefault();
		}

S
Sandeep Somavarapu 已提交
606 607 608 609 610 611 612 613 614 615 616 617 618
		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();
619
		}
620 621

		else if (keyboardEvent.equals(KeyCode.UpArrow)) {
J
jeanp413 已提交
622
			stopPropagationForMultiLineUpwards(keyboardEvent, this.replaceInput.getValue(), this.replaceInput.domNode.querySelector('textarea'));
623 624 625
		}

		else if (keyboardEvent.equals(KeyCode.DownArrow)) {
J
jeanp413 已提交
626
			stopPropagationForMultiLineDownwards(keyboardEvent, this.replaceInput.getValue(), this.replaceInput.domNode.querySelector('textarea'));
627
		}
628 629
	}

S
Sandeep Somavarapu 已提交
630 631 632 633 634 635 636
	private onReplaceActionbarKeyDown(keyboardEvent: IKeyboardEvent) {
		if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
			this.focusRegexAction();
			keyboardEvent.preventDefault();
		}
	}

637
	private submitSearch(triggeredOnType = false): void {
R
Rob Lourens 已提交
638 639 640 641 642
		this.searchInput.validate();
		if (!this.searchInput.inputBox.isInputValid()) {
			return;
		}

643
		const value = this.searchInput.getValue();
R
Rob Lourens 已提交
644
		const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
645 646
		if (value && useGlobalFindBuffer) {
			this.clipboardServce.writeFindText(value);
647
		}
648
		this._onSearchSubmit.fire(triggeredOnType);
649 650
	}

651 652 653 654
	contextLines() {
		return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0;
	}

R
Rob Lourens 已提交
655
	dispose(): void {
656
		this.setReplaceAllActionState(false);
657 658
		super.dispose();
	}
R
Rob Lourens 已提交
659 660 661 662

	private get searchConfiguration(): ISearchConfigurationProperties {
		return this.configurationService.getValue<ISearchConfigurationProperties>('search');
	}
663 664 665
}

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