diff --git a/platform/core-api/src/com/intellij/openapi/editor/event/DocumentListener.java b/platform/core-api/src/com/intellij/openapi/editor/event/DocumentListener.java index a51d3432f312..18e23a005a05 100644 --- a/platform/core-api/src/com/intellij/openapi/editor/event/DocumentListener.java +++ b/platform/core-api/src/com/intellij/openapi/editor/event/DocumentListener.java @@ -15,6 +15,9 @@ */ package com.intellij.openapi.editor.event; +import com.intellij.util.ArrayFactory; +import org.jetbrains.annotations.NotNull; + import java.util.EventListener; /** @@ -27,6 +30,14 @@ import java.util.EventListener; @SuppressWarnings("JavadocReference") public interface DocumentListener extends EventListener{ DocumentListener[] EMPTY_ARRAY = new DocumentListener[0]; + ArrayFactory ARRAY_FACTORY = new ArrayFactory() { + @NotNull + @Override + public DocumentListener[] create(int count) { + return count == 0 ? EMPTY_ARRAY : new DocumentListener[count]; + } + }; + /** * Called before the text of the document is changed. * diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java index 6c39a076c3a6..aeecacd55203 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java @@ -60,8 +60,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class DocumentImpl extends UserDataHolderBase implements DocumentEx { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.DocumentImpl"); - private final Ref myCachedDocumentListeners = Ref.create(null); - private final List myDocumentListeners = ContainerUtil.createLockFreeCopyOnWriteList(); + private final LockFreeCOWSortedArray myDocumentListeners = new LockFreeCOWSortedArray(PrioritizedDocumentListener.COMPARATOR, DocumentListener.ARRAY_FACTORY); private final List myBulkDocumentInternalListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private final RangeMarkerTree myRangeMarkers = new RangeMarkerTree(this); private final RangeMarkerTree myPersistentRangeMarkers = new RangeMarkerTree(this); @@ -461,7 +460,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { @Override public int getListenersCount() { - return myDocumentListeners.size(); + return getListeners().length; } @Override @@ -533,7 +532,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { } private void fireMoveText(int start, int end, int newBase) { - for (DocumentListener listener : getCachedListeners()) { + for (DocumentListener listener : getListeners()) { if (listener instanceof PrioritizedInternalDocumentListener) { ((PrioritizedInternalDocumentListener)listener).moveTextHappened(start, end, newBase); } @@ -722,7 +721,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { myChangeInProgress = true; try { DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced, initialStartOffset, initialOldLength); - doBeforeChangedUpdate(event); + beforeChangedUpdate(event); myTextString = null; ImmutableCharSequence prevText = myText; myText = newText; @@ -739,7 +738,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { return sequence.get(); } - private void doBeforeChangedUpdate(DocumentEvent event) { + private void beforeChangedUpdate(DocumentEvent event) { Application app = ApplicationManager.getApplication(); if (app != null) { FileDocumentManager manager = FileDocumentManager.getInstance(); @@ -753,7 +752,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { getLineSet(); // initialize line set to track changed lines if (!ShutDownTracker.isShutdownHookRunning()) { - DocumentListener[] listeners = getCachedListeners(); + DocumentListener[] listeners = getListeners(); for (int i = listeners.length - 1; i >= 0; i--) { try { listeners[i].beforeDocumentChange(event); @@ -791,7 +790,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { setModificationStamp(newModificationStamp); if (!ShutDownTracker.isShutdownHookRunning()) { - DocumentListener[] listeners = getCachedListeners(); + DocumentListener[] listeners = getListeners(); for (DocumentListener listener : listeners) { try { listener.documentChanged(event); @@ -865,43 +864,29 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { @Override public void addDocumentListener(@NotNull DocumentListener listener) { - myCachedDocumentListeners.set(null); - - if (myDocumentListeners.contains(listener)) { + if (ArrayUtil.contains(listener, getListeners())) { LOG.error("Already registered: " + listener); } - boolean added = myDocumentListeners.add(listener); - LOG.assertTrue(added, listener); + myDocumentListeners.add(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 myCachedDocumentListenersRef; - private final List myDocumentListeners; - - private DocumentListenerDisposable(@NotNull DocumentListener listener, - @NotNull Ref cachedDocumentListenersRef, - @NotNull List documentListeners) { - myListener = listener; - myCachedDocumentListenersRef = cachedDocumentListenersRef; - myDocumentListeners = documentListeners; - } - - @Override - public void dispose() { - doRemoveDocumentListener(myListener, myCachedDocumentListenersRef, myDocumentListeners); - } + Disposer.register(parentDisposable, new Disposable() { + @Override + public void dispose() { + removeDocumentListener(listener); + } + }); } @Override public void removeDocumentListener(@NotNull DocumentListener listener) { - doRemoveDocumentListener(listener, myCachedDocumentListeners, myDocumentListeners); + boolean success = myDocumentListeners.remove(listener); + if (!success) { + LOG.error("Can't remove document listener (" + listener + "). Registered listeners: " + Arrays.toString(getListeners())); + } } void addInternalBulkModeListener(@NotNull DocumentBulkUpdateListener listener) { @@ -912,16 +897,6 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { myBulkDocumentInternalListeners.remove(listener); } - private static void doRemoveDocumentListener(@NotNull DocumentListener listener, - @NotNull Ref cachedDocumentListenersRef, - @NotNull List 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); @@ -962,16 +937,8 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { } @NotNull - private DocumentListener[] getCachedListeners() { - DocumentListener[] cachedListeners = myCachedDocumentListeners.get(); - if (cachedListeners == null) { - DocumentListener[] listeners = ArrayUtil.stripTrailingNulls(myDocumentListeners.toArray(new DocumentListener[myDocumentListeners.size()])); - Arrays.sort(listeners, PrioritizedDocumentListener.COMPARATOR); - cachedListeners = listeners; - myCachedDocumentListeners.set(cachedListeners); - } - - return cachedListeners; + private DocumentListener[] getListeners() { + return myDocumentListeners.getArray(); } @Override diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/LockFreeCOWSortedArray.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/LockFreeCOWSortedArray.java new file mode 100644 index 000000000000..8bca9886e08a --- /dev/null +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/LockFreeCOWSortedArray.java @@ -0,0 +1,78 @@ +/* + * Copyright 2000-2016 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.util.ArrayFactory; +import com.intellij.util.ArrayUtil; +import com.intellij.util.concurrency.AtomicFieldUpdater; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; + +/** + * Maintains an atomic immutable array of listeners of type {@code T} in sorted order according to {@link #comparator} + * N.B. internal array is exposed for faster iterating listeners in to- and reverse order, so care should be taken for not mutating it by clients + */ +class LockFreeCOWSortedArray { + @NotNull private final Comparator comparator; + private final ArrayFactory arrayFactory; + /** changed by {@link #UPDATER} only */ + @SuppressWarnings("FieldMayBeFinal") + @NotNull private volatile T[] listeners; + private static final AtomicFieldUpdater UPDATER = AtomicFieldUpdater.forFieldOfType(LockFreeCOWSortedArray.class, Object[].class); + + LockFreeCOWSortedArray(@NotNull Comparator comparator, @NotNull ArrayFactory arrayFactory) { + this.comparator = comparator; + this.arrayFactory = arrayFactory; + listeners = arrayFactory.create(0); + } + + // returns true if changed + void add(@NotNull T listener) { + while (true) { + T[] oldListeners = listeners; + int i = insertionIndex(oldListeners, listener); + T[] newListeners = ArrayUtil.insert(oldListeners, i, listener); + if (UPDATER.compareAndSet(this, oldListeners, newListeners)) break; + } + } + + boolean remove(@NotNull T listener) { + while (true) { + T[] oldListeners = listeners; + T[] newListeners = ArrayUtil.remove(oldListeners, listener, arrayFactory); + //noinspection ArrayEquality + if (oldListeners == newListeners) return false; + if (UPDATER.compareAndSet(this, oldListeners, newListeners)) break; + } + return true; + } + + private int insertionIndex(@NotNull T[] elements, @NotNull T e) { + for (int i=0; i