Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/find/browser/findModel.ts
5270 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 { findFirstIdxMonotonousOrArrLen } from '../../../../base/common/arraysFind.js';
7
import { RunOnceScheduler, TimeoutTimer } from '../../../../base/common/async.js';
8
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
9
import { DisposableStore, dispose } from '../../../../base/common/lifecycle.js';
10
import { Constants } from '../../../../base/common/uint.js';
11
import { IActiveCodeEditor } from '../../../browser/editorBrowser.js';
12
import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from '../../../common/commands/replaceCommand.js';
13
import { EditorOption } from '../../../common/config/editorOptions.js';
14
import { CursorChangeReason, ICursorPositionChangedEvent } from '../../../common/cursorEvents.js';
15
import { Position } from '../../../common/core/position.js';
16
import { Range } from '../../../common/core/range.js';
17
import { Selection } from '../../../common/core/selection.js';
18
import { ICommand, ScrollType } from '../../../common/editorCommon.js';
19
import { EndOfLinePreference, FindMatch, ITextModel } from '../../../common/model.js';
20
import { SearchParams } from '../../../common/model/textModelSearch.js';
21
import { FindDecorations } from './findDecorations.js';
22
import { FindReplaceState, FindReplaceStateChangedEvent } from './findState.js';
23
import { ReplaceAllCommand } from './replaceAllCommand.js';
24
import { parseReplaceString, ReplacePattern } from './replacePattern.js';
25
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
26
import { IKeybindings } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
27
28
export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('findWidgetVisible', false);
29
export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated();
30
// Keep ContextKey use of 'Focussed' to not break when clauses
31
export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey<boolean>('findInputFocussed', false);
32
export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey<boolean>('replaceInputFocussed', false);
33
/**
34
* Context key that is true when any element within the Find widget has focus.
35
* This includes the Find input, Replace input, checkboxes, buttons, etc.
36
*/
37
export const CONTEXT_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('findWidgetFocused', false);
38
39
export const ToggleCaseSensitiveKeybinding: IKeybindings = {
40
primary: KeyMod.Alt | KeyCode.KeyC,
41
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC }
42
};
43
export const ToggleWholeWordKeybinding: IKeybindings = {
44
primary: KeyMod.Alt | KeyCode.KeyW,
45
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyW }
46
};
47
export const ToggleRegexKeybinding: IKeybindings = {
48
primary: KeyMod.Alt | KeyCode.KeyR,
49
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyR }
50
};
51
export const ToggleSearchScopeKeybinding: IKeybindings = {
52
primary: KeyMod.Alt | KeyCode.KeyL,
53
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyL }
54
};
55
export const TogglePreserveCaseKeybinding: IKeybindings = {
56
primary: KeyMod.Alt | KeyCode.KeyP,
57
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP }
58
};
59
60
export const FIND_IDS = {
61
StartFindAction: 'actions.find',
62
StartFindWithSelection: 'actions.findWithSelection',
63
StartFindWithArgs: 'editor.actions.findWithArgs',
64
NextMatchFindAction: 'editor.action.nextMatchFindAction',
65
PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
66
GoToMatchFindAction: 'editor.action.goToMatchFindAction',
67
NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
68
PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
69
StartFindReplaceAction: 'editor.action.startFindReplaceAction',
70
CloseFindWidgetCommand: 'closeFindWidget',
71
ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
72
ToggleWholeWordCommand: 'toggleFindWholeWord',
73
ToggleRegexCommand: 'toggleFindRegex',
74
ToggleSearchScopeCommand: 'toggleFindInSelection',
75
TogglePreserveCaseCommand: 'togglePreserveCase',
76
ReplaceOneAction: 'editor.action.replaceOne',
77
ReplaceAllAction: 'editor.action.replaceAll',
78
SelectAllMatchesAction: 'editor.action.selectAllMatches'
79
};
80
81
export const MATCHES_LIMIT = 19999;
82
const RESEARCH_DELAY = 240;
83
84
export class FindModelBoundToEditorModel {
85
86
private readonly _editor: IActiveCodeEditor;
87
private readonly _state: FindReplaceState;
88
private readonly _toDispose = new DisposableStore();
89
private readonly _decorations: FindDecorations;
90
private _ignoreModelContentChanged: boolean;
91
private readonly _startSearchingTimer: TimeoutTimer;
92
93
private readonly _updateDecorationsScheduler: RunOnceScheduler;
94
private _isDisposed: boolean;
95
96
constructor(editor: IActiveCodeEditor, state: FindReplaceState) {
97
this._editor = editor;
98
this._state = state;
99
this._isDisposed = false;
100
this._startSearchingTimer = new TimeoutTimer();
101
102
this._decorations = new FindDecorations(editor);
103
this._toDispose.add(this._decorations);
104
105
this._updateDecorationsScheduler = new RunOnceScheduler(() => {
106
if (!this._editor.hasModel()) {
107
return;
108
}
109
return this.research(false);
110
}, 100);
111
this._toDispose.add(this._updateDecorationsScheduler);
112
113
this._toDispose.add(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
114
if (
115
e.reason === CursorChangeReason.Explicit
116
|| e.reason === CursorChangeReason.Undo
117
|| e.reason === CursorChangeReason.Redo
118
) {
119
this._decorations.setStartPosition(this._editor.getPosition());
120
}
121
}));
122
123
this._ignoreModelContentChanged = false;
124
this._toDispose.add(this._editor.onDidChangeModelContent((e) => {
125
if (this._ignoreModelContentChanged) {
126
return;
127
}
128
if (e.isFlush) {
129
// a model.setValue() was called
130
this._decorations.reset();
131
}
132
this._decorations.setStartPosition(this._editor.getPosition());
133
this._updateDecorationsScheduler.schedule();
134
}));
135
136
this._toDispose.add(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
137
138
this.research(false, this._state.searchScope);
139
}
140
141
public dispose(): void {
142
this._isDisposed = true;
143
dispose(this._startSearchingTimer);
144
this._toDispose.dispose();
145
}
146
147
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
148
if (this._isDisposed) {
149
// The find model is disposed during a find state changed event
150
return;
151
}
152
if (!this._editor.hasModel()) {
153
// The find model will be disposed momentarily
154
return;
155
}
156
if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
157
const model = this._editor.getModel();
158
159
if (model.isTooLargeForSyncing()) {
160
this._startSearchingTimer.cancel();
161
162
this._startSearchingTimer.setIfNotSet(() => {
163
if (e.searchScope) {
164
this.research(e.moveCursor, this._state.searchScope);
165
} else {
166
this.research(e.moveCursor);
167
}
168
}, RESEARCH_DELAY);
169
} else {
170
if (e.searchScope) {
171
this.research(e.moveCursor, this._state.searchScope);
172
} else {
173
this.research(e.moveCursor);
174
}
175
}
176
}
177
}
178
179
private static _getSearchRange(model: ITextModel, findScope: Range | null): Range {
180
// If we have set now or before a find scope, use it for computing the search range
181
if (findScope) {
182
return findScope;
183
}
184
185
return model.getFullModelRange();
186
}
187
188
private research(moveCursor: boolean, newFindScope?: Range | Range[] | null): void {
189
let findScopes: Range[] | null = null;
190
if (typeof newFindScope !== 'undefined') {
191
if (newFindScope !== null) {
192
if (!Array.isArray(newFindScope)) {
193
findScopes = [newFindScope];
194
} else {
195
findScopes = newFindScope;
196
}
197
}
198
} else {
199
findScopes = this._decorations.getFindScopes();
200
}
201
if (findScopes !== null) {
202
findScopes = findScopes.map(findScope => {
203
if (findScope.startLineNumber !== findScope.endLineNumber) {
204
let endLineNumber = findScope.endLineNumber;
205
206
if (findScope.endColumn === 1) {
207
endLineNumber = endLineNumber - 1;
208
}
209
210
return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
211
}
212
return findScope;
213
});
214
}
215
216
const findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
217
this._decorations.set(findMatches, findScopes);
218
219
const editorSelection = this._editor.getSelection();
220
let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection);
221
if (currentMatchesPosition === 0 && findMatches.length > 0) {
222
// current selection is not on top of a match
223
// try to find its nearest result from the top of the document
224
const matchAfterSelection = findFirstIdxMonotonousOrArrLen(findMatches.map(match => match.range), range => Range.compareRangesUsingStarts(range, editorSelection) >= 0);
225
currentMatchesPosition = matchAfterSelection > 0 ? matchAfterSelection - 1 + 1 /** match position is one based */ : currentMatchesPosition;
226
}
227
228
this._state.changeMatchInfo(
229
currentMatchesPosition,
230
this._decorations.getCount(),
231
undefined
232
);
233
234
if (moveCursor && this._editor.getOption(EditorOption.find).cursorMoveOnType) {
235
this._moveToNextMatch(this._decorations.getStartPosition());
236
}
237
}
238
239
private _hasMatches(): boolean {
240
return (this._state.matchesCount > 0);
241
}
242
243
private _cannotFind(): boolean {
244
if (!this._hasMatches()) {
245
const findScope = this._decorations.getFindScope();
246
if (findScope) {
247
// Reveal the selection so user is reminded that 'selection find' is on.
248
this._editor.revealRangeInCenterIfOutsideViewport(findScope, ScrollType.Smooth);
249
}
250
return true;
251
}
252
return false;
253
}
254
255
private _setCurrentFindMatch(match: Range): void {
256
const matchesPosition = this._decorations.setCurrentFindMatch(match);
257
this._state.changeMatchInfo(
258
matchesPosition,
259
this._decorations.getCount(),
260
match
261
);
262
263
this._editor.setSelection(match);
264
this._editor.revealRangeInCenterIfOutsideViewport(match, ScrollType.Smooth);
265
}
266
267
private _prevSearchPosition(before: Position) {
268
const isUsingLineStops = this._state.isRegex && (
269
this._state.searchString.indexOf('^') >= 0
270
|| this._state.searchString.indexOf('$') >= 0
271
);
272
let { lineNumber, column } = before;
273
const model = this._editor.getModel();
274
275
if (isUsingLineStops || column === 1) {
276
if (lineNumber === 1) {
277
lineNumber = model.getLineCount();
278
} else {
279
lineNumber--;
280
}
281
column = model.getLineMaxColumn(lineNumber);
282
} else {
283
column--;
284
}
285
286
return new Position(lineNumber, column);
287
}
288
289
private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void {
290
if (!this._state.canNavigateBack()) {
291
// we are beyond the first matched find result
292
// instead of doing nothing, we should refocus the first item
293
const nextMatchRange = this._decorations.matchAfterPosition(before);
294
295
if (nextMatchRange) {
296
this._setCurrentFindMatch(nextMatchRange);
297
}
298
return;
299
}
300
if (this._decorations.getCount() < MATCHES_LIMIT) {
301
let prevMatchRange = this._decorations.matchBeforePosition(before);
302
303
if (prevMatchRange && prevMatchRange.isEmpty() && prevMatchRange.getStartPosition().equals(before)) {
304
before = this._prevSearchPosition(before);
305
prevMatchRange = this._decorations.matchBeforePosition(before);
306
}
307
308
if (prevMatchRange) {
309
this._setCurrentFindMatch(prevMatchRange);
310
}
311
312
return;
313
}
314
315
if (this._cannotFind()) {
316
return;
317
}
318
319
const findScope = this._decorations.getFindScope();
320
const searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
321
322
// ...(----)...|...
323
if (searchRange.getEndPosition().isBefore(before)) {
324
before = searchRange.getEndPosition();
325
}
326
327
// ...|...(----)...
328
if (before.isBefore(searchRange.getStartPosition())) {
329
before = searchRange.getEndPosition();
330
}
331
332
const { lineNumber, column } = before;
333
const model = this._editor.getModel();
334
335
let position = new Position(lineNumber, column);
336
337
let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, false);
338
339
if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
340
// Looks like we're stuck at this position, unacceptable!
341
position = this._prevSearchPosition(position);
342
prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, false);
343
}
344
345
if (!prevMatch) {
346
// there is precisely one match and selection is on top of it
347
return;
348
}
349
350
if (!isRecursed && !searchRange.containsRange(prevMatch.range)) {
351
return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true);
352
}
353
354
this._setCurrentFindMatch(prevMatch.range);
355
}
356
357
public moveToPrevMatch(): void {
358
this._moveToPrevMatch(this._editor.getSelection().getStartPosition());
359
}
360
361
private _nextSearchPosition(after: Position) {
362
const isUsingLineStops = this._state.isRegex && (
363
this._state.searchString.indexOf('^') >= 0
364
|| this._state.searchString.indexOf('$') >= 0
365
);
366
367
let { lineNumber, column } = after;
368
const model = this._editor.getModel();
369
370
if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
371
if (lineNumber === model.getLineCount()) {
372
lineNumber = 1;
373
} else {
374
lineNumber++;
375
}
376
column = 1;
377
} else {
378
column++;
379
}
380
381
return new Position(lineNumber, column);
382
}
383
384
private _moveToNextMatch(after: Position): void {
385
if (!this._state.canNavigateForward()) {
386
// we are beyond the last matched find result
387
// instead of doing nothing, we should refocus the last item
388
const prevMatchRange = this._decorations.matchBeforePosition(after);
389
390
if (prevMatchRange) {
391
this._setCurrentFindMatch(prevMatchRange);
392
}
393
return;
394
}
395
if (this._decorations.getCount() < MATCHES_LIMIT) {
396
let nextMatchRange = this._decorations.matchAfterPosition(after);
397
398
if (nextMatchRange && nextMatchRange.isEmpty() && nextMatchRange.getStartPosition().equals(after)) {
399
// Looks like we're stuck at this position, unacceptable!
400
after = this._nextSearchPosition(after);
401
nextMatchRange = this._decorations.matchAfterPosition(after);
402
}
403
if (nextMatchRange) {
404
this._setCurrentFindMatch(nextMatchRange);
405
}
406
407
return;
408
}
409
410
const nextMatch = this._getNextMatch(after, false, true);
411
if (nextMatch) {
412
this._setCurrentFindMatch(nextMatch.range);
413
}
414
}
415
416
private _getNextMatch(after: Position, captureMatches: boolean, forceMove: boolean, isRecursed: boolean = false): FindMatch | null {
417
if (this._cannotFind()) {
418
return null;
419
}
420
421
const findScope = this._decorations.getFindScope();
422
const searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
423
424
// ...(----)...|...
425
if (searchRange.getEndPosition().isBefore(after)) {
426
after = searchRange.getStartPosition();
427
}
428
429
// ...|...(----)...
430
if (after.isBefore(searchRange.getStartPosition())) {
431
after = searchRange.getStartPosition();
432
}
433
434
const { lineNumber, column } = after;
435
const model = this._editor.getModel();
436
437
let position = new Position(lineNumber, column);
438
439
let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches);
440
441
if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
442
// Looks like we're stuck at this position, unacceptable!
443
position = this._nextSearchPosition(position);
444
nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches);
445
}
446
447
if (!nextMatch) {
448
// there is precisely one match and selection is on top of it
449
return null;
450
}
451
452
if (!isRecursed && !searchRange.containsRange(nextMatch.range)) {
453
return this._getNextMatch(nextMatch.range.getEndPosition(), captureMatches, forceMove, true);
454
}
455
456
return nextMatch;
457
}
458
459
public moveToNextMatch(): void {
460
this._moveToNextMatch(this._editor.getSelection().getEndPosition());
461
}
462
463
private _moveToMatch(index: number): void {
464
const decorationRange = this._decorations.getDecorationRangeAt(index);
465
if (decorationRange) {
466
this._setCurrentFindMatch(decorationRange);
467
}
468
}
469
470
public moveToMatch(index: number): void {
471
this._moveToMatch(index);
472
}
473
474
private _getReplacePattern(): ReplacePattern {
475
if (this._state.isRegex) {
476
return parseReplaceString(this._state.replaceString);
477
}
478
return ReplacePattern.fromStaticValue(this._state.replaceString);
479
}
480
481
public replace(): void {
482
if (!this._hasMatches()) {
483
return;
484
}
485
486
const replacePattern = this._getReplacePattern();
487
const selection = this._editor.getSelection();
488
const nextMatch = this._getNextMatch(selection.getStartPosition(), true, false);
489
if (nextMatch) {
490
if (selection.equalsRange(nextMatch.range)) {
491
// selection sits on a find match => replace it!
492
const replaceString = replacePattern.buildReplaceString(nextMatch.matches, this._state.preserveCase);
493
494
const command = new ReplaceCommand(selection, replaceString);
495
496
this._executeEditorCommand('replace', command);
497
498
this._decorations.setStartPosition(new Position(selection.startLineNumber, selection.startColumn + replaceString.length));
499
this.research(true);
500
} else {
501
this._decorations.setStartPosition(this._editor.getPosition());
502
this._setCurrentFindMatch(nextMatch.range);
503
}
504
}
505
}
506
507
private _findMatches(findScopes: Range[] | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
508
const searchRanges = (findScopes as [] || [null]).map((scope: Range | null) =>
509
FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope)
510
);
511
512
return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
513
}
514
515
public replaceAll(): void {
516
if (!this._hasMatches()) {
517
return;
518
}
519
520
const findScopes = this._decorations.getFindScopes();
521
522
if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
523
// Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
524
this._largeReplaceAll();
525
} else {
526
this._regularReplaceAll(findScopes);
527
}
528
529
this.research(false);
530
}
531
532
private _largeReplaceAll(): void {
533
const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null);
534
const searchData = searchParams.parseSearchRequest();
535
if (!searchData) {
536
return;
537
}
538
539
let searchRegex = searchData.regex;
540
if (!searchRegex.multiline) {
541
let mod = 'mu';
542
if (searchRegex.ignoreCase) {
543
mod += 'i';
544
}
545
if (searchRegex.global) {
546
mod += 'g';
547
}
548
searchRegex = new RegExp(searchRegex.source, mod);
549
}
550
551
const model = this._editor.getModel();
552
const modelText = model.getValue(EndOfLinePreference.LF);
553
const fullModelRange = model.getFullModelRange();
554
555
const replacePattern = this._getReplacePattern();
556
let resultText: string;
557
const preserveCase = this._state.preserveCase;
558
559
if (replacePattern.hasReplacementPatterns || preserveCase) {
560
resultText = modelText.replace(searchRegex, function () {
561
// eslint-disable-next-line local/code-no-any-casts
562
return replacePattern.buildReplaceString(<string[]><any>arguments, preserveCase);
563
});
564
} else {
565
resultText = modelText.replace(searchRegex, replacePattern.buildReplaceString(null, preserveCase));
566
}
567
568
const command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
569
this._executeEditorCommand('replaceAll', command);
570
}
571
572
private _regularReplaceAll(findScopes: Range[] | null): void {
573
const replacePattern = this._getReplacePattern();
574
// Get all the ranges (even more than the highlighted ones)
575
const matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER);
576
577
const replaceStrings: string[] = [];
578
for (let i = 0, len = matches.length; i < len; i++) {
579
replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches, this._state.preserveCase);
580
}
581
582
const command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings);
583
this._executeEditorCommand('replaceAll', command);
584
}
585
586
public selectAllMatches(): void {
587
if (!this._hasMatches()) {
588
return;
589
}
590
591
const findScopes = this._decorations.getFindScopes();
592
593
// Get all the ranges (even more than the highlighted ones)
594
const matches = this._findMatches(findScopes, false, Constants.MAX_SAFE_SMALL_INTEGER);
595
let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));
596
597
// If one of the ranges is the editor selection, then maintain it as primary
598
const editorSelection = this._editor.getSelection();
599
for (let i = 0, len = selections.length; i < len; i++) {
600
const sel = selections[i];
601
if (sel.equalsRange(editorSelection)) {
602
selections = [editorSelection].concat(selections.slice(0, i)).concat(selections.slice(i + 1));
603
break;
604
}
605
}
606
607
this._editor.setSelections(selections);
608
}
609
610
private _executeEditorCommand(source: string, command: ICommand): void {
611
try {
612
this._ignoreModelContentChanged = true;
613
this._editor.pushUndoStop();
614
this._editor.executeCommand(source, command);
615
this._editor.pushUndoStop();
616
} finally {
617
this._ignoreModelContentChanged = false;
618
}
619
}
620
}
621
622