2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.refactoring.copy;
18 import com.intellij.codeInsight.actions.OptimizeImportsProcessor;
19 import com.intellij.featureStatistics.FeatureUsageTracker;
20 import com.intellij.ide.util.EditorHelper;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.application.WriteAction;
23 import com.intellij.openapi.command.CommandProcessor;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.roots.JavaProjectRootsUtil;
27 import com.intellij.openapi.roots.ProjectRootManager;
28 import com.intellij.openapi.ui.Messages;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vfs.VirtualFile;
31 import com.intellij.openapi.wm.ToolWindowManager;
32 import com.intellij.psi.*;
33 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
34 import com.intellij.psi.search.LocalSearchScope;
35 import com.intellij.psi.search.searches.ReferencesSearch;
36 import com.intellij.psi.util.PsiUtilCore;
37 import com.intellij.refactoring.MoveDestination;
38 import com.intellij.refactoring.RefactoringBundle;
39 import com.intellij.refactoring.move.moveClassesOrPackages.MoveDirectoryWithClassesProcessor;
40 import com.intellij.util.ArrayUtil;
41 import com.intellij.util.ArrayUtilRt;
42 import com.intellij.util.IncorrectOperationException;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
46 import java.io.IOException;
49 public class CopyClassesHandler extends CopyHandlerDelegateBase {
50 private static final Logger LOG = Logger.getInstance("#" + CopyClassesHandler.class.getName());
53 public boolean forbidToClone(PsiElement[] elements, boolean fromUpdate) {
54 final Map<PsiFile, PsiClass[]> fileMap = convertToTopLevelClasses(elements, fromUpdate, null, null);
55 if (fileMap != null && fileMap.size() == 1) {
56 final PsiClass[] psiClasses = fileMap.values().iterator().next();
57 return psiClasses != null && psiClasses.length > 1;
63 public boolean canCopy(PsiElement[] elements, boolean fromUpdate) {
64 return canCopyClass(fromUpdate, elements);
66 public static boolean canCopyClass(PsiElement... elements) {
67 return canCopyClass(false, elements);
69 public static boolean canCopyClass(boolean fromUpdate, PsiElement... elements) {
70 if (fromUpdate && elements.length > 0 && elements[0] instanceof PsiDirectory) return true;
71 return convertToTopLevelClasses(elements, fromUpdate, null, null) != null;
75 private static Map<PsiFile, PsiClass[]> convertToTopLevelClasses(final PsiElement[] elements,
76 final boolean fromUpdate,
78 Map<PsiFile, String> relativeMap) {
79 final Map<PsiFile, PsiClass[]> result = new HashMap<>();
80 for (PsiElement element : elements) {
81 final PsiElement navigationElement = element.getNavigationElement();
82 LOG.assertTrue(navigationElement != null, element);
83 final PsiFile containingFile = navigationElement.getContainingFile();
84 if (!(containingFile instanceof PsiClassOwner &&
85 JavaProjectRootsUtil.isOutsideJavaSourceRoot(containingFile))) {
86 PsiClass[] topLevelClasses = getTopLevelClasses(element);
87 if (topLevelClasses == null) {
88 if (element instanceof PsiDirectory) {
90 final String name = ((PsiDirectory)element).getName();
91 final String path = relativePath != null ? (relativePath.length() > 0 ? (relativePath + "/") : "") + name : null;
92 final Map<PsiFile, PsiClass[]> map = convertToTopLevelClasses(element.getChildren(), fromUpdate, path, relativeMap);
93 if (map == null) return null;
94 for (Map.Entry<PsiFile, PsiClass[]> entry : map.entrySet()) {
95 fillResultsMap(result, entry.getKey(), entry.getValue());
100 if (!(element instanceof PsiFileSystemItem)) return null;
102 fillResultsMap(result, containingFile, topLevelClasses);
103 if (relativeMap != null) {
104 relativeMap.put(containingFile, relativePath);
108 if (result.isEmpty()) {
112 boolean hasClasses = false;
113 for (PsiClass[] classes : result.values()) {
114 if (classes != null) {
119 return hasClasses ? result : null;
124 private static String normalizeRelativeMap(Map<PsiFile, String> relativeMap) {
125 String vector = null;
126 for (String relativePath : relativeMap.values()) {
127 if (vector == null) {
128 vector = relativePath;
129 } else if (vector.startsWith(relativePath + "/")) {
130 vector = relativePath;
131 } else if (!relativePath.startsWith(vector + "/") && !relativePath.equals(vector)) {
135 if (vector != null) {
136 for (PsiFile psiFile : relativeMap.keySet()) {
137 final String path = relativeMap.get(psiFile);
138 relativeMap.put(psiFile, path.equals(vector) ? "" : path.substring(vector.length() + 1));
144 private static void fillResultsMap(Map<PsiFile, PsiClass[]> result, PsiFile containingFile, PsiClass[] topLevelClasses) {
145 PsiClass[] classes = result.get(containingFile);
146 if (topLevelClasses != null) {
147 if (classes != null) {
148 topLevelClasses = ArrayUtil.mergeArrays(classes, topLevelClasses, PsiClass.ARRAY_FACTORY);
150 result.put(containingFile, topLevelClasses);
152 result.put(containingFile, classes);
156 public void doCopy(PsiElement[] elements, PsiDirectory defaultTargetDirectory) {
157 FeatureUsageTracker.getInstance().triggerFeatureUsed("refactoring.copyClass");
158 final HashMap<PsiFile, String> relativePathsMap = new HashMap<>();
159 final Map<PsiFile, PsiClass[]> classes = convertToTopLevelClasses(elements, false, "", relativePathsMap);
160 assert classes != null;
161 if (defaultTargetDirectory == null) {
162 final PsiFile psiFile = classes.keySet().iterator().next();
163 defaultTargetDirectory = psiFile.getContainingDirectory();
164 LOG.assertTrue(defaultTargetDirectory != null, psiFile);
166 Project project = defaultTargetDirectory.getProject();
167 VirtualFile sourceRootForFile = ProjectRootManager.getInstance(project).getFileIndex().getSourceRootForFile(defaultTargetDirectory.getVirtualFile());
168 if (sourceRootForFile == null) {
169 final List<PsiElement> files = new ArrayList<>();
170 for (PsiElement element : elements) {
171 PsiFile containingFile = element.getContainingFile();
172 if (containingFile != null) {
173 files.add(containingFile);
175 else if (element instanceof PsiDirectory) {
179 CopyFilesOrDirectoriesHandler.copyAsFiles(files.toArray(new PsiElement[files.size()]), defaultTargetDirectory, project);
182 Object targetDirectory = null;
183 String className = null;
184 boolean openInEditor = true;
185 if (copyOneClass(classes)) {
186 final String commonPath =
187 ArrayUtilRt.find(elements, classes.values().iterator().next()) == -1 ? normalizeRelativeMap(relativePathsMap) : null;
188 CopyClassDialog dialog = new CopyClassDialog(classes.values().iterator().next()[0], defaultTargetDirectory, project, false) {
190 protected String getQualifiedName() {
191 final String qualifiedName = super.getQualifiedName();
192 if (commonPath != null && !commonPath.isEmpty() && !qualifiedName.endsWith(commonPath)) {
193 return StringUtil.getQualifiedName(qualifiedName, commonPath.replaceAll("/", "."));
195 return qualifiedName;
198 dialog.setTitle(RefactoringBundle.message("copy.handler.copy.class"));
199 if (dialog.showAndGet()) {
200 openInEditor = dialog.openInEditor();
201 targetDirectory = dialog.getTargetDirectory();
202 className = dialog.getClassName();
203 if (className == null || className.length() == 0) return;
206 if (ApplicationManager.getApplication().isUnitTestMode()) {
207 targetDirectory = defaultTargetDirectory;
209 defaultTargetDirectory = CopyFilesOrDirectoriesHandler.resolveDirectory(defaultTargetDirectory);
210 if (defaultTargetDirectory == null) return;
211 PsiElement[] files = PsiUtilCore.toPsiFileArray(classes.keySet());
212 if (classes.keySet().size() == 1) {
213 //do not choose a new name for a file when multiple classes exist in one file
214 final PsiClass[] psiClasses = classes.values().iterator().next();
215 if (psiClasses != null) {
219 final CopyFilesOrDirectoriesDialog dialog = new CopyFilesOrDirectoriesDialog(files, defaultTargetDirectory, project, false);
220 if (dialog.showAndGet()) {
221 targetDirectory = dialog.getTargetDirectory();
222 className = dialog.getNewName();
223 openInEditor = dialog.openInEditor();
227 if (targetDirectory != null) {
228 copyClassesImpl(className, project, classes, relativePathsMap, targetDirectory, defaultTargetDirectory, RefactoringBundle.message(
229 "copy.handler.copy.class"), false, openInEditor);
233 private static boolean copyOneClass(Map<PsiFile, PsiClass[]> classes) {
234 if (classes.size() == 1){
235 final PsiClass[] psiClasses = classes.values().iterator().next();
236 return psiClasses != null && psiClasses.length == 1;
241 public void doClone(PsiElement element) {
242 FeatureUsageTracker.getInstance().triggerFeatureUsed("refactoring.copyClass");
243 PsiClass[] classes = getTopLevelClasses(element);
244 if (classes == null) {
245 CopyFilesOrDirectoriesHandler.doCloneFile(element);
248 Project project = element.getProject();
250 CopyClassDialog dialog = new CopyClassDialog(classes[0], null, project, true);
251 dialog.setTitle(RefactoringBundle.message("copy.handler.clone.class"));
252 if (dialog.showAndGet()) {
253 String className = dialog.getClassName();
254 PsiDirectory targetDirectory = element.getContainingFile().getContainingDirectory();
255 copyClassesImpl(className, project, Collections.singletonMap(classes[0].getContainingFile(), classes), null, targetDirectory,
256 targetDirectory, RefactoringBundle.message("copy.handler.clone.class"), true, true);
260 private static void copyClassesImpl(final String copyClassName,
261 final Project project,
262 final Map<PsiFile, PsiClass[]> classes,
263 final HashMap<PsiFile, String> map,
264 final Object targetDirectory,
265 final PsiDirectory defaultTargetDirectory,
266 final String commandName,
267 final boolean selectInActivePanel,
268 final boolean openInEditor) {
269 final boolean[] result = new boolean[] {false};
270 Runnable command = () -> {
272 if (targetDirectory instanceof PsiDirectory) {
273 target = (PsiDirectory)targetDirectory;
275 target = WriteAction.compute(() -> (MoveDestination)targetDirectory).getTargetDirectory(defaultTargetDirectory);
278 Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project);
281 for (PsiFile file : files) {
282 CopyHandler.updateSelectionInActiveProjectView(file, project, selectInActivePanel);
284 EditorHelper.openFilesInEditor(files.toArray(new PsiFile[files.size()]));
288 catch (IncorrectOperationException ex) {
289 Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon());
292 CommandProcessor processor = CommandProcessor.getInstance();
293 processor.executeCommand(project, command, commandName, null);
296 ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project).activateEditorComponent());
301 public static Collection<PsiFile> doCopyClasses(final Map<PsiFile, PsiClass[]> fileToClasses,
302 final String copyClassName,
303 final PsiDirectory targetDirectory,
304 final Project project) throws IncorrectOperationException {
305 return doCopyClasses(fileToClasses, null, copyClassName, targetDirectory, project);
309 public static Collection<PsiFile> doCopyClasses(final Map<PsiFile, PsiClass[]> fileToClasses,
310 @Nullable HashMap<PsiFile, String> map, final String copyClassName,
311 final PsiDirectory targetDirectory,
312 final Project project) throws IncorrectOperationException {
313 PsiElement newElement = null;
314 final Map<PsiClass, PsiElement> oldToNewMap = new HashMap<>();
315 for (final PsiClass[] psiClasses : fileToClasses.values()) {
316 if (psiClasses != null) {
317 for (PsiClass aClass : psiClasses) {
318 if (isSynthetic(aClass)) {
321 oldToNewMap.put(aClass, null);
325 final List<PsiFile> createdFiles = new ArrayList<>(fileToClasses.size());
326 int[] choice = fileToClasses.size() > 1 ? new int[]{-1} : null;
327 List<PsiFile> files = new ArrayList<>();
328 for (final Map.Entry<PsiFile, PsiClass[]> entry : fileToClasses.entrySet()) {
329 final PsiFile psiFile = entry.getKey();
330 final PsiClass[] sources = entry.getValue();
331 if (psiFile instanceof PsiClassOwner && sources != null) {
332 final PsiFile createdFile = copy(psiFile, targetDirectory, copyClassName, map == null ? null : map.get(psiFile), choice);
333 if (createdFile == null) {
334 //do not touch unmodified classes
335 for (PsiClass aClass : ((PsiClassOwner)psiFile).getClasses()) {
336 oldToNewMap.remove(aClass);
340 for (final PsiClass destination : ((PsiClassOwner)createdFile).getClasses()) {
341 if (isSynthetic(destination)) {
344 PsiClass source = findByName(sources, destination.getName());
345 if (source != null) {
346 final PsiClass copy = copy(source, copyClassName);
347 newElement = WriteAction.compute(() -> destination.replace(copy));
348 oldToNewMap.put(source, newElement);
351 WriteAction.run(() -> destination.delete());
354 createdFiles.add(createdFile);
361 for (PsiFile file : files) {
363 PsiDirectory finalTarget = targetDirectory;
364 final String relativePath = map != null ? map.get(file) : null;
365 if (relativePath != null && !relativePath.isEmpty()) {
366 finalTarget = WriteAction.compute(() -> buildRelativeDir(targetDirectory, relativePath).findOrCreateTargetDirectory());
368 final PsiFile fileCopy = CopyFilesOrDirectoriesHandler.copyToDirectory(file, getNewFileName(file, copyClassName), finalTarget, choice, null);
369 if (fileCopy != null) {
370 createdFiles.add(fileCopy);
373 catch (IOException e) {
374 throw new IncorrectOperationException(e.getMessage());
378 WriteAction.run(() -> {
379 final Set<PsiElement> rebindExpressions = new HashSet<>();
380 for (PsiElement element : oldToNewMap.values()) {
381 if (element == null) {
382 LOG.error(oldToNewMap.keySet());
385 decodeRefs(element, oldToNewMap, rebindExpressions);
388 final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project);
389 for (PsiFile psiFile : createdFiles) {
390 if (psiFile instanceof PsiJavaFile) {
391 codeStyleManager.removeRedundantImports((PsiJavaFile)psiFile);
394 for (PsiElement expression : rebindExpressions) {
395 //filter out invalid elements which are produced by nested elements:
396 //new expressions/type elements, like: List<List<String>>; new Foo(new Foo()), etc
397 if (expression.isValid()) {
398 codeStyleManager.shortenClassReferences(expression);
403 new OptimizeImportsProcessor(project, createdFiles.toArray(new PsiFile[createdFiles.size()]), null).run();
407 protected static boolean isSynthetic(PsiClass aClass) {
408 return aClass instanceof SyntheticElement || !aClass.isPhysical();
411 private static PsiFile copy(@NotNull PsiFile file, PsiDirectory directory, String name, String relativePath, int[] choice) {
412 final String fileName = getNewFileName(file, name);
413 if (relativePath != null && !relativePath.isEmpty()) {
414 return WriteAction.compute(() -> buildRelativeDir(directory, relativePath).findOrCreateTargetDirectory().copyFileFrom(fileName, file));
416 if (CopyFilesOrDirectoriesHandler.checkFileExist(directory, choice, file, fileName, "Copy")) return null;
417 return WriteAction.compute(() -> directory.copyFileFrom(fileName, file));
420 private static String getNewFileName(PsiFile file, String name) {
422 if (file instanceof PsiClassOwner) {
423 for (final PsiClass psiClass : ((PsiClassOwner)file).getClasses()) {
424 if (!isSynthetic(psiClass)) {
425 return name + "." + file.getViewProvider().getVirtualFile().getExtension();
431 return file.getName();
435 private static MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper buildRelativeDir(final @NotNull PsiDirectory directory,
436 final @NotNull String relativePath) {
437 MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper current = null;
438 for (String pathElement : relativePath.split("/")) {
439 if (current == null) {
440 current = new MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper(directory, pathElement);
442 current = new MoveDirectoryWithClassesProcessor.TargetDirectoryWrapper(current, pathElement);
445 LOG.assertTrue(current != null);
449 private static PsiClass copy(PsiClass aClass, String name) {
450 final PsiClass classNavigationElement = (PsiClass)aClass.getNavigationElement();
451 final PsiClass classCopy = (PsiClass)classNavigationElement.copy();
453 classCopy.setName(name);
459 private static PsiClass findByName(PsiClass[] classes, String name) {
461 for (PsiClass aClass : classes) {
462 if (name.equals(aClass.getName())) {
470 private static void rebindExternalReferences(PsiElement element,
471 Map<PsiClass, PsiElement> oldToNewMap,
472 Set<PsiElement> rebindExpressions) {
473 final LocalSearchScope searchScope = new LocalSearchScope(element);
474 for (PsiClass aClass : oldToNewMap.keySet()) {
475 final PsiElement newClass = oldToNewMap.get(aClass);
476 for (PsiReference reference : ReferencesSearch.search(aClass, searchScope)) {
477 rebindExpressions.add(reference.bindToElement(newClass));
483 private static void decodeRefs(@NotNull PsiElement element, final Map<PsiClass, PsiElement> oldToNewMap, final Set<PsiElement> rebindExpressions) {
484 final Map<PsiJavaCodeReferenceElement, PsiElement> rebindMap = new LinkedHashMap<>();
485 element.accept(new JavaRecursiveElementVisitor(){
487 public void visitReferenceElement(PsiJavaCodeReferenceElement reference) {
488 super.visitReferenceElement(reference);
489 decodeRef(reference, oldToNewMap, rebindMap);
492 for (Map.Entry<PsiJavaCodeReferenceElement, PsiElement> entry : rebindMap.entrySet()) {
493 rebindExpressions.add(entry.getKey().bindToElement(entry.getValue()));
495 rebindExternalReferences(element, oldToNewMap, rebindExpressions);
498 private static void decodeRef(final PsiJavaCodeReferenceElement expression,
499 final Map<PsiClass, PsiElement> oldToNewMap,
500 Map<PsiJavaCodeReferenceElement, PsiElement> rebindExpressions) {
501 final PsiElement resolved = expression.resolve();
502 if (resolved instanceof PsiClass) {
503 final PsiClass psiClass = (PsiClass)resolved;
504 if (oldToNewMap.containsKey(psiClass)) {
505 rebindExpressions.put(expression, oldToNewMap.get(psiClass));
511 private static PsiClass[] getTopLevelClasses(PsiElement element) {
513 if (element == null || element instanceof PsiFile) break;
514 if (element instanceof PsiClass && element.getParent() != null && ((PsiClass)element).getContainingClass() == null && !(element instanceof PsiAnonymousClass)) break;
515 element = element.getParent();
517 //if (element instanceof PsiCompiledElement) return null;
518 if (element instanceof PsiClassOwner) {
519 PsiClass[] classes = ((PsiClassOwner)element).getClasses();
520 ArrayList<PsiClass> buffer = new ArrayList<>();
521 for (final PsiClass aClass : classes) {
522 if (isSynthetic(aClass)) {
527 return buffer.toArray(new PsiClass[buffer.size()]);
529 return element instanceof PsiClass ? new PsiClass[]{(PsiClass)element} : null;