Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/find/browser/findWidget.ts
5272 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
9
import { alert as alertFn } from '../../../../base/browser/ui/aria/aria.js';
10
import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js';
11
import { IContextViewProvider } from '../../../../base/browser/ui/contextview/contextview.js';
12
import { FindInput } from '../../../../base/browser/ui/findinput/findInput.js';
13
import { ReplaceInput } from '../../../../base/browser/ui/findinput/replaceInput.js';
14
import { IMessage as InputBoxMessage } from '../../../../base/browser/ui/inputbox/inputBox.js';
15
import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from '../../../../base/browser/ui/sash/sash.js';
16
import { Widget } from '../../../../base/browser/ui/widget.js';
17
import { Delayer, disposableTimeout } from '../../../../base/common/async.js';
18
import { Codicon } from '../../../../base/common/codicons.js';
19
import { onUnexpectedError } from '../../../../base/common/errors.js';
20
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
21
import { toDisposable, IDisposable } from '../../../../base/common/lifecycle.js';
22
import * as platform from '../../../../base/common/platform.js';
23
import * as strings from '../../../../base/common/strings.js';
24
import './findWidget.css';
25
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, OverlayWidgetPositionPreference } from '../../../browser/editorBrowser.js';
26
import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';
27
import { Range } from '../../../common/core/range.js';
28
import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_FOCUSED, CONTEXT_REPLACE_INPUT_FOCUSED, FIND_IDS, MATCHES_LIMIT } from './findModel.js';
29
import { FindReplaceState, FindReplaceStateChangedEvent } from './findState.js';
30
import * as nls from '../../../../nls.js';
31
import { AccessibilitySupport, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
32
import { ContextScopedFindInput, ContextScopedReplaceInput } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
33
import { showHistoryKeybindingHint } from '../../../../platform/history/browser/historyWidgetKeybindingHint.js';
34
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
35
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
36
import { asCssVariable, contrastBorder, editorFindMatchForeground, editorFindMatchHighlightBorder, editorFindMatchHighlightForeground, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../platform/theme/common/colorRegistry.js';
37
import { registerIcon, widgetClose } from '../../../../platform/theme/common/iconRegistry.js';
38
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
39
import { ThemeIcon } from '../../../../base/common/themables.js';
40
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
41
import { assertReturnsDefined } from '../../../../base/common/types.js';
42
import { defaultInputBoxStyles, defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js';
43
import { Selection } from '../../../common/core/selection.js';
44
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
45
import { IHistory } from '../../../../base/common/history.js';
46
import { HoverStyle, type IHoverLifecycleOptions } from '../../../../base/browser/ui/hover/hover.js';
47
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
48
49
const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.'));
50
const findExpandedIcon = registerIcon('find-expanded', Codicon.chevronDown, nls.localize('findExpandedIcon', 'Icon to indicate that the editor find widget is expanded.'));
51
52
export const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.'));
53
export const findReplaceIcon = registerIcon('find-replace', Codicon.replace, nls.localize('findReplaceIcon', 'Icon for \'Replace\' in the editor find widget.'));
54
export const findReplaceAllIcon = registerIcon('find-replace-all', Codicon.replaceAll, nls.localize('findReplaceAllIcon', 'Icon for \'Replace All\' in the editor find widget.'));
55
export const findPreviousMatchIcon = registerIcon('find-previous-match', Codicon.arrowUp, nls.localize('findPreviousMatchIcon', 'Icon for \'Find Previous\' in the editor find widget.'));
56
export const findNextMatchIcon = registerIcon('find-next-match', Codicon.arrowDown, nls.localize('findNextMatchIcon', 'Icon for \'Find Next\' in the editor find widget.'));
57
58
export interface IFindController {
59
replace(): void;
60
replaceAll(): void;
61
getGlobalBufferTerm(): Promise<string>;
62
}
63
64
const NLS_FIND_DIALOG_LABEL = nls.localize('label.findDialog', "Find / Replace");
65
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
66
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
67
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match");
68
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match");
69
const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind', "Find in Selection");
70
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
71
const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace");
72
const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace");
73
const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace");
74
const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All");
75
const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace");
76
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first {0} results are highlighted, but all find operations work on the entire text.", MATCHES_LIMIT);
77
export const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}");
78
export const NLS_NO_RESULTS = nls.localize('label.noResults', "No results");
79
80
const FIND_WIDGET_INITIAL_WIDTH = 419;
81
const PART_WIDTH = 275;
82
const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54;
83
84
let MAX_MATCHES_COUNT_WIDTH = 69;
85
// let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */;
86
87
const FIND_INPUT_AREA_HEIGHT = 33; // The height of Find Widget when Replace Input is not visible.
88
89
const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
90
export class FindWidgetViewZone implements IViewZone {
91
public readonly afterLineNumber: number;
92
public heightInPx: number;
93
public readonly suppressMouseDown: boolean;
94
public readonly domNode: HTMLElement;
95
96
constructor(afterLineNumber: number) {
97
this.afterLineNumber = afterLineNumber;
98
99
this.heightInPx = FIND_INPUT_AREA_HEIGHT;
100
this.suppressMouseDown = false;
101
this.domNode = document.createElement('div');
102
this.domNode.className = 'dock-find-viewzone';
103
}
104
}
105
106
function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
107
const isMultiline = !!value.match(/\n/);
108
if (textarea && isMultiline && textarea.selectionStart > 0) {
109
event.stopPropagation();
110
return;
111
}
112
}
113
114
function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) {
115
const isMultiline = !!value.match(/\n/);
116
if (textarea && isMultiline && textarea.selectionEnd < textarea.value.length) {
117
event.stopPropagation();
118
return;
119
}
120
}
121
122
export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashLayoutProvider {
123
private static readonly ID = 'editor.contrib.findWidget';
124
private readonly _codeEditor: ICodeEditor;
125
private readonly _state: FindReplaceState;
126
private readonly _controller: IFindController;
127
private readonly _contextViewProvider: IContextViewProvider;
128
private readonly _keybindingService: IKeybindingService;
129
private readonly _contextKeyService: IContextKeyService;
130
131
private _domNode!: HTMLElement;
132
private _cachedHeight: number | null = null;
133
private _findInput!: FindInput;
134
private _replaceInput!: ReplaceInput;
135
136
private _toggleReplaceBtn!: SimpleButton;
137
private _matchesCount!: HTMLElement;
138
private _prevBtn!: SimpleButton;
139
private _nextBtn!: SimpleButton;
140
private _toggleSelectionFind!: Toggle;
141
private _closeBtn!: SimpleButton;
142
private _replaceBtn!: SimpleButton;
143
private _replaceAllBtn!: SimpleButton;
144
145
private _isVisible: boolean;
146
private _isReplaceVisible: boolean;
147
private _ignoreChangeEvent: boolean;
148
private _accessibilityHelpHintAnnounced: boolean;
149
private _labelResetTimeout: IDisposable | undefined;
150
private _lastFocusedInputWasReplace: boolean = false;
151
152
private readonly _findFocusTracker: dom.IFocusTracker;
153
private readonly _findInputFocused: IContextKey<boolean>;
154
private readonly _replaceFocusTracker: dom.IFocusTracker;
155
private readonly _replaceInputFocused: IContextKey<boolean>;
156
private _widgetFocusTracker: dom.IFocusTracker | undefined;
157
private readonly _findWidgetFocused: IContextKey<boolean>;
158
private _lastFocusedElement: HTMLElement | null = null;
159
private _viewZone?: FindWidgetViewZone;
160
private _viewZoneId?: string;
161
162
private _resizeSash!: Sash;
163
private _resized!: boolean;
164
private readonly _updateHistoryDelayer: Delayer<void>;
165
166
constructor(
167
codeEditor: ICodeEditor,
168
controller: IFindController,
169
state: FindReplaceState,
170
contextViewProvider: IContextViewProvider,
171
keybindingService: IKeybindingService,
172
contextKeyService: IContextKeyService,
173
private readonly _hoverService: IHoverService,
174
private readonly _findWidgetSearchHistory: IHistory<string> | undefined,
175
private readonly _replaceWidgetHistory: IHistory<string> | undefined,
176
private readonly _configurationService: IConfigurationService,
177
private readonly _accessibilityService: IAccessibilityService,
178
) {
179
super();
180
this._codeEditor = codeEditor;
181
this._controller = controller;
182
this._state = state;
183
this._contextViewProvider = contextViewProvider;
184
this._keybindingService = keybindingService;
185
this._contextKeyService = contextKeyService;
186
187
this._isVisible = false;
188
this._isReplaceVisible = false;
189
this._ignoreChangeEvent = false;
190
this._accessibilityHelpHintAnnounced = false;
191
192
this._updateHistoryDelayer = new Delayer<void>(500);
193
this._register(toDisposable(() => this._updateHistoryDelayer.cancel()));
194
this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
195
this._buildDomNode();
196
this._updateButtons();
197
this._tryUpdateWidgetWidth();
198
this._findInput.inputBox.layout();
199
200
this._register(this._codeEditor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
201
if (e.hasChanged(EditorOption.readOnly)) {
202
if (this._codeEditor.getOption(EditorOption.readOnly)) {
203
// Hide replace part if editor becomes read only
204
this._state.change({ isReplaceRevealed: false }, false);
205
}
206
this._updateButtons();
207
}
208
if (e.hasChanged(EditorOption.layoutInfo)) {
209
this._tryUpdateWidgetWidth();
210
}
211
212
if (e.hasChanged(EditorOption.accessibilitySupport)) {
213
this.updateAccessibilitySupport();
214
}
215
216
if (e.hasChanged(EditorOption.find)) {
217
const supportLoop = this._codeEditor.getOption(EditorOption.find).loop;
218
this._state.change({ loop: supportLoop }, false);
219
const addExtraSpaceOnTop = this._codeEditor.getOption(EditorOption.find).addExtraSpaceOnTop;
220
if (addExtraSpaceOnTop && !this._viewZone) {
221
this._viewZone = new FindWidgetViewZone(0);
222
this._showViewZone();
223
}
224
if (!addExtraSpaceOnTop && this._viewZone) {
225
this._removeViewZone();
226
}
227
}
228
}));
229
this.updateAccessibilitySupport();
230
this._register(this._codeEditor.onDidChangeCursorSelection(() => {
231
if (this._isVisible) {
232
this._updateToggleSelectionFindButton();
233
}
234
}));
235
this._register(this._codeEditor.onDidFocusEditorWidget(async () => {
236
if (this._isVisible) {
237
const globalBufferTerm = await this._controller.getGlobalBufferTerm();
238
if (globalBufferTerm && globalBufferTerm !== this._state.searchString) {
239
this._state.change({ searchString: globalBufferTerm }, false);
240
this._findInput.select();
241
}
242
}
243
}));
244
this._findInputFocused = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService);
245
this._findFocusTracker = this._register(dom.trackFocus(this._findInput.inputBox.inputElement));
246
this._register(this._findFocusTracker.onDidFocus(() => {
247
this._findInputFocused.set(true);
248
this._lastFocusedInputWasReplace = false;
249
this._updateSearchScope();
250
}));
251
this._register(this._findFocusTracker.onDidBlur(() => {
252
this._findInputFocused.set(false);
253
}));
254
255
this._replaceInputFocused = CONTEXT_REPLACE_INPUT_FOCUSED.bindTo(contextKeyService);
256
this._replaceFocusTracker = this._register(dom.trackFocus(this._replaceInput.inputBox.inputElement));
257
this._register(this._replaceFocusTracker.onDidFocus(() => {
258
this._replaceInputFocused.set(true);
259
this._lastFocusedInputWasReplace = true;
260
this._updateSearchScope();
261
}));
262
this._register(this._replaceFocusTracker.onDidBlur(() => {
263
this._replaceInputFocused.set(false);
264
}));
265
266
// Track focus on the entire Find widget for accessibility help
267
this._findWidgetFocused = CONTEXT_FIND_WIDGET_FOCUSED.bindTo(contextKeyService);
268
this._widgetFocusTracker = this._register(dom.trackFocus(this._domNode));
269
this._register(this._widgetFocusTracker.onDidFocus(() => {
270
this._findWidgetFocused.set(true);
271
}));
272
this._register(this._widgetFocusTracker.onDidBlur(() => {
273
this._findWidgetFocused.set(false);
274
}));
275
276
// Track which element was last focused within the widget using focusin (which bubbles)
277
this._register(dom.addDisposableListener(this._domNode, 'focusin', (e: FocusEvent) => {
278
if (dom.isHTMLElement(e.target)) {
279
this._lastFocusedElement = e.target;
280
}
281
}));
282
283
this._codeEditor.addOverlayWidget(this);
284
if (this._codeEditor.getOption(EditorOption.find).addExtraSpaceOnTop) {
285
this._viewZone = new FindWidgetViewZone(0); // Put it before the first line then users can scroll beyond the first line.
286
}
287
288
this._register(this._codeEditor.onDidChangeModel(() => {
289
if (!this._isVisible) {
290
return;
291
}
292
this._viewZoneId = undefined;
293
}));
294
295
296
this._register(this._codeEditor.onDidScrollChange((e) => {
297
if (e.scrollTopChanged) {
298
this._layoutViewZone();
299
return;
300
}
301
302
// for other scroll changes, layout the viewzone in next tick to avoid ruining current rendering.
303
setTimeout(() => {
304
this._layoutViewZone();
305
}, 0);
306
}));
307
}
308
309
// ----- IOverlayWidget API
310
311
public getId(): string {
312
return FindWidget.ID;
313
}
314
315
public getDomNode(): HTMLElement {
316
return this._domNode;
317
}
318
319
/**
320
* Returns whether the Replace input was the last focused input in the find widget.
321
* This persists even after focus leaves the widget, allowing external code to know
322
* which input to restore focus to.
323
*/
324
public get lastFocusedInputWasReplace(): boolean {
325
return this._lastFocusedInputWasReplace;
326
}
327
328
/**
329
* Returns the last focused element within the Find widget.
330
* This is useful for restoring focus to the exact element after
331
* accessibility help or other overlays are dismissed.
332
*/
333
public get lastFocusedElement(): HTMLElement | null {
334
return this._lastFocusedElement;
335
}
336
337
/**
338
* Focuses the last focused element in the Find widget.
339
* Falls back to the Find or Replace input based on lastFocusedInputWasReplace.
340
*/
341
public focusLastElement(): void {
342
if (!this._isVisible) {
343
return;
344
}
345
if (this._lastFocusedElement && this._domNode.contains(this._lastFocusedElement) && dom.getWindow(this._lastFocusedElement).document.body.contains(this._lastFocusedElement)) {
346
this._lastFocusedElement.focus();
347
} else if (this._lastFocusedInputWasReplace) {
348
this.focusReplaceInput();
349
} else {
350
this.focusFindInput();
351
}
352
}
353
354
public getPosition(): IOverlayWidgetPosition | null {
355
if (this._isVisible) {
356
return {
357
preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
358
};
359
}
360
return null;
361
}
362
363
// ----- React to state changes
364
365
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
366
if (e.searchString) {
367
try {
368
this._ignoreChangeEvent = true;
369
this._findInput.setValue(this._state.searchString);
370
} finally {
371
this._ignoreChangeEvent = false;
372
}
373
this._updateButtons();
374
}
375
if (e.replaceString) {
376
this._replaceInput.inputBox.value = this._state.replaceString;
377
}
378
if (e.isRevealed) {
379
if (this._state.isRevealed) {
380
this._reveal();
381
} else {
382
this._hide(true);
383
}
384
}
385
if (e.isReplaceRevealed) {
386
if (this._state.isReplaceRevealed) {
387
if (!this._codeEditor.getOption(EditorOption.readOnly) && !this._isReplaceVisible) {
388
this._isReplaceVisible = true;
389
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
390
this._updateButtons();
391
this._replaceInput.inputBox.layout();
392
}
393
} else {
394
if (this._isReplaceVisible) {
395
this._isReplaceVisible = false;
396
this._updateButtons();
397
}
398
}
399
}
400
if ((e.isRevealed || e.isReplaceRevealed) && (this._state.isRevealed || this._state.isReplaceRevealed)) {
401
if (this._tryUpdateHeight()) {
402
this._showViewZone();
403
}
404
}
405
406
if (e.isRegex) {
407
this._findInput.setRegex(this._state.isRegex);
408
}
409
if (e.wholeWord) {
410
this._findInput.setWholeWords(this._state.wholeWord);
411
}
412
if (e.matchCase) {
413
this._findInput.setCaseSensitive(this._state.matchCase);
414
}
415
if (e.preserveCase) {
416
this._replaceInput.setPreserveCase(this._state.preserveCase);
417
}
418
if (e.searchScope) {
419
if (this._state.searchScope) {
420
this._toggleSelectionFind.checked = true;
421
} else {
422
this._toggleSelectionFind.checked = false;
423
}
424
this._updateToggleSelectionFindButton();
425
}
426
if (e.searchString || e.matchesCount || e.matchesPosition) {
427
const showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0);
428
this._domNode.classList.toggle('no-results', showRedOutline);
429
430
this._updateMatchesCount();
431
this._updateButtons();
432
}
433
if (e.searchString || e.currentMatch) {
434
this._layoutViewZone();
435
}
436
if (e.updateHistory) {
437
this._delayedUpdateHistory();
438
}
439
if (e.loop) {
440
this._updateButtons();
441
}
442
}
443
444
private _delayedUpdateHistory() {
445
this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)).then(undefined, onUnexpectedError);
446
}
447
448
private _updateHistory() {
449
if (this._state.searchString) {
450
this._findInput.inputBox.addToHistory();
451
}
452
if (this._state.replaceString) {
453
this._replaceInput.inputBox.addToHistory();
454
}
455
}
456
457
private _updateMatchesCount(): void {
458
this._matchesCount.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px';
459
if (this._state.matchesCount >= MATCHES_LIMIT) {
460
this._matchesCount.title = NLS_MATCHES_COUNT_LIMIT_TITLE;
461
} else {
462
this._matchesCount.title = '';
463
}
464
465
// remove previous content
466
this._matchesCount.firstChild?.remove();
467
468
let label: string;
469
if (this._state.matchesCount > 0) {
470
let matchesCount: string = String(this._state.matchesCount);
471
if (this._state.matchesCount >= MATCHES_LIMIT) {
472
matchesCount += '+';
473
}
474
let matchesPosition: string = String(this._state.matchesPosition);
475
if (matchesPosition === '0') {
476
matchesPosition = '?';
477
}
478
label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount);
479
} else {
480
label = NLS_NO_RESULTS;
481
}
482
483
this._matchesCount.appendChild(document.createTextNode(label));
484
485
alertFn(this._getAriaLabel(label, this._state.currentMatch, this._state.searchString));
486
MAX_MATCHES_COUNT_WIDTH = Math.max(MAX_MATCHES_COUNT_WIDTH, this._matchesCount.clientWidth);
487
}
488
489
// ----- actions
490
491
private _getAriaLabel(label: string, currentMatch: Range | null, searchString: string): string {
492
let result: string;
493
if (label === NLS_NO_RESULTS) {
494
result = searchString === ''
495
? nls.localize('ariaSearchNoResultEmpty', "{0} found", label)
496
: nls.localize('ariaSearchNoResult', "{0} found for '{1}'", label, searchString);
497
} else if (currentMatch) {
498
const ariaLabel = nls.localize('ariaSearchNoResultWithLineNum', "{0} found for '{1}', at {2}", label, searchString, currentMatch.startLineNumber + ':' + currentMatch.startColumn);
499
const model = this._codeEditor.getModel();
500
if (model && (currentMatch.startLineNumber <= model.getLineCount()) && (currentMatch.startLineNumber >= 1)) {
501
const lineContent = model.getLineContent(currentMatch.startLineNumber);
502
result = `${lineContent}, ${ariaLabel}`;
503
} else {
504
result = ariaLabel;
505
}
506
} else {
507
result = nls.localize('ariaSearchNoResultWithLineNumNoCurrentMatch', "{0} found for '{1}'", label, searchString);
508
}
509
510
return result;
511
}
512
513
/**
514
* If 'selection find' is ON we should not disable the button (its function is to cancel 'selection find').
515
* If 'selection find' is OFF we enable the button only if there is a selection.
516
*/
517
private _updateToggleSelectionFindButton(): void {
518
const selection = this._codeEditor.getSelection();
519
const isSelection = selection ? (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn) : false;
520
const isChecked = this._toggleSelectionFind.checked;
521
522
if (this._isVisible && (isChecked || isSelection)) {
523
this._toggleSelectionFind.enable();
524
} else {
525
this._toggleSelectionFind.disable();
526
}
527
}
528
529
private _updateButtons(): void {
530
this._findInput.setEnabled(this._isVisible);
531
this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible);
532
this._updateToggleSelectionFindButton();
533
this._closeBtn.setEnabled(this._isVisible);
534
535
const findInputIsNonEmpty = (this._state.searchString.length > 0);
536
const matchesCount = this._state.matchesCount ? true : false;
537
this._prevBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount && this._state.canNavigateBack());
538
this._nextBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount && this._state.canNavigateForward());
539
this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
540
this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
541
542
this._domNode.classList.toggle('replaceToggled', this._isReplaceVisible);
543
this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);
544
545
const canReplace = !this._codeEditor.getOption(EditorOption.readOnly);
546
this._toggleReplaceBtn.setEnabled(this._isVisible && canReplace);
547
}
548
549
private _revealTimeouts: Timeout[] = [];
550
551
private _reveal(): void {
552
this._revealTimeouts.forEach(e => {
553
clearTimeout(e);
554
});
555
556
this._revealTimeouts = [];
557
558
if (!this._isVisible) {
559
this._isVisible = true;
560
561
const selection = this._codeEditor.getSelection();
562
563
switch (this._codeEditor.getOption(EditorOption.find).autoFindInSelection) {
564
case 'always':
565
this._toggleSelectionFind.checked = true;
566
break;
567
case 'never':
568
this._toggleSelectionFind.checked = false;
569
break;
570
case 'multiline': {
571
const isSelectionMultipleLine = !!selection && selection.startLineNumber !== selection.endLineNumber;
572
this._toggleSelectionFind.checked = isSelectionMultipleLine;
573
break;
574
}
575
default:
576
break;
577
}
578
579
this._tryUpdateWidgetWidth();
580
this._updateButtons();
581
582
this._revealTimeouts.push(setTimeout(() => {
583
this._domNode.classList.add('visible');
584
this._domNode.setAttribute('aria-hidden', 'false');
585
this._updateFindInputAriaLabel();
586
}, 0));
587
588
// validate query again as it's being dismissed when we hide the find widget.
589
this._revealTimeouts.push(setTimeout(() => {
590
this._findInput.validate();
591
}, 200));
592
593
this._codeEditor.layoutOverlayWidget(this);
594
595
let adjustEditorScrollTop = true;
596
if (this._codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection && selection) {
597
const domNode = this._codeEditor.getDomNode();
598
if (domNode) {
599
const editorCoords = dom.getDomNodePagePosition(domNode);
600
const startCoords = this._codeEditor.getScrolledVisiblePosition(selection.getStartPosition());
601
const startLeft = editorCoords.left + (startCoords ? startCoords.left : 0);
602
const startTop = startCoords ? startCoords.top : 0;
603
604
if (this._viewZone && startTop < this._viewZone.heightInPx) {
605
if (selection.endLineNumber > selection.startLineNumber) {
606
adjustEditorScrollTop = false;
607
}
608
609
const leftOfFindWidget = dom.getTopLeftOffset(this._domNode).left;
610
if (startLeft > leftOfFindWidget) {
611
adjustEditorScrollTop = false;
612
}
613
const endCoords = this._codeEditor.getScrolledVisiblePosition(selection.getEndPosition());
614
const endLeft = editorCoords.left + (endCoords ? endCoords.left : 0);
615
if (endLeft > leftOfFindWidget) {
616
adjustEditorScrollTop = false;
617
}
618
}
619
}
620
}
621
this._showViewZone(adjustEditorScrollTop);
622
}
623
}
624
625
private _hide(focusTheEditor: boolean): void {
626
this._revealTimeouts.forEach(e => {
627
clearTimeout(e);
628
});
629
630
this._revealTimeouts = [];
631
632
if (this._isVisible) {
633
this._isVisible = false;
634
this._accessibilityHelpHintAnnounced = false;
635
636
this._updateButtons();
637
638
this._domNode.classList.remove('visible');
639
this._domNode.setAttribute('aria-hidden', 'true');
640
this._findInput.clearMessage();
641
if (focusTheEditor) {
642
this._codeEditor.focus();
643
}
644
this._codeEditor.layoutOverlayWidget(this);
645
this._removeViewZone();
646
}
647
}
648
649
private _layoutViewZone(targetScrollTop?: number) {
650
const addExtraSpaceOnTop = this._codeEditor.getOption(EditorOption.find).addExtraSpaceOnTop;
651
652
if (!addExtraSpaceOnTop) {
653
this._removeViewZone();
654
return;
655
}
656
657
if (!this._isVisible) {
658
return;
659
}
660
const viewZone = this._viewZone;
661
if (this._viewZoneId !== undefined || !viewZone) {
662
return;
663
}
664
665
this._codeEditor.changeViewZones((accessor) => {
666
viewZone.heightInPx = this._getHeight();
667
this._viewZoneId = accessor.addZone(viewZone);
668
// scroll top adjust to make sure the editor doesn't scroll when adding viewzone at the beginning.
669
this._codeEditor.setScrollTop(targetScrollTop || this._codeEditor.getScrollTop() + viewZone.heightInPx);
670
});
671
}
672
673
private _showViewZone(adjustScroll: boolean = true) {
674
if (!this._isVisible) {
675
return;
676
}
677
678
const addExtraSpaceOnTop = this._codeEditor.getOption(EditorOption.find).addExtraSpaceOnTop;
679
680
if (!addExtraSpaceOnTop) {
681
return;
682
}
683
684
if (this._viewZone === undefined) {
685
this._viewZone = new FindWidgetViewZone(0);
686
}
687
688
const viewZone = this._viewZone;
689
690
this._codeEditor.changeViewZones((accessor) => {
691
if (this._viewZoneId !== undefined) {
692
// the view zone already exists, we need to update the height
693
const newHeight = this._getHeight();
694
if (newHeight === viewZone.heightInPx) {
695
return;
696
}
697
698
const scrollAdjustment = newHeight - viewZone.heightInPx;
699
viewZone.heightInPx = newHeight;
700
accessor.layoutZone(this._viewZoneId);
701
702
if (adjustScroll) {
703
this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment);
704
}
705
706
return;
707
} else {
708
let scrollAdjustment = this._getHeight();
709
710
// if the editor has top padding, factor that into the zone height
711
scrollAdjustment -= this._codeEditor.getOption(EditorOption.padding).top;
712
if (scrollAdjustment <= 0) {
713
return;
714
}
715
716
viewZone.heightInPx = scrollAdjustment;
717
this._viewZoneId = accessor.addZone(viewZone);
718
719
if (adjustScroll) {
720
this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment);
721
}
722
}
723
});
724
}
725
726
private _removeViewZone() {
727
this._codeEditor.changeViewZones((accessor) => {
728
if (this._viewZoneId !== undefined) {
729
accessor.removeZone(this._viewZoneId);
730
this._viewZoneId = undefined;
731
if (this._viewZone) {
732
this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() - this._viewZone.heightInPx);
733
this._viewZone = undefined;
734
}
735
}
736
});
737
}
738
739
private _tryUpdateWidgetWidth() {
740
if (!this._isVisible) {
741
return;
742
}
743
if (!this._domNode.isConnected) {
744
// the widget is not in the DOM
745
return;
746
}
747
748
const layoutInfo = this._codeEditor.getLayoutInfo();
749
const editorContentWidth = layoutInfo.contentWidth;
750
751
if (editorContentWidth <= 0) {
752
// for example, diff view original editor
753
this._domNode.classList.add('hiddenEditor');
754
return;
755
} else if (this._domNode.classList.contains('hiddenEditor')) {
756
this._domNode.classList.remove('hiddenEditor');
757
}
758
759
const editorWidth = layoutInfo.width;
760
const minimapWidth = layoutInfo.minimap.minimapWidth;
761
let collapsedFindWidget = false;
762
let reducedFindWidget = false;
763
let narrowFindWidget = false;
764
765
if (this._resized) {
766
const widgetWidth = dom.getTotalWidth(this._domNode);
767
768
if (widgetWidth > FIND_WIDGET_INITIAL_WIDTH) {
769
// as the widget is resized by users, we may need to change the max width of the widget as the editor width changes.
770
this._domNode.style.maxWidth = `${editorWidth - 28 - minimapWidth - 15}px`;
771
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
772
return;
773
}
774
}
775
776
if (FIND_WIDGET_INITIAL_WIDTH + 28 + minimapWidth >= editorWidth) {
777
reducedFindWidget = true;
778
}
779
if (FIND_WIDGET_INITIAL_WIDTH + 28 + minimapWidth - MAX_MATCHES_COUNT_WIDTH >= editorWidth) {
780
narrowFindWidget = true;
781
}
782
if (FIND_WIDGET_INITIAL_WIDTH + 28 + minimapWidth - MAX_MATCHES_COUNT_WIDTH >= editorWidth + 50) {
783
collapsedFindWidget = true;
784
}
785
this._domNode.classList.toggle('collapsed-find-widget', collapsedFindWidget);
786
this._domNode.classList.toggle('narrow-find-widget', narrowFindWidget);
787
this._domNode.classList.toggle('reduced-find-widget', reducedFindWidget);
788
789
if (!narrowFindWidget && !collapsedFindWidget) {
790
// the minimal left offset of findwidget is 15px.
791
this._domNode.style.maxWidth = `${editorWidth - 28 - minimapWidth - 15}px`;
792
}
793
794
this._findInput.layout({ collapsedFindWidget, narrowFindWidget, reducedFindWidget });
795
if (this._resized) {
796
const findInputWidth = this._findInput.inputBox.element.clientWidth;
797
if (findInputWidth > 0) {
798
this._replaceInput.width = findInputWidth;
799
}
800
} else if (this._isReplaceVisible) {
801
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
802
}
803
}
804
805
private _getHeight(): number {
806
let totalheight = 0;
807
808
// find input margin top
809
totalheight += 4;
810
811
// find input height
812
totalheight += this._findInput.inputBox.height + 2 /** input box border */;
813
814
if (this._isReplaceVisible) {
815
// replace input margin
816
totalheight += 4;
817
818
totalheight += this._replaceInput.inputBox.height + 2 /** input box border */;
819
}
820
821
// margin bottom
822
totalheight += 4;
823
return totalheight;
824
}
825
826
private _tryUpdateHeight(): boolean {
827
const totalHeight = this._getHeight();
828
if (this._cachedHeight !== null && this._cachedHeight === totalHeight) {
829
return false;
830
}
831
832
this._cachedHeight = totalHeight;
833
this._domNode.style.height = `${totalHeight}px`;
834
835
return true;
836
}
837
838
// ----- Public
839
840
public focusFindInput(): void {
841
this._findInput.select();
842
// Edge browser requires focus() in addition to select()
843
this._findInput.focus();
844
}
845
846
public focusReplaceInput(): void {
847
this._replaceInput.select();
848
// Edge browser requires focus() in addition to select()
849
this._replaceInput.focus();
850
}
851
852
public highlightFindOptions(): void {
853
this._findInput.highlightFindOptions();
854
}
855
856
private _updateSearchScope(): void {
857
if (!this._codeEditor.hasModel()) {
858
return;
859
}
860
861
if (this._toggleSelectionFind.checked) {
862
const selections = this._codeEditor.getSelections();
863
864
selections.map(selection => {
865
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
866
selection = selection.setEndPosition(
867
selection.endLineNumber - 1,
868
this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
869
);
870
}
871
const currentMatch = this._state.currentMatch;
872
if (selection.startLineNumber !== selection.endLineNumber) {
873
if (!Range.equalsRange(selection, currentMatch)) {
874
return selection;
875
}
876
}
877
return null;
878
}).filter(element => !!element);
879
880
if (selections.length) {
881
this._state.change({ searchScope: selections as Range[] }, true);
882
}
883
}
884
}
885
886
private _onFindInputMouseDown(e: IMouseEvent): void {
887
// on linux, middle key does pasting.
888
if (e.middleButton) {
889
e.stopPropagation();
890
}
891
}
892
893
private _onFindInputKeyDown(e: IKeyboardEvent): void {
894
if (e.equals(ctrlKeyMod | KeyCode.Enter)) {
895
if (this._keybindingService.dispatchEvent(e, e.target)) {
896
e.preventDefault();
897
return;
898
} else {
899
this._findInput.inputBox.insertAtCursor('\n');
900
e.preventDefault();
901
return;
902
}
903
}
904
905
if (e.equals(KeyCode.Tab)) {
906
if (this._isReplaceVisible) {
907
this._replaceInput.focus();
908
} else {
909
this._findInput.focusOnCaseSensitive();
910
}
911
e.preventDefault();
912
return;
913
}
914
915
if (e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow)) {
916
this._codeEditor.focus();
917
e.preventDefault();
918
return;
919
}
920
921
if (e.equals(KeyCode.UpArrow)) {
922
// eslint-disable-next-line no-restricted-syntax
923
return stopPropagationForMultiLineUpwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea'));
924
}
925
926
if (e.equals(KeyCode.DownArrow)) {
927
// eslint-disable-next-line no-restricted-syntax
928
return stopPropagationForMultiLineDownwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea'));
929
}
930
}
931
932
private _onReplaceInputKeyDown(e: IKeyboardEvent): void {
933
if (e.equals(ctrlKeyMod | KeyCode.Enter)) {
934
if (this._keybindingService.dispatchEvent(e, e.target)) {
935
e.preventDefault();
936
return;
937
} else {
938
this._replaceInput.inputBox.insertAtCursor('\n');
939
e.preventDefault();
940
return;
941
}
942
943
}
944
945
if (e.equals(KeyCode.Tab)) {
946
this._findInput.focusOnCaseSensitive();
947
e.preventDefault();
948
return;
949
}
950
951
if (e.equals(KeyMod.Shift | KeyCode.Tab)) {
952
this._findInput.focus();
953
e.preventDefault();
954
return;
955
}
956
957
if (e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow)) {
958
this._codeEditor.focus();
959
e.preventDefault();
960
return;
961
}
962
963
if (e.equals(KeyCode.UpArrow)) {
964
// eslint-disable-next-line no-restricted-syntax
965
return stopPropagationForMultiLineUpwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea'));
966
}
967
968
if (e.equals(KeyCode.DownArrow)) {
969
// eslint-disable-next-line no-restricted-syntax
970
return stopPropagationForMultiLineDownwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea'));
971
}
972
}
973
974
// ----- sash
975
public getVerticalSashLeft(_sash: Sash): number {
976
return 0;
977
}
978
// ----- initialization
979
980
private _keybindingLabelFor(actionId: string): string {
981
return this._keybindingService.appendKeybinding('', actionId);
982
}
983
984
private _buildDomNode(): void {
985
const flexibleHeight = true;
986
const flexibleWidth = true;
987
// Find input
988
const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).history;
989
const replaceHistoryConfig = this._codeEditor.getOption(EditorOption.find).replaceHistory;
990
this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, {
991
width: FIND_INPUT_AREA_WIDTH,
992
label: NLS_FIND_INPUT_LABEL,
993
placeholder: NLS_FIND_INPUT_PLACEHOLDER,
994
appendCaseSensitiveLabel: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
995
appendWholeWordsLabel: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
996
appendRegexLabel: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
997
validation: (value: string): InputBoxMessage | null => {
998
if (value.length === 0 || !this._findInput.getRegex()) {
999
return null;
1000
}
1001
try {
1002
// use `g` and `u` which are also used by the TextModel search
1003
new RegExp(value, 'gu');
1004
return null;
1005
} catch (e) {
1006
return { content: e.message };
1007
}
1008
},
1009
flexibleHeight,
1010
flexibleWidth,
1011
flexibleMaxHeight: 118,
1012
showCommonFindToggles: true,
1013
showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService),
1014
inputBoxStyles: defaultInputBoxStyles,
1015
toggleStyles: defaultToggleStyles,
1016
history: findSearchHistoryConfig === 'workspace' ? this._findWidgetSearchHistory : new Set([]),
1017
}, this._contextKeyService));
1018
this._findInput.setRegex(!!this._state.isRegex);
1019
this._findInput.setCaseSensitive(!!this._state.matchCase);
1020
this._findInput.setWholeWords(!!this._state.wholeWord);
1021
this._register(this._findInput.onKeyDown((e) => {
1022
if (e.equals(KeyCode.Enter) && !this._codeEditor.getOption(EditorOption.find).findOnType) {
1023
this._state.change({ searchString: this._findInput.getValue() }, true);
1024
}
1025
this._onFindInputKeyDown(e);
1026
}));
1027
this._register(this._findInput.inputBox.onDidChange(() => {
1028
if (this._ignoreChangeEvent || !this._codeEditor.getOption(EditorOption.find).findOnType) {
1029
return;
1030
}
1031
this._state.change({ searchString: this._findInput.getValue() }, true);
1032
}));
1033
this._register(this._findInput.onDidOptionChange(() => {
1034
this._state.change({
1035
isRegex: this._findInput.getRegex(),
1036
wholeWord: this._findInput.getWholeWords(),
1037
matchCase: this._findInput.getCaseSensitive()
1038
}, true);
1039
}));
1040
this._register(this._findInput.onCaseSensitiveKeyDown((e) => {
1041
if (e.equals(KeyMod.Shift | KeyCode.Tab)) {
1042
if (this._isReplaceVisible) {
1043
this._replaceInput.focus();
1044
e.preventDefault();
1045
}
1046
}
1047
}));
1048
this._register(this._findInput.onRegexKeyDown((e) => {
1049
if (e.equals(KeyCode.Tab)) {
1050
if (this._isReplaceVisible) {
1051
this._replaceInput.focusOnPreserve();
1052
e.preventDefault();
1053
}
1054
}
1055
}));
1056
this._register(this._findInput.inputBox.onDidHeightChange((e) => {
1057
if (this._tryUpdateHeight()) {
1058
this._showViewZone();
1059
}
1060
}));
1061
if (platform.isLinux) {
1062
this._register(this._findInput.onMouseDown((e) => this._onFindInputMouseDown(e)));
1063
}
1064
1065
this._matchesCount = document.createElement('div');
1066
this._matchesCount.className = 'matchesCount';
1067
this._updateMatchesCount();
1068
1069
const hoverLifecycleOptions: IHoverLifecycleOptions = { groupId: 'find-widget' };
1070
1071
// Previous button
1072
this._prevBtn = this._register(new SimpleButton({
1073
label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.PreviousMatchFindAction),
1074
icon: findPreviousMatchIcon,
1075
hoverLifecycleOptions,
1076
onTrigger: () => {
1077
assertReturnsDefined(this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction)).run().then(undefined, onUnexpectedError);
1078
}
1079
}, this._hoverService));
1080
1081
// Next button
1082
this._nextBtn = this._register(new SimpleButton({
1083
label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.NextMatchFindAction),
1084
icon: findNextMatchIcon,
1085
hoverLifecycleOptions,
1086
onTrigger: () => {
1087
assertReturnsDefined(this._codeEditor.getAction(FIND_IDS.NextMatchFindAction)).run().then(undefined, onUnexpectedError);
1088
}
1089
}, this._hoverService));
1090
1091
const findPart = document.createElement('div');
1092
findPart.className = 'find-part';
1093
findPart.appendChild(this._findInput.domNode);
1094
const actionsContainer = document.createElement('div');
1095
actionsContainer.className = 'find-actions';
1096
findPart.appendChild(actionsContainer);
1097
actionsContainer.appendChild(this._matchesCount);
1098
actionsContainer.appendChild(this._prevBtn.domNode);
1099
actionsContainer.appendChild(this._nextBtn.domNode);
1100
1101
// Toggle selection button
1102
this._toggleSelectionFind = this._register(new Toggle({
1103
icon: findSelectionIcon,
1104
title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand),
1105
isChecked: false,
1106
hoverLifecycleOptions,
1107
inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground),
1108
inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder),
1109
inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground),
1110
}));
1111
1112
this._register(this._toggleSelectionFind.onChange(() => {
1113
if (this._toggleSelectionFind.checked) {
1114
if (this._codeEditor.hasModel()) {
1115
let selections = this._codeEditor.getSelections();
1116
selections = selections.map(selection => {
1117
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
1118
selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1));
1119
}
1120
if (!selection.isEmpty()) {
1121
return selection;
1122
}
1123
return null;
1124
}).filter((element): element is Selection => !!element);
1125
1126
if (selections.length) {
1127
this._state.change({ searchScope: selections as Range[] }, true);
1128
}
1129
}
1130
} else {
1131
this._state.change({ searchScope: null }, true);
1132
}
1133
}));
1134
1135
actionsContainer.appendChild(this._toggleSelectionFind.domNode);
1136
1137
// Close button
1138
this._closeBtn = this._register(new SimpleButton({
1139
label: NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.CloseFindWidgetCommand),
1140
icon: widgetClose,
1141
hoverLifecycleOptions,
1142
onTrigger: () => {
1143
this._state.change({ isRevealed: false, searchScope: null }, false);
1144
},
1145
onKeyDown: (e) => {
1146
if (e.equals(KeyCode.Tab)) {
1147
if (this._isReplaceVisible) {
1148
if (this._replaceBtn.isEnabled()) {
1149
this._replaceBtn.focus();
1150
} else {
1151
this._codeEditor.focus();
1152
}
1153
e.preventDefault();
1154
}
1155
}
1156
}
1157
}, this._hoverService));
1158
1159
// Replace input
1160
this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, {
1161
label: NLS_REPLACE_INPUT_LABEL,
1162
placeholder: NLS_REPLACE_INPUT_PLACEHOLDER,
1163
appendPreserveCaseLabel: this._keybindingLabelFor(FIND_IDS.TogglePreserveCaseCommand),
1164
history: replaceHistoryConfig === 'workspace' ? this._replaceWidgetHistory : new Set([]),
1165
flexibleHeight,
1166
flexibleWidth,
1167
flexibleMaxHeight: 118,
1168
showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService),
1169
inputBoxStyles: defaultInputBoxStyles,
1170
toggleStyles: defaultToggleStyles,
1171
hoverLifecycleOptions,
1172
}, this._contextKeyService, true));
1173
this._replaceInput.setPreserveCase(!!this._state.preserveCase);
1174
this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e)));
1175
this._register(this._replaceInput.inputBox.onDidChange(() => {
1176
this._state.change({ replaceString: this._replaceInput.inputBox.value }, false);
1177
}));
1178
this._register(this._replaceInput.inputBox.onDidHeightChange((e) => {
1179
if (this._isReplaceVisible && this._tryUpdateHeight()) {
1180
this._showViewZone();
1181
}
1182
}));
1183
this._register(this._replaceInput.onDidOptionChange(() => {
1184
this._state.change({
1185
preserveCase: this._replaceInput.getPreserveCase()
1186
}, true);
1187
}));
1188
this._register(this._replaceInput.onPreserveCaseKeyDown((e) => {
1189
if (e.equals(KeyCode.Tab)) {
1190
if (this._prevBtn.isEnabled()) {
1191
this._prevBtn.focus();
1192
} else if (this._nextBtn.isEnabled()) {
1193
this._nextBtn.focus();
1194
} else if (this._toggleSelectionFind.enabled) {
1195
this._toggleSelectionFind.focus();
1196
} else if (this._closeBtn.isEnabled()) {
1197
this._closeBtn.focus();
1198
}
1199
1200
e.preventDefault();
1201
}
1202
}));
1203
1204
// Replace one button
1205
this._replaceBtn = this._register(new SimpleButton({
1206
label: NLS_REPLACE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceOneAction),
1207
icon: findReplaceIcon,
1208
hoverLifecycleOptions,
1209
onTrigger: () => {
1210
this._controller.replace();
1211
},
1212
onKeyDown: (e) => {
1213
if (e.equals(KeyMod.Shift | KeyCode.Tab)) {
1214
this._closeBtn.focus();
1215
e.preventDefault();
1216
}
1217
}
1218
}, this._hoverService));
1219
1220
// Replace all button
1221
this._replaceAllBtn = this._register(new SimpleButton({
1222
label: NLS_REPLACE_ALL_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceAllAction),
1223
icon: findReplaceAllIcon,
1224
hoverLifecycleOptions,
1225
onTrigger: () => {
1226
this._controller.replaceAll();
1227
}
1228
}, this._hoverService));
1229
1230
const replacePart = document.createElement('div');
1231
replacePart.className = 'replace-part';
1232
replacePart.appendChild(this._replaceInput.domNode);
1233
1234
const replaceActionsContainer = document.createElement('div');
1235
replaceActionsContainer.className = 'replace-actions';
1236
replacePart.appendChild(replaceActionsContainer);
1237
1238
replaceActionsContainer.appendChild(this._replaceBtn.domNode);
1239
replaceActionsContainer.appendChild(this._replaceAllBtn.domNode);
1240
1241
// Toggle replace button
1242
this._toggleReplaceBtn = this._register(new SimpleButton({
1243
label: NLS_TOGGLE_REPLACE_MODE_BTN_LABEL,
1244
className: 'codicon toggle left',
1245
onTrigger: () => {
1246
this._state.change({ isReplaceRevealed: !this._isReplaceVisible }, false);
1247
if (this._isReplaceVisible) {
1248
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
1249
this._replaceInput.inputBox.layout();
1250
}
1251
this._showViewZone();
1252
}
1253
}, this._hoverService));
1254
this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);
1255
1256
// Widget
1257
this._domNode = document.createElement('div');
1258
this._domNode.className = 'editor-widget find-widget';
1259
this._domNode.setAttribute('aria-hidden', 'true');
1260
this._domNode.ariaLabel = NLS_FIND_DIALOG_LABEL;
1261
this._domNode.role = 'dialog';
1262
1263
// We need to set this explicitly, otherwise on IE11, the width inheritence of flex doesn't work.
1264
this._domNode.style.width = `${FIND_WIDGET_INITIAL_WIDTH}px`;
1265
1266
this._domNode.appendChild(this._toggleReplaceBtn.domNode);
1267
this._domNode.appendChild(findPart);
1268
this._domNode.appendChild(this._closeBtn.domNode);
1269
this._domNode.appendChild(replacePart);
1270
1271
this._resizeSash = this._register(new Sash(this._domNode, this, { orientation: Orientation.VERTICAL, size: 2 }));
1272
this._resized = false;
1273
let originalWidth = FIND_WIDGET_INITIAL_WIDTH;
1274
1275
this._register(this._resizeSash.onDidStart(() => {
1276
originalWidth = dom.getTotalWidth(this._domNode);
1277
}));
1278
1279
this._register(this._resizeSash.onDidChange((evt: ISashEvent) => {
1280
this._resized = true;
1281
const width = originalWidth + evt.startX - evt.currentX;
1282
1283
if (width < FIND_WIDGET_INITIAL_WIDTH) {
1284
// narrow down the find widget should be handled by CSS.
1285
return;
1286
}
1287
1288
const maxWidth = parseFloat(dom.getComputedStyle(this._domNode).maxWidth) || 0;
1289
if (width > maxWidth) {
1290
return;
1291
}
1292
this._domNode.style.width = `${width}px`;
1293
if (this._isReplaceVisible) {
1294
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
1295
}
1296
1297
this._findInput.inputBox.layout();
1298
this._tryUpdateHeight();
1299
}));
1300
1301
this._register(this._resizeSash.onDidReset(() => {
1302
// users double click on the sash
1303
const currentWidth = dom.getTotalWidth(this._domNode);
1304
1305
if (currentWidth < FIND_WIDGET_INITIAL_WIDTH) {
1306
// The editor is narrow and the width of the find widget is controlled fully by CSS.
1307
return;
1308
}
1309
1310
let width = FIND_WIDGET_INITIAL_WIDTH;
1311
1312
if (!this._resized || currentWidth === FIND_WIDGET_INITIAL_WIDTH) {
1313
// 1. never resized before, double click should maximizes it
1314
// 2. users resized it already but its width is the same as default
1315
const layoutInfo = this._codeEditor.getLayoutInfo();
1316
width = layoutInfo.width - 28 - layoutInfo.minimap.minimapWidth - 15;
1317
this._resized = true;
1318
} else {
1319
/**
1320
* no op, the find widget should be shrinked to its default size.
1321
*/
1322
}
1323
1324
1325
this._domNode.style.width = `${width}px`;
1326
if (this._isReplaceVisible) {
1327
this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode);
1328
}
1329
1330
this._findInput.inputBox.layout();
1331
}));
1332
}
1333
1334
private updateAccessibilitySupport(): void {
1335
const value = this._codeEditor.getOption(EditorOption.accessibilitySupport);
1336
this._findInput.setFocusInputOnOptionClick(value !== AccessibilitySupport.Enabled);
1337
this._updateFindInputAriaLabel();
1338
}
1339
1340
private _updateFindInputAriaLabel(): void {
1341
let findLabel = NLS_FIND_INPUT_LABEL;
1342
let replaceLabel = NLS_REPLACE_INPUT_LABEL;
1343
if (!this._accessibilityHelpHintAnnounced && this._configurationService.getValue('accessibility.verbosity.find') && this._accessibilityService.isScreenReaderOptimized()) {
1344
const accessibilityHelpKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibilityHelp')?.getAriaLabel();
1345
if (accessibilityHelpKeybinding) {
1346
const hint = nls.localize('accessibilityHelpHintInLabel', "Press {0} for accessibility help", accessibilityHelpKeybinding);
1347
findLabel = nls.localize('findInputAriaLabelWithHint', "{0}, {1}", findLabel, hint);
1348
replaceLabel = nls.localize('replaceInputAriaLabelWithHint', "{0}, {1}", replaceLabel, hint);
1349
}
1350
this._accessibilityHelpHintAnnounced = true;
1351
// Schedule reset to plain labels after initial announcement
1352
this._labelResetTimeout?.dispose();
1353
this._labelResetTimeout = disposableTimeout(() => {
1354
if (this._isVisible) {
1355
this._findInput.inputBox.setAriaLabel(NLS_FIND_INPUT_LABEL);
1356
this._replaceInput.inputBox.setAriaLabel(NLS_REPLACE_INPUT_LABEL);
1357
}
1358
}, 1000);
1359
}
1360
this._findInput.inputBox.setAriaLabel(findLabel);
1361
this._replaceInput.inputBox.setAriaLabel(replaceLabel);
1362
}
1363
1364
getViewState() {
1365
let widgetViewZoneVisible = false;
1366
if (this._viewZone && this._viewZoneId) {
1367
widgetViewZoneVisible = this._viewZone.heightInPx > this._codeEditor.getScrollTop();
1368
}
1369
1370
return {
1371
widgetViewZoneVisible,
1372
scrollTop: this._codeEditor.getScrollTop()
1373
};
1374
}
1375
1376
setViewState(state?: { widgetViewZoneVisible: boolean; scrollTop: number }) {
1377
if (!state) {
1378
return;
1379
}
1380
1381
if (state.widgetViewZoneVisible) {
1382
// we should add the view zone
1383
this._layoutViewZone(state.scrollTop);
1384
}
1385
}
1386
}
1387
1388
export interface ISimpleButtonOpts {
1389
readonly label: string;
1390
readonly className?: string;
1391
readonly icon?: ThemeIcon;
1392
readonly hoverLifecycleOptions?: IHoverLifecycleOptions;
1393
readonly onTrigger: () => void;
1394
readonly onKeyDown?: (e: IKeyboardEvent) => void;
1395
}
1396
1397
export class SimpleButton extends Widget {
1398
1399
private readonly _opts: ISimpleButtonOpts;
1400
private readonly _domNode: HTMLElement;
1401
1402
constructor(
1403
opts: ISimpleButtonOpts,
1404
hoverService: IHoverService
1405
) {
1406
super();
1407
this._opts = opts;
1408
1409
let className = 'button';
1410
if (this._opts.className) {
1411
className = className + ' ' + this._opts.className;
1412
}
1413
if (this._opts.icon) {
1414
className = className + ' ' + ThemeIcon.asClassName(this._opts.icon);
1415
}
1416
1417
this._domNode = document.createElement('div');
1418
this._domNode.tabIndex = 0;
1419
this._domNode.className = className;
1420
this._domNode.setAttribute('role', 'button');
1421
this._domNode.setAttribute('aria-label', this._opts.label);
1422
this._register(hoverService.setupDelayedHover(this._domNode, {
1423
content: this._opts.label,
1424
style: HoverStyle.Pointer,
1425
}, opts.hoverLifecycleOptions));
1426
1427
this.onclick(this._domNode, (e) => {
1428
this._opts.onTrigger();
1429
e.preventDefault();
1430
});
1431
1432
this.onkeydown(this._domNode, (e) => {
1433
if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) {
1434
this._opts.onTrigger();
1435
e.preventDefault();
1436
return;
1437
}
1438
this._opts.onKeyDown?.(e);
1439
});
1440
}
1441
1442
public get domNode(): HTMLElement {
1443
return this._domNode;
1444
}
1445
1446
public isEnabled(): boolean {
1447
return (this._domNode.tabIndex >= 0);
1448
}
1449
1450
public focus(): void {
1451
this._domNode.focus();
1452
}
1453
1454
public setEnabled(enabled: boolean): void {
1455
this._domNode.classList.toggle('disabled', !enabled);
1456
this._domNode.setAttribute('aria-disabled', String(!enabled));
1457
this._domNode.tabIndex = enabled ? 0 : -1;
1458
}
1459
1460
public setExpanded(expanded: boolean): void {
1461
this._domNode.setAttribute('aria-expanded', String(!!expanded));
1462
if (expanded) {
1463
this._domNode.classList.remove(...ThemeIcon.asClassNameArray(findCollapsedIcon));
1464
this._domNode.classList.add(...ThemeIcon.asClassNameArray(findExpandedIcon));
1465
} else {
1466
this._domNode.classList.remove(...ThemeIcon.asClassNameArray(findExpandedIcon));
1467
this._domNode.classList.add(...ThemeIcon.asClassNameArray(findCollapsedIcon));
1468
}
1469
}
1470
}
1471
1472
// theming
1473
1474
registerThemingParticipant((theme, collector) => {
1475
const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder);
1476
if (findMatchHighlightBorder) {
1477
collector.addRule(`.monaco-editor .findMatch { border: 1px ${isHighContrast(theme.type) ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`);
1478
}
1479
1480
const findRangeHighlightBorder = theme.getColor(editorFindRangeHighlightBorder);
1481
if (findRangeHighlightBorder) {
1482
collector.addRule(`.monaco-editor .findScope { border: 1px ${isHighContrast(theme.type) ? 'dashed' : 'solid'} ${findRangeHighlightBorder}; }`);
1483
}
1484
1485
const hcBorder = theme.getColor(contrastBorder);
1486
if (hcBorder) {
1487
collector.addRule(`.monaco-editor .find-widget { border: 1px solid ${hcBorder}; }`);
1488
}
1489
const findMatchForeground = theme.getColor(editorFindMatchForeground);
1490
if (findMatchForeground) {
1491
collector.addRule(`.monaco-editor .findMatchInline { color: ${findMatchForeground}; }`);
1492
}
1493
const findMatchHighlightForeground = theme.getColor(editorFindMatchHighlightForeground);
1494
if (findMatchHighlightForeground) {
1495
collector.addRule(`.monaco-editor .currentFindMatchInline { color: ${findMatchHighlightForeground}; }`);
1496
}
1497
});
1498
1499