/* * Copyright 2010-2015 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 org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable; import com.intellij.codeInsight.template.Expression; import com.intellij.codeInsight.template.TemplateBuilderImpl; import com.intellij.codeInsight.template.impl.TemplateManagerImpl; import com.intellij.codeInsight.template.impl.TemplateState; import com.intellij.lang.ASTNode; 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.editor.Editor; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.*; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.psi.search.SearchScope; import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; import com.intellij.ui.NonFocusableCheckBox; import com.intellij.util.ui.PositionTracker; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention; import org.jetbrains.kotlin.idea.references.ReferenceUtilKt; import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers; import org.jetbrains.kotlin.lexer.KtTokens; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.types.KotlinType; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; public class KotlinInplaceVariableIntroducer<D extends KtCallableDeclaration> extends InplaceVariableIntroducer<KtExpression> { private static final Key<KotlinInplaceVariableIntroducer> ACTIVE_INTRODUCER = Key.create("ACTIVE_INTRODUCER"); public static final String TYPE_REFERENCE_VARIABLE_NAME = "TypeReferenceVariable"; public static final String PRIMARY_VARIABLE_NAME = "PrimaryVariable"; private static final Function0<Boolean> TRUE = new Function0<Boolean>() { @Override public Boolean invoke() { return true; } }; private static final Pass<JComponent> DO_NOTHING = new Pass<JComponent>() { @Override public void pass(JComponent component) { } }; protected static final class ControlWrapper { @NotNull private final Function0<JComponent> factory; @NotNull private final Function0<Boolean> condition; @NotNull private final Pass<JComponent> initializer; private JComponent component; public ControlWrapper( @NotNull Function0<JComponent> factory, @NotNull Function0<Boolean> condition, @NotNull Pass<JComponent> initializer) { this.factory = factory; this.condition = condition; this.initializer = initializer; } public ControlWrapper(@NotNull Function0<JComponent> factory) { this(factory, TRUE, DO_NOTHING); } public boolean isAvailable() { return condition.invoke(); } public void initialize() { initializer.pass(getComponent()); } @NotNull public JComponent getComponent() { if (component == null) { component = factory.invoke(); } return component; } } private final boolean myReplaceOccurrence; protected D myDeclaration; private final boolean isVar; private final boolean myDoNotChangeVar; @Nullable private final KotlinType myExprType; private final boolean noTypeInference; private final List<ControlWrapper> panelControls = new ArrayList<ControlWrapper>(); private JPanel contentPanel; public KotlinInplaceVariableIntroducer( PsiNamedElement elementToRename, Editor editor, Project project, String title, KtExpression[] occurrences, @Nullable KtExpression expr, boolean replaceOccurrence, D declaration, boolean isVar, boolean doNotChangeVar, @Nullable KotlinType exprType, boolean noTypeInference ) { super(elementToRename, editor, project, title, occurrences, expr); this.myReplaceOccurrence = replaceOccurrence; myDeclaration = declaration; this.isVar = isVar; myDoNotChangeVar = doNotChangeVar; myExprType = exprType; this.noTypeInference = noTypeInference; String advertisementActionId = getAdvertisementActionId(); if (advertisementActionId != null) { showDialogAdvertisement(advertisementActionId); } } @Nullable protected String getAdvertisementActionId() { return null; } @NotNull private JPanel getContentPanel() { if (contentPanel == null) { contentPanel = new JPanel(new GridBagLayout()); contentPanel.setBorder(null); } return contentPanel; } protected final void addPanelControl(@NotNull ControlWrapper panelControl) { panelControls.add(panelControl); } protected final void addPanelControl(@Nullable Function0<JComponent> initializer) { if (initializer != null) { addPanelControl(new ControlWrapper(initializer)); } } protected void initPanelControls() { addPanelControl(getCreateVarCheckBox()); addPanelControl(getCreateExplicitTypeCheckBox()); } protected final void updatePanelControls() { JPanel panel = getContentPanel(); panel.removeAll(); int count = 1; for (ControlWrapper panelControl : panelControls) { if (!panelControl.isAvailable()) continue; panelControl.initialize(); panel.add(panelControl.getComponent(), new GridBagConstraints(0, count, 1, 1, 1, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL, new Insets(0, 5, 0, 5), 0, 0)); ++count; } panel.add(Box.createVerticalBox(), new GridBagConstraints(0, count, 1, 1, 1, 1, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); } @Override @Nullable protected final JComponent getComponent() { panelControls.clear(); initPanelControls(); updatePanelControls(); return getContentPanel(); } @Nullable protected final Function0<JComponent> getCreateExplicitTypeCheckBox() { if (myExprType == null || noTypeInference) return null; return new Function0<JComponent>() { @Override public JComponent invoke() { final JCheckBox exprTypeCheckbox = new NonFocusableCheckBox("Specify type explicitly"); exprTypeCheckbox.setSelected(false); exprTypeCheckbox.setMnemonic('t'); exprTypeCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(@NotNull ActionEvent e) { runWriteActionAndRestartRefactoring( new Runnable() { @Override public void run() { if (exprTypeCheckbox.isSelected()) { String renderedType = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(myExprType); myDeclaration.setTypeReference(new KtPsiFactory(myProject).createType(renderedType)); } else { myDeclaration.setTypeReference(null); } } } ); } }); return exprTypeCheckbox; } }; } @Nullable protected final Function0<JComponent> getCreateVarCheckBox() { if (myDoNotChangeVar) return null; return new Function0<JComponent>() { @Override public JComponent invoke() { final JCheckBox varCheckbox = new NonFocusableCheckBox("Declare with var"); varCheckbox.setSelected(isVar); varCheckbox.setMnemonic('v'); varCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(@NotNull ActionEvent e) { new WriteCommandAction(myProject, getCommandName(), getCommandName()) { @Override protected void run(@NotNull Result result) throws Throwable { PsiDocumentManager.getInstance(myProject).commitDocument(myEditor.getDocument()); KtPsiFactory psiFactory = new KtPsiFactory(myProject); PsiElement keyword = varCheckbox.isSelected() ? psiFactory.createVarKeyword() : psiFactory.createValKeyword(); PsiElement valOrVar = myDeclaration instanceof KtProperty ? ((KtProperty) myDeclaration).getValOrVarKeyword() : ((KtParameter) myDeclaration).getValOrVarKeyword(); valOrVar.replace(keyword); } }.execute(); } }); return varCheckbox; } }; } protected final void runWriteActionAndRestartRefactoring(final Runnable runnable) { final Ref<Boolean> greedyToRight = new Ref<Boolean>(); new WriteCommandAction(myProject, getCommandName(), getCommandName()) { @Override protected void run(@NotNull Result result) throws Throwable { PsiDocumentManager.getInstance(myProject).commitDocument(myEditor.getDocument()); ASTNode identifier = myDeclaration.getNode().findChildByType(KtTokens.IDENTIFIER); if (identifier != null) { TextRange range = identifier.getTextRange(); RangeHighlighter[] highlighters = myEditor.getMarkupModel().getAllHighlighters(); for (RangeHighlighter highlighter : highlighters) { if (highlighter.getStartOffset() == range.getStartOffset()) { if (highlighter.getEndOffset() == range.getEndOffset()) { greedyToRight.set(highlighter.isGreedyToRight()); highlighter.setGreedyToRight(false); } } } } runnable.run(); TemplateState templateState = TemplateManagerImpl.getTemplateState(InjectedLanguageUtil.getTopLevelEditor(myEditor)); if (templateState != null) { myEditor.putUserData(INTRODUCE_RESTART, true); templateState.cancelTemplate(); } } }.execute(); ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { ASTNode identifier = myDeclaration.getNode().findChildByType(KtTokens.IDENTIFIER); if (identifier != null) { TextRange range = identifier.getTextRange(); RangeHighlighter[] highlighters = myEditor.getMarkupModel().getAllHighlighters(); for (RangeHighlighter highlighter : highlighters) { if (highlighter.getStartOffset() == range.getStartOffset()) { if (highlighter.getEndOffset() == range.getEndOffset()) { highlighter.setGreedyToRight(greedyToRight.get()); } } } } } }); if (myEditor.getUserData(INTRODUCE_RESTART) == Boolean.TRUE) { myInitialName = myDeclaration.getName(); performInplaceRefactoring(getSuggestionsForNextRun()); } } private LinkedHashSet<String> getSuggestionsForNextRun() { LinkedHashSet<String> nameSuggestions; String currentName = myDeclaration.getName(); if (myNameSuggestions.contains(currentName)) { nameSuggestions = myNameSuggestions; } else { nameSuggestions = new LinkedHashSet<String>(); nameSuggestions.add(currentName); nameSuggestions.addAll(myNameSuggestions); } return nameSuggestions; } protected void revalidate() { getContentPanel().revalidate(); if (myTarget != null) { myBalloon.revalidate(new PositionTracker.Static<Balloon>(myTarget)); } } protected void addTypeReferenceVariable(TemplateBuilderImpl builder) { KtTypeReference typeReference = myDeclaration.getTypeReference(); Expression expression = SpecifyTypeExplicitlyIntention.Companion.createTypeExpressionForTemplate(myExprType, myDeclaration); if (typeReference != null && expression != null) { builder.replaceElement(typeReference, TYPE_REFERENCE_VARIABLE_NAME, expression, false); } } @Override protected void addAdditionalVariables(TemplateBuilderImpl builder) { addTypeReferenceVariable(builder); } @Override protected boolean buildTemplateAndStart( Collection<PsiReference> refs, Collection<Pair<PsiElement, TextRange>> stringUsages, PsiElement scope, PsiFile containingFile ) { myEditor.putUserData(INTRODUCE_RESTART, false); //noinspection ConstantConditions myEditor.getCaretModel().moveToOffset(getNameIdentifier().getTextOffset()); boolean result = super.buildTemplateAndStart(refs, stringUsages, scope, containingFile); TemplateState templateState = TemplateManagerImpl.getTemplateState(InjectedLanguageUtil.getTopLevelEditor(myEditor)); if (templateState != null && myDeclaration.getTypeReference() != null) { templateState.addTemplateStateListener(SpecifyTypeExplicitlyIntention.Companion.createTypeReferencePostprocessor(myDeclaration)); } return result; } @Override protected Collection<PsiReference> collectRefs(SearchScope referencesSearchScope) { return kotlin.collections.CollectionsKt.map( kotlin.collections.ArraysKt.filterIsInstance(getOccurrences(), KtSimpleNameExpression.class), new Function1<KtSimpleNameExpression, PsiReference>() { @Override public PsiReference invoke(KtSimpleNameExpression expression) { return ReferenceUtilKt.getMainReference(expression); } } ); } @Override public boolean performInplaceRefactoring(LinkedHashSet<String> nameSuggestions) { if (super.performInplaceRefactoring(nameSuggestions)) { myEditor.putUserData(ACTIVE_INTRODUCER, this); return true; } return false; } @Override public void finish(boolean success) { super.finish(success); myEditor.putUserData(ACTIVE_INTRODUCER, null); } @Override protected void moveOffsetAfter(boolean success) { if (!myReplaceOccurrence || myExprMarker == null) { myEditor.getCaretModel().moveToOffset(myDeclaration.getTextRange().getEndOffset()); } else { int startOffset = myExprMarker.getStartOffset(); PsiFile file = myDeclaration.getContainingFile(); PsiElement elementAt = file.findElementAt(startOffset); if (elementAt != null) { myEditor.getCaretModel().moveToOffset(elementAt.getTextRange().getEndOffset()); } else { myEditor.getCaretModel().moveToOffset(myExprMarker.getEndOffset()); } } } public void stopIntroduce() { final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor); if (templateState != null) { Runnable runnable = new Runnable() { @Override public void run() { templateState.gotoEnd(true); } }; CommandProcessor.getInstance().executeCommand(myProject, runnable, getCommandName(), getCommandName()); } } @Nullable public static KotlinInplaceVariableIntroducer getActiveInstance(@NotNull Editor editor) { return editor.getUserData(ACTIVE_INTRODUCER); } }