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