mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
update usage node on exclude to fix IDEA-166924 'Exclude' in Find tool window does not exclude its children
This commit is contained in:
@@ -20,6 +20,10 @@ import com.intellij.find.findUsages.FindUsagesHandler;
|
||||
import com.intellij.find.findUsages.FindUsagesManager;
|
||||
import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter;
|
||||
import com.intellij.find.impl.FindManagerImpl;
|
||||
import com.intellij.ide.actions.exclusion.ExclusionHandler;
|
||||
import com.intellij.ide.impl.TypeSafeDataProviderAdapter;
|
||||
import com.intellij.openapi.actionSystem.DataProvider;
|
||||
import com.intellij.openapi.actionSystem.TypeSafeDataProvider;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.command.WriteCommandAction;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
@@ -38,7 +42,11 @@ import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCa
|
||||
import com.intellij.usageView.UsageInfo;
|
||||
import com.intellij.usages.*;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import com.intellij.util.ui.tree.TreeUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.tree.TreeNode;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -158,4 +166,101 @@ public class UsageViewTest extends LightPlatformCodeInsightFixtureTestCase {
|
||||
Set<Usage> usages = usageView.getUsages();
|
||||
assertEquals(2, usages.size());
|
||||
}
|
||||
|
||||
public void testExcludeUsageMustExcludeChildrenAndParents() throws Exception {
|
||||
PsiFile psiFile = myFixture.addFileToProject("X.java", "public class X{ int xxx; } //comment");
|
||||
Usage usage = new UsageInfo2UsageAdapter(new UsageInfo(psiFile, psiFile.getText().indexOf("xxx"), StringUtil.indexOfSubstringEnd(psiFile.getText(),"xxx")));
|
||||
|
||||
UsageViewImpl usageView =
|
||||
(UsageViewImpl)UsageViewManager.getInstance(getProject()).createUsageView(UsageTarget.EMPTY_ARRAY, new Usage[]{usage}, new UsageViewPresentation(), null);
|
||||
Disposer.register(myFixture.getTestRootDisposable(), usageView);
|
||||
|
||||
usageView.excludeUsages(new Usage[]{usage});
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
|
||||
Set<Node> excluded = new HashSet<>();
|
||||
Node[] usageNode = new Node[1];
|
||||
TreeUtil.traverse(usageView.getRoot(), node -> {
|
||||
if (((Node)node).isExcluded()) {
|
||||
excluded.add((Node)node);
|
||||
}
|
||||
if (node instanceof UsageNode && ((UsageNode)node).getUsage() == usage) {
|
||||
usageNode[0] = (UsageNode)node;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Set<Node> expectedExcluded = new HashSet<>();
|
||||
for (TreeNode n = usageNode[0]; n != usageView.getRoot(); n = n.getParent()) {
|
||||
expectedExcluded.add((Node)n);
|
||||
}
|
||||
assertEquals(expectedExcluded, excluded);
|
||||
|
||||
|
||||
usageView.includeUsages(new Usage[]{usage});
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
|
||||
excluded.clear();
|
||||
TreeUtil.traverse(usageView.getRoot(), node -> {
|
||||
if (((Node)node).isExcluded()) {
|
||||
excluded.add((Node)node);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
assertEmpty(excluded);
|
||||
}
|
||||
|
||||
public void testExcludeNodeMustExcludeChildrenAndParents() throws Exception {
|
||||
PsiFile psiFile = myFixture.addFileToProject("X.java", "public class X{ int xxx; } //comment");
|
||||
Usage usage = new UsageInfo2UsageAdapter(new UsageInfo(psiFile, psiFile.getText().indexOf("xxx"), StringUtil.indexOfSubstringEnd(psiFile.getText(),"xxx")));
|
||||
|
||||
UsageViewImpl usageView =
|
||||
(UsageViewImpl)UsageViewManager.getInstance(getProject()).createUsageView(UsageTarget.EMPTY_ARRAY, new Usage[]{usage}, new UsageViewPresentation(), null);
|
||||
Disposer.register(myFixture.getTestRootDisposable(), usageView);
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
|
||||
Node[] usageNode = new Node[1];
|
||||
TreeUtil.traverse(usageView.getRoot(), node -> {
|
||||
if (node instanceof UsageNode && ((UsageNode)node).getUsage() == usage) {
|
||||
usageNode[0] = (UsageNode)node;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
Node nodeToExclude = (Node)usageNode[0].getParent();
|
||||
|
||||
JComponent component = usageView.getComponent();
|
||||
DataProvider provider = new TypeSafeDataProviderAdapter((TypeSafeDataProvider)component);
|
||||
ExclusionHandler exclusionHandler = (ExclusionHandler)provider.getData(ExclusionHandler.EXCLUSION_HANDLER.getName());
|
||||
exclusionHandler.excludeNode(nodeToExclude);
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
|
||||
Set<Node> excluded = new HashSet<>();
|
||||
TreeUtil.traverse(usageView.getRoot(), node -> {
|
||||
if (((Node)node).isExcluded()) {
|
||||
excluded.add((Node)node);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Set<Node> expectedExcluded = new HashSet<>();
|
||||
for (TreeNode n = usageNode[0]; n != usageView.getRoot(); n = n.getParent()) {
|
||||
expectedExcluded.add((Node)n);
|
||||
}
|
||||
assertEquals(expectedExcluded, excluded);
|
||||
|
||||
|
||||
exclusionHandler.includeNode(nodeToExclude);
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
|
||||
excluded.clear();
|
||||
TreeUtil.traverse(usageView.getRoot(), node -> {
|
||||
if (((Node)node).isExcluded()) {
|
||||
excluded.add((Node)node);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
assertEmpty(excluded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,20 +31,21 @@ public abstract class Node extends DefaultMutableTreeNode {
|
||||
private int myCachedTextHash;
|
||||
|
||||
private byte myCachedFlags; // bit packed flags below:
|
||||
private static final byte INVALID_MASK = 1;
|
||||
private static final byte READ_ONLY_MASK = 1<<1;
|
||||
private static final byte READ_ONLY_COMPUTED_MASK = 1<<2;
|
||||
static final byte EXCLUDED_MASK = 1<<3;
|
||||
private static final byte UPDATED_MASK = 1<<4;
|
||||
|
||||
@MagicConstant(intValues = {INVALID_MASK, READ_ONLY_MASK, READ_ONLY_COMPUTED_MASK, EXCLUDED_MASK, UPDATED_MASK})
|
||||
private static final byte CACHED_INVALID_MASK = 1;
|
||||
private static final byte CACHED_READ_ONLY_MASK = 1 << 1;
|
||||
private static final byte READ_ONLY_COMPUTED_MASK = 1<<2;
|
||||
|
||||
@MagicConstant(intValues = {CACHED_INVALID_MASK, CACHED_READ_ONLY_MASK, READ_ONLY_COMPUTED_MASK, EXCLUDED_MASK, UPDATED_MASK})
|
||||
private @interface FlagConstant {}
|
||||
|
||||
boolean isFlagSet(@FlagConstant byte mask) {
|
||||
private boolean isFlagSet(@FlagConstant byte mask) {
|
||||
return BitUtil.isSet(myCachedFlags, mask);
|
||||
}
|
||||
|
||||
void setFlag(@FlagConstant byte mask, boolean value) {
|
||||
private void setFlag(@FlagConstant byte mask, boolean value) {
|
||||
myCachedFlags = BitUtil.set(myCachedFlags, mask, value);
|
||||
}
|
||||
|
||||
@@ -69,19 +70,19 @@ public abstract class Node extends DefaultMutableTreeNode {
|
||||
protected abstract String getText(@NotNull UsageView view);
|
||||
|
||||
public final boolean isValid() {
|
||||
return !isFlagSet(INVALID_MASK);
|
||||
return !isFlagSet(CACHED_INVALID_MASK);
|
||||
}
|
||||
|
||||
public final boolean isReadOnly() {
|
||||
boolean result;
|
||||
boolean computed = isFlagSet(READ_ONLY_COMPUTED_MASK);
|
||||
if (computed) {
|
||||
result = isFlagSet(READ_ONLY_MASK);
|
||||
result = isFlagSet(CACHED_READ_ONLY_MASK);
|
||||
}
|
||||
else {
|
||||
result = isDataReadOnly();
|
||||
setFlag(READ_ONLY_COMPUTED_MASK, true);
|
||||
setFlag(READ_ONLY_MASK, result);
|
||||
setFlag(CACHED_READ_ONLY_MASK, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -93,20 +94,16 @@ public abstract class Node extends DefaultMutableTreeNode {
|
||||
final synchronized void update(@NotNull UsageView view, @NotNull Consumer<Node> edtNodeChangedQueue) {
|
||||
boolean isDataValid = isDataValid();
|
||||
boolean isReadOnly = isDataReadOnly();
|
||||
boolean isExcluded = isDataExcluded();
|
||||
String text = getText(view);
|
||||
|
||||
boolean cachedValid = isValid();
|
||||
boolean cachedReadOnly = isFlagSet(READ_ONLY_MASK);
|
||||
boolean cachedExcluded = isFlagSet(EXCLUDED_MASK);
|
||||
boolean cachedReadOnly = isFlagSet(CACHED_READ_ONLY_MASK);
|
||||
|
||||
if (isDataValid != cachedValid ||
|
||||
isReadOnly != cachedReadOnly ||
|
||||
isExcluded != cachedExcluded ||
|
||||
myCachedTextHash != text.hashCode()) {
|
||||
setFlag(INVALID_MASK, !isDataValid);
|
||||
setFlag(READ_ONLY_MASK, isReadOnly);
|
||||
setFlag(EXCLUDED_MASK, isExcluded);
|
||||
setFlag(CACHED_INVALID_MASK, !isDataValid);
|
||||
setFlag(CACHED_READ_ONLY_MASK, isReadOnly);
|
||||
|
||||
myCachedTextHash = text.hashCode();
|
||||
updateNotify();
|
||||
@@ -131,8 +128,13 @@ public abstract class Node extends DefaultMutableTreeNode {
|
||||
// same as DefaultMutableTreeNode.insert() except it doesn't try to remove the newChild from its parent since we know it's new
|
||||
void insertNewNode(@NotNull Node newChild, int childIndex) {
|
||||
if (children == null) {
|
||||
children = new Vector();
|
||||
children = new Vector();
|
||||
}
|
||||
children.insertElementAt(newChild, childIndex);
|
||||
}
|
||||
|
||||
void setExcluded(boolean excluded, @NotNull Consumer<Node> edtNodeChangedQueue) {
|
||||
setFlag(EXCLUDED_MASK, excluded);
|
||||
edtNodeChangedQueue.consume(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.Arrays;
|
||||
*/
|
||||
public class UsageNode extends Node implements Comparable<UsageNode>, Navigatable {
|
||||
@Deprecated
|
||||
// todo remove in 2018.1
|
||||
public UsageNode(@NotNull Usage usage, UsageViewTreeModelBuilder model) {
|
||||
this(null, usage);
|
||||
}
|
||||
@@ -98,8 +99,4 @@ public class UsageNode extends Node implements Comparable<UsageNode>, Navigatabl
|
||||
return Arrays.asList(getUsage().getPresentation().getText()).toString();
|
||||
}
|
||||
}
|
||||
|
||||
void setUsageExcluded(boolean usageExcluded) {
|
||||
setFlag(EXCLUDED_MASK, usageExcluded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,7 @@ import com.intellij.usageView.UsageViewBundle;
|
||||
import com.intellij.usageView.UsageViewManager;
|
||||
import com.intellij.usages.*;
|
||||
import com.intellij.usages.rules.*;
|
||||
import com.intellij.util.Alarm;
|
||||
import com.intellij.util.Consumer;
|
||||
import com.intellij.util.EditSourceOnDoubleClickHandler;
|
||||
import com.intellij.util.ReflectionUtil;
|
||||
import com.intellij.util.*;
|
||||
import com.intellij.util.concurrency.EdtExecutorService;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.LinkedMultiMap;
|
||||
@@ -90,6 +87,8 @@ import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author max
|
||||
@@ -300,16 +299,38 @@ public class UsageViewImpl implements UsageView {
|
||||
|
||||
@Override
|
||||
public void excludeNode(@NotNull DefaultMutableTreeNode node) {
|
||||
final HashSet<Usage> usages = new HashSet<>();
|
||||
collectUsages(node, usages);
|
||||
excludeUsages(usages.toArray(new Usage[usages.size()]));
|
||||
Set<Node> nodes = new HashSet<>();
|
||||
collectAllChildNodes(node, nodes);
|
||||
collectParentNodes(node, nodes, true);
|
||||
setExcludeNodes(nodes, true);
|
||||
}
|
||||
|
||||
// include the parent if its all children (except the "node" itself) excluded flags are "almostAllChildrenExcluded"
|
||||
private void collectParentNodes(DefaultMutableTreeNode node, Set<Node> nodes, boolean almostAllChildrenExcluded) {
|
||||
TreeNode parent = node.getParent();
|
||||
if (parent == myRoot || !(parent instanceof GroupNode)) return;
|
||||
GroupNode parentNode = (GroupNode)parent;
|
||||
List<Node> otherNodes =
|
||||
parentNode.getChildren().stream().filter(n -> n.isExcluded() != almostAllChildrenExcluded).collect(Collectors.toList());
|
||||
if (otherNodes.size() == 1 && otherNodes.get(0) == node) {
|
||||
nodes.add(parentNode);
|
||||
collectParentNodes(parentNode, nodes, almostAllChildrenExcluded);
|
||||
}
|
||||
}
|
||||
|
||||
private void setExcludeNodes(@NotNull Set<Node> nodes, boolean excluded) {
|
||||
for (Node node : nodes) {
|
||||
node.setExcluded(excluded, edtNodeChangedQueue);
|
||||
}
|
||||
updateImmediatelyNodesUpToRoot(nodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void includeNode(@NotNull DefaultMutableTreeNode node) {
|
||||
final HashSet<Usage> usages = new HashSet<>();
|
||||
collectUsages(node, usages);
|
||||
includeUsages(usages.toArray(new Usage[usages.size()]));
|
||||
Set<Node> nodes = new HashSet<>();
|
||||
collectAllChildNodes(node, nodes);
|
||||
collectParentNodes(node, nodes, false);
|
||||
setExcludeNodes(nodes, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1085,13 +1106,7 @@ public class UsageViewImpl implements UsageView {
|
||||
|
||||
@Override
|
||||
public void removeUsagesBulk(@NotNull Collection<Usage> usages) {
|
||||
final Set<UsageNode> nodes = new THashSet<>(usages.size());
|
||||
for (Usage usage : usages) {
|
||||
UsageNode node = myUsageNodes.remove(usage);
|
||||
if (node != null && node != NULL_NODE) {
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
Set<UsageNode> nodes = usagesToNodes(usages.stream()).collect(Collectors.toSet());
|
||||
if (!nodes.isEmpty() && !myPresentation.isDetachedMode()) {
|
||||
UIUtil.invokeLaterIfNeeded(() -> {
|
||||
if (isDisposed) return;
|
||||
@@ -1112,44 +1127,30 @@ public class UsageViewImpl implements UsageView {
|
||||
|
||||
@Override
|
||||
public void includeUsages(@NotNull Usage[] usages) {
|
||||
List<TreeNode> nodes = new ArrayList<>(usages.length);
|
||||
for (Usage usage : usages) {
|
||||
final UsageNode node = myUsageNodes.get(usage);
|
||||
if (node != NULL_NODE && node != null) {
|
||||
node.setUsageExcluded(false);
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
updateImmediatelyNodesUpToRoot(nodes);
|
||||
usagesToNodes(Arrays.stream(usages))
|
||||
.forEach(myExclusionHandler::includeNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void excludeUsages(@NotNull Usage[] usages) {
|
||||
List<TreeNode> nodes = new ArrayList<>(usages.length);
|
||||
for (Usage usage : usages) {
|
||||
final UsageNode node = myUsageNodes.get(usage);
|
||||
if (node != NULL_NODE && node != null) {
|
||||
node.setUsageExcluded(true);
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
updateImmediatelyNodesUpToRoot(nodes);
|
||||
usagesToNodes(Arrays.stream(usages))
|
||||
.forEach(myExclusionHandler::excludeNode);
|
||||
}
|
||||
|
||||
private Stream<UsageNode> usagesToNodes(Stream<Usage> usages) {
|
||||
return usages
|
||||
.map(myUsageNodes::get)
|
||||
.filter(node -> node != NULL_NODE && node != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectUsages(@NotNull Usage[] usages) {
|
||||
List<TreePath> paths = new LinkedList<>();
|
||||
TreePath[] paths = usagesToNodes(Arrays.stream(usages))
|
||||
.map(node -> new TreePath(node.getPath()))
|
||||
.toArray(TreePath[]::new);
|
||||
|
||||
for (Usage usage : usages) {
|
||||
final UsageNode node = myUsageNodes.get(usage);
|
||||
|
||||
if (node != NULL_NODE && node != null) {
|
||||
paths.add(new TreePath(node.getPath()));
|
||||
}
|
||||
}
|
||||
|
||||
myTree.setSelectionPaths(paths.toArray(new TreePath[paths.size()]));
|
||||
if (!paths.isEmpty()) myTree.scrollPathToVisible(paths.get(0));
|
||||
myTree.setSelectionPaths(paths);
|
||||
if (paths.length != 0) myTree.scrollPathToVisible(paths[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1177,21 +1178,24 @@ public class UsageViewImpl implements UsageView {
|
||||
updateOnSelectionChanged();
|
||||
}
|
||||
|
||||
private void updateImmediatelyNodesUpToRoot(@NotNull List<TreeNode> nodes) {
|
||||
private void updateImmediatelyNodesUpToRoot(@NotNull Collection<Node> nodes) {
|
||||
ApplicationManager.getApplication().assertIsDispatchThread();
|
||||
if (myProject.isDisposed()) return;
|
||||
TreeNode root = (TreeNode)myTree.getModel().getRoot();
|
||||
|
||||
for (int i=0; i<nodes.size(); i++) {
|
||||
TreeNode node = nodes.get(i);
|
||||
if (node instanceof Node) {
|
||||
((Node)node).update(this, edtNodeChangedQueue);
|
||||
Set<Node> updated = new HashSet<>();
|
||||
while (true) {
|
||||
Set<Node> parents = new HashSet<>();
|
||||
for (Node node : nodes) {
|
||||
node.update(this, edtNodeChangedQueue);
|
||||
TreeNode parent = node.getParent();
|
||||
if (parent != root && parent != null) {
|
||||
nodes.add(parent);
|
||||
if (parent != root && parent instanceof Node && updated.add((Node)parent)) {
|
||||
parents.add((Node)parent);
|
||||
}
|
||||
}
|
||||
if (parents.isEmpty()) break;
|
||||
nodes = parents;
|
||||
}
|
||||
|
||||
updateImmediately();
|
||||
}
|
||||
|
||||
@@ -1369,7 +1373,7 @@ public class UsageViewImpl implements UsageView {
|
||||
return new MyPerformOperationRunnable(cannotMakeString, processRunnable, commandName, checkReadOnlyStatus);
|
||||
}
|
||||
|
||||
protected boolean allTargetsAreValid() {
|
||||
private boolean allTargetsAreValid() {
|
||||
for (UsageTarget target : myTargets) {
|
||||
if (!target.isValid()) {
|
||||
return false;
|
||||
@@ -1531,6 +1535,18 @@ public class UsageViewImpl implements UsageView {
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectAllChildNodes(@NotNull DefaultMutableTreeNode node, @NotNull Set<Node> nodes) {
|
||||
if (node instanceof Node) {
|
||||
nodes.add((Node)node);
|
||||
}
|
||||
|
||||
Enumeration enumeration = node.children();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
DefaultMutableTreeNode child = (DefaultMutableTreeNode)enumeration.nextElement();
|
||||
collectAllChildNodes(child, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private UsageTarget[] getSelectedUsageTargets() {
|
||||
ApplicationManager.getApplication().assertIsDispatchThread();
|
||||
|
||||
Reference in New Issue
Block a user