add ProjectFileIndex#getSourceFolder to simplify clients (IDEA-CR-57371)
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / quickFix / AbstractCreateFileFix.java
1 // Copyright 2000-2020 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.codeInsight.daemon.quickFix;
3
4 import com.intellij.codeInsight.CodeInsightBundle;
5 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
6 import com.intellij.codeInsight.hint.HintManager;
7 import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.command.WriteCommandAction;
10 import com.intellij.openapi.editor.Editor;
11 import com.intellij.openapi.project.Project;
12 import com.intellij.openapi.roots.ProjectFileIndex;
13 import com.intellij.openapi.roots.SourceFolder;
14 import com.intellij.openapi.ui.popup.JBPopupFactory;
15 import com.intellij.openapi.ui.popup.JBPopupListener;
16 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
17 import com.intellij.openapi.util.text.StringUtil;
18 import com.intellij.openapi.vfs.VirtualFile;
19 import com.intellij.psi.PsiDirectory;
20 import com.intellij.psi.PsiDocumentManager;
21 import com.intellij.psi.PsiElement;
22 import com.intellij.psi.PsiFile;
23 import com.intellij.ui.SimpleListCellRenderer;
24 import com.intellij.util.IconUtil;
25 import com.intellij.util.IncorrectOperationException;
26 import com.intellij.util.containers.ContainerUtil;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import javax.swing.*;
31 import java.util.List;
32
33 import static com.intellij.openapi.project.ProjectUtilCore.displayUrlRelativeToProject;
34 import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName;
35 import static com.intellij.openapi.vfs.VfsUtilCore.VFS_SEPARATOR_CHAR;
36
37 public abstract class AbstractCreateFileFix extends LocalQuickFixAndIntentionActionOnPsiElement {
38
39   private static final int REFRESH_INTERVAL = 1000;
40
41   protected static final String CURRENT_DIRECTORY_REF = ".";
42   protected static final String PARENT_DIRECTORY_REF = "..";
43
44   protected final String myNewFileName;
45   protected final List<TargetDirectory> myDirectories;
46   protected final String[] mySubPath;
47   @NotNull
48   protected final String myKey;
49
50   protected boolean myIsAvailable;
51   protected long myIsAvailableTimeStamp;
52
53   protected AbstractCreateFileFix(@Nullable PsiElement element,
54                                   @NotNull NewFileLocation newFileLocation,
55                                   @NotNull String fixLocaleKey) {
56     super(element);
57
58     myNewFileName = newFileLocation.getNewFileName();
59     myDirectories = newFileLocation.getDirectories();
60     mySubPath = newFileLocation.getSubPath();
61     myKey = fixLocaleKey;
62   }
63
64   @Override
65   public boolean isAvailable(@NotNull Project project,
66                              @NotNull PsiFile file,
67                              @NotNull PsiElement startElement,
68                              @NotNull PsiElement endElement) {
69     long current = System.currentTimeMillis();
70
71     if (ApplicationManager.getApplication().isUnitTestMode() || current - myIsAvailableTimeStamp > REFRESH_INTERVAL) {
72       if (myDirectories.size() == 1) {
73         PsiDirectory myDirectory = myDirectories.get(0).getDirectory();
74         myIsAvailable &= myDirectory != null && myDirectory.getVirtualFile().findChild(myNewFileName) == null;
75         myIsAvailableTimeStamp = current;
76       }
77       else {
78         // do not check availability for multiple roots
79       }
80     }
81
82     return myIsAvailable;
83   }
84
85   @Override
86   public void invoke(@NotNull Project project,
87                      @NotNull PsiFile file,
88                      @Nullable Editor editor,
89                      @NotNull PsiElement startElement,
90                      @NotNull PsiElement endElement) {
91     if (isAvailable(project, null, file)) {
92       if (myDirectories.size() == 1) {
93         apply(myStartElement.getProject(), myDirectories.get(0), editor);
94       }
95       else {
96         List<TargetDirectory> directories = ContainerUtil.filter(myDirectories, d -> d.getDirectory() != null);
97         if (directories.isEmpty()) {
98           // there are no valid PsiDirectory items
99           return;
100         }
101
102         if (editor == null || ApplicationManager.getApplication().isUnitTestMode()) {
103           // run on first item of sorted list in batch mode
104           apply(myStartElement.getProject(), directories.get(0), editor);
105         }
106         else {
107           showOptionsPopup(project, editor, directories);
108         }
109       }
110     }
111   }
112
113   private void apply(@NotNull Project project, @NotNull TargetDirectory directory, @Nullable Editor editor) {
114     myIsAvailableTimeStamp = 0; // to revalidate applicability
115
116     PsiDirectory currentDirectory = directory.getDirectory();
117     if (currentDirectory == null) {
118       return;
119     }
120
121     try {
122       for (String pathPart : directory.getPathToCreate()) {
123         currentDirectory = findOrCreateSubdirectory(currentDirectory, pathPart);
124       }
125       for (String pathPart : mySubPath) {
126         currentDirectory = findOrCreateSubdirectory(currentDirectory, pathPart);
127       }
128       if (currentDirectory == null) {
129         if (editor != null) {
130           HintManager hintManager = HintManager.getInstance();
131           hintManager.showErrorHint(editor, CodeInsightBundle.message("create.file.incorrect.path.hint", myNewFileName));
132         }
133         return;
134       }
135
136       apply(project, currentDirectory, editor);
137     }
138     catch (IncorrectOperationException e) {
139       myIsAvailable = false;
140     }
141   }
142
143   protected abstract void apply(@NotNull Project project, @NotNull PsiDirectory targetDirectory, @Nullable Editor editor)
144     throws IncorrectOperationException;
145
146   @Nullable
147   private static PsiDirectory findOrCreateSubdirectory(@Nullable PsiDirectory directory, @NotNull String subDirectoryName) {
148     if (directory == null) {
149       return null;
150     }
151     if (CURRENT_DIRECTORY_REF.equals(subDirectoryName)) {
152       return directory;
153     }
154     if (PARENT_DIRECTORY_REF.equals(subDirectoryName)) {
155       return directory.getParentDirectory();
156     }
157
158     PsiDirectory existingDirectory = directory.findSubdirectory(subDirectoryName);
159     if (existingDirectory == null) {
160       return directory.createSubdirectory(subDirectoryName);
161     }
162     return existingDirectory;
163   }
164
165   private void showOptionsPopup(@NotNull Project project,
166                                 @NotNull Editor editor,
167                                 List<TargetDirectory> directories) {
168     List<TargetDirectoryListItem> items = getTargetDirectoryListItems(directories);
169
170     String filePath = myNewFileName;
171     if (mySubPath.length > 0) {
172       filePath = toSystemDependentName(
173         StringUtil.join(mySubPath, Character.toString(VFS_SEPARATOR_CHAR))
174         + VFS_SEPARATOR_CHAR + myNewFileName
175       );
176     }
177
178     SimpleListCellRenderer<TargetDirectoryListItem> renderer = SimpleListCellRenderer.create((label, value, index) -> {
179       label.setIcon(value.getIcon());
180       label.setText(value.getPresentablePath());
181     });
182
183     JBPopupFactory.getInstance()
184       .createPopupChooserBuilder(items)
185       .setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
186       .setTitle(CodeInsightBundle.message(myKey, filePath))
187       .setMovable(false)
188       .setResizable(false)
189       .setRequestFocus(true)
190       .setRenderer(renderer)
191       .setNamerForFiltering(item -> item.getPresentablePath())
192       .setItemChosenCallback(chosenValue -> {
193
194         WriteCommandAction.writeCommandAction(project)
195           .withName(CodeInsightBundle.message("create.file.text", myNewFileName))
196           .run(() -> apply(project, chosenValue.getTarget(), editor));
197       })
198       .addListener(new JBPopupListener() {
199         @Override
200         public void onClosed(@NotNull LightweightWindowEvent event) {
201           // rerun code-insight after popup close
202           PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
203           if (file != null) {
204             DaemonCodeAnalyzer.getInstance(project).restart(file);
205           }
206         }
207       })
208       .createPopup()
209       .showInBestPositionFor(editor);
210   }
211
212   @NotNull
213   private static List<TargetDirectoryListItem> getTargetDirectoryListItems(List<TargetDirectory> directories) {
214     return ContainerUtil.map(directories, targetDirectory -> {
215       PsiDirectory d = targetDirectory.getDirectory();
216       assert d != null : "Invalid PsiDirectory instances found";
217
218       String presentablePath = getPresentableContentRootPath(d, targetDirectory.getPathToCreate());
219       Icon icon = getContentRootIcon(d);
220
221       return new TargetDirectoryListItem(targetDirectory, icon, presentablePath);
222     });
223   }
224
225   @NotNull
226   private static Icon getContentRootIcon(@NotNull PsiDirectory directory) {
227     VirtualFile file = directory.getVirtualFile();
228
229     Project project = directory.getProject();
230     ProjectFileIndex projectFileIndex = ProjectFileIndex.getInstance(project);
231     SourceFolder sourceFolder = projectFileIndex.getSourceFolder(file);
232     if (sourceFolder != null && sourceFolder.getFile() != null) {
233       return IconUtil.getIcon(sourceFolder.getFile(), 0, project);
234     }
235
236     return IconUtil.getIcon(file, 0, project);
237   }
238
239   @NotNull
240   private static String getPresentableContentRootPath(@NotNull PsiDirectory directory,
241                                                       String @NotNull [] pathToCreate) {
242     VirtualFile f = directory.getVirtualFile();
243     Project project = directory.getProject();
244
245     String path = f.getPath();
246     if (pathToCreate.length > 0) {
247       path += VFS_SEPARATOR_CHAR + StringUtil.join(pathToCreate, VFS_SEPARATOR_CHAR + "");
248     }
249     String presentablePath = f.getFileSystem().extractPresentableUrl(path);
250
251     return displayUrlRelativeToProject(f, presentablePath, project, true, true);
252   }
253
254   protected static class TargetDirectoryListItem {
255     private final TargetDirectory myTargetDirectory;
256     private final Icon myIcon;
257     private final String myPresentablePath;
258
259     public TargetDirectoryListItem(@NotNull TargetDirectory targetDirectory,
260                                    Icon icon, @NotNull String presentablePath) {
261       myTargetDirectory = targetDirectory;
262       myIcon = icon;
263       myPresentablePath = presentablePath;
264     }
265
266     public Icon getIcon() {
267       return myIcon;
268     }
269
270     private String getPresentablePath() {
271       return myPresentablePath;
272     }
273
274     private TargetDirectory getTarget() {
275       return myTargetDirectory;
276     }
277   }
278 }