| /* |
| * Copyright 2000-2013 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.refactoring.rename.inplace; |
| |
| import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; |
| import com.intellij.codeInsight.highlighting.HighlightManager; |
| import com.intellij.codeInsight.lookup.LookupManager; |
| import com.intellij.codeInsight.lookup.impl.LookupImpl; |
| import com.intellij.codeInsight.template.*; |
| import com.intellij.codeInsight.template.impl.TemplateManagerImpl; |
| import com.intellij.codeInsight.template.impl.TemplateState; |
| import com.intellij.injected.editor.VirtualFileWindow; |
| import com.intellij.lang.Language; |
| import com.intellij.lang.LanguageNamesValidation; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.lang.refactoring.NamesValidator; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.Shortcut; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.Result; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.command.WriteCommandAction; |
| import com.intellij.openapi.command.impl.FinishMarkAction; |
| import com.intellij.openapi.command.impl.StartMarkAction; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.RangeMarker; |
| import com.intellij.openapi.editor.ScrollType; |
| import com.intellij.openapi.editor.colors.EditorColors; |
| import com.intellij.openapi.editor.colors.EditorColorsManager; |
| import com.intellij.openapi.editor.impl.EditorImpl; |
| import com.intellij.openapi.editor.markup.RangeHighlighter; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.fileEditor.FileEditor; |
| import com.intellij.openapi.fileEditor.FileEditorManager; |
| import com.intellij.openapi.fileEditor.OpenFileDescriptor; |
| import com.intellij.openapi.fileEditor.TextEditor; |
| import com.intellij.openapi.keymap.Keymap; |
| import com.intellij.openapi.keymap.KeymapManager; |
| import com.intellij.openapi.keymap.KeymapUtil; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.ProjectRootManager; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.ui.popup.Balloon; |
| import com.intellij.openapi.ui.popup.BalloonBuilder; |
| import com.intellij.openapi.ui.popup.JBPopupFactory; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; |
| import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; |
| import com.intellij.psi.search.LocalSearchScope; |
| import com.intellij.psi.search.ProjectScope; |
| import com.intellij.psi.search.PsiSearchHelper; |
| import com.intellij.psi.search.SearchScope; |
| import com.intellij.psi.search.searches.ReferencesSearch; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.refactoring.RefactoringActionHandler; |
| import com.intellij.refactoring.RefactoringBundle; |
| import com.intellij.refactoring.util.CommonRefactoringUtil; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.ui.popup.PopupFactoryImpl; |
| import com.intellij.util.CommonProcessors; |
| import com.intellij.util.Query; |
| import com.intellij.util.containers.Stack; |
| import com.intellij.util.ui.PositionTracker; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.TestOnly; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.util.*; |
| import java.util.List; |
| |
| /** |
| * User: anna |
| * Date: 1/11/12 |
| */ |
| public abstract class InplaceRefactoring { |
| protected static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.rename.inplace.VariableInplaceRenamer"); |
| @NonNls protected static final String PRIMARY_VARIABLE_NAME = "PrimaryVariable"; |
| @NonNls protected static final String OTHER_VARIABLE_NAME = "OtherVariable"; |
| protected static final Stack<InplaceRefactoring> ourRenamersStack = new Stack<InplaceRefactoring>(); |
| public static final Key<InplaceRefactoring> INPLACE_RENAMER = Key.create("EditorInplaceRenamer"); |
| public static final Key<Boolean> INTRODUCE_RESTART = Key.create("INTRODUCE_RESTART"); |
| |
| protected PsiNamedElement myElementToRename; |
| protected final Editor myEditor; |
| protected final Project myProject; |
| protected RangeMarker myRenameOffset; |
| protected String myAdvertisementText; |
| private ArrayList<RangeHighlighter> myHighlighters; |
| protected String myInitialName; |
| protected String myOldName; |
| protected RangeMarker myBeforeRevert = null; |
| protected String myInsertedName; |
| protected LinkedHashSet<String> myNameSuggestions; |
| |
| protected StartMarkAction myMarkAction; |
| protected PsiElement myScope; |
| |
| protected RangeMarker myCaretRangeMarker; |
| |
| |
| protected Balloon myBalloon; |
| protected String myTitle; |
| protected RelativePoint myTarget; |
| |
| public InplaceRefactoring(Editor editor, PsiNamedElement elementToRename, Project project) { |
| this(editor, elementToRename, project, elementToRename != null ? elementToRename.getName() : null, |
| elementToRename != null ? elementToRename.getName() : null); |
| } |
| |
| public InplaceRefactoring(Editor editor, PsiNamedElement elementToRename, Project project, final String oldName) { |
| this(editor, elementToRename, project, elementToRename != null ? elementToRename.getName() : null, oldName); |
| } |
| |
| public InplaceRefactoring( |
| Editor editor, PsiNamedElement elementToRename, Project project, String initialName, final String oldName) { |
| myEditor = /*(editor instanceof EditorWindow)? ((EditorWindow)editor).getDelegate() : */editor; |
| myElementToRename = elementToRename; |
| myProject = project; |
| myOldName = oldName; |
| if (myElementToRename != null) { |
| myInitialName = initialName; |
| final PsiFile containingFile = myElementToRename.getContainingFile(); |
| if (!notSameFile(getTopLevelVirtualFile(containingFile.getViewProvider()), containingFile) && |
| myElementToRename != null && myElementToRename.getTextRange() != null) { |
| myRenameOffset = myEditor.getDocument().createRangeMarker(myElementToRename.getTextRange()); |
| myRenameOffset.setGreedyToRight(true); |
| myRenameOffset.setGreedyToLeft(true); // todo not sure if we need this |
| } |
| } |
| } |
| |
| public static void unableToStartWarning(Project project, Editor editor) { |
| final StartMarkAction startMarkAction = StartMarkAction.canStart(project); |
| final String message = startMarkAction.getCommandName() + " is not finished yet."; |
| final Document oldDocument = startMarkAction.getDocument(); |
| if (editor == null || oldDocument != editor.getDocument()) { |
| final int exitCode = Messages.showYesNoDialog(project, message, |
| RefactoringBundle.getCannotRefactorMessage(null), |
| "Continue Started", "Cancel Started", Messages.getErrorIcon()); |
| navigateToStarted(oldDocument, project, exitCode); |
| } |
| else { |
| CommonRefactoringUtil.showErrorHint(project, editor, message, RefactoringBundle.getCannotRefactorMessage(null), null); |
| } |
| } |
| |
| public void setAdvertisementText(String advertisementText) { |
| myAdvertisementText = advertisementText; |
| } |
| |
| |
| public boolean performInplaceRefactoring(final LinkedHashSet<String> nameSuggestions) { |
| myNameSuggestions = nameSuggestions; |
| if (InjectedLanguageUtil.isInInjectedLanguagePrefixSuffix(myElementToRename)) { |
| return false; |
| } |
| |
| final FileViewProvider fileViewProvider = myElementToRename.getContainingFile().getViewProvider(); |
| VirtualFile file = getTopLevelVirtualFile(fileViewProvider); |
| |
| SearchScope referencesSearchScope = getReferencesSearchScope(file); |
| |
| final Collection<PsiReference> refs = collectRefs(referencesSearchScope); |
| |
| addReferenceAtCaret(refs); |
| |
| for (PsiReference ref : refs) { |
| final PsiFile containingFile = ref.getElement().getContainingFile(); |
| |
| if (notSameFile(file, containingFile)) { |
| return false; |
| } |
| } |
| |
| final PsiElement scope = checkLocalScope(); |
| |
| if (scope == null) { |
| return false; // Should have valid local search scope for inplace rename |
| } |
| |
| final PsiFile containingFile = scope.getContainingFile(); |
| if (containingFile == null) { |
| return false; // Should have valid local search scope for inplace rename |
| } |
| //no need to process further when file is read-only |
| if (!CommonRefactoringUtil.checkReadOnlyStatus(myProject, containingFile)) return true; |
| |
| myEditor.putUserData(INPLACE_RENAMER, this); |
| ourRenamersStack.push(this); |
| |
| final List<Pair<PsiElement, TextRange>> stringUsages = new ArrayList<Pair<PsiElement, TextRange>>(); |
| collectAdditionalElementsToRename(stringUsages); |
| return buildTemplateAndStart(refs, stringUsages, scope, containingFile); |
| } |
| |
| protected boolean notSameFile(@Nullable VirtualFile file, @NotNull PsiFile containingFile) { |
| return !Comparing.equal(getTopLevelVirtualFile(containingFile.getViewProvider()), file); |
| } |
| |
| protected SearchScope getReferencesSearchScope(VirtualFile file) { |
| return file == null || ProjectRootManager.getInstance(myProject).getFileIndex().isInContent(file) |
| ? ProjectScope.getProjectScope(myElementToRename.getProject()) |
| : new LocalSearchScope(myElementToRename.getContainingFile()); |
| } |
| |
| @Nullable |
| protected PsiElement checkLocalScope() { |
| final SearchScope searchScope = PsiSearchHelper.SERVICE.getInstance(myElementToRename.getProject()).getUseScope(myElementToRename); |
| if (searchScope instanceof LocalSearchScope) { |
| final PsiElement[] elements = ((LocalSearchScope)searchScope).getScope(); |
| return PsiTreeUtil.findCommonParent(elements); |
| } |
| |
| return null; |
| } |
| |
| protected abstract void collectAdditionalElementsToRename(final List<Pair<PsiElement, TextRange>> stringUsages); |
| |
| protected abstract boolean shouldSelectAll(); |
| |
| protected MyLookupExpression createLookupExpression(PsiElement selectedElement) { |
| return new MyLookupExpression(getInitialName(), myNameSuggestions, myElementToRename, selectedElement, shouldSelectAll(), myAdvertisementText); |
| } |
| |
| protected boolean acceptReference(PsiReference reference) { |
| return true; |
| } |
| |
| protected Collection<PsiReference> collectRefs(SearchScope referencesSearchScope) { |
| final Query<PsiReference> search = ReferencesSearch.search(myElementToRename, referencesSearchScope, false); |
| |
| final CommonProcessors.CollectProcessor<PsiReference> processor = new CommonProcessors.CollectProcessor<PsiReference>() { |
| @Override |
| protected boolean accept(PsiReference reference) { |
| return acceptReference(reference); |
| } |
| }; |
| |
| search.forEach(processor); |
| return processor.getResults(); |
| } |
| |
| protected boolean buildTemplateAndStart(final Collection<PsiReference> refs, |
| final Collection<Pair<PsiElement, TextRange>> stringUsages, |
| final PsiElement scope, |
| final PsiFile containingFile) { |
| final PsiElement context = InjectedLanguageManager.getInstance(containingFile.getProject()).getInjectionHost(containingFile); |
| myScope = context != null ? context.getContainingFile() : scope; |
| final TemplateBuilderImpl builder = new TemplateBuilderImpl(myScope); |
| |
| PsiElement nameIdentifier = getNameIdentifier(); |
| int offset = InjectedLanguageUtil.getTopLevelEditor(myEditor).getCaretModel().getOffset(); |
| PsiElement selectedElement = getSelectedInEditorElement(nameIdentifier, refs, stringUsages, offset); |
| |
| boolean subrefOnPrimaryElement = false; |
| boolean hasReferenceOnNameIdentifier = false; |
| for (PsiReference ref : refs) { |
| if (isReferenceAtCaret(selectedElement, ref)) { |
| builder.replaceElement(ref, PRIMARY_VARIABLE_NAME, createLookupExpression(selectedElement), true); |
| subrefOnPrimaryElement = true; |
| continue; |
| } |
| addVariable(ref, selectedElement, builder, offset); |
| hasReferenceOnNameIdentifier |= isReferenceAtCaret(nameIdentifier, ref); |
| } |
| if (nameIdentifier != null) { |
| hasReferenceOnNameIdentifier |= selectedElement.getTextRange().contains(nameIdentifier.getTextRange()); |
| if (!subrefOnPrimaryElement || !hasReferenceOnNameIdentifier){ |
| addVariable(nameIdentifier, selectedElement, builder); |
| } |
| } |
| for (Pair<PsiElement, TextRange> usage : stringUsages) { |
| addVariable(usage.first, usage.second, selectedElement, builder); |
| } |
| addAdditionalVariables(builder); |
| try { |
| myMarkAction = startRename(); |
| } |
| catch (final StartMarkAction.AlreadyStartedException e) { |
| final Document oldDocument = e.getDocument(); |
| if (oldDocument != myEditor.getDocument()) { |
| final int exitCode = Messages.showYesNoCancelDialog(myProject, e.getMessage(), getCommandName(), |
| "Navigate to Started", "Cancel Started", "Cancel", Messages.getErrorIcon()); |
| if (exitCode == Messages.CANCEL) return true; |
| navigateToAlreadyStarted(oldDocument, exitCode); |
| return true; |
| } |
| else { |
| |
| if (!ourRenamersStack.isEmpty() && ourRenamersStack.peek() == this) { |
| ourRenamersStack.pop(); |
| if (!ourRenamersStack.empty()) { |
| myOldName = ourRenamersStack.peek().myOldName; |
| } |
| } |
| |
| revertState(); |
| final TemplateState templateState = TemplateManagerImpl.getTemplateState(InjectedLanguageUtil.getTopLevelEditor(myEditor)); |
| if (templateState != null) { |
| templateState.gotoEnd(true); |
| } |
| } |
| return false; |
| } |
| |
| beforeTemplateStart(); |
| |
| new WriteCommandAction(myProject, getCommandName()) { |
| @Override |
| protected void run(Result result) throws Throwable { |
| startTemplate(builder); |
| } |
| }.execute(); |
| |
| if (myBalloon == null) { |
| showBalloon(); |
| } |
| return true; |
| } |
| |
| protected boolean isReferenceAtCaret(PsiElement selectedElement, PsiReference ref) { |
| final TextRange textRange = ref.getRangeInElement().shiftRight(ref.getElement().getTextRange().getStartOffset()); |
| if (selectedElement != null){ |
| final TextRange selectedElementRange = selectedElement.getTextRange(); |
| LOG.assertTrue(selectedElementRange != null, selectedElement); |
| if (selectedElementRange != null && selectedElementRange.contains(textRange)) return true; |
| } |
| return false; |
| } |
| |
| protected void beforeTemplateStart() { |
| myCaretRangeMarker = myEditor.getDocument() |
| .createRangeMarker(new TextRange(myEditor.getCaretModel().getOffset(), myEditor.getCaretModel().getOffset())); |
| myCaretRangeMarker.setGreedyToLeft(true); |
| myCaretRangeMarker.setGreedyToRight(true); |
| } |
| |
| private void startTemplate(final TemplateBuilderImpl builder) { |
| final Disposable disposable = Disposer.newDisposable(); |
| DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(disposable); |
| |
| final MyTemplateListener templateListener = new MyTemplateListener() { |
| @Override |
| protected void restoreDaemonUpdateState() { |
| Disposer.dispose(disposable); |
| } |
| }; |
| |
| final int offset = myEditor.getCaretModel().getOffset(); |
| |
| Template template = builder.buildInlineTemplate(); |
| template.setToShortenLongNames(false); |
| template.setToReformat(false); |
| TextRange range = myScope.getTextRange(); |
| assert range != null; |
| myHighlighters = new ArrayList<RangeHighlighter>(); |
| Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor); |
| topLevelEditor.getCaretModel().moveToOffset(range.getStartOffset()); |
| |
| TemplateManager.getInstance(myProject).startTemplate(topLevelEditor, template, templateListener); |
| restoreOldCaretPositionAndSelection(offset); |
| highlightTemplateVariables(template, topLevelEditor); |
| } |
| |
| private void highlightTemplateVariables(Template template, Editor topLevelEditor) { |
| //add highlights |
| if (myHighlighters != null) { // can be null if finish is called during testing |
| Map<TextRange, TextAttributes> rangesToHighlight = new HashMap<TextRange, TextAttributes>(); |
| final TemplateState templateState = TemplateManagerImpl.getTemplateState(topLevelEditor); |
| if (templateState != null) { |
| EditorColorsManager colorsManager = EditorColorsManager.getInstance(); |
| for (int i = 0; i < templateState.getSegmentsCount(); i++) { |
| final TextRange segmentOffset = templateState.getSegmentRange(i); |
| final String name = template.getSegmentName(i); |
| TextAttributes attributes = null; |
| if (name.equals(PRIMARY_VARIABLE_NAME)) { |
| attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.WRITE_SEARCH_RESULT_ATTRIBUTES); |
| } |
| else if (name.equals(OTHER_VARIABLE_NAME)) { |
| attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES); |
| } |
| if (attributes == null) continue; |
| rangesToHighlight.put(segmentOffset, attributes); |
| } |
| } |
| addHighlights(rangesToHighlight, topLevelEditor, myHighlighters, HighlightManager.getInstance(myProject)); |
| } |
| } |
| |
| private void restoreOldCaretPositionAndSelection(final int offset) { |
| //move to old offset |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| myEditor.getCaretModel().moveToOffset(restoreCaretOffset(offset)); |
| restoreSelection(); |
| } |
| }; |
| |
| final LookupImpl lookup = (LookupImpl)LookupManager.getActiveLookup(myEditor); |
| if (lookup != null && lookup.getLookupStart() <= (restoreCaretOffset(offset))) { |
| lookup.setFocusDegree(LookupImpl.FocusDegree.UNFOCUSED); |
| lookup.performGuardedChange(runnable); |
| } |
| else { |
| runnable.run(); |
| } |
| } |
| |
| protected void restoreSelection() { |
| } |
| |
| protected int restoreCaretOffset(int offset) { |
| return myCaretRangeMarker.isValid() ? myCaretRangeMarker.getEndOffset() : offset; |
| } |
| |
| protected void navigateToAlreadyStarted(Document oldDocument, @Messages.YesNoResult int exitCode) { |
| navigateToStarted(oldDocument, myProject, exitCode); |
| } |
| |
| private static void navigateToStarted(final Document oldDocument, final Project project, @Messages.YesNoResult final int exitCode) { |
| final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(oldDocument); |
| if (file != null) { |
| final VirtualFile virtualFile = file.getVirtualFile(); |
| if (virtualFile != null) { |
| final FileEditor[] editors = FileEditorManager.getInstance(project).getEditors(virtualFile); |
| for (FileEditor editor : editors) { |
| if (editor instanceof TextEditor) { |
| final Editor textEditor = ((TextEditor)editor).getEditor(); |
| final TemplateState templateState = TemplateManagerImpl.getTemplateState(textEditor); |
| if (templateState != null) { |
| if (exitCode == Messages.YES) { |
| final TextRange range = templateState.getVariableRange(PRIMARY_VARIABLE_NAME); |
| if (range != null) { |
| new OpenFileDescriptor(project, virtualFile, range.getStartOffset()).navigate(true); |
| return; |
| } |
| } |
| else if (exitCode > 0){ |
| templateState.gotoEnd(); |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| protected PsiElement getNameIdentifier() { |
| return myElementToRename instanceof PsiNameIdentifierOwner ? ((PsiNameIdentifierOwner)myElementToRename).getNameIdentifier() : null; |
| } |
| |
| @Nullable |
| protected StartMarkAction startRename() throws StartMarkAction.AlreadyStartedException { |
| final StartMarkAction[] markAction = new StartMarkAction[1]; |
| final StartMarkAction.AlreadyStartedException[] ex = new StartMarkAction.AlreadyStartedException[1]; |
| CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { |
| @Override |
| public void run() { |
| try { |
| markAction[0] = StartMarkAction.start(myEditor, myProject, getCommandName()); |
| } |
| catch (StartMarkAction.AlreadyStartedException e) { |
| ex[0] = e; |
| } |
| } |
| }, getCommandName(), null); |
| if (ex[0] != null) throw ex[0]; |
| return markAction[0]; |
| } |
| |
| @Nullable |
| protected PsiNamedElement getVariable() { |
| // todo we can use more specific class, shouldn't we? |
| //Class clazz = myElementToRename != null? myElementToRename.getClass() : PsiNameIdentifierOwner.class; |
| if (myElementToRename != null && myElementToRename.isValid()) { |
| if (Comparing.strEqual(myOldName, myElementToRename.getName())) return myElementToRename; |
| if (myRenameOffset != null) return PsiTreeUtil.findElementOfClassAtRange( |
| myElementToRename.getContainingFile(), myRenameOffset.getStartOffset(), myRenameOffset.getEndOffset(), PsiNameIdentifierOwner.class); |
| } |
| |
| if (myRenameOffset != null) { |
| final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument()); |
| if (psiFile != null) { |
| return PsiTreeUtil.findElementOfClassAtRange(psiFile, myRenameOffset.getStartOffset(), myRenameOffset.getEndOffset(), PsiNameIdentifierOwner.class); |
| } |
| } |
| return myElementToRename; |
| } |
| |
| /** |
| * Called after the completion of the refactoring, either a successful or a failed one. |
| * |
| * @param success true if the refactoring was accepted, false if it was cancelled (by undo or Esc) |
| */ |
| protected void moveOffsetAfter(boolean success) { |
| if (myCaretRangeMarker != null) { |
| myCaretRangeMarker.dispose(); |
| } |
| } |
| |
| protected void addAdditionalVariables(TemplateBuilderImpl builder) { |
| } |
| |
| protected void addReferenceAtCaret(Collection<PsiReference> refs) { |
| PsiFile myEditorFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument()); |
| // Note, that myEditorFile can be different from myElement.getContainingFile() e.g. in injections: myElement declaration in one |
| // file / usage in another ! |
| final PsiReference reference = (myEditorFile != null ? |
| myEditorFile : myElementToRename.getContainingFile()) |
| .findReferenceAt(myEditor.getCaretModel().getOffset()); |
| if (reference instanceof PsiMultiReference) { |
| final PsiReference[] references = ((PsiMultiReference)reference).getReferences(); |
| for (PsiReference ref : references) { |
| addReferenceIfNeeded(refs, ref); |
| } |
| } |
| else { |
| addReferenceIfNeeded(refs, reference); |
| } |
| } |
| |
| private void addReferenceIfNeeded(@NotNull final Collection<PsiReference> refs, @Nullable final PsiReference reference) { |
| if (reference != null && reference.isReferenceTo(myElementToRename) && !refs.contains(reference)) { |
| refs.add(reference); |
| } |
| } |
| |
| protected void showDialogAdvertisement(final String actionId) { |
| final Keymap keymap = KeymapManager.getInstance().getActiveKeymap(); |
| final Shortcut[] shortcuts = keymap.getShortcuts(actionId); |
| if (shortcuts.length > 0) { |
| setAdvertisementText("Press " + KeymapUtil.getShortcutText(shortcuts[0]) + " to show dialog with more options"); |
| } |
| } |
| |
| public String getInitialName() { |
| if (myInitialName == null) { |
| final PsiNamedElement variable = getVariable(); |
| if (variable != null) { |
| return variable.getName(); |
| } |
| } |
| return myInitialName; |
| } |
| |
| protected void revertState() { |
| if (myOldName == null) return; |
| CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { |
| @Override |
| public void run() { |
| final Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor); |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| final TemplateState state = TemplateManagerImpl.getTemplateState(topLevelEditor); |
| assert state != null; |
| final int segmentsCount = state.getSegmentsCount(); |
| final Document document = topLevelEditor.getDocument(); |
| for (int i = 0; i < segmentsCount; i++) { |
| final TextRange segmentRange = state.getSegmentRange(i); |
| document.replaceString(segmentRange.getStartOffset(), segmentRange.getEndOffset(), myOldName); |
| } |
| } |
| }); |
| if (!myProject.isDisposed() && myProject.isOpen()) { |
| PsiDocumentManager.getInstance(myProject).commitDocument(topLevelEditor.getDocument()); |
| } |
| } |
| }, getCommandName(), null); |
| } |
| |
| /** |
| * Returns the name of the command performed by the refactoring. |
| * |
| * @return command name |
| */ |
| protected abstract String getCommandName(); |
| |
| public void finish(boolean success) { |
| if (!ourRenamersStack.isEmpty() && ourRenamersStack.peek() == this) { |
| ourRenamersStack.pop(); |
| } |
| if (myHighlighters != null) { |
| if (!myProject.isDisposed()) { |
| final HighlightManager highlightManager = HighlightManager.getInstance(myProject); |
| for (RangeHighlighter highlighter : myHighlighters) { |
| highlightManager.removeSegmentHighlighter(myEditor, highlighter); |
| } |
| } |
| |
| myHighlighters = null; |
| myEditor.putUserData(INPLACE_RENAMER, null); |
| } |
| if (myBalloon != null) { |
| if (!isRestart()) { |
| myBalloon.hide(); |
| } |
| } |
| } |
| |
| protected void addHighlights(@NotNull Map<TextRange, TextAttributes> ranges, |
| @NotNull Editor editor, |
| @NotNull Collection<RangeHighlighter> highlighters, |
| @NotNull HighlightManager highlightManager) { |
| for (Map.Entry<TextRange, TextAttributes> entry : ranges.entrySet()) { |
| TextRange range = entry.getKey(); |
| TextAttributes attributes = entry.getValue(); |
| highlightManager.addOccurrenceHighlight(editor, range.getStartOffset(), range.getEndOffset(), attributes, 0, highlighters, null); |
| } |
| |
| for (RangeHighlighter highlighter : highlighters) { |
| highlighter.setGreedyToLeft(true); |
| highlighter.setGreedyToRight(true); |
| } |
| } |
| |
| protected abstract boolean performRefactoring(); |
| |
| private void addVariable(final PsiReference reference, |
| final PsiElement selectedElement, |
| final TemplateBuilderImpl builder, |
| int offset) { |
| final PsiElement element = reference.getElement(); |
| if (element == selectedElement && checkRangeContainsOffset(offset, reference.getRangeInElement(), element)) { |
| builder.replaceElement(reference, PRIMARY_VARIABLE_NAME, createLookupExpression(selectedElement), true); |
| } |
| else { |
| builder.replaceElement(reference, OTHER_VARIABLE_NAME, PRIMARY_VARIABLE_NAME, false); |
| } |
| } |
| |
| private void addVariable(final PsiElement element, |
| final PsiElement selectedElement, |
| final TemplateBuilderImpl builder) { |
| addVariable(element, null, selectedElement, builder); |
| } |
| |
| private void addVariable(final PsiElement element, |
| @Nullable final TextRange textRange, |
| final PsiElement selectedElement, |
| final TemplateBuilderImpl builder) { |
| if (element == selectedElement) { |
| builder.replaceElement(element, PRIMARY_VARIABLE_NAME, createLookupExpression(myElementToRename), true); |
| } |
| else if (textRange != null) { |
| builder.replaceElement(element, textRange, OTHER_VARIABLE_NAME, PRIMARY_VARIABLE_NAME, false); |
| } |
| else { |
| builder.replaceElement(element, OTHER_VARIABLE_NAME, PRIMARY_VARIABLE_NAME, false); |
| } |
| } |
| |
| |
| public void setElementToRename(PsiNamedElement elementToRename) { |
| myElementToRename = elementToRename; |
| } |
| |
| protected boolean isIdentifier(final String newName, final Language language) { |
| final NamesValidator namesValidator = LanguageNamesValidation.INSTANCE.forLanguage(language); |
| return namesValidator == null || namesValidator.isIdentifier(newName, myProject); |
| } |
| |
| protected static VirtualFile getTopLevelVirtualFile(final FileViewProvider fileViewProvider) { |
| VirtualFile file = fileViewProvider.getVirtualFile(); |
| if (file instanceof VirtualFileWindow) file = ((VirtualFileWindow)file).getDelegate(); |
| return file; |
| } |
| |
| @TestOnly |
| public static void checkCleared() { |
| try { |
| assert ourRenamersStack.isEmpty() : ourRenamersStack; |
| } |
| finally { |
| ourRenamersStack.clear(); |
| } |
| } |
| |
| private PsiElement getSelectedInEditorElement(@Nullable PsiElement nameIdentifier, |
| final Collection<PsiReference> refs, |
| Collection<Pair<PsiElement, TextRange>> stringUsages, |
| final int offset) { |
| //prefer reference in case of self-references |
| for (PsiReference ref : refs) { |
| final PsiElement element = ref.getElement(); |
| if (checkRangeContainsOffset(offset, ref.getRangeInElement(), element)) return element; |
| } |
| |
| if (nameIdentifier != null) { |
| final TextRange range = nameIdentifier.getTextRange(); |
| if (range != null && checkRangeContainsOffset(offset, range, nameIdentifier, 0)) return nameIdentifier; |
| } |
| |
| for (Pair<PsiElement, TextRange> stringUsage : stringUsages) { |
| if (checkRangeContainsOffset(offset, stringUsage.second, stringUsage.first)) return stringUsage.first; |
| } |
| |
| LOG.error(nameIdentifier + " by " + this.getClass().getName()); |
| return null; |
| } |
| |
| private boolean checkRangeContainsOffset(int offset, final TextRange textRange, PsiElement element) { |
| return checkRangeContainsOffset(offset, textRange, element, element.getTextRange().getStartOffset()); |
| } |
| |
| private boolean checkRangeContainsOffset(int offset, final TextRange textRange, PsiElement element, int shiftOffset) { |
| final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(myProject); |
| final PsiLanguageInjectionHost injectionHost = injectedLanguageManager.getInjectionHost(element); |
| if (injectionHost != null) { |
| final PsiElement nameIdentifier = getNameIdentifier(); |
| final PsiLanguageInjectionHost initialInjectedHost = nameIdentifier != null ? injectedLanguageManager.getInjectionHost(nameIdentifier) : null; |
| if (initialInjectedHost != null && initialInjectedHost != injectionHost) { |
| return false; |
| } |
| return injectedLanguageManager.injectedToHost(element, textRange).shiftRight(shiftOffset).containsOffset(offset); |
| } |
| return textRange.shiftRight(shiftOffset).containsOffset(offset); |
| } |
| |
| protected boolean isRestart() { |
| final Boolean isRestart = myEditor.getUserData(INTRODUCE_RESTART); |
| return isRestart != null && isRestart; |
| } |
| |
| public static boolean canStartAnotherRefactoring(Editor editor, Project project, RefactoringActionHandler handler, PsiElement... element) { |
| final InplaceRefactoring inplaceRefactoring = getActiveInplaceRenamer(editor); |
| return StartMarkAction.canStart(project) == null || |
| (inplaceRefactoring != null && element.length == 1 && inplaceRefactoring.startsOnTheSameElement(handler, element[0])); |
| } |
| |
| public static InplaceRefactoring getActiveInplaceRenamer(Editor editor) { |
| return editor != null ? editor.getUserData(INPLACE_RENAMER) : null; |
| } |
| |
| protected boolean startsOnTheSameElement(RefactoringActionHandler handler, PsiElement element) { |
| return getVariable() == element; |
| } |
| |
| protected void releaseResources() { |
| } |
| |
| @Nullable |
| protected JComponent getComponent() { |
| return null; |
| } |
| |
| protected void showBalloon() { |
| final JComponent component = getComponent(); |
| if (component == null) return; |
| if (ApplicationManager.getApplication().isHeadlessEnvironment()) return; |
| final BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createDialogBalloonBuilder(component, null).setSmallVariant(true); |
| myBalloon = balloonBuilder.createBalloon(); |
| Disposer.register(myProject, myBalloon); |
| Disposer.register(myBalloon, new Disposable() { |
| @Override |
| public void dispose() { |
| releaseIfNotRestart(); |
| myEditor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null); |
| } |
| }); |
| myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); |
| final JBPopupFactory popupFactory = JBPopupFactory.getInstance(); |
| myBalloon.show(new PositionTracker<Balloon>(myEditor.getContentComponent()) { |
| @Override |
| public RelativePoint recalculateLocation(Balloon object) { |
| if (myTarget != null && !popupFactory.isBestPopupLocationVisible(myEditor)) { |
| return myTarget; |
| } |
| if (myCaretRangeMarker != null && myCaretRangeMarker.isValid()) { |
| myEditor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, |
| myEditor.offsetToVisualPosition(myCaretRangeMarker.getStartOffset())); |
| } |
| final RelativePoint target = popupFactory.guessBestPopupLocation(myEditor); |
| final Point screenPoint = target.getScreenPoint(); |
| int y = screenPoint.y; |
| if (target.getPoint().getY() > myEditor.getLineHeight() + myBalloon.getPreferredSize().getHeight()) { |
| y -= myEditor.getLineHeight(); |
| } |
| myTarget = new RelativePoint(new Point(screenPoint.x, y)); |
| return myTarget; |
| } |
| }, Balloon.Position.above); |
| } |
| |
| protected void releaseIfNotRestart() { |
| if (!isRestart()) { |
| releaseResources(); |
| } |
| } |
| |
| private abstract class MyTemplateListener extends TemplateEditingAdapter { |
| |
| protected abstract void restoreDaemonUpdateState(); |
| |
| @Override |
| public void beforeTemplateFinished(final TemplateState templateState, Template template) { |
| try { |
| final TextResult value = templateState.getVariableValue(PRIMARY_VARIABLE_NAME); |
| myInsertedName = value != null ? value.toString() : null; |
| |
| TextRange range = templateState.getCurrentVariableRange(); |
| final int currentOffset = myEditor.getCaretModel().getOffset(); |
| if (range == null && myRenameOffset != null) { |
| range = new TextRange(myRenameOffset.getStartOffset(), myRenameOffset.getEndOffset()); |
| } |
| myBeforeRevert = |
| range != null && range.getEndOffset() >= currentOffset && range.getStartOffset() <= currentOffset |
| ? myEditor.getDocument().createRangeMarker(range.getStartOffset(), currentOffset) |
| : null; |
| if (myBeforeRevert != null) { |
| myBeforeRevert.setGreedyToRight(true); |
| } |
| finish(true); |
| } |
| finally { |
| restoreDaemonUpdateState(); |
| } |
| } |
| |
| @Override |
| public void templateFinished(Template template, final boolean brokenOff) { |
| boolean bind = false; |
| try { |
| super.templateFinished(template, brokenOff); |
| if (!brokenOff) { |
| bind = performRefactoring(); |
| } |
| moveOffsetAfter(!brokenOff); |
| } |
| finally { |
| if (!bind) { |
| try { |
| ((EditorImpl)InjectedLanguageUtil.getTopLevelEditor(myEditor)).stopDumbLater(); |
| } |
| finally { |
| FinishMarkAction.finish(myProject, myEditor, myMarkAction); |
| if (myBeforeRevert != null) { |
| myBeforeRevert.dispose(); |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void templateCancelled(Template template) { |
| try { |
| final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); |
| documentManager.commitAllDocuments(); |
| finish(false); |
| moveOffsetAfter(false); |
| } |
| finally { |
| try { |
| restoreDaemonUpdateState(); |
| } |
| finally { |
| FinishMarkAction.finish(myProject, myEditor, myMarkAction); |
| } |
| } |
| } |
| } |
| } |