blob: 9f43164cf9d4a30cc3ab6dac68598479512a9641 [file] [log] [blame]
/*
* Copyright 2000-2009 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.command.impl;
import com.intellij.CommonBundle;
import com.intellij.ide.DataManager;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.*;
import com.intellij.openapi.command.undo.*;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.FragmentContent;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.EmptyRunnable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.psi.ExternalChangeAction;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.util.containers.HashSet;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.awt.*;
import java.util.*;
import java.util.List;
public class UndoManagerImpl extends UndoManager implements ProjectComponent, ApplicationComponent, Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.command.impl.UndoManagerImpl");
private static final int COMMANDS_TO_KEEP_LIVE_QUEUES = 100;
private static final int COMMAND_TO_RUN_COMPACT = 20;
private static final int FREE_QUEUES_LIMIT = 30;
@Nullable private final ProjectEx myProject;
private final CommandProcessor myCommandProcessor;
private final StartupManager myStartupManager;
private UndoProvider[] myUndoProviders;
private CurrentEditorProvider myEditorProvider;
private final UndoRedoStacksHolder myUndoStacksHolder = new UndoRedoStacksHolder(true);
private final UndoRedoStacksHolder myRedoStacksHolder = new UndoRedoStacksHolder(false);
private final CommandMerger myMerger;
private CommandMerger myCurrentMerger;
private Project myCurrentActionProject = DummyProject.getInstance();
private int myCommandTimestamp = 1;
private int myCommandLevel = 0;
private static final int NONE = 0;
private static final int UNDO = 1;
private static final int REDO = 2;
private int myCurrentOperationState = NONE;
private DocumentReference myOriginatorReference;
public static boolean isRefresh() {
return ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class);
}
public static int getGlobalUndoLimit() {
return Registry.intValue("undo.globalUndoLimit");
}
public static int getDocumentUndoLimit() {
return Registry.intValue("undo.documentUndoLimit");
}
public UndoManagerImpl(Application application, CommandProcessor commandProcessor) {
this(application, null, commandProcessor, null);
}
public UndoManagerImpl(Application application,
ProjectEx project,
CommandProcessor commandProcessor,
StartupManager startupManager) {
myProject = project;
myCommandProcessor = commandProcessor;
myStartupManager = startupManager;
init(application);
myMerger = new CommandMerger(this);
}
private void init(Application application) {
if (myProject == null || application.isUnitTestMode() && !myProject.isDefault()) {
initialize();
}
}
@Override
@NotNull
public String getComponentName() {
return "UndoManager";
}
@Nullable
public Project getProject() {
return myProject;
}
@Override
public void initComponent() {
}
@Override
public void projectOpened() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
initialize();
}
}
@Override
public void projectClosed() {
}
@Override
public void disposeComponent() {
}
@Override
public void dispose() {
}
private void initialize() {
if (myProject == null) {
runStartupActivity();
}
else {
myStartupManager.registerStartupActivity(new Runnable() {
@Override
public void run() {
runStartupActivity();
}
});
}
}
private void runStartupActivity() {
myEditorProvider = new FocusBasedCurrentEditorProvider();
CommandListener commandListener = new CommandAdapter() {
private boolean myStarted = false;
@Override
public void commandStarted(CommandEvent event) {
onCommandStarted(event.getProject(), event.getUndoConfirmationPolicy());
}
@Override
public void commandFinished(CommandEvent event) {
onCommandFinished(event.getProject(), event.getCommandName(), event.getCommandGroupId());
}
@Override
public void undoTransparentActionStarted() {
if (!isInsideCommand()) {
myStarted = true;
onCommandStarted(myProject, UndoConfirmationPolicy.DEFAULT);
}
}
@Override
public void undoTransparentActionFinished() {
if (myStarted) {
myStarted = false;
onCommandFinished(myProject, "", null);
}
}
};
myCommandProcessor.addCommandListener(commandListener, this);
Disposer.register(this, new DocumentUndoProvider(myProject));
myUndoProviders = myProject == null
? Extensions.getExtensions(UndoProvider.EP_NAME)
: Extensions.getExtensions(UndoProvider.PROJECT_EP_NAME, myProject);
for (UndoProvider undoProvider : myUndoProviders) {
if (undoProvider instanceof Disposable) {
Disposer.register(this, (Disposable)undoProvider);
}
}
}
public boolean isActive() {
return Comparing.equal(myProject, myCurrentActionProject);
}
private boolean isInsideCommand() {
return myCommandLevel > 0;
}
private void onCommandStarted(final Project project, UndoConfirmationPolicy undoConfirmationPolicy) {
if (myCommandLevel == 0) {
for (UndoProvider undoProvider : myUndoProviders) {
undoProvider.commandStarted(project);
}
myCurrentActionProject = project;
}
commandStarted(undoConfirmationPolicy, myProject == project);
LOG.assertTrue(myCommandLevel == 0 || !(myCurrentActionProject instanceof DummyProject));
}
private void onCommandFinished(final Project project, final String commandName, final Object commandGroupId) {
commandFinished(commandName, commandGroupId);
if (myCommandLevel == 0) {
for (UndoProvider undoProvider : myUndoProviders) {
undoProvider.commandFinished(project);
}
myCurrentActionProject = DummyProject.getInstance();
}
LOG.assertTrue(myCommandLevel == 0 || !(myCurrentActionProject instanceof DummyProject));
}
private void commandStarted(UndoConfirmationPolicy undoConfirmationPolicy, boolean recordOriginalReference) {
if (myCommandLevel == 0) {
myCurrentMerger = new CommandMerger(this, CommandProcessor.getInstance().isUndoTransparentActionInProgress());
if (recordOriginalReference && myProject != null) {
Editor editor = null;
final Application application = ApplicationManager.getApplication();
if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
editor = CommonDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext());
}
else {
Component component = WindowManagerEx.getInstanceEx().getFocusedComponent(myProject);
if (component != null) {
editor = CommonDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext(component));
}
}
if (editor != null) {
Document document = editor.getDocument();
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file != null && file.isValid()) {
myOriginatorReference = DocumentReferenceManager.getInstance().create(file);
}
}
}
}
LOG.assertTrue(myCurrentMerger != null, String.valueOf(myCommandLevel));
myCurrentMerger.setBeforeState(getCurrentState());
myCurrentMerger.mergeUndoConfirmationPolicy(undoConfirmationPolicy);
myCommandLevel++;
}
private void commandFinished(String commandName, Object groupId) {
if (myCommandLevel == 0) return; // possible if command listener was added within command
myCommandLevel--;
if (myCommandLevel > 0) return;
if (myProject != null && myCurrentMerger.hasActions() && !myCurrentMerger.isTransparent() && myCurrentMerger.isPhysical()) {
addFocusedDocumentAsAffected();
}
myOriginatorReference = null;
myCurrentMerger.setAfterState(getCurrentState());
myMerger.commandFinished(commandName, groupId, myCurrentMerger);
disposeCurrentMerger();
}
private void addFocusedDocumentAsAffected() {
if (myOriginatorReference == null || myCurrentMerger.hasChangesOf(myOriginatorReference, true)) return;
final DocumentReference[] refs = new DocumentReference[]{myOriginatorReference};
myCurrentMerger.addAction(new BasicUndoableAction() {
@Override
public void undo() throws UnexpectedUndoException {
}
@Override
public void redo() throws UnexpectedUndoException {
}
@Override
public DocumentReference[] getAffectedDocuments() {
return refs;
}
});
}
private EditorAndState getCurrentState() {
FileEditor editor = myEditorProvider.getCurrentEditor();
if (editor == null) {
return null;
}
return new EditorAndState(editor, editor.getState(FileEditorStateLevel.UNDO));
}
private void disposeCurrentMerger() {
LOG.assertTrue(myCommandLevel == 0);
if (myCurrentMerger != null) {
myCurrentMerger = null;
}
}
@Override
public void nonundoableActionPerformed(final DocumentReference ref, final boolean isGlobal) {
ApplicationManager.getApplication().assertIsDispatchThread();
undoableActionPerformed(new NonUndoableAction(ref, isGlobal));
}
@Override
public void undoableActionPerformed(UndoableAction action) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myCurrentOperationState != NONE) return;
if (myCommandLevel == 0) {
LOG.assertTrue(action instanceof NonUndoableAction,
"Undoable actions allowed inside commands only (see com.intellij.openapi.command.CommandProcessor.executeCommand())");
commandStarted(UndoConfirmationPolicy.DEFAULT, false);
myCurrentMerger.addAction(action);
commandFinished("", null);
return;
}
if (isRefresh()) myOriginatorReference = null;
myCurrentMerger.addAction(action);
}
public void markCurrentCommandAsGlobal() {
myCurrentMerger.markAsGlobal();
}
public void addAffectedDocuments(Document... docs) {
if (!isInsideCommand()) {
LOG.error("Must be called inside command");
return;
}
List<DocumentReference> refs = new ArrayList<DocumentReference>(docs.length);
for (Document each : docs) {
// is document's file still valid
VirtualFile file = FileDocumentManager.getInstance().getFile(each);
if (file != null && !file.isValid()) continue;
refs.add(DocumentReferenceManager.getInstance().create(each));
}
myCurrentMerger.addAdditionalAffectedDocuments(refs);
}
public void addAffectedFiles(VirtualFile... files) {
if (!isInsideCommand()) {
LOG.error("Must be called inside command");
return;
}
List<DocumentReference> refs = new ArrayList<DocumentReference>(files.length);
for (VirtualFile each : files) {
refs.add(DocumentReferenceManager.getInstance().create(each));
}
myCurrentMerger.addAdditionalAffectedDocuments(refs);
}
public void invalidateActionsFor(DocumentReference ref) {
ApplicationManager.getApplication().assertIsDispatchThread();
myMerger.invalidateActionsFor(ref);
if (myCurrentMerger != null) myCurrentMerger.invalidateActionsFor(ref);
myUndoStacksHolder.invalidateActionsFor(ref);
myRedoStacksHolder.invalidateActionsFor(ref);
}
@Override
public void undo(@Nullable FileEditor editor) {
ApplicationManager.getApplication().assertIsDispatchThread();
LOG.assertTrue(isUndoAvailable(editor));
undoOrRedo(editor, true);
}
@Override
public void redo(@Nullable FileEditor editor) {
ApplicationManager.getApplication().assertIsDispatchThread();
LOG.assertTrue(isRedoAvailable(editor));
undoOrRedo(editor, false);
}
private void undoOrRedo(final FileEditor editor, final boolean isUndo) {
myCurrentOperationState = isUndo ? UNDO : REDO;
final RuntimeException[] exception = new RuntimeException[1];
Runnable executeUndoOrRedoAction = new Runnable() {
@Override
public void run() {
try {
if (myProject != null) {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
}
CopyPasteManager.getInstance().stopKillRings();
myMerger.undoOrRedo(editor, isUndo);
}
catch (RuntimeException ex) {
exception[0] = ex;
}
finally {
myCurrentOperationState = NONE;
}
}
};
String name = getUndoOrRedoActionNameAndDescription(editor, isUndoInProgress()).second;
CommandProcessor.getInstance()
.executeCommand(myProject, executeUndoOrRedoAction, name, null, myMerger.getUndoConfirmationPolicy());
if (exception[0] != null) throw exception[0];
}
@Override
public boolean isUndoInProgress() {
return myCurrentOperationState == UNDO;
}
@Override
public boolean isRedoInProgress() {
return myCurrentOperationState == REDO;
}
@Override
public boolean isUndoAvailable(@Nullable FileEditor editor) {
return isUndoOrRedoAvailable(editor, true);
}
@Override
public boolean isRedoAvailable(@Nullable FileEditor editor) {
return isUndoOrRedoAvailable(editor, false);
}
protected boolean isUndoOrRedoAvailable(@Nullable FileEditor editor, boolean undo) {
ApplicationManager.getApplication().assertIsDispatchThread();
Collection<DocumentReference> refs = getDocRefs(editor);
if (refs == null) {
return false;
}
return isUndoOrRedoAvailable(refs, undo);
}
public boolean isUndoOrRedoAvailable(@NotNull DocumentReference ref) {
Set<DocumentReference> refs = Collections.singleton(ref);
return isUndoOrRedoAvailable(refs, true) || isUndoOrRedoAvailable(refs, false);
}
private boolean isUndoOrRedoAvailable(@NotNull Collection<DocumentReference> refs, boolean isUndo) {
if (isUndo && myMerger.isUndoAvailable(refs)) return true;
UndoRedoStacksHolder stackHolder = getStackHolder(isUndo);
return stackHolder.canBeUndoneOrRedone(refs);
}
private static Collection<DocumentReference> getDocRefs(@Nullable FileEditor editor) {
if (editor instanceof TextEditor && ((TextEditor)editor).getEditor().isViewer()) {
return null;
}
if (editor == null) {
return Collections.emptyList();
}
return getDocumentReferences(editor);
}
@NotNull
static Set<DocumentReference> getDocumentReferences(@NotNull FileEditor editor) {
Set<DocumentReference> result = new THashSet<DocumentReference>();
if (editor instanceof DocumentReferenceProvider) {
result.addAll(((DocumentReferenceProvider)editor).getDocumentReferences());
return result;
}
Document[] documents = TextEditorProvider.getDocuments(editor);
if (documents != null) {
for (Document each : documents) {
Document original = getOriginal(each);
// KirillK : in AnAction.update we may have an editor with an invalid file
VirtualFile f = FileDocumentManager.getInstance().getFile(each);
if (f != null && !f.isValid()) continue;
result.add(DocumentReferenceManager.getInstance().create(original));
}
}
return result;
}
@NotNull
private UndoRedoStacksHolder getStackHolder(boolean isUndo) {
return isUndo ? myUndoStacksHolder : myRedoStacksHolder;
}
@Override
public Pair<String, String> getUndoActionNameAndDescription(FileEditor editor) {
return getUndoOrRedoActionNameAndDescription(editor, true);
}
@Override
public Pair<String, String> getRedoActionNameAndDescription(FileEditor editor) {
return getUndoOrRedoActionNameAndDescription(editor, false);
}
private Pair<String, String> getUndoOrRedoActionNameAndDescription(FileEditor editor, boolean undo) {
String desc = isUndoOrRedoAvailable(editor, undo) ? doFormatAvailableUndoRedoAction(editor, undo) : null;
if (desc == null) desc = "";
String shortActionName = StringUtil.first(desc, 30, true);
if (desc.length() == 0) {
desc = undo
? ActionsBundle.message("action.undo.description.empty")
: ActionsBundle.message("action.redo.description.empty");
}
return Pair.create((undo ? ActionsBundle.message("action.undo.text", shortActionName)
: ActionsBundle.message("action.redo.text", shortActionName)).trim(),
(undo ? ActionsBundle.message("action.undo.description", desc)
: ActionsBundle.message("action.redo.description", desc)).trim());
}
@Nullable
private String doFormatAvailableUndoRedoAction(FileEditor editor, boolean isUndo) {
Collection<DocumentReference> refs = getDocRefs(editor);
if (refs == null) return null;
if (isUndo && myMerger.isUndoAvailable(refs)) return myMerger.getCommandName();
return getStackHolder(isUndo).getLastAction(refs).getCommandName();
}
public UndoRedoStacksHolder getUndoStacksHolder() {
return myUndoStacksHolder;
}
public UndoRedoStacksHolder getRedoStacksHolder() {
return myRedoStacksHolder;
}
public int nextCommandTimestamp() {
return ++myCommandTimestamp;
}
static Document getOriginal(Document document) {
Document result = document.getUserData(FragmentContent.ORIGINAL_DOCUMENT);
return result == null ? document : result;
}
static boolean isCopy(Document d) {
return d.getUserData(FragmentContent.ORIGINAL_DOCUMENT) != null;
}
protected void compact() {
if (myCurrentOperationState == NONE && myCommandTimestamp % COMMAND_TO_RUN_COMPACT == 0) {
doCompact();
}
}
private void doCompact() {
Collection<DocumentReference> refs = collectReferencesWithoutMergers();
Collection<DocumentReference> openDocs = new HashSet<DocumentReference>();
for (DocumentReference each : refs) {
VirtualFile file = each.getFile();
if (file == null) {
Document document = each.getDocument();
if (document != null && EditorFactory.getInstance().getEditors(document, myProject).length > 0) {
openDocs.add(each);
}
}
else {
if (myProject != null && FileEditorManager.getInstance(myProject).isFileOpen(file)) {
openDocs.add(each);
}
}
}
refs.removeAll(openDocs);
if (refs.size() <= FREE_QUEUES_LIMIT) return;
DocumentReference[] backSorted = refs.toArray(new DocumentReference[refs.size()]);
Arrays.sort(backSorted, new Comparator<DocumentReference>() {
@Override
public int compare(DocumentReference a, DocumentReference b) {
return getLastCommandTimestamp(a) - getLastCommandTimestamp(b);
}
});
for (int i = 0; i < backSorted.length - FREE_QUEUES_LIMIT; i++) {
DocumentReference each = backSorted[i];
if (getLastCommandTimestamp(each) + COMMANDS_TO_KEEP_LIVE_QUEUES > myCommandTimestamp) break;
clearUndoRedoQueue(each);
}
}
private int getLastCommandTimestamp(DocumentReference ref) {
return Math.max(myUndoStacksHolder.getLastCommandTimestamp(ref), myRedoStacksHolder.getLastCommandTimestamp(ref));
}
private Collection<DocumentReference> collectReferencesWithoutMergers() {
Set<DocumentReference> result = new THashSet<DocumentReference>();
myUndoStacksHolder.collectAllAffectedDocuments(result);
myRedoStacksHolder.collectAllAffectedDocuments(result);
return result;
}
private void clearUndoRedoQueue(DocumentReference docRef) {
myMerger.flushCurrentCommand();
disposeCurrentMerger();
myUndoStacksHolder.clearStacks(false, Collections.singleton(docRef));
myRedoStacksHolder.clearStacks(false, Collections.singleton(docRef));
}
@TestOnly
public void setEditorProvider(CurrentEditorProvider p) {
myEditorProvider = p;
}
@TestOnly
public CurrentEditorProvider getEditorProvider() {
return myEditorProvider;
}
@TestOnly
public void dropHistoryInTests() {
flushMergers();
LOG.assertTrue(myCommandLevel == 0);
myUndoStacksHolder.clearAllStacksInTests();
myRedoStacksHolder.clearAllStacksInTests();
}
@TestOnly
private void flushMergers() {
// Run dummy command in order to flush all mergers...
CommandProcessor.getInstance()
.executeCommand(myProject, EmptyRunnable.getInstance(), CommonBundle.message("drop.undo.history.command.name"), null);
}
@TestOnly
public void flushCurrentCommandMerger() {
myMerger.flushCurrentCommand();
}
@TestOnly
public void clearUndoRedoQueueInTests(VirtualFile file) {
clearUndoRedoQueue(DocumentReferenceManager.getInstance().create(file));
}
@TestOnly
public void clearUndoRedoQueueInTests(Document document) {
clearUndoRedoQueue(DocumentReferenceManager.getInstance().create(document));
}
}