rename: simplify error processing
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / rename / RenameProcessor.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.rename;
18
19 import com.intellij.lang.findUsages.DescriptiveNameUtil;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.application.ModalityState;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.extensions.Extensions;
24 import com.intellij.openapi.progress.ProgressManager;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.ui.MessageType;
27 import com.intellij.openapi.ui.Messages;
28 import com.intellij.openapi.util.Computable;
29 import com.intellij.openapi.util.Ref;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.openapi.wm.IdeFrame;
32 import com.intellij.openapi.wm.WindowManager;
33 import com.intellij.openapi.wm.ex.StatusBarEx;
34 import com.intellij.psi.*;
35 import com.intellij.psi.impl.light.LightElement;
36 import com.intellij.refactoring.BaseRefactoringProcessor;
37 import com.intellij.refactoring.RefactoringBundle;
38 import com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler;
39 import com.intellij.refactoring.listeners.RefactoringElementListener;
40 import com.intellij.refactoring.listeners.RefactoringEventData;
41 import com.intellij.refactoring.listeners.RefactoringEventListener;
42 import com.intellij.refactoring.rename.naming.AutomaticRenamer;
43 import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory;
44 import com.intellij.refactoring.ui.ConflictsDialog;
45 import com.intellij.refactoring.util.CommonRefactoringUtil;
46 import com.intellij.refactoring.util.MoveRenameUsageInfo;
47 import com.intellij.refactoring.util.NonCodeUsageInfo;
48 import com.intellij.refactoring.util.RelatedUsageInfo;
49 import com.intellij.usageView.UsageInfo;
50 import com.intellij.usageView.UsageViewDescriptor;
51 import com.intellij.usageView.UsageViewUtil;
52 import com.intellij.util.IncorrectOperationException;
53 import com.intellij.util.containers.ContainerUtil;
54 import com.intellij.util.containers.MultiMap;
55 import org.jetbrains.annotations.NonNls;
56 import org.jetbrains.annotations.NotNull;
57 import org.jetbrains.annotations.Nullable;
58
59 import javax.swing.event.HyperlinkEvent;
60 import javax.swing.event.HyperlinkListener;
61 import java.util.*;
62
63 public class RenameProcessor extends BaseRefactoringProcessor {
64   private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.rename.RenameProcessor");
65
66   protected final LinkedHashMap<PsiElement, String> myAllRenames = new LinkedHashMap<>();
67
68   private @NotNull PsiElement myPrimaryElement;
69   private String myNewName = null;
70
71   private boolean mySearchInComments;
72   private boolean mySearchTextOccurrences;
73   protected boolean myForceShowPreview;
74
75   private String myCommandName;
76
77   private NonCodeUsageInfo[] myNonCodeUsages = new NonCodeUsageInfo[0];
78   private final List<AutomaticRenamerFactory> myRenamerFactories = new ArrayList<>();
79   private final List<AutomaticRenamer> myRenamers = new ArrayList<>();
80   private final List<UnresolvableCollisionUsageInfo> mySkippedUsages = new ArrayList<>();
81
82   public RenameProcessor(Project project,
83                          @NotNull PsiElement element,
84                          @NotNull @NonNls String newName,
85                          boolean isSearchInComments,
86                          boolean isSearchTextOccurrences) {
87     super(project);
88     myPrimaryElement = element;
89
90     assertNonCompileElement(element);
91     //assertValidName(element, newName);
92
93     mySearchInComments = isSearchInComments;
94     mySearchTextOccurrences = isSearchTextOccurrences;
95
96     setNewName(newName);
97   }
98
99   public Set<PsiElement> getElements() {
100     return Collections.unmodifiableSet(myAllRenames.keySet());
101   }
102
103   public String getNewName(PsiElement element) {
104     return myAllRenames.get(element);
105   }
106
107   public void addRenamerFactory(AutomaticRenamerFactory factory) {
108     if (!myRenamerFactories.contains(factory)) {
109       myRenamerFactories.add(factory);
110     }
111   }
112
113   public void removeRenamerFactory(AutomaticRenamerFactory factory) {
114     myRenamerFactories.remove(factory);
115   }
116
117   @Override
118   public void doRun() {
119     if (!myPrimaryElement.isValid()) return;
120     prepareRenaming(myPrimaryElement, myNewName, myAllRenames);
121
122     super.doRun();
123   }
124
125   public void prepareRenaming(@NotNull final PsiElement element, final String newName, final LinkedHashMap<PsiElement, String> allRenames) {
126     final List<RenamePsiElementProcessor> processors = RenamePsiElementProcessor.allForElement(element);
127     myForceShowPreview = false;
128     for (RenamePsiElementProcessor processor : processors) {
129       if (processor.canProcessElement(element)) {
130         processor.prepareRenaming(element, newName, allRenames);
131         myForceShowPreview |= processor.forcesShowPreview();
132       }
133     }
134   }
135
136   @Nullable
137   private String getHelpID() {
138     return RenamePsiElementProcessor.forElement(myPrimaryElement).getHelpID(myPrimaryElement);
139   }
140
141   @Override
142   public boolean preprocessUsages(@NotNull final Ref<UsageInfo[]> refUsages) {
143     UsageInfo[] usagesIn = refUsages.get();
144     MultiMap<PsiElement, String> conflicts = new MultiMap<>();
145
146     RenameUtil.addConflictDescriptions(usagesIn, conflicts);
147     RenamePsiElementProcessor.forElement(myPrimaryElement).findExistingNameConflicts(myPrimaryElement, myNewName, conflicts, myAllRenames);
148     if (!conflicts.isEmpty()) {
149
150       final RefactoringEventData conflictData = new RefactoringEventData();
151       conflictData.putUserData(RefactoringEventData.CONFLICTS_KEY, conflicts.values());
152       myProject.getMessageBus().syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC)
153         .conflictsDetected("refactoring.rename", conflictData);
154
155       if (ApplicationManager.getApplication().isUnitTestMode()) {
156         throw new ConflictsInTestsException(conflicts.values());
157       }
158       ConflictsDialog conflictsDialog = prepareConflictsDialog(conflicts, refUsages.get());
159       if (!conflictsDialog.showAndGet()) {
160         if (conflictsDialog.isShowConflicts()) prepareSuccessful();
161         return false;
162       }
163     }
164
165     final List<UsageInfo> variableUsages = new ArrayList<>();
166     if (!myRenamers.isEmpty()) {
167       if (!findRenamedVariables(variableUsages)) return false;
168       final LinkedHashMap<PsiElement, String> renames = new LinkedHashMap<>();
169       for (final AutomaticRenamer renamer : myRenamers) {
170         final List<? extends PsiNamedElement> variables = renamer.getElements();
171         for (final PsiNamedElement variable : variables) {
172           final String newName = renamer.getNewName(variable);
173           if (newName != null) {
174             addElement(variable, newName);
175             prepareRenaming(variable, newName, renames);
176           }
177         }
178       }
179       if (!renames.isEmpty()) {
180         for (PsiElement element : renames.keySet()) {
181           assertNonCompileElement(element);
182         }
183         myAllRenames.putAll(renames);
184         final Runnable runnable = () -> {
185           for (final Map.Entry<PsiElement, String> entry : renames.entrySet()) {
186             final UsageInfo[] usages =
187               ApplicationManager.getApplication().runReadAction(new Computable<UsageInfo[]>() {
188                 @Override
189                 public UsageInfo[] compute() {
190                   return RenameUtil.findUsages(entry.getKey(), entry.getValue(), mySearchInComments, mySearchTextOccurrences, myAllRenames);
191                 }
192               });
193             Collections.addAll(variableUsages, usages);
194           }
195         };
196         if (!ProgressManager.getInstance()
197           .runProcessWithProgressSynchronously(runnable, RefactoringBundle.message("searching.for.variables"), true, myProject)) {
198           return false;
199         }
200       }
201     }
202
203     final int[] choice = myAllRenames.size() > 1 ? new int[]{-1} : null;
204     try {
205       for (Iterator<Map.Entry<PsiElement, String>> iterator = myAllRenames.entrySet().iterator(); iterator.hasNext(); ) {
206         Map.Entry<PsiElement, String> entry = iterator.next();
207         if (entry.getKey() instanceof PsiFile) {
208           final PsiFile file = (PsiFile)entry.getKey();
209           final PsiDirectory containingDirectory = file.getContainingDirectory();
210           if (CopyFilesOrDirectoriesHandler.checkFileExist(containingDirectory, choice, file, entry.getValue(), "Rename")) {
211             iterator.remove();
212             continue;
213           }
214         }
215         RenameUtil.checkRename(entry.getKey(), entry.getValue());
216       }
217     }
218     catch (IncorrectOperationException e) {
219       CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("rename.title"), e.getMessage(), getHelpID(), myProject);
220       return false;
221     }
222
223     final Set<UsageInfo> usagesSet = ContainerUtil.newLinkedHashSet(usagesIn);
224     usagesSet.addAll(variableUsages);
225     final List<UnresolvableCollisionUsageInfo> conflictUsages = RenameUtil.removeConflictUsages(usagesSet);
226     if (conflictUsages != null) {
227       mySkippedUsages.addAll(conflictUsages);
228     }
229     refUsages.set(usagesSet.toArray(new UsageInfo[usagesSet.size()]));
230
231     prepareSuccessful();
232     return PsiElementRenameHandler.canRename(myProject, null, myPrimaryElement);
233   }
234
235   public static void assertNonCompileElement(PsiElement element) {
236     LOG.assertTrue(!(element instanceof PsiCompiledElement), element);
237   }
238   
239   private void assertValidName(PsiElement element, String newName) {
240     LOG.assertTrue(RenameUtil.isValidName(myProject, element, newName), "element: " + element + ", newName: " + newName);
241   }
242
243   private boolean findRenamedVariables(final List<UsageInfo> variableUsages) {
244     for (Iterator<AutomaticRenamer> iterator = myRenamers.iterator(); iterator.hasNext(); ) {
245       AutomaticRenamer automaticVariableRenamer = iterator.next();
246       if (!automaticVariableRenamer.hasAnythingToRename()) continue;
247       if (!showAutomaticRenamingDialog(automaticVariableRenamer)) {
248         iterator.remove();
249       }
250     }
251
252     final Runnable runnable = () -> ApplicationManager.getApplication().runReadAction(() -> {
253       for (final AutomaticRenamer renamer : myRenamers) {
254         renamer.findUsages(variableUsages, mySearchInComments, mySearchTextOccurrences, mySkippedUsages, myAllRenames);
255       }
256     });
257
258     return ProgressManager.getInstance()
259       .runProcessWithProgressSynchronously(runnable, RefactoringBundle.message("searching.for.variables"), true, myProject);
260   }
261
262   protected boolean showAutomaticRenamingDialog(AutomaticRenamer automaticVariableRenamer) {
263     if (ApplicationManager.getApplication().isUnitTestMode()) {
264       for (PsiNamedElement element : automaticVariableRenamer.getElements()) {
265         automaticVariableRenamer.setRename(element, automaticVariableRenamer.getNewName(element));
266       }
267       return true;
268     }
269     final AutomaticRenamingDialog dialog = new AutomaticRenamingDialog(myProject, automaticVariableRenamer);
270     return dialog.showAndGet();
271   }
272
273   public void addElement(@NotNull PsiElement element, @NotNull String newName) {
274     assertNonCompileElement(element);
275     myAllRenames.put(element, newName);
276   }
277
278   private void setNewName(@NotNull String newName) {
279     myNewName = newName;
280     myAllRenames.put(myPrimaryElement, newName);
281     myCommandName = RefactoringBundle
282       .message("renaming.0.1.to.2", UsageViewUtil.getType(myPrimaryElement), DescriptiveNameUtil.getDescriptiveName(myPrimaryElement), newName);
283   }
284
285   @Override
286   @NotNull
287   protected UsageViewDescriptor createUsageViewDescriptor(@NotNull UsageInfo[] usages) {
288     return new RenameViewDescriptor(myAllRenames);
289   }
290
291   @Override
292   @NotNull
293   public UsageInfo[] findUsages() {
294     myRenamers.clear();
295     ArrayList<UsageInfo> result = new ArrayList<>();
296
297     List<PsiElement> elements = new ArrayList<>(myAllRenames.keySet());
298     //noinspection ForLoopReplaceableByForEach
299     for (int i = 0; i < elements.size(); i++) {
300       PsiElement element = elements.get(i);
301       if (element == null) {
302         LOG.error("primary: " + myPrimaryElement + "; renamers: " + myRenamers);
303         continue;
304       }
305       final String newName = myAllRenames.get(element);
306       final UsageInfo[] usages = RenameUtil.findUsages(element, newName, mySearchInComments, mySearchTextOccurrences, myAllRenames);
307       final List<UsageInfo> usagesList = Arrays.asList(usages);
308       result.addAll(usagesList);
309
310       for (AutomaticRenamerFactory factory : myRenamerFactories) {
311         if (factory.isApplicable(element)) {
312           myRenamers.add(factory.createRenamer(element, newName, usagesList));
313         }
314       }
315
316       for (AutomaticRenamerFactory factory : Extensions.getExtensions(AutomaticRenamerFactory.EP_NAME)) {
317         if (factory.getOptionName() == null && factory.isApplicable(element)) {
318           myRenamers.add(factory.createRenamer(element, newName, usagesList));
319         }
320       }
321     }
322     UsageInfo[] usageInfos = result.toArray(new UsageInfo[result.size()]);
323     usageInfos = UsageViewUtil.removeDuplicatedUsages(usageInfos);
324     return usageInfos;
325   }
326
327   @Override
328   protected void refreshElements(@NotNull PsiElement[] elements) {
329     LOG.assertTrue(elements.length > 0);
330     myPrimaryElement = elements[0];
331
332     final Iterator<String> newNames = myAllRenames.values().iterator();
333     LinkedHashMap<PsiElement, String> newAllRenames = new LinkedHashMap<>();
334     for (PsiElement resolved : elements) {
335       newAllRenames.put(resolved, newNames.next());
336     }
337     myAllRenames.clear();
338     myAllRenames.putAll(newAllRenames);
339   }
340
341   @Override
342   protected boolean isPreviewUsages(@NotNull UsageInfo[] usages) {
343     if (myForceShowPreview) return true;
344     if (super.isPreviewUsages(usages)) return true;
345     if (UsageViewUtil.reportNonRegularUsages(usages, myProject)) return true;
346     return false;
347   }
348
349   @Nullable
350   @Override
351   protected String getRefactoringId() {
352     return "refactoring.rename";
353   }
354
355   @Nullable
356   @Override
357   protected RefactoringEventData getBeforeData() {
358     final RefactoringEventData data = new RefactoringEventData();
359     data.addElement(myPrimaryElement);
360     return data;
361   }
362
363   @Nullable
364   @Override
365   protected RefactoringEventData getAfterData(@NotNull UsageInfo[] usages) {
366     final RefactoringEventData data = new RefactoringEventData();
367     data.addElement(myPrimaryElement);
368     return data;
369   }
370
371   @Override
372   public void performRefactoring(@NotNull UsageInfo[] usages) {
373     List<Runnable> postRenameCallbacks = new ArrayList<>();
374
375     final MultiMap<PsiElement, UsageInfo> classified = classifyUsages(myAllRenames.keySet(), usages);
376     for (final PsiElement element : myAllRenames.keySet()) {
377       String newName = myAllRenames.get(element);
378
379       final RefactoringElementListener elementListener = getTransaction().getElementListener(element);
380       final RenamePsiElementProcessor renamePsiElementProcessor = RenamePsiElementProcessor.forElement(element);
381       Runnable postRenameCallback = renamePsiElementProcessor.getPostRenameCallback(element, newName, elementListener);
382       final Collection<UsageInfo> infos = classified.get(element);
383       try {
384         RenameUtil.doRename(element, newName, infos.toArray(new UsageInfo[infos.size()]), myProject, elementListener);
385       }
386       catch (final IncorrectOperationException e) {
387         RenameUtil.showErrorMessage(e, element, myProject);
388         return;
389       }
390       if (postRenameCallback != null) {
391         postRenameCallbacks.add(postRenameCallback);
392       }
393     }
394
395     for (Runnable runnable : postRenameCallbacks) {
396       runnable.run();
397     }
398
399     List<NonCodeUsageInfo> nonCodeUsages = new ArrayList<>();
400     for (UsageInfo usage : usages) {
401       if (usage instanceof NonCodeUsageInfo) {
402         nonCodeUsages.add((NonCodeUsageInfo)usage);
403       }
404     }
405     myNonCodeUsages = nonCodeUsages.toArray(new NonCodeUsageInfo[nonCodeUsages.size()]);
406     if (!mySkippedUsages.isEmpty()) {
407       if (!ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) {
408         ApplicationManager.getApplication().invokeLater(() -> {
409           final IdeFrame ideFrame = WindowManager.getInstance().getIdeFrame(myProject);
410           if (ideFrame != null) {
411
412             StatusBarEx statusBar = (StatusBarEx)ideFrame.getStatusBar();
413             HyperlinkListener listener = new HyperlinkListener() {
414               @Override
415               public void hyperlinkUpdate(HyperlinkEvent e) {
416                 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) return;
417                 Messages.showMessageDialog("<html>Following usages were safely skipped:<br>" +
418                                            StringUtil.join(mySkippedUsages, unresolvableCollisionUsageInfo -> unresolvableCollisionUsageInfo.getDescription(), "<br>") +
419                                            "</html>", "Not All Usages Were Renamed", null);
420               }
421             };
422             statusBar.notifyProgressByBalloon(MessageType.WARNING, "<html><body>Unable to rename certain usages. <a href=\"\">Browse</a></body></html>", null, listener);
423           }
424         }, ModalityState.NON_MODAL);
425       }
426     }
427   }
428
429   @Override
430   protected void performPsiSpoilingRefactoring() {
431     RenameUtil.renameNonCodeUsages(myProject, myNonCodeUsages);
432   }
433
434   @Override
435   protected String getCommandName() {
436     return myCommandName;
437   }
438
439   public static MultiMap<PsiElement, UsageInfo> classifyUsages(Collection<? extends PsiElement> elements, UsageInfo[] usages) {
440     final MultiMap<PsiElement, UsageInfo> result = new MultiMap<>();
441     for (UsageInfo usage : usages) {
442       LOG.assertTrue(usage instanceof MoveRenameUsageInfo);
443       if (usage.getReference() instanceof LightElement) {
444         continue; //filter out implicit references (e.g. from derived class to super class' default constructor)
445       }
446       MoveRenameUsageInfo usageInfo = (MoveRenameUsageInfo)usage;
447       if (usage instanceof RelatedUsageInfo) {
448         final PsiElement relatedElement = ((RelatedUsageInfo)usage).getRelatedElement();
449         if (elements.contains(relatedElement)) {
450           result.putValue(relatedElement, usage);
451         }
452       } else {
453         PsiElement referenced = usageInfo.getReferencedElement();
454         if (elements.contains(referenced)) {
455           result.putValue(referenced, usage);
456         } else if (referenced != null) {
457           PsiElement indirect = referenced.getNavigationElement();
458           if (elements.contains(indirect)) {
459             result.putValue(indirect, usage);
460           }
461         }
462
463       }
464     }
465     return result;
466   }
467
468   public Collection<String> getNewNames() {
469     return myAllRenames.values();
470   }
471
472   public void setSearchInComments(boolean value) {
473     mySearchInComments = value;
474   }
475
476   public void setSearchTextOccurrences(boolean searchTextOccurrences) {
477     mySearchTextOccurrences = searchTextOccurrences;
478   }
479
480   public boolean isSearchInComments() {
481     return mySearchInComments;
482   }
483
484   public boolean isSearchTextOccurrences() {
485     return mySearchTextOccurrences;
486   }
487
488   public void setCommandName(final String commandName) {
489     myCommandName = commandName;
490   }
491 }