/*
* Copyright 2000-2017 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.history.LocalHistory;
import com.intellij.history.core.LocalHistoryFacade;
import com.intellij.history.core.changes.Change;
import com.intellij.history.core.changes.ContentChange;
import com.intellij.history.core.changes.StructuralChange;
import com.intellij.history.integration.IdeaGateway;
import com.intellij.history.integration.LocalHistoryImpl;
import com.intellij.openapi.command.undo.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.*;
import com.intellij.util.FileContentUtilCore;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
public class FileUndoProvider implements UndoProvider, VirtualFileListener {
public static final Logger LOG = Logger.getInstance(FileUndoProvider.class);
private final Key<DocumentReference> DELETION_WAS_UNDOABLE = new Key<>(FileUndoProvider.class.getName() + ".DeletionWasUndoable");
private final Project myProject;
private boolean myIsInsideCommand;
private LocalHistoryFacade myLocalHistory;
private IdeaGateway myGateway;
private long myLastChangeId;
@SuppressWarnings("UnusedDeclaration")
public FileUndoProvider() {
this(null);
}
private FileUndoProvider(Project project) {
myProject = project;
if (myProject == null) return;
LocalHistoryImpl localHistory = LocalHistoryImpl.getInstanceImpl();
myLocalHistory = localHistory.getFacade();
myGateway = localHistory.getGateway();
if (myLocalHistory == null || myGateway == null) return; // local history was not initialized (e.g. in headless environment)
localHistory.addVFSListenerAfterLocalHistoryOne(this, project);
myLocalHistory.addListener(new LocalHistoryFacade.Listener() {
@Override
public void changeAdded(Change c) {
if (!(c instanceof StructuralChange) || c instanceof ContentChange) return;
myLastChangeId = c.getId();
}
}, myProject);
}
@Override
public void commandStarted(Project p) {
if (myProject != p) return;
myIsInsideCommand = true;
}
@Override
public void commandFinished(Project p) {
if (myProject != p) return;
myIsInsideCommand = false;
}
@Override
public void fileCreated(@NotNull VirtualFileEvent e) {
processEvent(e);
}
@Override
public void propertyChanged(@NotNull VirtualFilePropertyEvent e) {
if (!e.getPropertyName().equals(VirtualFile.PROP_NAME)) return;
processEvent(e);
}
@Override
public void fileMoved(@NotNull VirtualFileMoveEvent e) {
processEvent(e);
}
private void processEvent(VirtualFileEvent e) {
if (!shouldProcess(e)) return;
if (isUndoable(e)) {
registerUndoableAction(e);
}
else {
registerNonUndoableAction(e);
}
}
@Override
public void beforeContentsChange(@NotNull VirtualFileEvent e) {
if (!shouldProcess(e)) return;
if (isUndoable(e)) return;
registerNonUndoableAction(e);
}
@Override
public void beforeFileDeletion(@NotNull VirtualFileEvent e) {
if (!shouldProcess(e)) {
invalidateActionsFor(e);
return;
}
if (isUndoable(e)) {
VirtualFile file = e.getFile();
file.putUserData(DELETION_WAS_UNDOABLE, createDocumentReference(e));
}
else {
registerNonUndoableAction(e);
}
}
@Override
public void fileDeleted(@NotNull VirtualFileEvent e) {
if (!shouldProcess(e)) return;
VirtualFile f = e.getFile();
DocumentReference ref = f.getUserData(DELETION_WAS_UNDOABLE);
if (ref != null) {
registerUndoableAction(ref);
f.putUserData(DELETION_WAS_UNDOABLE, null);
}
}
private boolean shouldProcess(VirtualFileEvent e) {
return !myProject.isDisposed() &&
LocalHistory.getInstance().isUnderControl(e.getFile()) &&
myIsInsideCommand &&
!FileContentUtilCore.FORCE_RELOAD_REQUESTOR.equals(e.getRequestor());
}
private static boolean isUndoable(VirtualFileEvent e) {
return !e.isFromRefresh() || e.getFile().getUserData(UndoConstants.FORCE_RECORD_UNDO) == Boolean.TRUE;
}
private void registerUndoableAction(VirtualFileEvent e) {
registerUndoableAction(createDocumentReference(e));
}
private void registerUndoableAction(DocumentReference ref) {
getUndoManager().undoableActionPerformed(new MyUndoableAction(ref));
}
private void registerNonUndoableAction(VirtualFileEvent e) {
getUndoManager().nonundoableActionPerformed(createDocumentReference(e), true);
}
private void invalidateActionsFor(VirtualFileEvent e) {
if (myProject == null || !myProject.isDisposed()) {
getUndoManager().invalidateActionsFor(createDocumentReference(e));
}
}
private static DocumentReference createDocumentReference(VirtualFileEvent e) {
return DocumentReferenceManager.getInstance().create(e.getFile());
}
private UndoManagerImpl getUndoManager() {
if (myProject != null) {
return (UndoManagerImpl)UndoManager.getInstance(myProject);
}
return (UndoManagerImpl)UndoManager.getGlobalInstance();
}
private class MyUndoableAction extends GlobalUndoableAction {
private ChangeRange myActionChangeRange;
private ChangeRange myUndoChangeRange;
MyUndoableAction(DocumentReference r) {
super(r);
myActionChangeRange = new ChangeRange(myGateway, myLocalHistory, myLastChangeId);
}
@Override
public void undo() throws UnexpectedUndoException {
try {
myUndoChangeRange = myActionChangeRange.revert(myUndoChangeRange);
}
catch (IOException e) {
LOG.warn(e);
throw new UnexpectedUndoException(e.getMessage());
}
}
@Override
public void redo() throws UnexpectedUndoException {
try {
myActionChangeRange = myUndoChangeRange.revert(myActionChangeRange);
}
catch (IOException e) {
LOG.warn(e);
throw new UnexpectedUndoException(e.getMessage());
}
}
}
}