fb1e9471fe6d4b4b8c6448d3a1447bf51163d2f8
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / move / moveFilesOrDirectories / MoveFilesOrDirectoriesUtil.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
17 package com.intellij.refactoring.move.moveFilesOrDirectories;
18
19 import com.intellij.ide.util.DirectoryChooserUtil;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.command.CommandProcessor;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.ui.DialogWrapper;
25 import com.intellij.openapi.util.Computable;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.psi.*;
28 import com.intellij.refactoring.RefactoringBundle;
29 import com.intellij.refactoring.RefactoringSettings;
30 import com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler;
31 import com.intellij.refactoring.move.MoveCallback;
32 import com.intellij.refactoring.move.MoveHandler;
33 import com.intellij.refactoring.util.CommonRefactoringUtil;
34 import com.intellij.util.Function;
35 import com.intellij.util.IncorrectOperationException;
36 import com.intellij.util.containers.ContainerUtil;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.Collection;
43 import java.util.HashMap;
44 import java.util.List;
45
46 public class MoveFilesOrDirectoriesUtil {
47   private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil");
48
49   private MoveFilesOrDirectoriesUtil() {
50   }
51
52   /**
53    * Moves the specified directory to the specified parent directory. Does not process non-code usages!
54    *
55    * @param dir          the directory to move.
56    * @param newParentDir the directory to move <code>dir</code> into.
57    * @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
58    */
59   public static void doMoveDirectory(final PsiDirectory aDirectory, final PsiDirectory destDirectory) throws IncorrectOperationException {
60     PsiManager manager = aDirectory.getManager();
61     // do actual move
62     checkMove(aDirectory, destDirectory);
63
64     try {
65       aDirectory.getVirtualFile().move(manager, destDirectory.getVirtualFile());
66     }
67     catch (IOException e) {
68       throw new IncorrectOperationException(e);
69     }
70   }
71
72   /**
73    * Moves the specified file to the specified directory. Does not process non-code usages! file may be invalidated, need to be refreshed before use, like {@code newDirectory.findFile(file.getName())}
74    *
75    * @param file         the file to move.
76    * @param newDirectory the directory to move the file into.
77    * @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
78    */
79   public static void doMoveFile(final PsiFile file, final PsiDirectory newDirectory) throws IncorrectOperationException {
80     PsiManager manager = file.getManager();
81     // the class is already there, this is true when multiple classes are defined in the same file
82     if (!newDirectory.equals(file.getContainingDirectory())) {
83       // do actual move
84       checkMove(file, newDirectory);
85
86       try {
87         final VirtualFile virtualFile = file.getVirtualFile();
88         LOG.assertTrue(virtualFile != null, file);
89         virtualFile.move(manager, newDirectory.getVirtualFile());
90       }
91       catch (IOException e) {
92         throw new IncorrectOperationException(e);
93       }
94     }
95   }
96
97   /**
98    * @param elements should contain PsiDirectories or PsiFiles only
99    */
100   public static void doMove(final Project project,
101                             final PsiElement[] elements,
102                             final PsiElement[] targetElement,
103                             final MoveCallback moveCallback) {
104     doMove(project, elements, targetElement, moveCallback, null);
105   }
106
107   /**
108    * @param elements should contain PsiDirectories or PsiFiles only if adjustElements == null
109    */
110   public static void doMove(final Project project,
111                             final PsiElement[] elements,
112                             final PsiElement[] targetElement,
113                             final MoveCallback moveCallback,
114                             final Function<PsiElement[], PsiElement[]> adjustElements) {
115     if (adjustElements == null) {
116       for (PsiElement element : elements) {
117         if (!(element instanceof PsiFile) && !(element instanceof PsiDirectory)) {
118           throw new IllegalArgumentException("unexpected element type: " + element);
119         }
120       }
121     }
122
123     final PsiDirectory targetDirectory = resolveToDirectory(project, targetElement[0]);
124     if (targetElement[0] != null && targetDirectory == null) return;
125
126     final PsiElement[] newElements = adjustElements != null ? adjustElements.fun(elements) : elements;
127
128     final PsiDirectory initialTargetDirectory = getInitialTargetDirectory(targetDirectory, elements);
129
130     final MoveFilesOrDirectoriesDialog.Callback doRun = new MoveFilesOrDirectoriesDialog.Callback() {
131       @Override
132       public void run(final MoveFilesOrDirectoriesDialog moveDialog) {
133         CommandProcessor.getInstance().executeCommand(project, () -> {
134           final PsiDirectory targetDirectory1 = moveDialog != null ? moveDialog.getTargetDirectory() : initialTargetDirectory;
135           if (targetDirectory1 == null) {
136             LOG.error("It is null! The target directory, it is null!");
137             return;
138           }
139
140           Collection<PsiElement> toCheck = ContainerUtil.newArrayList((PsiElement)targetDirectory1);
141           for (PsiElement e : newElements) {
142             toCheck.add(e instanceof PsiFileSystemItem && e.getParent() != null ? e.getParent() : e);
143           }
144           if (!CommonRefactoringUtil.checkReadOnlyStatus(project, toCheck, false)) {
145             return;
146           }
147
148           targetElement[0] = targetDirectory1;
149
150           try {
151             final int[] choice = elements.length > 1 || elements[0] instanceof PsiDirectory ? new int[]{-1} : null;
152             final List<PsiElement> els = new ArrayList<>();
153             for (final PsiElement psiElement : newElements) {
154               if (psiElement instanceof PsiFile) {
155                 final PsiFile file = (PsiFile)psiElement;
156                 final boolean fileExist = ApplicationManager.getApplication().runWriteAction(new Computable<Boolean>() {
157                   @Override
158                   public Boolean compute() {
159                     return CopyFilesOrDirectoriesHandler.checkFileExist(targetDirectory1, choice, file, file.getName(), "Move");
160                   }
161                 });
162                 if (fileExist) continue;
163               }
164               checkMove(psiElement, targetDirectory1);
165               els.add(psiElement);
166             }
167             final Runnable callback = () -> {
168               if (moveDialog != null) moveDialog.close(DialogWrapper.CANCEL_EXIT_CODE);
169             };
170             if (els.isEmpty()) {
171               callback.run();
172               return;
173             }
174             new MoveFilesOrDirectoriesProcessor(project, els.toArray(new PsiElement[els.size()]), targetDirectory1,
175                                                 RefactoringSettings.getInstance().MOVE_SEARCH_FOR_REFERENCES_FOR_FILE,
176                                                 false, false, moveCallback, callback).run();
177           }
178           catch (IncorrectOperationException e) {
179             CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(),
180                                                    "refactoring.moveFile", project);
181           }
182         }, MoveHandler.REFACTORING_NAME, null);
183       }
184     };
185
186     if (ApplicationManager.getApplication().isUnitTestMode()) {
187       doRun.run(null);
188     }
189     else {
190       final MoveFilesOrDirectoriesDialog moveDialog = new MoveFilesOrDirectoriesDialog(project, doRun);
191       moveDialog.setData(newElements, initialTargetDirectory, "refactoring.moveFile");
192       moveDialog.show();
193     }
194   }
195
196   @Nullable
197   public static PsiDirectory resolveToDirectory(final Project project, final PsiElement element) {
198     if (!(element instanceof PsiDirectoryContainer)) {
199       return (PsiDirectory)element;
200     }
201
202     PsiDirectory[] directories = ((PsiDirectoryContainer)element).getDirectories();
203     switch (directories.length) {
204       case 0:
205         return null;
206       case 1:
207         return directories[0];
208       default:
209         return DirectoryChooserUtil.chooseDirectory(directories, directories[0], project, new HashMap<>());
210     }
211
212   }
213
214   @Nullable
215   private static PsiDirectory getCommonDirectory(PsiElement[] movedElements) {
216     PsiDirectory commonDirectory = null;
217
218     for (PsiElement movedElement : movedElements) {
219       final PsiDirectory containingDirectory;
220       if (movedElement instanceof PsiDirectory) {
221         containingDirectory = ((PsiDirectory)movedElement).getParentDirectory();
222       }
223       else {
224         final PsiFile containingFile = movedElement.getContainingFile();
225         containingDirectory = containingFile == null ? null : containingFile.getContainingDirectory();
226       }
227
228       if (containingDirectory != null) {
229         if (commonDirectory == null) {
230           commonDirectory = containingDirectory;
231         }
232         else {
233           if (commonDirectory != containingDirectory) {
234             return null;
235           }
236         }
237       }
238     }
239     return commonDirectory;
240   }
241
242   @Nullable
243   public static PsiDirectory getInitialTargetDirectory(PsiDirectory initialTargetElement, final PsiElement[] movedElements) {
244     PsiDirectory initialTargetDirectory = initialTargetElement;
245     if (initialTargetDirectory == null) {
246       if (movedElements != null) {
247         final PsiDirectory commonDirectory = getCommonDirectory(movedElements);
248         if (commonDirectory != null) {
249           initialTargetDirectory = commonDirectory;
250         }
251         else {
252           initialTargetDirectory = getContainerDirectory(movedElements[0]);
253         }
254       }
255     }
256     return initialTargetDirectory;
257   }
258
259   @Nullable
260   private static PsiDirectory getContainerDirectory(final PsiElement psiElement) {
261     if (psiElement instanceof PsiDirectory) {
262       return (PsiDirectory)psiElement;
263     }
264     else if (psiElement != null) {
265       return psiElement.getContainingFile().getContainingDirectory();
266     }
267     else {
268       return null;
269     }
270   }
271
272   /**
273    * Checks if it is possible to move the specified PSI element under the specified container,
274    * and throws an exception if the move is not possible. Does not actually modify anything.
275    *
276    * @param element      the element to check the move possibility.
277    * @param newContainer the target container element to move into.
278    * @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
279    */
280   public static void checkMove(@NotNull PsiElement element, @NotNull PsiElement newContainer) throws IncorrectOperationException {
281     if (element instanceof PsiDirectoryContainer) {
282       PsiDirectory[] dirs = ((PsiDirectoryContainer)element).getDirectories();
283       if (dirs.length == 0) {
284         throw new IncorrectOperationException();
285       }
286       else if (dirs.length > 1) {
287         throw new IncorrectOperationException(
288           "Moving of packages represented by more than one physical directory is not supported.");
289       }
290       checkMove(dirs[0], newContainer);
291       return;
292     }
293
294     //element.checkDelete(); //move != delete + add
295     newContainer.checkAdd(element);
296     checkIfMoveIntoSelf(element, newContainer);
297   }
298
299   public static void checkIfMoveIntoSelf(PsiElement element, PsiElement newContainer) throws IncorrectOperationException {
300     PsiElement container = newContainer;
301     while (container != null) {
302       if (container == element) {
303         if (element instanceof PsiDirectory) {
304           if (element == newContainer) {
305             throw new IncorrectOperationException("Cannot place directory into itself.");
306           }
307           else {
308             throw new IncorrectOperationException("Cannot place directory into its subdirectory.");
309           }
310         }
311         else {
312           throw new IncorrectOperationException();
313         }
314       }
315       container = container.getParent();
316     }
317   }
318 }