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 2b304ae3f440..3153ee035154 100644 --- a/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java +++ b/platform/core-api/src/com/intellij/openapi/application/TransactionGuard.java @@ -19,6 +19,7 @@ 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 com.intellij.util.ThrowableRunnable; import org.jetbrains.annotations.NotNull; /** @@ -30,7 +31,9 @@ import org.jetbrains.annotations.NotNull; * and process UI events in other ways: it's guaranteed that no one will be able to sneak in with an unexpected model change using * {@link javax.swing.SwingUtilities#invokeLater(Runnable)} or analogs.

* - * Transactions are run on UI thread. They have read access by default. All write actions should be performed inside a transaction.

+ * Transactions are run on UI thread. They have read access by default. All write actions that don't happen as the result of direct user input + * 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.

@@ -192,4 +195,13 @@ public abstract class TransactionGuard { * @param errorMessage the message that will be logged if current transaction status differs from the expected one */ public abstract void assertInsideTransaction(boolean transactionRequired, @NotNull String errorMessage); + + /** + * Executes the given code and marks it as a user activity, to allow write actions to be run without requiring transactions. + * Please note that only direct keyboard/mouse event processing can be wrapped in such calls. If you wish to invoke some actionPerformed, + * 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 abstract void performUserActivity(ThrowableRunnable activity) throws T; + } 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 620fc91fca6e..53e26bf6c5a1 100644 --- a/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java +++ b/platform/core-impl/src/com/intellij/openapi/application/TransactionGuardImpl.java @@ -18,7 +18,9 @@ package com.intellij.openapi.application; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.impl.DebugUtil; +import com.intellij.util.ThrowableRunnable; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -37,13 +39,14 @@ public class TransactionGuardImpl extends TransactionGuard { private final Set myMergeableKinds = ContainerUtil.newHashSet(); private String myTransactionStartTrace; private ModalityState myTransactionModality; + private boolean myUserActivity; @Override @NotNull public AccessToken startSynchronousTransaction(@NotNull TransactionKind kind) throws IllegalStateException { ModalityState modality = ModalityState.current(); if (isInsideTransaction()) { - if (modality.equals(myTransactionModality)) { + if (modality.equals(myTransactionModality) || myUserActivity) { return AccessToken.EMPTY_ACCESS_TOKEN; } @@ -200,4 +203,39 @@ public class TransactionGuardImpl extends TransactionGuard { throw new RuntimeException(exception[0]); } } + + @Override + public void performUserActivity(ThrowableRunnable activity) throws T { + ApplicationManager.getApplication().assertIsDispatchThread(); + AccessToken token = startActivity(true); + try { + activity.run(); + } + finally { + token.finish(); + } + } + + /** + * An absolutely guru method, only intended to be used from Swing event processing. Please consult Peter if you think you need to invoke this. + */ + @NotNull + public AccessToken startActivity(boolean userActivity) { + if (myUserActivity == userActivity) { + return AccessToken.EMPTY_ACCESS_TOKEN; + } + + final boolean prev = myUserActivity; + myUserActivity = userActivity; + return new AccessToken() { + @Override + public void finish() { + myUserActivity = prev; + } + }; + } + + public boolean isWriteActionAllowed() { + return !Registry.is("ide.require.transaction.for.model.changes", false) || isInsideTransaction() || myUserActivity; + } } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/GotoActionAction.java b/platform/lang-impl/src/com/intellij/ide/actions/GotoActionAction.java index 502ff5851c92..bbc74f4bb4e5 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/GotoActionAction.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/GotoActionAction.java @@ -29,6 +29,7 @@ import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.actionSystem.ex.QuickList; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.TransactionGuard; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; @@ -250,10 +251,9 @@ public class GotoActionAction extends GotoActionBase implements DumbAware { public static void performAction(Object element, @Nullable final Component component, @Nullable final AnActionEvent e) { // element could be AnAction (SearchEverywhere) final AnAction action = element instanceof AnAction ? (AnAction)element : ((GotoActionModel.ActionWrapper)element).getAction(); - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - if (component == null) return; + ApplicationManager.getApplication().invokeLater(() -> { + if (component == null) return; + TransactionGuard.getInstance().performUserActivity(() -> { DataManager instance = DataManager.getInstance(); DataContext context = instance != null ? instance.getDataContext(component) : DataContext.EMPTY_CONTEXT; InputEvent inputEvent = e == null ? null : e.getInputEvent(); @@ -270,12 +270,12 @@ public class GotoActionAction extends GotoActionBase implements DumbAware { else { popup.showInFocusCenter(); } - } + } else { ActionUtil.performActionDumbAware(action, event); } } - } + }); }, ModalityState.NON_MODAL); } diff --git a/platform/platform-impl/src/com/intellij/ide/IdeEventQueue.java b/platform/platform-impl/src/com/intellij/ide/IdeEventQueue.java index 3ceb6f620964..cdf1d46b57d6 100644 --- a/platform/platform-impl/src/com/intellij/ide/IdeEventQueue.java +++ b/platform/platform-impl/src/com/intellij/ide/IdeEventQueue.java @@ -21,9 +21,7 @@ import com.intellij.ide.plugins.PluginManager; import com.intellij.ide.ui.UISettings; import com.intellij.idea.IdeaApplication; import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.*; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.diagnostic.FrequentEventDetector; @@ -385,6 +383,7 @@ public class IdeEventQueue extends EventQueue { } AWTEvent oldEvent = myCurrentEvent; myCurrentEvent = e; + AccessToken token = ((TransactionGuardImpl)TransactionGuard.getInstance()).startActivity(myIsInInputEvent); try { _dispatchEvent(e, false); @@ -393,6 +392,7 @@ public class IdeEventQueue extends EventQueue { processException(t); } finally { + token.finish(); myIsInInputEvent = wasInputEvent; myCurrentEvent = oldEvent; diff --git a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionManagerImpl.java b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionManagerImpl.java index c6fe3dfc6c8e..375f764abfc1 100644 --- a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionManagerImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionManagerImpl.java @@ -29,10 +29,7 @@ import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.actionSystem.ex.AnActionListener; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationActivationListener; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.*; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.PluginId; @@ -1298,15 +1295,14 @@ public final class ActionManagerImpl extends ActionManagerEx implements Disposab private void tryToExecuteNow(final AnAction action, final InputEvent inputEvent, final Component contextComponent, final String place, final ActionCallback result) { final Presentation presentation = action.getTemplatePresentation().clone(); - IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(new Runnable() { - @Override - public void run() { + IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown( + () -> TransactionGuard.getInstance().performUserActivity(() -> { final DataContext context = getContextBy(contextComponent); AnActionEvent event = new AnActionEvent( inputEvent, context, place != null ? place : ActionPlaces.UNKNOWN, - presentation, ActionManagerImpl.this, + presentation, this, inputEvent.getModifiersEx() ); @@ -1346,7 +1342,7 @@ public final class ActionManagerImpl extends ActionManagerEx implements Disposab result.setDone(); queueActionPerformedEvent(action, context, event); } - }); + )); } private class MyTimer extends Timer implements ActionListener { diff --git a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java index beee7d02aef0..48e6b1d9e314 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java @@ -1229,9 +1229,9 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App private void startWrite(/*@NotNull*/ Class clazz) { assertIsDispatchThread(getStatus(), "Write access is allowed from event dispatch thread only"); HeavyProcessLatch.INSTANCE.stopThreadPrioritizing(); // let non-cancellable read actions complete faster, if present - if (Registry.is("ide.require.transaction.for.model.changes", false)) { - TransactionGuard.getInstance().assertInsideTransaction( - true, "Write access is allowed from model transactions only, see TransactionGuard documentation for details"); + if (!((TransactionGuardImpl)TransactionGuard.getInstance()).isWriteActionAllowed()) { + // please assign exceptions here to Peter + LOG.error("Write access is allowed from model transactions only, see TransactionGuard documentation for details"); } boolean writeActionPending = myWriteActionPending; if (gatherWriteActionStatistics && myWriteActionsStack.isEmpty() && !writeActionPending) { diff --git a/platform/platform-impl/src/com/intellij/openapi/keymap/impl/IdeKeyEventDispatcher.java b/platform/platform-impl/src/com/intellij/openapi/keymap/impl/IdeKeyEventDispatcher.java index 1cfb301ef9d7..992c83d5a577 100644 --- a/platform/platform-impl/src/com/intellij/openapi/keymap/impl/IdeKeyEventDispatcher.java +++ b/platform/platform-impl/src/com/intellij/openapi/keymap/impl/IdeKeyEventDispatcher.java @@ -868,17 +868,15 @@ public final class IdeKeyEventDispatcher implements Disposable { } private static void invokeAction(@NotNull final AnAction action, final DataContext ctx) { - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { + ApplicationManager.getApplication().invokeLater( + () -> TransactionGuard.getInstance().performUserActivity(() -> { final AnActionEvent event = new AnActionEvent(null, ctx, ActionPlaces.UNKNOWN, action.getTemplatePresentation().clone(), ActionManager.getInstance(), 0); if (ActionUtil.lastUpdateAndCheckDumb(action, event, true)) { ActionUtil.performActionDumbAware(action, event); } - } - }); + })); } @Override diff --git a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java index 76959083f905..312a605add38 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java @@ -28,6 +28,7 @@ import com.intellij.openapi.actionSystem.DataProvider; import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.TransactionGuard; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; @@ -1392,7 +1393,7 @@ public class AbstractPopup implements JBPopup { //noinspection SSBasedInspection SwingUtilities.invokeLater(() -> { if (ModalityState.current().equals(modalityState)) { - finalRunnable.run(); + TransactionGuard.getInstance().performUserActivity(finalRunnable::run); } // Otherwise the UI has changed unexpectedly and the action is likely not applicable. // And we don't want finalRunnable to perform potentially destructive actions