logging for EA-30339 - NPE: RenameUtil.renameNonCodeUsages
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / rename / RenameUtil.java
1 /*
2  * Copyright 2000-2009 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.codeInsight.CodeInsightUtilBase;
20 import com.intellij.ide.actions.CopyReferenceAction;
21 import com.intellij.lang.Language;
22 import com.intellij.lang.LanguageNamesValidation;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.command.undo.BasicUndoableAction;
25 import com.intellij.openapi.command.undo.UndoManager;
26 import com.intellij.openapi.command.undo.UndoableAction;
27 import com.intellij.openapi.command.undo.UnexpectedUndoException;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.editor.Document;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.util.*;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.pom.PomTargetPsiElement;
35 import com.intellij.psi.*;
36 import com.intellij.psi.meta.PsiMetaData;
37 import com.intellij.psi.meta.PsiMetaOwner;
38 import com.intellij.psi.meta.PsiWritableMetaData;
39 import com.intellij.psi.search.GlobalSearchScope;
40 import com.intellij.refactoring.RefactoringBundle;
41 import com.intellij.refactoring.listeners.RefactoringElementListener;
42 import com.intellij.refactoring.listeners.UndoRefactoringElementListener;
43 import com.intellij.refactoring.util.*;
44 import com.intellij.usageView.UsageInfo;
45 import com.intellij.util.IncorrectOperationException;
46 import com.intellij.util.containers.HashMap;
47 import com.intellij.util.containers.MultiMap;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50
51 import java.util.*;
52
53 public class RenameUtil {
54   private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.rename.RenameUtil");
55
56   private RenameUtil() {
57   }
58
59   @NotNull
60   public static UsageInfo[] findUsages(final PsiElement element,
61                                        final String newName,
62                                        boolean searchInStringsAndComments,
63                                        boolean searchForTextOccurrences,
64                                        Map<? extends PsiElement, String> allRenames) {
65     final List<UsageInfo> result = Collections.synchronizedList(new ArrayList<UsageInfo>());
66
67     PsiManager manager = element.getManager();
68     GlobalSearchScope projectScope = GlobalSearchScope.projectScope(manager.getProject());
69     RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(element);
70
71     Collection<PsiReference> refs = processor.findReferences(element);
72     for (final PsiReference ref : refs) {
73       if (ref == null) {
74         LOG.error("null reference from processor " + processor);
75         continue;
76       }
77       PsiElement referenceElement = ref.getElement();
78       result.add(new MoveRenameUsageInfo(referenceElement, ref, ref.getRangeInElement().getStartOffset(),
79                                          ref.getRangeInElement().getEndOffset(), element,
80                                          ref.resolve() == null));
81     }
82
83     processor.findCollisions(element, newName, allRenames, result);
84
85     final PsiElement searchForInComments = processor.getElementToSearchInStringsAndComments(element);
86
87     if (searchInStringsAndComments && searchForInComments != null) {
88       String stringToSearch = ElementDescriptionUtil.getElementDescription(searchForInComments, NonCodeSearchDescriptionLocation.STRINGS_AND_COMMENTS);
89       if (stringToSearch.length() > 0) {
90         final String stringToReplace = getStringToReplace(element, newName, false, processor);
91         TextOccurrencesUtil.UsageInfoFactory factory = new NonCodeUsageInfoFactory(searchForInComments, stringToReplace);
92         TextOccurrencesUtil.addUsagesInStringsAndComments(searchForInComments, stringToSearch, result, factory);
93       }
94     }
95
96     if (searchForTextOccurrences && searchForInComments != null) {
97       String stringToSearch = ElementDescriptionUtil.getElementDescription(searchForInComments, NonCodeSearchDescriptionLocation.NON_JAVA);
98       if (stringToSearch.length() > 0) {
99         final String stringToReplace = getStringToReplace(element, newName, true, processor);
100         addTextOccurrence(searchForInComments, result, projectScope, stringToSearch, stringToReplace);
101       }
102
103       final Pair<String, String> additionalStringToSearch = processor.getTextOccurrenceSearchStrings(searchForInComments, newName);
104       if (additionalStringToSearch != null && additionalStringToSearch.first.length() > 0) {
105         addTextOccurrence(searchForInComments, result, projectScope, additionalStringToSearch.first, additionalStringToSearch.second);
106       }
107     }
108
109     return result.toArray(new UsageInfo[result.size()]);
110   }
111
112   private static void addTextOccurrence(final PsiElement element, final List<UsageInfo> result, final GlobalSearchScope projectScope,
113                                         final String stringToSearch, final String stringToReplace) {
114     TextOccurrencesUtil.UsageInfoFactory factory = new TextOccurrencesUtil.UsageInfoFactory() {
115       public UsageInfo createUsageInfo(@NotNull PsiElement usage, int startOffset, int endOffset) {
116         TextRange textRange = usage.getTextRange();
117         int start = textRange == null ? 0 : textRange.getStartOffset();
118         return NonCodeUsageInfo.create(usage.getContainingFile(), start + startOffset, start + endOffset, element, stringToReplace);
119       }
120     };
121     TextOccurrencesUtil.addTextOccurences(element, stringToSearch, projectScope, result, factory);
122   }
123
124
125   public static void buildPackagePrefixChangedMessage(final VirtualFile[] virtualFiles, StringBuffer message, final String qualifiedName) {
126     if (virtualFiles.length > 0) {
127       message.append(RefactoringBundle.message("package.occurs.in.package.prefixes.of.the.following.source.folders.n", qualifiedName));
128       for (final VirtualFile virtualFile : virtualFiles) {
129         message.append(virtualFile.getPresentableUrl()).append("\n");
130       }
131       message.append(RefactoringBundle.message("these.package.prefixes.will.be.changed"));
132     }
133   }
134
135   private static String getStringToReplace(PsiElement element, String newName, boolean nonJava, final RenamePsiElementProcessor theProcessor) {
136     if (element instanceof PsiMetaOwner) {
137       final PsiMetaOwner psiMetaOwner = (PsiMetaOwner)element;
138       final PsiMetaData metaData = psiMetaOwner.getMetaData();
139       if (metaData != null) {
140         return metaData.getName();
141       }
142     }
143
144     if (theProcessor != null) {
145       String result = theProcessor.getQualifiedNameAfterRename(element, newName, nonJava);
146       if (result != null) {
147         return result;
148       }
149     }
150
151     if (element instanceof PsiNamedElement) {
152       return newName;
153     }
154     else {
155       LOG.error("Unknown element type");
156       return null;
157     }
158   }
159
160   public static void checkRename(PsiElement element, String newName) throws IncorrectOperationException {
161     if (element instanceof PsiCheckedRenameElement) {
162       ((PsiCheckedRenameElement)element).checkSetName(newName);
163     }
164   }
165
166   public static void doRename(final PsiElement element, String newName, UsageInfo[] usages, final Project project,
167                               final RefactoringElementListener listener) throws IncorrectOperationException{
168     final RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(element);
169     final String fqn = element instanceof PsiFile ? ((PsiFile)element).getVirtualFile().getPath() : CopyReferenceAction.elementToFqn(element);
170     if (fqn != null) {
171       UndoableAction action = new BasicUndoableAction() {
172         public void undo() throws UnexpectedUndoException {
173           if (listener instanceof UndoRefactoringElementListener) {
174             ((UndoRefactoringElementListener)listener).undoElementMovedOrRenamed(element, fqn);
175           }
176         }
177
178         @Override
179         public void redo() throws UnexpectedUndoException {
180         }
181       };
182       UndoManager.getInstance(project).undoableActionPerformed(action);
183     }
184     processor.renameElement(element, newName, usages, listener);
185   }
186
187   public static void showErrorMessage(final IncorrectOperationException e, final PsiElement element, final Project project) {
188     // may happen if the file or package cannot be renamed. e.g. locked by another application
189     if (ApplicationManager.getApplication().isUnitTestMode()) {
190       throw new RuntimeException(e);
191       //LOG.error(e);
192       //return;
193     }
194     ApplicationManager.getApplication().invokeLater(new Runnable() {
195       public void run() {
196         final String helpID = RenamePsiElementProcessor.forElement(element).getHelpID(element);
197         String message = e.getMessage();
198         if (StringUtil.isEmpty(message)) {
199           message = RefactoringBundle.message("rename.not.supported");
200         }
201         CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("rename.title"), message, helpID, project);
202       }
203     });
204   }
205
206   public static void doRenameGenericNamedElement(PsiElement namedElement, String newName, UsageInfo[] usages,
207                                                  @Nullable RefactoringElementListener listener) throws IncorrectOperationException {
208     PsiWritableMetaData writableMetaData = null;
209     if (namedElement instanceof PsiMetaOwner) {
210       final PsiMetaData metaData = ((PsiMetaOwner)namedElement).getMetaData();
211       if (metaData instanceof PsiWritableMetaData) {
212         writableMetaData = (PsiWritableMetaData)metaData;
213       }
214     }
215     if (writableMetaData == null && !(namedElement instanceof PsiNamedElement)) {
216       LOG.error("Unknown element type");
217     }
218
219     boolean hasBindables = false;
220     for (UsageInfo usage : usages) {
221       if (!(usage.getReference() instanceof BindablePsiReference)) {
222         rename(usage, newName);
223       } else {
224         hasBindables = true;
225       }
226     }
227
228     if (writableMetaData != null) {
229       writableMetaData.setName(newName);
230     }
231     else {
232       PsiElement namedElementAfterRename = ((PsiNamedElement)namedElement).setName(newName);
233       if (namedElementAfterRename != null) namedElement = namedElementAfterRename;
234     }
235
236     if (hasBindables) {
237       for (UsageInfo usage : usages) {
238         final PsiReference ref = usage.getReference();
239         if (ref instanceof BindablePsiReference) {
240           try {
241             ref.bindToElement(namedElement);
242           }
243           catch (IncorrectOperationException e) {//fall back to old scheme
244             ref.handleElementRename(newName);
245           }
246         }
247       }
248     }
249     if (listener != null) {
250       listener.elementRenamed(namedElement);
251     }
252   }
253
254   public static void rename(UsageInfo info, String newName) throws IncorrectOperationException {
255     if (info.getElement() == null) return;
256     PsiReference ref = info.getReference();
257     if (ref == null) return;
258     ref.handleElementRename(newName);
259   }
260
261   @Nullable
262   public static List<UnresolvableCollisionUsageInfo> removeConflictUsages(Set<UsageInfo> usages) {
263     final List<UnresolvableCollisionUsageInfo> result = new ArrayList<UnresolvableCollisionUsageInfo>();
264     for (Iterator<UsageInfo> iterator = usages.iterator(); iterator.hasNext();) {
265       UsageInfo usageInfo = iterator.next();
266       if (usageInfo instanceof UnresolvableCollisionUsageInfo) {
267         result.add((UnresolvableCollisionUsageInfo)usageInfo);
268         iterator.remove();
269       }
270     }
271     return result.isEmpty() ? null : result;
272   }
273
274   public static void addConflictDescriptions(UsageInfo[] usages, MultiMap<PsiElement, String> conflicts) {
275     for (UsageInfo usage : usages) {
276       if (usage instanceof UnresolvableCollisionUsageInfo) {
277         conflicts.putValue(usage.getElement(), ((UnresolvableCollisionUsageInfo)usage).getDescription());
278       }
279     }
280   }
281
282   public static void renameNonCodeUsages(@NotNull Project project, @NotNull NonCodeUsageInfo[] usages) {
283     PsiDocumentManager.getInstance(project).commitAllDocuments();
284     Map<Document, List<UsageOffset>> docsToOffsetsMap = new HashMap<Document, List<UsageOffset>>();
285     final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project);
286     for (NonCodeUsageInfo usage : usages) {
287       PsiElement element = usage.getElement();
288
289       if (element == null) continue;
290       element = CodeInsightUtilBase.forcePsiPostprocessAndRestoreElement(element);
291       if (element == null) continue;
292
293       final ProperTextRange rangeInElement = usage.getRangeInElement();
294       if (rangeInElement == null) continue;
295
296       final PsiFile containingFile = element.getContainingFile();
297       final Document document = psiDocumentManager.getDocument(containingFile);
298
299       final Segment segment = usage.getSegment();
300       LOG.assertTrue(segment != null);
301       int fileOffset = segment.getStartOffset();
302
303       List<UsageOffset> list = docsToOffsetsMap.get(document);
304       if (list == null) {
305         list = new ArrayList<UsageOffset>();
306         docsToOffsetsMap.put(document, list);
307       }
308       
309       list.add(new UsageOffset(fileOffset, fileOffset + rangeInElement.getLength(), usage.newText));
310     }
311
312     for (Document document : docsToOffsetsMap.keySet()) {
313       List<UsageOffset> list = docsToOffsetsMap.get(document);
314       LOG.assertTrue(list != null, document);
315       UsageOffset[] offsets = list.toArray(new UsageOffset[list.size()]);
316       Arrays.sort(offsets);
317
318       for (int i = offsets.length - 1; i >= 0; i--) {
319         UsageOffset usageOffset = offsets[i];
320         document.replaceString(usageOffset.startOffset, usageOffset.endOffset, usageOffset.newText);
321       }
322       PsiDocumentManager.getInstance(project).commitDocument(document);
323     }
324     PsiDocumentManager.getInstance(project).commitAllDocuments();
325   }
326
327   public static boolean isValidName(final Project project, final PsiElement psiElement, final String newName) {
328     if (newName == null || newName.length() == 0) {
329       return false;
330     }
331     final Condition<String> inputValidator = RenameInputValidatorRegistry.getInputValidator(psiElement);
332     if (inputValidator != null) {
333       return inputValidator.value(newName);
334     }
335     if (psiElement instanceof PsiFile || psiElement instanceof PsiDirectory) {
336       return newName.indexOf('\\') < 0 && newName.indexOf('/') < 0;
337     }
338     if (psiElement instanceof PomTargetPsiElement) {
339       return !StringUtil.isEmptyOrSpaces(newName);
340     }
341
342     final PsiFile file = psiElement.getContainingFile();
343     final Language elementLanguage = psiElement.getLanguage();
344
345     final Language fileLanguage = file == null ? null : file.getLanguage();
346     Language language = fileLanguage == null ? elementLanguage : fileLanguage.isKindOf(elementLanguage) ? fileLanguage : elementLanguage;
347
348     return LanguageNamesValidation.INSTANCE.forLanguage(language).isIdentifier(newName.trim(), project);
349   }
350
351   private static class UsageOffset implements Comparable<UsageOffset> {
352     final int startOffset;
353     final int endOffset;
354     final String newText;
355
356     public UsageOffset(int startOffset, int endOffset, String newText) {
357       this.startOffset = startOffset;
358       this.endOffset = endOffset;
359       this.newText = newText;
360     }
361
362     public int compareTo(final UsageOffset o) {
363       return startOffset - o.startOffset;
364     }
365   }
366 }