f082e2b84fedc705958f72ebf8730c91f80cd210
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / projectView / impl / nodes / ProjectViewDirectoryHelper.java
1 // Copyright 2000-2019 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.
2
3 package com.intellij.ide.projectView.impl.nodes;
4
5 import com.intellij.ide.projectView.ProjectViewSettings;
6 import com.intellij.ide.projectView.ViewSettings;
7 import com.intellij.ide.projectView.impl.ProjectRootsUtil;
8 import com.intellij.ide.util.treeView.AbstractTreeNode;
9 import com.intellij.ide.util.treeView.AbstractTreeUi;
10 import com.intellij.openapi.components.ServiceManager;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.fileTypes.FileTypeRegistry;
13 import com.intellij.openapi.module.Module;
14 import com.intellij.openapi.module.ModuleManager;
15 import com.intellij.openapi.module.UnloadedModuleDescription;
16 import com.intellij.openapi.project.Project;
17 import com.intellij.openapi.project.ProjectBundle;
18 import com.intellij.openapi.roots.*;
19 import com.intellij.openapi.roots.impl.DirectoryIndex;
20 import com.intellij.openapi.roots.impl.DirectoryInfo;
21 import com.intellij.openapi.roots.ui.configuration.ModuleSourceRootEditHandler;
22 import com.intellij.openapi.util.Comparing;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.registry.Registry;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
27 import com.intellij.psi.*;
28 import com.intellij.psi.search.PsiElementProcessor;
29 import com.intellij.psi.util.PsiTreeUtil;
30 import com.intellij.psi.util.PsiUtilCore;
31 import com.intellij.util.FontUtil;
32 import com.intellij.util.containers.ContainerUtil;
33 import com.intellij.util.containers.JBIterable;
34 import gnu.trove.THashSet;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37 import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes;
38 import org.jetbrains.jps.model.java.JavaSourceRootProperties;
39
40 import java.util.*;
41 import java.util.stream.Collectors;
42
43 public class ProjectViewDirectoryHelper {
44   protected static final Logger LOG = Logger.getInstance(ProjectViewDirectoryHelper.class);
45
46   private final Project myProject;
47   private final DirectoryIndex myIndex;
48
49   public static ProjectViewDirectoryHelper getInstance(Project project) {
50     return ServiceManager.getService(project, ProjectViewDirectoryHelper.class);
51   }
52
53   public ProjectViewDirectoryHelper(Project project, DirectoryIndex index) {
54     myProject = project;
55     myIndex = index;
56   }
57
58   public Project getProject() {
59     return myProject;
60   }
61
62
63   @Nullable
64   public String getLocationString(@NotNull PsiDirectory psiDirectory) {
65     boolean includeUrl = ProjectRootsUtil.isModuleContentRoot(psiDirectory);
66     return getLocationString(psiDirectory, includeUrl, false);
67   }
68
69   @Nullable
70   public String getLocationString(@NotNull PsiDirectory psiDirectory, boolean includeUrl, boolean includeRootType) {
71     StringBuilder result = new StringBuilder();
72
73     final VirtualFile directory = psiDirectory.getVirtualFile();
74
75     if (ProjectRootsUtil.isLibraryRoot(directory, psiDirectory.getProject())) {
76       result.append(ProjectBundle.message("module.paths.root.node", "library").toLowerCase(Locale.getDefault()));
77     }
78     else if (includeRootType) {
79       SourceFolder sourceRoot = ProjectRootsUtil.getModuleSourceRoot(psiDirectory.getVirtualFile(), psiDirectory.getProject());
80       if (sourceRoot != null) {
81         ModuleSourceRootEditHandler<?> handler = ModuleSourceRootEditHandler.getEditHandler(sourceRoot.getRootType());
82         if (handler != null) {
83           JavaSourceRootProperties properties = sourceRoot.getJpsElement().getProperties(JavaModuleSourceRootTypes.SOURCES);
84           if (properties != null && properties.isForGeneratedSources()) {
85             result.append("generated ");
86           }
87           result.append(handler.getFullRootTypeName().toLowerCase(Locale.getDefault()));
88         }
89       }
90     }
91
92     if (includeUrl) {
93       if (result.length() > 0) result.append(",").append(FontUtil.spaceAndThinSpace());
94       result.append(FileUtil.getLocationRelativeToUserHome(directory.getPresentableUrl()));
95     }
96     
97     return result.length() == 0 ? null : result.toString();
98   }
99
100
101   public boolean isShowFQName(ViewSettings settings, Object parentValue, PsiDirectory value) {
102     return false;
103   }
104
105   /**
106    * Returns {@code true} if the directory containing project configuration files (.idea) should be hidden in Project View.
107    */
108   public boolean shouldHideProjectConfigurationFilesDirectory() {
109     return true;
110   }
111
112   @Nullable
113   public String getNodeName(ViewSettings settings, Object parentValue, PsiDirectory directory) {
114     return directory.getName();
115   }
116
117   public boolean skipDirectory(PsiDirectory directory) {
118     return true;
119   }
120
121   public boolean isEmptyMiddleDirectory(PsiDirectory directory, final boolean strictlyEmpty) {
122     return isEmptyMiddleDirectory(directory, strictlyEmpty, null);
123   }
124
125   public boolean isEmptyMiddleDirectory(PsiDirectory directory,
126                                         final boolean strictlyEmpty,
127                                         @Nullable PsiFileSystemItemFilter filter) {
128     return false;
129   }
130
131   public boolean supportsFlattenPackages() {
132     return false;
133   }
134
135   public boolean supportsHideEmptyMiddlePackages() {
136     return false;
137   }
138
139   public boolean canRepresent(Object element, PsiDirectory directory) {
140     if (element instanceof VirtualFile) {
141       VirtualFile vFile = (VirtualFile) element;
142       return Comparing.equal(directory.getVirtualFile(), vFile);
143     }
144     return false;
145   }
146
147   public boolean canRepresent(Object element, PsiDirectory directory, Object owner, ViewSettings settings) {
148     if (directory != null) {
149       if (canRepresent(element, directory)) return true;
150       if (settings == null) return false; // unexpected
151       if (!settings.isFlattenPackages() && settings.isHideEmptyMiddlePackages()) {
152         if (element instanceof PsiDirectory) {
153           if (getParents(directory, owner).find(dir -> Comparing.equal(element, dir)) != null) return true;
154         }
155         else if (element instanceof VirtualFile) {
156           if (getParents(directory, owner).find(dir -> Comparing.equal(element, dir.getVirtualFile())) != null) return true;
157         }
158       }
159     }
160     return false;
161   }
162
163   boolean isValidDirectory(PsiDirectory directory, Object owner, ViewSettings settings, PsiFileSystemItemFilter filter) {
164     if (directory == null || !directory.isValid()) return false;
165     if (settings == null) return true; // unexpected
166     if (!settings.isFlattenPackages() && settings.isHideEmptyMiddlePackages()) {
167       PsiDirectory parent = directory.getParent();
168       if (parent == null || skipDirectory(parent)) return true;
169       if (ProjectRootsUtil.isSourceRoot(directory)) return true;
170       if (isEmptyMiddleDirectory(directory, true, filter)) return false;
171       for (PsiDirectory dir : getParents(directory, owner)) {
172         if (!dir.isValid()) return false;
173         parent = dir.getParent();
174         if (parent == null || skipDirectory(parent)) return false;
175         if (!isEmptyMiddleDirectory(dir, true, filter)) return false;
176       }
177     }
178     return true;
179   }
180
181   @NotNull
182   private static JBIterable<PsiDirectory> getParents(PsiDirectory directory, Object owner) {
183     if (directory != null) directory = directory.getParent(); // because JBIterable adds first parent without comparing with owner
184     return directory != null && owner instanceof PsiDirectory && PsiTreeUtil.isAncestor((PsiDirectory)owner, directory, true)
185            ? JBIterable.generate(directory, PsiDirectory::getParent).takeWhile(dir -> dir != null && !dir.equals(owner))
186            : JBIterable.empty();
187   }
188
189   @NotNull
190   public Collection<AbstractTreeNode> getDirectoryChildren(final PsiDirectory psiDirectory,
191                                                            final ViewSettings settings,
192                                                            final boolean withSubDirectories) {
193     return getDirectoryChildren(psiDirectory, settings, withSubDirectories, null);
194   }
195
196   @NotNull
197   public Collection<AbstractTreeNode> getDirectoryChildren(final PsiDirectory psiDirectory,
198                                                            final ViewSettings settings,
199                                                            final boolean withSubDirectories,
200                                                            @Nullable PsiFileSystemItemFilter filter) {
201     return AbstractTreeUi.calculateYieldingToWriteAction(() -> doGetDirectoryChildren(psiDirectory, settings, withSubDirectories, filter));
202   }
203
204   @NotNull
205   private Collection<AbstractTreeNode> doGetDirectoryChildren(PsiDirectory psiDirectory,
206                                                               ViewSettings settings,
207                                                               boolean withSubDirectories,
208                                                               @Nullable PsiFileSystemItemFilter filter) {
209     final List<AbstractTreeNode> children = new ArrayList<>();
210     final Project project = psiDirectory.getProject();
211     final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
212     final Module module = fileIndex.getModuleForFile(psiDirectory.getVirtualFile());
213     final ModuleFileIndex moduleFileIndex = module == null ? null : ModuleRootManager.getInstance(module).getFileIndex();
214     if (!settings.isFlattenPackages() || skipDirectory(psiDirectory)) {
215       processPsiDirectoryChildren(psiDirectory, directoryChildrenInProject(psiDirectory, settings),
216                                   children, fileIndex, null, settings, withSubDirectories, filter);
217     }
218     else { // source directory in "flatten packages" mode
219       final PsiDirectory parentDir = psiDirectory.getParentDirectory();
220       if (parentDir == null || skipDirectory(parentDir) && withSubDirectories) {
221         addAllSubpackages(children, psiDirectory, moduleFileIndex, settings, filter);
222       }
223       if (withSubDirectories) {
224         PsiDirectory[] subdirs = psiDirectory.getSubdirectories();
225         for (PsiDirectory subdir : subdirs) {
226           if (!skipDirectory(subdir) || filter != null && !filter.shouldShow(subdir)) {
227             continue;
228           }
229           VirtualFile directoryFile = subdir.getVirtualFile();
230
231           if (Registry.is("ide.hide.excluded.files")) {
232             if (fileIndex.isExcluded(directoryFile)) continue;
233           }
234           else {
235             if (FileTypeRegistry.getInstance().isFileIgnored(directoryFile)) continue;
236           }
237
238           children.add(new PsiDirectoryNode(project, subdir, settings, filter));
239         }
240       }
241       processPsiDirectoryChildren(psiDirectory, psiDirectory.getFiles(), children, fileIndex, moduleFileIndex, settings,
242                                   withSubDirectories, filter);
243     }
244     return children;
245   }
246
247   @NotNull
248   public List<VirtualFile> getTopLevelRoots() {
249     List<VirtualFile> topLevelContentRoots = new ArrayList<>();
250     ProjectRootManager prm = ProjectRootManager.getInstance(myProject);
251
252     for (VirtualFile root : prm.getContentRoots()) {
253       VirtualFile parent = root.getParent();
254       if (!isFileUnderContentRoot(myIndex, parent)) {
255         topLevelContentRoots.add(root);
256       }
257     }
258     Collection<UnloadedModuleDescription> descriptions = ModuleManager.getInstance(myProject).getUnloadedModuleDescriptions();
259     for (UnloadedModuleDescription description : descriptions) {
260       for (VirtualFilePointer pointer : description.getContentRoots()) {
261         VirtualFile root = pointer.getFile();
262         if (root != null) {
263           VirtualFile parent = root.getParent();
264           if (!isFileUnderContentRoot(myIndex, parent)) {
265             topLevelContentRoots.add(root);
266           }
267         }
268       }
269     }
270     return topLevelContentRoots;
271   }
272
273   @NotNull
274   List<VirtualFile> getTopLevelModuleRoots(Module module, ViewSettings settings) {
275     return ContainerUtil.filter(ModuleRootManager.getInstance(module).getContentRoots(), root -> {
276       if (!shouldBeShown(root, settings)) return false;
277       VirtualFile parent = root.getParent();
278       if (parent == null) return true;
279       DirectoryInfo info = myIndex.getInfoForFile(parent);
280       if (!module.equals(info.getModule())) return true;
281       //show inner content root separately only if it won't be shown under outer content root
282       return info.isExcluded(parent) && !shouldShowExcludedFiles(settings);
283     });
284   }
285
286   @NotNull
287   List<VirtualFile> getTopLevelUnloadedModuleRoots(UnloadedModuleDescription module, ViewSettings settings) {
288     return module.getContentRoots().stream()
289       .map(VirtualFilePointer::getFile)
290       .filter(root -> root != null && shouldBeShown(root, settings))
291       .collect(Collectors.toList());
292   }
293
294
295   private static boolean isFileUnderContentRoot(@NotNull DirectoryIndex index, @Nullable VirtualFile file) {
296     return file != null && index.getInfoForFile(file).getContentRoot() != null;
297   }
298
299   @NotNull
300   private PsiElement[] directoryChildrenInProject(PsiDirectory psiDirectory, final ViewSettings settings) {
301     final VirtualFile dir = psiDirectory.getVirtualFile();
302     if (shouldBeShown(dir, settings)) {
303       final List<PsiElement> children = new ArrayList<>();
304       psiDirectory.processChildren(new PsiElementProcessor<PsiFileSystemItem>() {
305         @Override
306         public boolean execute(@NotNull PsiFileSystemItem element) {
307           if (shouldBeShown(element.getVirtualFile(), settings)) {
308             children.add(element);
309           }
310           return true;
311         }
312       });
313       return PsiUtilCore.toPsiElementArray(children);
314     }
315
316     PsiManager manager = psiDirectory.getManager();
317     Set<PsiElement> directoriesOnTheWayToContentRoots = new THashSet<>();
318     for (VirtualFile root : getTopLevelRoots()) {
319       VirtualFile current = root;
320       while (current != null) {
321         VirtualFile parent = current.getParent();
322
323         if (Comparing.equal(parent, dir)) {
324           final PsiDirectory psi = manager.findDirectory(current);
325           if (psi != null) {
326             directoriesOnTheWayToContentRoots.add(psi);
327           }
328         }
329         current = parent;
330       }
331     }
332
333     return PsiUtilCore.toPsiElementArray(directoriesOnTheWayToContentRoots);
334   }
335
336   private boolean shouldBeShown(@NotNull VirtualFile dir, ViewSettings settings) {
337     if (!dir.isValid()) return false;
338     DirectoryInfo directoryInfo = myIndex.getInfoForFile(dir);
339     return directoryInfo.isInProject(dir) || shouldShowExcludedFiles(settings) && directoryInfo.isExcluded(dir);
340   }
341
342   private static boolean shouldShowExcludedFiles(ViewSettings settings) {
343     return !Registry.is("ide.hide.excluded.files") && settings instanceof ProjectViewSettings && ((ProjectViewSettings)settings).isShowExcludedFiles();
344   }
345
346   // used only for non-flatten packages mode
347   private void processPsiDirectoryChildren(final PsiDirectory psiDir,
348                                            PsiElement[] children,
349                                            List<? super AbstractTreeNode> container,
350                                            ProjectFileIndex projectFileIndex,
351                                            @Nullable ModuleFileIndex moduleFileIndex,
352                                            ViewSettings viewSettings,
353                                            boolean withSubDirectories,
354                                            @Nullable PsiFileSystemItemFilter filter) {
355     for (PsiElement child : children) {
356       LOG.assertTrue(child.isValid());
357
358       if (!(child instanceof PsiFileSystemItem)) {
359         LOG.error("Either PsiFile or PsiDirectory expected as a child of " + child.getParent() + ", but was " + child);
360         continue;
361       }
362       final VirtualFile vFile = ((PsiFileSystemItem) child).getVirtualFile();
363       if (vFile == null) {
364         continue;
365       }
366       if (moduleFileIndex != null && !moduleFileIndex.isInContent(vFile)) {
367         continue;
368       }
369       if (filter != null && !filter.shouldShow((PsiFileSystemItem)child)) {
370         continue;
371       }
372       if (child instanceof PsiFile) {
373         container.add(new PsiFileNode(child.getProject(), (PsiFile) child, viewSettings));
374       }
375       else if (child instanceof PsiDirectory) {
376         if (withSubDirectories) {
377           PsiDirectory dir = (PsiDirectory)child;
378           if (!vFile.equals(projectFileIndex.getSourceRootForFile(vFile))) { // if is not a source root
379             if (viewSettings.isHideEmptyMiddlePackages() && !skipDirectory(psiDir) && isEmptyMiddleDirectory(dir, true, filter)) {
380               processPsiDirectoryChildren(
381                 dir, directoryChildrenInProject(dir, viewSettings), container, projectFileIndex, moduleFileIndex, viewSettings, true, filter
382               ); // expand it recursively
383               continue;
384             }
385           }
386           container.add(new PsiDirectoryNode(child.getProject(), (PsiDirectory)child, viewSettings, filter));
387         }
388       }
389     }
390   }
391
392   // used only in flatten packages mode
393   private void addAllSubpackages(List<? super AbstractTreeNode> container,
394                                  PsiDirectory dir,
395                                  @Nullable ModuleFileIndex moduleFileIndex,
396                                  ViewSettings viewSettings,
397                                  @Nullable PsiFileSystemItemFilter filter) {
398     final Project project = dir.getProject();
399     PsiDirectory[] subdirs = dir.getSubdirectories();
400     for (PsiDirectory subdir : subdirs) {
401       if (skipDirectory(subdir) || filter != null && !filter.shouldShow(subdir)) {
402         continue;
403       }
404       if (moduleFileIndex != null && !moduleFileIndex.isInContent(subdir.getVirtualFile())) {
405         container.add(new PsiDirectoryNode(project, subdir, viewSettings, filter));
406         continue;
407       }
408       if (viewSettings.isHideEmptyMiddlePackages()) {
409         if (!isEmptyMiddleDirectory(subdir, false, filter)) {
410
411           container.add(new PsiDirectoryNode(project, subdir, viewSettings, filter));
412         }
413       }
414       else {
415         container.add(new PsiDirectoryNode(project, subdir, viewSettings, filter));
416       }
417       addAllSubpackages(container, subdir, moduleFileIndex, viewSettings, filter);
418     }
419   }
420
421   @NotNull
422   public Collection<AbstractTreeNode> createFileAndDirectoryNodes(@NotNull List<? extends VirtualFile> files, ViewSettings viewSettings) {
423     final List<AbstractTreeNode> children = new ArrayList<>(files.size());
424     final PsiManager psiManager = PsiManager.getInstance(myProject);
425     for (final VirtualFile virtualFile : files) {
426       if (virtualFile.isDirectory()) {
427         PsiDirectory directory = psiManager.findDirectory(virtualFile);
428         if (directory != null) {
429           children.add(new PsiDirectoryNode(myProject, directory, viewSettings));
430         }
431       }
432       else {
433         PsiFile file = psiManager.findFile(virtualFile);
434         if (file != null) {
435           children.add(new PsiFileNode(myProject, file, viewSettings));
436         }
437       }
438     }
439     return children;
440   }
441 }