package jetbrains.mps.vcs.changesmanager.tree;
/*Generated by MPS */
import jetbrains.mps.ide.ui.tree.TreeMessageOwner;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import java.util.Map;
import com.intellij.openapi.vcs.FileStatus;
import jetbrains.mps.ide.ui.tree.TreeMessage;
import jetbrains.mps.internal.collections.runtime.MapSequence;
import java.util.HashMap;
import jetbrains.mps.vcs.changesmanager.CurrentDifferenceRegistry;
import jetbrains.mps.vcs.changesmanager.SimpleCommandQueue;
import jetbrains.mps.vcs.diff.changes.ModelChange;
import jetbrains.mps.ide.ui.tree.MPSTree;
import com.intellij.util.ui.update.MergingUpdateQueue;
import org.jetbrains.annotations.NotNull;
import com.intellij.openapi.vcs.FileStatusManager;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import org.jetbrains.mps.openapi.module.SRepository;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.internal.collections.runtime.Sequence;
import jetbrains.mps.vcs.changesmanager.tree.features.Feature;
import org.apache.log4j.Level;
import jetbrains.mps.util.AbstractComputeRunnable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.EditableSModel;
import jetbrains.mps.vcs.changesmanager.tree.features.ModelFeature;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import org.jetbrains.mps.openapi.model.SModelReference;
import jetbrains.mps.internal.collections.runtime.IWhereFilter;
import com.intellij.util.ui.update.Update;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.vcs.diff.changes.AddRootChange;
import com.intellij.openapi.project.Project;
import jetbrains.mps.vcs.changesmanager.BaseVersionUtil;
import jetbrains.mps.vcs.platform.util.ConflictsUtil;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.persistence.DataSource;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.extapi.persistence.FileDataSource;
import jetbrains.mps.persistence.FilePerRootDataSource;
import jetbrains.mps.ide.vfs.IdeaFile;
import com.intellij.openapi.vfs.VirtualFile;
import jetbrains.mps.ide.ui.tree.MPSTreeNodeListener;
import com.intellij.openapi.vcs.FileStatusListener;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.smodel.SModelFileTracker;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import org.jetbrains.mps.openapi.module.SModule;
import jetbrains.mps.internal.collections.runtime.CollectionSequence;
import jetbrains.mps.internal.collections.runtime.IVisitor;
import com.intellij.util.containers.MultiMap;
import jetbrains.mps.internal.collections.runtime.SetSequence;
public class TreeHighlighter implements TreeMessageOwner {
private static final Logger LOG = LogManager.getLogger(TreeHighlighter.class);
private Map<FileStatus, TreeMessage> myTreeMessages = MapSequence.fromMap(new HashMap<FileStatus, TreeMessage>());
private CurrentDifferenceRegistry myRegistry;
private SimpleCommandQueue myCommandQueue;
private FeatureForestMap<ModelChange> myMap;
private MPSTree myTree;
private TreeNodeFeatureExtractor myFeatureExtractor;
private boolean myInitialized;
private TreeHighlighter.MyTreeNodeListener myTreeNodeListener = new TreeHighlighter.MyTreeNodeListener();
private TreeHighlighter.MyFeatureForestMapListener myFeatureListener = new TreeHighlighter.MyFeatureForestMapListener();
private TreeHighlighter.MyFileStatusListener myFileStatusListener = new TreeHighlighter.MyFileStatusListener();
private TreeHighlighter.MyModelDisposeListener myGlobalModelListener;
private final TreeHighlighter.FeaturesHolder myFeaturesHolder = new TreeHighlighter.FeaturesHolder();
private MergingUpdateQueue myQueue = new MergingUpdateQueue("MPS Changes Manager RehighlightAll Watcher Queue", 500, true, null);
public TreeHighlighter(@NotNull CurrentDifferenceRegistry registry, @NotNull FeatureForestMapSupport featureForestMapSupport, @NotNull MPSTree tree, @NotNull TreeNodeFeatureExtractor featureExtractor, boolean removeNodesOnModelDisposal) {
myRegistry = registry;
myCommandQueue = registry.getCommandQueue();
myMap = featureForestMapSupport.getMap();
myTree = tree;
myFeatureExtractor = featureExtractor;
if (removeNodesOnModelDisposal) {
myGlobalModelListener = new TreeHighlighter.MyModelDisposeListener();
}
}
public synchronized void init() {
if (myInitialized) {
return;
}
myInitialized = true;
myMap.addListener(myFeatureListener);
myTree.addTreeNodeListener(myTreeNodeListener);
FileStatusManager.getInstance(myRegistry.getProject()).addFileStatusListener(myFileStatusListener);
if (myGlobalModelListener != null) {
new RepoListenerRegistrar(getProjectRepository(), myGlobalModelListener).attach();
}
getProjectRepository().getModelAccess().runReadInEDT(new Runnable() {
public void run() {
MPSTreeNode rootNode = myTree.getRootNode();
if (rootNode != null) {
registerNodeRecursively(rootNode);
}
}
});
}
public synchronized void dispose() {
if (!(myInitialized)) {
return;
}
myInitialized = false;
if (myGlobalModelListener != null) {
new RepoListenerRegistrar(getProjectRepository(), myGlobalModelListener).detach();
}
FileStatusManager.getInstance(myRegistry.getProject()).removeFileStatusListener(myFileStatusListener);
myTree.removeTreeNodeListener(myTreeNodeListener);
myMap.removeListener(myFeatureListener);
myQueue.dispose();
}
private SRepository getProjectRepository() {
return ProjectHelper.getProjectRepository(myRegistry.getProject());
}
private void registerNodeRecursively(@NotNull MPSTreeNode node) {
registerNode(node);
for (MPSTreeNode child : Sequence.fromIterable(node)) {
registerNodeRecursively(child);
}
}
private void registerNode(@NotNull final MPSTreeNode node) {
final Feature feature = myFeatureExtractor.getFeature(node);
if (feature != null) {
synchronized (myFeaturesHolder) {
myFeaturesHolder.putNodeWithFeature(feature, node);
}
myCommandQueue.runTask(new Runnable() {
public void run() {
final boolean featureIsStillThere;
synchronized (myFeaturesHolder) {
// check if node isn't already removed from tree
featureIsStillThere = myFeaturesHolder.getNodesByFeature(feature).contains(node);
}
if (featureIsStillThere) {
rehighlightNode(node, feature);
}
}
});
}
}
private void unregisterNode(@NotNull MPSTreeNode node) {
Feature feature = myFeatureExtractor.getFeature(node);
if (feature != null) {
synchronized (myFeaturesHolder) {
if (myFeaturesHolder.getNodesByFeature(feature).contains(node)) {
myFeaturesHolder.removeNodeWithFeature(feature, node);
} else {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error("trying to remove tree node which was not registered: " + node.getClass().getName() + " " + feature);
}
}
}
unhighlightNode(node);
}
}
private void unhighlightNode(@NotNull MPSTreeNode node) {
if (!(node.removeTreeMessages(this).isEmpty())) {
updatePresentation(node);
}
}
/**
* This method runs with model read lock, and shall own lock on myFeatureHolder as it might lead
* to a deadlock (MPSTree rebuilds itself in a model read, thus treeNodeAdded and registerNode keep model read + myFeatureHolder, and if this method
* is invoked with myFeatureHolder lock, then we get opposite order of the locks)
*/
private void rehighlightNode(@NotNull MPSTreeNode node, @NotNull final Feature feature) {
unhighlightNode(node);
AbstractComputeRunnable<TreeMessage> cr = new AbstractComputeRunnable<TreeMessage>() {
protected TreeMessage compute() {
SModel model = feature.getModelReference().resolve(getProjectRepository());
if (model instanceof EditableSModel && !(model.isReadOnly())) {
EditableSModel emd = (EditableSModel) model;
if (feature instanceof ModelFeature) {
// do not try to compute changes in case we need only model status
return getMessage(emd);
}
myRegistry.getCurrentDifference(emd).setEnabled(true);
ModelChange change = myMap.get(feature);
if (change == null) {
change = myMap.getAddedAncestorValue(feature);
}
if (change != null) {
return getMessage(change, emd);
} else if (myMap.isAncestorOfAddedFeature(feature)) {
return getMessage(FileStatus.MODIFIED);
}
}
return null;
}
};
getProjectRepository().getModelAccess().runReadAction(cr);
TreeMessage message = cr.getResult();
if (message != null) {
node.addTreeMessage(message);
updatePresentation(node);
}
}
private void updatePresentation(final MPSTreeNode treeNode) {
// schedules node update to run in correct thread
getProjectRepository().getModelAccess().runReadInEDT(new Runnable() {
public void run() {
treeNode.renewPresentation();
}
});
}
private void rehighlightFeature(@NotNull Feature feature) {
List<MPSTreeNode> toUpdate = new ArrayList<MPSTreeNode>();
synchronized (myFeaturesHolder) {
Collection<MPSTreeNode> nodesByFeature = myFeaturesHolder.getNodesByFeature(feature);
if (nodesByFeature != null) {
toUpdate.addAll(nodesByFeature);
}
}
for (MPSTreeNode node : ListSequence.fromList(toUpdate)) {
rehighlightNode(node, feature);
}
}
private void rehighlightFeatureAndDescendants(@NotNull final Feature feature) {
if (myTree.isDisposed()) {
return;
}
final List<Feature> toCheck = new ArrayList<Feature>();
synchronized (myFeaturesHolder) {
SModelReference modelRef = feature.getModelReference();
toCheck.addAll(myFeaturesHolder.getFeaturesByModelReference(modelRef));
}
final List<Feature> toUpdate = new ArrayList<Feature>();
toUpdate.add(feature);
getProjectRepository().getModelAccess().runReadAction(new Runnable() {
public void run() {
for (Feature anotherFeature : ListSequence.fromList(toCheck)) {
// getAncestors might require (see NodeFeature) model read access, which shall not be under myFeaturesHolder lock
if (Sequence.fromIterable(Sequence.fromArray(anotherFeature.getAncestors(getProjectRepository()))).any(new IWhereFilter<Feature>() {
public boolean accept(Feature a) {
return feature.equals(a);
}
})) {
toUpdate.add(anotherFeature);
}
}
}
});
for (Feature f : ListSequence.fromList(toUpdate)) {
rehighlightFeature(f);
}
}
private final Update rehighlightAllFeaturesUpdate = new Update(this) {
@Override
public void run() {
if (myRegistry.getProject().isDisposed()) {
return;
}
if (IMakeService.INSTANCE.isSessionActive()) {
// re-queue, it will be executed in next batch after delay
rehighlightAllFeaturesLater();
} else {
rehighlightAllFeaturesNow();
}
}
};
private void rehighlightAllFeaturesLater() {
myQueue.queue(rehighlightAllFeaturesUpdate);
}
private void rehighlightAllFeaturesNow() {
List<Feature> toUpdate = new ArrayList<Feature>();
synchronized (myFeaturesHolder) {
toUpdate.addAll(myFeaturesHolder.getAllModelFeatures());
}
for (Feature f : ListSequence.fromList(toUpdate)) {
rehighlightFeatureAndDescendants(f);
}
}
@NotNull
private TreeMessage getMessage(@NotNull FileStatus fileStatus) {
if (!(MapSequence.fromMap(myTreeMessages).containsKey(fileStatus))) {
MapSequence.fromMap(myTreeMessages).put(fileStatus, new TreeMessage(fileStatus.getColor(), null, this));
}
return MapSequence.fromMap(myTreeMessages).get(fileStatus);
}
@NotNull
private TreeMessage getMessage(@NotNull ModelChange modelChange, @NotNull EditableSModel modelDescriptor) {
switch (modelChange.getType()) {
case ADD:
if (modelChange instanceof AddRootChange) {
Project project = myRegistry.getProject();
FileStatus modelStatus = getModelFileStatus(modelDescriptor, project);
if (BaseVersionUtil.isAddedFileStatus(modelStatus)) {
return getMessage(modelStatus);
} else if (ConflictsUtil.isModelOrModuleConflicting(modelDescriptor, project)) {
return getMessage(FileStatus.MERGED_WITH_CONFLICTS);
}
}
return getMessage(FileStatus.ADDED);
case CHANGE:
return getMessage(FileStatus.MODIFIED);
default:
assert false;
return getMessage(FileStatus.MERGED_WITH_CONFLICTS);
}
}
@Nullable
private TreeMessage getMessage(@NotNull EditableSModel md) {
FileStatus status = getModelFileStatus(md, myRegistry.getProject());
return (status == null ? null : getMessage(status));
}
@Nullable
private static FileStatus getModelFileStatus(@NotNull EditableSModel ed, @NotNull Project project) {
DataSource ds = ed.getSource();
IFile file = null;
if (ds instanceof FileDataSource) {
file = ((FileDataSource) ds).getFile();
} else if (ds instanceof FilePerRootDataSource) {
file = ((FilePerRootDataSource) ds).getFile(FilePerRootDataSource.HEADER_FILE);
}
if (!(file instanceof IdeaFile)) {
if (LOG.isEnabledFor(Level.WARN)) {
LOG.warn("File " + file + " must be a project file and managed by IDEA FS");
}
return null;
}
VirtualFile vf = ((IdeaFile) file).getVirtualFile();
return (vf == null ? null : FileStatusManager.getInstance(project).getStatus(vf));
}
private class MyTreeNodeListener implements MPSTreeNodeListener {
public MyTreeNodeListener() {
}
@Override
public void treeNodeAdded(MPSTreeNode node, MPSTree tree) {
registerNode(node);
}
@Override
public void treeNodeRemoved(MPSTreeNode node, MPSTree tree) {
unregisterNode(node);
}
@Override
public void treeNodeUpdated(MPSTreeNode node, MPSTree tree) {
}
@Override
public void beforeTreeDisposed(MPSTree tree) {
TreeHighlighterFactory.getInstance(myRegistry.getProject()).unhighlightTree(myTree);
}
}
private class MyFeatureForestMapListener implements FeatureForestMapListener {
public MyFeatureForestMapListener() {
}
@Override
public void featureStateChanged(Feature feature) {
rehighlightFeatureAndDescendants(feature);
}
}
private class MyFileStatusListener implements FileStatusListener {
public MyFileStatusListener() {
}
@Override
public void fileStatusChanged(@NotNull VirtualFile file) {
IFile ifile = VirtualFileUtils.toIFile(file);
SModel emd = SModelFileTracker.getInstance(getProjectRepository()).findModel(ifile);
if (emd != null) {
rehighlightFeatureAndDescendants(new ModelFeature(emd.getReference()));
}
}
@Override
public void fileStatusesChanged() {
rehighlightAllFeaturesLater();
}
}
/**
* In fact, shall listen to specific models only (FeaturesHolder.myModelRefToFeatures.keySet), whole repository is bit too much
*/
private class MyModelDisposeListener extends SRepositoryContentAdapter {
@Override
protected boolean isIncluded(SModule module) {
return !(module.isReadOnly());
}
@Override
public void beforeModelRemoved(SModule module, SModel model) {
super.beforeModelRemoved(module, model);
SModelReference modelRef = model.getReference();
List<MPSTreeNode> obsoleteTreeNodes = ListSequence.fromList(new ArrayList<MPSTreeNode>());
synchronized (myFeaturesHolder) {
for (Feature f : ListSequence.fromList(myFeaturesHolder.getFeaturesByModelReference(modelRef))) {
if (!(f instanceof ModelFeature)) {
ListSequence.fromList(obsoleteTreeNodes).addSequence(CollectionSequence.fromCollection(myFeaturesHolder.getNodesByFeature(f)));
myFeaturesHolder.removeFeature(f);
}
}
}
ListSequence.fromList(obsoleteTreeNodes).visitAll(new IVisitor<MPSTreeNode>() {
public void visit(MPSTreeNode tn) {
unhighlightNode(tn);
}
});
}
}
private class FeaturesHolder {
private final MultiMap<Feature, MPSTreeNode> myFeatureToNodes = new MultiMap<Feature, MPSTreeNode>();
private final MultiMap<SModelReference, Feature> myModelRefToFeatures = new MultiMap<SModelReference, Feature>();
public FeaturesHolder() {
}
public void putNodeWithFeature(Feature feature, MPSTreeNode node) {
myFeatureToNodes.putValue(feature, node);
myModelRefToFeatures.putValue(feature.getModelReference(), feature);
}
public void removeNodeWithFeature(Feature feature, MPSTreeNode node) {
myFeatureToNodes.removeValue(feature, node);
if (myFeatureToNodes.get(feature).isEmpty()) {
myModelRefToFeatures.removeValue(feature.getModelReference(), feature);
}
}
public void removeFeature(Feature feature) {
myFeatureToNodes.remove(feature);
myModelRefToFeatures.removeValue(feature.getModelReference(), feature);
}
public Collection<MPSTreeNode> getNodesByFeature(Feature feature) {
return myFeatureToNodes.get(feature);
}
public List<Feature> getFeaturesByModelReference(SModelReference modelRef) {
List<Feature> features = ListSequence.fromList(new ArrayList<Feature>());
ListSequence.fromList(features).addSequence(CollectionSequence.fromCollection(myModelRefToFeatures.get(modelRef)));
return features;
}
public List<Feature> getAllModelFeatures() {
List<Feature> features = ListSequence.fromList(new ArrayList<Feature>());
for (Feature f : SetSequence.fromSet(myFeatureToNodes.keySet())) {
if (f instanceof ModelFeature) {
ListSequence.fromList(features).addElement(f);
}
}
return features;
}
}
}