[undo] IJPL-201535 Support speculative undo for global and multi-project case

GitOrigin-RevId: 09805ee7fdf15ec02117d86ce604345775af5bf0
This commit is contained in:
Alexander Trushev
2025-11-11 01:19:00 +00:00
committed by intellij-monorepo-bot
parent f887e139ca
commit 46a8fcb887
9 changed files with 101 additions and 53 deletions
@@ -29,7 +29,7 @@ object SpeculativeUndoStatCollector : CounterUsagesCollector() {
/**
* Collects project id, file type and number of successful undo/redo requests before the corruption
*/
fun logUndoCorruption(project: Project, fileEditor: FileEditor?, undoCount: Int) {
fun logUndoCorruption(project: Project?, fileEditor: FileEditor?, undoCount: Int) {
UNDO_CORRUPTION.log(project) {
val fileType = fileEditor?.file?.fileType
add(EventFields.FileType with fileType)
@@ -97,19 +97,35 @@ public final class CommandSeparator implements CommandListener {
}
private void notifyCommandStarted(@NotNull CommandEvent event) {
publisher.onCommandStarted(createCmdEvent(event));
notifyCommandStarted(createCmdEvent(event));
}
private void notifyCommandFinished(@NotNull CommandEvent event) {
publisher.onCommandFinished(createCmdEvent(event));
notifyCommandFinished(createCmdEvent(event));
}
private void notifyTransparentStarted() {
publisher.onCommandStarted(createTransparentCmdEvent());
notifyCommandStarted(createTransparentCmdEvent());
}
private void notifyTransparentFinished() {
publisher.onCommandFinished(createTransparentCmdEvent());
notifyCommandFinished(createTransparentCmdEvent());
}
private void notifyCommandStarted(@NotNull CmdEvent cmdEvent) {
publisher.onCommandStarted(cmdEvent);
UndoSpy undoSpy = UndoSpy.getInstance();
if (undoSpy != null) {
undoSpy.commandStarted(cmdEvent);
}
}
private void notifyCommandFinished(@NotNull CmdEvent cmdEvent) {
publisher.onCommandFinished(cmdEvent);
UndoSpy undoSpy = UndoSpy.getInstance();
if (undoSpy != null) {
undoSpy.commandFinished(cmdEvent);
}
}
private void assertInsideCommand() {
@@ -7,13 +7,18 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Supplier;
@ApiStatus.Internal
final class SharedUndoRedoStacksHolder extends UndoRedoStacksHolderBase<ImmutableActionChangeRange> {
private final SharedAdjustableUndoableActionsHolder myAdjustableUndoableActionsHolder;
private final boolean myIsPerClientSupported;
private final Supplier<Boolean> myIsPerClientSupported;
SharedUndoRedoStacksHolder(SharedAdjustableUndoableActionsHolder undoableActionsHolder, boolean isPerClientSupported, boolean isUndo) {
SharedUndoRedoStacksHolder(
@NotNull SharedAdjustableUndoableActionsHolder undoableActionsHolder,
@NotNull Supplier<Boolean> isPerClientSupported,
boolean isUndo
) {
super(isUndo);
myAdjustableUndoableActionsHolder = undoableActionsHolder;
myIsPerClientSupported = isPerClientSupported;
@@ -60,7 +65,7 @@ final class SharedUndoRedoStacksHolder extends UndoRedoStacksHolderBase<Immutabl
}
@NotNull MovementAvailability canMoveToStackTop(@NotNull DocumentReference reference, @NotNull Map<Integer, MutableActionChangeRange> rangesToMove) {
if (!myIsPerClientSupported) {
if (!myIsPerClientSupported.get()) {
return MovementAvailability.ALREADY_MOVED;
}
UndoRedoList<ImmutableActionChangeRange> stack = getStack(reference);
@@ -41,7 +41,6 @@ final class UndoClientState implements Disposable {
private final @NotNull UndoRedoStacksHolder undoStacksHolder;
private final @NotNull UndoRedoStacksHolder redoStacksHolder;
private final @NotNull UndoSpy undoSpy;
private final boolean isConfirmationSupported;
private final boolean isCompactSupported;
private final boolean isGlobalSplitSupported;
@@ -65,7 +64,6 @@ final class UndoClientState implements Disposable {
private UndoClientState(@NotNull UndoManagerImpl undoManager, @NotNull ClientId clientId) {
this.clientId = clientId;
this.project = undoManager.getProject();
this.undoSpy = undoManager.getUndoSpy();
this.isConfirmationSupported = undoManager.isConfirmationSupported();
this.isCompactSupported = undoManager.isCompactSupported();
this.isGlobalSplitSupported = undoManager.isGlobalSplitSupported();
@@ -162,13 +160,12 @@ final class UndoClientState implements Disposable {
void commandStarted(@NotNull CmdEvent cmdEvent, @NotNull CurrentEditorProvider editorProvider) {
commandBuilder.commandStarted(cmdEvent, editorProvider);
undoSpy.commandStarted(cmdEvent);
}
void commandFinished(@NotNull CmdEvent cmdEvent) {
PerformedCommand performedCommand = commandBuilder.commandFinished(cmdEvent);
commitCommand(performedCommand);
notifyUndoSpy(cmdEvent, performedCommand);
notifyUndoSpy(performedCommand);
}
private void commitCommand(@NotNull PerformedCommand performedCommand) {
@@ -183,14 +180,14 @@ final class UndoClientState implements Disposable {
commandMerger.mergeWithPerformedCommand(performedCommand);
}
private void notifyUndoSpy(@NotNull CmdEvent cmdEvent, @NotNull PerformedCommand performedCommand) {
private void notifyUndoSpy(@NotNull PerformedCommand performedCommand) {
for (UndoableAction action : performedCommand.undoableActions()) {
sharedState.addAction(action);
if (cmdEvent.project() != null) {
undoSpy.undoableActionAdded(cmdEvent.project(), action, UndoableActionType.forAction(action));
UndoSpy undoSpy = UndoSpy.getInstance();
if (undoSpy != null) {
undoSpy.undoableActionAdded(project, action, UndoableActionType.forAction(action));
}
}
undoSpy.commandFinished(cmdEvent);
}
void flushCommandMerger(@NotNull UndoCommandFlushReason flushReason) {
@@ -369,7 +366,10 @@ final class UndoClientState implements Disposable {
private void flushCommandMerger(@NotNull UndoCommandFlushReason flushReason, @Nullable PerformedCommand performedCommand) {
if (performedCommand != null && !performedCommand.hasActions() && commandMerger.hasActions() && !isUndoOrRedoInProgress()) {
undoSpy.commandMergerFlushed(project);
UndoSpy undoSpy = UndoSpy.getInstance();
if (undoSpy != null) {
undoSpy.commandMergerFlushed(project);
}
}
UndoableGroup group = commandMerger.formGroup(flushReason, nextCommandTimestamp());
if (group != null) {
@@ -384,7 +384,7 @@ final class UndoClientState implements Disposable {
docsOnStacks.removeIf(doc -> UndoDocumentUtil.isDocumentOpened(project, doc));
if (docsOnStacks.size() > FREE_QUEUES_LIMIT) {
DocumentReference[] docsBackSorted = docsOnStacks.toArray(DocumentReference.EMPTY_ARRAY);
Arrays.sort(docsBackSorted, Comparator.comparingInt(doc -> getLastCommandTimestamp(doc)));
Arrays.sort(docsBackSorted, Comparator.comparingInt(this::getLastCommandTimestamp));
for (int i = 0; i < docsBackSorted.length - FREE_QUEUES_LIMIT; i++) {
DocumentReference doc = docsBackSorted[i];
if (getLastCommandTimestamp(doc) + COMMANDS_TO_KEEP_LIVE_QUEUES > commandTimestamp) {
@@ -77,7 +77,7 @@ public class UndoManagerImpl extends UndoManager {
@NonInjectable
protected UndoManagerImpl(@Nullable ComponentManager componentManager) {
myProject = componentManager instanceof Project project ? project : null;
myUndoSharedState = new UndoSharedState(isPerClientSupported());
myUndoSharedState = new UndoSharedState(this::isPerClientSupported);
}
@Override
@@ -292,8 +292,9 @@ public class UndoManagerImpl extends UndoManager {
} finally {
Disposer.dispose(disposable);
}
if (myProject != null) {
getUndoSpy().undoRedoPerformed(myProject, editor, isUndo);
UndoSpy undoSpy = UndoSpy.getInstance();
if (undoSpy != null) {
undoSpy.undoRedoPerformed(myProject, editor, isUndo);
}
}
}
@@ -306,11 +307,6 @@ public class UndoManagerImpl extends UndoManager {
.undoRedoStarted(myProject, this, editor, isUndo, disposable);
}
@ApiStatus.Internal
protected @NotNull UndoSpy getUndoSpy() {
return UndoSpy.BLIND;
}
@ApiStatus.Internal
protected boolean isTransparentSupported() {
return true;
@@ -534,11 +530,11 @@ public class UndoManagerImpl extends UndoManager {
}
private @NotNull List<UndoProvider> getUndoProviders() {
return ProgressManager.getInstance().computeInNonCancelableSection(() -> {
return myProject == null
? UndoProvider.EP_NAME.getExtensionList()
: UndoProvider.PROJECT_EP_NAME.getExtensionList(myProject);
});
return ProgressManager.getInstance().computeInNonCancelableSection(
() -> myProject == null
? UndoProvider.EP_NAME.getExtensionList()
: UndoProvider.PROJECT_EP_NAME.getExtensionList(myProject)
);
}
private @NotNull ComponentManager getComponentManager() {
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
final class UndoSharedState {
@@ -17,7 +18,7 @@ final class UndoSharedState {
private final @NotNull SharedUndoRedoStacksHolder undoStacks;
private final @NotNull SharedUndoRedoStacksHolder redoStacks;
UndoSharedState(boolean isPerClientSupported) {
UndoSharedState(@NotNull Supplier<Boolean> isPerClientSupported) {
adjustableActions = new SharedAdjustableUndoableActionsHolder();
undoStacks = new SharedUndoRedoStacksHolder(adjustableActions, isPerClientSupported, true);
redoStacks = new SharedUndoRedoStacksHolder(adjustableActions, isPerClientSupported, false);
@@ -1,8 +1,12 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.command.impl
import com.intellij.ide.impl.UndoRemoteBehaviorService
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.undo.UndoableAction
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
@@ -13,33 +17,28 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface UndoSpy {
companion object {
@JvmStatic
fun getInstance(): UndoSpy? {
return ProgressManager.getInstance().computeInNonCancelableSection<UndoSpy?, Exception> {
if (UndoRemoteBehaviorService.isSpeculativeUndoEnabled()) {
val application = ApplicationManager.getApplication()
application?.service<UndoSpy>()
} else {
null
}
}
}
}
fun commandStarted(cmdEvent: CmdEvent)
fun undoableActionAdded(
project: Project?,
action: UndoableAction,
type: UndoableActionType,
)
fun undoableActionAdded(undoProject: Project?, action: UndoableAction, type: UndoableActionType)
fun commandFinished(cmdEvent: CmdEvent)
fun undoRedoPerformed(
project: Project?,
editor: FileEditor?,
isUndo: Boolean,
)
fun undoRedoPerformed(project: Project?, editor: FileEditor?, isUndo: Boolean)
// TODO: sync FE commands instead of flush
fun commandMergerFlushed(project: Project?)
companion object {
@JvmField
val BLIND: UndoSpy = object : UndoSpy {
override fun commandStarted(cmdEvent: CmdEvent) {}
override fun undoableActionAdded(project: Project?, action: UndoableAction, type: UndoableActionType) {}
override fun commandFinished(cmdEvent: CmdEvent) {}
override fun undoRedoPerformed(project: Project?, editor: FileEditor?, isUndo: Boolean) {}
override fun commandMergerFlushed(project: Project?) {}
}
}
}
@@ -0,0 +1,28 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.command.impl
import com.intellij.openapi.command.undo.UndoableAction
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
@ApiStatus.Internal
open class UndoSpyImpl : UndoSpy {
override fun commandStarted(cmdEvent: CmdEvent) {
}
override fun undoableActionAdded(undoProject: Project?, action: UndoableAction, type: UndoableActionType) {
}
override fun commandFinished(cmdEvent: CmdEvent) {
}
override fun undoRedoPerformed(project: Project?, editor: FileEditor?, isUndo: Boolean) {
}
override fun commandMergerFlushed(project: Project?) {
}
}
@@ -45,6 +45,9 @@
<projectService serviceInterface="com.intellij.openapi.command.undo.UndoManager"
serviceImplementation="com.intellij.openapi.command.impl.UndoManagerImpl" preload="notHeadless"/>
<applicationService serviceInterface="com.intellij.openapi.command.impl.UndoSpy"
serviceImplementation="com.intellij.openapi.command.impl.UndoSpyImpl" preload="notHeadless"/>
<applicationService serviceInterface="com.intellij.ide.ui.LafManager"
serviceImplementation="com.intellij.ide.ui.laf.LafManagerImpl"
headlessImplementation="com.intellij.ide.ui.laf.HeadlessLafManagerImpl"/>