searchWidget.ts 15.6 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 7 8
import * as nls from 'vs/nls';
import * as strings from 'vs/base/common/strings';
import * as dom from 'vs/base/browser/dom';
9 10 11
import { TPromise } from 'vs/base/common/winjs.base';
import { Widget } from 'vs/base/browser/ui/widget';
import { Action } from 'vs/base/common/actions';
S
Sandeep Somavarapu 已提交
12
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
13
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
14
import { InputBox, IMessage } from 'vs/base/browser/ui/inputbox/inputBox';
15 16
import { Button } from 'vs/base/browser/ui/button/button';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
17
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
18
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
19
import { ContextKeyExpr, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
20
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
21
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
22 23
import Event, { Emitter } from 'vs/base/common/event';
import { Builder } from 'vs/base/browser/builder';
B
Benjamin Pasero 已提交
24
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
25
import { isSearchViewletFocused, appendKeyBindingLabel } from 'vs/workbench/parts/search/browser/searchActions';
26
import { HistoryNavigator } from 'vs/base/common/history';
27
import * as Constants from 'vs/workbench/parts/search/common/constants';
B
Benjamin Pasero 已提交
28
import { attachInputBoxStyler, attachFindInputBoxStyler, attachButtonStyler } from 'vs/platform/theme/common/styler';
B
Benjamin Pasero 已提交
29
import { IThemeService } from 'vs/platform/theme/common/themeService';
30
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
31
import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/findModel';
32 33
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
34
import { ISearchConfigurationProperties } from '../../../../platform/search/common/search';
35 36

export interface ISearchWidgetOptions {
J
Johannes Rieken 已提交
37 38 39 40
	value?: string;
	isRegex?: boolean;
	isCaseSensitive?: boolean;
	isWholeWords?: boolean;
41
	history?: string[];
42 43
}

44 45
class ReplaceAllAction extends Action {

J
Johannes Rieken 已提交
46 47
	private static fgInstance: ReplaceAllAction = null;
	public static ID: string = 'search.action.replaceAll';
48

J
Johannes Rieken 已提交
49
	static get INSTANCE(): ReplaceAllAction {
50
		if (ReplaceAllAction.fgInstance === null) {
J
Johannes Rieken 已提交
51
			ReplaceAllAction.fgInstance = new ReplaceAllAction();
52 53 54 55
		}
		return ReplaceAllAction.fgInstance;
	}

J
Johannes Rieken 已提交
56
	private _searchWidget: SearchWidget = null;
57 58 59 60 61 62

	constructor() {
		super(ReplaceAllAction.ID, '', 'action-replace-all', false);
	}

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

J
Johannes Rieken 已提交
66
	run(): TPromise<any> {
67 68 69 70 71 72 73
		if (this._searchWidget) {
			return this._searchWidget.triggerReplaceAll();
		}
		return TPromise.as(null);
	}
}

74 75
export class SearchWidget extends Widget {

76 77
	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 => {
78
		let kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID);
B
Benjamin Pasero 已提交
79
		return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb, keyBindingService2);
E
Erich Gamma 已提交
80
	}
S
Sandeep Somavarapu 已提交
81

82 83
	public domNode: HTMLElement;
	public searchInput: FindInput;
84 85
	private searchInputBoxFocused: IContextKey<boolean>;
	private replaceInputBoxFocused: IContextKey<boolean>;
86 87
	private replaceInput: InputBox;

88 89 90
	public searchInputFocusTracker: dom.IFocusTracker;
	public replaceInputFocusTracker: dom.IFocusTracker;

S
Sandeep Somavarapu 已提交
91
	private replaceContainer: HTMLElement;
92
	private toggleReplaceButton: Button;
93
	private replaceAllAction: ReplaceAllAction;
A
Alex Dima 已提交
94
	private replaceActive: IContextKey<boolean>;
S
Sandeep Somavarapu 已提交
95
	private replaceActionBar: ActionBar;
96

97
	private searchHistory: HistoryNavigator<string>;
98 99
	private ignoreGlobalFindBufferOnNextFocus = false;
	private previousGlobalFindBufferValue: string;
100

101 102 103 104 105 106 107 108 109
	private _onSearchSubmit = this._register(new Emitter<boolean>());
	public onSearchSubmit: Event<boolean> = this._onSearchSubmit.event;

	private _onSearchCancel = this._register(new Emitter<void>());
	public onSearchCancel: Event<void> = this._onSearchCancel.event;

	private _onReplaceToggled = this._register(new Emitter<void>());
	public onReplaceToggled: Event<void> = this._onReplaceToggled.event;

S
Sandeep Somavarapu 已提交
110 111
	private _onReplaceStateChange = this._register(new Emitter<boolean>());
	public onReplaceStateChange: Event<boolean> = this._onReplaceStateChange.event;
112 113 114 115 116 117 118

	private _onReplaceValueChanged = this._register(new Emitter<string>());
	public onReplaceValueChanged: Event<string> = this._onReplaceValueChanged.event;

	private _onReplaceAll = this._register(new Emitter<void>());
	public onReplaceAll: Event<void> = this._onReplaceAll.event;

119 120 121 122 123 124 125
	constructor(
		container: Builder,
		options: ISearchWidgetOptions,
		@IContextViewService private contextViewService: IContextViewService,
		@IThemeService private themeService: IThemeService,
		@IContextKeyService private keyBindingService: IContextKeyService,
		@IKeybindingService private keyBindingService2: IKeybindingService,
126 127
		@IClipboardService private clipboardServce: IClipboardService,
		@IConfigurationService private configurationService: IConfigurationService
128
	) {
129
		super();
130
		this.searchHistory = new HistoryNavigator<string>(options.history);
131
		this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.keyBindingService);
132 133
		this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.keyBindingService);
		this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.keyBindingService);
134 135 136
		this.render(container, options);
	}

137 138 139
	public focus(select: boolean = true, focusReplace: boolean = false, suppressGlobalSearchBuffer = false): void {
		this.ignoreGlobalFindBufferOnNextFocus = suppressGlobalSearchBuffer;

140 141 142 143 144 145 146 147 148 149 150 151 152 153
		if (focusReplace && this.isReplaceShown()) {
			this.replaceInput.focus();
			if (select) {
				this.replaceInput.select();
			}
		} else {
			this.searchInput.focus();
			if (select) {
				this.searchInput.select();
			}
		}
	}

	public setWidth(width: number) {
154
		this.searchInput.setWidth(width);
J
Johannes Rieken 已提交
155
		this.replaceInput.width = width - 28;
156 157 158 159
	}

	public clear() {
		this.searchInput.clear();
J
Johannes Rieken 已提交
160
		this.replaceInput.value = '';
S
Sandeep Somavarapu 已提交
161
		this.setReplaceAllActionState(false);
162 163 164
	}

	public isReplaceShown(): boolean {
S
Sandeep Somavarapu 已提交
165
		return !dom.hasClass(this.replaceContainer, 'disabled');
166 167
	}

J
Johannes Rieken 已提交
168
	public getReplaceValue(): string {
S
Sandeep Somavarapu 已提交
169
		return this.replaceInput.value;
170 171
	}

J
Johannes Rieken 已提交
172
	public toggleReplace(show?: boolean): void {
173 174
		if (show === void 0 || show !== this.isReplaceShown()) {
			this.onToggleReplaceButton();
S
Sandeep Somavarapu 已提交
175 176 177
		}
	}

178 179 180 181
	public getHistory(): string[] {
		return this.searchHistory.getHistory();
	}

182 183 184 185 186 187 188 189
	public showNextSearchTerm() {
		let next = this.searchHistory.next();
		if (next) {
			this.searchInput.setValue(next);
		}
	}

	public showPreviousSearchTerm() {
190
		let previous;
191
		if (this.searchInput.getValue().length === 0) {
W
Wim Spaargaren 已提交
192 193
			previous = this.searchHistory.current();
		} else {
194
			this.searchHistory.addIfNotPresent(this.searchInput.getValue());
W
Wim Spaargaren 已提交
195
			previous = this.searchHistory.previous();
196
		}
197 198 199 200 201
		if (previous) {
			this.searchInput.setValue(previous);
		}
	}

202
	public searchInputHasFocus(): boolean {
203
		return this.searchInputBoxFocused.get();
204 205 206 207 208 209
	}

	public replaceInputHasFocus(): boolean {
		return this.replaceInput.hasFocus();
	}

210 211 212 213 214 215 216 217 218
	private render(container: Builder, options: ISearchWidgetOptions): void {
		this.domNode = container.div({ 'class': 'search-widget' }).style({ position: 'relative' }).getHTMLElement();
		this.renderToggleReplaceButton(this.domNode);

		this.renderSearchInput(this.domNode, options);
		this.renderReplaceInput(this.domNode);
	}

	private renderToggleReplaceButton(parent: HTMLElement): void {
J
Johannes Rieken 已提交
219
		this.toggleReplaceButton = this._register(new Button(parent));
B
Benjamin Pasero 已提交
220 221 222 223
		attachButtonStyler(this.toggleReplaceButton, this.themeService, {
			buttonBackground: SIDE_BAR_BACKGROUND,
			buttonHoverBackground: SIDE_BAR_BACKGROUND
		});
J
Johannes Rieken 已提交
224
		this.toggleReplaceButton.icon = 'toggle-replace-button collapse';
J
Joao Moreno 已提交
225 226
		// TODO@joh need to dispose this listener eventually
		this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton());
J
Johannes Rieken 已提交
227
		this.toggleReplaceButton.getElement().title = nls.localize('search.replace.toggle.button.title', "Toggle Replace");
228 229 230 231 232
	}

	private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
		let inputOptions: IFindInputOptions = {
			label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
233
			validation: (value: string) => this.validateSearchInput(value),
S
Sandeep Somavarapu 已提交
234
			placeholder: nls.localize('search.placeHolder', "Search"),
R
Rob Lourens 已提交
235 236 237
			appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keyBindingService2.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keyBindingService2),
			appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService2.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService2),
			appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService2.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService2)
238 239
		};

J
Johannes Rieken 已提交
240
		let searchInputContainer = dom.append(parent, dom.$('.search-container.input-box'));
241
		this.searchInput = this._register(new FindInput(searchInputContainer, this.contextViewService, inputOptions));
B
Benjamin Pasero 已提交
242
		this._register(attachFindInputBoxStyler(this.searchInput, this.themeService));
243 244 245 246 247
		this.searchInput.onKeyUp((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyUp(keyboardEvent));
		this.searchInput.setValue(options.value || '');
		this.searchInput.setRegex(!!options.isRegex);
		this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
		this.searchInput.setWholeWords(!!options.isWholeWords);
248 249 250
		this._register(this.onSearchSubmit(() => {
			this.searchHistory.add(this.searchInput.getValue());
		}));
251

S
Sandeep Somavarapu 已提交
252
		this.searchInputFocusTracker = this._register(dom.trackFocus(this.searchInput.inputBox.inputElement));
253 254 255
		this._register(this.searchInputFocusTracker.onDidFocus(() => {
			this.searchInputBoxFocused.set(true);

256
			const useGlobalFindBuffer = this.configurationService.getValue<ISearchConfigurationProperties>('search').globalFindClipboard;
257 258 259
			if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) {
				const globalBufferText = this.clipboardServce.readFindText();
				if (this.previousGlobalFindBufferValue !== globalBufferText) {
260
					this.searchHistory.add(this.searchInput.getValue());
261 262 263 264 265 266 267 268 269
					this.searchInput.setValue(globalBufferText);
					this.searchInput.select();
				}

				this.previousGlobalFindBufferValue = globalBufferText;
			}

			this.ignoreGlobalFindBufferOnNextFocus = false;
		}));
270
		this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
271 272 273
	}

	private renderReplaceInput(parent: HTMLElement): void {
J
Joao Moreno 已提交
274
		this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
J
Johannes Rieken 已提交
275
		let replaceBox = dom.append(this.replaceContainer, dom.$('.input-box'));
S
Sandeep Somavarapu 已提交
276
		this.replaceInput = this._register(new InputBox(replaceBox, this.contextViewService, {
S
Sandeep Somavarapu 已提交
277
			ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
S
Sandeep Somavarapu 已提交
278
			placeholder: nls.localize('search.replace.placeHolder', "Replace")
279
		}));
B
Benjamin Pasero 已提交
280
		this._register(attachInputBoxStyler(this.replaceInput, this.themeService));
281 282
		this.onkeyup(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyUp(keyboardEvent));
		this.replaceInput.onDidChange(() => this._onReplaceValueChanged.fire());
S
Sandeep Somavarapu 已提交
283
		this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged());
S
Sandeep Somavarapu 已提交
284 285

		this.replaceAllAction = ReplaceAllAction.INSTANCE;
J
Johannes Rieken 已提交
286
		this.replaceAllAction.searchWidget = this;
S
Sandeep Somavarapu 已提交
287 288 289
		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 });
290

S
Sandeep Somavarapu 已提交
291
		this.replaceInputFocusTracker = this._register(dom.trackFocus(this.replaceInput.inputElement));
292 293
		this._register(this.replaceInputFocusTracker.onDidFocus(() => this.replaceInputBoxFocused.set(true)));
		this._register(this.replaceInputFocusTracker.onDidBlur(() => this.replaceInputBoxFocused.set(false)));
294 295
	}

296 297 298 299 300
	triggerReplaceAll(): TPromise<any> {
		this._onReplaceAll.fire();
		return TPromise.as(null);
	}

J
Johannes Rieken 已提交
301
	private onToggleReplaceButton(): void {
S
Sandeep Somavarapu 已提交
302
		dom.toggleClass(this.replaceContainer, 'disabled');
303 304
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'collapse');
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'expand');
305
		this.updateReplaceActiveState();
306
		this._onReplaceToggled.fire();
307 308
	}

J
Johannes Rieken 已提交
309
	public setReplaceAllActionState(enabled: boolean): void {
310
		if (this.replaceAllAction.enabled !== enabled) {
J
Johannes Rieken 已提交
311 312
			this.replaceAllAction.enabled = enabled;
			this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService2) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
313 314 315 316 317
			this.updateReplaceActiveState();
		}
	}

	private isReplaceActive(): boolean {
A
Alex Dima 已提交
318
		return this.replaceActive.get();
319 320 321
	}

	private updateReplaceActiveState(): void {
J
Johannes Rieken 已提交
322 323
		let currentState = this.isReplaceActive();
		let newState = this.isReplaceShown() && this.replaceAllAction.enabled;
324
		if (currentState !== newState) {
S
Sandeep Somavarapu 已提交
325 326
			this.replaceActive.set(newState);
			this._onReplaceStateChange.fire(newState);
327
		}
328 329
	}

330
	private validateSearchInput(value: string): IMessage {
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
		if (value.length === 0) {
			return null;
		}
		if (!this.searchInput.getRegex()) {
			return null;
		}
		let regExp: RegExp;
		try {
			regExp = new RegExp(value);
		} catch (e) {
			return { content: e.message };
		}
		if (strings.regExpLeadsToEndlessLoop(regExp)) {
			return { content: nls.localize('regexp.validationFailure', "Expression matches everything") };
		}
346 347 348 349 350 351

		if (strings.regExpContainsBackreference(value)) {
			return { content: nls.localize('regexp.backreferenceValidationFailure', "Backreferences are not supported") };
		}

		return null;
352 353
	}

S
Sandeep Somavarapu 已提交
354
	private onSearchInputChanged(): void {
S
Sandeep Somavarapu 已提交
355
		this.setReplaceAllActionState(false);
S
Sandeep Somavarapu 已提交
356 357
	}

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
	private onSearchInputKeyUp(keyboardEvent: IKeyboardEvent) {
		switch (keyboardEvent.keyCode) {
			case KeyCode.Enter:
				this.submitSearch();
				return;
			case KeyCode.Escape:
				this._onSearchCancel.fire();
				return;
			default:
				return;
		}
	}

	private onReplaceInputKeyUp(keyboardEvent: IKeyboardEvent) {
		switch (keyboardEvent.keyCode) {
			case KeyCode.Enter:
				this.submitSearch();
				return;
			default:
				return;
		}
	}

J
Johannes Rieken 已提交
381
	private submitSearch(refresh: boolean = true): void {
382 383 384
		const value = this.searchInput.getValue();
		if (value) {
			this.clipboardServce.writeFindText(value);
385 386 387 388 389
			this._onSearchSubmit.fire(refresh);
		}
	}

	public dispose(): void {
390
		this.setReplaceAllActionState(false);
J
Johannes Rieken 已提交
391
		this.replaceAllAction.searchWidget = null;
S
Sandeep Somavarapu 已提交
392
		this.replaceActionBar = null;
393 394
		super.dispose();
	}
395 396 397
}

export function registerContributions() {
J
Johannes Rieken 已提交
398 399
	KeybindingsRegistry.registerCommandAndKeybindingRule({
		id: ReplaceAllAction.ID,
400
		weight: KeybindingsRegistry.WEIGHT.workbenchContrib(),
S
Sandeep Somavarapu 已提交
401
		when: ContextKeyExpr.and(Constants.SearchViewletVisibleKey, Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE),
402 403
		primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter,
		handler: accessor => {
404
			if (isSearchViewletFocused(accessor.get(IViewletService))) {
405 406 407 408
				ReplaceAllAction.INSTANCE.run();
			}
		}
	});
A
Alex Dima 已提交
409
}