findWidget.ts 23.7 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import 'vs/css!./findWidget';
A
Alex Dima 已提交
9
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
10 11
import { onUnexpectedError } from 'vs/base/common/errors';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
A
Alex Dima 已提交
12 13
import * as strings from 'vs/base/common/strings';
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
14 15 16 17 18 19 20 21 22 23 24 25 26
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
import { IMessage as InputBoxMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { Widget } from 'vs/base/browser/ui/widget';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IConfigurationChangedEvent } from 'vs/editor/common/editorCommon';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { FIND_IDS, MATCHES_LIMIT } from 'vs/editor/contrib/find/common/findModel';
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/common/findState';
import { Range } from 'vs/editor/common/core/range';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { CONTEXT_FIND_INPUT_FOCUSSED } from 'vs/editor/contrib/find/common/findController';
E
Erich Gamma 已提交
27 28 29 30 31 32

export interface IFindController {
	replace(): void;
	replaceAll(): void;
}

33 34 35 36 37 38 39 40 41 42 43
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind', "Find in selection");
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace");
const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace");
const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace");
const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All");
const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode");
A
Alex Dima 已提交
44
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text.");
45
const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}");
I
isidor 已提交
46
const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results");
47

48
let MAX_MATCHES_COUNT_WIDTH = 69;
49
const WIDGET_FIXED_WIDTH = 411 - 69;
50

A
Alex Dima 已提交
51
export class FindWidget extends Widget implements IOverlayWidget {
E
Erich Gamma 已提交
52 53 54 55 56 57

	private static ID = 'editor.contrib.findWidget';
	private static PART_WIDTH = 275;
	private static FIND_INPUT_AREA_WIDTH = FindWidget.PART_WIDTH - 54;
	private static REPLACE_INPUT_AREA_WIDTH = FindWidget.FIND_INPUT_AREA_WIDTH;

A
Alex Dima 已提交
58
	private _codeEditor: ICodeEditor;
A
Alex Dima 已提交
59
	private _state: FindReplaceState;
E
Erich Gamma 已提交
60
	private _controller: IFindController;
A
Alex Dima 已提交
61
	private _contextViewProvider: IContextViewProvider;
62
	private _keybindingService: IKeybindingService;
E
Erich Gamma 已提交
63

A
Alex Dima 已提交
64 65 66
	private _domNode: HTMLElement;
	private _findInput: FindInput;
	private _replaceInputBox: InputBox;
E
Erich Gamma 已提交
67

A
Alex Dima 已提交
68
	private _toggleReplaceBtn: SimpleButton;
69
	private _matchesCount: HTMLElement;
A
Alex Dima 已提交
70 71
	private _prevBtn: SimpleButton;
	private _nextBtn: SimpleButton;
72
	private _toggleSelectionFind: SimpleCheckbox;
A
Alex Dima 已提交
73 74 75
	private _closeBtn: SimpleButton;
	private _replaceBtn: SimpleButton;
	private _replaceAllBtn: SimpleButton;
E
Erich Gamma 已提交
76

A
Alex Dima 已提交
77 78
	private _isVisible: boolean;
	private _isReplaceVisible: boolean;
E
Erich Gamma 已提交
79

80
	private _focusTracker: dom.IFocusTracker;
S
Sandeep Somavarapu 已提交
81
	private _findInputFocussed: IContextKey<boolean>;
82

83
	constructor(
A
Alex Dima 已提交
84
		codeEditor: ICodeEditor,
A
Alex Dima 已提交
85 86 87
		controller: IFindController,
		state: FindReplaceState,
		contextViewProvider: IContextViewProvider,
S
Sandeep Somavarapu 已提交
88 89
		keybindingService: IKeybindingService,
		contextKeyService: IContextKeyService
90
	) {
A
Alex Dima 已提交
91
		super();
E
Erich Gamma 已提交
92 93
		this._codeEditor = codeEditor;
		this._controller = controller;
A
Alex Dima 已提交
94
		this._state = state;
E
Erich Gamma 已提交
95
		this._contextViewProvider = contextViewProvider;
96
		this._keybindingService = keybindingService;
E
Erich Gamma 已提交
97 98 99 100

		this._isVisible = false;
		this._isReplaceVisible = false;

A
Alex Dima 已提交
101
		this._register(this._state.addChangeListener((e) => this._onStateChanged(e)));
E
Erich Gamma 已提交
102 103

		this._buildDomNode();
104
		this._updateButtons();
E
Erich Gamma 已提交
105

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
		let checkEditorWidth = () => {
			let editorWidth = this._codeEditor.getConfiguration().layoutInfo.width;
			let collapsedFindWidget = false;
			let reducedFindWidget = false;
			let narrowFindWidget = false;
			if (WIDGET_FIXED_WIDTH + 28 >= editorWidth + 50) {
				collapsedFindWidget = true;
			}
			if (WIDGET_FIXED_WIDTH + 28 >= editorWidth) {
				narrowFindWidget = true;
			}
			if (WIDGET_FIXED_WIDTH + MAX_MATCHES_COUNT_WIDTH + 28 >= editorWidth) {
				reducedFindWidget = true;
			}
			dom.toggleClass(this._domNode, 'collapsed-find-widget', collapsedFindWidget);
			dom.toggleClass(this._domNode, 'reduced-find-widget', reducedFindWidget);
			dom.toggleClass(this._domNode, 'narrow-find-widget', narrowFindWidget);
		};
		checkEditorWidth();

J
Johannes Rieken 已提交
126
		this._register(this._codeEditor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
127
			if (e.readOnly) {
128 129 130 131 132 133
				if (this._codeEditor.getConfiguration().readOnly) {
					// Hide replace part if editor becomes read only
					this._state.change({ isReplaceRevealed: false }, false);
				}
				this._updateButtons();
			}
134 135 136
			if (e.layoutInfo) {
				checkEditorWidth();
			}
137
		}));
A
Alex Dima 已提交
138
		this._register(this._codeEditor.onDidChangeCursorSelection(() => {
139 140
			if (this._isVisible) {
				this._updateToggleSelectionFindButton();
141 142
			}
		}));
S
Sandeep Somavarapu 已提交
143
		this._findInputFocussed = CONTEXT_FIND_INPUT_FOCUSSED.bindTo(contextKeyService);
144 145
		this._focusTracker = this._register(dom.trackFocus(this._findInput.inputBox.inputElement));
		this._focusTracker.addFocusListener(() => {
S
Sandeep Somavarapu 已提交
146
			this._findInputFocussed.set(true);
147 148 149 150 151 152 153 154 155 156 157 158

			if (this._toggleSelectionFind.checked) {
				let selection = this._codeEditor.getSelection();
				if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
					selection = selection.setEndPosition(selection.endLineNumber - 1, 1);
				}
				let currentMatch = this._state.currentMatch;
				if (selection.startLineNumber !== selection.endLineNumber) {
					if (!Range.equalsRange(selection, currentMatch)) {
						// Reseed find scope
						this._state.change({ searchScope: selection }, true);
					}
159 160 161
				}
			}
		});
S
Sandeep Somavarapu 已提交
162 163 164
		this._focusTracker.addBlurListener(() => {
			this._findInputFocussed.set(false);
		});
165

A
Alex Dima 已提交
166
		this._codeEditor.addOverlayWidget(this);
E
Erich Gamma 已提交
167 168 169 170 171 172 173 174 175 176 177 178
	}

	// ----- IOverlayWidget API

	public getId(): string {
		return FindWidget.ID;
	}

	public getDomNode(): HTMLElement {
		return this._domNode;
	}

A
Alex Dima 已提交
179
	public getPosition(): IOverlayWidgetPosition {
E
Erich Gamma 已提交
180 181
		if (this._isVisible) {
			return {
A
Alex Dima 已提交
182
				preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
E
Erich Gamma 已提交
183 184 185 186 187
			};
		}
		return null;
	}

A
Alex Dima 已提交
188
	// ----- React to state changes
E
Erich Gamma 已提交
189

J
Johannes Rieken 已提交
190
	private _onStateChanged(e: FindReplaceStateChangedEvent): void {
A
Alex Dima 已提交
191 192
		if (e.searchString) {
			this._findInput.setValue(this._state.searchString);
193
			this._updateButtons();
A
Alex Dima 已提交
194 195 196 197 198 199 200 201 202 203 204 205 206
		}
		if (e.replaceString) {
			this._replaceInputBox.value = this._state.replaceString;
		}
		if (e.isRevealed) {
			if (this._state.isRevealed) {
				this._reveal(true);
			} else {
				this._hide(true);
			}
		}
		if (e.isReplaceRevealed) {
			if (this._state.isReplaceRevealed) {
207 208 209 210
				if (!this._codeEditor.getConfiguration().readOnly && !this._isReplaceVisible) {
					this._isReplaceVisible = true;
					this._updateButtons();
				}
A
Alex Dima 已提交
211
			} else {
212 213 214 215
				if (this._isReplaceVisible) {
					this._isReplaceVisible = false;
					this._updateButtons();
				}
A
Alex Dima 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228
			}
		}
		if (e.isRegex) {
			this._findInput.setRegex(this._state.isRegex);
		}
		if (e.wholeWord) {
			this._findInput.setWholeWords(this._state.wholeWord);
		}
		if (e.matchCase) {
			this._findInput.setCaseSensitive(this._state.matchCase);
		}
		if (e.searchScope) {
			if (this._state.searchScope) {
229
				this._toggleSelectionFind.checked = true;
A
Alex Dima 已提交
230
			} else {
231
				this._toggleSelectionFind.checked = false;
A
Alex Dima 已提交
232 233 234
			}
			this._updateToggleSelectionFindButton();
		}
235
		if (e.searchString || e.matchesCount || e.matchesPosition) {
A
Alex Dima 已提交
236
			let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0);
A
Alex Dima 已提交
237
			dom.toggleClass(this._domNode, 'no-results', showRedOutline);
A
Alex Dima 已提交
238

239 240 241 242 243
			this._updateMatchesCount();
		}
	}

	private _updateMatchesCount(): void {
244
		this._matchesCount.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px';
245 246 247 248 249
		if (this._state.matchesCount >= MATCHES_LIMIT) {
			this._matchesCount.title = NLS_MATCHES_COUNT_LIMIT_TITLE;
		} else {
			this._matchesCount.title = '';
		}
A
Alex Dima 已提交
250

251 252 253 254 255 256
		// remove previous content
		if (this._matchesCount.firstChild) {
			this._matchesCount.removeChild(this._matchesCount.firstChild);
		}

		let label: string;
257
		if (this._state.matchesCount > 0) {
J
Johannes Rieken 已提交
258
			let matchesCount: string = String(this._state.matchesCount);
A
Alex Dima 已提交
259 260 261
			if (this._state.matchesCount >= MATCHES_LIMIT) {
				matchesCount += '+';
			}
J
Johannes Rieken 已提交
262
			let matchesPosition: string = String(this._state.matchesPosition);
263 264 265
			if (matchesPosition === '0') {
				matchesPosition = '?';
			}
A
Alex Dima 已提交
266
			label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount);
267
		} else {
268
			label = NLS_NO_RESULTS;
E
Erich Gamma 已提交
269
		}
270
		this._matchesCount.appendChild(document.createTextNode(label));
271 272

		MAX_MATCHES_COUNT_WIDTH = Math.max(MAX_MATCHES_COUNT_WIDTH, this._matchesCount.clientWidth);
E
Erich Gamma 已提交
273 274
	}

275 276 277 278
	// ----- actions

	/**
	 * If 'selection find' is ON we should not disable the button (its function is to cancel 'selection find').
279
	 * If 'selection find' is OFF we enable the button only if there is a selection.
280 281 282
	 */
	private _updateToggleSelectionFindButton(): void {
		let selection = this._codeEditor.getSelection();
283
		let isSelection = selection ? (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn) : false;
284 285
		let isChecked = this._toggleSelectionFind.checked;

286
		this._toggleSelectionFind.setEnabled(this._isVisible && (isChecked || isSelection));
287 288 289 290 291 292 293 294 295 296 297 298 299 300
	}

	private _updateButtons(): void {
		this._findInput.setEnabled(this._isVisible);
		this._replaceInputBox.setEnabled(this._isVisible && this._isReplaceVisible);
		this._updateToggleSelectionFindButton();
		this._closeBtn.setEnabled(this._isVisible);

		let findInputIsNonEmpty = (this._state.searchString.length > 0);
		this._prevBtn.setEnabled(this._isVisible && findInputIsNonEmpty);
		this._nextBtn.setEnabled(this._isVisible && findInputIsNonEmpty);
		this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
		this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);

A
Alex Dima 已提交
301
		dom.toggleClass(this._domNode, 'replaceToggled', this._isReplaceVisible);
302 303 304 305 306 307 308 309
		this._toggleReplaceBtn.toggleClass('collapse', !this._isReplaceVisible);
		this._toggleReplaceBtn.toggleClass('expand', this._isReplaceVisible);
		this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);

		let canReplace = !this._codeEditor.getConfiguration().readOnly;
		this._toggleReplaceBtn.setEnabled(this._isVisible && canReplace);
	}

J
Johannes Rieken 已提交
310
	private _reveal(animate: boolean): void {
311 312 313 314 315 316
		if (!this._isVisible) {
			this._isVisible = true;

			this._updateButtons();

			setTimeout(() => {
A
Alex Dima 已提交
317
				dom.addClass(this._domNode, 'visible');
318
				if (!animate) {
A
Alex Dima 已提交
319
					dom.addClass(this._domNode, 'noanimation');
320
					setTimeout(() => {
A
Alex Dima 已提交
321
						dom.removeClass(this._domNode, 'noanimation');
322 323 324 325 326 327 328
					}, 200);
				}
			}, 0);
			this._codeEditor.layoutOverlayWidget(this);
		}
	}

J
Johannes Rieken 已提交
329
	private _hide(focusTheEditor: boolean): void {
330 331 332 333 334
		if (this._isVisible) {
			this._isVisible = false;

			this._updateButtons();

A
Alex Dima 已提交
335
			dom.removeClass(this._domNode, 'visible');
336 337 338 339 340 341 342
			if (focusTheEditor) {
				this._codeEditor.focus();
			}
			this._codeEditor.layoutOverlayWidget(this);
		}
	}

E
Erich Gamma 已提交
343 344
	// ----- Public

345 346 347 348 349 350 351 352 353 354 355 356
	public focusFindInput(): void {
		this._findInput.select();
		// Edge browser requires focus() in addition to select()
		this._findInput.focus();
	}

	public focusReplaceInput(): void {
		this._replaceInputBox.select();
		// Edge browser requires focus() in addition to select()
		this._replaceInputBox.focus();
	}

357 358 359 360
	public highlightFindOptions(): void {
		this._findInput.highlightFindOptions();
	}

J
Johannes Rieken 已提交
361
	private _onFindInputKeyDown(e: IKeyboardEvent): void {
E
Erich Gamma 已提交
362

363
		switch (e.toKeybinding().value) {
A
Alexandru Dima 已提交
364
			case KeyCode.Enter:
A
Alex Dima 已提交
365
				this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().done(null, onUnexpectedError);
366 367
				e.preventDefault();
				return;
E
Erich Gamma 已提交
368

A
Alexandru Dima 已提交
369
			case KeyMod.Shift | KeyCode.Enter:
A
Alex Dima 已提交
370
				this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().done(null, onUnexpectedError);
371 372 373
				e.preventDefault();
				return;

A
Alexandru Dima 已提交
374
			case KeyCode.Tab:
375 376 377 378 379 380 381 382
				if (this._isReplaceVisible) {
					this._replaceInputBox.focus();
				} else {
					this._findInput.focusOnCaseSensitive();
				}
				e.preventDefault();
				return;

A
Alexandru Dima 已提交
383
			case KeyMod.CtrlCmd | KeyCode.DownArrow:
384 385 386
				this._codeEditor.focus();
				e.preventDefault();
				return;
E
Erich Gamma 已提交
387 388 389
		}
	}

J
Johannes Rieken 已提交
390
	private _onReplaceInputKeyDown(e: IKeyboardEvent): void {
E
Erich Gamma 已提交
391

392
		switch (e.toKeybinding().value) {
A
Alexandru Dima 已提交
393
			case KeyCode.Enter:
394 395 396
				this._controller.replace();
				e.preventDefault();
				return;
E
Erich Gamma 已提交
397

A
Alexandru Dima 已提交
398
			case KeyMod.CtrlCmd | KeyCode.Enter:
399 400 401 402
				this._controller.replaceAll();
				e.preventDefault();
				return;

A
Alexandru Dima 已提交
403
			case KeyCode.Tab:
404 405 406 407
				this._findInput.focusOnCaseSensitive();
				e.preventDefault();
				return;

A
Alexandru Dima 已提交
408
			case KeyMod.Shift | KeyCode.Tab:
409 410 411 412
				this._findInput.focus();
				e.preventDefault();
				return;

A
Alexandru Dima 已提交
413
			case KeyMod.CtrlCmd | KeyCode.DownArrow:
414 415 416
				this._codeEditor.focus();
				e.preventDefault();
				return;
E
Erich Gamma 已提交
417 418 419
		}
	}

A
Alex Dima 已提交
420
	// ----- initialization
E
Erich Gamma 已提交
421

J
Johannes Rieken 已提交
422
	private _keybindingLabelFor(actionId: string): string {
B
Benjamin Pasero 已提交
423 424
		let [kb] = this._keybindingService.lookupKeybindings(actionId);
		if (!kb) {
425 426
			return '';
		}
B
Benjamin Pasero 已提交
427
		return ` (${this._keybindingService.getLabelFor(kb)})`;
428 429
	}

E
Erich Gamma 已提交
430 431
	private _buildFindPart(): HTMLElement {
		// Find input
A
Alex Dima 已提交
432
		this._findInput = this._register(new FindInput(null, this._contextViewProvider, {
E
Erich Gamma 已提交
433
			width: FindWidget.FIND_INPUT_AREA_WIDTH,
434 435
			label: NLS_FIND_INPUT_LABEL,
			placeholder: NLS_FIND_INPUT_PLACEHOLDER,
A
Alex Dima 已提交
436 437 438
			appendCaseSensitiveLabel: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
			appendWholeWordsLabel: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
			appendRegexLabel: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
J
Johannes Rieken 已提交
439
			validation: (value: string): InputBoxMessage => {
E
Erich Gamma 已提交
440 441 442 443 444 445 446
				if (value.length === 0) {
					return null;
				}
				if (!this._findInput.getRegex()) {
					return null;
				}
				try {
A
tslint  
Alex Dima 已提交
447
					/* tslint:disable:no-unused-expression */
A
tslint  
Alex Dima 已提交
448
					new RegExp(value);
A
tslint  
Alex Dima 已提交
449
					/* tslint:enable:no-unused-expression */
E
Erich Gamma 已提交
450 451 452 453 454
					return null;
				} catch (e) {
					return { content: e.message };
				}
			}
A
Alex Dima 已提交
455 456
		}));
		this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e)));
457 458 459
		this._register(this._findInput.onInput(() => {
			this._state.change({ searchString: this._findInput.getValue() }, true);
		}));
A
Alex Dima 已提交
460
		this._register(this._findInput.onDidOptionChange(() => {
A
Alex Dima 已提交
461 462 463 464 465
			this._state.change({
				isRegex: this._findInput.getRegex(),
				wholeWord: this._findInput.getWholeWords(),
				matchCase: this._findInput.getCaseSensitive()
			}, true);
A
Alex Dima 已提交
466
		}));
467
		this._register(this._findInput.onCaseSensitiveKeyDown((e) => {
A
Alexandru Dima 已提交
468
			if (e.equals(KeyMod.Shift | KeyCode.Tab)) {
469 470 471 472 473 474
				if (this._isReplaceVisible) {
					this._replaceInputBox.focus();
					e.preventDefault();
				}
			}
		}));
E
Erich Gamma 已提交
475

476 477 478 479
		this._matchesCount = document.createElement('div');
		this._matchesCount.className = 'matchesCount';
		this._updateMatchesCount();

E
Erich Gamma 已提交
480
		// Previous button
A
Alex Dima 已提交
481
		this._prevBtn = this._register(new SimpleButton({
A
Alex Dima 已提交
482
			label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.PreviousMatchFindAction),
A
Alex Dima 已提交
483 484
			className: 'previous',
			onTrigger: () => {
A
Alex Dima 已提交
485
				this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().done(null, onUnexpectedError);
A
Alex Dima 已提交
486
			},
J
Johannes Rieken 已提交
487
			onKeyDown: (e) => { }
A
Alex Dima 已提交
488
		}));
E
Erich Gamma 已提交
489 490

		// Next button
A
Alex Dima 已提交
491
		this._nextBtn = this._register(new SimpleButton({
A
Alex Dima 已提交
492
			label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.NextMatchFindAction),
A
Alex Dima 已提交
493 494
			className: 'next',
			onTrigger: () => {
A
Alex Dima 已提交
495
				this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().done(null, onUnexpectedError);
A
Alex Dima 已提交
496
			},
J
Johannes Rieken 已提交
497
			onKeyDown: (e) => { }
A
Alex Dima 已提交
498
		}));
E
Erich Gamma 已提交
499

A
Alex Dima 已提交
500
		let findPart = document.createElement('div');
E
Erich Gamma 已提交
501 502
		findPart.className = 'find-part';
		findPart.appendChild(this._findInput.domNode);
503
		findPart.appendChild(this._matchesCount);
E
Erich Gamma 已提交
504 505 506 507
		findPart.appendChild(this._prevBtn.domNode);
		findPart.appendChild(this._nextBtn.domNode);

		// Toggle selection button
508 509 510 511 512
		this._toggleSelectionFind = this._register(new SimpleCheckbox({
			parent: findPart,
			title: NLS_TOGGLE_SELECTION_FIND_TITLE,
			onChange: () => {
				if (this._toggleSelectionFind.checked) {
513
					let selection = this._codeEditor.getSelection();
514
					if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
515 516
						selection = selection.setEndPosition(selection.endLineNumber - 1, 1);
					}
517 518 519
					if (!selection.isEmpty()) {
						this._state.change({ searchScope: selection }, true);
					}
520 521 522
				} else {
					this._state.change({ searchScope: null }, true);
				}
E
Erich Gamma 已提交
523 524 525 526
			}
		}));

		// Close button
A
Alex Dima 已提交
527
		this._closeBtn = this._register(new SimpleButton({
A
Alex Dima 已提交
528
			label: NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.CloseFindWidgetCommand),
A
Alex Dima 已提交
529 530 531 532 533
			className: 'close-fw',
			onTrigger: () => {
				this._state.change({ isRevealed: false }, false);
			},
			onKeyDown: (e) => {
A
Alexandru Dima 已提交
534
				if (e.equals(KeyCode.Tab)) {
A
Alex Dima 已提交
535
					if (this._isReplaceVisible) {
536 537 538 539 540
						if (this._replaceBtn.isEnabled()) {
							this._replaceBtn.focus();
						} else {
							this._codeEditor.focus();
						}
A
Alex Dima 已提交
541 542
						e.preventDefault();
					}
A
Alex Dima 已提交
543
				}
E
Erich Gamma 已提交
544
			}
A
Alex Dima 已提交
545
		}));
E
Erich Gamma 已提交
546 547 548 549 550 551 552 553

		findPart.appendChild(this._closeBtn.domNode);

		return findPart;
	}

	private _buildReplacePart(): HTMLElement {
		// Replace input
A
Alex Dima 已提交
554
		let replaceInput = document.createElement('div');
E
Erich Gamma 已提交
555 556
		replaceInput.className = 'replace-input';
		replaceInput.style.width = FindWidget.REPLACE_INPUT_AREA_WIDTH + 'px';
A
Alex Dima 已提交
557
		this._replaceInputBox = this._register(new InputBox(replaceInput, null, {
558 559
			ariaLabel: NLS_REPLACE_INPUT_LABEL,
			placeholder: NLS_REPLACE_INPUT_PLACEHOLDER
A
Alex Dima 已提交
560
		}));
E
Erich Gamma 已提交
561

A
Alex Dima 已提交
562 563
		this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'keydown', (e) => this._onReplaceInputKeyDown(e)));
		this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'input', (e) => {
564 565
			this._state.change({ replaceString: this._replaceInputBox.value }, false);
		}));
E
Erich Gamma 已提交
566 567

		// Replace one button
A
Alex Dima 已提交
568
		this._replaceBtn = this._register(new SimpleButton({
569
			label: NLS_REPLACE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceOneAction),
A
Alex Dima 已提交
570 571 572 573
			className: 'replace',
			onTrigger: () => {
				this._controller.replace();
			},
A
Alex Dima 已提交
574
			onKeyDown: (e) => {
A
Alexandru Dima 已提交
575
				if (e.equals(KeyMod.Shift | KeyCode.Tab)) {
A
Alex Dima 已提交
576 577 578 579 580
					this._closeBtn.focus();
					e.preventDefault();
				}
			}
		}));
E
Erich Gamma 已提交
581 582

		// Replace all button
A
Alex Dima 已提交
583
		this._replaceAllBtn = this._register(new SimpleButton({
584
			label: NLS_REPLACE_ALL_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceAllAction),
A
Alex Dima 已提交
585 586 587 588
			className: 'replace-all',
			onTrigger: () => {
				this._controller.replaceAll();
			},
J
Johannes Rieken 已提交
589
			onKeyDown: (e) => { }
A
Alex Dima 已提交
590
		}));
E
Erich Gamma 已提交
591

A
Alex Dima 已提交
592
		let replacePart = document.createElement('div');
E
Erich Gamma 已提交
593 594 595 596 597 598 599 600 601 602
		replacePart.className = 'replace-part';
		replacePart.appendChild(replaceInput);
		replacePart.appendChild(this._replaceBtn.domNode);
		replacePart.appendChild(this._replaceAllBtn.domNode);

		return replacePart;
	}

	private _buildDomNode(): void {
		// Find part
A
Alex Dima 已提交
603
		let findPart = this._buildFindPart();
E
Erich Gamma 已提交
604 605

		// Replace part
A
Alex Dima 已提交
606
		let replacePart = this._buildReplacePart();
E
Erich Gamma 已提交
607 608

		// Toggle replace button
A
Alex Dima 已提交
609
		this._toggleReplaceBtn = this._register(new SimpleButton({
A
Alex Dima 已提交
610 611 612
			label: NLS_TOGGLE_REPLACE_MODE_BTN_LABEL,
			className: 'toggle left',
			onTrigger: () => {
613
				this._state.change({ isReplaceRevealed: !this._isReplaceVisible }, false);
A
Alex Dima 已提交
614
			},
J
Johannes Rieken 已提交
615
			onKeyDown: (e) => { }
A
Alex Dima 已提交
616
		}));
E
Erich Gamma 已提交
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
		this._toggleReplaceBtn.toggleClass('expand', this._isReplaceVisible);
		this._toggleReplaceBtn.toggleClass('collapse', !this._isReplaceVisible);
		this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);

		// Widget
		this._domNode = document.createElement('div');
		this._domNode.className = 'editor-widget find-widget';
		this._domNode.setAttribute('aria-hidden', 'false');

		this._domNode.appendChild(this._toggleReplaceBtn.domNode);
		this._domNode.appendChild(findPart);
		this._domNode.appendChild(replacePart);
	}
}

632 633 634 635 636 637 638
interface ISimpleCheckboxOpts {
	parent: HTMLElement;
	title: string;
	onChange: () => void;
}

class SimpleCheckbox extends Widget {
A
Alex Dima 已提交
639 640

	private static _COUNTER = 0;
E
Erich Gamma 已提交
641

642
	private _opts: ISimpleCheckboxOpts;
A
Alex Dima 已提交
643 644 645
	private _domNode: HTMLElement;
	private _checkbox: HTMLInputElement;
	private _label: HTMLLabelElement;
E
Erich Gamma 已提交
646

J
Johannes Rieken 已提交
647
	constructor(opts: ISimpleCheckboxOpts) {
A
Alex Dima 已提交
648
		super();
649 650
		this._opts = opts;

E
Erich Gamma 已提交
651 652
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-checkbox';
653
		this._domNode.title = this._opts.title;
E
Erich Gamma 已提交
654 655 656 657

		this._checkbox = document.createElement('input');
		this._checkbox.type = 'checkbox';
		this._checkbox.className = 'checkbox';
658
		this._checkbox.id = 'checkbox-' + SimpleCheckbox._COUNTER++;
E
Erich Gamma 已提交
659

A
Alex Dima 已提交
660 661
		this._label = document.createElement('label');
		this._label.className = 'label';
E
Erich Gamma 已提交
662
		// Connect the label and the checkbox. Checkbox will get checked when the label recieves a click.
A
Alex Dima 已提交
663
		this._label.htmlFor = this._checkbox.id;
E
Erich Gamma 已提交
664 665

		this._domNode.appendChild(this._checkbox);
A
Alex Dima 已提交
666
		this._domNode.appendChild(this._label);
E
Erich Gamma 已提交
667

668 669 670 671 672
		this._opts.parent.appendChild(this._domNode);

		this.onchange(this._checkbox, (e) => {
			this._opts.onChange();
		});
E
Erich Gamma 已提交
673 674 675 676 677 678
	}

	public get domNode(): HTMLElement {
		return this._domNode;
	}

679 680 681 682
	public get checked(): boolean {
		return this._checkbox.checked;
	}

J
Johannes Rieken 已提交
683
	public set checked(newValue: boolean) {
684
		this._checkbox.checked = newValue;
E
Erich Gamma 已提交
685 686 687 688 689 690
	}

	public focus(): void {
		this._checkbox.focus();
	}

691
	private enable(): void {
E
Erich Gamma 已提交
692 693 694
		this._checkbox.removeAttribute('disabled');
	}

695
	private disable(): void {
E
Erich Gamma 已提交
696 697
		this._checkbox.disabled = true;
	}
698

J
Johannes Rieken 已提交
699
	public setEnabled(enabled: boolean): void {
700 701 702 703 704 705
		if (enabled) {
			this.enable();
		} else {
			this.disable();
		}
	}
E
Erich Gamma 已提交
706 707
}

A
Alex Dima 已提交
708 709 710
interface ISimpleButtonOpts {
	label: string;
	className: string;
J
Johannes Rieken 已提交
711 712
	onTrigger: () => void;
	onKeyDown: (e: IKeyboardEvent) => void;
A
Alex Dima 已提交
713
}
E
Erich Gamma 已提交
714

A
Alex Dima 已提交
715
class SimpleButton extends Widget {
E
Erich Gamma 已提交
716

A
Alex Dima 已提交
717 718
	private _opts: ISimpleButtonOpts;
	private _domNode: HTMLElement;
E
Erich Gamma 已提交
719

J
Johannes Rieken 已提交
720
	constructor(opts: ISimpleButtonOpts) {
A
Alex Dima 已提交
721
		super();
A
Alex Dima 已提交
722
		this._opts = opts;
E
Erich Gamma 已提交
723 724

		this._domNode = document.createElement('div');
A
Alex Dima 已提交
725
		this._domNode.title = this._opts.label;
A
Alex Dima 已提交
726
		this._domNode.tabIndex = 0;
A
Alex Dima 已提交
727
		this._domNode.className = 'button ' + this._opts.className;
E
Erich Gamma 已提交
728
		this._domNode.setAttribute('role', 'button');
A
Alex Dima 已提交
729
		this._domNode.setAttribute('aria-label', this._opts.label);
E
Erich Gamma 已提交
730

A
Alex Dima 已提交
731
		this.onclick(this._domNode, (e) => {
A
Alex Dima 已提交
732
			this._opts.onTrigger();
E
Erich Gamma 已提交
733
			e.preventDefault();
A
Alex Dima 已提交
734 735
		});
		this.onkeydown(this._domNode, (e) => {
A
Alexandru Dima 已提交
736
			if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) {
A
Alex Dima 已提交
737
				this._opts.onTrigger();
E
Erich Gamma 已提交
738 739 740
				e.preventDefault();
				return;
			}
A
Alex Dima 已提交
741
			this._opts.onKeyDown(e);
A
Alex Dima 已提交
742
		});
E
Erich Gamma 已提交
743 744 745 746 747 748
	}

	public get domNode(): HTMLElement {
		return this._domNode;
	}

749 750 751 752
	public isEnabled(): boolean {
		return (this._domNode.tabIndex >= 0);
	}

E
Erich Gamma 已提交
753 754 755 756
	public focus(): void {
		this._domNode.focus();
	}

J
Johannes Rieken 已提交
757
	public setEnabled(enabled: boolean): void {
A
Alex Dima 已提交
758
		dom.toggleClass(this._domNode, 'disabled', !enabled);
E
Erich Gamma 已提交
759 760 761 762
		this._domNode.setAttribute('aria-disabled', String(!enabled));
		this._domNode.tabIndex = enabled ? 0 : -1;
	}

J
Johannes Rieken 已提交
763
	public setExpanded(expanded: boolean): void {
E
Erich Gamma 已提交
764 765 766
		this._domNode.setAttribute('aria-expanded', String(expanded));
	}

J
Johannes Rieken 已提交
767
	public toggleClass(className: string, shouldHaveIt: boolean): void {
A
Alex Dima 已提交
768
		dom.toggleClass(this._domNode, className, shouldHaveIt);
E
Erich Gamma 已提交
769 770
	}
}