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