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