089df084fb100f5ef843c9a76ce2f93df8094b37
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / copy / CopyFilesOrDirectoriesHandler.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.refactoring.copy;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.ide.CopyPasteDelegator;
20 import com.intellij.ide.util.EditorHelper;
21 import com.intellij.ide.util.PlatformPackageUtil;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.application.Result;
24 import com.intellij.openapi.application.WriteAction;
25 import com.intellij.openapi.command.WriteCommandAction;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.roots.ProjectRootManager;
29 import com.intellij.openapi.ui.Messages;
30 import com.intellij.openapi.util.ThrowableComputable;
31 import com.intellij.openapi.vfs.VfsUtil;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.openapi.vfs.encoding.EncodingRegistry;
34 import com.intellij.openapi.wm.ToolWindowManager;
35 import com.intellij.psi.*;
36 import com.intellij.psi.impl.file.PsiDirectoryFactory;
37 import com.intellij.psi.util.PsiTreeUtil;
38 import com.intellij.refactoring.RefactoringBundle;
39 import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil;
40 import com.intellij.refactoring.util.CommonRefactoringUtil;
41 import com.intellij.util.ArrayUtil;
42 import com.intellij.util.IncorrectOperationException;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45
46 import java.io.IOException;
47 import java.util.Collections;
48 import java.util.HashSet;
49 import java.util.Set;
50
51 public class CopyFilesOrDirectoriesHandler extends CopyHandlerDelegateBase {
52   private static Logger LOG = Logger.getInstance("com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler");
53
54   @Override
55   public boolean canCopy(PsiElement[] elements, boolean fromUpdate) {
56     Set<String> names = new HashSet<>();
57     for (PsiElement element : elements) {
58       if (!(element instanceof PsiFileSystemItem)) return false;
59       if (!element.isValid()) return false;
60       if (element instanceof PsiCompiledFile) return false;
61
62       String name = ((PsiFileSystemItem) element).getName();
63       if (names.contains(name)) {
64         return false;
65       }
66       names.add(name);
67     }
68
69     PsiElement[] filteredElements = PsiTreeUtil.filterAncestors(elements);
70     return filteredElements.length == elements.length;
71   }
72
73   @Override
74   public void doCopy(final PsiElement[] elements, PsiDirectory defaultTargetDirectory) {
75     if (defaultTargetDirectory == null) {
76       defaultTargetDirectory = getCommonParentDirectory(elements);
77     }
78     Project project = defaultTargetDirectory != null ? defaultTargetDirectory.getProject() : elements [0].getProject();
79     if (defaultTargetDirectory != null) {
80       defaultTargetDirectory = resolveDirectory(defaultTargetDirectory);
81       if (defaultTargetDirectory == null) return;
82     }
83
84     defaultTargetDirectory = tryNotNullizeDirectory(project, defaultTargetDirectory);
85
86     copyAsFiles(elements, defaultTargetDirectory, project);
87   }
88
89   @Nullable
90   private static PsiDirectory tryNotNullizeDirectory(@NotNull Project project, @Nullable PsiDirectory defaultTargetDirectory) {
91     if (defaultTargetDirectory == null) {
92       VirtualFile root = ArrayUtil.getFirstElement(ProjectRootManager.getInstance(project).getContentRoots());
93       if (root == null) root = project.getBaseDir();
94       if (root == null) root = VfsUtil.getUserHomeDir();
95       defaultTargetDirectory = root != null ? PsiManager.getInstance(project).findDirectory(root) : null;
96
97       if (defaultTargetDirectory == null) {
98         LOG.warn("No directory found for project: " + project.getName() +", root: " + root);
99       }
100     }
101     return defaultTargetDirectory;
102   }
103
104   public static void copyAsFiles(PsiElement[] elements, @Nullable PsiDirectory defaultTargetDirectory, Project project) {
105     doCopyAsFiles(elements, defaultTargetDirectory, project);
106   }
107
108   private static void doCopyAsFiles(PsiElement[] elements, @Nullable PsiDirectory defaultTargetDirectory, Project project) {
109     PsiDirectory targetDirectory = null;
110     String newName = null;
111     boolean openInEditor = true;
112
113     if (ApplicationManager.getApplication().isUnitTestMode()) {
114       targetDirectory = defaultTargetDirectory;
115     }
116     else {
117       CopyFilesOrDirectoriesDialog dialog = new CopyFilesOrDirectoriesDialog(elements, defaultTargetDirectory, project, false);
118       if (dialog.showAndGet()) {
119         newName = elements.length == 1 ? dialog.getNewName() : null;
120         targetDirectory = dialog.getTargetDirectory();
121         openInEditor = dialog.openInEditor();
122       }
123     }
124
125     if (targetDirectory != null) {
126       try {
127         for (PsiElement element : elements) {
128           PsiFileSystemItem psiElement = (PsiFileSystemItem)element;
129           if (psiElement.isDirectory()) {
130             MoveFilesOrDirectoriesUtil.checkIfMoveIntoSelf(psiElement, targetDirectory);
131           }
132         }
133       }
134       catch (IncorrectOperationException e) {
135         CommonRefactoringUtil.showErrorHint(project, null, e.getMessage(), CommonBundle.getErrorTitle(), null);
136         return;
137       }
138
139       copyImpl(elements, newName, targetDirectory, false, openInEditor);
140     }
141   }
142
143   @Override
144   public void doClone(final PsiElement element) {
145     doCloneFile(element);
146   }
147
148   public static void doCloneFile(PsiElement element) {
149     PsiDirectory targetDirectory;
150     if (element instanceof PsiDirectory) {
151       targetDirectory = ((PsiDirectory)element).getParentDirectory();
152     }
153     else {
154       targetDirectory = PlatformPackageUtil.getDirectory(element);
155     }
156     targetDirectory = tryNotNullizeDirectory(element.getProject(), targetDirectory);
157     if (targetDirectory == null) return;
158
159     PsiElement[] elements = {element};
160     CopyFilesOrDirectoriesDialog dialog = new CopyFilesOrDirectoriesDialog(elements, null, element.getProject(), true);
161     if (dialog.showAndGet()) {
162       String newName = dialog.getNewName();
163       copyImpl(elements, newName, targetDirectory, true, true);
164     }
165   }
166
167   @Nullable
168   private static PsiDirectory getCommonParentDirectory(PsiElement[] elements){
169     PsiDirectory result = null;
170
171     for (PsiElement element : elements) {
172       PsiDirectory directory;
173
174       if (element instanceof PsiDirectory) {
175         directory = (PsiDirectory)element;
176         directory = directory.getParentDirectory();
177       }
178       else if (element instanceof PsiFile) {
179         directory = PlatformPackageUtil.getDirectory(element);
180       }
181       else {
182         throw new IllegalArgumentException("unexpected element " + element);
183       }
184
185       if (directory == null) continue;
186
187       if (result == null) {
188         result = directory;
189       }
190       else {
191         if (PsiTreeUtil.isAncestor(directory, result, true)) {
192           result = directory;
193         }
194       }
195     }
196
197     return result;
198   }
199
200   /**
201    * @param elements
202    * @param newName can be not null only if elements.length == 1
203    * @param targetDirectory
204    * @param openInEditor
205    */
206   private static void copyImpl(@NotNull final PsiElement[] elements,
207                                @Nullable final String newName,
208                                @NotNull final PsiDirectory targetDirectory,
209                                final boolean doClone,
210                                final boolean openInEditor) {
211     if (doClone && elements.length != 1) {
212       throw new IllegalArgumentException("invalid number of elements to clone:" + elements.length);
213     }
214
215     if (newName != null && elements.length != 1) {
216       throw new IllegalArgumentException("no new name should be set; number of elements is: " + elements.length);
217     }
218
219     final Project project = targetDirectory.getProject();
220     if (!CommonRefactoringUtil.checkReadOnlyStatus(project, Collections.singleton(targetDirectory), false)) {
221       return;
222     }
223
224     String title = RefactoringBundle.message(doClone ? "copy,handler.clone.files.directories" : "copy.handler.copy.files.directories");
225     new WriteCommandAction(project, title) {
226       @Override
227       protected void run(@NotNull Result result) {
228         try {
229           PsiFile firstFile = null;
230           final int[] choice = elements.length > 1 || elements[0] instanceof PsiDirectory ? new int[]{-1} : null;
231           for (PsiElement element : elements) {
232             PsiFile f = copyToDirectory((PsiFileSystemItem)element, newName, targetDirectory, choice);
233             if (firstFile == null) {
234               firstFile = f;
235             }
236           }
237
238           if (firstFile != null && openInEditor) {
239             CopyHandler.updateSelectionInActiveProjectView(firstFile, project, doClone);
240             if (!(firstFile instanceof PsiBinaryFile)) {
241               EditorHelper.openInEditor(firstFile);
242               ApplicationManager.getApplication().invokeLater(() -> ToolWindowManager.getInstance(project).activateEditorComponent(), project.getDisposed());
243             }
244           }
245         }
246         catch (final IncorrectOperationException ex) {
247           ApplicationManager.getApplication().invokeLater(
248             () -> Messages.showErrorDialog(project, ex.getMessage(), RefactoringBundle.message("error.title")));
249         }
250         catch (final IOException ex) {
251           ApplicationManager.getApplication().invokeLater(
252             () -> Messages.showErrorDialog(project, ex.getMessage(), RefactoringBundle.message("error.title")));
253         }
254       }
255     }.execute();
256   }
257
258   /**
259    * @param elementToCopy PsiFile or PsiDirectory
260    * @param newName can be not null only if elements.length == 1
261    * @return first copied PsiFile (recursively); null if no PsiFiles copied
262    */
263   @Nullable
264   public static PsiFile copyToDirectory(@NotNull PsiFileSystemItem elementToCopy,
265                                         @Nullable String newName,
266                                         @NotNull PsiDirectory targetDirectory) throws IncorrectOperationException, IOException {
267     return copyToDirectory(elementToCopy, newName, targetDirectory, null);
268   }
269
270   /**
271    * @param elementToCopy PsiFile or PsiDirectory
272    * @param newName can be not null only if elements.length == 1
273    * @param choice a horrible way to pass/keep user preference
274    * @return first copied PsiFile (recursively); null if no PsiFiles copied
275    */
276   @Nullable
277   public static PsiFile copyToDirectory(@NotNull PsiFileSystemItem elementToCopy,
278                                         @Nullable String newName,
279                                         @NotNull PsiDirectory targetDirectory,
280                                         @Nullable int[] choice) throws IncorrectOperationException, IOException {
281     if (elementToCopy instanceof PsiFile) {
282       PsiFile file = (PsiFile)elementToCopy;
283       String name = newName == null ? file.getName() : newName;
284       if (checkFileExist(targetDirectory, choice, file, name, "Copy")) return null;
285       return targetDirectory.copyFileFrom(name, file);
286     }
287     else if (elementToCopy instanceof PsiDirectory) {
288       PsiDirectory directory = (PsiDirectory)elementToCopy;
289       if (directory.equals(targetDirectory)) {
290         return null;
291       }
292       if (newName == null) newName = directory.getName();
293       final PsiDirectory existing = targetDirectory.findSubdirectory(newName);
294       final PsiDirectory subdirectory = existing == null ? targetDirectory.createSubdirectory(newName) : existing;
295       EncodingRegistry.doActionAndRestoreEncoding(directory.getVirtualFile(),
296                                                   (ThrowableComputable<VirtualFile, IOException>)() -> subdirectory.getVirtualFile());
297
298       PsiFile firstFile = null;
299       PsiElement[] children = directory.getChildren();
300       for (PsiElement child : children) {
301         PsiFileSystemItem item = (PsiFileSystemItem)child;
302         PsiFile f = copyToDirectory(item, item.getName(), subdirectory, choice);
303         if (firstFile == null) {
304           firstFile = f;
305         }
306       }
307       return firstFile;
308     }
309     else {
310       throw new IllegalArgumentException("unexpected elementToCopy: " + elementToCopy);
311     }
312   }
313
314   public static boolean checkFileExist(@Nullable PsiDirectory targetDirectory, int[] choice, PsiFile file, String name, String title) {
315     if (targetDirectory == null) return false;
316     final PsiFile existing = targetDirectory.findFile(name);
317     if (existing != null && !existing.equals(file)) {
318       int selection;
319       if (choice == null || choice[0] == -1) {
320         String message = String.format("File '%s' already exists in directory '%s'", name, targetDirectory.getVirtualFile().getPath());
321         String[] options = choice == null ? new String[]{"Overwrite", "Skip"}
322                                           : new String[]{"Overwrite", "Skip", "Overwrite for all", "Skip for all"};
323         selection = Messages.showDialog(message, title, options, 0, Messages.getQuestionIcon());
324       }
325       else {
326         selection = choice[0];
327       }
328
329       if (choice != null && selection > 1) {
330         choice[0] = selection % 2;
331         selection = choice[0];
332       }
333
334       if (selection == 0 && file != existing) {
335         WriteAction.run(() -> existing.delete());
336       }
337       else {
338         return true;
339       }
340     }
341
342     return false;
343   }
344
345   @Nullable
346   protected static PsiDirectory resolveDirectory(@NotNull PsiDirectory defaultTargetDirectory) {
347     final Project project = defaultTargetDirectory.getProject();
348     final Boolean showDirsChooser = defaultTargetDirectory.getCopyableUserData(CopyPasteDelegator.SHOW_CHOOSER_KEY);
349     if (showDirsChooser != null && showDirsChooser.booleanValue()) {
350       final PsiDirectoryContainer directoryContainer =
351         PsiDirectoryFactory.getInstance(project).getDirectoryContainer(defaultTargetDirectory);
352       if (directoryContainer == null) {
353         return defaultTargetDirectory;
354       }
355       return MoveFilesOrDirectoriesUtil.resolveToDirectory(project, directoryContainer);
356     }
357     return defaultTargetDirectory;
358   }
359 }