package jetbrains.mps.idea.java.psi; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiCodeBlock; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiModifierList; import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiParameterList; import com.intellij.psi.PsiReferenceList; import com.intellij.psi.PsiTreeChangeEvent; import com.intellij.psi.PsiTypeParameter; import com.intellij.psi.PsiTypeParameterList; import com.intellij.psi.PsiWhiteSpace; import jetbrains.mps.ide.platform.watching.ReloadParticipant; import jetbrains.mps.idea.java.psi.JavaPsiListener.FSMove; import jetbrains.mps.idea.java.psi.JavaPsiListener.FSRename; import jetbrains.mps.idea.java.psi.JavaPsiListener.PsiEvent; import org.jetbrains.mps.openapi.util.ProgressMonitor; import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * danilla 5/25/13 */ public class PsiChangeProcessor extends ReloadParticipant { // Per project change data // The thing is there's only one instance of reload participant for a given participant class, // whereas PsiChangesWatcher is a project component (as PsiManager) private Map<Project, PsiChangeData> changeData = new HashMap<Project, PsiChangeData>(); public PsiChangeProcessor() { } @Override public boolean wantsToShowProgress() { // we'll not request progress indicator for psi updates return false; } // TODO look with what locks is it called @Override public void update(ProgressMonitor monitor) { monitor.start("PSI update", 1); Runnable notify = new Runnable() { @Override public void run() { try { for (Entry<Project, PsiChangeData> e : changeData.entrySet()) { final Project project = e.getKey(); final PsiChangeData change = e.getValue(); // we do update asynchronously, so we want to check if project is live yet if (project.isDisposed()) continue; project.getComponent(PsiChangesWatcher.class).notifyListeners(change); } } finally { // clean-up changeData = new HashMap<Project, PsiChangeData>(); } } }; notify.run(); // the following code shouldn't be needed any more, because update happens in reload session // let's leave it for now, and look how it works (including when from ReloadSession.flush()) // if (ModelAccess.instance().isInsideCommand()) { // notify.run(); // } else { // ModelAccess.instance().runUndoTransparentCommand(notify); // } monitor.done(); } @Override public boolean isEmpty() { for (PsiChangeData data : changeData.values()) { if (data.isNotEmpty()) return false; } return true; } // The following methods are called by PsiChangesWatcher when it receives a PSI event // We're not PsiTreeChangeAdapter ourselves for a reason: // we're a ReloadParticipant => we can be instantiated by ReloadManager itself and there's only // one instance of us per application, whereas psi listeners exist per project (as well as PsiManager) // todo filter out changes not related to stub structure /*package*/ void childAdded(final PsiTreeChangeEvent event) { if (!filter(event.getChild())) return; PsiChangeData data = projectData(event.getChild()); PsiElement elem = event.getChild(); if (elem instanceof PsiFileSystemItem) { data.created.add((PsiFileSystemItem) elem); } else { data.changed.add(elem.getContainingFile()); } } /*package*/ void childRemoved(PsiTreeChangeEvent event) { if (!(event.getChild() instanceof PsiFileSystemItem) && !filter(event.getParent())) { // can't use getChild() here as it's not valid any longer return; } // so, if fs item or passed filtering then proceed PsiChangeData data = projectData(event.getParent()); PsiElement elem = event.getChild(); if (elem instanceof PsiFileSystemItem) { data.removed.add((PsiFileSystemItem) elem); } else { // todo fix is parent is a file itself data.changed.add(event.getParent().getContainingFile()); } } /*package*/ void childReplaced(PsiTreeChangeEvent event) { // if both are uninteresting, only then ignore if (!filter(event.getOldChild()) && !filter(event.getNewChild())) return; PsiChangeData data = projectData(event.getNewChild()); // todo Q: should we check if it's PsiFile? data.changed.add(event.getNewChild().getContainingFile()); } /*package*/ void childrenChanged(PsiTreeChangeEvent event) { if (!filter(event.getParent())) return; if (event.getParent() instanceof PsiFile) { // it's some generic notification, we don't need it // (don't remember already what that means) return; } PsiChangeData data = projectData(event.getParent()); data.changed.add(event.getParent().getContainingFile()); } /*package*/ void childMoved(@NotNull PsiTreeChangeEvent event) { if (!filter(event.getChild())) return; PsiChangeData data = projectData(event.getChild()); PsiElement elem = event.getChild(); if (elem instanceof PsiFileSystemItem) { // file item; data.moved.add(new FSMove((PsiFileSystemItem) elem, (PsiFileSystemItem) event.getOldParent(), (PsiFileSystemItem) event.getNewParent())); } else { // todo what if old/new parent is PsiFileSystemItem ? data.changed.add(event.getOldParent().getContainingFile()); data.changed.add(event.getNewParent().getContainingFile()); } } /*package*/ void propertyChanged(@NotNull PsiTreeChangeEvent event) { if (!(event.getElement() instanceof PsiFileSystemItem && (PsiTreeChangeEvent.PROP_FILE_NAME.equals(event.getPropertyName()) || PsiTreeChangeEvent.PROP_DIRECTORY_NAME.equals(event.getPropertyName()))) ) { return; } PsiChangeData data = projectData(event.getElement()); FSRename rename = new FSRename((PsiFileSystemItem) event.getElement(), (String) event.getOldValue()); data.renamed.add(rename); } private PsiChangeData projectData(PsiElement subject) { Project project = subject.getProject(); PsiChangeData data = changeData.get(project); if (data == null) { data = new PsiChangeData(); changeData.put(project, data); } return data; } private boolean filter(PsiElement elem) { if (elem == null || elem instanceof PsiWhiteSpace) { return false; } if (elem instanceof PsiJavaFile || elem instanceof PsiDirectory) { return true; } PsiElement e = elem; do { if (interesting(e)) { return true; } if (notInteresting(e)) { return false; } e = e.getParent(); } while (e != null); return false; } private boolean interesting(PsiElement elem) { if (elem instanceof PsiClass || elem instanceof PsiMethod || elem instanceof PsiField || elem instanceof PsiParameterList || elem instanceof PsiParameter || elem instanceof PsiReferenceList // but not PsiReference ! || elem instanceof PsiModifierList || elem instanceof PsiModifier || elem instanceof PsiTypeParameterList || elem instanceof PsiTypeParameter) { return true; } return false; } private boolean notInteresting(PsiElement elem) { return elem instanceof PsiCodeBlock || elem instanceof PsiExpression; } } class PsiChangeData implements PsiEvent { Set<PsiFileSystemItem> created = new HashSet<PsiFileSystemItem>(); Set<PsiFileSystemItem> removed = new HashSet<PsiFileSystemItem>(); Set<FSMove> moved = new HashSet<FSMove>(); Set<FSRename> renamed = new HashSet<FSRename>(); Set<PsiFile> changed = new HashSet<PsiFile>(); @Override public Iterable<PsiFileSystemItem> getCreated() { return created; } @Override public Iterable<PsiFileSystemItem> getRemoved() { return removed; } @Override public Iterable<FSMove> getMoved() { return moved; } @Override public Iterable<FSRename> getRenamed() { return renamed; } @Override public Set<PsiFile> getChanged() { return changed; } boolean isNotEmpty() { return !(changed.isEmpty() && created.isEmpty() && renamed.isEmpty() && moved.isEmpty() && removed.isEmpty()); } }