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