diff --git a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceHelper.java b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceHelper.java index e70c07524357..eda7ec9b1d67 100644 --- a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceHelper.java +++ b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceHelper.java @@ -70,12 +70,32 @@ public abstract class FileReferenceHelper { @NotNull public abstract Collection getContexts(final Project project, @NotNull final VirtualFile file); - // JavaDoc + /** + * Provides file target contexts, locations where users can create a file, depending on passed {@code file}. + * + * @param project project + * @param file file + * @return target contexts + */ @NotNull public Collection getTargetContexts(Project project, @NotNull VirtualFile file) { return ContainerUtil.map(getContexts(project, file), FileTargetContext::new); } + /** + * Sorts target contexts depending on passed {@code file}. This sorting affects UI of quick fixes that create files in target locations. + * + * @param project project + * @param file file + * @param targetContexts available targets + * @return sorted targets collection + */ + @NotNull + public Collection sortTargetContexts(Project project, @NotNull VirtualFile file, + @NotNull Collection targetContexts) { + return targetContexts; + } + public abstract boolean isMine(final Project project, @NotNull final VirtualFile file); public boolean isFallback() { diff --git a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceSet.java b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceSet.java index 2ceeca6fd642..8bbd842b1eb5 100644 --- a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceSet.java +++ b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceSet.java @@ -411,17 +411,30 @@ public class FileReferenceSet { PsiFile file = getContainingFile(); if (file == null) return Collections.emptyList(); + Collection result; + Collection contexts = getCustomizationContexts(file); if (contexts != null) { - return toTargetContexts(contexts); + result = toTargetContexts(contexts); } - - if (isAbsolutePathReference()) { + else if (isAbsolutePathReference()) { Collection locations = getAbsoluteTopLevelDirLocations(file); - return toTargetContexts(locations); + result = toTargetContexts(locations); + } + else { + result = getTargetContextByFile(file); } - return getTargetContextByFile(file); + return sortTargetContexts(file, result); + } + + private static Collection sortTargetContexts(PsiFile file, Collection targetContexts) { + for (FileReferenceHelper helper : FileReferenceHelperRegistrar.getHelpers()) { + if (helper.isMine(file.getProject(), file.getVirtualFile())) { + return helper.sortTargetContexts(file.getProject(), file.getVirtualFile(), targetContexts); + } + } + return targetContexts; } @Nullable @@ -471,7 +484,7 @@ public class FileReferenceSet { Collection contexts = helper.getTargetContexts(project, virtualFile); for (FileTargetContext context : contexts) { list.add(context); - hasRealContexts |= !(context.getContext() instanceof FileReferenceResolver); + hasRealContexts |= !(context.getFileSystemItem() instanceof FileReferenceResolver); } } } diff --git a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileTargetContext.java b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileTargetContext.java index eda13b49d683..13f4d169cf42 100644 --- a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileTargetContext.java +++ b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileTargetContext.java @@ -9,23 +9,35 @@ import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.Collections; -// todo JavaDoc +/** + * Describes existing or non-existing location in file system where new files can be created. + */ public class FileTargetContext { private final PsiFileSystemItem myContext; private final String[] myPathToCreate; - // todo JavaDoc + /** + * Constructs new target context. + * + * @param context file system item that will be used as target directory + * @param pathToCreate additional existing or non-existing paths + */ public FileTargetContext(@NotNull PsiFileSystemItem context, @NotNull String[] pathToCreate) { myContext = context; myPathToCreate = pathToCreate; } + /** + * Constructs new target context. + * + * @param context file system item that will be used as target directory + */ public FileTargetContext(@NotNull PsiFileSystemItem context) { this(context, ArrayUtil.EMPTY_STRING_ARRAY); } - public PsiFileSystemItem getContext() { + public PsiFileSystemItem getFileSystemItem() { return myContext; } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/AbstractCreateFileFix.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/AbstractCreateFileFix.java index 786c9fa9b332..9f6d2e68b4ca 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/AbstractCreateFileFix.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/AbstractCreateFileFix.java @@ -4,30 +4,28 @@ package com.intellij.codeInsight.daemon.quickFix; import com.intellij.codeInsight.CodeInsightBundle; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement; -import com.intellij.icons.AllIcons; +import com.intellij.ide.projectView.impl.ProjectRootsUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; -import com.intellij.openapi.roots.*; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.roots.SourceFolder; +import com.intellij.openapi.roots.impl.ProjectFileIndexImpl; import com.intellij.openapi.ui.popup.*; import com.intellij.openapi.ui.popup.util.BaseListPopupStep; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.util.IconUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.PlatformIcons; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.jps.model.java.JavaResourceRootType; -import org.jetbrains.jps.model.java.JavaSourceRootType; -import org.jetbrains.jps.model.module.JpsModuleSourceRootType; import javax.swing.*; import java.util.List; @@ -48,15 +46,13 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct protected long myIsAvailableTimeStamp; protected AbstractCreateFileFix(@Nullable PsiElement element, - String newFileName, - List directories, - String[] subPath, + @NotNull NewFileLocation newFileLocation, @NotNull String fixLocaleKey) { super(element); - myNewFileName = newFileName; - myDirectories = directories; - mySubPath = subPath; + myNewFileName = newFileLocation.getNewFileName(); + myDirectories = newFileLocation.getDirectories(); + mySubPath = newFileLocation.getSubPath(); myKey = fixLocaleKey; } @@ -98,15 +94,12 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct return; } - List sortedDirectories = getTargetDirectoryListItems(directories); - sortWithResourcePriority(file, sortedDirectories); - if (editor == null) { // run on first item of sorted list in batch mode apply(myStartElement.getProject(), directories.get(0)); } else { - showOptionsPopup(project, editor, sortedDirectories); + showOptionsPopup(project, editor, directories); } } } @@ -114,20 +107,6 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct protected abstract void apply(@NotNull Project project, TargetDirectory directory) throws IncorrectOperationException; - // todo move sorting and source sets logic to extension point like FileReferenceHelper ? - protected void sortWithResourcePriority(@NotNull PsiFile file, List sortedDirectories) { - // sort only if we have resource roots - if (sortedDirectories.stream().anyMatch(AbstractCreateFileFix::isResourceRoot)) { - ProjectFileIndex projectFileIndex = ProjectFileIndex.getInstance(file.getProject()); - if (projectFileIndex.isInTestSourceContent(file.getVirtualFile())) { - sortedDirectories.sort(AbstractCreateFileFix::compareTargetsForTests); - } - else if (projectFileIndex.isInSourceContent(file.getVirtualFile())) { - sortedDirectories.sort(AbstractCreateFileFix::compareTargetsForProduction); - } - } - } - protected static PsiDirectory findOrCreateSubdirectory(PsiDirectory directory, String subDirectoryName) { PsiDirectory existingDirectory = directory.findSubdirectory(subDirectoryName); if (existingDirectory == null) { @@ -138,7 +117,9 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct protected void showOptionsPopup(@NotNull Project project, @NotNull Editor editor, - List items) { + List directories) { + List items = getTargetDirectoryListItems(directories); + String filePath = myNewFileName; if (mySubPath.length > 0) { filePath = StringUtil.join(mySubPath, VFS_SEPARATOR_CHAR + "") + VFS_SEPARATOR_CHAR + myNewFileName; @@ -148,14 +129,20 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct new BaseListPopupStep(CodeInsightBundle.message(myKey, filePath), items) { @Override public Icon getIconFor(TargetDirectoryListItem value) { - JpsModuleSourceRootType type = value.getSourceRootType(); + PsiDirectory directory = value.getTarget().getDirectory(); + if (directory == null) { + return PlatformIcons.FOLDER_ICON; + } - if (isSourceItem(type)) return AllIcons.Modules.SourceRoot; - if (isTestSourceItem(type)) return AllIcons.Nodes.TestSourceFolder; - if (isResourceItem(type)) return AllIcons.Modules.ResourcesRoot; - if (isTestResourceItem(type)) return AllIcons.Modules.TestResourcesRoot; + VirtualFile file = directory.getVirtualFile(); - return PlatformIcons.FOLDER_ICON; + ProjectFileIndexImpl projectFileIndex = (ProjectFileIndexImpl)ProjectRootManager.getInstance(project).getFileIndex(); + SourceFolder sourceFolder = projectFileIndex.getSourceFolder(file); + if (sourceFolder != null && sourceFolder.getFile() != null) { + return IconUtil.getIcon(sourceFolder.getFile(), 0, project); + } + + return IconUtil.getIcon(file, 0, project); } @NotNull @@ -193,61 +180,12 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct PsiDirectory d = targetDirectory.getDirectory(); assert d != null : "Invalid PsiDirectory instances found"; - ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(d.getProject()).getFileIndex(); - - Module targetModule = projectFileIndex.getModuleForFile(d.getVirtualFile()); - JpsModuleSourceRootType sourceRootType = null; - if (targetModule != null) { - SourceFolder folder = getSourceFolder(targetModule, d); - if (folder != null) { - sourceRootType = folder.getRootType(); - } - } - String presentablePath = getPresentableContentRootPath(d.getProject(), d.getVirtualFile(), targetDirectory.getPathToCreate()); - return new TargetDirectoryListItem(targetDirectory, sourceRootType, presentablePath); + return new TargetDirectoryListItem(targetDirectory, presentablePath); }); } - private static int getTestsTargetOrdinal(TargetDirectoryListItem item) { - JpsModuleSourceRootType type = item.getSourceRootType(); - - if (isSourceItem(type)) return 4; - if (isTestSourceItem(type)) return 3; - if (isResourceItem(type)) return 2; - if (isTestResourceItem(type)) return 1; - - return 0; - } - - private static int getSourcesTargetOrdinal(TargetDirectoryListItem item) { - JpsModuleSourceRootType type = item.getSourceRootType(); - - if (isTestSourceItem(type)) return 4; - if (isSourceItem(type)) return 3; - if (isTestResourceItem(type)) return 2; - if (isResourceItem(type)) return 1; - - return 0; - } - - private static boolean isTestResourceItem(@Nullable JpsModuleSourceRootType type) { - return type == JavaResourceRootType.TEST_RESOURCE; - } - - private static boolean isResourceItem(@Nullable JpsModuleSourceRootType type) { - return type == JavaResourceRootType.RESOURCE; - } - - private static boolean isTestSourceItem(@Nullable JpsModuleSourceRootType type) { - return type == JavaSourceRootType.TEST_SOURCE; - } - - private static boolean isSourceItem(@Nullable JpsModuleSourceRootType type) { - return type == JavaSourceRootType.SOURCE; - } - @NotNull private static String getPresentableContentRootPath(@NotNull Project project, @NotNull VirtualFile f, @@ -260,63 +198,16 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct return toProjectPath; } - protected static int compareTargetsForTests(@NotNull TargetDirectoryListItem d1, @NotNull TargetDirectoryListItem d2) { - int o1 = getTestsTargetOrdinal(d1); - int o2 = getTestsTargetOrdinal(d2); - - if (o1 > 0 && o2 > 0) { - return Integer.compare(o1, o2); - } - - return compareDirectoryPaths(d1, d2); - } - - protected static int compareTargetsForProduction(@NotNull TargetDirectoryListItem d1, @NotNull TargetDirectoryListItem d2) { - int o1 = getSourcesTargetOrdinal(d1); - int o2 = getSourcesTargetOrdinal(d2); - - if (o1 > 0 && o2 > 0) { - return Integer.compare(o1, o2); - } - - return compareDirectoryPaths(d1, d2); - } - - private static int compareDirectoryPaths(@NotNull TargetDirectoryListItem d1, @NotNull TargetDirectoryListItem d2) { - PsiDirectory directory1 = d1.getTarget().getDirectory(); - PsiDirectory directory2 = d2.getTarget().getDirectory(); - - assert directory1 != null : "Invalid PsiDirectory instances found"; - assert directory2 != null : "Invalid PsiDirectory instances found"; - - VirtualFile f1 = directory1.getVirtualFile(); - VirtualFile f2 = directory2.getVirtualFile(); - return f1.getPath().compareTo(f2.getPath()); - } - - protected static boolean isResourceRoot(TargetDirectoryListItem d) { - return isResourceItem(d.getSourceRootType()) || - isTestResourceItem(d.getSourceRootType()); - } - protected static class TargetDirectoryListItem { private final TargetDirectory myTargetDirectory; - private final JpsModuleSourceRootType mySourceRootType; private final String myPresentablePath; public TargetDirectoryListItem(@NotNull TargetDirectory targetDirectory, - @Nullable JpsModuleSourceRootType type, @NotNull String presentablePath) { myTargetDirectory = targetDirectory; - mySourceRootType = type; myPresentablePath = presentablePath; } - @Nullable - private JpsModuleSourceRootType getSourceRootType() { - return mySourceRootType; - } - private String getPresentablePath() { return myPresentablePath; } @@ -325,19 +216,4 @@ public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionAct return myTargetDirectory; } } - - @Nullable - private static SourceFolder getSourceFolder(@NotNull Module module, @NotNull PsiDirectory directory) { - ContentEntry[] entries = ModuleRootManager.getInstance(module).getContentEntries(); - for (ContentEntry contentEntry : entries) { - for (SourceFolder sourceFolder : contentEntry.getSourceFolders()) { - if (sourceFolder.getFile() != null - && VfsUtilCore.isAncestor(sourceFolder.getFile(), directory.getVirtualFile(), false)) { - return sourceFolder; - } - } - } - - return null; - } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateDirectoryFix.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateDirectoryFix.java index 31237d5ec400..a3e8005b3626 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateDirectoryFix.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateDirectoryFix.java @@ -10,27 +10,26 @@ import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - +/** + * Quick fix that creates a new directory in one of the target directories. Automatically creates all intermediate directories of + * {@link TargetDirectory#getPathToCreate()} and {@link NewFileLocation#getSubPath()}. If there are multiple target directories it shows + * a popup where users can select desired target directory. + */ public class CreateDirectoryFix extends AbstractCreateFileFix { // invoked from other module @SuppressWarnings("WeakerAccess") public CreateDirectoryFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newDirectoryName, + @NotNull NewFileLocation newFileLocation, @NotNull String fixLocaleKey) { - super(psiElement, newDirectoryName, directories, subPath, fixLocaleKey); + super(psiElement, newFileLocation, fixLocaleKey); myIsAvailable = true; myIsAvailableTimeStamp = System.currentTimeMillis(); } public CreateDirectoryFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newDirectoryName) { - this(psiElement, directories, subPath, newDirectoryName, "create.directory.text"); + @NotNull NewFileLocation newFileLocation) { + this(psiElement, newFileLocation, "create.directory.text"); } @Override diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateFileWithScopeFix.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateFileWithScopeFix.java index 627c931f4789..533afb0746fe 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateFileWithScopeFix.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/CreateFileWithScopeFix.java @@ -26,43 +26,40 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.util.List; import java.util.function.Supplier; +/** + * Quick fix that creates a new file in one of the target directories. Automatically creates all intermediate directories of + * {@link TargetDirectory#getPathToCreate()} and {@link NewFileLocation#getSubPath()}. If there are multiple target directories it shows + * a popup where users can select desired target directory. + */ public class CreateFileWithScopeFix extends AbstractCreateFileFix { private final String myText; @Nullable private Supplier myFileTextSupplier; - // todo JavaDoc // invoked from other module @SuppressWarnings("WeakerAccess") public CreateFileWithScopeFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newFileName, + @NotNull NewFileLocation newFileLocation, @Nullable String fileText, @NotNull String fixLocaleKey) { - super(psiElement, newFileName, directories, subPath, fixLocaleKey); + super(psiElement, newFileLocation, fixLocaleKey); myText = fileText; - myIsAvailable = !FileTypeManager.getInstance().getFileTypeByFileName(newFileName).isBinary(); + myIsAvailable = !FileTypeManager.getInstance().getFileTypeByFileName(myNewFileName).isBinary(); myIsAvailableTimeStamp = System.currentTimeMillis(); } public CreateFileWithScopeFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newFileName) { - this(psiElement, directories, subPath, newFileName, null, "create.file.text"); + @NotNull NewFileLocation newFileLocation) { + this(psiElement, newFileLocation, null, "create.file.text"); } public CreateFileWithScopeFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newFileName, + @NotNull NewFileLocation newFileLocation, @NotNull Supplier fileTextSupplier) { - this(psiElement, directories, subPath, newFileName, null, "create.file.text"); + this(psiElement, newFileLocation, null, "create.file.text"); myFileTextSupplier = fileTextSupplier; } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/FileReferenceQuickFixProvider.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/FileReferenceQuickFixProvider.java index 489f1f78dd68..fd2e2b25f832 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/FileReferenceQuickFixProvider.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/FileReferenceQuickFixProvider.java @@ -79,14 +79,15 @@ public class FileReferenceQuickFixProvider { List targetDirectories = getTargets(reference, module, newFileName, false); if (targetDirectories.isEmpty()) return emptyList(); - return singletonList(new MyCreateFileFix(element, targetDirectories, getPathToReferencePart(reference), newFileName, - reference.getNewFileTemplateName())); + NewFileLocation location = new NewFileLocation(targetDirectories, getPathToReferencePart(reference), newFileName); + return singletonList(new MyCreateFileFix(element, location, reference.getNewFileTemplateName())); } else { List targetDirectories = getTargets(reference, module, newFileName, true); if (targetDirectories.isEmpty()) return emptyList(); - return singletonList(new CreateDirectoryFix(element, targetDirectories, getPathToReferencePart(reference), newFileName)); + NewFileLocation location = new NewFileLocation(targetDirectories, getPathToReferencePart(reference), newFileName); + return singletonList(new CreateDirectoryFix(element, location)); } } @@ -100,7 +101,7 @@ public class FileReferenceQuickFixProvider { List targetDirectories = new SmartList<>(); for (FileTargetContext targetContext : contexts) { - PsiFileSystemItem context = targetContext.getContext(); + PsiFileSystemItem context = targetContext.getFileSystemItem(); VirtualFile virtualFile = context.getVirtualFile(); if (virtualFile == null || !virtualFile.isValid()) continue; @@ -215,7 +216,7 @@ public class FileReferenceQuickFixProvider { SmartList contexts = new SmartList<>(); for (FileTargetContext targetContext : targetContexts) { if (targetContext != null) { - PsiFileSystemItem fsContext = targetContext.getContext(); + PsiFileSystemItem fsContext = targetContext.getFileSystemItem(); VirtualFile virtualFile = fsContext.getVirtualFile(); if (virtualFile != null && fsContext.isDirectory() && virtualFile.isInLocalFileSystem()) { if (module != null) { @@ -246,11 +247,9 @@ public class FileReferenceQuickFixProvider { private final String myNewFileTemplateName; private MyCreateFileFix(@NotNull PsiElement psiElement, - @NotNull List directories, - @NotNull String[] subPath, - @NotNull String newFileName, + @NotNull NewFileLocation newFileLocation, @Nullable String newFileTemplateName) { - super(psiElement, directories, subPath, newFileName); + super(psiElement, newFileLocation); myNewFileTemplateName = newFileTemplateName; } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/NewFileLocation.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/NewFileLocation.java new file mode 100644 index 000000000000..42925fb25ad9 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/NewFileLocation.java @@ -0,0 +1,55 @@ +// 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. +package com.intellij.codeInsight.daemon.quickFix; + +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Describes possible locations for new file / directory that can be created by a quick fix. + * + * @see CreateFileWithScopeFix + * @see CreateDirectoryFix + */ +public class NewFileLocation { + private final List myDirectories; + private final String[] mySubPath; + private final String myNewFileName; + + public NewFileLocation(List directories, String newFileName) { + this(directories, ArrayUtil.EMPTY_STRING_ARRAY, newFileName); + } + + public NewFileLocation(@NotNull List targetDirectories, + @NotNull String[] subPath, + @NotNull String newFileName) { + myDirectories = targetDirectories; + mySubPath = subPath; + myNewFileName = newFileName; + } + + /** + * @return target directories where sub path and new file can be created + */ + @NotNull + public List getDirectories() { + return myDirectories; + } + + /** + * @return intermediate path to new file that may not exist but should be created when a quick fix applied + */ + @NotNull + public String[] getSubPath() { + return mySubPath; + } + + /** + * @return new file name + */ + @NotNull + public String getNewFileName() { + return myNewFileName; + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/TargetDirectory.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/TargetDirectory.java index 19384373f243..4647c13ed5eb 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/TargetDirectory.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/quickFix/TargetDirectory.java @@ -8,6 +8,12 @@ import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * Describes target directory for quick fixes that create new files. + * + * @see CreateDirectoryFix + * @see CreateFileWithScopeFix + */ public class TargetDirectory { private final SmartPsiElementPointer myDirectory; @NotNull diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/PsiFileReferenceHelper.java b/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/PsiFileReferenceHelper.java index e216ce256b3a..2e73de07a726 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/PsiFileReferenceHelper.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/PsiFileReferenceHelper.java @@ -21,6 +21,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.impl.DirectoryIndex; +import com.intellij.openapi.roots.impl.ProjectFileIndexImpl; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; @@ -31,9 +32,13 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ArrayUtil; import com.intellij.util.Query; import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes; +import org.jetbrains.jps.model.java.JavaResourceRootType; +import org.jetbrains.jps.model.java.JavaSourceRootType; +import org.jetbrains.jps.model.module.JpsModuleSourceRootType; import java.util.*; import java.util.stream.Collectors; @@ -69,6 +74,13 @@ public class PsiFileReferenceHelper extends FileReferenceHelper { return getContextsForModule(module, "", module.getModuleWithDependenciesScope()); } + @NotNull + @Override + public Collection sortTargetContexts(Project project, @NotNull VirtualFile file, + @NotNull Collection targetContexts) { + return sortWithResourcePriority(project, file, targetContexts); + } + @NotNull @Override public Collection getTargetContexts(@NotNull Project project, @NotNull VirtualFile file) { @@ -135,6 +147,109 @@ public class PsiFileReferenceHelper extends FileReferenceHelper { return Collections.emptyList(); } + private static Collection sortWithResourcePriority(@NotNull Project project, @NotNull VirtualFile file, + @NotNull Collection targetContexts) { + // here we try to sort target locations depending on src/test origin + if (targetContexts.isEmpty() || targetContexts.size() == 1) { + return targetContexts; + } + + List targetContextWrappers = findSourceRootTypes(targetContexts); + + // sort only if we have resource roots + if (ContainerUtil.find(targetContextWrappers, PsiFileReferenceHelper::isResourceRoot) == null) { + return targetContexts; + } + + // if file is under sources root then src/resources directories at the top + // if file is under test sources root then test/resources directories at the top + ProjectFileIndex projectFileIndex = ProjectFileIndex.getInstance(project); + if (projectFileIndex.isInTestSourceContent(file)) { + targetContextWrappers.sort(PsiFileReferenceHelper::compareTargetsForTests); + } + else if (projectFileIndex.isInSourceContent(file)) { + targetContextWrappers.sort(PsiFileReferenceHelper::compareTargetsForProduction); + } + return ContainerUtil.map(targetContextWrappers, FileTargetContextWrapper::getTargetContext); + } + + private static List findSourceRootTypes(Collection targetContexts) { + return ContainerUtil.map(targetContexts, c -> { + Project project = c.getFileSystemItem().getProject(); + + SourceFolder sourceFolder = getSourceFolder(project, c.getFileSystemItem().getVirtualFile()); + + return new FileTargetContextWrapper(c, sourceFolder != null ? sourceFolder.getRootType() : null); + }); + } + + @Nullable + private static SourceFolder getSourceFolder(@NotNull Project project, @NotNull VirtualFile directory) { + ProjectFileIndexImpl projectFileIndex = (ProjectFileIndexImpl)ProjectRootManager.getInstance(project).getFileIndex(); + return projectFileIndex.getSourceFolder(directory); + } + + private static int compareTargetsForTests(@NotNull FileTargetContextWrapper d1, @NotNull FileTargetContextWrapper d2) { + int o1 = getTestsTargetOrdinal(d1); + int o2 = getTestsTargetOrdinal(d2); + + if (o1 > 0 && o2 > 0) { + return Integer.compare(o1, o2); + } + + return compareDirectoryPaths(d1, d2); + } + + private static int compareTargetsForProduction(@NotNull FileTargetContextWrapper d1, @NotNull FileTargetContextWrapper d2) { + int o1 = getSourcesTargetOrdinal(d1); + int o2 = getSourcesTargetOrdinal(d2); + + if (o1 > 0 && o2 > 0) { + return Integer.compare(o1, o2); + } + + return compareDirectoryPaths(d1, d2); + } + + private static int getTestsTargetOrdinal(@NotNull FileTargetContextWrapper item) { + JpsModuleSourceRootType type = item.getSourceRootType(); + + if (isSourceItem(type)) return 4; + if (isTestSourceItem(type)) return 3; + if (isResourceItem(type)) return 2; + if (isTestResourceItem(type)) return 1; + + return 0; + } + + private static int getSourcesTargetOrdinal(@NotNull FileTargetContextWrapper item) { + JpsModuleSourceRootType type = item.getSourceRootType(); + + if (isTestSourceItem(type)) return 4; + if (isSourceItem(type)) return 3; + if (isTestResourceItem(type)) return 2; + if (isResourceItem(type)) return 1; + + return 0; + } + + private static boolean isResourceRoot(@NotNull FileTargetContextWrapper d) { + return isResourceItem(d.getSourceRootType()) || + isTestResourceItem(d.getSourceRootType()); + } + + private static int compareDirectoryPaths(@NotNull FileTargetContextWrapper d1, @NotNull FileTargetContextWrapper d2) { + PsiFileSystemItem directory1 = d1.getTargetContext().getFileSystemItem(); + PsiFileSystemItem directory2 = d2.getTargetContext().getFileSystemItem(); + + assert directory1 != null : "Invalid PsiFileSystemItem instances found"; + assert directory2 != null : "Invalid PsiFileSystemItem instances found"; + + VirtualFile f1 = directory1.getVirtualFile(); + VirtualFile f2 = directory2.getVirtualFile(); + return f1.getPath().compareTo(f2.getPath()); + } + private static String[] removeCommonStartPackages(String[] path, String packagePath) { List packages = StringUtil.split(packagePath, "."); List result = new SmartList<>(); @@ -161,23 +276,27 @@ public class PsiFileReferenceHelper extends FileReferenceHelper { @NotNull private static String[] getRelativePath(VirtualFile file, VirtualFile root) { - LinkedList names = new LinkedList<>(); + List names = new ArrayList<>(); VirtualFile parent = file; while (parent != null && !parent.equals(root)) { - - names.addFirst(parent.getName()); + names.add(parent.getName()); parent = parent.getParent(); } - return ArrayUtil.toStringArray(names); + if (names.isEmpty()) return ArrayUtil.EMPTY_STRING_ARRAY; + String[] path = new String[names.size()]; + for (int i = 0; i < names.size(); i++) { + path[i] = names.get(names.size() - i - 1); + } + return path; } @NotNull private static List getMissingTargetFolders(Module module, Collection contextsForModule) { - // todo comment + // find additional source folders that can be used to create a file, e.g. they do not have the exact package, but it can be created ModuleRootModel model = ModuleRootManager.getInstance(module); return Arrays.stream(model.getContentEntries()) @@ -278,4 +397,38 @@ public class PsiFileReferenceHelper extends FileReferenceHelper { return result != null ? result:Collections.emptyList(); } + + private static boolean isTestResourceItem(@Nullable JpsModuleSourceRootType type) { + return type == JavaResourceRootType.TEST_RESOURCE; + } + + private static boolean isResourceItem(@Nullable JpsModuleSourceRootType type) { + return type == JavaResourceRootType.RESOURCE; + } + + private static boolean isTestSourceItem(@Nullable JpsModuleSourceRootType type) { + return type == JavaSourceRootType.TEST_SOURCE; + } + + private static boolean isSourceItem(@Nullable JpsModuleSourceRootType type) { + return type == JavaSourceRootType.SOURCE; + } + + private static class FileTargetContextWrapper { + private final FileTargetContext myTargetContext; + private final JpsModuleSourceRootType myRootType; + + private FileTargetContextWrapper(FileTargetContext context, JpsModuleSourceRootType type) { + myTargetContext = context; + myRootType = type; + } + + private FileTargetContext getTargetContext() { + return myTargetContext; + } + + private JpsModuleSourceRootType getSourceRootType() { + return myRootType; + } + } }