Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/find/browser/findController.ts
5252 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 { Delayer } from '../../../../base/common/async.js';
7
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
8
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
9
import * as strings from '../../../../base/common/strings.js';
10
import { ICodeEditor } from '../../../browser/editorBrowser.js';
11
import { EditorAction, EditorCommand, EditorContributionInstantiation, MultiEditorAction, registerEditorAction, registerEditorCommand, registerEditorContribution, registerMultiEditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js';
12
import { EditorOption } from '../../../common/config/editorOptions.js';
13
import { overviewRulerRangeHighlight } from '../../../common/core/editorColorRegistry.js';
14
import { IRange } from '../../../common/core/range.js';
15
import { IEditorContribution } from '../../../common/editorCommon.js';
16
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
17
import { OverviewRulerLane } from '../../../common/model.js';
18
import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, CONTEXT_REPLACE_INPUT_FOCUSED, FindModelBoundToEditorModel, FIND_IDS, ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding } from './findModel.js';
19
import { FindOptionsWidget } from './findOptionsWidget.js';
20
import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from './findState.js';
21
import { FindWidget, IFindController } from './findWidget.js';
22
import * as nls from '../../../../nls.js';
23
import { MenuId } from '../../../../platform/actions/common/actions.js';
24
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
25
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
27
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
28
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
29
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
30
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
31
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
32
import { themeColorFromId } from '../../../../platform/theme/common/themeService.js';
33
import { Selection } from '../../../common/core/selection.js';
34
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
35
import { FindWidgetSearchHistory } from './findWidgetSearchHistory.js';
36
import { ReplaceWidgetHistory } from './replaceWidgetHistory.js';
37
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
38
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
39
40
const SEARCH_STRING_MAX_LENGTH = 524288;
41
42
export function getSelectionSearchString(editor: ICodeEditor, seedSearchStringFromSelection: 'single' | 'multiple' = 'single', seedSearchStringFromNonEmptySelection: boolean = false): string | null {
43
if (!editor.hasModel()) {
44
return null;
45
}
46
47
const selection = editor.getSelection();
48
// if selection spans multiple lines, default search string to empty
49
50
if ((seedSearchStringFromSelection === 'single' && selection.startLineNumber === selection.endLineNumber)
51
|| seedSearchStringFromSelection === 'multiple') {
52
if (selection.isEmpty()) {
53
const wordAtPosition = editor.getConfiguredWordAtPosition(selection.getStartPosition());
54
if (wordAtPosition && (false === seedSearchStringFromNonEmptySelection)) {
55
return wordAtPosition.word;
56
}
57
} else {
58
if (editor.getModel().getValueLengthInRange(selection) < SEARCH_STRING_MAX_LENGTH) {
59
return editor.getModel().getValueInRange(selection);
60
}
61
}
62
}
63
64
return null;
65
}
66
67
export const enum FindStartFocusAction {
68
NoFocusChange,
69
FocusFindInput,
70
FocusReplaceInput
71
}
72
73
export interface IFindStartOptions {
74
forceRevealReplace: boolean;
75
seedSearchStringFromSelection: 'none' | 'single' | 'multiple';
76
seedSearchStringFromNonEmptySelection: boolean;
77
seedSearchStringFromGlobalClipboard: boolean;
78
shouldFocus: FindStartFocusAction;
79
shouldAnimate: boolean;
80
updateSearchScope: boolean;
81
loop: boolean;
82
}
83
84
export interface IFindStartArguments {
85
searchString?: string;
86
replaceString?: string;
87
isRegex?: boolean;
88
matchWholeWord?: boolean;
89
isCaseSensitive?: boolean;
90
preserveCase?: boolean;
91
findInSelection?: boolean;
92
}
93
94
export class CommonFindController extends Disposable implements IEditorContribution {
95
96
public static readonly ID = 'editor.contrib.findController';
97
98
protected _editor: ICodeEditor;
99
private readonly _findWidgetVisible: IContextKey<boolean>;
100
protected _state: FindReplaceState;
101
protected _updateHistoryDelayer: Delayer<void>;
102
private _model: FindModelBoundToEditorModel | null;
103
protected readonly _storageService: IStorageService;
104
private readonly _clipboardService: IClipboardService;
105
protected readonly _contextKeyService: IContextKeyService;
106
protected readonly _notificationService: INotificationService;
107
protected readonly _hoverService: IHoverService;
108
109
get editor() {
110
return this._editor;
111
}
112
113
public static get(editor: ICodeEditor): CommonFindController | null {
114
return editor.getContribution<CommonFindController>(CommonFindController.ID);
115
}
116
117
constructor(
118
editor: ICodeEditor,
119
@IContextKeyService contextKeyService: IContextKeyService,
120
@IStorageService storageService: IStorageService,
121
@IClipboardService clipboardService: IClipboardService,
122
@INotificationService notificationService: INotificationService,
123
@IHoverService hoverService: IHoverService
124
) {
125
super();
126
this._editor = editor;
127
this._findWidgetVisible = CONTEXT_FIND_WIDGET_VISIBLE.bindTo(contextKeyService);
128
this._contextKeyService = contextKeyService;
129
this._storageService = storageService;
130
this._clipboardService = clipboardService;
131
this._notificationService = notificationService;
132
this._hoverService = hoverService;
133
134
this._updateHistoryDelayer = this._register(new Delayer<void>(500));
135
this._state = this._register(new FindReplaceState());
136
this.loadQueryState();
137
this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
138
139
this._model = null;
140
141
this._register(this._editor.onDidChangeModel(() => {
142
const shouldRestartFind = (this._editor.getModel() && this._state.isRevealed);
143
144
this.disposeModel();
145
146
this._state.change({
147
searchScope: null,
148
matchCase: this._storageService.getBoolean('editor.matchCase', StorageScope.WORKSPACE, false),
149
wholeWord: this._storageService.getBoolean('editor.wholeWord', StorageScope.WORKSPACE, false),
150
isRegex: this._storageService.getBoolean('editor.isRegex', StorageScope.WORKSPACE, false),
151
preserveCase: this._storageService.getBoolean('editor.preserveCase', StorageScope.WORKSPACE, false)
152
}, false);
153
154
if (shouldRestartFind) {
155
this._start({
156
forceRevealReplace: false,
157
seedSearchStringFromSelection: 'none',
158
seedSearchStringFromNonEmptySelection: false,
159
seedSearchStringFromGlobalClipboard: false,
160
shouldFocus: FindStartFocusAction.NoFocusChange,
161
shouldAnimate: false,
162
updateSearchScope: false,
163
loop: this._editor.getOption(EditorOption.find).loop
164
});
165
}
166
}));
167
}
168
169
public override dispose(): void {
170
this.disposeModel();
171
super.dispose();
172
}
173
174
private disposeModel(): void {
175
if (this._model) {
176
this._model.dispose();
177
this._model = null;
178
}
179
}
180
181
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
182
this.saveQueryState(e);
183
184
if (e.isRevealed) {
185
if (this._state.isRevealed) {
186
this._findWidgetVisible.set(true);
187
} else {
188
this._findWidgetVisible.reset();
189
this.disposeModel();
190
}
191
}
192
if (e.searchString) {
193
this.setGlobalBufferTerm(this._state.searchString);
194
}
195
}
196
197
private saveQueryState(e: FindReplaceStateChangedEvent) {
198
if (e.isRegex) {
199
this._storageService.store('editor.isRegex', this._state.actualIsRegex, StorageScope.WORKSPACE, StorageTarget.MACHINE);
200
}
201
if (e.wholeWord) {
202
this._storageService.store('editor.wholeWord', this._state.actualWholeWord, StorageScope.WORKSPACE, StorageTarget.MACHINE);
203
}
204
if (e.matchCase) {
205
this._storageService.store('editor.matchCase', this._state.actualMatchCase, StorageScope.WORKSPACE, StorageTarget.MACHINE);
206
}
207
if (e.preserveCase) {
208
this._storageService.store('editor.preserveCase', this._state.actualPreserveCase, StorageScope.WORKSPACE, StorageTarget.MACHINE);
209
}
210
}
211
212
private loadQueryState() {
213
this._state.change({
214
matchCase: this._storageService.getBoolean('editor.matchCase', StorageScope.WORKSPACE, this._state.matchCase),
215
wholeWord: this._storageService.getBoolean('editor.wholeWord', StorageScope.WORKSPACE, this._state.wholeWord),
216
isRegex: this._storageService.getBoolean('editor.isRegex', StorageScope.WORKSPACE, this._state.isRegex),
217
preserveCase: this._storageService.getBoolean('editor.preserveCase', StorageScope.WORKSPACE, this._state.preserveCase)
218
}, false);
219
}
220
221
public isFindInputFocused(): boolean {
222
return !!CONTEXT_FIND_INPUT_FOCUSED.getValue(this._contextKeyService);
223
}
224
225
/**
226
* Returns whether the Replace input was the last focused input in the find widget.
227
* Returns false by default; overridden in FindController.
228
*/
229
public wasReplaceInputLastFocused(): boolean {
230
return false;
231
}
232
233
/**
234
* Focuses the last focused element in the find widget.
235
* Implemented by FindController; base implementation does nothing.
236
*/
237
public focusLastElement(): void {
238
// Base implementation - overridden in FindController
239
}
240
241
public getState(): FindReplaceState {
242
return this._state;
243
}
244
245
public closeFindWidget(): void {
246
this._state.change({
247
isRevealed: false,
248
searchScope: null
249
}, false);
250
this._editor.focus();
251
}
252
253
public toggleCaseSensitive(): void {
254
this._state.change({ matchCase: !this._state.matchCase }, false);
255
if (!this._state.isRevealed) {
256
this.highlightFindOptions();
257
}
258
}
259
260
public toggleWholeWords(): void {
261
this._state.change({ wholeWord: !this._state.wholeWord }, false);
262
if (!this._state.isRevealed) {
263
this.highlightFindOptions();
264
}
265
}
266
267
public toggleRegex(): void {
268
this._state.change({ isRegex: !this._state.isRegex }, false);
269
if (!this._state.isRevealed) {
270
this.highlightFindOptions();
271
}
272
}
273
274
public togglePreserveCase(): void {
275
this._state.change({ preserveCase: !this._state.preserveCase }, false);
276
if (!this._state.isRevealed) {
277
this.highlightFindOptions();
278
}
279
}
280
281
public toggleSearchScope(): void {
282
if (this._state.searchScope) {
283
this._state.change({ searchScope: null }, true);
284
} else {
285
if (this._editor.hasModel()) {
286
let selections = this._editor.getSelections();
287
selections = selections.map(selection => {
288
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
289
selection = selection.setEndPosition(
290
selection.endLineNumber - 1,
291
this._editor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
292
);
293
}
294
if (!selection.isEmpty()) {
295
return selection;
296
}
297
return null;
298
}).filter((element): element is Selection => !!element);
299
300
if (selections.length) {
301
this._state.change({ searchScope: selections }, true);
302
}
303
}
304
}
305
}
306
307
public setSearchString(searchString: string): void {
308
if (this._state.isRegex) {
309
searchString = strings.escapeRegExpCharacters(searchString);
310
}
311
this._state.change({ searchString: searchString }, false);
312
}
313
314
public highlightFindOptions(ignoreWhenVisible: boolean = false): void {
315
// overwritten in subclass
316
}
317
318
protected async _start(opts: IFindStartOptions, newState?: INewFindReplaceState): Promise<void> {
319
this.disposeModel();
320
321
if (!this._editor.hasModel()) {
322
// cannot do anything with an editor that doesn't have a model...
323
return;
324
}
325
326
const stateChanges: INewFindReplaceState = {
327
...newState,
328
isRevealed: true
329
};
330
331
if (opts.seedSearchStringFromSelection === 'single') {
332
const selectionSearchString = getSelectionSearchString(this._editor, opts.seedSearchStringFromSelection, opts.seedSearchStringFromNonEmptySelection);
333
if (selectionSearchString) {
334
if (this._state.isRegex) {
335
stateChanges.searchString = strings.escapeRegExpCharacters(selectionSearchString);
336
} else {
337
stateChanges.searchString = selectionSearchString;
338
}
339
}
340
} else if (opts.seedSearchStringFromSelection === 'multiple' && !opts.updateSearchScope) {
341
const selectionSearchString = getSelectionSearchString(this._editor, opts.seedSearchStringFromSelection);
342
if (selectionSearchString) {
343
stateChanges.searchString = selectionSearchString;
344
}
345
}
346
347
if (!stateChanges.searchString && opts.seedSearchStringFromGlobalClipboard) {
348
const selectionSearchString = await this.getGlobalBufferTerm();
349
350
if (!this._editor.hasModel()) {
351
// the editor has lost its model in the meantime
352
return;
353
}
354
355
if (selectionSearchString) {
356
stateChanges.searchString = selectionSearchString;
357
}
358
}
359
360
// Overwrite isReplaceRevealed
361
if (opts.forceRevealReplace || stateChanges.isReplaceRevealed) {
362
stateChanges.isReplaceRevealed = true;
363
} else if (!this._findWidgetVisible.get()) {
364
stateChanges.isReplaceRevealed = false;
365
}
366
367
if (opts.updateSearchScope) {
368
const currentSelections = this._editor.getSelections();
369
if (currentSelections.some(selection => !selection.isEmpty())) {
370
stateChanges.searchScope = currentSelections;
371
}
372
}
373
374
stateChanges.loop = opts.loop;
375
376
this._state.change(stateChanges, false);
377
378
if (!this._model) {
379
this._model = new FindModelBoundToEditorModel(this._editor, this._state);
380
}
381
}
382
383
public start(opts: IFindStartOptions, newState?: INewFindReplaceState): Promise<void> {
384
return this._start(opts, newState);
385
}
386
387
public moveToNextMatch(): boolean {
388
if (this._model) {
389
this._model.moveToNextMatch();
390
return true;
391
}
392
return false;
393
}
394
395
public moveToPrevMatch(): boolean {
396
if (this._model) {
397
this._model.moveToPrevMatch();
398
return true;
399
}
400
return false;
401
}
402
403
public goToMatch(index: number): boolean {
404
if (this._model) {
405
this._model.moveToMatch(index);
406
return true;
407
}
408
return false;
409
}
410
411
public replace(): boolean {
412
if (this._model) {
413
this._model.replace();
414
return true;
415
}
416
return false;
417
}
418
419
public replaceAll(): boolean {
420
if (this._model) {
421
if (this._editor.getModel()?.isTooLargeForHeapOperation()) {
422
this._notificationService.warn(nls.localize('too.large.for.replaceall', "The file is too large to perform a replace all operation."));
423
return false;
424
}
425
this._model.replaceAll();
426
return true;
427
}
428
return false;
429
}
430
431
public selectAllMatches(): boolean {
432
if (this._model) {
433
this._model.selectAllMatches();
434
this._editor.focus();
435
return true;
436
}
437
return false;
438
}
439
440
public async getGlobalBufferTerm(): Promise<string> {
441
if (this._editor.getOption(EditorOption.find).globalFindClipboard
442
&& this._editor.hasModel()
443
&& !this._editor.getModel().isTooLargeForSyncing()
444
) {
445
return this._clipboardService.readFindText();
446
}
447
return '';
448
}
449
450
public setGlobalBufferTerm(text: string): void {
451
if (this._editor.getOption(EditorOption.find).globalFindClipboard
452
&& this._editor.hasModel()
453
&& !this._editor.getModel().isTooLargeForSyncing()
454
) {
455
// intentionally not awaited
456
this._clipboardService.writeFindText(text);
457
}
458
}
459
}
460
461
export class FindController extends CommonFindController implements IFindController {
462
463
private _widget: FindWidget | null;
464
private _findOptionsWidget: FindOptionsWidget | null;
465
private _findWidgetSearchHistory: FindWidgetSearchHistory;
466
private _replaceWidgetHistory: ReplaceWidgetHistory;
467
468
constructor(
469
editor: ICodeEditor,
470
@IContextViewService private readonly _contextViewService: IContextViewService,
471
@IContextKeyService _contextKeyService: IContextKeyService,
472
@IKeybindingService private readonly _keybindingService: IKeybindingService,
473
@INotificationService notificationService: INotificationService,
474
@IStorageService _storageService: IStorageService,
475
@IClipboardService clipboardService: IClipboardService,
476
@IHoverService hoverService: IHoverService,
477
@IConfigurationService private readonly _configurationService: IConfigurationService,
478
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
479
) {
480
super(editor, _contextKeyService, _storageService, clipboardService, notificationService, hoverService);
481
this._widget = null;
482
this._findOptionsWidget = null;
483
this._findWidgetSearchHistory = FindWidgetSearchHistory.getOrCreate(_storageService);
484
this._replaceWidgetHistory = ReplaceWidgetHistory.getOrCreate(_storageService);
485
}
486
487
protected override async _start(opts: IFindStartOptions, newState?: INewFindReplaceState): Promise<void> {
488
if (!this._widget) {
489
this._createFindWidget();
490
}
491
492
const selection = this._editor.getSelection();
493
let updateSearchScope = false;
494
495
switch (this._editor.getOption(EditorOption.find).autoFindInSelection) {
496
case 'always':
497
updateSearchScope = true;
498
break;
499
case 'never':
500
updateSearchScope = false;
501
break;
502
case 'multiline': {
503
const isSelectionMultipleLine = !!selection && selection.startLineNumber !== selection.endLineNumber;
504
updateSearchScope = isSelectionMultipleLine;
505
break;
506
}
507
default:
508
break;
509
}
510
511
opts.updateSearchScope = opts.updateSearchScope || updateSearchScope;
512
513
await super._start(opts, newState);
514
515
if (this._widget) {
516
if (opts.shouldFocus === FindStartFocusAction.FocusReplaceInput) {
517
this._widget.focusReplaceInput();
518
} else if (opts.shouldFocus === FindStartFocusAction.FocusFindInput) {
519
this._widget.focusFindInput();
520
}
521
}
522
}
523
524
public override highlightFindOptions(ignoreWhenVisible: boolean = false): void {
525
if (!this._widget) {
526
this._createFindWidget();
527
}
528
if (this._state.isRevealed && !ignoreWhenVisible) {
529
this._widget!.highlightFindOptions();
530
} else {
531
this._findOptionsWidget!.highlightFindOptions();
532
}
533
}
534
535
private _createFindWidget() {
536
this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._hoverService, this._findWidgetSearchHistory, this._replaceWidgetHistory, this._configurationService, this._accessibilityService));
537
this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService));
538
}
539
540
/**
541
* Returns whether the Replace input was the last focused input in the find widget.
542
*/
543
public override wasReplaceInputLastFocused(): boolean {
544
return this._widget?.lastFocusedInputWasReplace ?? false;
545
}
546
547
/**
548
* Focuses the last focused element in the find widget.
549
* This is more precise than just focusing the Find or Replace input,
550
* as it can restore focus to checkboxes, buttons, etc.
551
*/
552
public override focusLastElement(): void {
553
this._widget?.focusLastElement();
554
}
555
556
saveViewState(): any {
557
return this._widget?.getViewState();
558
}
559
560
restoreViewState(state: any): void {
561
this._widget?.setViewState(state);
562
}
563
}
564
565
export const StartFindAction = registerMultiEditorAction(new MultiEditorAction({
566
id: FIND_IDS.StartFindAction,
567
label: nls.localize2('startFindAction', "Find"),
568
precondition: ContextKeyExpr.or(EditorContextKeys.focus, ContextKeyExpr.has('editorIsOpen')),
569
kbOpts: {
570
kbExpr: null,
571
primary: KeyMod.CtrlCmd | KeyCode.KeyF,
572
weight: KeybindingWeight.EditorContrib
573
},
574
menuOpts: {
575
menuId: MenuId.MenubarEditMenu,
576
group: '3_find',
577
title: nls.localize({ key: 'miFind', comment: ['&& denotes a mnemonic'] }, "&&Find"),
578
order: 1
579
}
580
}));
581
582
StartFindAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise<void> => {
583
const controller = CommonFindController.get(editor);
584
if (!controller) {
585
return false;
586
}
587
return controller.start({
588
forceRevealReplace: false,
589
seedSearchStringFromSelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none',
590
seedSearchStringFromNonEmptySelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection',
591
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).globalFindClipboard,
592
shouldFocus: FindStartFocusAction.FocusFindInput,
593
shouldAnimate: true,
594
updateSearchScope: false,
595
loop: editor.getOption(EditorOption.find).loop
596
});
597
});
598
599
const findArgDescription = {
600
description: 'Open a new In-Editor Find Widget.',
601
args: [{
602
name: 'Open a new In-Editor Find Widget args',
603
schema: {
604
properties: {
605
searchString: { type: 'string' },
606
replaceString: { type: 'string' },
607
isRegex: { type: 'boolean' },
608
matchWholeWord: { type: 'boolean' },
609
isCaseSensitive: { type: 'boolean' },
610
preserveCase: { type: 'boolean' },
611
findInSelection: { type: 'boolean' },
612
}
613
}
614
}]
615
} as const;
616
617
export class StartFindWithArgsAction extends EditorAction {
618
619
constructor() {
620
super({
621
id: FIND_IDS.StartFindWithArgs,
622
label: nls.localize2('startFindWithArgsAction', "Find with Arguments"),
623
precondition: undefined,
624
kbOpts: {
625
kbExpr: null,
626
primary: 0,
627
weight: KeybindingWeight.EditorContrib
628
},
629
metadata: findArgDescription
630
});
631
}
632
633
public async run(accessor: ServicesAccessor, editor: ICodeEditor, args?: IFindStartArguments): Promise<void> {
634
const controller = CommonFindController.get(editor);
635
if (controller) {
636
const newState: INewFindReplaceState = args ? {
637
searchString: args.searchString,
638
replaceString: args.replaceString,
639
isReplaceRevealed: args.replaceString !== undefined,
640
isRegex: args.isRegex,
641
// isRegexOverride: args.regexOverride,
642
wholeWord: args.matchWholeWord,
643
// wholeWordOverride: args.wholeWordOverride,
644
matchCase: args.isCaseSensitive,
645
// matchCaseOverride: args.matchCaseOverride,
646
preserveCase: args.preserveCase,
647
// preserveCaseOverride: args.preserveCaseOverride,
648
} : {};
649
650
await controller.start({
651
forceRevealReplace: false,
652
seedSearchStringFromSelection: (controller.getState().searchString.length === 0) && editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none',
653
seedSearchStringFromNonEmptySelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection',
654
seedSearchStringFromGlobalClipboard: true,
655
shouldFocus: FindStartFocusAction.FocusFindInput,
656
shouldAnimate: true,
657
updateSearchScope: args?.findInSelection || false,
658
loop: editor.getOption(EditorOption.find).loop
659
}, newState);
660
661
controller.setGlobalBufferTerm(controller.getState().searchString);
662
}
663
}
664
}
665
666
export class StartFindWithSelectionAction extends EditorAction {
667
668
constructor() {
669
super({
670
id: FIND_IDS.StartFindWithSelection,
671
label: nls.localize2('startFindWithSelectionAction', "Find with Selection"),
672
precondition: undefined,
673
kbOpts: {
674
kbExpr: null,
675
primary: 0,
676
mac: {
677
primary: KeyMod.CtrlCmd | KeyCode.KeyE,
678
},
679
weight: KeybindingWeight.EditorContrib
680
}
681
});
682
}
683
684
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
685
const controller = CommonFindController.get(editor);
686
if (controller) {
687
await controller.start({
688
forceRevealReplace: false,
689
seedSearchStringFromSelection: 'multiple',
690
seedSearchStringFromNonEmptySelection: false,
691
seedSearchStringFromGlobalClipboard: false,
692
shouldFocus: FindStartFocusAction.NoFocusChange,
693
shouldAnimate: true,
694
updateSearchScope: false,
695
loop: editor.getOption(EditorOption.find).loop
696
});
697
698
controller.setGlobalBufferTerm(controller.getState().searchString);
699
}
700
}
701
}
702
export abstract class MatchFindAction extends EditorAction {
703
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
704
const controller = CommonFindController.get(editor);
705
if (controller && !this._run(controller)) {
706
await controller.start({
707
forceRevealReplace: false,
708
seedSearchStringFromSelection: (controller.getState().searchString.length === 0) && editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none',
709
seedSearchStringFromNonEmptySelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection',
710
seedSearchStringFromGlobalClipboard: true,
711
shouldFocus: FindStartFocusAction.NoFocusChange,
712
shouldAnimate: true,
713
updateSearchScope: false,
714
loop: editor.getOption(EditorOption.find).loop
715
});
716
this._run(controller);
717
}
718
}
719
720
protected abstract _run(controller: CommonFindController): boolean;
721
}
722
723
async function matchFindAction(editor: ICodeEditor, next: boolean): Promise<void> {
724
const controller = CommonFindController.get(editor);
725
if (!controller) {
726
return;
727
}
728
729
const runMatch = (): boolean => {
730
const result = next ? controller.moveToNextMatch() : controller.moveToPrevMatch();
731
if (result) {
732
controller.editor.pushUndoStop();
733
return true;
734
}
735
return false;
736
};
737
738
if (!runMatch()) {
739
await controller.start({
740
forceRevealReplace: false,
741
seedSearchStringFromSelection: (controller.getState().searchString.length === 0) && editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none',
742
seedSearchStringFromNonEmptySelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection',
743
seedSearchStringFromGlobalClipboard: true,
744
shouldFocus: FindStartFocusAction.NoFocusChange,
745
shouldAnimate: true,
746
updateSearchScope: false,
747
loop: editor.getOption(EditorOption.find).loop
748
});
749
runMatch();
750
}
751
}
752
753
export const NextMatchFindAction = registerMultiEditorAction(new MultiEditorAction({
754
id: FIND_IDS.NextMatchFindAction,
755
label: nls.localize2('findNextMatchAction', "Find Next"),
756
precondition: undefined,
757
kbOpts: [{
758
kbExpr: EditorContextKeys.focus,
759
primary: KeyCode.F3,
760
mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyG, secondary: [KeyCode.F3] },
761
weight: KeybindingWeight.EditorContrib
762
}, {
763
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED),
764
primary: KeyCode.Enter,
765
weight: KeybindingWeight.EditorContrib
766
}]
767
}));
768
769
NextMatchFindAction.addImplementation(0, async (accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise<void> => {
770
return matchFindAction(editor, true);
771
});
772
773
774
export const PreviousMatchFindAction = registerMultiEditorAction(new MultiEditorAction({
775
id: FIND_IDS.PreviousMatchFindAction,
776
label: nls.localize2('findPreviousMatchAction', "Find Previous"),
777
precondition: undefined,
778
kbOpts: [{
779
kbExpr: EditorContextKeys.focus,
780
primary: KeyMod.Shift | KeyCode.F3,
781
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, secondary: [KeyMod.Shift | KeyCode.F3] },
782
weight: KeybindingWeight.EditorContrib
783
}, {
784
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED),
785
primary: KeyMod.Shift | KeyCode.Enter,
786
weight: KeybindingWeight.EditorContrib
787
}]
788
}));
789
790
PreviousMatchFindAction.addImplementation(0, async (accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise<void> => {
791
return matchFindAction(editor, false);
792
});
793
794
export class MoveToMatchFindAction extends EditorAction {
795
796
private _highlightDecorations: string[] = [];
797
constructor() {
798
super({
799
id: FIND_IDS.GoToMatchFindAction,
800
label: nls.localize2('findMatchAction.goToMatch', "Go to Match..."),
801
precondition: CONTEXT_FIND_WIDGET_VISIBLE
802
});
803
}
804
805
public run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
806
const controller = CommonFindController.get(editor);
807
if (!controller) {
808
return;
809
}
810
811
const matchesCount = controller.getState().matchesCount;
812
if (matchesCount < 1) {
813
const notificationService = accessor.get(INotificationService);
814
notificationService.notify({
815
severity: Severity.Warning,
816
message: nls.localize('findMatchAction.noResults', "No matches. Try searching for something else.")
817
});
818
return;
819
}
820
821
const quickInputService = accessor.get(IQuickInputService);
822
const disposables = new DisposableStore();
823
const inputBox = disposables.add(quickInputService.createInputBox());
824
inputBox.placeholder = nls.localize('findMatchAction.inputPlaceHolder', "Type a number to go to a specific match (between 1 and {0})", matchesCount);
825
826
const toFindMatchIndex = (value: string): number | undefined => {
827
const index = parseInt(value);
828
if (isNaN(index)) {
829
return undefined;
830
}
831
832
const matchCount = controller.getState().matchesCount;
833
if (index > 0 && index <= matchCount) {
834
return index - 1; // zero based
835
} else if (index < 0 && index >= -matchCount) {
836
return matchCount + index;
837
}
838
839
return undefined;
840
};
841
842
const updatePickerAndEditor = (value: string) => {
843
const index = toFindMatchIndex(value);
844
if (typeof index === 'number') {
845
// valid
846
inputBox.validationMessage = undefined;
847
controller.goToMatch(index);
848
const currentMatch = controller.getState().currentMatch;
849
if (currentMatch) {
850
this.addDecorations(editor, currentMatch);
851
}
852
} else {
853
inputBox.validationMessage = nls.localize('findMatchAction.inputValidationMessage', "Please type a number between 1 and {0}", controller.getState().matchesCount);
854
this.clearDecorations(editor);
855
}
856
};
857
disposables.add(inputBox.onDidChangeValue(value => {
858
updatePickerAndEditor(value);
859
}));
860
861
disposables.add(inputBox.onDidAccept(() => {
862
const index = toFindMatchIndex(inputBox.value);
863
if (typeof index === 'number') {
864
controller.goToMatch(index);
865
inputBox.hide();
866
} else {
867
inputBox.validationMessage = nls.localize('findMatchAction.inputValidationMessage', "Please type a number between 1 and {0}", controller.getState().matchesCount);
868
}
869
}));
870
871
disposables.add(inputBox.onDidHide(() => {
872
this.clearDecorations(editor);
873
disposables.dispose();
874
}));
875
876
inputBox.show();
877
}
878
879
private clearDecorations(editor: ICodeEditor): void {
880
editor.changeDecorations(changeAccessor => {
881
this._highlightDecorations = changeAccessor.deltaDecorations(this._highlightDecorations, []);
882
});
883
}
884
885
private addDecorations(editor: ICodeEditor, range: IRange): void {
886
editor.changeDecorations(changeAccessor => {
887
this._highlightDecorations = changeAccessor.deltaDecorations(this._highlightDecorations, [
888
{
889
range,
890
options: {
891
description: 'find-match-quick-access-range-highlight',
892
className: 'rangeHighlight',
893
isWholeLine: true
894
}
895
},
896
{
897
range,
898
options: {
899
description: 'find-match-quick-access-range-highlight-overview',
900
overviewRuler: {
901
color: themeColorFromId(overviewRulerRangeHighlight),
902
position: OverviewRulerLane.Full
903
}
904
}
905
}
906
]);
907
});
908
}
909
}
910
911
export abstract class SelectionMatchFindAction extends EditorAction {
912
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
913
const controller = CommonFindController.get(editor);
914
if (!controller) {
915
return;
916
}
917
918
const selectionSearchString = getSelectionSearchString(editor, 'single', false);
919
if (selectionSearchString) {
920
controller.setSearchString(selectionSearchString);
921
}
922
if (!this._run(controller)) {
923
await controller.start({
924
forceRevealReplace: false,
925
seedSearchStringFromSelection: 'none',
926
seedSearchStringFromNonEmptySelection: false,
927
seedSearchStringFromGlobalClipboard: false,
928
shouldFocus: FindStartFocusAction.NoFocusChange,
929
shouldAnimate: true,
930
updateSearchScope: false,
931
loop: editor.getOption(EditorOption.find).loop
932
});
933
this._run(controller);
934
}
935
}
936
937
protected abstract _run(controller: CommonFindController): boolean;
938
}
939
940
export class NextSelectionMatchFindAction extends SelectionMatchFindAction {
941
942
constructor() {
943
super({
944
id: FIND_IDS.NextSelectionMatchFindAction,
945
label: nls.localize2('nextSelectionMatchFindAction', "Find Next Selection"),
946
precondition: undefined,
947
kbOpts: {
948
kbExpr: EditorContextKeys.focus,
949
primary: KeyMod.CtrlCmd | KeyCode.F3,
950
weight: KeybindingWeight.EditorContrib
951
}
952
});
953
}
954
955
protected _run(controller: CommonFindController): boolean {
956
return controller.moveToNextMatch();
957
}
958
}
959
960
export class PreviousSelectionMatchFindAction extends SelectionMatchFindAction {
961
962
constructor() {
963
super({
964
id: FIND_IDS.PreviousSelectionMatchFindAction,
965
label: nls.localize2('previousSelectionMatchFindAction', "Find Previous Selection"),
966
precondition: undefined,
967
kbOpts: {
968
kbExpr: EditorContextKeys.focus,
969
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F3,
970
weight: KeybindingWeight.EditorContrib
971
}
972
});
973
}
974
975
protected _run(controller: CommonFindController): boolean {
976
return controller.moveToPrevMatch();
977
}
978
}
979
980
export const StartFindReplaceAction = registerMultiEditorAction(new MultiEditorAction({
981
id: FIND_IDS.StartFindReplaceAction,
982
label: nls.localize2('startReplace', "Replace"),
983
precondition: ContextKeyExpr.or(EditorContextKeys.focus, ContextKeyExpr.has('editorIsOpen')),
984
kbOpts: {
985
kbExpr: null,
986
primary: KeyMod.CtrlCmd | KeyCode.KeyH,
987
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF },
988
weight: KeybindingWeight.EditorContrib
989
},
990
menuOpts: {
991
menuId: MenuId.MenubarEditMenu,
992
group: '3_find',
993
title: nls.localize({ key: 'miReplace', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
994
order: 2
995
}
996
}));
997
998
StartFindReplaceAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise<void> => {
999
if (!editor.hasModel() || editor.getOption(EditorOption.readOnly)) {
1000
return false;
1001
}
1002
const controller = CommonFindController.get(editor);
1003
if (!controller) {
1004
return false;
1005
}
1006
1007
const currentSelection = editor.getSelection();
1008
const findInputFocused = controller.isFindInputFocused();
1009
// we only seed search string from selection when the current selection is single line and not empty,
1010
// + the find input is not focused
1011
const seedSearchStringFromSelection = !currentSelection.isEmpty()
1012
&& currentSelection.startLineNumber === currentSelection.endLineNumber
1013
&& (editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never')
1014
&& !findInputFocused;
1015
/*
1016
* if the existing search string in find widget is empty and we don't seed search string from selection, it means the Find Input is still empty, so we should focus the Find Input instead of Replace Input.
1017
1018
* findInputFocused true -> seedSearchStringFromSelection false, FocusReplaceInput
1019
* findInputFocused false, seedSearchStringFromSelection true FocusReplaceInput
1020
* findInputFocused false seedSearchStringFromSelection false FocusFindInput
1021
*/
1022
const shouldFocus = (findInputFocused || seedSearchStringFromSelection) ?
1023
FindStartFocusAction.FocusReplaceInput : FindStartFocusAction.FocusFindInput;
1024
1025
return controller.start({
1026
forceRevealReplace: true,
1027
seedSearchStringFromSelection: seedSearchStringFromSelection ? 'single' : 'none',
1028
seedSearchStringFromNonEmptySelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection',
1029
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never',
1030
shouldFocus: shouldFocus,
1031
shouldAnimate: true,
1032
updateSearchScope: false,
1033
loop: editor.getOption(EditorOption.find).loop
1034
});
1035
});
1036
1037
registerEditorContribution(CommonFindController.ID, FindController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState`
1038
1039
registerEditorAction(StartFindWithArgsAction);
1040
registerEditorAction(StartFindWithSelectionAction);
1041
registerEditorAction(MoveToMatchFindAction);
1042
registerEditorAction(NextSelectionMatchFindAction);
1043
registerEditorAction(PreviousSelectionMatchFindAction);
1044
1045
const FindCommand = EditorCommand.bindToContribution<CommonFindController>(CommonFindController.get);
1046
1047
registerEditorCommand(new FindCommand({
1048
id: FIND_IDS.CloseFindWidgetCommand,
1049
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1050
handler: x => x.closeFindWidget(),
1051
kbOpts: {
1052
weight: KeybindingWeight.EditorContrib + 5,
1053
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
1054
primary: KeyCode.Escape,
1055
secondary: [KeyMod.Shift | KeyCode.Escape]
1056
}
1057
}));
1058
1059
registerEditorCommand(new FindCommand({
1060
id: FIND_IDS.ToggleCaseSensitiveCommand,
1061
precondition: undefined,
1062
handler: x => x.toggleCaseSensitive(),
1063
kbOpts: {
1064
weight: KeybindingWeight.EditorContrib + 5,
1065
kbExpr: EditorContextKeys.focus,
1066
primary: ToggleCaseSensitiveKeybinding.primary,
1067
mac: ToggleCaseSensitiveKeybinding.mac,
1068
win: ToggleCaseSensitiveKeybinding.win,
1069
linux: ToggleCaseSensitiveKeybinding.linux
1070
}
1071
}));
1072
1073
registerEditorCommand(new FindCommand({
1074
id: FIND_IDS.ToggleWholeWordCommand,
1075
precondition: undefined,
1076
handler: x => x.toggleWholeWords(),
1077
kbOpts: {
1078
weight: KeybindingWeight.EditorContrib + 5,
1079
kbExpr: EditorContextKeys.focus,
1080
primary: ToggleWholeWordKeybinding.primary,
1081
mac: ToggleWholeWordKeybinding.mac,
1082
win: ToggleWholeWordKeybinding.win,
1083
linux: ToggleWholeWordKeybinding.linux
1084
}
1085
}));
1086
1087
registerEditorCommand(new FindCommand({
1088
id: FIND_IDS.ToggleRegexCommand,
1089
precondition: undefined,
1090
handler: x => x.toggleRegex(),
1091
kbOpts: {
1092
weight: KeybindingWeight.EditorContrib + 5,
1093
kbExpr: EditorContextKeys.focus,
1094
primary: ToggleRegexKeybinding.primary,
1095
mac: ToggleRegexKeybinding.mac,
1096
win: ToggleRegexKeybinding.win,
1097
linux: ToggleRegexKeybinding.linux
1098
}
1099
}));
1100
1101
registerEditorCommand(new FindCommand({
1102
id: FIND_IDS.ToggleSearchScopeCommand,
1103
precondition: undefined,
1104
handler: x => x.toggleSearchScope(),
1105
kbOpts: {
1106
weight: KeybindingWeight.EditorContrib + 5,
1107
kbExpr: EditorContextKeys.focus,
1108
primary: ToggleSearchScopeKeybinding.primary,
1109
mac: ToggleSearchScopeKeybinding.mac,
1110
win: ToggleSearchScopeKeybinding.win,
1111
linux: ToggleSearchScopeKeybinding.linux
1112
}
1113
}));
1114
1115
registerEditorCommand(new FindCommand({
1116
id: FIND_IDS.TogglePreserveCaseCommand,
1117
precondition: undefined,
1118
handler: x => x.togglePreserveCase(),
1119
kbOpts: {
1120
weight: KeybindingWeight.EditorContrib + 5,
1121
kbExpr: EditorContextKeys.focus,
1122
primary: TogglePreserveCaseKeybinding.primary,
1123
mac: TogglePreserveCaseKeybinding.mac,
1124
win: TogglePreserveCaseKeybinding.win,
1125
linux: TogglePreserveCaseKeybinding.linux
1126
}
1127
}));
1128
1129
registerEditorCommand(new FindCommand({
1130
id: FIND_IDS.ReplaceOneAction,
1131
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1132
handler: x => x.replace(),
1133
kbOpts: {
1134
weight: KeybindingWeight.EditorContrib + 5,
1135
kbExpr: EditorContextKeys.focus,
1136
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit1
1137
}
1138
}));
1139
1140
registerEditorCommand(new FindCommand({
1141
id: FIND_IDS.ReplaceOneAction,
1142
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1143
handler: x => x.replace(),
1144
kbOpts: {
1145
weight: KeybindingWeight.EditorContrib + 5,
1146
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED, EditorContextKeys.isComposing.negate()),
1147
primary: KeyCode.Enter
1148
}
1149
}));
1150
1151
registerEditorCommand(new FindCommand({
1152
id: FIND_IDS.ReplaceAllAction,
1153
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1154
handler: x => x.replaceAll(),
1155
kbOpts: {
1156
weight: KeybindingWeight.EditorContrib + 5,
1157
kbExpr: EditorContextKeys.focus,
1158
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter
1159
}
1160
}));
1161
1162
registerEditorCommand(new FindCommand({
1163
id: FIND_IDS.ReplaceAllAction,
1164
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1165
handler: x => x.replaceAll(),
1166
kbOpts: {
1167
weight: KeybindingWeight.EditorContrib + 5,
1168
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED),
1169
primary: undefined,
1170
mac: {
1171
primary: KeyMod.CtrlCmd | KeyCode.Enter,
1172
}
1173
}
1174
}));
1175
1176
registerEditorCommand(new FindCommand({
1177
id: FIND_IDS.SelectAllMatchesAction,
1178
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
1179
handler: x => x.selectAllMatches(),
1180
kbOpts: {
1181
weight: KeybindingWeight.EditorContrib + 5,
1182
kbExpr: EditorContextKeys.focus,
1183
primary: KeyMod.Alt | KeyCode.Enter
1184
}
1185
}));
1186
1187