/* * Copyright 2000-2011 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.introduce.inplace; import com.intellij.codeInsight.highlighting.HighlightManager; import com.intellij.codeInsight.template.TextResult; import com.intellij.codeInsight.template.impl.TemplateManagerImpl; import com.intellij.codeInsight.template.impl.TemplateState; 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.StartMarkAction; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.event.DocumentAdapter; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.rename.inplace.InplaceRefactoring; import com.intellij.ui.DottedBorder; import com.intellij.util.ui.PositionTracker; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import java.awt.*; import java.util.*; import java.util.List; /** * User: anna * Date: 3/15/11 */ public abstract class AbstractInplaceIntroducer<V extends PsiNameIdentifierOwner, E extends PsiElement> extends InplaceVariableIntroducer<E> { protected V myLocalVariable; protected RangeMarker myLocalMarker; protected final String myExprText; private final String myLocalName; public static final Key<AbstractInplaceIntroducer> ACTIVE_INTRODUCE = Key.create("ACTIVE_INTRODUCE"); private EditorEx myPreview; private final JComponent myPreviewComponent; private DocumentAdapter myDocumentAdapter; protected final JPanel myWholePanel; private boolean myFinished = false; public AbstractInplaceIntroducer(Project project, Editor editor, E expr, @Nullable V localVariable, E[] occurrences, String title, final FileType languageFileType) { super(null, editor, project, title, occurrences, expr); myLocalVariable = localVariable; if (localVariable != null) { final PsiElement nameIdentifier = localVariable.getNameIdentifier(); if (nameIdentifier != null) { myLocalMarker = createMarker(nameIdentifier); } } else { myLocalMarker = null; } myExprText = getExpressionText(expr); myLocalName = localVariable != null ? localVariable.getName() : null; myPreview = (EditorEx)EditorFactory.getInstance().createEditor(EditorFactory.getInstance().createDocument(""), project, languageFileType, true); myPreview.setOneLineMode(true); final EditorSettings settings = myPreview.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); myPreview.setHorizontalScrollbarVisible(false); myPreview.setVerticalScrollbarVisible(false); myPreview.setCaretEnabled(false); settings.setLineCursorWidth(1); final Color bg = myPreview.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); myPreview.setBackgroundColor(bg); myPreview.setBorder(BorderFactory.createCompoundBorder(new DottedBorder(Color.gray), new LineBorder(bg, 2))); myPreviewComponent = new JPanel(new BorderLayout()); myPreviewComponent.add(myPreview.getComponent(), BorderLayout.CENTER); myPreviewComponent.setBorder(new EmptyBorder(2, 2, 6, 2)); myWholePanel = new JPanel(new GridBagLayout()); myWholePanel.setBorder(null); showDialogAdvertisement(getActionName()); } @Nullable protected String getExpressionText(E expr) { return expr != null ? expr.getText() : null; } protected final void setPreviewText(final String text) { if (myPreview == null) return; //already disposed ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { myPreview.getDocument().replaceString(0, myPreview.getDocument().getTextLength(), text); } }); } protected final JComponent getPreviewComponent() { return myPreviewComponent; } protected final Editor getPreviewEditor() { return myPreview; } @Override protected StartMarkAction startRename() throws StartMarkAction.AlreadyStartedException { return StartMarkAction.start(myEditor, myProject, getCommandName()); } /** * Returns ID of the action the shortcut of which is used to show the non-in-place refactoring dialog. * * @return action ID */ protected abstract String getActionName(); /** * Creates an initial version of the declaration for the introduced element. Note that this method is not called in a write action * and most likely needs to create one itself. * * @param replaceAll whether all occurrences are going to be replaced * @param names the suggested names for the declaration * @return the declaration */ @Nullable protected abstract V createFieldToStartTemplateOn(boolean replaceAll, String[] names); /** * Returns the suggested names for the introduced element. * * @param replaceAll whether all occurrences are going to be replaced * @param variable introduced element declaration, if already created. * @return the suggested names */ protected abstract String[] suggestNames(boolean replaceAll, @Nullable V variable); protected abstract void performIntroduce(); protected void performPostIntroduceTasks() {} public abstract boolean isReplaceAllOccurrences(); public abstract void setReplaceAllOccurrences(boolean allOccurrences); @Override protected abstract JComponent getComponent(); protected abstract void saveSettings(@NotNull V variable); @Override protected abstract V getVariable(); public abstract E restoreExpression(PsiFile containingFile, V variable, RangeMarker marker, String exprText); /** * Begins the in-place refactoring operation. * * @return true if the in-place refactoring was successfully started, false if it failed to start and a dialog should be shown instead. */ public boolean startInplaceIntroduceTemplate() { final boolean replaceAllOccurrences = isReplaceAllOccurrences(); final Ref<Boolean> result = new Ref<Boolean>(); CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { final String[] names = suggestNames(replaceAllOccurrences, getLocalVariable()); final V variable = createFieldToStartTemplateOn(replaceAllOccurrences, names); boolean started = false; if (variable != null) { int caretOffset = getCaretOffset(); myEditor.getCaretModel().moveToOffset(caretOffset); myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); final LinkedHashSet<String> nameSuggestions = new LinkedHashSet<String>(); nameSuggestions.add(variable.getName()); nameSuggestions.addAll(Arrays.asList(names)); initOccurrencesMarkers(); setElementToRename(variable); updateTitle(getVariable()); started = AbstractInplaceIntroducer.super.performInplaceRefactoring(nameSuggestions); if (started) { myDocumentAdapter = new DocumentAdapter() { @Override public void documentChanged(DocumentEvent e) { if (myPreview == null) return; final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor); if (templateState != null) { final TextResult value = templateState.getVariableValue(InplaceRefactoring.PRIMARY_VARIABLE_NAME); if (value != null) { updateTitle(getVariable(), value.getText()); } } } }; myEditor.getDocument().addDocumentListener(myDocumentAdapter); updateTitle(getVariable()); if (TemplateManagerImpl.getTemplateState(myEditor) != null) { myEditor.putUserData(ACTIVE_INTRODUCE, AbstractInplaceIntroducer.this); } } } result.set(started); if (!started) { finish(true); } } }, getCommandName(), getCommandName()); return result.get(); } protected int getCaretOffset() { RangeMarker r; if (myLocalMarker != null) { final PsiReference reference = myExpr != null ? myExpr.getReference() : null; if (reference != null && reference.resolve() == myLocalVariable) { r = myExprMarker; } else { r = myLocalMarker; } } else { r = myExprMarker; } return r != null ? r.getStartOffset() : 0; } protected void updateTitle(@Nullable V variable, String value) { if (variable == null) return; final String variableText = variable.getText(); final PsiElement identifier = variable.getNameIdentifier(); if (identifier != null) { final int startOffsetInParent = identifier.getStartOffsetInParent(); setPreviewText(variableText.substring(0, startOffsetInParent) + value + variableText.substring(startOffsetInParent + identifier.getTextLength())); } else { setPreviewText(variableText.replaceFirst(variable.getName(), value)); } revalidate(); } protected void updateTitle(@Nullable V variable) { if (variable == null) return; setPreviewText(variable.getText()); revalidate(); } protected void revalidate() { myWholePanel.revalidate(); if (myTarget != null) { myBalloon.revalidate(new PositionTracker.Static<Balloon>(myTarget)); } } private boolean myShouldSelect = true; @Override protected boolean shouldSelectAll() { return myShouldSelect; } public void restartInplaceIntroduceTemplate() { Runnable restartTemplateRunnable = new Runnable() { @Override public void run() { final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor); if (templateState != null) { myEditor.putUserData(INTRODUCE_RESTART, true); try { final TextRange range = templateState.getCurrentVariableRange(); if (range != null && range.isEmpty()) { final String[] names = suggestNames(isReplaceAllOccurrences(), getLocalVariable()); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { myEditor.getDocument().insertString(myEditor.getCaretModel().getOffset(), names[0]); } }); } templateState.gotoEnd(true); try { myShouldSelect = false; startInplaceIntroduceTemplate(); } finally { myShouldSelect = true; } } finally { myEditor.putUserData(INTRODUCE_RESTART, false); } } updateTitle(getVariable()); } }; CommandProcessor.getInstance().executeCommand(myProject, restartTemplateRunnable, getCommandName(), getCommandName()); } @Override protected void restoreSelection() { if (!shouldSelectAll()) { myEditor.getSelectionModel().removeSelection(); } } public String getInputName() { return myInsertedName; } @Override public void finish(boolean success) { myFinished = true; final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor); if (templateState != null) { myEditor.putUserData(ACTIVE_INTRODUCE, null); } if (myDocumentAdapter != null) { myEditor.getDocument().removeDocumentListener(myDocumentAdapter); } if (myBalloon == null) { releaseIfNotRestart(); } super.finish(success); if (success) { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); final V variable = getVariable(); if (variable == null) { return; } restoreState(variable); } } @Override protected void releaseResources() { super.releaseResources(); if (myPreview == null) return; EditorFactory.getInstance().releaseEditor(myPreview); myPreview = null; } @Override protected void addReferenceAtCaret(Collection<PsiReference> refs) { final V variable = getLocalVariable(); if (variable != null) { for (PsiReference reference : ReferencesSearch.search(variable)) { refs.add(reference); } } else { refs.clear(); } } @Override protected void collectAdditionalElementsToRename(List<Pair<PsiElement, TextRange>> stringUsages) { if (isReplaceAllOccurrences()) { for (E expression : getOccurrences()) { LOG.assertTrue(expression.isValid(), expression.getText()); stringUsages.add(Pair.<PsiElement, TextRange>create(expression, new TextRange(0, expression.getTextLength()))); } } else if (getExpr() != null) { correctExpression(); final E expr = getExpr(); LOG.assertTrue(expr.isValid(), expr.getText()); stringUsages.add(Pair.<PsiElement, TextRange>create(expr, new TextRange(0, expr.getTextLength()))); } final V localVariable = getLocalVariable(); if (localVariable != null) { final PsiElement nameIdentifier = localVariable.getNameIdentifier(); if (nameIdentifier != null) { int length = nameIdentifier.getTextLength(); stringUsages.add(Pair.<PsiElement, TextRange>create(nameIdentifier, new TextRange(0, length))); } } } protected void correctExpression() {} @Override protected void addHighlights(@NotNull Map<TextRange, TextAttributes> ranges, @NotNull Editor editor, @NotNull Collection<RangeHighlighter> highlighters, @NotNull HighlightManager highlightManager) { final TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES); final V variable = getVariable(); if (variable != null) { final String name = variable.getName(); LOG.assertTrue(name != null, variable); final int variableNameLength = name.length(); if (isReplaceAllOccurrences()) { for (RangeMarker marker : getOccurrenceMarkers()) { final int startOffset = marker.getStartOffset(); highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null); } } else if (getExpr() != null) { final int startOffset = getExprMarker().getStartOffset(); highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null); } } for (RangeHighlighter highlighter : highlighters) { highlighter.setGreedyToLeft(true); highlighter.setGreedyToRight(true); } } protected void restoreState(final V psiField) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { final PsiFile containingFile = psiField.getContainingFile(); final RangeMarker exprMarker = getExprMarker(); if (exprMarker != null) { myExpr = restoreExpression(containingFile, psiField, exprMarker, myExprText); if (myExpr != null && myExpr.isPhysical()) { myExprMarker = createMarker(myExpr); } } if (myLocalMarker != null) { final PsiElement refVariableElement = containingFile.findElementAt(myLocalMarker.getStartOffset()); if (refVariableElement != null) { final PsiElement parent = refVariableElement.getParent(); if (parent instanceof PsiNamedElement) { ((PsiNamedElement)parent).setName(myLocalName); } } final V localVariable = getLocalVariable(); if (localVariable != null && localVariable.isPhysical()) { myLocalVariable = localVariable; final PsiElement nameIdentifier = localVariable.getNameIdentifier(); if (nameIdentifier != null) { myLocalMarker = createMarker(nameIdentifier); } } } final List<RangeMarker> occurrenceMarkers = getOccurrenceMarkers(); for (int i = 0, occurrenceMarkersSize = occurrenceMarkers.size(); i < occurrenceMarkersSize; i++) { RangeMarker marker = occurrenceMarkers.get(i); if (getExprMarker() != null && marker.getStartOffset() == getExprMarker().getStartOffset() && myExpr != null) { myOccurrences[i] = myExpr; continue; } final E psiExpression = restoreExpression(containingFile, psiField, marker, getLocalVariable() != null ? myLocalName : myExprText); if (psiExpression != null) { myOccurrences[i] = psiExpression; } } myOccurrenceMarkers = null; deleteTemplateField(psiField); } }); } protected void deleteTemplateField(V psiField) { if (psiField.isValid()) { psiField.delete(); } } @Override protected boolean performRefactoring() { final String newName = getInputName(); if (getLocalVariable() == null && myExpr == null || newName == null || getLocalVariable() != null && !getLocalVariable().isValid() || myExpr != null && !myExpr.isValid()) { super.moveOffsetAfter(false); return false; } if (getLocalVariable() != null) { new WriteCommandAction(myProject, getCommandName(), getCommandName()) { @Override protected void run(Result result) throws Throwable { getLocalVariable().setName(myLocalName); } }.execute(); } if (!isIdentifier(newName, myExpr != null ? myExpr.getLanguage() : getLocalVariable().getLanguage())) return false; CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { performIntroduce(); } }, getCommandName(), getCommandName()); V variable = getVariable(); if (variable != null) { saveSettings(variable); } return false; } @Override protected void moveOffsetAfter(boolean success) { if (getLocalVariable() != null && getLocalVariable().isValid()) { myEditor.getCaretModel().moveToOffset(getLocalVariable().getTextOffset()); myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); } else if (getExprMarker() != null) { final RangeMarker exprMarker = getExprMarker(); if (exprMarker.isValid()) { myEditor.getCaretModel().moveToOffset(exprMarker.getStartOffset()); myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); } } super.moveOffsetAfter(success); if (myLocalMarker != null && !isRestart()) { myLocalMarker.dispose(); } if (success) { performPostIntroduceTasks(); } } @Override protected boolean startsOnTheSameElement(RefactoringActionHandler handler, PsiElement element) { return super.startsOnTheSameElement(handler, element) || getLocalVariable() == element; } public V getLocalVariable() { if (myLocalVariable != null && myLocalVariable.isValid()) { return myLocalVariable; } if (myLocalMarker != null) { V variable = getVariable(); PsiFile containingFile; if (variable != null) { containingFile = variable.getContainingFile(); } else { containingFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument()); } PsiNameIdentifierOwner identifierOwner = PsiTreeUtil.getParentOfType(containingFile.findElementAt(myLocalMarker.getStartOffset()), PsiNameIdentifierOwner.class, false); return identifierOwner != null && identifierOwner.getClass() == myLocalVariable.getClass() ? (V)identifierOwner : null; } return myLocalVariable; } public void stopIntroduce(Editor editor) { final TemplateState templateState = TemplateManagerImpl.getTemplateState(editor); if (templateState != null) { final Runnable runnable = new Runnable() { @Override public void run() { templateState.gotoEnd(true); } }; CommandProcessor.getInstance().executeCommand(myProject, runnable, getCommandName(), getCommandName()); } } @Override protected void navigateToAlreadyStarted(Document oldDocument, int exitCode) { finish(true); super.navigateToAlreadyStarted(oldDocument, exitCode); } @Override protected void showBalloon() { if (myFinished) return; super.showBalloon(); } public boolean startsOnTheSameElement(E expr, V localVariable) { if (myExprMarker != null && myExprMarker.isValid() && expr != null && myExprMarker.getStartOffset() == expr.getTextOffset()) { return true; } if (myLocalMarker != null && myLocalMarker.isValid() && localVariable != null && myLocalMarker.getStartOffset() == localVariable.getTextOffset()) { return true; } return isRestart(); } @Nullable public static AbstractInplaceIntroducer getActiveIntroducer(@Nullable Editor editor) { if (editor == null) return null; return editor.getUserData(ACTIVE_INTRODUCE); } }