| /* |
| * 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.openapi.command.UndoConfirmationPolicy; |
| import com.intellij.openapi.command.undo.BasicUndoableAction; |
| import com.intellij.openapi.command.undo.DocumentReference; |
| import com.intellij.openapi.command.undo.UndoableAction; |
| import com.intellij.openapi.fileEditor.FileEditor; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.testFramework.LightVirtualFile; |
| import com.intellij.util.ArrayUtil; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| public class CommandMerger { |
| private final UndoManagerImpl myManager; |
| private Object myLastGroupId = null; |
| private boolean myForcedGlobal = false; |
| private boolean myTransparent = false; |
| private String myCommandName = null; |
| private boolean myValid = true; |
| private List<UndoableAction> myCurrentActions = new ArrayList<UndoableAction>(); |
| private Set<DocumentReference> myAllAffectedDocuments = new THashSet<DocumentReference>(); |
| private Set<DocumentReference> myAdditionalAffectedDocuments = new THashSet<DocumentReference>(); |
| private EditorAndState myStateBefore; |
| private EditorAndState myStateAfter; |
| private UndoConfirmationPolicy myUndoConfirmationPolicy = UndoConfirmationPolicy.DEFAULT; |
| |
| public CommandMerger(@NotNull UndoManagerImpl manager) { |
| myManager = manager; |
| } |
| |
| public CommandMerger(@NotNull UndoManagerImpl manager, boolean isTransparent) { |
| myManager = manager; |
| myTransparent = isTransparent; |
| } |
| |
| public String getCommandName() { |
| return myCommandName; |
| } |
| |
| public void addAction(@NotNull UndoableAction action) { |
| myCurrentActions.add(action); |
| DocumentReference[] refs = action.getAffectedDocuments(); |
| if (refs != null) { |
| Collections.addAll(myAllAffectedDocuments, refs); |
| } |
| myForcedGlobal |= action.isGlobal(); |
| } |
| |
| public void commandFinished(String commandName, Object groupId, CommandMerger nextCommandToMerge) { |
| if (!shouldMerge(groupId, nextCommandToMerge)) { |
| flushCurrentCommand(); |
| myManager.compact(); |
| } |
| merge(nextCommandToMerge); |
| |
| // we do not want to spoil redo stack in situation, when some 'transparent' actions occurred right after undo. |
| if (nextCommandToMerge.isTransparent() || !hasActions()) return; |
| |
| clearRedoStacks(nextCommandToMerge); |
| |
| myLastGroupId = groupId; |
| if (myCommandName == null) myCommandName = commandName; |
| } |
| |
| private boolean shouldMerge(Object groupId, CommandMerger nextCommandToMerge) { |
| if (isTransparent() || nextCommandToMerge.isTransparent()) { |
| return !hasActions() || !nextCommandToMerge.hasActions() || myAllAffectedDocuments.equals(nextCommandToMerge.myAllAffectedDocuments); |
| } |
| return !myForcedGlobal && !nextCommandToMerge.myForcedGlobal && canMergeGroup(groupId, myLastGroupId); |
| } |
| |
| public static boolean canMergeGroup(Object groupId, Object lastGroupId) { |
| return groupId != null && Comparing.equal(lastGroupId, groupId); |
| } |
| |
| private void merge(CommandMerger nextCommandToMerge) { |
| setBeforeState(nextCommandToMerge.myStateBefore); |
| myStateAfter = nextCommandToMerge.myStateAfter; |
| if (myTransparent) { // todo write test |
| if (nextCommandToMerge.hasActions()) { |
| myTransparent &= nextCommandToMerge.myTransparent; |
| } |
| } |
| else { |
| if (!hasActions()) { |
| myTransparent = nextCommandToMerge.myTransparent; |
| } |
| } |
| myValid &= nextCommandToMerge.myValid; |
| myForcedGlobal |= nextCommandToMerge.myForcedGlobal; |
| myCurrentActions.addAll(nextCommandToMerge.myCurrentActions); |
| myAllAffectedDocuments.addAll(nextCommandToMerge.myAllAffectedDocuments); |
| myAdditionalAffectedDocuments.addAll(nextCommandToMerge.myAdditionalAffectedDocuments); |
| mergeUndoConfirmationPolicy(nextCommandToMerge.getUndoConfirmationPolicy()); |
| } |
| |
| public void mergeUndoConfirmationPolicy(UndoConfirmationPolicy undoConfirmationPolicy) { |
| if (myUndoConfirmationPolicy == UndoConfirmationPolicy.DEFAULT) { |
| myUndoConfirmationPolicy = undoConfirmationPolicy; |
| } |
| else if (myUndoConfirmationPolicy == UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION) { |
| if (undoConfirmationPolicy == UndoConfirmationPolicy.REQUEST_CONFIRMATION) { |
| myUndoConfirmationPolicy = UndoConfirmationPolicy.REQUEST_CONFIRMATION; |
| } |
| } |
| } |
| |
| public void flushCurrentCommand() { |
| if (hasActions()) { |
| if (!myAdditionalAffectedDocuments.isEmpty()) { |
| DocumentReference[] refs = myAdditionalAffectedDocuments.toArray(new DocumentReference[myAdditionalAffectedDocuments.size()]); |
| myCurrentActions.add(new BasicUndoableAction(refs) { |
| @Override |
| public void undo() { |
| } |
| |
| @Override |
| public void redo() { |
| } |
| }); |
| } |
| |
| myManager.getUndoStacksHolder().addToStacks(new UndoableGroup(myCommandName, |
| isGlobal(), |
| myManager, |
| myStateBefore, |
| myStateAfter, |
| myCurrentActions, |
| myUndoConfirmationPolicy, |
| isTransparent(), |
| myValid)); |
| } |
| |
| reset(); |
| } |
| |
| private void reset() { |
| myCurrentActions = new ArrayList<UndoableAction>(); |
| myAllAffectedDocuments = new THashSet<DocumentReference>(); |
| myAdditionalAffectedDocuments = new THashSet<DocumentReference>(); |
| myLastGroupId = null; |
| myForcedGlobal = false; |
| myTransparent = false; |
| myCommandName = null; |
| myValid = true; |
| myStateAfter = null; |
| myStateBefore = null; |
| myUndoConfirmationPolicy = UndoConfirmationPolicy.DEFAULT; |
| } |
| |
| private void clearRedoStacks(CommandMerger nextMerger) { |
| myManager.getRedoStacksHolder().clearStacks(isGlobal(), nextMerger.myAllAffectedDocuments); |
| } |
| |
| boolean isGlobal() { |
| return myForcedGlobal || affectsMultiplePhysicalDocs(); |
| } |
| |
| public void markAsGlobal() { |
| myForcedGlobal = true; |
| } |
| |
| public boolean isTransparent() { |
| return myTransparent; |
| } |
| |
| private boolean affectsMultiplePhysicalDocs() { |
| Set<VirtualFile> affectedFiles = new HashSet<VirtualFile>(); |
| for (DocumentReference each : myAllAffectedDocuments) { |
| VirtualFile file = each.getFile(); |
| if (isVirtualDocumentChange(file)) continue; |
| affectedFiles.add(file); |
| if (affectedFiles.size() > 1) return true; |
| } |
| return false; |
| } |
| |
| private static boolean isVirtualDocumentChange(VirtualFile file) { |
| return file == null || file instanceof LightVirtualFile; |
| } |
| |
| public void undoOrRedo(FileEditor editor, boolean isUndo) { |
| flushCurrentCommand(); |
| |
| // here we _undo_ (regardless 'isUndo' flag) and drop all 'transparent' actions made right after undoRedo/redo. |
| // Such actions should not get into redo/undoRedo stacks. Note that 'transparent' actions that have been merged with normal actions |
| // are not dropped, since this means they did not occur after undo/redo |
| UndoRedo undoRedo; |
| while ((undoRedo = createUndoOrRedo(editor, true)) != null) { |
| if (!undoRedo.isTransparent()) break; |
| if (!undoRedo.execute(true, false)) return; |
| if (!undoRedo.hasMoreActions()) break; |
| } |
| |
| boolean isInsideStartFinishGroup = false; |
| while ((undoRedo = createUndoOrRedo(editor, isUndo)) != null) { |
| if (!undoRedo.execute(false, isInsideStartFinishGroup)) return; |
| isInsideStartFinishGroup = undoRedo.myUndoableGroup.isInsideStartFinishGroup(isUndo, isInsideStartFinishGroup); |
| if (isInsideStartFinishGroup) continue; |
| boolean shouldRepeat = undoRedo.isTransparent() && undoRedo.hasMoreActions(); |
| if (!shouldRepeat) break; |
| } |
| } |
| |
| @Nullable |
| private UndoRedo createUndoOrRedo(FileEditor editor, boolean isUndo) { |
| if (!myManager.isUndoOrRedoAvailable(editor, isUndo)) return null; |
| return isUndo ? new Undo(myManager, editor) : new Redo(myManager, editor); |
| } |
| |
| public UndoConfirmationPolicy getUndoConfirmationPolicy() { |
| return myUndoConfirmationPolicy; |
| } |
| |
| public boolean hasActions() { |
| return !myCurrentActions.isEmpty(); |
| } |
| |
| public boolean isPhysical() { |
| if (myAllAffectedDocuments.isEmpty()) return false; |
| for (DocumentReference each : myAllAffectedDocuments) { |
| if (isVirtualDocumentChange(each.getFile())) return false; |
| } |
| return true; |
| } |
| |
| public boolean isUndoAvailable(@NotNull Collection<DocumentReference> refs) { |
| if (hasNonUndoableActions()) { |
| return false; |
| } |
| if (refs.isEmpty()) return isGlobal() && hasActions(); |
| |
| for (DocumentReference each : refs) { |
| if (hasChangesOf(each)) return true; |
| } |
| return false; |
| } |
| |
| private boolean hasNonUndoableActions() { |
| for (UndoableAction each : myCurrentActions) { |
| if (each instanceof NonUndoableAction) return true; |
| } |
| return false; |
| } |
| |
| public boolean hasChangesOf(DocumentReference ref) { |
| return hasChangesOf(ref, false); |
| } |
| |
| public boolean hasChangesOf(DocumentReference ref, boolean onlyDirectChanges) { |
| for (UndoableAction action : myCurrentActions) { |
| DocumentReference[] refs = action.getAffectedDocuments(); |
| if (refs == null) { |
| if (!onlyDirectChanges) return true; |
| } |
| else if (ArrayUtil.contains(ref, refs)) return true; |
| } |
| return hasActions() && myAdditionalAffectedDocuments.contains(ref); |
| } |
| |
| public void setBeforeState(EditorAndState state) { |
| if (myStateBefore == null || !hasActions()) { |
| myStateBefore = state; |
| } |
| } |
| |
| public void setAfterState(EditorAndState state) { |
| myStateAfter = state; |
| } |
| |
| public void addAdditionalAffectedDocuments(Collection<DocumentReference> refs) { |
| myAllAffectedDocuments.addAll(refs); |
| myAdditionalAffectedDocuments.addAll(refs); |
| } |
| |
| public void invalidateActionsFor(DocumentReference ref) { |
| if (myAllAffectedDocuments.contains(ref)) { |
| myValid = false; |
| } |
| } |
| } |