PY-15143 Do not allow to remove default value from parameter without any replacement...
[idea/community.git] / python / src / com / jetbrains / python / refactoring / changeSignature / PyChangeSignatureDialog.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.refactoring.changeSignature;
17
18 import com.intellij.lang.LanguageNamesValidation;
19 import com.intellij.lang.refactoring.NamesValidator;
20 import com.intellij.openapi.editor.Document;
21 import com.intellij.openapi.editor.event.DocumentAdapter;
22 import com.intellij.openapi.editor.event.DocumentEvent;
23 import com.intellij.openapi.fileTypes.LanguageFileType;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.ValidationInfo;
26 import com.intellij.openapi.ui.VerticalFlowLayout;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.psi.PsiCodeFragment;
30 import com.intellij.psi.PsiDocumentManager;
31 import com.intellij.psi.util.PsiTreeUtil;
32 import com.intellij.refactoring.BaseRefactoringProcessor;
33 import com.intellij.refactoring.changeSignature.CallerChooserBase;
34 import com.intellij.refactoring.changeSignature.ChangeSignatureDialogBase;
35 import com.intellij.refactoring.changeSignature.ParameterTableModelItemBase;
36 import com.intellij.refactoring.ui.ComboBoxVisibilityPanel;
37 import com.intellij.refactoring.ui.VisibilityPanelBase;
38 import com.intellij.ui.EditorTextField;
39 import com.intellij.ui.components.JBLabel;
40 import com.intellij.ui.treeStructure.Tree;
41 import com.intellij.util.Consumer;
42 import com.intellij.util.IJSwingUtilities;
43 import com.intellij.util.containers.HashSet;
44 import com.intellij.util.ui.UIUtil;
45 import com.intellij.util.ui.table.JBListTable;
46 import com.intellij.util.ui.table.JBTableRow;
47 import com.intellij.util.ui.table.JBTableRowEditor;
48 import com.jetbrains.python.PyBundle;
49 import com.jetbrains.python.PyNames;
50 import com.jetbrains.python.PythonFileType;
51 import com.jetbrains.python.PythonLanguage;
52 import com.jetbrains.python.psi.LanguageLevel;
53 import com.jetbrains.python.psi.PyFunction;
54 import com.jetbrains.python.psi.PyParameterList;
55 import com.jetbrains.python.refactoring.introduce.IntroduceValidator;
56 import org.jetbrains.annotations.NonNls;
57 import org.jetbrains.annotations.Nullable;
58
59 import javax.swing.*;
60 import java.awt.*;
61 import java.awt.event.ItemEvent;
62 import java.awt.event.ItemListener;
63 import java.util.ArrayList;
64 import java.util.List;
65 import java.util.Set;
66
67 /**
68  * User : ktisha
69  */
70
71 public class PyChangeSignatureDialog extends ChangeSignatureDialogBase<PyParameterInfo, PyFunction, String, PyMethodDescriptor, PyParameterTableModelItem, PyParameterTableModel> {
72
73   public PyChangeSignatureDialog(Project project,
74                                  PyMethodDescriptor method) {
75     super(project, method, false, method.getMethod().getContext());
76   }
77
78   @Override
79   protected LanguageFileType getFileType() {
80     return PythonFileType.INSTANCE;
81   }
82
83   @Override
84   protected PyParameterTableModel createParametersInfoModel(PyMethodDescriptor method) {
85     final PyParameterList parameterList = PsiTreeUtil.getChildOfType(method.getMethod(), PyParameterList.class);
86     return new PyParameterTableModel(parameterList, myDefaultValueContext, myProject);
87   }
88
89   @Override
90   protected BaseRefactoringProcessor createRefactoringProcessor() {
91     final List<PyParameterInfo> parameters = getParameters();
92     return new PyChangeSignatureProcessor(myProject, myMethod.getMethod(), getMethodName(),
93                                           parameters.toArray(new PyParameterInfo[parameters.size()]));
94   }
95
96   @Nullable
97   @Override
98   protected PsiCodeFragment createReturnTypeCodeFragment() {
99     return null;
100   }
101
102   @Nullable
103   @Override
104   protected CallerChooserBase<PyFunction> createCallerChooser(String title, Tree treeToReuse, Consumer<Set<PyFunction>> callback) {
105     return null;
106   }
107
108   public boolean isNameValid(final String name, final Project project) {
109     final NamesValidator validator = LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance());
110     return (name != null) &&
111            (validator.isIdentifier(name, project)) &&
112            !(validator.isKeyword(name, project));
113   }
114
115   @Nullable
116   @Override
117   protected String validateAndCommitData() {
118     final String functionName = myNameField.getText().trim();
119     if (!functionName.equals(myMethod.getName())) {
120       final boolean defined = IntroduceValidator.isDefinedInScope(functionName, myMethod.getMethod());
121       if (defined) {
122         return PyBundle.message("refactoring.change.signature.dialog.validation.name.defined");
123       }
124       if (!isNameValid(functionName, myProject)) {
125         return PyBundle.message("refactoring.change.signature.dialog.validation.function.name");
126       }
127     }
128     final List<PyParameterTableModelItem> parameters = myParametersTableModel.getItems();
129     Set<String> parameterNames = new HashSet<String>();
130     boolean hadPositionalContainer = false;
131     boolean hadKeywordContainer = false;
132     boolean hadDefaultValue = false;
133     boolean hadSingleStar = false;
134     boolean hadParamsAfterSingleStar = false;
135     LanguageLevel languageLevel = LanguageLevel.forElement(myMethod.getMethod());
136
137     int parametersLength = parameters.size();
138
139     for (int index = 0; index != parametersLength; ++index) {
140       PyParameterTableModelItem info = parameters.get(index);
141       final PyParameterInfo parameter = info.parameter;
142       final String name = parameter.getName();
143       final String nameWithoutStars = StringUtil.trimLeading(name, '*').trim();
144       if (parameterNames.contains(nameWithoutStars)) {
145         return PyBundle.message("ANN.duplicate.param.name");
146       }
147       parameterNames.add(nameWithoutStars);
148
149       if (name.equals("*")) {
150         hadSingleStar = true;
151         if (index == parametersLength-1) {
152           return PyBundle.message("ANN.named.arguments.after.star");
153         }
154       }
155       else if (name.startsWith("*") && !name.startsWith("**")) {
156         if (hadKeywordContainer) {
157           return PyBundle.message("ANN.starred.param.after.kwparam");
158         }
159         if (hadSingleStar || hadPositionalContainer) {
160           return PyBundle.message("refactoring.change.signature.dialog.validation.multiple.star");
161         }
162         if (!isNameValid(name.substring(1), myProject)) {
163           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
164         }
165         hadPositionalContainer = true;
166       }
167       else if (name.startsWith("**")) {
168         if (hadSingleStar && !hadParamsAfterSingleStar) {
169           return PyBundle.message("ANN.named.arguments.after.star");
170         }
171         if (hadKeywordContainer) {
172           return PyBundle.message("refactoring.change.signature.dialog.validation.multiple.double.star");
173         }
174         if (!isNameValid(name.substring(2), myProject)) {
175           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
176         }
177         hadKeywordContainer = true;
178       }
179       else {
180         if (!isNameValid(name, myProject)) {
181           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
182         }
183         if (hadSingleStar) {
184           hadParamsAfterSingleStar = true;
185         }
186         if (hadPositionalContainer && !languageLevel.isPy3K()) {
187           return PyBundle.message("ANN.regular.param.after.vararg");
188         }
189         else if (hadKeywordContainer) {
190           return PyBundle.message("ANN.regular.param.after.keyword");
191         }
192         final String defaultValue = info.getDefaultValue();
193         if (defaultValue != null && !StringUtil.isEmptyOrSpaces(defaultValue) && parameter.getDefaultInSignature()) {
194           hadDefaultValue = true;
195         }
196         else {
197           if (hadDefaultValue && !hadSingleStar && (!languageLevel.isPy3K() || !hadPositionalContainer)) {
198             return PyBundle.message("ANN.non.default.param.after.default");
199           }
200         }
201       }
202       if (parameter.getOldIndex() < 0) {
203         if (!parameter.getName().startsWith("*")) {
204           if (StringUtil.isEmpty(info.defaultValueCodeFragment.getText())) {
205             return PyBundle.message("refactoring.change.signature.dialog.validation.default.missing");
206           }
207           if (StringUtil.isEmptyOrSpaces(parameter.getName())) {
208             return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.missing");
209           }
210         }
211       }
212       else if (myMethod.getParameters().get(parameter.getOldIndex()).getDefaultInSignature() &&
213                StringUtil.isEmptyOrSpaces(parameter.getDefaultValue())) {
214           return PyBundle.message("refactoring.change.signature.dialog.validation.default.missing");
215       }
216     }
217
218     return null;
219   }
220
221   @Override
222   protected ValidationInfo doValidate() {
223     final String message = validateAndCommitData();
224     SwingUtilities.invokeLater(new Runnable() {
225       public void run() {
226         getRefactorAction().setEnabled(message == null);
227         getPreviewAction().setEnabled(message == null);
228       }
229     });
230     if (message != null) return new ValidationInfo(message);
231     return super.doValidate();
232   }
233
234   @Override
235   public JComponent getPreferredFocusedComponent() {
236     return myNameField;
237   }
238
239   @Override
240   protected String calculateSignature() {
241     @NonNls StringBuilder builder = new StringBuilder();
242     builder.append(getMethodName());
243     builder.append("(");
244     final List<PyParameterTableModelItem> parameters = myParametersTableModel.getItems();
245     for (int i = 0; i != parameters.size(); ++i) {
246       PyParameterTableModelItem parameterInfo = parameters.get(i);
247       builder.append(parameterInfo.parameter.getName());
248       final String defaultValue = parameterInfo.defaultValueCodeFragment.getText();
249       if (!defaultValue.isEmpty() && parameterInfo.isDefaultInSignature()) {
250         builder.append(" = " + defaultValue);
251       }
252       if (i != parameters.size()-1)
253         builder.append(", ");
254     }
255     builder.append(")");
256     return builder.toString();
257   }
258
259   @Override
260   protected VisibilityPanelBase<String> createVisibilityControl() {
261     return new ComboBoxVisibilityPanel<String>(new String[0]);
262   }
263
264   @Override
265   protected JComponent getRowPresentation(ParameterTableModelItemBase<PyParameterInfo> item, boolean selected, final boolean focused) {
266     String text = item.parameter.getName();
267     final String defaultCallValue = item.defaultValueCodeFragment.getText();
268     PyParameterTableModelItem pyItem = (PyParameterTableModelItem)item;
269     final String defaultValue = pyItem.isDefaultInSignature()? pyItem.defaultValueCodeFragment.getText() : "";
270
271     if (StringUtil.isNotEmpty(defaultValue)) {
272       text += " = " + defaultValue;
273     }
274
275     String tail = "";
276     if (StringUtil.isNotEmpty(defaultCallValue)) {
277       tail += " default value = " + defaultCallValue;
278     }
279     if (!StringUtil.isEmpty(tail)) {
280       text += " //" + tail;
281     }
282     return JBListTable.createEditorTextFieldPresentation(getProject(), getFileType(), " " + text, selected, focused);
283   }
284
285   @Override
286   protected boolean isListTableViewSupported() {
287     return true;
288   }
289
290   @Override
291   protected JBTableRowEditor getTableEditor(final JTable t, final ParameterTableModelItemBase<PyParameterInfo> item) {
292     return new JBTableRowEditor() {
293       private EditorTextField myNameEditor;
294       private EditorTextField myDefaultValueEditor;
295       private JCheckBox myDefaultInSignature;
296
297       @Override
298       public void prepareEditor(JTable table, int row) {
299         setLayout(new GridLayout(1, 3));
300         final JPanel parameterPanel = createParameterPanel();
301         add(parameterPanel);
302         final JPanel defaultValuePanel = createDefaultValuePanel();
303         add(defaultValuePanel);
304         final JPanel defaultValueCheckBox = createDefaultValueCheckBox();
305         add(defaultValueCheckBox);
306
307         final String nameText = myNameEditor.getText();
308         myDefaultValueEditor.setEnabled(!nameText.startsWith("*")
309                                         && !PyNames.CANONICAL_SELF.equals(nameText));
310         myDefaultInSignature.setEnabled(!nameText.startsWith("*")
311                                         && !PyNames.CANONICAL_SELF.equals(nameText));
312       }
313
314       private JPanel createDefaultValueCheckBox() {
315         final JPanel defaultValuePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
316
317         final JBLabel inSignatureLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.default.value.checkbox"),
318                                                      UIUtil.ComponentStyle.SMALL);
319         IJSwingUtilities.adjustComponentsOnMac(inSignatureLabel,
320                                                myDefaultInSignature);
321         defaultValuePanel.add(inSignatureLabel, BorderLayout.WEST);
322         myDefaultInSignature = new JCheckBox();
323         myDefaultInSignature.setSelected(
324           ((PyParameterTableModelItem)item).isDefaultInSignature());
325         myDefaultInSignature.addItemListener(new ItemListener() {
326           @Override
327           public void itemStateChanged(ItemEvent event) {
328             ((PyParameterTableModelItem)item)
329               .setDefaultInSignature(myDefaultInSignature.isSelected());
330           }
331         });
332         myDefaultInSignature.addChangeListener(mySignatureUpdater);
333         myDefaultInSignature.setEnabled(item.parameter.getOldIndex() == -1);
334         defaultValuePanel.add(myDefaultInSignature, BorderLayout.EAST);
335         return defaultValuePanel;
336       }
337
338       private JPanel createDefaultValuePanel() {
339         final JPanel defaultValuePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
340         final Document doc = PsiDocumentManager.getInstance(getProject()).getDocument(item.defaultValueCodeFragment);
341         myDefaultValueEditor = new EditorTextField(doc, getProject(), getFileType());
342         final JBLabel defaultValueLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.default.value.label"),
343                                                       UIUtil.ComponentStyle.SMALL);
344         IJSwingUtilities.adjustComponentsOnMac(defaultValueLabel, myDefaultValueEditor);
345         defaultValuePanel.add(defaultValueLabel);
346         defaultValuePanel.add(myDefaultValueEditor);
347         myDefaultValueEditor.setPreferredWidth(t.getWidth() / 2);
348         myDefaultValueEditor.addDocumentListener(mySignatureUpdater);
349         return defaultValuePanel;
350       }
351
352       private JPanel createParameterPanel() {
353         final JPanel namePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
354         myNameEditor = new EditorTextField(item.parameter.getName(), getProject(), getFileType());
355         final JBLabel nameLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.name.label"),
356                                               UIUtil.ComponentStyle.SMALL);
357         IJSwingUtilities.adjustComponentsOnMac(nameLabel, myNameEditor);
358         namePanel.add(nameLabel);
359         namePanel.add(myNameEditor);
360         myNameEditor.setPreferredWidth(t.getWidth() / 2);
361         myNameEditor.addDocumentListener(new DocumentAdapter() {
362           @Override
363           public void documentChanged(DocumentEvent event) {
364             fireDocumentChanged(event, 0);
365             myDefaultValueEditor.setEnabled(!myNameEditor.getText().startsWith("*"));
366             myDefaultInSignature.setEnabled(!myNameEditor.getText().startsWith("*"));
367           }
368         });
369
370         myNameEditor.addDocumentListener(mySignatureUpdater);
371         return namePanel;
372       }
373
374       @Override
375       public JBTableRow getValue() {
376         return new JBTableRow() {
377           @Override
378           public Object getValueAt(int column) {
379             switch (column) {
380               case 0: return myNameEditor.getText().trim();
381               case 1: return new Pair<PsiCodeFragment, Boolean>(item.defaultValueCodeFragment,
382                                                                 ((PyParameterTableModelItem)item).isDefaultInSignature());
383             }
384             return null;
385           }
386         };
387       }
388
389       @Override
390       public JComponent getPreferredFocusedComponent() {
391         return myNameEditor.getFocusTarget();
392       }
393
394       @Override
395       public JComponent[] getFocusableComponents() {
396         final List<JComponent> focusable = new ArrayList<JComponent>();
397         focusable.add(myNameEditor.getFocusTarget());
398         if (myDefaultValueEditor != null) {
399           focusable.add(myDefaultValueEditor.getFocusTarget());
400         }
401         return focusable.toArray(new JComponent[focusable.size()]);
402       }
403     };
404   }
405
406   @Override
407   protected boolean mayPropagateParameters() {
408     return false;
409   }
410
411   @Override
412   protected boolean postponeValidation() {
413     return false;
414   }
415 }