| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.openapi.editor.impl; |
| |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.actionSystem.DocCommandGroupId; |
| import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.event.DocumentListener; |
| import com.intellij.openapi.editor.ex.*; |
| import com.intellij.openapi.editor.impl.event.DocumentEventImpl; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.reference.SoftReference; |
| import com.intellij.util.DocumentUtil; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.LocalTimeCounter; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.IntArrayList; |
| import com.intellij.util.text.CharArrayUtil; |
| import com.intellij.util.text.ImmutableText; |
| import gnu.trove.TIntObjectHashMap; |
| import gnu.trove.TObjectProcedure; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.TestOnly; |
| |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeSupport; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| public class DocumentImpl extends UserDataHolderBase implements DocumentEx { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.DocumentImpl"); |
| |
| private final Ref<DocumentListener[]> myCachedDocumentListeners = Ref.create(null); |
| private final List<DocumentListener> myDocumentListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final RangeMarkerTree<RangeMarkerEx> myRangeMarkers = new RangeMarkerTree<RangeMarkerEx>(this); |
| private final List<RangeMarker> myGuardedBlocks = new ArrayList<RangeMarker>(); |
| private ReadonlyFragmentModificationHandler myReadonlyFragmentModificationHandler; |
| |
| private final Object myLineSetLock = new String("line set lock"); |
| private volatile LineSet myLineSet; |
| private volatile ImmutableText myText; |
| private volatile SoftReference<String> myTextString; |
| |
| private boolean myIsReadOnly = false; |
| private volatile boolean isStripTrailingSpacesEnabled = true; |
| private volatile long myModificationStamp; |
| private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); |
| |
| private final List<EditReadOnlyListener> myReadOnlyListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| |
| private volatile boolean myMightContainTabs = true; // optimisation flag: when document contains no tabs it is dramatically easier to calculate positions in editor |
| private int myTabTrackingRequestors = 0; |
| |
| private int myCheckGuardedBlocks = 0; |
| private boolean myGuardsSuppressed = false; |
| private boolean myEventsHandling = false; |
| private final boolean myAssertThreading; |
| private volatile boolean myDoingBulkUpdate = false; |
| private volatile boolean myAcceptSlashR = false; |
| private boolean myChangeInProgress; |
| private volatile int myBufferSize; |
| private final CharSequence myMutableCharSequence = new CharSequence() { |
| @Override |
| public int length() { |
| return myText.length(); |
| } |
| |
| @Override |
| public char charAt(int index) { |
| return myText.charAt(index); |
| } |
| |
| @Override |
| public CharSequence subSequence(int start, int end) { |
| return myText.subSequence(start, end); |
| } |
| |
| @NotNull |
| @Override |
| public String toString() { |
| return doGetText(); |
| } |
| }; |
| |
| public DocumentImpl(@NotNull String text) { |
| this(text, false); |
| } |
| |
| public DocumentImpl(@NotNull CharSequence chars) { |
| this(chars, false); |
| } |
| |
| public DocumentImpl(@NotNull CharSequence chars, boolean forUseInNonAWTThread) { |
| this(chars, false, forUseInNonAWTThread); |
| } |
| |
| public DocumentImpl(@NotNull CharSequence chars, boolean acceptSlashR, boolean forUseInNonAWTThread) { |
| setAcceptSlashR(acceptSlashR); |
| assertValidSeparators(chars); |
| myText = ImmutableText.valueOf(chars); |
| setCyclicBufferSize(0); |
| setModificationStamp(LocalTimeCounter.currentTime()); |
| myAssertThreading = !forUseInNonAWTThread; |
| } |
| |
| public boolean setAcceptSlashR(boolean accept) { |
| try { |
| return myAcceptSlashR; |
| } |
| finally { |
| myAcceptSlashR = accept; |
| } |
| } |
| |
| private LineSet getLineSet() { |
| LineSet lineSet = myLineSet; |
| if (lineSet == null) { |
| synchronized (myLineSetLock) { |
| lineSet = myLineSet; |
| if (lineSet == null) { |
| lineSet = new LineSet(); |
| lineSet.documentCreated(this); |
| myLineSet = lineSet; |
| } |
| } |
| } |
| |
| return lineSet; |
| } |
| |
| @Override |
| @NotNull |
| public char[] getChars() { |
| return CharArrayUtil.fromSequence(myText); |
| } |
| |
| @Override |
| public void setStripTrailingSpacesEnabled(boolean isEnabled) { |
| isStripTrailingSpacesEnabled = isEnabled; |
| } |
| |
| @TestOnly |
| public boolean stripTrailingSpaces(Project project) { |
| return stripTrailingSpaces(project, false, false, -1, -1); |
| } |
| |
| /** |
| * @return true if stripping was completed successfully, false if the document prevented stripping by e.g. caret being in the way |
| * |
| * @deprecated should be replaced with {@link #stripTrailingSpaces(com.intellij.openapi.project.Project, boolean, boolean, int[])} |
| * once multicaret logic will become unconditional (not controlled by configuration flag) |
| */ |
| boolean stripTrailingSpaces(@Nullable final Project project, |
| boolean inChangedLinesOnly, |
| boolean virtualSpaceEnabled, |
| int caretLine, |
| int caretOffset) { |
| if (!isStripTrailingSpacesEnabled) { |
| return true; |
| } |
| |
| boolean markAsNeedsStrippingLater = false; |
| CharSequence text = myText; |
| RangeMarker caretMarker = caretOffset < 0 || caretOffset > getTextLength() ? null : createRangeMarker(caretOffset, caretOffset); |
| try { |
| LineSet lineSet = getLineSet(); |
| for (int line = 0; line < lineSet.getLineCount(); line++) { |
| if (inChangedLinesOnly && !lineSet.isModified(line)) continue; |
| int whiteSpaceStart = -1; |
| final int lineEnd = lineSet.getLineEnd(line) - lineSet.getSeparatorLength(line); |
| int lineStart = lineSet.getLineStart(line); |
| for (int offset = lineEnd - 1; offset >= lineStart; offset--) { |
| char c = text.charAt(offset); |
| if (c != ' ' && c != '\t') { |
| break; |
| } |
| whiteSpaceStart = offset; |
| } |
| if (whiteSpaceStart == -1) continue; |
| if (!virtualSpaceEnabled && caretLine == line && caretMarker != null && |
| caretMarker.getStartOffset() >= 0 && whiteSpaceStart < caretMarker.getStartOffset()) { |
| // mark this as a document that needs stripping later |
| // otherwise the caret would jump madly |
| markAsNeedsStrippingLater = true; |
| } |
| else { |
| final int finalStart = whiteSpaceStart; |
| // document must be unblocked by now. If not, some Save handler attempted to modify PSI |
| // which should have been caught by assertion in com.intellij.pom.core.impl.PomModelImpl.runTransaction |
| DocumentUtil.writeInRunUndoTransparentAction(new DocumentRunnable(DocumentImpl.this, project) { |
| @Override |
| public void run() { |
| deleteString(finalStart, lineEnd); |
| } |
| }); |
| text = myText; |
| } |
| } |
| } |
| finally { |
| if (caretMarker != null) caretMarker.dispose(); |
| } |
| return markAsNeedsStrippingLater; |
| } |
| |
| /** |
| * @return true if stripping was completed successfully, false if the document prevented stripping by e.g. caret(s) being in the way |
| */ |
| boolean stripTrailingSpaces(@Nullable final Project project, |
| boolean inChangedLinesOnly, |
| boolean virtualSpaceEnabled, |
| @NotNull int[] caretOffsets) { |
| if (!isStripTrailingSpacesEnabled) { |
| return true; |
| } |
| |
| boolean markAsNeedsStrippingLater = false; |
| CharSequence text = myText; |
| TIntObjectHashMap<List<RangeMarker>> caretMarkers = new TIntObjectHashMap<List<RangeMarker>>(caretOffsets.length); |
| try { |
| if (!virtualSpaceEnabled) { |
| for (int caretOffset : caretOffsets) { |
| if (caretOffset < 0 || caretOffset > getTextLength()) { |
| continue; |
| } |
| int line = getLineNumber(caretOffset); |
| List<RangeMarker> markers = caretMarkers.get(line); |
| if (markers == null) { |
| markers = new ArrayList<RangeMarker>(); |
| caretMarkers.put(line, markers); |
| } |
| RangeMarker marker = createRangeMarker(caretOffset, caretOffset); |
| markers.add(marker); |
| } |
| } |
| LineSet lineSet = getLineSet(); |
| lineLoop: |
| for (int line = 0; line < lineSet.getLineCount(); line++) { |
| if (inChangedLinesOnly && !lineSet.isModified(line)) continue; |
| int whiteSpaceStart = -1; |
| final int lineEnd = lineSet.getLineEnd(line) - lineSet.getSeparatorLength(line); |
| int lineStart = lineSet.getLineStart(line); |
| for (int offset = lineEnd - 1; offset >= lineStart; offset--) { |
| char c = text.charAt(offset); |
| if (c != ' ' && c != '\t') { |
| break; |
| } |
| whiteSpaceStart = offset; |
| } |
| if (whiteSpaceStart == -1) continue; |
| if (!virtualSpaceEnabled) { |
| List<RangeMarker> markers = caretMarkers.get(line); |
| if (markers != null) { |
| for (RangeMarker marker : markers) { |
| if (marker.getStartOffset() >= 0 && whiteSpaceStart < marker.getStartOffset()) { |
| // mark this as a document that needs stripping later |
| // otherwise the caret would jump madly |
| markAsNeedsStrippingLater = true; |
| continue lineLoop; |
| } |
| } |
| } |
| } |
| final int finalStart = whiteSpaceStart; |
| // document must be unblocked by now. If not, some Save handler attempted to modify PSI |
| // which should have been caught by assertion in com.intellij.pom.core.impl.PomModelImpl.runTransaction |
| DocumentUtil.writeInRunUndoTransparentAction(new DocumentRunnable(DocumentImpl.this, project) { |
| @Override |
| public void run() { |
| deleteString(finalStart, lineEnd); |
| } |
| }); |
| text = myText; |
| } |
| } |
| finally { |
| caretMarkers.forEachValue(new TObjectProcedure<List<RangeMarker>>() { |
| @Override |
| public boolean execute(List<RangeMarker> markerList) { |
| if (markerList != null) { |
| for (RangeMarker marker : markerList) { |
| try { |
| marker.dispose(); |
| } |
| catch (Exception e) { |
| LOG.error(e); |
| } |
| } |
| } |
| return true; |
| } |
| }); |
| } |
| return markAsNeedsStrippingLater; |
| } |
| |
| @Override |
| public void setReadOnly(boolean isReadOnly) { |
| if (myIsReadOnly != isReadOnly) { |
| myIsReadOnly = isReadOnly; |
| myPropertyChangeSupport.firePropertyChange(Document.PROP_WRITABLE, !isReadOnly, isReadOnly); |
| } |
| } |
| |
| public ReadonlyFragmentModificationHandler getReadonlyFragmentModificationHandler() { |
| return myReadonlyFragmentModificationHandler; |
| } |
| |
| public void setReadonlyFragmentModificationHandler(final ReadonlyFragmentModificationHandler readonlyFragmentModificationHandler) { |
| myReadonlyFragmentModificationHandler = readonlyFragmentModificationHandler; |
| } |
| |
| @Override |
| public boolean isWritable() { |
| return !myIsReadOnly; |
| } |
| |
| @Override |
| public boolean removeRangeMarker(@NotNull RangeMarkerEx rangeMarker) { |
| return myRangeMarkers.removeInterval(rangeMarker); |
| } |
| |
| @Override |
| public void registerRangeMarker(@NotNull RangeMarkerEx rangeMarker, |
| int start, |
| int end, |
| boolean greedyToLeft, |
| boolean greedyToRight, |
| int layer) { |
| myRangeMarkers.addInterval(rangeMarker, start, end, greedyToLeft, greedyToRight, layer); |
| } |
| |
| @TestOnly |
| public int getRangeMarkersSize() { |
| return myRangeMarkers.size(); |
| } |
| |
| @TestOnly |
| public int getRangeMarkersNodeSize() { |
| return myRangeMarkers.nodeSize(); |
| } |
| |
| @Override |
| @NotNull |
| public RangeMarker createGuardedBlock(int startOffset, int endOffset) { |
| LOG.assertTrue(startOffset <= endOffset, "Should be startOffset <= endOffset"); |
| RangeMarker block = createRangeMarker(startOffset, endOffset, true); |
| myGuardedBlocks.add(block); |
| return block; |
| } |
| |
| @Override |
| public void removeGuardedBlock(@NotNull RangeMarker block) { |
| myGuardedBlocks.remove(block); |
| } |
| |
| @Override |
| @NotNull |
| public List<RangeMarker> getGuardedBlocks() { |
| return myGuardedBlocks; |
| } |
| |
| @Override |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) // Way too many garbage is produced otherwise in AbstractList.iterator() |
| public RangeMarker getOffsetGuard(int offset) { |
| for (int i = 0; i < myGuardedBlocks.size(); i++) { |
| RangeMarker block = myGuardedBlocks.get(i); |
| if (offsetInRange(offset, block.getStartOffset(), block.getEndOffset())) return block; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public RangeMarker getRangeGuard(int start, int end) { |
| for (RangeMarker block : myGuardedBlocks) { |
| if (rangesIntersect(start, true, block.getStartOffset(), block.isGreedyToLeft(), end, true, block.getEndOffset(), |
| block.isGreedyToRight())) { |
| return block; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public void startGuardedBlockChecking() { |
| myCheckGuardedBlocks++; |
| } |
| |
| @Override |
| public void stopGuardedBlockChecking() { |
| LOG.assertTrue(myCheckGuardedBlocks > 0, "Unpaired start/stopGuardedBlockChecking"); |
| myCheckGuardedBlocks--; |
| } |
| |
| private static boolean offsetInRange(int offset, int start, int end) { |
| return start <= offset && offset < end; |
| } |
| |
| private static boolean rangesIntersect(int start0, boolean leftInclusive0, |
| int start1, boolean leftInclusive1, |
| int end0, boolean rightInclusive0, |
| int end1, boolean rightInclusive1) { |
| if (start0 > start1 || start0 == start1 && !leftInclusive0) { |
| return rangesIntersect(start1, leftInclusive1, start0, leftInclusive0, end1, rightInclusive1, end0, rightInclusive0); |
| } |
| if (end0 == start1) return leftInclusive1 && rightInclusive0; |
| return end0 > start1; |
| } |
| |
| @Override |
| @NotNull |
| public RangeMarker createRangeMarker(int startOffset, int endOffset) { |
| return createRangeMarker(startOffset, endOffset, false); |
| } |
| |
| @Override |
| @NotNull |
| public RangeMarker createRangeMarker(int startOffset, int endOffset, boolean surviveOnExternalChange) { |
| if (!(0 <= startOffset && startOffset <= endOffset && endOffset <= getTextLength())) { |
| LOG.error("Incorrect offsets: startOffset=" + startOffset + ", endOffset=" + endOffset + ", text length=" + getTextLength()); |
| } |
| return surviveOnExternalChange |
| ? new PersistentRangeMarker(this, startOffset, endOffset, true) |
| : new RangeMarkerImpl(this, startOffset, endOffset, true); |
| } |
| |
| @Override |
| public long getModificationStamp() { |
| return myModificationStamp; |
| } |
| |
| @Override |
| public void setModificationStamp(long modificationStamp) { |
| myModificationStamp = modificationStamp; |
| } |
| |
| @Override |
| public void replaceText(@NotNull CharSequence chars, long newModificationStamp) { |
| replaceString(0, getTextLength(), chars, newModificationStamp, true); //TODO: optimization!!! |
| clearLineModificationFlags(); |
| } |
| |
| @Override |
| public int getListenersCount() { |
| return myDocumentListeners.size(); |
| } |
| |
| @Override |
| public void insertString(int offset, @NotNull CharSequence s) { |
| if (offset < 0) throw new IndexOutOfBoundsException("Wrong offset: " + offset); |
| if (offset > getTextLength()) { |
| throw new IndexOutOfBoundsException( |
| "Wrong offset: " + offset + "; documentLength: " + getTextLength() + "; " + s.subSequence(Math.max(0, s.length() - 20), s.length()) |
| ); |
| } |
| assertWriteAccess(); |
| assertValidSeparators(s); |
| |
| if (!isWritable()) throw new ReadOnlyModificationException(this); |
| if (s.length() == 0) return; |
| |
| RangeMarker marker = getRangeGuard(offset, offset); |
| if (marker != null) { |
| throwGuardedFragment(marker, offset, null, s.toString()); |
| } |
| |
| updateText(myText.insert(offset, ImmutableText.valueOf(s)), offset, null, s, false, LocalTimeCounter.currentTime()); |
| trimToSize(); |
| } |
| |
| private void trimToSize() { |
| if (myBufferSize != 0 && getTextLength() > myBufferSize) { |
| deleteString(0, getTextLength() - myBufferSize); |
| } |
| } |
| |
| @Override |
| public void deleteString(int startOffset, int endOffset) { |
| assertBounds(startOffset, endOffset); |
| |
| assertWriteAccess(); |
| if (!isWritable()) throw new ReadOnlyModificationException(this); |
| if (startOffset == endOffset) return; |
| |
| CharSequence sToDelete = myText.subSequence(startOffset, endOffset); |
| |
| RangeMarker marker = getRangeGuard(startOffset, endOffset); |
| if (marker != null) { |
| throwGuardedFragment(marker, startOffset, sToDelete.toString(), null); |
| } |
| |
| updateText(myText.delete(startOffset, endOffset), startOffset, sToDelete, null, false, LocalTimeCounter.currentTime()); |
| } |
| |
| @Override |
| public void moveText(int srcStart, int srcEnd, int dstOffset) { |
| assertBounds(srcStart, srcEnd); |
| if (dstOffset == srcEnd) return; |
| ProperTextRange srcRange = new ProperTextRange(srcStart, srcEnd); |
| assert !srcRange.containsOffset(dstOffset) : "Can't perform text move from range [" +srcStart+ "; " + srcEnd+ ") to offset "+dstOffset; |
| |
| String replacement = getCharsSequence().subSequence(srcStart, srcEnd).toString(); |
| |
| insertString(dstOffset, replacement); |
| int shift = 0; |
| if (dstOffset < srcStart) { |
| shift = srcEnd - srcStart; |
| } |
| fireMoveText(srcStart + shift, srcEnd + shift, dstOffset); |
| |
| deleteString(srcStart + shift, srcEnd + shift); |
| } |
| |
| private void fireMoveText(int start, int end, int newBase) { |
| for (DocumentListener listener : getCachedListeners()) { |
| if (listener instanceof PrioritizedInternalDocumentListener) { |
| ((PrioritizedInternalDocumentListener)listener).moveTextHappened(start, end, newBase); |
| } |
| } |
| } |
| |
| @Override |
| public void replaceString(int startOffset, int endOffset, @NotNull CharSequence s) { |
| replaceString(startOffset, endOffset, s, LocalTimeCounter.currentTime(), startOffset == 0 && endOffset == getTextLength()); |
| } |
| |
| private void replaceString(int startOffset, int endOffset, @NotNull CharSequence s, final long newModificationStamp, boolean wholeTextReplaced) { |
| assertBounds(startOffset, endOffset); |
| |
| assertWriteAccess(); |
| assertValidSeparators(s); |
| |
| if (!isWritable()) { |
| throw new ReadOnlyModificationException(this); |
| } |
| |
| final int newStringLength = s.length(); |
| final CharSequence chars = getCharsSequence(); |
| int newStartInString = 0; |
| int newEndInString = newStringLength; |
| while (newStartInString < newStringLength && |
| startOffset < endOffset && |
| s.charAt(newStartInString) == chars.charAt(startOffset)) { |
| startOffset++; |
| newStartInString++; |
| } |
| |
| while (endOffset > startOffset && |
| newEndInString > newStartInString && |
| s.charAt(newEndInString - 1) == chars.charAt(endOffset - 1)) { |
| newEndInString--; |
| endOffset--; |
| } |
| |
| CharSequence changedPart = s.subSequence(newStartInString, newEndInString); |
| CharSequence sToDelete = myText.subSequence(startOffset, endOffset); |
| RangeMarker guard = getRangeGuard(startOffset, endOffset); |
| if (guard != null) { |
| throwGuardedFragment(guard, startOffset, sToDelete.toString(), changedPart.toString()); |
| } |
| |
| ImmutableText newText; |
| if (wholeTextReplaced && s instanceof ImmutableText) { |
| newText = (ImmutableText)s; |
| } |
| else { |
| newText = myText.delete(startOffset, endOffset).insert(startOffset, changedPart); |
| } |
| updateText(newText, startOffset, sToDelete, changedPart, wholeTextReplaced, newModificationStamp); |
| trimToSize(); |
| } |
| |
| private void assertBounds(final int startOffset, final int endOffset) { |
| if (startOffset < 0 || startOffset > getTextLength()) { |
| throw new IndexOutOfBoundsException("Wrong startOffset: " + startOffset + "; documentLength: " + getTextLength()); |
| } |
| if (endOffset < 0 || endOffset > getTextLength()) { |
| throw new IndexOutOfBoundsException("Wrong endOffset: " + endOffset + "; documentLength: " + getTextLength()); |
| } |
| if (endOffset < startOffset) { |
| throw new IllegalArgumentException( |
| "endOffset < startOffset: " + endOffset + " < " + startOffset + "; documentLength: " + getTextLength()); |
| } |
| } |
| |
| private void assertWriteAccess() { |
| if (myAssertThreading) { |
| final Application application = ApplicationManager.getApplication(); |
| if (application != null) { |
| application.assertWriteAccessAllowed(); |
| } |
| } |
| } |
| |
| private void assertValidSeparators(@NotNull CharSequence s) { |
| if (myAcceptSlashR) return; |
| StringUtil.assertValidSeparators(s); |
| } |
| |
| /** |
| * All document change actions follows the algorithm below: |
| * <pre> |
| * <ol> |
| * <li> |
| * All {@link #addDocumentListener(com.intellij.openapi.editor.event.DocumentListener) registered listeners} are notified |
| * {@link com.intellij.openapi.editor.event.DocumentListener#beforeDocumentChange(com.intellij.openapi.editor.event.DocumentEvent) before the change}; |
| * </li> |
| * <li>The change is performed </li> |
| * <li> |
| * All {@link #addDocumentListener(com.intellij.openapi.editor.event.DocumentListener) registered listeners} are notified |
| * {@link com.intellij.openapi.editor.event.DocumentListener#documentChanged(com.intellij.openapi.editor.event.DocumentEvent) after the change}; |
| * </li> |
| * </ol> |
| * </pre> |
| * <p/> |
| * There is a possible case that <code>'before change'</code> notification produces new change. We have a problem then - imagine |
| * that initial change was <code>'replace particular range at document end'</code> and <code>'nested change'</code> was to |
| * <code>'remove text at document end'</code>. That means that when initial change will be actually performed, the document may be |
| * not long enough to contain target range. |
| * <p/> |
| * Current method allows to check if document change is a <code>'nested call'</code>. |
| * |
| * @throws IllegalStateException if this method is called during a <code>'nested document modification'</code> |
| */ |
| private void assertNotNestedModification() throws IllegalStateException { |
| if (myChangeInProgress) { |
| throw new IllegalStateException("Detected nested request for document modification from 'before change' callback!"); |
| } |
| } |
| |
| private void throwGuardedFragment(@NotNull RangeMarker guard, int offset, String oldString, String newString) { |
| if (myCheckGuardedBlocks > 0 && !myGuardsSuppressed) { |
| DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, false); |
| throw new ReadOnlyFragmentModificationException(event, guard); |
| } |
| } |
| |
| @Override |
| public void suppressGuardedExceptions() { |
| myGuardsSuppressed = true; |
| } |
| |
| @Override |
| public void unSuppressGuardedExceptions() { |
| myGuardsSuppressed = false; |
| } |
| |
| @Override |
| public boolean isInEventsHandling() { |
| return myEventsHandling; |
| } |
| |
| @Override |
| public void clearLineModificationFlags() { |
| getLineSet().clearModificationFlags(); |
| } |
| |
| public void clearLineModificationFlagsExcept(@NotNull int[] caretLines) { |
| IntArrayList modifiedLines = new IntArrayList(caretLines.length); |
| LineSet lineSet = getLineSet(); |
| for (int line : caretLines) { |
| if (line >= 0 && line < lineSet.getLineCount() && lineSet.isModified(line)) { |
| modifiedLines.add(line); |
| } |
| } |
| clearLineModificationFlags(); |
| for (int i = 0; i < modifiedLines.size(); i++) { |
| lineSet.setModified(modifiedLines.get(i)); |
| } |
| } |
| |
| private void updateText(@NotNull ImmutableText newText, |
| int offset, |
| @Nullable CharSequence oldString, |
| @Nullable CharSequence newString, |
| boolean wholeTextReplaced, |
| long newModificationStamp) { |
| assertNotNestedModification(); |
| myChangeInProgress = true; |
| final DocumentEvent event; |
| try { |
| event = doBeforeChangedUpdate(offset, oldString, newString, wholeTextReplaced); |
| } |
| finally { |
| myChangeInProgress = false; |
| } |
| myTextString = null; |
| myText = newText; |
| changedUpdate(event, newModificationStamp); |
| } |
| |
| @NotNull |
| private DocumentEvent doBeforeChangedUpdate(int offset, CharSequence oldString, CharSequence newString, boolean wholeTextReplaced) { |
| Application app = ApplicationManager.getApplication(); |
| if (app != null) { |
| FileDocumentManager manager = FileDocumentManager.getInstance(); |
| if (manager != null) { |
| VirtualFile file = manager.getFile(this); |
| if (file != null && !file.isValid()) { |
| LOG.error("File of this document has been deleted."); |
| } |
| } |
| } |
| assertInsideCommand(); |
| |
| getLineSet(); // initialize line set to track changed lines |
| |
| DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced); |
| |
| if (!ShutDownTracker.isShutdownHookRunning()) { |
| DocumentListener[] listeners = getCachedListeners(); |
| for (int i = listeners.length - 1; i >= 0; i--) { |
| try { |
| listeners[i].beforeDocumentChange(event); |
| } |
| catch (Throwable e) { |
| LOG.error(e); |
| } |
| } |
| } |
| |
| myEventsHandling = true; |
| return event; |
| } |
| |
| private void assertInsideCommand() { |
| CommandProcessor commandProcessor = CommandProcessor.getInstance(); |
| if (!commandProcessor.isUndoTransparentActionInProgress() && |
| commandProcessor.getCurrentCommand() == null && |
| myAssertThreading) { |
| 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"); |
| } |
| } |
| |
| private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp) { |
| try { |
| if (LOG.isDebugEnabled()) LOG.debug(event.toString()); |
| |
| getLineSet().changedUpdate(event); |
| if (myTabTrackingRequestors > 0) { |
| updateMightContainTabs(event.getNewFragment()); |
| } |
| setModificationStamp(newModificationStamp); |
| |
| if (!ShutDownTracker.isShutdownHookRunning()) { |
| DocumentListener[] listeners = getCachedListeners(); |
| for (DocumentListener listener : listeners) { |
| try { |
| listener.documentChanged(event); |
| } |
| catch (Throwable e) { |
| LOG.error(e); |
| } |
| } |
| } |
| } |
| finally { |
| myEventsHandling = false; |
| } |
| } |
| |
| @NotNull |
| @Override |
| public String getText() { |
| return ApplicationManager.getApplication().runReadAction(new Computable<String>() { |
| @Override |
| public String compute() { |
| return doGetText(); |
| } |
| }); |
| } |
| |
| @NotNull |
| private String doGetText() { |
| String s = SoftReference.dereference(myTextString); |
| if (s == null) { |
| myTextString = new SoftReference<String>(s = myText.toString()); |
| } |
| return s; |
| } |
| |
| @NotNull |
| @Override |
| public String getText(@NotNull final TextRange range) { |
| return ApplicationManager.getApplication().runReadAction(new Computable<String>() { |
| @Override |
| public String compute() { |
| return myText.subSequence(range.getStartOffset(), range.getEndOffset()).toString(); |
| } |
| }); |
| } |
| |
| @Override |
| public int getTextLength() { |
| return myText.length(); |
| } |
| |
| @Override |
| @NotNull |
| public CharSequence getCharsSequence() { |
| return myMutableCharSequence; |
| } |
| |
| @NotNull |
| @Override |
| public CharSequence getImmutableCharSequence() { |
| return myText; |
| } |
| |
| |
| @Override |
| public void addDocumentListener(@NotNull DocumentListener listener) { |
| myCachedDocumentListeners.set(null); |
| |
| if (myDocumentListeners.contains(listener)) { |
| LOG.error("Already registered: " + listener); |
| } |
| boolean added = myDocumentListeners.add(listener); |
| LOG.assertTrue(added, listener); |
| } |
| |
| @Override |
| public void addDocumentListener(@NotNull final DocumentListener listener, @NotNull Disposable parentDisposable) { |
| addDocumentListener(listener); |
| Disposer.register(parentDisposable, new DocumentListenerDisposable(listener, myCachedDocumentListeners, myDocumentListeners)); |
| } |
| |
| private static class DocumentListenerDisposable implements Disposable { |
| private final DocumentListener myListener; |
| private final Ref<DocumentListener[]> myCachedDocumentListenersRef; |
| private final List<DocumentListener> myDocumentListeners; |
| |
| public DocumentListenerDisposable(@NotNull DocumentListener listener, |
| @NotNull Ref<DocumentListener[]> cachedDocumentListenersRef, |
| @NotNull List<DocumentListener> documentListeners) { |
| myListener = listener; |
| myCachedDocumentListenersRef = cachedDocumentListenersRef; |
| myDocumentListeners = documentListeners; |
| } |
| |
| @Override |
| public void dispose() { |
| doRemoveDocumentListener(myListener, myCachedDocumentListenersRef, myDocumentListeners); |
| } |
| } |
| |
| @Override |
| public void removeDocumentListener(@NotNull DocumentListener listener) { |
| doRemoveDocumentListener(listener, myCachedDocumentListeners, myDocumentListeners); |
| } |
| |
| private static void doRemoveDocumentListener(@NotNull DocumentListener listener, |
| @NotNull Ref<DocumentListener[]> cachedDocumentListenersRef, |
| @NotNull List<DocumentListener> documentListeners) { |
| cachedDocumentListenersRef.set(null); |
| boolean success = documentListeners.remove(listener); |
| if (!success) { |
| LOG.error("Can't remove document listener (" + listener + "). Registered listeners: " + documentListeners); |
| } |
| } |
| |
| @Override |
| public int getLineNumber(final int offset) { |
| return getLineSet().findLineIndex(offset); |
| } |
| |
| @Override |
| @NotNull |
| public LineIterator createLineIterator() { |
| return getLineSet().createIterator(); |
| } |
| |
| @Override |
| public final int getLineStartOffset(final int line) { |
| if (line == 0) return 0; // otherwise it crashed for zero-length document |
| return getLineSet().getLineStart(line); |
| } |
| |
| @Override |
| public final int getLineEndOffset(int line) { |
| if (getTextLength() == 0 && line == 0) return 0; |
| int result = getLineSet().getLineEnd(line) - getLineSeparatorLength(line); |
| assert result >= 0; |
| return result; |
| } |
| |
| @Override |
| public final int getLineSeparatorLength(int line) { |
| int separatorLength = getLineSet().getSeparatorLength(line); |
| assert separatorLength >= 0; |
| return separatorLength; |
| } |
| |
| @Override |
| public final int getLineCount() { |
| int lineCount = getLineSet().getLineCount(); |
| assert lineCount >= 0; |
| return lineCount; |
| } |
| |
| @NotNull |
| private DocumentListener[] getCachedListeners() { |
| DocumentListener[] cachedListeners = myCachedDocumentListeners.get(); |
| if (cachedListeners == null) { |
| DocumentListener[] listeners = myDocumentListeners.toArray(new DocumentListener[myDocumentListeners.size()]); |
| Arrays.sort(listeners, PrioritizedDocumentListener.COMPARATOR); |
| cachedListeners = listeners; |
| myCachedDocumentListeners.set(cachedListeners); |
| } |
| |
| return cachedListeners; |
| } |
| |
| @Override |
| public void fireReadOnlyModificationAttempt() { |
| for (EditReadOnlyListener listener : myReadOnlyListeners) { |
| listener.readOnlyModificationAttempt(this); |
| } |
| } |
| |
| @Override |
| public void addEditReadOnlyListener(@NotNull EditReadOnlyListener listener) { |
| myReadOnlyListeners.add(listener); |
| } |
| |
| @Override |
| public void removeEditReadOnlyListener(@NotNull EditReadOnlyListener listener) { |
| myReadOnlyListeners.remove(listener); |
| } |
| |
| |
| @Override |
| public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { |
| myPropertyChangeSupport.addPropertyChangeListener(listener); |
| } |
| |
| @Override |
| public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { |
| myPropertyChangeSupport.removePropertyChangeListener(listener); |
| } |
| |
| @Override |
| public void setCyclicBufferSize(int bufferSize) { |
| assert bufferSize >= 0 : bufferSize; |
| myBufferSize = bufferSize; |
| } |
| |
| @Override |
| public void setText(@NotNull final CharSequence text) { |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| replaceString(0, getTextLength(), text, LocalTimeCounter.currentTime(), true); |
| } |
| }; |
| if (CommandProcessor.getInstance().isUndoTransparentActionInProgress()) { |
| runnable.run(); |
| } |
| else { |
| CommandProcessor.getInstance().executeCommand(null, runnable, "", DocCommandGroupId.noneGroupId(this)); |
| } |
| |
| clearLineModificationFlags(); |
| } |
| |
| @Override |
| @NotNull |
| public RangeMarker createRangeMarker(@NotNull final TextRange textRange) { |
| return createRangeMarker(textRange.getStartOffset(), textRange.getEndOffset()); |
| } |
| |
| @Override |
| public final boolean isInBulkUpdate() { |
| return myDoingBulkUpdate; |
| } |
| |
| @Override |
| public final void setInBulkUpdate(boolean value) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| if (myDoingBulkUpdate == value) { |
| // do not fire listeners or otherwise updateStarted() will be called more times than updateFinished() |
| return; |
| } |
| myDoingBulkUpdate = value; |
| if (value) { |
| getPublisher().updateStarted(this); |
| } |
| else { |
| getPublisher().updateFinished(this); |
| } |
| } |
| |
| private static class DocumentBulkUpdateListenerHolder { |
| private static final DocumentBulkUpdateListener ourBulkChangePublisher = |
| ApplicationManager.getApplication().getMessageBus().syncPublisher(DocumentBulkUpdateListener.TOPIC); |
| } |
| |
| @NotNull |
| private static DocumentBulkUpdateListener getPublisher() { |
| return DocumentBulkUpdateListenerHolder.ourBulkChangePublisher; |
| } |
| |
| @Override |
| public boolean processRangeMarkers(@NotNull Processor<RangeMarker> processor) { |
| return myRangeMarkers.process(processor); |
| } |
| |
| @Override |
| public boolean processRangeMarkersOverlappingWith(int start, int end, @NotNull Processor<RangeMarker> processor) { |
| return myRangeMarkers.processOverlappingWith(start, end, processor); |
| } |
| |
| @NotNull |
| public String dumpState() { |
| @NonNls StringBuilder result = new StringBuilder(); |
| result.append(", intervals:\n"); |
| for (int line = 0; line < getLineCount(); line++) { |
| result.append(line).append(": ").append(getLineStartOffset(line)).append("-") |
| .append(getLineEndOffset(line)).append(", "); |
| } |
| if (result.length() > 0) { |
| result.setLength(result.length() - 1); |
| } |
| return result.toString(); |
| } |
| |
| @Override |
| public String toString() { |
| return "DocumentImpl[" + FileDocumentManager.getInstance().getFile(this) + "]"; |
| } |
| |
| public void requestTabTracking() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| if (myTabTrackingRequestors++ == 0) { |
| myMightContainTabs = false; |
| updateMightContainTabs(myText); |
| } |
| } |
| |
| public void giveUpTabTracking() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| if (--myTabTrackingRequestors == 0) { |
| myMightContainTabs = true; |
| } |
| } |
| |
| public boolean mightContainTabs() { |
| return myMightContainTabs; |
| } |
| |
| private void updateMightContainTabs(CharSequence text) { |
| if (!myMightContainTabs) { |
| myMightContainTabs = StringUtil.contains(text, 0, text.length(), '\t'); |
| } |
| } |
| } |