avoid dlg from write action
[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     try {
226       PsiFile firstFile = null;
227       final int[] choice = elements.length > 1 || elements[0] instanceof PsiDirectory ? new int[]{-1} : null;
228       for (PsiElement element : elements) {
229         PsiFile f = copyToDirectory((PsiFileSystemItem)element, newName, targetDirectory, choice, title);
230         if (firstFile == null) {
231           firstFile = f;
232         }
233       }
234
235       if (firstFile != null && openInEditor) {
236         CopyHandler.updateSelectionInActiveProjectView(firstFile, project, doClone);
237         if (!(firstFile instanceof PsiBinaryFile)) {
238           EditorHelper.openInEditor(firstFile);
239           ToolWindowManager.getInstance(project).activateEditorComponent();
240         }
241       }
242     }
243     catch (final IncorrectOperationException ex) {
244       Messages.showErrorDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"));
245     }
246     catch (final IOException ex) {
247       Messages.showErrorDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"));
248     }
249   }
250
251   /**
252    * @param elementToCopy PsiFile or PsiDirectory
253    * @param newName can be not null only if elements.length == 1
254    * @return first copied PsiFile (recursively); null if no PsiFiles copied
255    */
256   @Nullable
257   public static PsiFile copyToDirectory(@NotNull PsiFileSystemItem elementToCopy,
258                                         @Nullable String newName,
259                                         @NotNull PsiDirectory targetDirectory) throws IncorrectOperationException, IOException {
260     return copyToDirectory(elementToCopy, newName, targetDirectory, null, null);
261   }
262
263   /**
264    * @param elementToCopy PsiFile or PsiDirectory
265    * @param newName can be not null only if elements.length == 1
266    * @param choice a horrible way to pass/keep user preference
267    * @return first copied PsiFile (recursively); null if no PsiFiles copied
268    */
269   @Nullable
270   public static PsiFile copyToDirectory(@NotNull PsiFileSystemItem elementToCopy,
271                                         @Nullable String newName,
272                                         @NotNull PsiDirectory targetDirectory,
273                                         @Nullable int[] choice,
274                                         @Nullable String title) throws IncorrectOperationException, IOException {
275     if (elementToCopy instanceof PsiFile) {
276       PsiFile file = (PsiFile)elementToCopy;
277       String name = newName == null ? file.getName() : newName;
278       if (checkFileExist(targetDirectory, choice, file, name, "Copy")) return null;
279       return new WriteCommandAction<PsiFile>(targetDirectory.getProject(), title) {
280         @Override
281         protected void run(@NotNull Result<PsiFile> result) throws Throwable {
282           result.setResult(targetDirectory.copyFileFrom(name, file));
283         }
284       }.execute().getResultObject();
285     }
286     else if (elementToCopy instanceof PsiDirectory) {
287       PsiDirectory directory = (PsiDirectory)elementToCopy;
288       if (directory.equals(targetDirectory)) {
289         return null;
290       }
291       if (newName == null) newName = directory.getName();
292       final PsiDirectory existing = targetDirectory.findSubdirectory(newName);
293       final PsiDirectory subdirectory = existing == null ? targetDirectory.createSubdirectory(newName) : existing;
294       EncodingRegistry.doActionAndRestoreEncoding(directory.getVirtualFile(),
295                                                   (ThrowableComputable<VirtualFile, IOException>)() -> subdirectory.getVirtualFile());
296
297       PsiFile firstFile = null;
298       PsiElement[] children = directory.getChildren();
299       for (PsiElement child : children) {
300         PsiFileSystemItem item = (PsiFileSystemItem)child;
301         PsiFile f = copyToDirectory(item, item.getName(), subdirectory, choice, title);
302         if (firstFile == null) {
303           firstFile = f;
304         }
305       }
306       return firstFile;
307     }
308     else {
309       throw new IllegalArgumentException("unexpected elementToCopy: " + elementToCopy);
310     }
311   }
312
313   public static boolean checkFileExist(@Nullable PsiDirectory targetDirectory, int[] choice, PsiFile file, String name, String title) {
314     if (targetDirectory == null) return false;
315     final PsiFile existing = targetDirectory.findFile(name);
316     if (existing != null && !existing.equals(file)) {
317       int selection;
318       if (choice == null || choice[0] == -1) {
319         String message = String.format("File '%s' already exists in directory '%s'", name, targetDirectory.getVirtualFile().getPath());
320         String[] options = choice == null ? new String[]{"Overwrite", "Skip"}
321                                           : new String[]{"Overwrite", "Skip", "Overwrite for all", "Skip for all"};
322         selection = Messages.showDialog(message, title, options, 0, Messages.getQuestionIcon());
323       }
324       else {
325         selection = choice[0];
326       }
327
328       if (choice != null && selection > 1) {
329         choice[0] = selection % 2;
330         selection = choice[0];
331       }
332
333       if (selection == 0 && file != existing) {
334         WriteAction.run(() -> existing.delete());
335       }
336       else {
337         return true;
338       }
339     }
340
341     return false;
342   }
343
344   @Nullable
345   protected static PsiDirectory resolveDirectory(@NotNull PsiDirectory defaultTargetDirectory) {
346     final Project project = defaultTargetDirectory.getProject();
347     final Boolean showDirsChooser = defaultTargetDirectory.getCopyableUserData(CopyPasteDelegator.SHOW_CHOOSER_KEY);
348     if (showDirsChooser != null && showDirsChooser.booleanValue()) {
349       final PsiDirectoryContainer directoryContainer =
350         PsiDirectoryFactory.getInstance(project).getDirectoryContainer(defaultTargetDirectory);
351       if (directoryContainer == null) {
352         return defaultTargetDirectory;
353       }
354       return MoveFilesOrDirectoriesUtil.resolveToDirectory(project, directoryContainer);
355     }
356     return defaultTargetDirectory;
357   }
358 }