searchWidget.ts 12.3 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 14 15 16
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
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 24
import Event, { Emitter } from 'vs/base/common/event';
import { Builder } from 'vs/base/browser/builder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
25
import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService';
26
import { isSearchViewletFocussed, appendKeyBindingLabel } from 'vs/workbench/parts/search/browser/searchActions';
A
Alex Dima 已提交
27
import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/common/findController';
28
import * as Constants from 'vs/workbench/parts/search/common/constants';
29 30 31 32 33 34 35 36

export interface ISearchWidgetOptions {
	value?:string;
	isRegex?:boolean;
	isCaseSensitive?:boolean;
	isWholeWords?:boolean;
}

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
class ReplaceAllAction extends Action {

	private static fgInstance:ReplaceAllAction= null;
	public static ID:string= 'search.action.replaceAll';

	static get INSTANCE():ReplaceAllAction {
		if (ReplaceAllAction.fgInstance === null) {
			ReplaceAllAction.fgInstance= new ReplaceAllAction();
		}
		return ReplaceAllAction.fgInstance;
	}

	private _searchWidget: SearchWidget= null;

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

	set searchWidget(searchWidget: SearchWidget) {
		this._searchWidget= searchWidget;
	}

	run():TPromise<any> {
		if (this._searchWidget) {
			return this._searchWidget.triggerReplaceAll();
		}
		return TPromise.as(null);
	}
}

67 68
export class SearchWidget extends Widget {

69
	private static REPLACE_ALL_DISABLED_LABEL= nls.localize('search.action.replaceAll.disabled.label', "Replace All (Submit Search to Enable)");
70
	private static REPLACE_ALL_ENABLED_LABEL=(keyBindingService2: IKeybindingService):string=>{
A
Alex Dima 已提交
71 72
		let keybindings = keyBindingService2.lookupKeybindings(ReplaceAllAction.ID);
		return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), keybindings[0], keyBindingService2);
73
	};
S
Sandeep Somavarapu 已提交
74

75 76
	public domNode: HTMLElement;
	public searchInput: FindInput;
77
	private searchInputBoxFocussed: IContextKey<boolean>;
78 79
	private replaceInput: InputBox;

80 81 82
	public searchInputFocusTracker: dom.IFocusTracker;
	public replaceInputFocusTracker: dom.IFocusTracker;

S
Sandeep Somavarapu 已提交
83
	private replaceContainer: HTMLElement;
84
	private toggleReplaceButton: Button;
85
	private replaceAllAction: ReplaceAllAction;
A
Alex Dima 已提交
86
	private replaceActive: IContextKey<boolean>;
S
Sandeep Somavarapu 已提交
87
	private replaceActionBar: ActionBar;
88 89 90 91 92 93 94 95 96 97

	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 已提交
98 99
	private _onReplaceStateChange = this._register(new Emitter<boolean>());
	public onReplaceStateChange: Event<boolean> = this._onReplaceStateChange.event;
100 101 102 103 104 105 106 107

	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;

	constructor(container: Builder, private contextViewService: IContextViewService, options: ISearchWidgetOptions= Object.create(null),
108
					private keyBindingService: IContextKeyService, private keyBindingService2: IKeybindingService, private instantiationService: IInstantiationService) {
109
		super();
110 111
		this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.keyBindingService);
		this.searchInputBoxFocussed = Constants.SearchInputBoxFocussedKey.bindTo(this.keyBindingService);
112 113 114 115
		this.render(container, options);
	}

	public focus(select:boolean= true, focusReplace: boolean= false):void {
116 117
		if ((!focusReplace && this.searchInput.inputBox.hasFocus())
					|| (focusReplace && this.replaceInput.hasFocus())) {
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
			return;
		}

		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) {
		this.searchInput.setWidth(width - 2);
		this.replaceInput.width= width - 28;
	}

	public clear() {
		this.searchInput.clear();
		this.replaceInput.value= '';
S
Sandeep Somavarapu 已提交
142
		this.setReplaceAllActionState(false);
143 144 145
	}

	public isReplaceShown(): boolean {
S
Sandeep Somavarapu 已提交
146
		return !dom.hasClass(this.replaceContainer, 'disabled');
147 148 149
	}

	public getReplaceValue():string {
S
Sandeep Somavarapu 已提交
150
		return this.replaceInput.value;
151 152
	}

153 154 155
	public toggleReplace(show?:boolean): void {
		if (show === void 0 || show !== this.isReplaceShown()) {
			this.onToggleReplaceButton();
S
Sandeep Somavarapu 已提交
156 157 158
		}
	}

159 160 161 162 163 164 165 166
	public searchInputHasFocus(): boolean {
		return this.searchInputBoxFocussed.get();
	}

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

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
	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 {
		this.toggleReplaceButton= this._register(new Button(parent));
		this.toggleReplaceButton.icon= 'toggle-replace-button collapse';
		this.toggleReplaceButton.addListener2('click', () => this.onToggleReplaceButton());
		this.toggleReplaceButton.getElement().title= nls.localize('search.replace.toggle.button.title', "Toggle Replace");
	}

	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'),
			validation: (value: string) => this.validatSearchInput(value),
			placeholder: nls.localize('search.placeHolder', "Search")
		};

J
Joao Moreno 已提交
189
		let searchInputContainer= dom.append(parent, dom.$('.search-container.input-box'));
190 191 192 193 194 195
		this.searchInput = this._register(new FindInput(searchInputContainer, this.contextViewService, inputOptions));
		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);
196 197 198 199 200 201 202 203

		this.searchInputFocusTracker = dom.trackFocus(this.searchInput.inputBox.inputElement);
		this._register(this.searchInputFocusTracker.addFocusListener(() => {
			this.searchInputBoxFocussed.set(true);
		}));
		this._register(this.searchInputFocusTracker.addBlurListener(() => {
			this.searchInputBoxFocussed.set(false);
		}));
204 205 206
	}

	private renderReplaceInput(parent: HTMLElement): void {
J
Joao Moreno 已提交
207 208
		this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
		let replaceBox= dom.append(this.replaceContainer, dom.$('.input-box'));
S
Sandeep Somavarapu 已提交
209
		this.replaceInput = this._register(new InputBox(replaceBox, this.contextViewService, {
S
Sandeep Somavarapu 已提交
210
			ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
S
Sandeep Somavarapu 已提交
211
			placeholder: nls.localize('search.replace.placeHolder', "Replace")
212 213 214
		}));
		this.onkeyup(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyUp(keyboardEvent));
		this.replaceInput.onDidChange(() => this._onReplaceValueChanged.fire());
S
Sandeep Somavarapu 已提交
215
		this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged());
S
Sandeep Somavarapu 已提交
216 217 218 219 220 221

		this.replaceAllAction = ReplaceAllAction.INSTANCE;
		this.replaceAllAction.searchWidget= this;
		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 });
222 223

		this.replaceInputFocusTracker = dom.trackFocus(this.replaceInput.inputElement);
224 225
	}

226 227 228 229 230
	triggerReplaceAll(): TPromise<any> {
		this._onReplaceAll.fire();
		return TPromise.as(null);
	}

231
	private onToggleReplaceButton():void {
S
Sandeep Somavarapu 已提交
232
		dom.toggleClass(this.replaceContainer, 'disabled');
233 234
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'collapse');
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'expand');
235
		this.updateReplaceActiveState();
236
		this._onReplaceToggled.fire();
237 238 239 240 241
	}

	public setReplaceAllActionState(enabled:boolean):void {
		if (this.replaceAllAction.enabled !== enabled) {
			this.replaceAllAction.enabled= enabled;
A
Alex Dima 已提交
242
			this.replaceAllAction.label= enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService2) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
243 244 245 246 247
			this.updateReplaceActiveState();
		}
	}

	private isReplaceActive(): boolean {
A
Alex Dima 已提交
248
		return this.replaceActive.get();
249 250 251 252 253 254
	}

	private updateReplaceActiveState(): void {
		let currentState= this.isReplaceActive();
		let newState= this.isReplaceShown() && this.replaceAllAction.enabled;
		if (currentState !== newState) {
S
Sandeep Somavarapu 已提交
255 256
			this.replaceActive.set(newState);
			this._onReplaceStateChange.fire(newState);
257
		}
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
	}

	private validatSearchInput(value: string): any {
		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") };
		}
	}

S
Sandeep Somavarapu 已提交
278
	private onSearchInputChanged(): void {
S
Sandeep Somavarapu 已提交
279
		this.setReplaceAllActionState(false);
S
Sandeep Somavarapu 已提交
280 281
	}

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
	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;
			case KeyCode.Escape:
				this.onToggleReplaceButton();
				this.searchInput.focus();
				return;
			default:
				return;
		}
	}

	private submitSearch(refresh: boolean= true): void {
		if (this.searchInput.getValue()) {
			this._onSearchSubmit.fire(refresh);
		}
	}

	public dispose(): void {
316 317
		this.setReplaceAllActionState(false);
		this.replaceAllAction.searchWidget= null;
S
Sandeep Somavarapu 已提交
318
		this.replaceActionBar = null;
319 320 321 322 323 324
		if (this.searchInputFocusTracker) {
			this.searchInputFocusTracker.dispose();
		}
		if (this.replaceInputFocusTracker) {
			this.replaceInputFocusTracker.dispose();
		}
325 326
		super.dispose();
	}
327 328 329
}

export function registerContributions() {
A
Alex Dima 已提交
330
	KeybindingsRegistry.registerCommandAndKeybindingRule({id: ReplaceAllAction.ID,
331
		weight: KeybindingsRegistry.WEIGHT.workbenchContrib(),
332
		when: ContextKeyExpr.and(ContextKeyExpr.has('searchViewletVisible'), Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE),
333 334 335 336 337 338 339
		primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter,
		handler: accessor => {
			if (isSearchViewletFocussed(accessor.get(IViewletService))) {
				ReplaceAllAction.INSTANCE.run();
			}
		}
	});
A
Alex Dima 已提交
340
}