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