Separate file nesting algorithm from PsiFile and extend API for Rider

This commit is contained in:
Evgeniy.Stepanov
2017-10-03 17:54:51 +02:00
parent bf1e561431
commit eb10ad975d
4 changed files with 210 additions and 187 deletions
@@ -0,0 +1,190 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.projectView.impl;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* Helper component that stores nesting rules and apply them to files
* @see NestingTreeStructureProvider
*/
public class FileNestingBuilder {
public static FileNestingBuilder getInstance() {
return ServiceManager.getService(FileNestingBuilder.class);
}
private long myBaseListModCount = -1;
private Set<ProjectViewFileNestingService.NestingRule> myNestingRules;
/**
* Returns all possible nesting rules, including transitive rules
*/
@NotNull
public Collection<ProjectViewFileNestingService.NestingRule> getNestingRules() {
final ProjectViewFileNestingService fileNestingService = ProjectViewFileNestingService.getInstance();
final List<ProjectViewFileNestingService.NestingRule> baseRules = fileNestingService.getRules();
final long modCount = fileNestingService.getModificationCount();
if (myNestingRules == null || myBaseListModCount != modCount) {
myNestingRules = new THashSet<>();
myBaseListModCount = modCount;
final MultiMap<String, String> childToParentSuffix = new MultiMap<>();
final MultiMap<String, String> parentToChildSuffix = new MultiMap<>();
for (ProjectViewFileNestingService.NestingRule rule : baseRules) {
final String parentFileSuffix = rule.getParentFileSuffix();
final String childFileSuffix = rule.getChildFileSuffix();
if (parentFileSuffix.isEmpty() || childFileSuffix.isEmpty()) continue; // shouldn't happen, checked on component loading and in UI
if (parentFileSuffix.equals(childFileSuffix)) continue; // shouldn't happen, checked on component loading and in UI
myNestingRules.add(rule);
childToParentSuffix.putValue(childFileSuffix, parentFileSuffix);
parentToChildSuffix.putValue(parentFileSuffix, childFileSuffix);
// for all cases like A -> B -> C we also add a rule A -> C
for (String s : parentToChildSuffix.get(childFileSuffix)) {
myNestingRules.add(new ProjectViewFileNestingService.NestingRule(parentFileSuffix, s));
parentToChildSuffix.putValue(parentFileSuffix, s);
childToParentSuffix.putValue(s, parentFileSuffix);
}
for (String s : childToParentSuffix.get(parentFileSuffix)) {
myNestingRules.add(new ProjectViewFileNestingService.NestingRule(s, childFileSuffix));
parentToChildSuffix.putValue(s, childFileSuffix);
childToParentSuffix.putValue(childFileSuffix, s);
}
}
}
return myNestingRules;
}
/*
This is a graph theory problem. T is a graph node that represents a file. fileNameFunc should return appropriate file name for a T node.
Edges go from parent file to child file according to NestingRules, for example foo.js->foo.min.js.
Parent may have several children. Child may have several parents.
There may be cycles with 3 or more nodes, but cycle with 2 nodes (A->B and B->A) is impossible because parentFileSuffix != childFileSuffix
For each child its outbound edges are removed. For example in case of a cycle all edges that form it are removed. In case of A->B->C only A->B remains.
As a result we get a number of separated parent-to-many-children sub-graphs, and use them to nest child files under parent file in Project View.
One child still may have more than one parent. For real use cases it is not expected to happen, but anyway it's not a big problem, it will be shown as a subnode more than once.
*/
@NotNull
public <T> MultiMap<T, T> mapParentToChildren(@NotNull final Collection<T> nodes,
@NotNull final Function<T, String> fileNameFunc) {
final Collection<ProjectViewFileNestingService.NestingRule> rules = getNestingRules();
if (rules.isEmpty()) return MultiMap.empty();
// result that will contain number of separated parent-to-many-children sub-graphs
MultiMap<T, T> parentToChildren = null;
Set<T> allChildNodes = null; // helps to remove all outbound edges of a node that has inbound edge itself
Map<Pair<String, ProjectViewFileNestingService.NestingRule>, Edge<T>> baseNameAndRuleToEdge = null; // temporary map for building edges
for (T node : nodes) {
final String fileName = fileNameFunc.apply(node);
if (fileName == null) continue;
for (ProjectViewFileNestingService.NestingRule rule : rules) {
final Couple<Boolean> c = checkMatchingAsParentOrChild(rule, fileName);
final boolean matchesParent = c.first;
final boolean matchesChild = c.second;
if (!matchesChild && !matchesParent) continue;
if (baseNameAndRuleToEdge == null) {
baseNameAndRuleToEdge = new THashMap<>();
parentToChildren = new MultiMap<>();
allChildNodes = new THashSet<>();
}
if (matchesParent) {
final String baseName = fileName.substring(0, fileName.length() - rule.getParentFileSuffix().length());
final Edge<T> edge = getOrCreateEdge(baseNameAndRuleToEdge, baseName, rule);
edge.from = node;
updateInfoIfEdgeComplete(parentToChildren, allChildNodes, edge);
}
if (matchesChild) {
final String baseName = fileName.substring(0, fileName.length() - rule.getChildFileSuffix().length());
final Edge<T> edge = getOrCreateEdge(baseNameAndRuleToEdge, baseName, rule);
edge.to = node;
updateInfoIfEdgeComplete(parentToChildren, allChildNodes, edge);
}
}
}
return parentToChildren == null ? MultiMap.empty() : parentToChildren;
}
/**
* Returns true if the rule applies to the file [as parent; as child] pair
*/
public static Couple<Boolean> checkMatchingAsParentOrChild(@NotNull final ProjectViewFileNestingService.NestingRule rule,
@NotNull final String fileName) {
String parentFileSuffix = rule.getParentFileSuffix();
String childFileSuffix = rule.getChildFileSuffix();
boolean matchesParent = !fileName.equalsIgnoreCase(parentFileSuffix) && StringUtil.endsWithIgnoreCase(fileName, parentFileSuffix);
boolean matchesChild = !fileName.equalsIgnoreCase(childFileSuffix) && StringUtil.endsWithIgnoreCase(fileName, childFileSuffix);
if (matchesParent && matchesChild) {
if (parentFileSuffix.length() > childFileSuffix.length()) {
matchesChild = false;
}
else {
matchesParent = false;
}
}
return Couple.of(matchesParent, matchesChild);
}
@NotNull
private static <T> Edge<T> getOrCreateEdge(@NotNull final Map<Pair<String, ProjectViewFileNestingService.NestingRule>, Edge<T>> baseNameAndRuleToEdge,
@NotNull final String baseName,
@NotNull final ProjectViewFileNestingService.NestingRule rule) {
final Pair<String, ProjectViewFileNestingService.NestingRule> baseNameAndRule = Pair.create(baseName, rule);
Edge<T> edge = baseNameAndRuleToEdge.get(baseNameAndRule);
if (edge == null) {
edge = new Edge<>();
baseNameAndRuleToEdge.put(baseNameAndRule, edge);
}
return edge;
}
private static <T> void updateInfoIfEdgeComplete(@NotNull final MultiMap<T, T> parentToChildren,
@NotNull final Set<T> allChildNodes,
@NotNull final Edge<T> edge) {
if (edge.from != null && edge.to != null) { // if edge complete
allChildNodes.add(edge.to);
parentToChildren.remove(edge.to); // nodes that appear as a child shouldn't be a parent of another edge, corresponding edges removed
if (!allChildNodes.contains(edge.from)) {
parentToChildren.putValue(edge.from, edge.to);
}
}
}
private static class Edge<T> {
@Nullable
private T from;
@Nullable
private T to;
}
}
@@ -41,7 +41,7 @@ public class FileNestingInProjectViewDialog extends DialogWrapper {
}
};
protected FileNestingInProjectViewDialog(@NotNull final Project project) {
public FileNestingInProjectViewDialog(@NotNull final Project project) {
super(project);
setTitle(IdeBundle.message("file.nesting.dialog.title"));
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2017 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.
*/
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.projectView.impl;
import com.intellij.ide.projectView.*;
@@ -22,21 +8,18 @@ import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.SmartList;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
/**
* {@code NestingTreeStructureProvider} moves some files in the Project View to be shown as children of another peer file. Standard use
@@ -52,60 +35,6 @@ import java.util.*;
public class NestingTreeStructureProvider implements TreeStructureProvider, DumbAware {
private static final Logger LOG = Logger.getInstance(NestingTreeStructureProvider.class);
private long myBaseListModCount = -1;
private Set<NestingRule> myNestingRules;
@NotNull
private Collection<NestingRule> getNestingRules() {
final ProjectViewFileNestingService fileNestingService = ProjectViewFileNestingService.getInstance();
final List<NestingRule> baseRules = fileNestingService.getRules();
final long modCount = fileNestingService.getModificationCount();
if (myNestingRules == null || myBaseListModCount != modCount) {
myNestingRules = new THashSet<>();
myBaseListModCount = modCount;
final MultiMap<String, String> childToParentSuffix = new MultiMap<>();
final MultiMap<String, String> parentToChildSuffix = new MultiMap<>();
for (NestingRule rule : baseRules) {
final String parentFileSuffix = rule.getParentFileSuffix();
final String childFileSuffix = rule.getChildFileSuffix();
if (parentFileSuffix.isEmpty() || childFileSuffix.isEmpty()) continue; // shouldn't happen, checked on component loading and in UI
if (parentFileSuffix.equals(childFileSuffix)) continue; // shouldn't happen, checked on component loading and in UI
myNestingRules.add(rule);
childToParentSuffix.putValue(childFileSuffix, parentFileSuffix);
parentToChildSuffix.putValue(parentFileSuffix, childFileSuffix);
// for all cases like A -> B -> C we also add a rule A -> C
for (String s : parentToChildSuffix.get(childFileSuffix)) {
myNestingRules.add(new NestingRule(parentFileSuffix, s));
parentToChildSuffix.putValue(parentFileSuffix, s);
childToParentSuffix.putValue(s, parentFileSuffix);
}
for (String s : childToParentSuffix.get(parentFileSuffix)) {
myNestingRules.add(new NestingRule(s, childFileSuffix));
parentToChildSuffix.putValue(s, childFileSuffix);
childToParentSuffix.putValue(childFileSuffix, s);
}
}
}
return myNestingRules;
}
@NotNull
private static Collection<NestingRule> getNestingRulesStatic(@NotNull final Project project) {
for (TreeStructureProvider provider : Extensions.getExtensions(TreeStructureProvider.EP_NAME, project)) {
if (provider instanceof NestingTreeStructureProvider) {
return ((NestingTreeStructureProvider)provider).getNestingRules();
}
}
return Collections.emptyList();
}
@NotNull
@Override
public Collection<AbstractTreeNode> modify(@NotNull final AbstractTreeNode parent,
@@ -114,10 +43,19 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
if (!(settings instanceof ProjectViewSettings) || !((ProjectViewSettings)settings).isUseFileNestingRules()) return children;
if (!(parent instanceof PsiDirectoryNode)) return children;
final Collection<NestingRule> rules = getNestingRules();
if (rules.isEmpty()) return children;
final ArrayList<PsiFileNode> childNodes = new ArrayList<>();
for (AbstractTreeNode node : children) {
if (!(node instanceof PsiFileNode)) continue;
childNodes.add((PsiFileNode)node);
}
final MultiMap<PsiFileNode, PsiFileNode> parentToChildren = calcParentToChildren(children, rules);
Function<PsiFileNode, String> fileNameFunc = psiFileNode -> {
final PsiFile file = psiFileNode.getValue();
if (file == null) return null;
return file.getName();
};
FileNestingBuilder fileNestingBuilder = FileNestingBuilder.getInstance();
final MultiMap<PsiFileNode, PsiFileNode> parentToChildren = fileNestingBuilder.mapParentToChildren(childNodes, fileNameFunc);
if (parentToChildren.isEmpty()) return children;
// initial ArrayList size may be not exact, not a big problem
@@ -160,7 +98,7 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
final VirtualFile dir = parentFile.getParent();
if (dir == null) return Collections.emptyList();
final Collection<NestingRule> rules = getNestingRulesStatic(project);
final Collection<NestingRule> rules = FileNestingBuilder.getInstance().getNestingRules();
if (rules.isEmpty()) return Collections.emptyList();
final VirtualFile[] children = dir.getChildren();
@@ -181,7 +119,7 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
for (NestingRule rule : rulesWhereItCanBeChild) {
final String childName = child.getName();
final Couple<Boolean> c = checkMatchingAsParentOrChild(rule, childName);
final Couple<Boolean> c = FileNestingBuilder.checkMatchingAsParentOrChild(rule, childName);
final boolean matchesParent = c.first;
if (matchesParent) {
@@ -195,7 +133,7 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
for (NestingRule rule : rulesWhereItCanBeParent) {
final String childName = child.getName();
final Couple<Boolean> c = checkMatchingAsParentOrChild(rule, childName);
final Couple<Boolean> c = FileNestingBuilder.checkMatchingAsParentOrChild(rule, childName);
final boolean matchesChild = c.second;
if (matchesChild) {
@@ -220,7 +158,7 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
final boolean parentNotChild) {
final SmartList<NestingRule> result = new SmartList<>();
for (NestingRule rule : rules) {
final Couple<Boolean> c = checkMatchingAsParentOrChild(rule, fileName);
final Couple<Boolean> c = FileNestingBuilder.checkMatchingAsParentOrChild(rule, fileName);
final boolean matchesParent = c.first;
final boolean matchesChild = c.second;
@@ -238,112 +176,6 @@ public class NestingTreeStructureProvider implements TreeStructureProvider, Dumb
return result;
}
/*
This is a graph theory problem. PsiFileNodes are graph nodes.
Edges go from parent file to child file according to NestingRules, for example foo.js->foo.min.js.
Parent may have several children. Child may have several parents.
There may be cycles with 3 or more nodes, but cycle with 2 nodes (A->B and B->A) is impossible because parentFileSuffix != childFileSuffix
For each child its outbound edges are removed. For example in case of a cycle all edges that form it are removed. In case of A->B->C only A->B remains.
As a result we get a number of separated parent-to-many-children sub-graphs, and use them to nest child files under parent file in Project View.
One child still may have more than one parent. For real use cases it is not expected to happen, but anyway it's not a big problem, it will be shown as a subnode more than once.
*/
@NotNull
private static MultiMap<PsiFileNode, PsiFileNode> calcParentToChildren(@NotNull final Collection<AbstractTreeNode> nodes,
@NotNull final Collection<NestingRule> rules) {
// result that will contain number of separated parent-to-many-children sub-graphs
MultiMap<PsiFileNode, PsiFileNode> parentToChildren = null;
Set<PsiFileNode> allChildNodes = null; // helps to remove all outbound edges of a node that has inbound edge itself
Map<Pair<String, NestingRule>, Edge<PsiFileNode>> baseNameAndRuleToEdge = null; // temporary map for building edges
for (AbstractTreeNode node : nodes) {
if (!(node instanceof PsiFileNode)) continue;
final PsiFile file = ((PsiFileNode)node).getValue();
if (file == null) continue;
for (NestingRule rule : rules) {
final String fileName = file.getName();
final Couple<Boolean> c = checkMatchingAsParentOrChild(rule, fileName);
final boolean matchesParent = c.first;
final boolean matchesChild = c.second;
if (!matchesChild && !matchesParent) continue;
if (baseNameAndRuleToEdge == null) {
baseNameAndRuleToEdge = new THashMap<>();
parentToChildren = new MultiMap<>();
allChildNodes = new THashSet<>();
}
if (matchesParent) {
final String baseName = fileName.substring(0, fileName.length() - rule.getParentFileSuffix().length());
final Edge<PsiFileNode> edge = getOrCreateEdge(baseNameAndRuleToEdge, baseName, rule);
edge.from = (PsiFileNode)node;
updateInfoIfEdgeComplete(parentToChildren, allChildNodes, edge);
}
if (matchesChild) {
final String baseName = fileName.substring(0, fileName.length() - rule.getChildFileSuffix().length());
final Edge<PsiFileNode> edge = getOrCreateEdge(baseNameAndRuleToEdge, baseName, rule);
edge.to = (PsiFileNode)node;
updateInfoIfEdgeComplete(parentToChildren, allChildNodes, edge);
}
}
}
return parentToChildren == null ? MultiMap.empty() : parentToChildren;
}
private static Couple<Boolean> checkMatchingAsParentOrChild(@NotNull final NestingRule rule, @NotNull final String fileName) {
boolean matchesParent = !fileName.equals(rule.getParentFileSuffix()) && fileName.endsWith(rule.getParentFileSuffix());
boolean matchesChild = !fileName.equals(rule.getChildFileSuffix()) && fileName.endsWith(rule.getChildFileSuffix());
if (matchesParent && matchesChild) {
if (rule.getParentFileSuffix().length() > rule.getChildFileSuffix().length()) {
matchesChild = false;
}
else {
matchesParent = false;
}
}
return Couple.of(matchesParent, matchesChild);
}
@NotNull
private static Edge<PsiFileNode> getOrCreateEdge(@NotNull final Map<Pair<String, NestingRule>, Edge<PsiFileNode>> baseNameAndRuleToEdge,
@NotNull final String baseName,
@NotNull final NestingRule rule) {
final Pair<String, NestingRule> baseNameAndRule = Pair.create(baseName, rule);
Edge<PsiFileNode> edge = baseNameAndRuleToEdge.get(baseNameAndRule);
if (edge == null) {
edge = new Edge<>();
baseNameAndRuleToEdge.put(baseNameAndRule, edge);
}
return edge;
}
private static void updateInfoIfEdgeComplete(@NotNull final MultiMap<PsiFileNode, PsiFileNode> parentToChildren,
@NotNull final Set<PsiFileNode> allChildNodes,
@NotNull final Edge<PsiFileNode> edge) {
if (edge.from != null && edge.to != null) { // if edge complete
allChildNodes.add(edge.to);
parentToChildren.remove(edge.to); // nodes that appear as a child shouldn't be a parent of another edge, corresponding edges removed
if (!allChildNodes.contains(edge.from)) {
parentToChildren.putValue(edge.from, edge.to);
}
}
}
private static class Edge<T> {
@Nullable private T from;
@Nullable private T to;
}
public static class ChildFileInfo {
@NotNull public final VirtualFile file;
@NotNull public final String namePartCommonWithParentFile;
@@ -1043,6 +1043,7 @@
<applicationService serviceInterface="com.intellij.ide.projectView.impl.ProjectViewFileNestingService"
serviceImplementation="com.intellij.ide.projectView.impl.ProjectViewFileNestingService"/>
<treeStructureProvider implementation="com.intellij.ide.projectView.impl.NestingTreeStructureProvider" order="last"/>
<applicationService serviceImplementation="com.intellij.ide.projectView.impl.FileNestingBuilder" />
<refactoring.safeDeleteProcessor implementation="com.intellij.refactoring.safeDelete.RelatedFilesSafeDeleteProcessorDelegate"/>
<automaticRenamerFactory implementation="com.intellij.refactoring.rename.RelatedFilesRenamerFactory"/>
<refactoring.moveHandler implementation="com.intellij.refactoring.move.moveFilesOrDirectories.MoveRelatedFilesHandler"