todo navigation: respect order (IDEA-51997)
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / todo / TodoTreeBuilder.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.ide.todo;
18
19 import com.intellij.ide.highlighter.HighlighterFactory;
20 import com.intellij.ide.projectView.ProjectViewNode;
21 import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
22 import com.intellij.ide.todo.nodes.TodoFileNode;
23 import com.intellij.ide.todo.nodes.TodoItemNode;
24 import com.intellij.ide.todo.nodes.TodoTreeHelper;
25 import com.intellij.ide.util.treeView.*;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
29 import com.intellij.openapi.module.Module;
30 import com.intellij.openapi.module.ModuleUtil;
31 import com.intellij.openapi.progress.ProgressIndicator;
32 import com.intellij.openapi.progress.util.StatusBarProgress;
33 import com.intellij.openapi.project.DumbService;
34 import com.intellij.openapi.project.IndexNotReadyException;
35 import com.intellij.openapi.project.Project;
36 import com.intellij.openapi.roots.ModuleRootManager;
37 import com.intellij.openapi.roots.ProjectFileIndex;
38 import com.intellij.openapi.roots.ProjectRootManager;
39 import com.intellij.openapi.util.ActionCallback;
40 import com.intellij.openapi.util.AsyncResult;
41 import com.intellij.openapi.vcs.FileStatusListener;
42 import com.intellij.openapi.vcs.FileStatusManager;
43 import com.intellij.openapi.vfs.VirtualFile;
44 import com.intellij.psi.*;
45 import com.intellij.psi.search.PsiSearchHelper;
46 import com.intellij.psi.util.PsiTreeUtil;
47 import com.intellij.usageView.UsageTreeColorsScheme;
48 import com.intellij.util.containers.HashMap;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import javax.swing.*;
53 import javax.swing.tree.DefaultMutableTreeNode;
54 import javax.swing.tree.DefaultTreeModel;
55 import javax.swing.tree.TreePath;
56 import java.util.*;
57
58 /**
59  * @author Vladimir Kondratyev
60  */
61 public abstract class TodoTreeBuilder extends AbstractTreeBuilder {
62   private static final Logger LOG = Logger.getInstance("#com.intellij.ide.todo.TodoTreeBuilder");
63   protected final Project myProject;
64
65   /**
66    * All files that have T.O.D.O items are presented as tree. This tree help a lot
67    * to separate these files by directories.
68    */
69   protected final FileTree myFileTree;
70   /**
71    * This set contains "dirty" files. File is "dirty" if it's currently not nkown
72    * whether the file contains T.O.D.O item or not. To determine this it's necessary
73    * to perform some (perhaps, CPU expensive) operation. These "dirty" files are
74    * validated in <code>validateCache()</code> method.
75    */
76   protected final HashSet<VirtualFile> myDirtyFileSet;
77
78   protected final HashMap<VirtualFile, EditorHighlighter> myFile2Highlighter;
79
80   protected final PsiSearchHelper mySearchHelper;
81   /**
82    * If this flag is false then the updateTree() method does nothing. But when
83    * the flag becomes true and myDirtyFileSet isn't empty the update is invoked.
84    * This is done for optimization reasons: if TodoPane is not visible then
85    * updates isn't invoked.
86    */
87   private boolean myUpdatable;
88
89   /** Updates tree if containing files change VCS status. */
90   private final MyFileStatusListener myFileStatusListener;
91
92   TodoTreeBuilder(JTree tree, DefaultTreeModel treeModel, Project project) {
93     super(tree, treeModel, null, MyComparator.ourInstance, false);
94     myProject = project;
95
96     myFileTree = new FileTree();
97     myDirtyFileSet = new HashSet<VirtualFile>();
98
99     myFile2Highlighter = new HashMap<VirtualFile, EditorHighlighter>();
100
101     PsiManager psiManager = PsiManager.getInstance(myProject);
102     mySearchHelper = PsiSearchHelper.SERVICE.getInstance(myProject);
103     psiManager.addPsiTreeChangeListener(new MyPsiTreeChangeListener());
104
105     myFileStatusListener = new MyFileStatusListener();
106
107     setCanYieldUpdate(true);
108   }
109
110   /**
111    * Initializes the builder. Subclasses should don't forget to call this method after constuctor has
112    * been invoked.
113    */
114   public final void init() {
115     TodoTreeStructure todoTreeStructure = createTreeStructure();
116     setTreeStructure(todoTreeStructure);
117     todoTreeStructure.setTreeBuilder(this);
118
119     rebuildCache();
120     initRootNode();
121
122     Object selectableElement = todoTreeStructure.getFirstSelectableElement();
123     if (selectableElement != null) {
124       buildNodeForElement(selectableElement);
125       DefaultMutableTreeNode node = getNodeForElement(selectableElement);
126       if (node != null) {
127         getTree().getSelectionModel().setSelectionPath(new TreePath(node.getPath()));
128       }
129     }
130
131     FileStatusManager.getInstance(myProject).addFileStatusListener(myFileStatusListener);
132   }
133
134   public final void dispose() {
135     FileStatusManager.getInstance(myProject).removeFileStatusListener(myFileStatusListener);
136     super.dispose();
137   }
138
139   final boolean isUpdatable() {
140     return myUpdatable;
141   }
142
143   /**
144    * Sets whenther the builder updates the tree when data change.
145    */
146   final void setUpdatable(boolean updatable) {
147     if (myUpdatable != updatable) {
148       myUpdatable = updatable;
149       if (updatable) {
150         DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
151           public void run() {
152             updateTree(false);
153           }
154         });
155       }
156     }
157   }
158
159   protected abstract TodoTreeStructure createTreeStructure();
160
161   protected boolean validateNode(final Object child) {
162     if (child instanceof ProjectViewNode) {
163       final ProjectViewNode projectViewNode = (ProjectViewNode)child;
164       projectViewNode.update();
165       if (projectViewNode.getValue() == null) {
166         return false;
167       }
168     }
169     return true;
170   }
171
172   public final TodoTreeStructure getTodoTreeStructure() {
173     return (TodoTreeStructure)getTreeStructure();
174   }
175
176   protected final AbstractTreeUpdater createUpdater() {
177     return new AbstractTreeUpdater(this) {
178       @Override
179       protected ActionCallback beforeUpdate(final TreeUpdatePass pass) {
180         if (!myDirtyFileSet.isEmpty()) { // suppress redundant cache validations
181           final AsyncResult callback = new AsyncResult();
182           DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
183             public void run() {
184               try {
185                 validateCache();
186                 getTodoTreeStructure().validateCache();
187               }
188               finally {
189                 callback.setDone();
190               }
191             }
192           });
193           return callback;
194         }
195
196         return new ActionCallback.Done();
197       }
198     };
199   }
200
201   /**
202    * @return read-only iterator of all current PSI files that can contain TODOs.
203    *         Don't invoke its <code>remove</code> method. For "removing" use <code>markFileAsDirty</code> method.
204    *         <b>Note, that <code>next()</code> method of iterator can return <code>null</code> elements.</b>
205    *         These <code>null</code> elements correspond to the invalid PSI files (PSI file cannot be found by
206    *         virtual file, or virtual file is invalid).
207    *         The reason why we return such "dirty" iterator is the peformance.
208    */
209   public Iterator<PsiFile> getAllFiles() {
210     final Iterator<VirtualFile> iterator = myFileTree.getFileIterator();
211     return new Iterator<PsiFile>() {
212       public boolean hasNext() {
213         return iterator.hasNext();
214       }
215
216       @Nullable public PsiFile next() {
217         VirtualFile vFile = iterator.next();
218         if (vFile == null || !vFile.isValid()) {
219           return null;
220         }
221         PsiFile psiFile = PsiManager.getInstance(myProject).findFile(vFile);
222         if (psiFile == null || !psiFile.isValid()) {
223           return null;
224         }
225         return psiFile;
226       }
227
228       public void remove() {
229         throw new IllegalArgumentException();
230       }
231     };
232   }
233
234   /**
235    * @return read-only iterator of all valid PSI files that can have T.O.D.O items
236    *         and which are located under specified <code>psiDirctory</code>.
237    * @see com.intellij.ide.todo.FileTree#getFiles(com.intellij.openapi.vfs.VirtualFile)
238    */
239   public Iterator<PsiFile> getFiles(PsiDirectory psiDirectory) {
240     return getFiles(psiDirectory, true);
241   }
242
243   /**
244    * @return read-only iterator of all valid PSI files that can have T.O.D.O items
245    *         and which are located under specified <code>psiDirctory</code>.
246    * @see FileTree#getFiles(VirtualFile)
247    */
248   public Iterator<PsiFile> getFiles(PsiDirectory psiDirectory, final boolean skip) {
249     ArrayList<VirtualFile> files = myFileTree.getFiles(psiDirectory.getVirtualFile());
250     ArrayList<PsiFile> psiFileList = new ArrayList<PsiFile>(files.size());
251     PsiManager psiManager = PsiManager.getInstance(myProject);
252     for (VirtualFile file : files) {
253       final Module module = ModuleUtil.findModuleForPsiElement(psiDirectory);
254       if (module != null) {
255         final boolean isInContent = ModuleRootManager.getInstance(module).getFileIndex().isInContent(file);
256         if (!isInContent) continue;
257       }
258       if (file.isValid()) {
259         PsiFile psiFile = psiManager.findFile(file);
260         if (psiFile != null) {
261           final PsiDirectory directory = psiFile.getContainingDirectory();
262           if (directory == null || !skip || !TodoTreeHelper.getInstance(myProject).skipDirectory(directory)) {
263             psiFileList.add(psiFile);
264           }
265         }
266       }
267     }
268     return psiFileList.iterator();
269   }
270
271   /**
272    * @return read-only iterator of all valid PSI files that can have T.O.D.O items
273    *         and which are located under specified <code>psiDirctory</code>.
274    * @see FileTree#getFiles(VirtualFile)
275    */
276   public Iterator<PsiFile> getFilesUnderDirectory(PsiDirectory psiDirectory) {
277     ArrayList<VirtualFile> files = myFileTree.getFilesUnderDirectory(psiDirectory.getVirtualFile());
278     ArrayList<PsiFile> psiFileList = new ArrayList<PsiFile>(files.size());
279     PsiManager psiManager = PsiManager.getInstance(myProject);
280     for (VirtualFile file : files) {
281       final Module module = ModuleUtil.findModuleForPsiElement(psiDirectory);
282       if (module != null) {
283         final boolean isInContent = ModuleRootManager.getInstance(module).getFileIndex().isInContent(file);
284         if (!isInContent) continue;
285       }
286       if (file.isValid()) {
287         PsiFile psiFile = psiManager.findFile(file);
288         if (psiFile != null) {
289           psiFileList.add(psiFile);
290         }
291       }
292     }
293     return psiFileList.iterator();
294   }
295
296
297
298   /**
299     * @return read-only iterator of all valid PSI files that can have T.O.D.O items
300     *         and which in specified <code>module</code>.
301     * @see FileTree#getFiles(VirtualFile)
302     */
303    public Iterator<PsiFile> getFiles(Module module) {
304     if (module.isDisposed()) return Collections.<PsiFile>emptyList().iterator();
305     ArrayList<PsiFile> psiFileList = new ArrayList<PsiFile>();
306     final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
307     final VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
308     for (VirtualFile virtualFile : contentRoots) {
309       ArrayList<VirtualFile> files = myFileTree.getFiles(virtualFile);
310       PsiManager psiManager = PsiManager.getInstance(myProject);
311       for (VirtualFile file : files) {
312         if (fileIndex.getModuleForFile(file) != module) continue;
313         if (file.isValid()) {
314           PsiFile psiFile = psiManager.findFile(file);
315           if (psiFile != null) {
316             psiFileList.add(psiFile);
317           }
318         }
319       }
320     }
321     return psiFileList.iterator();
322    }
323
324
325   /**
326    * @return <code>true</code> if specified <code>psiFile</code> can contains too items.
327    *         It means that file is in "dirty" file set or in "current" file set.
328    */
329   private boolean canContainTodoItems(PsiFile psiFile) {
330     VirtualFile vFile = psiFile.getVirtualFile();
331     return myFileTree.contains(vFile) || myDirtyFileSet.contains(vFile);
332   }
333
334   /**
335    * Marks specified PsiFile as dirty. It means that file is being add into "dirty" file set.
336    * It presents in current file set also but the next validateCache call will validate this
337    * "dirty" file. This method should be invoked when any modifications inside the file
338    * have happend.
339    */
340   private void markFileAsDirty(@NotNull PsiFile psiFile) {
341     VirtualFile vFile = psiFile.getVirtualFile();
342     if (vFile != null) { // If PSI file isn't valid then its VirtualFile can be null
343         myDirtyFileSet.add(vFile);
344     }
345   }
346
347   /**
348    * Clear and rebuild whole the caches. It means that after rebuilding all caches are valid.
349    */
350   abstract void rebuildCache();
351
352   private void validateCache() {
353     TodoTreeStructure treeStructure = getTodoTreeStructure();
354     // First of all we need to update "dirty" file set.
355     for (Iterator<VirtualFile> i = myDirtyFileSet.iterator(); i.hasNext();) {
356       VirtualFile file = i.next();
357       PsiFile psiFile = file.isValid() ? PsiManager.getInstance(myProject).findFile(file) : null;
358       if (psiFile == null || !treeStructure.accept(psiFile)) {
359         if (myFileTree.contains(file)) {
360           myFileTree.removeFile(file);
361           if (myFile2Highlighter.containsKey(file)) { // highlighter isn't needed any more
362             myFile2Highlighter.remove(file);
363           }
364         }
365       }
366       else { // file is valid and contains T.O.D.O items
367         myFileTree.removeFile(file);
368         myFileTree.add(file); // file can be moved. remove/add calls move it to another place
369         if (myFile2Highlighter.containsKey(file)) { // update highlighter's text
370           Document document = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
371           EditorHighlighter highlighter = myFile2Highlighter.get(file);
372           highlighter.setText(document.getCharsSequence());
373         }
374       }
375       i.remove();
376     }
377     LOG.assertTrue(myDirtyFileSet.isEmpty());
378     // Now myDirtyFileSet should be empty
379   }
380
381   protected boolean isAutoExpandNode(NodeDescriptor descriptor) {
382     return getTodoTreeStructure().isAutoExpandNode(descriptor);
383   }
384
385   protected boolean isAlwaysShowPlus(NodeDescriptor nodeDescriptor) {
386     final Object element= nodeDescriptor.getElement();
387     if (element instanceof TodoItemNode){
388       return false;
389     } else if(element instanceof PsiFileNode) {
390       try {
391         return getTodoTreeStructure().mySearchHelper.getTodoItemsCount(((PsiFileNode)element).getValue()) > 0;
392       }
393       catch (IndexNotReadyException e) {
394         return true;
395       }
396     }
397     return true;
398   }
399
400   /**
401    * @return first <code>SmartTodoItemPointer</code> that is the children (in depth) of the specified <code>element</code>.
402    *         If <code>element</code> itself is a <code>TodoItem</code> then the method returns the <code>element</code>.
403    */
404   public TodoItemNode getFirstPointerForElement(Object element) {
405     if (element instanceof TodoItemNode) {
406       return (TodoItemNode)element;
407     }
408     else {
409       Object[] children = getTreeStructure().getChildElements(element);
410       if (children.length == 0) {
411         return null;
412       }
413       Object firstChild = children[0];
414       if (firstChild instanceof TodoItemNode) {
415         return (TodoItemNode)firstChild;
416       }
417       else {
418         return getFirstPointerForElement(firstChild);
419       }
420     }
421   }
422
423   /**
424    * @return last <code>SmartTodoItemPointer</code> that is the children (in depth) of the specified <code>element</code>.
425    *         If <code>element</code> itself is a <code>TodoItem</code> then the method returns the <code>element</code>.
426    */
427   public TodoItemNode getLastPointerForElement(Object element) {
428     if (element instanceof TodoItemNode) {
429       return (TodoItemNode)element;
430     }
431     else {
432       Object[] children = getTreeStructure().getChildElements(element);
433       if (children.length == 0) {
434         return null;
435       }
436       Object firstChild = children[children.length - 1];
437       if (firstChild instanceof TodoItemNode) {
438         return (TodoItemNode)firstChild;
439       }
440       else {
441         return getLastPointerForElement(firstChild);
442       }
443     }
444   }
445
446   protected final void updateTree(boolean later) {
447     if (myUpdatable) {
448       getUpdater().addSubtreeToUpdate(getRootNode());
449       if (!later) {
450         getUpdater().performUpdate();
451       }
452     }
453   }
454
455   static PsiFile getFileForNode(DefaultMutableTreeNode node) {
456     Object obj = node.getUserObject();
457     if (obj instanceof TodoFileNode) {
458       return ((TodoFileNode)obj).getValue();
459     }
460     else if (obj instanceof TodoItemNode) {
461       SmartTodoItemPointer pointer = ((TodoItemNode)obj).getValue();
462       return pointer.getTodoItem().getFile();
463     }
464     return null;
465   }
466
467   void collapseAll() {
468     int row = getTree().getRowCount() - 1;
469     while (row > 0) {
470       getTree().collapseRow(row);
471       row--;
472     }
473   }
474
475   /**
476    * Sets whether packages are shown or not.
477    */
478   void setShowPackages(boolean state) {
479     getTodoTreeStructure().setShownPackages(state);
480     ArrayList<Object> pathsToExpand = new ArrayList<Object>();
481     ArrayList<Object> pathsToSelect = new ArrayList<Object>();
482     TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
483     getTree().clearSelection();
484     getTodoTreeStructure().validateCache();
485     updateTree(false);
486     TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
487   }
488
489   /**
490    * @param state if <code>true</code> then view is in "flatten packages" mode.
491    */
492   void setFlattenPackages(boolean state) {
493     ArrayList<Object> pathsToExpand = new ArrayList<Object>();
494     ArrayList<Object> pathsToSelect = new ArrayList<Object>();
495     TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
496     getTree().clearSelection();
497     TodoTreeStructure todoTreeStructure = getTodoTreeStructure();
498     todoTreeStructure.setFlattenPackages(state);
499     todoTreeStructure.validateCache();
500     updateTree(false);
501     TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
502   }
503
504   /**
505    * Sets new <code>TodoFilter</code>, rebuild whole the caches and immediately update the tree.
506    *
507    * @see TodoTreeStructure#setTodoFilter
508    */
509   void setTodoFilter(TodoFilter filter) {
510     getTodoTreeStructure().setTodoFilter(filter);
511     rebuildCache();
512     updateTree(false);
513   }
514
515   /**
516    * @return next <code>TodoItem</code> for the passed <code>pointer</code>. Returns <code>null</code>
517    *         if the <code>pointer</code> is the last t.o.d.o item in the tree.
518    */
519   public TodoItemNode getNextPointer(TodoItemNode pointer) {
520     Object sibling = getNextSibling(pointer);
521     if (sibling == null) {
522       return null;
523     }
524     if (sibling instanceof TodoItemNode) {
525       return (TodoItemNode)sibling;
526     }
527     else {
528       return getFirstPointerForElement(sibling);
529     }
530   }
531
532   /**
533    * @return next sibling of the passed element. If there is no sibling then
534    *         returns <code>null</code>.
535    */
536   Object getNextSibling(Object obj) {
537     Object parent = getTreeStructure().getParentElement(obj);
538     if (parent == null) {
539       return null;
540     }
541     Object[] children = getTreeStructure().getChildElements(parent);
542     Arrays.sort(children, getUi().getNodeDescriptorComparator());
543     int idx = -1;
544     for (int i = 0; i < children.length; i++) {
545       if (obj.equals(children[i])) {
546         idx = i;
547         break;
548       }
549     }
550     if (idx == -1) {
551       return null;
552     }
553     if (idx < children.length - 1) {
554       return children[idx + 1];
555     }
556     // passed object is the last in the list. In this case we have to return first child of the
557     // next parent's sibling.
558     return getNextSibling(parent);
559   }
560
561   /**
562    * @return next <code>SmartTodoItemPointer</code> for the passed <code>pointer</code>. Returns <code>null</code>
563    *         if the <code>pointer</code> is the last t.o.d.o item in the tree.
564    */
565   public TodoItemNode getPreviousPointer(TodoItemNode pointer) {
566     Object sibling = getPreviousSibling(pointer);
567     if (sibling == null) {
568       return null;
569     }
570     if (sibling instanceof TodoItemNode) {
571       return (TodoItemNode)sibling;
572     }
573     else {
574       return getLastPointerForElement(sibling);
575     }
576   }
577
578   /**
579    * @return previous sibling of the element of passed type. If there is no sibling then
580    *         returns <code>null</code>.
581    */
582   Object getPreviousSibling(Object obj) {
583     Object parent = getTreeStructure().getParentElement(obj);
584     if (parent == null) {
585       return null;
586     }
587     Object[] children = getTreeStructure().getChildElements(parent);
588     Arrays.sort(children, getUi().getNodeDescriptorComparator());
589     int idx = -1;
590     for (int i = 0; i < children.length; i++) {
591       if (obj.equals(children[i])) {
592         idx = i;
593
594         break;
595       }
596     }
597     if (idx == -1) {
598       return null;
599     }
600     if (idx > 0) {
601       return children[idx - 1];
602     }
603     // passed object is the first in the list. In this case we have to return last child of the
604     // previous parent's sibling.
605     return getPreviousSibling(parent);
606   }
607
608   /**
609    * @return <code>SelectInEditorManager</code> for the specified <code>psiFile</code>. Highlighters are
610    *         lazy created and initialized.
611    */
612   public EditorHighlighter getHighlighter(PsiFile psiFile, Document document) {
613     VirtualFile file = psiFile.getVirtualFile();
614     if (myFile2Highlighter.containsKey(file)) {
615       return myFile2Highlighter.get(file);
616     }
617     else {
618       EditorHighlighter highlighter = HighlighterFactory.createHighlighter(UsageTreeColorsScheme.getInstance().getScheme(), file.getName(), myProject);
619       highlighter.setText(document.getCharsSequence());
620       myFile2Highlighter.put(file, highlighter);
621       return highlighter;
622     }
623   }
624
625   void setShowModules(boolean state) {
626     getTodoTreeStructure().setShownModules(state);
627     ArrayList<Object> pathsToExpand = new ArrayList<Object>();
628     ArrayList<Object> pathsToSelect = new ArrayList<Object>();
629     TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
630     getTree().clearSelection();
631     getTodoTreeStructure().validateCache();
632     updateTree(false);
633     TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
634   }
635
636   public boolean isDirectoryEmpty(@NotNull PsiDirectory psiDirectory){
637     return myFileTree.isDirectoryEmpty(psiDirectory.getVirtualFile());
638   }
639
640   @NotNull
641   protected ProgressIndicator createProgressIndicator() {
642     return new StatusBarProgress();
643   }
644
645   private static final class MyComparator implements Comparator<NodeDescriptor> {
646     public static final Comparator<NodeDescriptor> ourInstance = new MyComparator();
647
648     public int compare(NodeDescriptor descriptor1, NodeDescriptor descriptor2) {
649       int weight1 = descriptor1.getWeight();
650       int weight2 = descriptor2.getWeight();
651       if (weight1 != weight2) {
652         return weight1 - weight2;
653       }
654       else {
655         return descriptor1.getIndex() - descriptor2.getIndex();
656       }
657     }
658   }
659
660   private final class MyPsiTreeChangeListener extends PsiTreeChangeAdapter {
661     public void childAdded(PsiTreeChangeEvent e) {
662       // If local modification
663       if (e.getFile() != null) {
664         markFileAsDirty(e.getFile());
665         updateTree(true);
666         return;
667       }
668       // If added element if PsiFile and it doesn't contains TODOs, then do nothing
669       PsiElement child = e.getChild();
670       if (!(child instanceof PsiFile)) {
671         return;
672       }
673       PsiFile psiFile = (PsiFile)e.getChild();
674       markFileAsDirty(psiFile);
675       updateTree(true);
676     }
677
678     public void beforeChildRemoval(PsiTreeChangeEvent e) {
679       // If local midification
680       final PsiFile file = e.getFile();
681       if (file != null) {
682         markFileAsDirty(file);
683         updateTree(true);
684         return;
685       }
686       //
687       PsiElement child = e.getChild();
688       if (child instanceof PsiFile) { // file will be removed
689         PsiFile psiFile = (PsiFile)child;
690         markFileAsDirty(psiFile);
691         updateTree(true);
692       }
693       else if (child instanceof PsiDirectory) { // directory will be removed
694         PsiDirectory psiDirectory = (PsiDirectory)child;
695         for (Iterator<PsiFile> i = getAllFiles(); i.hasNext();) {
696           PsiFile psiFile = i.next();
697           if (psiFile == null) { // skip invalid PSI files
698             continue;
699           }
700           if (PsiTreeUtil.isAncestor(psiDirectory, psiFile, true)) {
701             markFileAsDirty(psiFile);
702           }
703         }
704         updateTree(true);
705       }
706       else {
707         if (PsiTreeUtil.getParentOfType(child, PsiComment.class, false) != null) { // change inside comment
708           markFileAsDirty(child.getContainingFile());
709           updateTree(true);
710         }
711       }
712     }
713
714     public void childMoved(PsiTreeChangeEvent e) {
715       if (e.getFile() != null) { // local change
716         markFileAsDirty(e.getFile());
717         updateTree(true);
718         return;
719       }
720       if (e.getChild() instanceof PsiFile) { // file was moved
721         PsiFile psiFile = (PsiFile)e.getChild();
722         if (!canContainTodoItems(psiFile)) { // moved file doesn't contain TODOs
723           return;
724         }
725         markFileAsDirty(psiFile);
726         updateTree(true);
727       }
728       else if (e.getChild() instanceof PsiDirectory) { // directory was moved. mark all its files as dirty.
729         PsiDirectory psiDirectory = (PsiDirectory)e.getChild();
730         boolean shouldUpdate = false;
731         for (Iterator<PsiFile> i = getAllFiles(); i.hasNext();) {
732           PsiFile psiFile = i.next();
733           if (psiFile == null) { // skip invalid PSI files
734             continue;
735           }
736           if (PsiTreeUtil.isAncestor(psiDirectory, psiFile, true)) {
737             markFileAsDirty(psiFile);
738             shouldUpdate = true;
739           }
740         }
741         if (shouldUpdate) {
742           updateTree(true);
743         }
744       }
745     }
746
747     public void childReplaced(PsiTreeChangeEvent e) {
748       if (e.getFile() != null) {
749         markFileAsDirty(e.getFile());
750         updateTree(true);
751       }
752     }
753
754     public void childrenChanged(PsiTreeChangeEvent e) {
755       if (e.getFile() != null) {
756         markFileAsDirty(e.getFile());
757         updateTree(true);
758       }
759     }
760
761     public void propertyChanged(PsiTreeChangeEvent e) {
762       String propertyName = e.getPropertyName();
763       if (propertyName.equals(PsiTreeChangeEvent.PROP_ROOTS)) { // rebuild all tree when source roots were changed
764         getUpdater().runBeforeUpdate(
765           new Runnable() {
766             public void run() {
767               DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
768                 @Override
769                 public void run() {
770                   rebuildCache();
771                 }
772               });
773             }
774           }
775         );
776         updateTree(true);
777       }
778       else if (PsiTreeChangeEvent.PROP_WRITABLE.equals(propertyName)) {
779         PsiFile psiFile = (PsiFile)e.getElement();
780         if (!canContainTodoItems(psiFile)) { // don't do anything if file cannot contain todos
781           return;
782         }
783         updateTree(true);
784       }
785       else if (PsiTreeChangeEvent.PROP_FILE_NAME.equals(propertyName)) {
786         PsiFile psiFile = (PsiFile)e.getElement();
787         if (!canContainTodoItems(psiFile)) {
788           return;
789         }
790         updateTree(true);
791       }
792       else if (PsiTreeChangeEvent.PROP_DIRECTORY_NAME.equals(propertyName)) {
793         PsiDirectory psiDirectory = (PsiDirectory)e.getElement();
794         Iterator<PsiFile> iterator = getFiles(psiDirectory);
795         if (iterator.hasNext()) {
796           updateTree(true);
797         }
798       }
799     }
800   }
801
802   private final class MyFileStatusListener implements FileStatusListener {
803     public void fileStatusesChanged() {
804       updateTree(true);
805     }
806
807     public void fileStatusChanged(@NotNull VirtualFile virtualFile) {
808       PsiFile psiFile = PsiManager.getInstance(myProject).findFile(virtualFile);
809       if (psiFile != null && canContainTodoItems(psiFile)) {
810         updateTree(true);
811       }
812     }
813   }
814 }