blob: b925661ed9ae507481ed6abfeb1c7e3e1bafef1a [file] [log] [blame]
/*
* Copyright 2000-2011 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.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.command.undo.DocumentReference;
import com.intellij.openapi.command.undo.UndoableAction;
import com.intellij.openapi.command.undo.UnexpectedUndoException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
class UndoableGroup {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.command.impl.UndoableGroup");
private final String myCommandName;
private final boolean myGlobal;
private final int myCommandTimestamp;
private final boolean myTransparent;
private final List<UndoableAction> myActions;
private EditorAndState myStateBefore;
private EditorAndState myStateAfter;
private final Project myProject;
private final UndoConfirmationPolicy myConfirmationPolicy;
private boolean myValid;
public UndoableGroup(String commandName,
boolean isGlobal,
UndoManagerImpl manager,
EditorAndState stateBefore,
EditorAndState stateAfter,
List<UndoableAction> actions,
UndoConfirmationPolicy confirmationPolicy,
boolean transparent,
boolean valid) {
myCommandName = commandName;
myGlobal = isGlobal;
myCommandTimestamp = manager.nextCommandTimestamp();
myActions = actions;
myProject = manager.getProject();
myStateBefore = stateBefore;
myStateAfter = stateAfter;
myConfirmationPolicy = confirmationPolicy;
myTransparent = transparent;
myValid = valid;
composeStartFinishGroup(manager.getUndoStacksHolder());
}
public boolean isGlobal() {
return myGlobal;
}
public boolean isTransparent() {
return myTransparent;
}
public boolean isUndoable() {
for (UndoableAction action : myActions) {
if (action instanceof NonUndoableAction) return false;
}
return true;
}
public void undo() {
undoOrRedo(true);
}
public void redo() {
undoOrRedo(false);
}
private void undoOrRedo(boolean isUndo) {
LocalHistoryAction action;
if (myProject != null && isGlobal()) {
String actionName = CommonBundle.message(isUndo ? "local.vcs.action.name.undo.command" : "local.vcs.action.name.redo.command", myCommandName);
action = LocalHistory.getInstance().startAction(actionName);
}
else {
action = LocalHistoryAction.NULL;
}
try {
doUndoOrRedo(isUndo);
}
finally {
action.finish();
}
}
private void doUndoOrRedo(final boolean isUndo) {
final boolean wrapInBulkUpdate = myActions.size() > 50;
// perform undo action by action, setting bulk update flag if possible
// if multiple consecutive actions share a document, then set the bulk flag only once
final Set<DocumentEx> bulkDocuments = new THashSet<DocumentEx>();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
try {
for (final UndoableAction action : isUndo ? ContainerUtil.iterateBackward(myActions) : myActions) {
final Collection<DocumentEx> newDocuments;
if (wrapInBulkUpdate) {
newDocuments = new THashSet<DocumentEx>();
Set<DocumentEx> documentsToRemoveFromBulk = new THashSet<DocumentEx>(bulkDocuments);
DocumentReference[] affectedDocuments = action.getAffectedDocuments();
if (affectedDocuments != null) {
for (DocumentReference affectedDocument : affectedDocuments) {
VirtualFile file = affectedDocument.getFile();
if (file != null && !file.isValid()) continue;
DocumentEx document = (DocumentEx)affectedDocument.getDocument();
if (document == null) continue;
documentsToRemoveFromBulk.remove(document);
if (bulkDocuments.contains(document)) continue;
newDocuments.add(document);
document.setInBulkUpdate(true);
}
}
for (DocumentEx document : documentsToRemoveFromBulk) {
document.setInBulkUpdate(false);
}
bulkDocuments.removeAll(documentsToRemoveFromBulk);
bulkDocuments.addAll(newDocuments);
}
else {
newDocuments = Collections.emptyList();
}
if (isUndo) {
action.undo();
}
else {
action.redo();
}
}
for (DocumentEx bulkDocument : bulkDocuments) {
bulkDocument.setInBulkUpdate(false);
}
}
catch (UnexpectedUndoException e) {
reportUndoProblem(e, isUndo);
}
}
});
commitAllDocuments();
}
boolean isInsideStartFinishGroup(boolean isUndo, boolean isInsideStartFinishGroup) {
final List<FinishMarkAction> finishMarks = new ArrayList<FinishMarkAction>();
final List<StartMarkAction> startMarks = new ArrayList<StartMarkAction>();
for (UndoableAction action : myActions) {
if (action instanceof StartMarkAction) {
startMarks.add((StartMarkAction)action);
} else if (action instanceof FinishMarkAction) {
finishMarks.add((FinishMarkAction)action);
}
}
final int startNmb = startMarks.size();
final int finishNmb = finishMarks.size();
if (startNmb != finishNmb) {
if (isUndo) {
if (finishNmb > startNmb) {
return true;
}
else {
return false;
}
}
else {
if (startNmb > finishNmb) {
return true;
}
else {
return false;
}
}
}
return isInsideStartFinishGroup;
}
void composeStartFinishGroup(final UndoRedoStacksHolder holder) {
FinishMarkAction finishMark = getFinishMark();
if (finishMark != null) {
boolean global = false;
String commandName = null;
LinkedList<UndoableGroup> stack = holder.getStack(finishMark.getAffectedDocument());
for (Iterator<UndoableGroup> iterator = stack.descendingIterator(); iterator.hasNext(); ) {
UndoableGroup group = iterator.next();
if (group.isGlobal()) {
global = true;
commandName = group.getCommandName();
break;
}
if (group.getStartMark() != null) {
break;
}
}
if (global) {
finishMark.setGlobal(global);
finishMark.setCommandName(commandName);
}
}
}
private boolean shouldAskConfirmationForStartFinishGroup(boolean redo) {
if (redo) {
StartMarkAction mark = getStartMark();
if (mark != null) {
return mark.isGlobal();
}
}
else {
FinishMarkAction finishMark = getFinishMark();
if (finishMark != null) {
return finishMark.isGlobal();
}
}
return false;
}
private static void commitAllDocuments() {
for (Project p : ProjectManager.getInstance().getOpenProjects()) {
PsiDocumentManager.getInstance(p).commitAllDocuments();
}
}
private void reportUndoProblem(UnexpectedUndoException e, boolean isUndo) {
String title;
String message;
if (isUndo) {
title = CommonBundle.message("cannot.undo.dialog.title");
message = CommonBundle.message("cannot.undo.message");
}
else {
title = CommonBundle.message("cannot.redo.dialog.title");
message = CommonBundle.message("cannot.redo.message");
}
if (!ApplicationManager.getApplication().isUnitTestMode()) {
if (e.getMessage() != null) {
message += ".\n" + e.getMessage();
}
Messages.showMessageDialog(myProject, message, title, Messages.getErrorIcon());
}
else {
LOG.error(e);
}
}
@NotNull
public Collection<DocumentReference> getAffectedDocuments() {
Set<DocumentReference> result = new THashSet<DocumentReference>();
for (UndoableAction action : myActions) {
DocumentReference[] refs = action.getAffectedDocuments();
if (refs != null) Collections.addAll(result, refs);
}
return result;
}
public EditorAndState getStateBefore() {
return myStateBefore;
}
public EditorAndState getStateAfter() {
return myStateAfter;
}
public void setStateBefore(EditorAndState stateBefore) {
myStateBefore = stateBefore;
}
public void setStateAfter(EditorAndState stateAfter) {
myStateAfter = stateAfter;
}
public String getCommandName() {
for (UndoableAction action : myActions) {
if (action instanceof StartMarkAction) {
String commandName = ((StartMarkAction)action).getCommandName();
if (commandName != null) return commandName;
} else if (action instanceof FinishMarkAction) {
String commandName = ((FinishMarkAction)action).getCommandName();
if (commandName != null) return commandName;
}
}
return myCommandName;
}
public int getCommandTimestamp() {
return myCommandTimestamp;
}
@Nullable
public StartMarkAction getStartMark() {
for (UndoableAction action : myActions) {
if (action instanceof StartMarkAction) return (StartMarkAction)action;
}
return null;
}
@Nullable
public FinishMarkAction getFinishMark() {
for (UndoableAction action : myActions) {
if (action instanceof FinishMarkAction) return (FinishMarkAction)action;
}
return null;
}
public boolean shouldAskConfirmation(boolean redo) {
if (shouldAskConfirmationForStartFinishGroup(redo)) return true;
return myConfirmationPolicy == UndoConfirmationPolicy.REQUEST_CONFIRMATION ||
myConfirmationPolicy != UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION && myGlobal;
}
public void invalidateActionsFor(DocumentReference ref) {
if (getAffectedDocuments().contains(ref)) {
myValid = false;
}
}
public boolean isValid() {
return myValid;
}
public String toString() {
StringBuilder result = new StringBuilder("UndoableGroup[");
final boolean multiline = myActions.size() > 1;
if (multiline) result.append("\n");
result.append(StringUtil.join(myActions, new Function<UndoableAction, String>() {
@Override
public String fun(UndoableAction each) {
return (multiline ? " " : "") + each.toString();
}
}, ",\n"));
if (multiline) result.append("\n");
result.append("]");
return result.toString();
}
}