searchWidget.ts 12.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import nls = require('vs/nls');
import strings = require('vs/base/common/strings');
import dom = require('vs/base/browser/dom');
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';
A
Alex Dima 已提交
19
import { ContextKeyExpr, RawContextKey, 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 29 30 31 32 33 34 35

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

36 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
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);
	}
}

66 67
export class SearchWidget extends Widget {

A
Alex Dima 已提交
68
	static REPLACE_ACTIVE_CONTEXT_KEY= new RawContextKey<boolean>('replaceActive', false);
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 77 78
	public domNode: HTMLElement;
	public searchInput: FindInput;
	private replaceInput: InputBox;

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

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

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

	private _onKeyDownArrow = this._register(new Emitter<void>());
	public onKeyDownArrow: Event<void> = this._onKeyDownArrow.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),
107
					private keyBindingService: IContextKeyService, private keyBindingService2: IKeybindingService, private instantiationService: IInstantiationService) {
108
		super();
A
Alex Dima 已提交
109
		this.replaceActive = SearchWidget.REPLACE_ACTIVE_CONTEXT_KEY.bindTo(this.keyBindingService);
110 111 112 113
		this.render(container, options);
	}

	public focus(select:boolean= true, focusReplace: boolean= false):void {
114 115
		if ((!focusReplace && this.searchInput.inputBox.hasFocus())
					|| (focusReplace && this.replaceInput.hasFocus())) {
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
			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 已提交
140
		this.setReplaceAllActionState(false);
141 142 143
	}

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

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

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

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
	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 已提交
179
		let searchInputContainer= dom.append(parent, dom.$('.search-container.input-box'));
180 181 182 183 184 185 186 187 188 189
		this.searchInput = this._register(new FindInput(searchInputContainer, this.contextViewService, inputOptions));
		this.searchInput.onKeyUp((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyUp(keyboardEvent));
		this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
		this.searchInput.setValue(options.value || '');
		this.searchInput.setRegex(!!options.isRegex);
		this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
		this.searchInput.setWholeWords(!!options.isWholeWords);
	}

	private renderReplaceInput(parent: HTMLElement): void {
J
Joao Moreno 已提交
190 191
		this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
		let replaceBox= dom.append(this.replaceContainer, dom.$('.input-box'));
S
Sandeep Somavarapu 已提交
192
		this.replaceInput = this._register(new InputBox(replaceBox, this.contextViewService, {
S
Sandeep Somavarapu 已提交
193
			ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
S
Sandeep Somavarapu 已提交
194
			placeholder: nls.localize('search.replace.placeHolder', "Replace")
195 196 197 198
		}));
		this.onkeydown(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent));
		this.onkeyup(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyUp(keyboardEvent));
		this.replaceInput.onDidChange(() => this._onReplaceValueChanged.fire());
S
Sandeep Somavarapu 已提交
199
		this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged());
S
Sandeep Somavarapu 已提交
200 201 202 203 204 205

		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 });
206 207
	}

208 209 210 211 212
	triggerReplaceAll(): TPromise<any> {
		this._onReplaceAll.fire();
		return TPromise.as(null);
	}

213
	private onToggleReplaceButton():void {
S
Sandeep Somavarapu 已提交
214
		dom.toggleClass(this.replaceContainer, 'disabled');
215 216
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'collapse');
		dom.toggleClass(this.toggleReplaceButton.getElement(), 'expand');
217
		this.updateReplaceActiveState();
218
		this._onReplaceToggled.fire();
219 220 221 222 223
	}

	public setReplaceAllActionState(enabled:boolean):void {
		if (this.replaceAllAction.enabled !== enabled) {
			this.replaceAllAction.enabled= enabled;
A
Alex Dima 已提交
224
			this.replaceAllAction.label= enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService2) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
225 226 227 228 229
			this.updateReplaceActiveState();
		}
	}

	private isReplaceActive(): boolean {
A
Alex Dima 已提交
230
		return this.replaceActive.get();
231 232 233 234 235 236
	}

	private updateReplaceActiveState(): void {
		let currentState= this.isReplaceActive();
		let newState= this.isReplaceShown() && this.replaceAllAction.enabled;
		if (currentState !== newState) {
S
Sandeep Somavarapu 已提交
237 238
			this.replaceActive.set(newState);
			this._onReplaceStateChange.fire(newState);
239
		}
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
	}

	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 已提交
260
	private onSearchInputChanged(): void {
S
Sandeep Somavarapu 已提交
261
		this.setReplaceAllActionState(false);
S
Sandeep Somavarapu 已提交
262 263
	}

264 265 266 267 268 269 270 271 272 273 274 275 276 277
	private onSearchInputKeyUp(keyboardEvent: IKeyboardEvent) {
		switch (keyboardEvent.keyCode) {
			case KeyCode.Enter:
				this.submitSearch();
				return;
			case KeyCode.Escape:
				this._onSearchCancel.fire();
				return;
			default:
				return;
		}
	}

	private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
S
Sandeep Somavarapu 已提交
278
		let handled= false;
279 280 281
		switch (keyboardEvent.keyCode) {
			case KeyCode.DownArrow:
				if (this.isReplaceShown()) {
S
Sandeep Somavarapu 已提交
282
					this.focus(true, true);
283 284 285
				} else {
					this._onKeyDownArrow.fire();
				}
S
Sandeep Somavarapu 已提交
286 287 288 289 290
				handled= true;
				break;
		}
		if (handled) {
			keyboardEvent.preventDefault();
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
		}
	}

	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 onReplaceInputKeyDown(keyboardEvent: IKeyboardEvent) {
S
Sandeep Somavarapu 已提交
309
		let handled= false;
310 311
		switch (keyboardEvent.keyCode) {
			case KeyCode.UpArrow:
S
Sandeep Somavarapu 已提交
312 313 314
				this.focus(true);
				handled= true;
				break;
315 316
			case KeyCode.DownArrow:
				this._onKeyDownArrow.fire();
S
Sandeep Somavarapu 已提交
317 318 319 320 321
				handled= true;
				break;
		}
		if (handled) {
			keyboardEvent.preventDefault();
322 323 324 325 326 327 328 329 330 331
		}
	}

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

	public dispose(): void {
332 333
		this.setReplaceAllActionState(false);
		this.replaceAllAction.searchWidget= null;
S
Sandeep Somavarapu 已提交
334
		this.replaceActionBar = null;
335 336
		super.dispose();
	}
337 338 339
}

export function registerContributions() {
A
Alex Dima 已提交
340
	KeybindingsRegistry.registerCommandAndKeybindingRule({id: ReplaceAllAction.ID,
341
		weight: KeybindingsRegistry.WEIGHT.workbenchContrib(),
A
Alex Dima 已提交
342
		when: ContextKeyExpr.and(ContextKeyExpr.has('searchViewletVisible'), SearchWidget.REPLACE_ACTIVE_CONTEXT_KEY, CONTEXT_FIND_WIDGET_NOT_VISIBLE),
343 344 345 346 347 348 349
		primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter,
		handler: accessor => {
			if (isSearchViewletFocussed(accessor.get(IViewletService))) {
				ReplaceAllAction.INSTANCE.run();
			}
		}
	});
A
Alex Dima 已提交
350
}