/* * Copyright 2000-2017 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.command.undo.UndoUtil; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.util.EditorUtil; 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.fileTypes.FileType; 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.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.util.registry.Registry; 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.DottedBorder; 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 javax.swing.border.LineBorder; import java.awt.*; import java.util.*; import java.util.List; 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<>(); 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<>(); 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) { if (file == null) { return ProjectScope.getProjectScope(myElementToRename.getProject()); } else { final PsiFile containingFile = myElementToRename.getContainingFile(); if (!file.equals(containingFile.getVirtualFile())) { final PsiFile topLevelFile = PsiManager.getInstance(myProject).findFile(file); return topLevelFile == null ? ProjectScope.getProjectScope(myElementToRename.getProject()) : new LocalSearchScope(topLevelFile); } return new LocalSearchScope(containingFile); } } @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.getElement(), getRangeToRename(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); int segmentsLimit = Registry.intValue("inplace.rename.segments.limit", -1); if (segmentsLimit != -1 && builder.getElementsCount() > segmentsLimit) { return false; } 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(@NotNull 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(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(); Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor); TextRange range = myScope.getTextRange(); assert range != null; RangeMarker rangeMarker = topLevelEditor.getDocument().createRangeMarker(range); Template template = builder.buildInlineTemplate(); template.setToShortenLongNames(false); template.setToReformat(false); myHighlighters = new ArrayList<>(); topLevelEditor.getCaretModel().moveToOffset(rangeMarker.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<>(); 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 = () -> { 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; } public static EditorEx createPreviewComponent(Project project, FileType languageFileType) { Document document = EditorFactory.getInstance().createDocument(""); UndoUtil.disableUndoFor(document); EditorEx previewEditor = (EditorEx)EditorFactory.getInstance().createEditor(document, project, languageFileType, true); previewEditor.setOneLineMode(true); final EditorSettings settings = previewEditor.getSettings(); settings.setAdditionalLinesCount(0); settings.setAdditionalColumnsCount(1); settings.setRightMarginShown(false); settings.setFoldingOutlineShown(false); settings.setLineNumbersShown(false); settings.setLineMarkerAreaShown(false); settings.setIndentGuidesShown(false); settings.setVirtualSpace(false); previewEditor.setHorizontalScrollbarVisible(false); previewEditor.setVerticalScrollbarVisible(false); previewEditor.setCaretEnabled(false); settings.setLineCursorWidth(1); final Color bg = previewEditor.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); previewEditor.setBackgroundColor(bg); previewEditor.setBorder(BorderFactory.createCompoundBorder(new DottedBorder(Color.gray), new LineBorder(bg, 2))); return previewEditor; } @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, () -> { 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(); } LOG.error("Initial name should be provided"); return ""; } return myInitialName; } protected void revertState() { if (myOldName == null) return; CommandProcessor.getInstance().executeCommand(myProject, () -> { final Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor); ApplicationManager.getApplication().runWriteAction(() -> { 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(); /** * if brokenOff but not canceled */ protected void performCleanup() {} 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.getElement(), getRangeToRename(reference), PRIMARY_VARIABLE_NAME, createLookupExpression(selectedElement), true); } else { builder.replaceElement(reference.getElement(), getRangeToRename(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, getRangeToRename(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, getRangeToRename(element), OTHER_VARIABLE_NAME, PRIMARY_VARIABLE_NAME, false); } } @NotNull protected TextRange getRangeToRename(@NotNull PsiElement element) { return new TextRange(0, element.getTextLength()); } @NotNull protected TextRange getRangeToRename(@NotNull PsiReference reference) { return reference.getRangeInElement(); } 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 && inplaceRefactoring.startsOnTheSameElements(editor, handler, element)); } public static InplaceRefactoring getActiveInplaceRenamer(Editor editor) { return editor != null ? editor.getUserData(INPLACE_RENAMER) : null; } protected boolean startsOnTheSameElements(Editor editor, RefactoringActionHandler handler, PsiElement[] element) { return element.length == 1 && startsOnTheSameElement(handler, element[0]); } 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); } }); EditorUtil.disposeWithEditor(myEditor, myBalloon); 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 { if (!brokenOff) { bind = performRefactoring(); } else { performCleanup(); } 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); } } } } }