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