diff --git a/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java b/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java index 5027cba4fc4b..e4a3bf7723aa 100644 --- a/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java +++ b/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java @@ -18,9 +18,8 @@ package com.intellij.openapi.application; import com.intellij.openapi.Disposable; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.progress.ProcessCanceledException; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.Ref; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * A service managing model transactions.

@@ -35,8 +34,8 @@ import org.jetbrains.annotations.NotNull; * should be performed inside a transaction. No write action should be performed from within an {@code invokeLater}-like call * unless wrapped into a transaction.

* - * The recommended way to perform a transaction is to invoke {@link #submitTransaction(Runnable)}. It either runs the transaction immediately - * (if on UI thread and there's no other transaction running) or queues it to invoke at some later moment, when it becomes possible.

+ * The recommended way to perform a transaction is to invoke {@link #submitTransaction(Disposable, Runnable)}. It either runs the transaction immediately + * (if on UI thread and not inside invokeLater) or queues it to invoke at some later moment, when it becomes possible.

* * Sometimes transactions need to be processed immediately, even if another transaction is already running. Example: * outer transaction has shown a dialog with an editor, and typing into that editor (which requires a transaction for changing document) @@ -71,21 +70,7 @@ import org.jetbrains.annotations.NotNull; * * Q: I've got "Write access is allowed from model transactions only" exception, what do I do?
* A: You're likely inside an "invokeLater"-like call. Please consider replacing it with {@link #submitTransaction(Disposable, Runnable)} or - * {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)} - *

- * - * Q: I've got "Nested transactions are not allowed" exception, what do I do?
- * A: First, identify the place in the stack where the outer transaction is started (the exception attachment should contain it). - * Then, see if there is any Swing event pumping - * in between two transactions (e.g. a dialog is shown). If not, one of the two involved transactions is superfluous, remove it. If there - * is event pumping present, check if the client code (e.g. the one showing the dialog) is prepared to such nested model modifications. - * For example, refactoring dialogs might be prepared to {@link TransactionKind#TEXT_EDITING} kind - * (for text field editing inside the dialogs) but not to - * other model changes, e.g. root changes. The outer transaction code might then specify which kinds it's prepared to (by using - * {@link #acceptNestedTransactions(TransactionKind...)}), and the inner transaction code should have the very same transaction kind - * (by using {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)} or {@link #startSynchronousTransaction(TransactionKind)}). - * If the nested transaction is not expected by the outer code, it must be made asynchronous by using either {@link #submitTransaction(Disposable, Runnable)} - * or {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}. + * {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)} *

* * Q: What's the difference between transactions and read/write actions and commands ({@link com.intellij.openapi.command.CommandProcessor})?
@@ -95,7 +80,7 @@ import org.jetbrains.annotations.NotNull; * * @see Application#runReadAction(Runnable) * @see Application#runWriteAction(Runnable) - * @since 146.* + * @since 2016.2 * @author peter */ public abstract class TransactionGuard { @@ -112,11 +97,11 @@ public abstract class TransactionGuard { } /** - * Ensures that some code will be run in a transaction. It's guaranteed that no other transactions are run at the same time. - * The code will be run on Swing thread immediately or after all other queued transactions (if any) have been completed.

+ * Ensures that some code will be run in a transaction. It's guaranteed that no other transactions can run at the same time, + * except for the ones started from within this runnable. The code will be run on Swing thread immediately + * or after other queued transactions (if any) have been completed.

* - * For more advanced version, see {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)} - * Transactions submitted via this method use {@link TransactionKind#ANY_CHANGE} kind. + * For more advanced version, see {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)}. * * @param parentDisposable an object whose disposing (via {@link com.intellij.openapi.util.Disposer} makes this transaction invalid, * and so it won't be run after it has been disposed @@ -153,20 +138,6 @@ public abstract class TransactionGuard { */ public abstract void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull Runnable transaction) throws ProcessCanceledException; - /** - * Same as {@link #submitTransactionAndWait(TransactionKind, Runnable)}, but returns a value computed by the transaction. - */ - public T submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Computable transaction) throws ProcessCanceledException { - final Ref result = Ref.create(); - submitTransactionAndWait(kind, new Runnable() { - @Override - public void run() { - result.set(transaction.compute()); - } - }); - return result.get(); - } - /** * A synchronous version of {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}. * @return a token object for this transaction. Call {@link AccessToken#finish()} (inside finally) when the transaction is complete. @@ -194,6 +165,19 @@ public abstract class TransactionGuard { */ public abstract void submitMergeableTransaction(@NotNull Disposable parentDisposable, @NotNull TransactionKind kind, @NotNull Runnable transaction); + /** + * Executes the given runnable inside a transaction as soon as possible on the UI thread. The runnable is executed either when there's + * no active transaction running, or when the running transaction has the same (or compatible) id as {@code mergeInto}. If the id of + * the current transaction is passed, the transaction is executed immediately. Otherwise adds the runnable to a queue, + * to execute after all transactions scheduled before this one are finished. + * @param parentDisposable an object whose disposing (via {@link com.intellij.openapi.util.Disposer} makes this transaction invalid, + * and so it won't be run after it has been disposed. + * @param mergeInto an optional id of another transaction, to allow execution inside that transaction if it's still running + * @param transaction code to execute inside a transaction. + * @see #getCurrentMergeableTransaction() + */ + public abstract void submitMergeableTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId mergeInto, @NotNull Runnable transaction); + /** * Allow incoming transactions of the specified kinds to be executed immediately, instead of being queued until the current transaction is finished.

* @@ -215,4 +199,9 @@ public abstract class TransactionGuard { */ public abstract void assertInsideTransaction(boolean transactionRequired, @NotNull String errorMessage); + /** + * @return the id of the currently running transaction for using in {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)}, + * or null if there's no transaction running or merging is not allowed in the callee context (e.g. from invokeLater). + */ + public abstract TransactionId getCurrentMergeableTransaction(); } diff --git a/platform/core-api/src/com/intellij/openapi/application/TransactionId.java b/platform/core-api/src/com/intellij/openapi/application/TransactionId.java new file mode 100644 index 000000000000..c42233849a6f --- /dev/null +++ b/platform/core-api/src/com/intellij/openapi/application/TransactionId.java @@ -0,0 +1,14 @@ +package com.intellij.openapi.application; + +import com.intellij.openapi.Disposable; + +/** + * A unique object identifying each running transaction. Can be retrieved from {@link TransactionGuard#getCurrentMergeableTransaction()} + * while inside a transaction, and used to merge other transactions into it via + * {@link TransactionGuard#submitMergeableTransaction(Disposable, TransactionId, Runnable)}. + * + * @author peter + * @since 2016.2 + */ +public interface TransactionId { +} diff --git a/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java b/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java index 2b73e59d05a7..11d0f3a00fd8 100644 --- a/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java +++ b/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java @@ -16,91 +16,99 @@ package com.intellij.openapi.application; import com.intellij.openapi.Disposable; -import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.registry.Registry; -import com.intellij.psi.impl.DebugUtil; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; /** * @author peter */ public class TransactionGuardImpl extends TransactionGuard { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.application.TransactionGuardImpl"); - private final Queue myQueue = new LinkedBlockingQueue(); + private final Queue myQueue = new LinkedBlockingQueue(); private final Set myMergeableKinds = ContainerUtil.newHashSet(); - private String myTransactionStartTrace; - private ModalityState myTransactionModality; - private boolean myUserActivity; + private final Map myProgresses = ContainerUtil.createConcurrentWeakMap(); + private TransactionIdImpl myCurrentTransaction; + private boolean myWritingAllowed; @Override @NotNull public AccessToken startSynchronousTransaction(@NotNull TransactionKind kind) throws IllegalStateException { - ModalityState modality = ModalityState.current(); - if (isInsideTransaction()) { - if (modality.equals(myTransactionModality) || myUserActivity) { - return AccessToken.EMPTY_ACCESS_TOKEN; - } - - if (myMergeableKinds.contains(kind)) { - final ModalityState prev = myTransactionModality; - myTransactionModality = modality; - return new AccessToken() { - @Override - public void finish() { - myTransactionModality = prev; - } - }; - } - + if (isInsideTransaction() && !myWritingAllowed && !myMergeableKinds.contains(kind)) { // please assign exceptions that occur here to Peter - LOG.error("Nested transactions are not allowed, see FAQ in TransactionGuard class javadoc. Transaction start trace is in attachment. Kind is " + kind, - new Attachment("trace.txt", myTransactionStartTrace)); + LOG.error("Synchronous transactions are allowed only from user actions. " + + "Please use submit*Transaction instead of invokeLater. " + + "See FAQ in TransactionGuard class javadoc."); return AccessToken.EMPTY_ACCESS_TOKEN; } - myTransactionModality = modality; - myTransactionStartTrace = DebugUtil.currentStackTrace(); + + return startTransactionUnchecked(); + } + + @NotNull + private AccessToken startTransactionUnchecked() { + final TransactionIdImpl prevTransaction = myCurrentTransaction; + final boolean wasWritingAllowed = myWritingAllowed; + + myWritingAllowed = true; + myCurrentTransaction = new TransactionIdImpl(); + return new AccessToken() { @Override public void finish() { - myTransactionStartTrace = null; - myTransactionModality = null; - if (!myQueue.isEmpty()) { + Queue queue = getQueue(prevTransaction); + queue.addAll(myCurrentTransaction.myQueue); + if (!queue.isEmpty()) { pollQueueLater(); } + + myWritingAllowed = wasWritingAllowed; + myCurrentTransaction = prevTransaction; } }; } + @NotNull + private Queue getQueue(@Nullable TransactionIdImpl prevTransaction) { + return prevTransaction == null ? myQueue : prevTransaction.myQueue; + } + private void pollQueueLater() { //todo replace with SwingUtilities when write actions are required to run under a guard final Application app = ApplicationManager.getApplication(); app.invokeLater(new Runnable() { @Override public void run() { - if (isInsideTransaction()) return; - - Runnable next = myQueue.poll(); - if (next != null) { - runSyncTransaction(TransactionKind.ANY_CHANGE, next); + Queue queue = getQueue(myCurrentTransaction); + Transaction next = queue.peek(); + if (next != null && canRunTransactionNow(next, false)) { + queue.remove(); + runSyncTransaction(next); } } }, app.getDisposed()); } - private void runSyncTransaction(@NotNull TransactionKind kind, @NotNull Runnable code) { - AccessToken token = startSynchronousTransaction(kind); + private void runSyncTransaction(@NotNull Transaction transaction) { + AccessToken token = startTransactionUnchecked(); try { - code.run(); + if (!Disposer.isDisposed(transaction.parentDisposable)) { + transaction.runnable.run(); + } } finally { token.finish(); @@ -109,34 +117,40 @@ public class TransactionGuardImpl extends TransactionGuard { private boolean isInsideTransaction() { ApplicationManager.getApplication().assertIsDispatchThread(); - return myTransactionStartTrace != null; + return myCurrentTransaction != null; } @Override public void submitMergeableTransaction(@NotNull final Disposable parentDisposable, @NotNull final TransactionKind kind, @NotNull final Runnable _transaction) { - @NotNull final Runnable transaction = new Runnable() { - @Override - public void run() { - if (!Disposer.isDisposed(parentDisposable)) { - _transaction.run(); - } - } - }; + submitMergeableTransaction(parentDisposable, kind, getCurrentMergeableTransaction(), _transaction); + } + + @Override + public void submitMergeableTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId mergeInto, @NotNull Runnable transaction) { + submitMergeableTransaction(parentDisposable, TransactionKind.ANY_CHANGE, (TransactionIdImpl)mergeInto, transaction); + } + + private void submitMergeableTransaction(@NotNull final Disposable parentDisposable, + @NotNull final TransactionKind kind, + @Nullable final TransactionIdImpl expectedId, + @NotNull final Runnable _transaction) { + @NotNull final Transaction transaction = new Transaction(_transaction, expectedId, kind, parentDisposable); + final Application app = ApplicationManager.getApplication(); + final boolean isDispatchThread = app.isDispatchThread(); Runnable runnable = new Runnable() { @Override public void run() { - if (canRunTransactionNow(kind)) { - runSyncTransaction(kind, transaction); + if (canRunTransactionNow(transaction, isDispatchThread)) { + runSyncTransaction(transaction); } else { - myQueue.offer(transaction); + getQueue(expectedId).offer(transaction); pollQueueLater(); } } }; - final Application app = ApplicationManager.getApplication(); - if (app.isDispatchThread()) { + if (isDispatchThread) { runnable.run(); } else { //todo add ModalityState.any() when write actions are required to run under a guard @@ -144,8 +158,17 @@ public class TransactionGuardImpl extends TransactionGuard { } } - protected boolean canRunTransactionNow(@NotNull TransactionKind kind) { - return !isInsideTransaction() || myMergeableKinds.contains(kind) || ModalityState.current().equals(myTransactionModality); + private boolean canRunTransactionNow(Transaction transaction, boolean sync) { + TransactionIdImpl currentId = myCurrentTransaction; + if (currentId == null || myMergeableKinds.contains(transaction.kind)) { + return true; + } + + if (sync && !myWritingAllowed) { + return false; + } + + return transaction.mergeInto != null && currentId.myStartCounter <= transaction.mergeInto.myStartCounter; } @Override @@ -179,13 +202,16 @@ public class TransactionGuardImpl extends TransactionGuard { } @Override - public void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Runnable transaction) throws ProcessCanceledException { + public void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Runnable runnable) throws ProcessCanceledException { Application app = ApplicationManager.getApplication(); if (app.isDispatchThread()) { - if (!canRunTransactionNow(kind)) { - throw new AssertionError("Cannot run submitTransactionAndWait from another transaction, kind " + kind + " is not allowed"); + Transaction transaction = new Transaction(runnable, getCurrentMergeableTransaction(), kind, app); + if (!canRunTransactionNow(transaction, true)) { + throw new AssertionError("Cannot run synchronous submitTransactionAndWait from invokeLater. " + + "Please use asynchronous submit*Transaction. " + + "See TransactionGuard FAQ for details."); } - runSyncTransaction(kind, transaction); + runSyncTransaction(transaction); return; } @@ -197,7 +223,7 @@ public class TransactionGuardImpl extends TransactionGuard { @Override public void run() { try { - transaction.run(); + runnable.run(); } catch (Throwable e) { exception[0] = e; @@ -224,7 +250,7 @@ public class TransactionGuardImpl extends TransactionGuard { * please consider using {@code ActionManager.tryToExecute()} instead, or ensure in some other way that the action is enabled * and can be invoked in the current modality state. */ - public void performUserActivity(Runnable activity) { + public void performUserActivity(Runnable activity) { ApplicationManager.getApplication().assertIsDispatchThread(); AccessToken token = startActivity(true); try { @@ -240,32 +266,77 @@ public class TransactionGuardImpl extends TransactionGuard { */ @NotNull public AccessToken startActivity(boolean userActivity) { - if (myUserActivity == userActivity) { + if (myWritingAllowed == userActivity) { return AccessToken.EMPTY_ACCESS_TOKEN; } - final boolean prev = myUserActivity; - myUserActivity = userActivity; + final boolean prev = myWritingAllowed; + myWritingAllowed = userActivity; return new AccessToken() { @Override public void finish() { - myUserActivity = prev; + myWritingAllowed = prev; } }; } public boolean isWriteActionAllowed() { - return !Registry.is("ide.require.transaction.for.model.changes", false) || isInsideTransaction() || myUserActivity; + return !Registry.is("ide.require.transaction.for.model.changes", false) || isInsideTransaction() || myWritingAllowed; } @Override public void submitTransactionLater(@NotNull final Disposable parentDisposable, @NotNull final Runnable transaction) { + final TransactionIdImpl id = getCurrentMergeableTransaction(); Application app = ApplicationManager.getApplication(); app.invokeLater(new Runnable() { @Override public void run() { - submitMergeableTransaction(parentDisposable, TransactionKind.ANY_CHANGE, transaction); + submitMergeableTransaction(parentDisposable, id, transaction); } }, app.getDisposed()); } + + @Override + public TransactionIdImpl getCurrentMergeableTransaction() { + if (!ApplicationManager.getApplication().isDispatchThread()) { + ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); + return indicator != null ? myProgresses.get(indicator) : null; + } + + return myWritingAllowed ? myCurrentTransaction : null; + } + + public void registerProgress(@NotNull ProgressIndicator indicator, @Nullable TransactionIdImpl contextTransaction) { + if (contextTransaction != null) { + myProgresses.put(indicator, contextTransaction); + } + } + + private static class Transaction { + @NotNull final Runnable runnable; + @Nullable final TransactionIdImpl mergeInto; + @NotNull final TransactionKind kind; + @NotNull final Disposable parentDisposable; + + Transaction(@NotNull Runnable runnable, + @Nullable TransactionIdImpl mergeInto, + @NotNull TransactionKind kind, + @NotNull Disposable parentDisposable) { + this.runnable = runnable; + this.mergeInto = mergeInto; + this.kind = kind; + this.parentDisposable = parentDisposable; + } + } + + private static class TransactionIdImpl implements TransactionId { + private static final AtomicLong ourTransactionCounter = new AtomicLong(); + final long myStartCounter = ourTransactionCounter.getAndIncrement(); + final Queue myQueue = new LinkedBlockingQueue(); + + @Override + public String toString() { + return "Transaction " + myStartCounter; + } + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/ProgressWindow.java b/platform/platform-impl/src/com/intellij/openapi/progress/util/ProgressWindow.java index 0e4bb8ec6b28..9b6500f42bea 100644 --- a/platform/platform-impl/src/com/intellij/openapi/progress/util/ProgressWindow.java +++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/ProgressWindow.java @@ -18,6 +18,8 @@ package com.intellij.openapi.progress.util; import com.intellij.ide.IdeEventQueue; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.TransactionGuard; +import com.intellij.openapi.application.TransactionGuardImpl; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -93,6 +95,9 @@ public class ProgressWindow extends ProgressIndicatorBase implements BlockingPro myShouldShowCancel = shouldShowCancel; myCancelText = cancelText; setModalityProgress(shouldShowBackground ? null : this); + TransactionGuardImpl guard = (TransactionGuardImpl)TransactionGuard.getInstance(); + guard.registerProgress(this, guard.getCurrentMergeableTransaction()); + myFocusTrackback = new FocusTrackback(this, WindowManager.getInstance().suggestParentWindow(project), false); Component parent = parentComponent; diff --git a/platform/platform-tests/testSrc/com/intellij/application/TransactionTest.groovy b/platform/platform-tests/testSrc/com/intellij/application/TransactionTest.groovy index c744cc514fc4..1465e8aed36f 100644 --- a/platform/platform-tests/testSrc/com/intellij/application/TransactionTest.groovy +++ b/platform/platform-tests/testSrc/com/intellij/application/TransactionTest.groovy @@ -4,6 +4,8 @@ import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.TransactionGuard import com.intellij.openapi.application.TransactionGuardImpl +import com.intellij.openapi.application.TransactionId +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.registry.Registry import com.intellij.testFramework.LightPlatformTestCase @@ -15,6 +17,8 @@ import javax.swing.* * @author peter */ class TransactionTest extends LightPlatformTestCase { + List log = [] + static TransactionGuardImpl getGuard() { return TransactionGuard.getInstance() as TransactionGuardImpl } @@ -33,6 +37,7 @@ class TransactionTest extends LightPlatformTestCase { @Override protected void tearDown() throws Exception { Registry.get("ide.require.transaction.for.model.changes").resetToDefault() + log.clear() super.tearDown() } @@ -54,7 +59,6 @@ class TransactionTest extends LightPlatformTestCase { public void "test parent disposable"() { def parent = Disposer.newDisposable() - def log = [] SwingUtilities.invokeLater { TransactionGuard.submitTransaction parent, { log << "1" } } UIUtil.dispatchAllInvocationEvents() @@ -65,4 +69,95 @@ class TransactionTest extends LightPlatformTestCase { UIUtil.dispatchAllInvocationEvents() assert log == ['1'] } + + public void "test no current id inside invokeLater"() { + SwingUtilities.invokeLater { + log << '2' + assert !guard.currentMergeableTransaction + } + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + UIUtil.dispatchAllInvocationEvents() + } + assert log == ['1', '2'] + } + + + public void "test has id inside nested transaction"() { + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + def id = guard.currentMergeableTransaction + assert id + TransactionGuard.submitTransaction testRootDisposable, { + log << '2' + assert guard.currentMergeableTransaction + assert id != guard.currentMergeableTransaction + } + assert log == ['1', '2'] + } + assert log == ['1', '2'] + } + + public void "test modal progress started from inside a transaction has the same id"() { + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + def id = guard.currentMergeableTransaction + assert id + ProgressManager.instance.runProcessWithProgressSynchronously({ + assert !ApplicationManager.application.dispatchThread + assert id == guard.currentMergeableTransaction + log << '2' + }, 'title', true, project) + } + assert log == ['1', '2'] + } + public void "test no id on pooled thread"() { + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + ApplicationManager.application.executeOnPooledThread({ + assert !ApplicationManager.application.dispatchThread + assert !guard.currentMergeableTransaction + log << '2' + }).get() + } + assert log == ['1', '2'] + } + + public void "test do not merge transactions with null id"() { + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + guard.submitMergeableTransaction testRootDisposable, (TransactionId)null, { log << '2' } + UIUtil.dispatchAllInvocationEvents() + assert log == ['1'] + } + assert log == ['1'] + UIUtil.dispatchAllInvocationEvents() + assert log == ['1', '2'] + } + + public void "test do not merge into newly started nested transactions"() { + TransactionGuard.submitTransaction testRootDisposable, { + log << '1' + def id = guard.currentMergeableTransaction + SwingUtilities.invokeLater { + guard.submitMergeableTransaction testRootDisposable, id, { log << '4' } + } + guard.submitMergeableTransaction testRootDisposable, id, { + UIUtil.dispatchAllInvocationEvents() + log << '2' + UIUtil.dispatchAllInvocationEvents() + guard.submitMergeableTransaction testRootDisposable, id, { log << '5' } + def nestedId = guard.currentMergeableTransaction + SwingUtilities.invokeLater { + guard.submitMergeableTransaction testRootDisposable, nestedId, { log << '3' } + assert log == ['1', '2'] + } + UIUtil.dispatchAllInvocationEvents() + } + assert log == ['1', '2', '3'] + UIUtil.dispatchAllInvocationEvents() + assert log == ['1', '2', '3', '4', '5'] + } + } + } diff --git a/platform/util/src/com/intellij/util/ui/UIUtil.java b/platform/util/src/com/intellij/util/ui/UIUtil.java index 0280eff63ed1..4b022e798557 100644 --- a/platform/util/src/com/intellij/util/ui/UIUtil.java +++ b/platform/util/src/com/intellij/util/ui/UIUtil.java @@ -1895,9 +1895,9 @@ public class UIUtil { AWTEvent event = eventQueue.peekEvent(); if (event == null) return false; try { - AWTEvent event1 = eventQueue.getNextEvent(); - if (event1 instanceof InvocationEvent) { - ((InvocationEvent)event1).dispatch(); + event = eventQueue.getNextEvent(); + if (event instanceof InvocationEvent) { + eventQueue.getClass().getDeclaredMethod("dispatchEvent", AWTEvent.class).invoke(eventQueue, event); } } catch (Exception e) {