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