From 1fe6df268afc89b76e1f1773723cda87a5e6e4b3 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Thu, 10 Apr 2014 19:58:24 +0400 Subject: [PATCH] Extract Function: UI and refactoring handler --- .../com/intellij/refactoring/annotations.xml | 8 + .../JetRefactoringSupportProvider.java | 6 + .../refactoring/JetRefactoringUtil.java | 32 +- .../ExtractKotlinFunctionHandler.kt | 208 +++++++++++ .../ui/KotlinExtractFunctionDialog.form | 122 +++++++ .../ui/KotlinExtractFunctionDialog.java | 219 ++++++++++++ .../ui/KotlinFunctionSignatureComponent.kt | 29 ++ .../ui/KotlinParameterTablePanel.java | 338 ++++++++++++++++++ .../plugin/refactoring/jetRefactoringUtil.kt | 30 ++ 9 files changed, 985 insertions(+), 7 deletions(-) create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.form create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinFunctionSignatureComponent.kt create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinParameterTablePanel.java diff --git a/annotations/com/intellij/refactoring/annotations.xml b/annotations/com/intellij/refactoring/annotations.xml index df013310544..f9e6d56b074 100644 --- a/annotations/com/intellij/refactoring/annotations.xml +++ b/annotations/com/intellij/refactoring/annotations.xml @@ -16,6 +16,14 @@ name='com.intellij.refactoring.MultiFileTestCase.PerformAction void performAction(com.intellij.openapi.vfs.VirtualFile, com.intellij.openapi.vfs.VirtualFile) 0'> + + + + + + diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringSupportProvider.java b/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringSupportProvider.java index 96adb04ede1..e800a1f7578 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringSupportProvider.java +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringSupportProvider.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jet.lang.psi.*; import org.jetbrains.jet.plugin.refactoring.changeSignature.JetChangeSignatureHandler; +import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractKotlinFunctionHandler; import org.jetbrains.jet.plugin.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler; import org.jetbrains.jet.plugin.refactoring.safeDelete.SafeDeletePackage; @@ -38,6 +39,11 @@ public class JetRefactoringSupportProvider extends RefactoringSupportProvider { return new KotlinIntroduceVariableHandler(); } + @Override + public RefactoringActionHandler getExtractMethodHandler() { + return new ExtractKotlinFunctionHandler(); + } + @Override public boolean isInplaceRenameAvailable(@NotNull PsiElement element, PsiElement context) { if (element instanceof JetProperty) { diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringUtil.java b/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringUtil.java index d09a5f6aa8d..b8ba93efa32 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringUtil.java +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/JetRefactoringUtil.java @@ -366,8 +366,16 @@ public class JetRefactoringUtil { void run(@Nullable JetExpression expression); } + public static void selectExpression( + @NotNull Editor editor, + @NotNull PsiFile file, + @NotNull SelectExpressionCallback callback) throws IntroduceRefactoringException { + selectExpression(editor, file, true, callback); + } + public static void selectExpression(@NotNull Editor editor, @NotNull PsiFile file, + boolean failOnEmptySuggestion, @NotNull SelectExpressionCallback callback) throws IntroduceRefactoringException { if (editor.getSelectionModel().hasSelection()) { int selectionStart = editor.getSelectionModel().getSelectionStart(); @@ -375,11 +383,11 @@ public class JetRefactoringUtil { String text = file.getText(); while (selectionStart < selectionEnd && Character.isSpaceChar(text.charAt(selectionStart))) ++selectionStart; while (selectionStart < selectionEnd && Character.isSpaceChar(text.charAt(selectionEnd - 1))) --selectionEnd; - callback.run(findExpression(file, selectionStart, selectionEnd)); + callback.run(findExpression(file, selectionStart, selectionEnd, failOnEmptySuggestion)); } else { int offset = editor.getCaretModel().getOffset(); - smartSelectExpression(editor, file, offset, callback); + smartSelectExpression(editor, file, offset, failOnEmptySuggestion, callback); } } @@ -442,11 +450,15 @@ public class JetRefactoringUtil { private static void smartSelectExpression( @NotNull Editor editor, @NotNull PsiFile file, int offset, + boolean failOnEmptySuggestion, @NotNull final SelectExpressionCallback callback) throws IntroduceRefactoringException { List expressions = getSmartSelectSuggestions(file, offset); - if (expressions.size() == 0) throw new IntroduceRefactoringException(JetRefactoringBundle.message("cannot.refactor.not.expression")); + if (expressions.size() == 0) { + if (failOnEmptySuggestion) throw new IntroduceRefactoringException(JetRefactoringBundle.message("cannot.refactor.not.expression")); + return; + } - if (expressions.size() == 1) { + if (expressions.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) { callback.run(expressions.get(0)); return; } @@ -509,12 +521,18 @@ public class JetRefactoringUtil { return expressionText; } - @NotNull - private static JetExpression findExpression(@NotNull PsiFile file, int startOffset, int endOffset) throws IntroduceRefactoringException { + @Nullable + private static JetExpression findExpression( + @NotNull PsiFile file, int startOffset, int endOffset, boolean failOnNoExpression + ) throws IntroduceRefactoringException { JetExpression element = CodeInsightUtils.findExpression(file, startOffset, endOffset); if (element == null) { //todo: if it's infix expression => add (), then commit document then return new created expression - throw new IntroduceRefactoringException(JetRefactoringBundle.message("cannot.refactor.not.expression")); + + if (failOnNoExpression) { + throw new IntroduceRefactoringException(JetRefactoringBundle.message("cannot.refactor.not.expression")); + } + return null; } return element; } diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt new file mode 100644 index 00000000000..044e93fcd73 --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2010-2014 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.jet.plugin.refactoring.extractFunction + +import com.intellij.refactoring.RefactoringActionHandler +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.psi.PsiFile +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.openapi.project.Project +import com.intellij.openapi.editor.ScrollType +import org.jetbrains.jet.plugin.codeInsight.CodeInsightUtils +import org.jetbrains.jet.plugin.refactoring.JetRefactoringUtil +import com.intellij.refactoring.HelpID +import org.jetbrains.jet.plugin.refactoring.JetRefactoringBundle +import org.jetbrains.jet.plugin.refactoring.extractFunction.ui.KotlinExtractFunctionDialog +import org.jetbrains.jet.lang.psi.JetFile +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.jet.lang.psi.JetElement +import org.jetbrains.jet.plugin.refactoring.getAllExtractionContainers +import com.intellij.refactoring.IntroduceTargetChooser +import com.intellij.openapi.util.Pass +import com.intellij.openapi.application.ApplicationManager +import org.jetbrains.jet.lang.psi.JetPsiFactory +import org.jetbrains.jet.lang.psi.psiUtil.getOutermostParentContainedIn +import org.jetbrains.jet.plugin.refactoring.checkConflictsInteractively +import org.jetbrains.jet.plugin.refactoring.executeWriteCommand +import org.jetbrains.jet.plugin.util.MaybeError +import org.jetbrains.jet.plugin.util.MaybeValue +import org.jetbrains.jet.lang.psi.JetBlockExpression +import org.jetbrains.jet.lang.psi.JetClassOrObject +import org.jetbrains.jet.lang.psi.JetClassBody +import org.jetbrains.jet.lang.psi.JetNamedFunction +import org.jetbrains.jet.plugin.codeInsight.ShortenReferences +import kotlin.test.fail +import org.jetbrains.jet.lang.psi.JetDeclarationWithBody + +public class ExtractKotlinFunctionHandler : RefactoringActionHandler { + fun doInvoke( + editor: Editor, + file: JetFile, + elements: List, + nextSibling: PsiElement + ) { + val project = file.getProject() + + val analysisResult = ExtractionData(file, elements, nextSibling).performAnalysis() + if (analysisResult is MaybeError) { + showErrorHint(project, editor, analysisResult.error) + return + } + + val validationResult = (analysisResult as MaybeValue).value.validate() + if (!project.checkConflictsInteractively(validationResult.conflicts)) return + + val descriptor = + if (ApplicationManager.getApplication()!!.isUnitTestMode()) { + validationResult.descriptor + } + else { + val dialog = KotlinExtractFunctionDialog(project, validationResult.descriptor) + if (!dialog.showAndGet()) return + + dialog.getCurrentDescriptor() + } + + project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction() } + } + + override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { + if (file !is JetFile) return + + selectElements(editor, file) { (elements, targetNextSibling) -> + doInvoke(editor, file, elements, targetNextSibling) + } + } + + override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { + fail("Extract Function can only be invoked from editor") + } +} + +private val EXTRACT_FUNCTION: String = JetRefactoringBundle.message("extract.function")!! + +private fun showErrorHint(project: Project, editor: Editor, message: String) { + CodeInsightUtils.showErrorHint(project, editor, message, EXTRACT_FUNCTION, HelpID.EXTRACT_METHOD) +} + +private fun showErrorHintByKey(project: Project, editor: Editor, key: String) { + showErrorHint(project, editor, JetRefactoringBundle.message(key)!!) +} + +fun selectElements( + editor: Editor, + file: PsiFile, + continuation: (elements: List, targetNextSibling: PsiElement) -> Unit +) { + fun noExpressionError() { + showErrorHintByKey(file.getProject(), editor, "cannot.refactor.no.expression") + } + + fun noContainerError() { + showErrorHintByKey(file.getProject(), editor, "cannot.refactor.no.container") + } + + fun onSelectionComplete(parent: PsiElement, elements: List, targetContainer: JetElement) { + if (parent == targetContainer) { + continuation(elements, elements.first!!) + return + } + + val outermostParent = parent.getOutermostParentContainedIn(targetContainer) + if (outermostParent == null) { + noContainerError() + return + } + + continuation(elements, outermostParent) + } + + fun selectTargetContainer(elements: List) { + val parent = PsiTreeUtil.findCommonParent(elements) + ?: throw AssertionError("Should have at least one parent: ${elements.makeString("\n")}") + + val containers = parent.getAllExtractionContainers(elements.size == 1) + if (containers.empty) { + noContainerError() + return + } + + if (ApplicationManager.getApplication()!!.isUnitTestMode()) { + onSelectionComplete(parent, elements, containers[0]) + return + } + + IntroduceTargetChooser.showChooser( + editor, + containers, + object: Pass() { + override fun pass(targetContainer: JetElement?) { + if (targetContainer == null) { + noContainerError() + return + } + + onSelectionComplete(parent, elements, targetContainer) + } + }, + { + when (it) { + is JetFile -> "File: ${it.getName()}" + is JetBlockExpression -> { + (it.getParent() as? JetDeclarationWithBody)?.getText() ?: "...${it.getStatements().first?.getText()}" + } + is JetClassBody -> { + (it.getParent() as? JetClassOrObject)?.getText() + } + else -> it?.getText() + } + }, + "Select target code block" + ) + } + + fun selectMultipleExpressions() { + val startOffset = editor.getSelectionModel().getSelectionStart() + val endOffset = editor.getSelectionModel().getSelectionEnd() + + val elements = CodeInsightUtils.findStatements(file, startOffset, endOffset) + if (elements.isEmpty()) { + noExpressionError() + return + } + + selectTargetContainer(elements.toList()) + } + + fun selectSingleExpression() { + JetRefactoringUtil.selectExpression(editor, file, false) { expr -> + if (expr != null) { + selectTargetContainer(listOf(expr)) + } + else { + if (!editor.getSelectionModel().hasSelection()) { + editor.getSelectionModel().selectLineAtCaret() + } + selectMultipleExpressions() + } + } + } + + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE) + selectSingleExpression() +} diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.form b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.form new file mode 100644 index 00000000000..85baf2a39b5 --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.form @@ -0,0 +1,122 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java new file mode 100644 index 00000000000..a8219aa044a --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2014 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.jet.plugin.refactoring.extractFunction.ui; + +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.psi.PsiElement; +import com.intellij.ui.EditorTextField; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jet.lang.psi.JetClassOrObject; +import org.jetbrains.jet.lang.psi.JetFile; +import org.jetbrains.jet.plugin.refactoring.JetNameSuggester; +import org.jetbrains.jet.plugin.refactoring.JetRefactoringBundle; +import org.jetbrains.jet.plugin.refactoring.RefactoringPackage; +import org.jetbrains.jet.plugin.refactoring.extractFunction.*; +import org.jetbrains.jet.renderer.DescriptorRenderer; + +import javax.swing.*; +import java.util.List; +import java.util.Map; + +public class KotlinExtractFunctionDialog extends DialogWrapper { + private JPanel contentPane; + private JPanel inputParametersPanel; + private JComboBox visibilityBox; + private KotlinFunctionSignatureComponent signaturePreviewField; + private EditorTextField functionNameField; + private JLabel functionNameLabel; + private KotlinParameterTablePanel parameterTablePanel; + + private final Project project; + + private final ExtractionDescriptor originalDescriptor; + private ExtractionDescriptor currentDescriptor; + + public KotlinExtractFunctionDialog(Project project, ExtractionDescriptor originalDescriptor) { + super(project, true); + + this.project = project; + this.originalDescriptor = originalDescriptor; + this.currentDescriptor = originalDescriptor; + + setModal(true); + setTitle(JetRefactoringBundle.message("extract.function")); + init(); + update(false); + } + + private void createUIComponents() { + this.signaturePreviewField = new KotlinFunctionSignatureComponent("", project); + } + + private boolean isVisibilitySectionAvailable() { + PsiElement target = originalDescriptor.getExtractionData().getNextSibling().getParent(); + return target instanceof JetClassOrObject || target instanceof JetFile; + } + + private String getFunctionName() { + return functionNameField.getText(); + } + + private String getVisibility() { + if (!isVisibilitySectionAvailable()) return ""; + + String value = (String) visibilityBox.getSelectedItem(); + return "internal".equals(value) ? "" : value; + } + + private boolean checkNames() { + if (!JetNameSuggester.isIdentifier(getFunctionName())) return false; + for (KotlinParameterTablePanel.ParameterInfo parameterInfo : parameterTablePanel.getParameterInfos()) { + if (!JetNameSuggester.isIdentifier(parameterInfo.getName())) return false; + } + return true; + } + + private void update(boolean recreateDescriptor) { + if (recreateDescriptor) { + this.currentDescriptor = createDescriptor(); + } + + setOKActionEnabled(checkNames()); + signaturePreviewField.setText( + ExtractFunctionPackage.getFunctionText(currentDescriptor, false, DescriptorRenderer.SOURCE_CODE_SHORT_NAMES_IN_TYPES) + ); + } + + @Override + protected void init() { + super.init(); + + functionNameLabel.setLabelFor(functionNameField); + + functionNameField.setText(originalDescriptor.getName()); + functionNameField.addDocumentListener( + new DocumentAdapter() { + @Override + public void documentChanged(DocumentEvent event) { + update(true); + } + } + ); + + boolean enableVisibility = isVisibilitySectionAvailable(); + visibilityBox.setEnabled(enableVisibility); + if (enableVisibility) { + String visibility = originalDescriptor.getVisibility(); + visibilityBox.setSelectedItem(visibility.isEmpty() ? "internal" : visibility); + } + + parameterTablePanel = new KotlinParameterTablePanel() { + @Override + protected void updateSignature() { + KotlinExtractFunctionDialog.this.update(true); + } + + @Override + protected void onEnterAction() { + doOKAction(); + } + + @Override + protected void onCancelAction() { + doCancelAction(); + } + }; + parameterTablePanel.init(originalDescriptor.getParameters()); + inputParametersPanel.add(parameterTablePanel); + } + + @Override + protected void doOKAction() { + ExtractionDescriptorWithConflicts validationResult = ExtractFunctionPackage.validate(currentDescriptor); + if (RefactoringPackage.checkConflictsInteractively(project, validationResult.getConflicts())) { + super.doOKAction(); + } + } + + @Override + public JComponent getPreferredFocusedComponent() { + return functionNameField; + } + + @Override + protected JComponent createCenterPanel() { + return contentPane; + } + + @Override + protected JComponent createContentPane() { + return contentPane; + } + + @NotNull + private ExtractionDescriptor createDescriptor() { + List parameterInfos = parameterTablePanel.getParameterInfos(); + + Map oldToNewParameters = ContainerUtil.newLinkedHashMap(); + for (KotlinParameterTablePanel.ParameterInfo parameterInfo : parameterInfos) { + oldToNewParameters.put(parameterInfo.getOriginalParameter(), parameterInfo.toParameter()); + } + + ControlFlow controlFlow = originalDescriptor.getControlFlow(); + if (controlFlow instanceof ParameterUpdate) { + controlFlow = new ParameterUpdate(oldToNewParameters.get(((ParameterUpdate) controlFlow).getParameter())); + } + + Map replacementMap = ContainerUtil.newHashMap(); + for (Map.Entry e : originalDescriptor.getReplacementMap().entrySet()) { + Integer offset = e.getKey(); + Replacement replacement = e.getValue(); + + if (replacement instanceof ParameterReplacement) { + ParameterReplacement parameterReplacement = (ParameterReplacement) replacement; + Parameter parameter = parameterReplacement.getParameter(); + + Parameter newParameter = oldToNewParameters.get(parameter); + if (newParameter != null) { + replacementMap.put(offset, parameterReplacement.copy(newParameter)); + } + } + else { + replacementMap.put(offset, replacement); + } + } + + return new ExtractionDescriptor( + originalDescriptor.getExtractionData(), + getFunctionName(), + getVisibility(), + ContainerUtil.newArrayList(oldToNewParameters.values()), + originalDescriptor.getReceiverParameter(), + replacementMap, + controlFlow + ); + } + + @NotNull + public ExtractionDescriptor getCurrentDescriptor() { + return currentDescriptor; + } +} diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinFunctionSignatureComponent.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinFunctionSignatureComponent.kt new file mode 100644 index 00000000000..6bdbf3f2ff2 --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinFunctionSignatureComponent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2014 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.jet.plugin.refactoring.extractFunction.ui + +import com.intellij.openapi.project.Project +import org.jetbrains.jet.plugin.JetFileType +import com.intellij.refactoring.ui.MethodSignatureComponent + +public class KotlinFunctionSignatureComponent( + signature: String, project: Project +) : MethodSignatureComponent(signature, project, JetFileType.INSTANCE) { + private val myFileName = "dummy." + JetFileType.INSTANCE.getDefaultExtension() + + override fun getFileName(): String = myFileName +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinParameterTablePanel.java b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinParameterTablePanel.java new file mode 100644 index 00000000000..119c3cb47f2 --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinParameterTablePanel.java @@ -0,0 +1,338 @@ +/* + * Copyright 2010-2014 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.jet.plugin.refactoring.extractFunction.ui; + +import com.intellij.ui.BooleanTableCellRenderer; +import com.intellij.ui.TableUtil; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.table.JBTable; +import com.intellij.util.ui.EditableModel; +import kotlin.Function1; +import kotlin.KotlinPackage; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jet.plugin.refactoring.JetNameSuggester; +import org.jetbrains.jet.plugin.refactoring.extractFunction.Parameter; +import org.jetbrains.jet.renderer.DescriptorRenderer; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.List; + +public class KotlinParameterTablePanel extends JPanel { + public static class ParameterInfo { + private final Parameter originalParameter; + private String name; + private boolean enabled = true; + + public ParameterInfo(Parameter originalParameter) { + this.originalParameter = originalParameter; + this.name = originalParameter.getName(); + } + + public Parameter getOriginalParameter() { + return originalParameter; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTypeAsString() { + return DescriptorRenderer.SHORT_NAMES_IN_TYPES.renderType(getOriginalParameter().getParameterType()); + } + + public Parameter toParameter() { + return new Parameter( + originalParameter.getArgumentText(), + name, + originalParameter.getMirrorVarName(), + originalParameter.getParameterType(), + originalParameter.getReceiverCandidate() + ); + } + } + + private List parameterInfos; + + private JBTable myTable; + private MyTableModel myTableModel; + + public KotlinParameterTablePanel() { + super(new BorderLayout()); + } + + public void init(List parameters) { + parameterInfos = KotlinPackage.map( + parameters, + new Function1() { + @Override + public ParameterInfo invoke(Parameter parameter) { + return new ParameterInfo(parameter); + } + } + ); + + myTableModel = new MyTableModel(); + myTable = new JBTable(myTableModel); + DefaultCellEditor defaultEditor = (DefaultCellEditor) myTable.getDefaultEditor(Object.class); + defaultEditor.setClickCountToStart(1); + + myTable.setTableHeader(null); + myTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableColumn checkBoxColumn = myTable.getColumnModel().getColumn(MyTableModel.CHECKMARK_COLUMN); + TableUtil.setupCheckboxColumn(checkBoxColumn); + checkBoxColumn.setCellRenderer( + new BooleanTableCellRenderer() { + @NotNull + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + Component rendererComponent = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + rendererComponent.setEnabled(KotlinParameterTablePanel.this.isEnabled()); + return rendererComponent; + } + } + ); + + myTable.setPreferredScrollableViewportSize(new Dimension(250, myTable.getRowHeight() * 5)); + myTable.setShowGrid(false); + myTable.setIntercellSpacing(new Dimension(0, 0)); + + @NonNls InputMap inputMap = myTable.getInputMap(); + @NonNls ActionMap actionMap = myTable.getActionMap(); + + // SPACE: toggle enable/disable + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "enable_disable"); + actionMap.put("enable_disable", new AbstractAction() { + @Override + public void actionPerformed(@NotNull ActionEvent e) { + if (myTable.isEditing()) return; + int[] rows = myTable.getSelectedRows(); + if (rows.length > 0) { + boolean valueToBeSet = false; + for (int row : rows) { + if (!parameterInfos.get(row).isEnabled()) { + valueToBeSet = true; + break; + } + } + for (int row : rows) { + parameterInfos.get(row).setEnabled(valueToBeSet); + } + myTableModel.fireTableRowsUpdated(rows[0], rows[rows.length - 1]); + TableUtil.selectRows(myTable, rows); + } + } + }); + + // F2: edit parameter name + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "edit_parameter_name"); + actionMap.put("edit_parameter_name", new AbstractAction() { + @Override + public void actionPerformed(@NotNull ActionEvent e) { + if (!myTable.isEditing()) { + int row = myTable.getSelectedRow(); + if (row >= 0 && row < myTableModel.getRowCount()) { + TableUtil.editCellAt(myTable, row, MyTableModel.PARAMETER_NAME_COLUMN); + } + } + } + }); + + // make ENTER work when the table has focus + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "invoke_impl"); + actionMap.put("invoke_impl", new AbstractAction() { + @Override + public void actionPerformed(@NotNull ActionEvent e) { + TableCellEditor editor = myTable.getCellEditor(); + if (editor != null) { + editor.stopCellEditing(); + } + else { + onEnterAction(); + } + } + }); + + // make ESCAPE work when the table has focus + actionMap.put("doCancel", new AbstractAction() { + @Override + public void actionPerformed(@NotNull ActionEvent e) { + TableCellEditor editor = myTable.getCellEditor(); + if (editor != null) { + editor.stopCellEditing(); + } + else { + onCancelAction(); + } + } + }); + + JPanel listPanel = ToolbarDecorator.createDecorator(myTable).disableAddAction().disableRemoveAction().createPanel(); + add(listPanel, BorderLayout.CENTER); + } + + protected void updateSignature() { + + } + + protected void onEnterAction() { + + } + + protected void onCancelAction() { + + } + + private class MyTableModel extends AbstractTableModel implements EditableModel { + public static final int CHECKMARK_COLUMN = 0; + public static final int PARAMETER_NAME_COLUMN = 1; + public static final int PARAMETER_TYPE_COLUMN = 2; + + @Override + public void addRow() { + throw new IllegalAccessError("Not implemented"); + } + + @Override + public void removeRow(int index) { + throw new IllegalAccessError("Not implemented"); + } + + @Override + public void exchangeRows(int oldIndex, int newIndex) { + if (oldIndex < 0 || newIndex < 0) return; + if (oldIndex >= parameterInfos.size() || newIndex >= parameterInfos.size()) return; + + ParameterInfo old = parameterInfos.get(oldIndex); + parameterInfos.set(oldIndex, parameterInfos.get(newIndex)); + parameterInfos.set(newIndex, old); + + fireTableRowsUpdated(Math.min(oldIndex, newIndex), Math.max(oldIndex, newIndex)); + updateSignature(); + } + + @Override + public boolean canExchangeRows(int oldIndex, int newIndex) { + if (oldIndex < 0 || newIndex < 0) return false; + if (oldIndex >= parameterInfos.size() || newIndex >= parameterInfos.size()) return false; + return true; + } + + @Override + public int getRowCount() { + return parameterInfos.size(); + } + + @Override + public int getColumnCount() { + return 3; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex) { + case CHECKMARK_COLUMN: { + return parameterInfos.get(rowIndex).isEnabled(); + } + case PARAMETER_NAME_COLUMN: { + return parameterInfos.get(rowIndex).getName(); + } + case PARAMETER_TYPE_COLUMN: { + return parameterInfos.get(rowIndex).getTypeAsString(); + } + } + assert false; + return null; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + switch (columnIndex) { + case CHECKMARK_COLUMN: { + parameterInfos.get(rowIndex).setEnabled((Boolean) aValue); + fireTableRowsUpdated(rowIndex, rowIndex); + myTable.getSelectionModel().setSelectionInterval(rowIndex, rowIndex); + updateSignature(); + break; + } + case PARAMETER_NAME_COLUMN: { + ParameterInfo info = parameterInfos.get(rowIndex); + String name = (String) aValue; + if (JetNameSuggester.isIdentifier(name)) { + info.setName(name); + } + updateSignature(); + break; + } + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case CHECKMARK_COLUMN: + return isEnabled(); + case PARAMETER_NAME_COLUMN: + return isEnabled() && parameterInfos.get(rowIndex).isEnabled(); + default: + return false; + } + } + + @NotNull + @Override + public Class getColumnClass(int columnIndex) { + if (columnIndex == CHECKMARK_COLUMN) { + return Boolean.class; + } + return super.getColumnClass(columnIndex); + } + } + + public List getParameterInfos() { + return KotlinPackage.filter( + parameterInfos, + new Function1() { + @Override + public Boolean invoke(ParameterInfo info) { + return info.isEnabled(); + } + } + ); + } +} diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/jetRefactoringUtil.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/jetRefactoringUtil.kt index 489022fe5c6..5ce2235a8e3 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/jetRefactoringUtil.kt +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/jetRefactoringUtil.kt @@ -131,3 +131,33 @@ public inline fun JetFile.createTempCopy(textTransform: (String) -> String): Jet return tmpFile } +public fun PsiElement.getAllExtractionContainers(strict: Boolean): List { + val containers = ArrayList() + + var element: PsiElement? = if (strict) getParent() else this + while (element != null) { + when (element) { + is JetBlockExpression, is JetClassBody, is JetFile -> containers.add(element as JetElement) + } + + element = element!!.getParent() + } + + return containers +} + +public fun Project.checkConflictsInteractively(conflicts: MultiMap): Boolean { + if (conflicts.isEmpty()) return true + + if (ApplicationManager.getApplication()!!.isUnitTestMode()) throw ConflictsInTestsException(conflicts.values()) + + val dialog = ConflictsDialog(this, conflicts) + dialog.show() + return dialog.isOK() +} + +public fun Project.executeWriteCommand(name: String, command: () -> Unit) { + CommandProcessor.getInstance()!!.executeCommand( + this, { ApplicationManager.getApplication()!!.runWriteAction(command) }, name, null + ) +}