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; } } }