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