some more assertions
[idea/community.git] / platform / core-impl / src / com / intellij / openapi / editor / impl / DocumentImpl.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.editor.impl;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.application.Application;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.application.TransactionGuard;
22 import com.intellij.openapi.application.TransactionGuardImpl;
23 import com.intellij.openapi.command.CommandProcessor;
24 import com.intellij.openapi.diagnostic.Attachment;
25 import com.intellij.openapi.diagnostic.ExceptionWithAttachments;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.*;
28 import com.intellij.openapi.editor.actionSystem.DocCommandGroupId;
29 import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
30 import com.intellij.openapi.editor.event.DocumentEvent;
31 import com.intellij.openapi.editor.event.DocumentListener;
32 import com.intellij.openapi.editor.ex.*;
33 import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
34 import com.intellij.openapi.fileEditor.FileDocumentManager;
35 import com.intellij.openapi.progress.ProcessCanceledException;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.util.*;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.vfs.VirtualFile;
40 import com.intellij.reference.SoftReference;
41 import com.intellij.util.*;
42 import com.intellij.util.containers.ContainerUtil;
43 import com.intellij.util.containers.IntArrayList;
44 import com.intellij.util.text.CharArrayUtil;
45 import com.intellij.util.text.ImmutableCharSequence;
46 import gnu.trove.TIntObjectHashMap;
47 import gnu.trove.TObjectProcedure;
48 import org.jetbrains.annotations.NonNls;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51 import org.jetbrains.annotations.TestOnly;
52
53 import java.beans.PropertyChangeListener;
54 import java.beans.PropertyChangeSupport;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.List;
58 import java.util.concurrent.atomic.AtomicInteger;
59
60 public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
61   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.DocumentImpl");
62
63   private final Ref<DocumentListener[]> myCachedDocumentListeners = Ref.create(null);
64   private final List<DocumentListener> myDocumentListeners = ContainerUtil.createLockFreeCopyOnWriteList();
65   private final List<DocumentBulkUpdateListener> myBulkDocumentInternalListeners = ContainerUtil.createLockFreeCopyOnWriteList();
66   private final RangeMarkerTree<RangeMarkerEx> myRangeMarkers = new RangeMarkerTree<RangeMarkerEx>(this);
67   private final RangeMarkerTree<RangeMarkerEx> myPersistentRangeMarkers = new RangeMarkerTree<RangeMarkerEx>(this);
68   private final List<RangeMarker> myGuardedBlocks = new ArrayList<RangeMarker>();
69   private ReadonlyFragmentModificationHandler myReadonlyFragmentModificationHandler;
70
71   @SuppressWarnings("RedundantStringConstructorCall") private final Object myLineSetLock = new String("line set lock");
72   private volatile LineSet myLineSet;
73   private volatile ImmutableCharSequence myText;
74   private volatile SoftReference<String> myTextString;
75   private volatile FrozenDocument myFrozen;
76
77   private boolean myIsReadOnly;
78   private volatile boolean isStripTrailingSpacesEnabled = true;
79   private volatile long myModificationStamp;
80   private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);
81
82   private final List<EditReadOnlyListener> myReadOnlyListeners = ContainerUtil.createLockFreeCopyOnWriteList();
83
84   private volatile boolean myMightContainTabs = true; // optimisation flag: when document contains no tabs it is dramatically easier to calculate positions in editor
85   private int myTabTrackingRequestors;
86
87   private int myCheckGuardedBlocks;
88   private boolean myGuardsSuppressed;
89   private boolean myEventsHandling;
90   private final boolean myAssertThreading;
91   private volatile boolean myDoingBulkUpdate;
92   private volatile Throwable myBulkUpdateEnteringTrace;
93   private boolean myUpdatingBulkModeStatus;
94   private volatile boolean myAcceptSlashR;
95   private boolean myChangeInProgress;
96   private volatile int myBufferSize;
97   private final CharSequence myMutableCharSequence = new CharSequence() {
98     @Override
99     public int length() {
100       return myText.length();
101     }
102
103     @Override
104     public char charAt(int index) {
105       return myText.charAt(index);
106     }
107
108     @Override
109     public CharSequence subSequence(int start, int end) {
110       return myText.subSequence(start, end);
111     }
112
113     @NotNull
114     @Override
115     public String toString() {
116       return doGetText();
117     }
118   };
119   private final AtomicInteger sequence = new AtomicInteger();
120
121   public DocumentImpl(@NotNull String text) {
122     this(text, false);
123   }
124
125   public DocumentImpl(@NotNull CharSequence chars) {
126     this(chars, false);
127   }
128
129   /**
130    * NOTE: if client sets forUseInNonAWTThread to true it's supposed that client will completely control document and its listeners.
131    * The noticeable peculiarity of DocumentImpl behavior in this mode is that DocumentImpl won't suppress ProcessCancelledException
132    * thrown from listeners during changedUpdate event, so the exception will be rethrown and rest of the listeners WON'T be notified.
133    */
134   public DocumentImpl(@NotNull CharSequence chars, boolean forUseInNonAWTThread) {
135     this(chars, false, forUseInNonAWTThread);
136   }
137
138   public DocumentImpl(@NotNull CharSequence chars, boolean acceptSlashR, boolean forUseInNonAWTThread) {
139     setAcceptSlashR(acceptSlashR);
140     assertValidSeparators(chars);
141     myText = CharArrayUtil.createImmutableCharSequence(chars);
142     setCyclicBufferSize(0);
143     setModificationStamp(LocalTimeCounter.currentTime());
144     myAssertThreading = !forUseInNonAWTThread;
145   }
146
147   public boolean setAcceptSlashR(boolean accept) {
148     try {
149       return myAcceptSlashR;
150     }
151     finally {
152       myAcceptSlashR = accept;
153     }
154   }
155
156   public boolean acceptsSlashR() {
157     return myAcceptSlashR;
158   }
159
160   private LineSet getLineSet() {
161     LineSet lineSet = myLineSet;
162     if (lineSet == null) {
163       synchronized (myLineSetLock) {
164         lineSet = myLineSet;
165         if (lineSet == null) {
166           lineSet = LineSet.createLineSet(myText);
167           myLineSet = lineSet;
168         }
169       }
170     }
171
172     return lineSet;
173   }
174
175   @Override
176   @NotNull
177   public char[] getChars() {
178     return CharArrayUtil.fromSequence(myText);
179   }
180
181   @Override
182   public void setStripTrailingSpacesEnabled(boolean isEnabled) {
183     isStripTrailingSpacesEnabled = isEnabled;
184   }
185
186   @TestOnly
187   public boolean stripTrailingSpaces(Project project) {
188     return stripTrailingSpaces(project, false);
189   }
190
191   @TestOnly
192   public boolean stripTrailingSpaces(Project project, boolean inChangedLinesOnly) {
193     return stripTrailingSpaces(project, inChangedLinesOnly, true, new int[0]);
194   }
195
196   /**
197    * @return true if stripping was completed successfully, false if the document prevented stripping by e.g. caret(s) being in the way
198    */
199   boolean stripTrailingSpaces(@Nullable final Project project,
200                               boolean inChangedLinesOnly,
201                               boolean skipCaretLines,
202                               @NotNull int[] caretOffsets) {
203     if (!isStripTrailingSpacesEnabled) {
204       return true;
205     }
206     List<StripTrailingSpacesFilter> filters = new ArrayList<StripTrailingSpacesFilter>();
207     for (StripTrailingSpacesFilterFactory filterFactory : StripTrailingSpacesFilterFactory.EXTENSION_POINT.getExtensions()) {
208       StripTrailingSpacesFilter filter = filterFactory.createFilter(project, this);
209       if (filter == StripTrailingSpacesFilter.NOT_ALLOWED) {
210         return true;
211       }
212       else if (filter == StripTrailingSpacesFilter.POSTPONED) {
213         return false;
214       }
215       else {
216         filters.add(filter);
217       }
218     }
219
220     boolean markAsNeedsStrippingLater = false;
221     CharSequence text = myText;
222     TIntObjectHashMap<List<RangeMarker>> caretMarkers = new TIntObjectHashMap<List<RangeMarker>>(caretOffsets.length);
223     try {
224       if (skipCaretLines) {
225         for (int caretOffset : caretOffsets) {
226           if (caretOffset < 0 || caretOffset > getTextLength()) {
227             continue;
228           }
229           int line = getLineNumber(caretOffset);
230           List<RangeMarker> markers = caretMarkers.get(line);
231           if (markers == null) {
232             markers = new ArrayList<RangeMarker>();
233             caretMarkers.put(line, markers);
234           }
235           RangeMarker marker = createRangeMarker(caretOffset, caretOffset);
236           markers.add(marker);
237         }
238       }
239       lineLoop:
240       for (int line = 0; line < getLineCount(); line++) {
241         LineSet lineSet = getLineSet();
242         if (inChangedLinesOnly && !lineSet.isModified(line) || !canStripSpacesFrom(line, filters)) continue;
243         int whiteSpaceStart = -1;
244         final int lineEnd = lineSet.getLineEnd(line) - lineSet.getSeparatorLength(line);
245         int lineStart = lineSet.getLineStart(line);
246         for (int offset = lineEnd - 1; offset >= lineStart; offset--) {
247           char c = text.charAt(offset);
248           if (c != ' ' && c != '\t') {
249             break;
250           }
251           whiteSpaceStart = offset;
252         }
253         if (whiteSpaceStart == -1) continue;
254         if (skipCaretLines) {
255           List<RangeMarker> markers = caretMarkers.get(line);
256           if (markers != null) {
257             for (RangeMarker marker : markers) {
258               if (marker.getStartOffset() >= 0 && whiteSpaceStart < marker.getStartOffset()) {
259                 // mark this as a document that needs stripping later
260                 // otherwise the caret would jump madly
261                 markAsNeedsStrippingLater = true;
262                 continue lineLoop;
263               }
264             }
265           }
266         }
267         final int finalStart = whiteSpaceStart;
268         // document must be unblocked by now. If not, some Save handler attempted to modify PSI
269         // which should have been caught by assertion in com.intellij.pom.core.impl.PomModelImpl.runTransaction
270         DocumentUtil.writeInRunUndoTransparentAction(new DocumentRunnable(DocumentImpl.this, project) {
271           @Override
272           public void run() {
273             deleteString(finalStart, lineEnd);
274           }
275         });
276         text = myText;
277       }
278     }
279     finally {
280       caretMarkers.forEachValue(new TObjectProcedure<List<RangeMarker>>() {
281         @Override
282         public boolean execute(List<RangeMarker> markerList) {
283           if (markerList != null) {
284             for (RangeMarker marker : markerList) {
285               try {
286                 marker.dispose();
287               }
288               catch (Exception e) {
289                 LOG.error(e);
290               }
291             }
292           }
293           return true;
294         }
295       });
296     }
297     return markAsNeedsStrippingLater;
298   }
299
300   private static boolean canStripSpacesFrom(int line, @NotNull List<StripTrailingSpacesFilter> filters) {
301     for (StripTrailingSpacesFilter filter :  filters) {
302       if (!filter.isStripSpacesAllowedForLine(line)) return false;
303     }
304     return true;
305   }
306
307   @Override
308   public void setReadOnly(boolean isReadOnly) {
309     if (myIsReadOnly != isReadOnly) {
310       myIsReadOnly = isReadOnly;
311       myPropertyChangeSupport.firePropertyChange(Document.PROP_WRITABLE, !isReadOnly, isReadOnly);
312     }
313   }
314
315   ReadonlyFragmentModificationHandler getReadonlyFragmentModificationHandler() {
316     return myReadonlyFragmentModificationHandler;
317   }
318
319   void setReadonlyFragmentModificationHandler(final ReadonlyFragmentModificationHandler readonlyFragmentModificationHandler) {
320     myReadonlyFragmentModificationHandler = readonlyFragmentModificationHandler;
321   }
322
323   @Override
324   public boolean isWritable() {
325     return !myIsReadOnly;
326   }
327
328   private  RangeMarkerTree<RangeMarkerEx> treeFor(@NotNull RangeMarkerEx rangeMarker) {
329     return rangeMarker instanceof PersistentRangeMarker ? myPersistentRangeMarkers : myRangeMarkers;
330   }
331   @Override
332   public boolean removeRangeMarker(@NotNull RangeMarkerEx rangeMarker) {
333     return treeFor(rangeMarker).removeInterval(rangeMarker);
334   }
335
336   @Override
337   public void registerRangeMarker(@NotNull RangeMarkerEx rangeMarker,
338                                   int start,
339                                   int end,
340                                   boolean greedyToLeft,
341                                   boolean greedyToRight,
342                                   int layer) {
343     treeFor(rangeMarker).addInterval(rangeMarker, start, end, greedyToLeft, greedyToRight, layer);
344   }
345
346   @TestOnly
347   int getRangeMarkersSize() {
348     return myRangeMarkers.size() + myPersistentRangeMarkers.size();
349   }
350
351   @TestOnly
352   int getRangeMarkersNodeSize() {
353     return myRangeMarkers.nodeSize()+myPersistentRangeMarkers.nodeSize();
354   }
355
356   @Override
357   @NotNull
358   public RangeMarker createGuardedBlock(int startOffset, int endOffset) {
359     LOG.assertTrue(startOffset <= endOffset, "Should be startOffset <= endOffset");
360     RangeMarker block = createRangeMarker(startOffset, endOffset, true);
361     myGuardedBlocks.add(block);
362     return block;
363   }
364
365   @Override
366   public void removeGuardedBlock(@NotNull RangeMarker block) {
367     myGuardedBlocks.remove(block);
368   }
369
370   @Override
371   @NotNull
372   public List<RangeMarker> getGuardedBlocks() {
373     return myGuardedBlocks;
374   }
375
376   @Override
377   @SuppressWarnings("ForLoopReplaceableByForEach") // Way too many garbage is produced otherwise in AbstractList.iterator()
378   public RangeMarker getOffsetGuard(int offset) {
379     for (int i = 0; i < myGuardedBlocks.size(); i++) {
380       RangeMarker block = myGuardedBlocks.get(i);
381       if (offsetInRange(offset, block.getStartOffset(), block.getEndOffset())) return block;
382     }
383
384     return null;
385   }
386
387   @Override
388   public RangeMarker getRangeGuard(int start, int end) {
389     for (RangeMarker block : myGuardedBlocks) {
390       if (rangesIntersect(start, end, true, true,
391                           block.getStartOffset(), block.getEndOffset(), block.isGreedyToLeft(), block.isGreedyToRight())) {
392         return block;
393       }
394     }
395
396     return null;
397   }
398
399   @Override
400   public void startGuardedBlockChecking() {
401     myCheckGuardedBlocks++;
402   }
403
404   @Override
405   public void stopGuardedBlockChecking() {
406     LOG.assertTrue(myCheckGuardedBlocks > 0, "Unpaired start/stopGuardedBlockChecking");
407     myCheckGuardedBlocks--;
408   }
409
410   private static boolean offsetInRange(int offset, int start, int end) {
411     return start <= offset && offset < end;
412   }
413
414   private static boolean rangesIntersect(int start0, int end0, boolean start0Inclusive, boolean end0Inclusive,
415                                          int start1, int end1, boolean start1Inclusive, boolean end1Inclusive) {
416     if (start0 > start1 || start0 == start1 && !start0Inclusive) {
417       if (end1 == start0) return start0Inclusive && end1Inclusive;
418       return end1 > start0;
419     }
420     if (end0 == start1) return start1Inclusive && end0Inclusive;
421     return end0 > start1;
422   }
423
424   @Override
425   @NotNull
426   public RangeMarker createRangeMarker(int startOffset, int endOffset) {
427     return createRangeMarker(startOffset, endOffset, false);
428   }
429
430   @Override
431   @NotNull
432   public RangeMarker createRangeMarker(int startOffset, int endOffset, boolean surviveOnExternalChange) {
433     if (!(0 <= startOffset && startOffset <= endOffset && endOffset <= getTextLength())) {
434       LOG.error("Incorrect offsets: startOffset=" + startOffset + ", endOffset=" + endOffset + ", text length=" + getTextLength());
435     }
436     return surviveOnExternalChange
437            ? new PersistentRangeMarker(this, startOffset, endOffset, true)
438            : new RangeMarkerImpl(this, startOffset, endOffset, true);
439   }
440
441   @Override
442   public long getModificationStamp() {
443     return myModificationStamp;
444   }
445
446   @Override
447   public void setModificationStamp(long modificationStamp) {
448     myModificationStamp = modificationStamp;
449   }
450
451   @Override
452   public void replaceText(@NotNull CharSequence chars, long newModificationStamp) {
453     replaceString(0, getTextLength(), chars, newModificationStamp, true); //TODO: optimization!!!
454     clearLineModificationFlags();
455   }
456
457   @Override
458   public int getListenersCount() {
459     return myDocumentListeners.size();
460   }
461
462   @Override
463   public void insertString(int offset, @NotNull CharSequence s) {
464     if (offset < 0) throw new IndexOutOfBoundsException("Wrong offset: " + offset);
465     if (offset > getTextLength()) {
466       throw new IndexOutOfBoundsException(
467         "Wrong offset: " + offset + "; documentLength: " + getTextLength() + "; " + s.subSequence(Math.max(0, s.length() - 20), s.length())
468       );
469     }
470     assertWriteAccess();
471     assertValidSeparators(s);
472
473     if (!isWritable()) throw new ReadOnlyModificationException(this);
474     if (s.length() == 0) return;
475
476     RangeMarker marker = getRangeGuard(offset, offset);
477     if (marker != null) {
478       throwGuardedFragment(marker, offset, "", s);
479     }
480
481     ImmutableCharSequence newText = myText.insert(offset, s);
482     ImmutableCharSequence newString = newText.subtext(offset, offset + s.length());
483     updateText(newText, offset, "", newString, false, LocalTimeCounter.currentTime(), offset, 0);
484     trimToSize();
485   }
486
487   private void trimToSize() {
488     if (myBufferSize != 0 && getTextLength() > myBufferSize) {
489       deleteString(0, getTextLength() - myBufferSize);
490     }
491   }
492
493   @Override
494   public void deleteString(int startOffset, int endOffset) {
495     assertBounds(startOffset, endOffset);
496
497     assertWriteAccess();
498     if (!isWritable()) throw new ReadOnlyModificationException(this);
499     if (startOffset == endOffset) return;
500
501     RangeMarker marker = getRangeGuard(startOffset, endOffset);
502     if (marker != null) {
503       throwGuardedFragment(marker, startOffset, myText.subSequence(startOffset, endOffset), "");
504     }
505
506     ImmutableCharSequence newText = myText.delete(startOffset, endOffset);
507     ImmutableCharSequence oldString = myText.subtext(startOffset, endOffset);
508     updateText(newText, startOffset, oldString, "", false, LocalTimeCounter.currentTime(), startOffset, endOffset - startOffset);
509   }
510
511   @Override
512   public void moveText(int srcStart, int srcEnd, int dstOffset) {
513     assertBounds(srcStart, srcEnd);
514     if (dstOffset == srcEnd) return;
515     ProperTextRange srcRange = new ProperTextRange(srcStart, srcEnd);
516     assert !srcRange.containsOffset(dstOffset) : "Can't perform text move from range [" +srcStart+ "; " + srcEnd+ ") to offset "+dstOffset;
517
518     String replacement = getCharsSequence().subSequence(srcStart, srcEnd).toString();
519
520     insertString(dstOffset, replacement);
521     int shift = 0;
522     if (dstOffset < srcStart) {
523       shift = srcEnd - srcStart;
524     }
525     fireMoveText(srcStart + shift, srcEnd + shift, dstOffset);
526
527     deleteString(srcStart + shift, srcEnd + shift);
528   }
529
530   private void fireMoveText(int start, int end, int newBase) {
531     for (DocumentListener listener : getCachedListeners()) {
532       if (listener instanceof PrioritizedInternalDocumentListener) {
533         ((PrioritizedInternalDocumentListener)listener).moveTextHappened(start, end, newBase);
534       }
535     }
536   }
537
538   @Override
539   public void replaceString(int startOffset, int endOffset, @NotNull CharSequence s) {
540     replaceString(startOffset, endOffset, s, LocalTimeCounter.currentTime(), false);
541   }
542
543   private void replaceString(int startOffset, int endOffset, @NotNull CharSequence s, final long newModificationStamp, boolean wholeTextReplaced) {
544     assertBounds(startOffset, endOffset);
545
546     assertWriteAccess();
547     assertValidSeparators(s);
548
549     if (!isWritable()) {
550       throw new ReadOnlyModificationException(this);
551     }
552
553     int initialStartOffset = startOffset;
554     int initialOldLength = endOffset - startOffset;
555
556     final int newStringLength = s.length();
557     final CharSequence chars = myText;
558     int newStartInString = 0;
559     while (newStartInString < newStringLength &&
560            startOffset < endOffset &&
561            s.charAt(newStartInString) == chars.charAt(startOffset)) {
562       startOffset++;
563       newStartInString++;
564     }
565
566     int newEndInString = newStringLength;
567     while (endOffset > startOffset &&
568            newEndInString > newStartInString &&
569            s.charAt(newEndInString - 1) == chars.charAt(endOffset - 1)) {
570       newEndInString--;
571       endOffset--;
572     }
573
574     if (startOffset == 0 && endOffset == getTextLength()) {
575       wholeTextReplaced = true;
576     }
577
578     CharSequence changedPart = s.subSequence(newStartInString, newEndInString);
579     CharSequence sToDelete = myText.subtext(startOffset, endOffset);
580     RangeMarker guard = getRangeGuard(startOffset, endOffset);
581     if (guard != null) {
582       throwGuardedFragment(guard, startOffset, sToDelete, changedPart);
583     }
584
585     ImmutableCharSequence newText;
586     if (wholeTextReplaced && s instanceof ImmutableCharSequence) {
587       newText = (ImmutableCharSequence)s;
588     }
589     else {
590       newText = myText.delete(startOffset, endOffset).insert(startOffset, changedPart);
591       changedPart = newText.subtext(startOffset, startOffset + changedPart.length());
592     }
593     updateText(newText, startOffset, sToDelete, changedPart, wholeTextReplaced, newModificationStamp, initialStartOffset, initialOldLength);
594     trimToSize();
595   }
596
597   private void assertBounds(final int startOffset, final int endOffset) {
598     if (startOffset < 0 || startOffset > getTextLength()) {
599       throw new IndexOutOfBoundsException("Wrong startOffset: " + startOffset + "; documentLength: " + getTextLength());
600     }
601     if (endOffset < 0 || endOffset > getTextLength()) {
602       throw new IndexOutOfBoundsException("Wrong endOffset: " + endOffset + "; documentLength: " + getTextLength());
603     }
604     if (endOffset < startOffset) {
605       throw new IllegalArgumentException(
606         "endOffset < startOffset: " + endOffset + " < " + startOffset + "; documentLength: " + getTextLength());
607     }
608   }
609
610   private void assertWriteAccess() {
611     if (myAssertThreading) {
612       final Application application = ApplicationManager.getApplication();
613       if (application != null) {
614         application.assertWriteAccessAllowed();
615         VirtualFile file = FileDocumentManager.getInstance().getFile(this);
616         if (file != null && file.isInLocalFileSystem()) {
617           ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
618         }
619       }
620     }
621   }
622
623   private void assertValidSeparators(@NotNull CharSequence s) {
624     if (myAcceptSlashR) return;
625     StringUtil.assertValidSeparators(s);
626   }
627
628   /**
629    * All document change actions follows the algorithm below:
630    * <pre>
631    * <ol>
632    *   <li>
633    *     All {@link #addDocumentListener(DocumentListener) registered listeners} are notified
634    *     {@link DocumentListener#beforeDocumentChange(DocumentEvent) before the change};
635    *   </li>
636    *   <li>The change is performed </li>
637    *   <li>
638    *     All {@link #addDocumentListener(DocumentListener) registered listeners} are notified
639    *     {@link DocumentListener#documentChanged(DocumentEvent) after the change};
640    *   </li>
641    * </ol>
642    * </pre>
643    * <p/>
644    * There is a possible case that {@code 'before change'} notification produces new change. We have a problem then - imagine
645    * that initial change was {@code 'replace particular range at document end'} and {@code 'nested change'} was to
646    * {@code 'remove text at document end'}. That means that when initial change will be actually performed, the document may be
647    * not long enough to contain target range.
648    * <p/>
649    * Current method allows to check if document change is a {@code 'nested call'}.
650    *
651    * @throws IllegalStateException if this method is called during a {@code 'nested document modification'}
652    */
653   private void assertNotNestedModification() throws IllegalStateException {
654     if (myChangeInProgress) {
655       throw new IllegalStateException("Detected document modification from DocumentListener");
656     }
657   }
658
659   private void throwGuardedFragment(@NotNull RangeMarker guard, int offset, @NotNull CharSequence oldString, @NotNull CharSequence newString) {
660     if (myCheckGuardedBlocks > 0 && !myGuardsSuppressed) {
661       DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, false);
662       throw new ReadOnlyFragmentModificationException(event, guard);
663     }
664   }
665
666   @Override
667   public void suppressGuardedExceptions() {
668     myGuardsSuppressed = true;
669   }
670
671   @Override
672   public void unSuppressGuardedExceptions() {
673     myGuardsSuppressed = false;
674   }
675
676   @Override
677   public boolean isInEventsHandling() {
678     return myEventsHandling;
679   }
680
681   @Override
682   public void clearLineModificationFlags() {
683     myLineSet = getLineSet().clearModificationFlags();
684     myFrozen = null;
685   }
686
687   public void clearLineModificationFlags(int startLine, int endLine) {
688     myLineSet = getLineSet().clearModificationFlags(startLine, endLine);
689     myFrozen = null;
690   }
691
692   void clearLineModificationFlagsExcept(@NotNull int[] caretLines) {
693     IntArrayList modifiedLines = new IntArrayList(caretLines.length);
694     LineSet lineSet = getLineSet();
695     for (int line : caretLines) {
696       if (line >= 0 && line < lineSet.getLineCount() && lineSet.isModified(line)) {
697         modifiedLines.add(line);
698       }
699     }
700     lineSet = lineSet.clearModificationFlags();
701     for (int i = 0; i < modifiedLines.size(); i++) {
702       lineSet = lineSet.setModified(modifiedLines.get(i));
703     }
704     myLineSet = lineSet;
705     myFrozen = null;
706   }
707
708   private void updateText(@NotNull ImmutableCharSequence newText,
709                           int offset,
710                           @NotNull CharSequence oldString,
711                           @NotNull CharSequence newString,
712                           boolean wholeTextReplaced,
713                           long newModificationStamp,
714                           int initialStartOffset,
715                           int initialOldLength) {
716     assertNotNestedModification();
717     myChangeInProgress = true;
718     try {
719       DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced, initialStartOffset, initialOldLength);
720       doBeforeChangedUpdate(event);
721       myTextString = null;
722       ImmutableCharSequence prevText = myText;
723       myText = newText;
724       sequence.incrementAndGet(); // increment sequence before firing events so that modification sequence on commit will match this sequence now
725       changedUpdate(event, newModificationStamp, prevText);
726     }
727     finally {
728       myChangeInProgress = false;
729     }
730   }
731
732   @Override
733   public int getModificationSequence() {
734     return sequence.get();
735   }
736
737   private void doBeforeChangedUpdate(DocumentEvent event) {
738     Application app = ApplicationManager.getApplication();
739     if (app != null) {
740       FileDocumentManager manager = FileDocumentManager.getInstance();
741       VirtualFile file = manager.getFile(this);
742       if (file != null && !file.isValid()) {
743         LOG.error("File of this document has been deleted.");
744       }
745     }
746     assertInsideCommand();
747
748     getLineSet(); // initialize line set to track changed lines
749
750     if (!ShutDownTracker.isShutdownHookRunning()) {
751       DocumentListener[] listeners = getCachedListeners();
752       for (int i = listeners.length - 1; i >= 0; i--) {
753         try {
754           listeners[i].beforeDocumentChange(event);
755         }
756         catch (Throwable e) {
757           LOG.error(e);
758         }
759       }
760     }
761
762     myEventsHandling = true;
763   }
764
765   private void assertInsideCommand() {
766     if (!myAssertThreading) return;
767     CommandProcessor commandProcessor = CommandProcessor.getInstance();
768     if (!commandProcessor.isUndoTransparentActionInProgress() &&
769         commandProcessor.getCurrentCommand() == null) {
770       throw new IncorrectOperationException("Must not change document outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor");
771     }
772   }
773
774   private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText) {
775     try {
776       if (LOG.isDebugEnabled()) LOG.debug(event.toString());
777
778       assert event.getOldFragment().length() ==  event.getOldLength() : "event.getOldFragment().length() = " + event.getOldFragment().length()+"; event.getOldLength() = " + event.getOldLength();
779       assert event.getNewFragment().length() ==  event.getNewLength() : "event.getNewFragment().length() = " + event.getNewFragment().length()+"; event.getNewLength() = " + event.getNewLength();
780       assert prevText.length() + event.getNewLength() - event.getOldLength() == getTextLength() : "prevText.length() = " + prevText.length()+ "; event.getNewLength() = " + event.getNewLength()+ "; event.getOldLength() = " + event.getOldLength()+ "; getTextLength() = " + getTextLength();
781
782       myLineSet = getLineSet().update(prevText, event.getOffset(), event.getOffset() + event.getOldLength(), event.getNewFragment(), event.isWholeTextReplaced());
783       assert getTextLength() == myLineSet.getLength() : "getTextLength() = " + getTextLength()+ "; myLineSet.getLength() = " + myLineSet.getLength();
784
785       myFrozen = null;
786       if (myTabTrackingRequestors > 0) {
787         updateMightContainTabs(event.getNewFragment());
788       }
789       setModificationStamp(newModificationStamp);
790
791       if (!ShutDownTracker.isShutdownHookRunning()) {
792         DocumentListener[] listeners = getCachedListeners();
793         for (DocumentListener listener : listeners) {
794           try {
795             listener.documentChanged(event);
796           }
797           catch (ProcessCanceledException e) {
798             if (!myAssertThreading) {
799               throw e;
800             }
801             else {
802               LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e));
803             }
804           }
805           catch (Throwable e) {
806             LOG.error(e);
807           }
808         }
809       }
810     }
811     finally {
812       myEventsHandling = false;
813     }
814   }
815
816   @NotNull
817   @Override
818   public String getText() {
819     return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
820       @Override
821       public String compute() {
822         return doGetText();
823       }
824     });
825   }
826
827   @NotNull
828   private String doGetText() {
829     String s = SoftReference.dereference(myTextString);
830     if (s == null) {
831       myTextString = new SoftReference<String>(s = myText.toString());
832     }
833     return s;
834   }
835
836   @NotNull
837   @Override
838   public String getText(@NotNull final TextRange range) {
839     return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
840       @Override
841       public String compute() {
842         return myText.subSequence(range.getStartOffset(), range.getEndOffset()).toString();
843       }
844     });
845   }
846
847   @Override
848   public int getTextLength() {
849     return myText.length();
850   }
851
852   @Override
853   @NotNull
854   public CharSequence getCharsSequence() {
855     return myMutableCharSequence;
856   }
857
858   @NotNull
859   @Override
860   public CharSequence getImmutableCharSequence() {
861     return myText;
862   }
863
864   @Override
865   public void addDocumentListener(@NotNull DocumentListener listener) {
866     myCachedDocumentListeners.set(null);
867
868     if (myDocumentListeners.contains(listener)) {
869       LOG.error("Already registered: " + listener);
870     }
871     boolean added = myDocumentListeners.add(listener);
872     LOG.assertTrue(added, listener);
873   }
874
875   @Override
876   public void addDocumentListener(@NotNull final DocumentListener listener, @NotNull Disposable parentDisposable) {
877     addDocumentListener(listener);
878     Disposer.register(parentDisposable, new DocumentListenerDisposable(listener, myCachedDocumentListeners, myDocumentListeners));
879   }
880
881   private static class DocumentListenerDisposable implements Disposable {
882     private final DocumentListener myListener;
883     private final Ref<DocumentListener[]> myCachedDocumentListenersRef;
884     private final List<DocumentListener> myDocumentListeners;
885
886     private DocumentListenerDisposable(@NotNull DocumentListener listener,
887                                        @NotNull Ref<DocumentListener[]> cachedDocumentListenersRef,
888                                        @NotNull List<DocumentListener> documentListeners) {
889       myListener = listener;
890       myCachedDocumentListenersRef = cachedDocumentListenersRef;
891       myDocumentListeners = documentListeners;
892     }
893
894     @Override
895     public void dispose() {
896       doRemoveDocumentListener(myListener, myCachedDocumentListenersRef, myDocumentListeners);
897     }
898   }
899
900   @Override
901   public void removeDocumentListener(@NotNull DocumentListener listener) {
902     doRemoveDocumentListener(listener, myCachedDocumentListeners, myDocumentListeners);
903   }
904
905   void addInternalBulkModeListener(@NotNull DocumentBulkUpdateListener listener) {
906     myBulkDocumentInternalListeners.add(listener);
907   }
908
909   void removeInternalBulkModeListener(@NotNull DocumentBulkUpdateListener listener) {
910     myBulkDocumentInternalListeners.remove(listener);
911   }
912
913   private static void doRemoveDocumentListener(@NotNull DocumentListener listener,
914                                                @NotNull Ref<DocumentListener[]> cachedDocumentListenersRef,
915                                                @NotNull List<DocumentListener> documentListeners) {
916     cachedDocumentListenersRef.set(null);
917     boolean success = documentListeners.remove(listener);
918     if (!success) {
919       LOG.error("Can't remove document listener (" + listener + "). Registered listeners: " + documentListeners);
920     }
921   }
922
923   @Override
924   public int getLineNumber(final int offset) {
925     return getLineSet().findLineIndex(offset);
926   }
927
928   @Override
929   @NotNull
930   public LineIterator createLineIterator() {
931     return getLineSet().createIterator();
932   }
933
934   @Override
935   public final int getLineStartOffset(final int line) {
936     if (line == 0) return 0; // otherwise it crashed for zero-length document
937     return getLineSet().getLineStart(line);
938   }
939
940   @Override
941   public final int getLineEndOffset(int line) {
942     if (getTextLength() == 0 && line == 0) return 0;
943     int result = getLineSet().getLineEnd(line) - getLineSeparatorLength(line);
944     assert result >= 0;
945     return result;
946   }
947
948   @Override
949   public final int getLineSeparatorLength(int line) {
950     int separatorLength = getLineSet().getSeparatorLength(line);
951     assert separatorLength >= 0;
952     return separatorLength;
953   }
954
955   @Override
956   public final int getLineCount() {
957     int lineCount = getLineSet().getLineCount();
958     assert lineCount >= 0;
959     return lineCount;
960   }
961
962   @NotNull
963   private DocumentListener[] getCachedListeners() {
964     DocumentListener[] cachedListeners = myCachedDocumentListeners.get();
965     if (cachedListeners == null) {
966       DocumentListener[] listeners = ArrayUtil.stripTrailingNulls(myDocumentListeners.toArray(new DocumentListener[myDocumentListeners.size()]));
967       Arrays.sort(listeners, PrioritizedDocumentListener.COMPARATOR);
968       cachedListeners = listeners;
969       myCachedDocumentListeners.set(cachedListeners);
970     }
971
972     return cachedListeners;
973   }
974
975   @Override
976   public void fireReadOnlyModificationAttempt() {
977     for (EditReadOnlyListener listener : myReadOnlyListeners) {
978       listener.readOnlyModificationAttempt(this);
979     }
980   }
981
982   @Override
983   public void addEditReadOnlyListener(@NotNull EditReadOnlyListener listener) {
984     myReadOnlyListeners.add(listener);
985   }
986
987   @Override
988   public void removeEditReadOnlyListener(@NotNull EditReadOnlyListener listener) {
989     myReadOnlyListeners.remove(listener);
990   }
991
992
993   @Override
994   public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
995     myPropertyChangeSupport.addPropertyChangeListener(listener);
996   }
997
998   @Override
999   public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
1000     myPropertyChangeSupport.removePropertyChangeListener(listener);
1001   }
1002
1003   @Override
1004   public void setCyclicBufferSize(int bufferSize) {
1005     assert bufferSize >= 0 : bufferSize;
1006     myBufferSize = bufferSize;
1007   }
1008
1009   @Override
1010   public void setText(@NotNull final CharSequence text) {
1011     Runnable runnable = new Runnable() {
1012       @Override
1013       public void run() {
1014         replaceString(0, getTextLength(), text, LocalTimeCounter.currentTime(), true);
1015       }
1016     };
1017     if (CommandProcessor.getInstance().isUndoTransparentActionInProgress()) {
1018       runnable.run();
1019     }
1020     else {
1021       CommandProcessor.getInstance().executeCommand(null, runnable, "", DocCommandGroupId.noneGroupId(this));
1022     }
1023
1024     clearLineModificationFlags();
1025   }
1026
1027   @Override
1028   @NotNull
1029   public RangeMarker createRangeMarker(@NotNull final TextRange textRange) {
1030     return createRangeMarker(textRange.getStartOffset(), textRange.getEndOffset());
1031   }
1032
1033   @Override
1034   public final boolean isInBulkUpdate() {
1035     return myDoingBulkUpdate;
1036   }
1037
1038   @Override
1039   public final void setInBulkUpdate(boolean value) {
1040     if (myAssertThreading) {
1041       ApplicationManager.getApplication().assertIsDispatchThread();
1042     }
1043     if (myUpdatingBulkModeStatus) {
1044       throw new IllegalStateException("Detected bulk mode status update from DocumentBulkUpdateListener");
1045     }
1046     if (myDoingBulkUpdate == value) {
1047       return;
1048     }
1049     myUpdatingBulkModeStatus = true;
1050     try {
1051       if (value) {
1052         getPublisher().updateStarted(this);
1053         notifyInternalListenersOnBulkModeStarted();
1054         myBulkUpdateEnteringTrace = new Throwable();
1055         myDoingBulkUpdate = true;
1056       }
1057       else {
1058         myDoingBulkUpdate = false;
1059         myBulkUpdateEnteringTrace = null;
1060         notifyInternalListenersOnBulkModeFinished();
1061         getPublisher().updateFinished(this);
1062       }
1063     }
1064     finally {
1065       myUpdatingBulkModeStatus = false;
1066     }
1067   }
1068
1069   private void notifyInternalListenersOnBulkModeStarted() {
1070     for (DocumentBulkUpdateListener listener : myBulkDocumentInternalListeners) {
1071       listener.updateStarted(this);
1072     }
1073   }
1074
1075   private void notifyInternalListenersOnBulkModeFinished() {
1076     for (DocumentBulkUpdateListener listener : myBulkDocumentInternalListeners) {
1077       listener.updateFinished(this);
1078     }
1079   }
1080
1081   private static class DocumentBulkUpdateListenerHolder {
1082     private static final DocumentBulkUpdateListener ourBulkChangePublisher =
1083       ApplicationManager.getApplication().getMessageBus().syncPublisher(DocumentBulkUpdateListener.TOPIC);
1084   }
1085
1086   @NotNull
1087   private static DocumentBulkUpdateListener getPublisher() {
1088     return DocumentBulkUpdateListenerHolder.ourBulkChangePublisher;
1089   }
1090
1091   @Override
1092   public boolean processRangeMarkers(@NotNull Processor<RangeMarker> processor) {
1093     return processRangeMarkersOverlappingWith(0, getTextLength(), processor);
1094   }
1095
1096   @Override
1097   public boolean processRangeMarkersOverlappingWith(int start, int end, @NotNull Processor<RangeMarker> processor) {
1098     TextRangeInterval interval = new TextRangeInterval(start, end);
1099     MarkupIterator<RangeMarkerEx> iterator = IntervalTreeImpl
1100       .mergingOverlappingIterator(myRangeMarkers, interval, myPersistentRangeMarkers, interval, RangeMarker.BY_START_OFFSET);
1101     try {
1102       return ContainerUtil.process(iterator, processor);
1103     }
1104     finally {
1105       iterator.dispose();
1106     }
1107   }
1108
1109   @NotNull
1110   public String dumpState() {
1111     @NonNls StringBuilder result = new StringBuilder();
1112     result.append(", intervals:\n");
1113     for (int line = 0; line < getLineCount(); line++) {
1114       result.append(line).append(": ").append(getLineStartOffset(line)).append("-")
1115         .append(getLineEndOffset(line)).append(", ");
1116     }
1117     if (result.length() > 0) {
1118       result.setLength(result.length() - 1);
1119     }
1120     return result.toString();
1121   }
1122
1123   @Override
1124   public String toString() {
1125     return "DocumentImpl[" + FileDocumentManager.getInstance().getFile(this) + "]";
1126   }
1127
1128   void requestTabTracking() {
1129     if (myAssertThreading) {
1130       ApplicationManager.getApplication().assertIsDispatchThread();
1131     }
1132     if (myTabTrackingRequestors++ == 0) {
1133       myMightContainTabs = false;
1134       updateMightContainTabs(myText);
1135     }
1136   }
1137
1138   void giveUpTabTracking() {
1139     if (myAssertThreading) {
1140       ApplicationManager.getApplication().assertIsDispatchThread();
1141     }
1142     if (--myTabTrackingRequestors == 0) {
1143       myMightContainTabs = true;
1144     }
1145   }
1146
1147   boolean mightContainTabs() {
1148     return myMightContainTabs;
1149   }
1150
1151   private void updateMightContainTabs(CharSequence text) {
1152     if (!myMightContainTabs) {
1153       myMightContainTabs = StringUtil.contains(text, 0, text.length(), '\t');
1154     }
1155   }
1156
1157   @NotNull
1158   public FrozenDocument freeze() {
1159     FrozenDocument frozen = myFrozen;
1160     if (frozen == null) {
1161       synchronized (myLineSetLock) {
1162         frozen = myFrozen;
1163         if (frozen == null) {
1164           frozen = new FrozenDocument(myText, myLineSet, myModificationStamp, SoftReference.dereference(myTextString));
1165         }
1166       }
1167     }
1168     return frozen;
1169   }
1170
1171   public void assertNotInBulkUpdate() {
1172     if (myDoingBulkUpdate) throw new UnexpectedBulkUpdateStateException(myBulkUpdateEnteringTrace);
1173   }
1174
1175   private static class UnexpectedBulkUpdateStateException extends RuntimeException implements ExceptionWithAttachments {
1176     private final Attachment[] myAttachments;
1177
1178     private UnexpectedBulkUpdateStateException(Throwable enteringTrace) {
1179       myAttachments = enteringTrace == null ? Attachment.EMPTY_ARRAY
1180                                             : new Attachment[] {new Attachment("enteringTrace.txt", enteringTrace)};
1181     }
1182
1183     @NotNull
1184     @Override
1185     public Attachment[] getAttachments() {
1186       return myAttachments;
1187     }
1188   }
1189 }