blob: 7b7e1da8ec16dc13a320875efd492dac4df72ae0 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ide.todo;
import com.intellij.ide.highlighter.HighlighterFactory;
import com.intellij.ide.projectView.ProjectViewNode;
import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
import com.intellij.ide.todo.nodes.TodoFileNode;
import com.intellij.ide.todo.nodes.TodoItemNode;
import com.intellij.ide.todo.nodes.TodoTreeHelper;
import com.intellij.ide.util.treeView.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.util.StatusBarProgress;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.AsyncResult;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiTodoSearchHelper;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.usageView.UsageTreeColorsScheme;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import java.util.*;
/**
* @author Vladimir Kondratyev
*/
public abstract class TodoTreeBuilder extends AbstractTreeBuilder {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.todo.TodoTreeBuilder");
protected final Project myProject;
/**
* All files that have T.O.D.O items are presented as tree. This tree help a lot
* to separate these files by directories.
*/
protected final FileTree myFileTree;
/**
* This set contains "dirty" files. File is "dirty" if it's currently not unknown
* whether the file contains T.O.D.O item or not. To determine this it's necessary
* to perform some (perhaps, CPU expensive) operation. These "dirty" files are
* validated in <code>validateCache()</code> method.
*/
protected final HashSet<VirtualFile> myDirtyFileSet;
protected final HashMap<VirtualFile, EditorHighlighter> myFile2Highlighter;
protected final PsiTodoSearchHelper mySearchHelper;
/**
* If this flag is false then the updateTree() method does nothing. But when
* the flag becomes true and myDirtyFileSet isn't empty the update is invoked.
* This is done for optimization reasons: if TodoPane is not visible then
* updates isn't invoked.
*/
private boolean myUpdatable;
/** Updates tree if containing files change VCS status. */
private final MyFileStatusListener myFileStatusListener;
TodoTreeBuilder(JTree tree, DefaultTreeModel treeModel, Project project) {
super(tree, treeModel, null, MyComparator.ourInstance, false);
myProject = project;
myFileTree = new FileTree();
myDirtyFileSet = new HashSet<VirtualFile>();
myFile2Highlighter = new HashMap<VirtualFile, EditorHighlighter>();
PsiManager psiManager = PsiManager.getInstance(myProject);
mySearchHelper = PsiTodoSearchHelper.SERVICE.getInstance(myProject);
psiManager.addPsiTreeChangeListener(new MyPsiTreeChangeListener());
myFileStatusListener = new MyFileStatusListener();
setCanYieldUpdate(true);
}
/**
* Initializes the builder. Subclasses should don't forget to call this method after constructor has
* been invoked.
*/
public final void init() {
TodoTreeStructure todoTreeStructure = createTreeStructure();
setTreeStructure(todoTreeStructure);
todoTreeStructure.setTreeBuilder(this);
try {
rebuildCache();
}
catch (IndexNotReadyException ignore) {}
initRootNode();
Object selectableElement = todoTreeStructure.getFirstSelectableElement();
if (selectableElement != null) {
buildNodeForElement(selectableElement);
DefaultMutableTreeNode node = getNodeForElement(selectableElement);
if (node != null) {
getTree().getSelectionModel().setSelectionPath(new TreePath(node.getPath()));
}
}
FileStatusManager.getInstance(myProject).addFileStatusListener(myFileStatusListener);
}
@Override
public final void dispose() {
FileStatusManager.getInstance(myProject).removeFileStatusListener(myFileStatusListener);
super.dispose();
}
final boolean isUpdatable() {
return myUpdatable;
}
/**
* Sets whether the builder updates the tree when data change.
*/
final void setUpdatable(boolean updatable) {
if (myUpdatable != updatable) {
myUpdatable = updatable;
if (updatable) {
DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
@Override
public void run() {
updateTree(false);
}
});
}
}
}
@NotNull
protected abstract TodoTreeStructure createTreeStructure();
@Override
protected boolean validateNode(final Object child) {
if (child instanceof ProjectViewNode) {
final ProjectViewNode projectViewNode = (ProjectViewNode)child;
projectViewNode.update();
if (projectViewNode.getValue() == null) {
return false;
}
}
return true;
}
public final TodoTreeStructure getTodoTreeStructure() {
return (TodoTreeStructure)getTreeStructure();
}
@Override
protected final AbstractTreeUpdater createUpdater() {
return new AbstractTreeUpdater(this) {
@Override
protected ActionCallback beforeUpdate(final TreeUpdatePass pass) {
if (!myDirtyFileSet.isEmpty()) { // suppress redundant cache validations
final AsyncResult callback = new AsyncResult();
DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
@Override
public void run() {
try {
validateCache();
getTodoTreeStructure().validateCache();
}
finally {
callback.setDone();
}
}
});
return callback;
}
return new ActionCallback.Done();
}
};
}
/**
* @return read-only iterator of all current PSI files that can contain TODOs.
* Don't invoke its <code>remove</code> method. For "removing" use <code>markFileAsDirty</code> method.
* <b>Note, that <code>next()</code> method of iterator can return <code>null</code> elements.</b>
* These <code>null</code> elements correspond to the invalid PSI files (PSI file cannot be found by
* virtual file, or virtual file is invalid).
* The reason why we return such "dirty" iterator is the performance.
*/
public Iterator<PsiFile> getAllFiles() {
final Iterator<VirtualFile> iterator = myFileTree.getFileIterator();
return new Iterator<PsiFile>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
@Nullable public PsiFile next() {
VirtualFile vFile = iterator.next();
if (vFile == null || !vFile.isValid()) {
return null;
}
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(vFile);
if (psiFile == null || !psiFile.isValid()) {
return null;
}
return psiFile;
}
@Override
public void remove() {
throw new IllegalArgumentException();
}
};
}
/**
* @return read-only iterator of all valid PSI files that can have T.O.D.O items
* and which are located under specified <code>psiDirectory</code>.
* @see com.intellij.ide.todo.FileTree#getFiles(com.intellij.openapi.vfs.VirtualFile)
*/
public Iterator<PsiFile> getFiles(PsiDirectory psiDirectory) {
return getFiles(psiDirectory, true);
}
/**
* @return read-only iterator of all valid PSI files that can have T.O.D.O items
* and which are located under specified <code>psiDirectory</code>.
* @see FileTree#getFiles(VirtualFile)
*/
public Iterator<PsiFile> getFiles(PsiDirectory psiDirectory, final boolean skip) {
List<VirtualFile> files = myFileTree.getFiles(psiDirectory.getVirtualFile());
List<PsiFile> psiFileList = new ArrayList<PsiFile>(files.size());
PsiManager psiManager = PsiManager.getInstance(myProject);
for (VirtualFile file : files) {
final Module module = ModuleUtilCore.findModuleForPsiElement(psiDirectory);
if (module != null) {
final boolean isInContent = ModuleRootManager.getInstance(module).getFileIndex().isInContent(file);
if (!isInContent) continue;
}
if (file.isValid()) {
PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null) {
final PsiDirectory directory = psiFile.getContainingDirectory();
if (directory == null || !skip || !TodoTreeHelper.getInstance(myProject).skipDirectory(directory)) {
psiFileList.add(psiFile);
}
}
}
}
return psiFileList.iterator();
}
/**
* @return read-only iterator of all valid PSI files that can have T.O.D.O items
* and which are located under specified <code>psiDirectory</code>.
* @see FileTree#getFiles(VirtualFile)
*/
public Iterator<PsiFile> getFilesUnderDirectory(PsiDirectory psiDirectory) {
List<VirtualFile> files = myFileTree.getFilesUnderDirectory(psiDirectory.getVirtualFile());
List<PsiFile> psiFileList = new ArrayList<PsiFile>(files.size());
PsiManager psiManager = PsiManager.getInstance(myProject);
for (VirtualFile file : files) {
final Module module = ModuleUtilCore.findModuleForPsiElement(psiDirectory);
if (module != null) {
final boolean isInContent = ModuleRootManager.getInstance(module).getFileIndex().isInContent(file);
if (!isInContent) continue;
}
if (file.isValid()) {
PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null) {
psiFileList.add(psiFile);
}
}
}
return psiFileList.iterator();
}
/**
* @return read-only iterator of all valid PSI files that can have T.O.D.O items
* and which in specified <code>module</code>.
* @see FileTree#getFiles(VirtualFile)
*/
public Iterator<PsiFile> getFiles(Module module) {
if (module.isDisposed()) return Collections.<PsiFile>emptyList().iterator();
ArrayList<PsiFile> psiFileList = new ArrayList<PsiFile>();
final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
final VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
for (VirtualFile virtualFile : contentRoots) {
List<VirtualFile> files = myFileTree.getFiles(virtualFile);
PsiManager psiManager = PsiManager.getInstance(myProject);
for (VirtualFile file : files) {
if (fileIndex.getModuleForFile(file) != module) continue;
if (file.isValid()) {
PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null) {
psiFileList.add(psiFile);
}
}
}
}
return psiFileList.iterator();
}
/**
* @return <code>true</code> if specified <code>psiFile</code> can contains too items.
* It means that file is in "dirty" file set or in "current" file set.
*/
private boolean canContainTodoItems(PsiFile psiFile) {
VirtualFile vFile = psiFile.getVirtualFile();
return myFileTree.contains(vFile) || myDirtyFileSet.contains(vFile);
}
/**
* Marks specified PsiFile as dirty. It means that file is being add into "dirty" file set.
* It presents in current file set also but the next validateCache call will validate this
* "dirty" file. This method should be invoked when any modifications inside the file
* have happened.
*/
private void markFileAsDirty(@NotNull PsiFile psiFile) {
VirtualFile vFile = psiFile.getVirtualFile();
if (vFile != null) { // If PSI file isn't valid then its VirtualFile can be null
myDirtyFileSet.add(vFile);
}
}
void rebuildCache(){
myFileTree.clear();
myDirtyFileSet.clear();
myFile2Highlighter.clear();
TodoTreeStructure treeStructure=getTodoTreeStructure();
PsiFile[] psiFiles= mySearchHelper.findFilesWithTodoItems();
for (PsiFile psiFile : psiFiles) {
if (mySearchHelper.getTodoItemsCount(psiFile) > 0 && treeStructure.accept(psiFile)) {
myFileTree.add(psiFile.getVirtualFile());
}
}
treeStructure.validateCache();
}
private void validateCache() {
TodoTreeStructure treeStructure = getTodoTreeStructure();
// First of all we need to update "dirty" file set.
for (Iterator<VirtualFile> i = myDirtyFileSet.iterator(); i.hasNext();) {
VirtualFile file = i.next();
PsiFile psiFile = file.isValid() ? PsiManager.getInstance(myProject).findFile(file) : null;
if (psiFile == null || !treeStructure.accept(psiFile)) {
if (myFileTree.contains(file)) {
myFileTree.removeFile(file);
if (myFile2Highlighter.containsKey(file)) { // highlighter isn't needed any more
myFile2Highlighter.remove(file);
}
}
}
else { // file is valid and contains T.O.D.O items
myFileTree.removeFile(file);
myFileTree.add(file); // file can be moved. remove/add calls move it to another place
if (myFile2Highlighter.containsKey(file)) { // update highlighter text
Document document = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
EditorHighlighter highlighter = myFile2Highlighter.get(file);
highlighter.setText(document.getCharsSequence());
}
}
i.remove();
}
LOG.assertTrue(myDirtyFileSet.isEmpty());
// Now myDirtyFileSet should be empty
}
@Override
protected boolean isAutoExpandNode(NodeDescriptor descriptor) {
return getTodoTreeStructure().isAutoExpandNode(descriptor);
}
@Override
protected boolean isAlwaysShowPlus(NodeDescriptor nodeDescriptor) {
final Object element= nodeDescriptor.getElement();
if (element instanceof TodoItemNode){
return false;
} else if(element instanceof PsiFileNode) {
try {
return getTodoTreeStructure().mySearchHelper.getTodoItemsCount(((PsiFileNode)element).getValue()) > 0;
}
catch (IndexNotReadyException e) {
return true;
}
}
return true;
}
/**
* @return first <code>SmartTodoItemPointer</code> that is the children (in depth) of the specified <code>element</code>.
* If <code>element</code> itself is a <code>TodoItem</code> then the method returns the <code>element</code>.
*/
public TodoItemNode getFirstPointerForElement(Object element) {
if (element instanceof TodoItemNode) {
return (TodoItemNode)element;
}
else {
Object[] children = getTreeStructure().getChildElements(element);
if (children.length == 0) {
return null;
}
Object firstChild = children[0];
if (firstChild instanceof TodoItemNode) {
return (TodoItemNode)firstChild;
}
else {
return getFirstPointerForElement(firstChild);
}
}
}
/**
* @return last <code>SmartTodoItemPointer</code> that is the children (in depth) of the specified <code>element</code>.
* If <code>element</code> itself is a <code>TodoItem</code> then the method returns the <code>element</code>.
*/
public TodoItemNode getLastPointerForElement(Object element) {
if (element instanceof TodoItemNode) {
return (TodoItemNode)element;
}
else {
Object[] children = getTreeStructure().getChildElements(element);
if (children.length == 0) {
return null;
}
Object firstChild = children[children.length - 1];
if (firstChild instanceof TodoItemNode) {
return (TodoItemNode)firstChild;
}
else {
return getLastPointerForElement(firstChild);
}
}
}
protected final void updateTree(boolean later) {
if (myUpdatable) {
getUpdater().addSubtreeToUpdate(getRootNode());
if (!later) {
getUpdater().performUpdate();
}
}
}
static PsiFile getFileForNode(DefaultMutableTreeNode node) {
Object obj = node.getUserObject();
if (obj instanceof TodoFileNode) {
return ((TodoFileNode)obj).getValue();
}
else if (obj instanceof TodoItemNode) {
SmartTodoItemPointer pointer = ((TodoItemNode)obj).getValue();
return pointer.getTodoItem().getFile();
}
return null;
}
void collapseAll() {
int row = getTree().getRowCount() - 1;
while (row > 0) {
getTree().collapseRow(row);
row--;
}
}
/**
* Sets whether packages are shown or not.
*/
void setShowPackages(boolean state) {
getTodoTreeStructure().setShownPackages(state);
ArrayList<Object> pathsToExpand = new ArrayList<Object>();
ArrayList<Object> pathsToSelect = new ArrayList<Object>();
TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
getTree().clearSelection();
getTodoTreeStructure().validateCache();
updateTree(false);
TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
}
/**
* @param state if <code>true</code> then view is in "flatten packages" mode.
*/
void setFlattenPackages(boolean state) {
ArrayList<Object> pathsToExpand = new ArrayList<Object>();
ArrayList<Object> pathsToSelect = new ArrayList<Object>();
TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
getTree().clearSelection();
TodoTreeStructure todoTreeStructure = getTodoTreeStructure();
todoTreeStructure.setFlattenPackages(state);
todoTreeStructure.validateCache();
updateTree(false);
TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
}
/**
* Sets new <code>TodoFilter</code>, rebuild whole the caches and immediately update the tree.
*
* @see TodoTreeStructure#setTodoFilter
*/
void setTodoFilter(TodoFilter filter) {
getTodoTreeStructure().setTodoFilter(filter);
rebuildCache();
updateTree(false);
}
/**
* @return next <code>TodoItem</code> for the passed <code>pointer</code>. Returns <code>null</code>
* if the <code>pointer</code> is the last t.o.d.o item in the tree.
*/
public TodoItemNode getNextPointer(TodoItemNode pointer) {
Object sibling = getNextSibling(pointer);
if (sibling == null) {
return null;
}
if (sibling instanceof TodoItemNode) {
return (TodoItemNode)sibling;
}
else {
return getFirstPointerForElement(sibling);
}
}
/**
* @return next sibling of the passed element. If there is no sibling then
* returns <code>null</code>.
*/
Object getNextSibling(Object obj) {
Object parent = getTreeStructure().getParentElement(obj);
if (parent == null) {
return null;
}
Object[] children = getTreeStructure().getChildElements(parent);
Arrays.sort(children, getUi().getNodeDescriptorComparator());
int idx = -1;
for (int i = 0; i < children.length; i++) {
if (obj.equals(children[i])) {
idx = i;
break;
}
}
if (idx == -1) {
return null;
}
if (idx < children.length - 1) {
return children[idx + 1];
}
// passed object is the last in the list. In this case we have to return first child of the
// next parent's sibling.
return getNextSibling(parent);
}
/**
* @return next <code>SmartTodoItemPointer</code> for the passed <code>pointer</code>. Returns <code>null</code>
* if the <code>pointer</code> is the last t.o.d.o item in the tree.
*/
public TodoItemNode getPreviousPointer(TodoItemNode pointer) {
Object sibling = getPreviousSibling(pointer);
if (sibling == null) {
return null;
}
if (sibling instanceof TodoItemNode) {
return (TodoItemNode)sibling;
}
else {
return getLastPointerForElement(sibling);
}
}
/**
* @return previous sibling of the element of passed type. If there is no sibling then
* returns <code>null</code>.
*/
Object getPreviousSibling(Object obj) {
Object parent = getTreeStructure().getParentElement(obj);
if (parent == null) {
return null;
}
Object[] children = getTreeStructure().getChildElements(parent);
Arrays.sort(children, getUi().getNodeDescriptorComparator());
int idx = -1;
for (int i = 0; i < children.length; i++) {
if (obj.equals(children[i])) {
idx = i;
break;
}
}
if (idx == -1) {
return null;
}
if (idx > 0) {
return children[idx - 1];
}
// passed object is the first in the list. In this case we have to return last child of the
// previous parent's sibling.
return getPreviousSibling(parent);
}
/**
* @return <code>SelectInEditorManager</code> for the specified <code>psiFile</code>. Highlighters are
* lazy created and initialized.
*/
public EditorHighlighter getHighlighter(PsiFile psiFile, Document document) {
VirtualFile file = psiFile.getVirtualFile();
if (myFile2Highlighter.containsKey(file)) {
return myFile2Highlighter.get(file);
}
else {
EditorHighlighter highlighter = HighlighterFactory.createHighlighter(UsageTreeColorsScheme.getInstance().getScheme(), file.getName(), myProject);
highlighter.setText(document.getCharsSequence());
myFile2Highlighter.put(file, highlighter);
return highlighter;
}
}
void setShowModules(boolean state) {
getTodoTreeStructure().setShownModules(state);
ArrayList<Object> pathsToExpand = new ArrayList<Object>();
ArrayList<Object> pathsToSelect = new ArrayList<Object>();
TreeBuilderUtil.storePaths(this, getRootNode(), pathsToExpand, pathsToSelect, true);
getTree().clearSelection();
getTodoTreeStructure().validateCache();
updateTree(false);
TreeBuilderUtil.restorePaths(this, pathsToExpand, pathsToSelect, true);
}
public boolean isDirectoryEmpty(@NotNull PsiDirectory psiDirectory){
return myFileTree.isDirectoryEmpty(psiDirectory.getVirtualFile());
}
@Override
@NotNull
protected ProgressIndicator createProgressIndicator() {
return new StatusBarProgress();
}
private static final class MyComparator implements Comparator<NodeDescriptor> {
public static final Comparator<NodeDescriptor> ourInstance = new MyComparator();
@Override
public int compare(NodeDescriptor descriptor1, NodeDescriptor descriptor2) {
int weight1 = descriptor1.getWeight();
int weight2 = descriptor2.getWeight();
if (weight1 != weight2) {
return weight1 - weight2;
}
else {
return descriptor1.getIndex() - descriptor2.getIndex();
}
}
}
private final class MyPsiTreeChangeListener extends PsiTreeChangeAdapter {
@Override
public void childAdded(@NotNull PsiTreeChangeEvent e) {
// If local modification
if (e.getFile() != null) {
markFileAsDirty(e.getFile());
updateTree(true);
return;
}
// If added element if PsiFile and it doesn't contains TODOs, then do nothing
PsiElement child = e.getChild();
if (!(child instanceof PsiFile)) {
return;
}
PsiFile psiFile = (PsiFile)e.getChild();
markFileAsDirty(psiFile);
updateTree(true);
}
@Override
public void beforeChildRemoval(@NotNull PsiTreeChangeEvent e) {
// local modification
final PsiFile file = e.getFile();
if (file != null) {
markFileAsDirty(file);
updateTree(true);
return;
}
PsiElement child = e.getChild();
if (child instanceof PsiFile) { // file will be removed
PsiFile psiFile = (PsiFile)child;
markFileAsDirty(psiFile);
updateTree(true);
}
else if (child instanceof PsiDirectory) { // directory will be removed
PsiDirectory psiDirectory = (PsiDirectory)child;
for (Iterator<PsiFile> i = getAllFiles(); i.hasNext();) {
PsiFile psiFile = i.next();
if (psiFile == null) { // skip invalid PSI files
continue;
}
if (PsiTreeUtil.isAncestor(psiDirectory, psiFile, true)) {
markFileAsDirty(psiFile);
}
}
updateTree(true);
}
else {
if (PsiTreeUtil.getParentOfType(child, PsiComment.class, false) != null) { // change inside comment
markFileAsDirty(child.getContainingFile());
updateTree(true);
}
}
}
@Override
public void childMoved(@NotNull PsiTreeChangeEvent e) {
if (e.getFile() != null) { // local change
markFileAsDirty(e.getFile());
updateTree(true);
return;
}
if (e.getChild() instanceof PsiFile) { // file was moved
PsiFile psiFile = (PsiFile)e.getChild();
if (!canContainTodoItems(psiFile)) { // moved file doesn't contain TODOs
return;
}
markFileAsDirty(psiFile);
updateTree(true);
}
else if (e.getChild() instanceof PsiDirectory) { // directory was moved. mark all its files as dirty.
PsiDirectory psiDirectory = (PsiDirectory)e.getChild();
boolean shouldUpdate = false;
for (Iterator<PsiFile> i = getAllFiles(); i.hasNext();) {
PsiFile psiFile = i.next();
if (psiFile == null) { // skip invalid PSI files
continue;
}
if (PsiTreeUtil.isAncestor(psiDirectory, psiFile, true)) {
markFileAsDirty(psiFile);
shouldUpdate = true;
}
}
if (shouldUpdate) {
updateTree(true);
}
}
}
@Override
public void childReplaced(@NotNull PsiTreeChangeEvent e) {
if (e.getFile() != null) {
markFileAsDirty(e.getFile());
updateTree(true);
}
}
@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent e) {
if (e.getFile() != null) {
markFileAsDirty(e.getFile());
updateTree(true);
}
}
@Override
public void propertyChanged(@NotNull PsiTreeChangeEvent e) {
String propertyName = e.getPropertyName();
if (propertyName.equals(PsiTreeChangeEvent.PROP_ROOTS)) { // rebuild all tree when source roots were changed
getUpdater().runBeforeUpdate(
new Runnable() {
@Override
public void run() {
DumbService.getInstance(myProject).runWhenSmart(new Runnable() {
@Override
public void run() {
rebuildCache();
}
});
}
}
);
updateTree(true);
}
else if (PsiTreeChangeEvent.PROP_WRITABLE.equals(propertyName) || PsiTreeChangeEvent.PROP_FILE_NAME.equals(propertyName)) {
PsiFile psiFile = (PsiFile)e.getElement();
if (!canContainTodoItems(psiFile)) { // don't do anything if file cannot contain to-do items
return;
}
updateTree(true);
}
else if (PsiTreeChangeEvent.PROP_DIRECTORY_NAME.equals(propertyName)) {
PsiDirectory psiDirectory = (PsiDirectory)e.getElement();
Iterator<PsiFile> iterator = getFiles(psiDirectory);
if (iterator.hasNext()) {
updateTree(true);
}
}
}
}
private final class MyFileStatusListener implements FileStatusListener {
@Override
public void fileStatusesChanged() {
updateTree(true);
}
@Override
public void fileStatusChanged(@NotNull VirtualFile virtualFile) {
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(virtualFile);
if (psiFile != null && canContainTodoItems(psiFile)) {
updateTree(true);
}
}
}
}