EA-32658 - initialize field in method while introduce field
[idea/community.git] / plugins / groovy / src / org / jetbrains / plugins / groovy / refactoring / introduce / GrIntroduceHandlerBase.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  * http://www.apache.org/licenses/LICENSE-2.0
7  * Unless required by applicable law or agreed to in writing, software
8  * distributed under the License is distributed on an "AS IS" BASIS,
9  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  * See the License for the specific language governing permissions and
11  * limitations under the License.
12  */
13 package org.jetbrains.plugins.groovy.refactoring.introduce;
14
15 import com.intellij.codeInsight.highlighting.HighlightManager;
16 import com.intellij.openapi.actionSystem.DataContext;
17 import com.intellij.openapi.application.AccessToken;
18 import com.intellij.openapi.application.WriteAction;
19 import com.intellij.openapi.command.CommandProcessor;
20 import com.intellij.openapi.editor.Document;
21 import com.intellij.openapi.editor.Editor;
22 import com.intellij.openapi.editor.SelectionModel;
23 import com.intellij.openapi.editor.colors.EditorColors;
24 import com.intellij.openapi.editor.colors.EditorColorsManager;
25 import com.intellij.openapi.editor.markup.RangeHighlighter;
26 import com.intellij.openapi.editor.markup.TextAttributes;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.util.Pass;
29 import com.intellij.openapi.util.TextRange;
30 import com.intellij.openapi.wm.WindowManager;
31 import com.intellij.psi.*;
32 import com.intellij.psi.search.LocalSearchScope;
33 import com.intellij.psi.search.searches.ReferencesSearch;
34 import com.intellij.psi.util.PsiTreeUtil;
35 import com.intellij.psi.util.TypeConversionUtil;
36 import com.intellij.refactoring.IntroduceTargetChooser;
37 import com.intellij.refactoring.RefactoringActionHandler;
38 import com.intellij.refactoring.RefactoringBundle;
39 import com.intellij.refactoring.util.CommonRefactoringUtil;
40 import com.intellij.util.Function;
41 import com.intellij.util.Processor;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44 import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
45 import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
46 import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
47 import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
48 import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
49 import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
50 import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
51 import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
52 import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
53 import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
54 import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
55 import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;
56 import org.jetbrains.plugins.groovy.refactoring.NameValidator;
57
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.List;
61
62 /**
63  * @author Maxim.Medvedev
64  */
65 public abstract class GrIntroduceHandlerBase<Settings extends GrIntroduceSettings> implements RefactoringActionHandler {
66   protected abstract String getRefactoringName();
67
68   protected abstract String getHelpID();
69
70   @NotNull
71   protected abstract PsiElement findScope(GrExpression expression, GrVariable variable);
72
73   protected abstract void checkExpression(GrExpression selectedExpr) throws GrIntroduceRefactoringError;
74
75   protected abstract void checkVariable(GrVariable variable) throws GrIntroduceRefactoringError;
76
77   protected abstract void checkOccurrences(PsiElement[] occurrences);
78
79   protected abstract GrIntroduceDialog<Settings> getDialog(GrIntroduceContext context);
80
81   @Nullable
82   public abstract GrVariable runRefactoring(GrIntroduceContext context, Settings settings);
83
84   public static List<GrExpression> collectExpressions(final PsiFile file, final Editor editor, final int offset) {
85     int correctedOffset = correctOffset(editor, offset);
86     final PsiElement elementAtCaret = file.findElementAt(correctedOffset);
87     final List<GrExpression> expressions = new ArrayList<GrExpression>();
88
89     for (GrExpression expression = PsiTreeUtil.getParentOfType(elementAtCaret, GrExpression.class);
90          expression != null;
91          expression = PsiTreeUtil.getParentOfType(expression, GrExpression.class)) {
92       if (expressions.contains(expression)) continue;
93       if (expressionIsNotCorrect(expression)) continue;
94
95       expressions.add(expression);
96     }
97     return expressions;
98   }
99
100   private static boolean expressionIsNotCorrect(GrExpression expression) {
101     if (expression instanceof GrParenthesizedExpression) return true;
102     if (expression instanceof GrSuperReferenceExpression) return true;
103     if (expression.getType() == PsiType.VOID) return true;
104     if (expression instanceof GrAssignmentExpression) return true;
105     if (expression instanceof GrReferenceExpression && expression.getParent() instanceof GrCall) {
106       final GroovyResolveResult resolveResult = ((GrReferenceExpression)expression).advancedResolve();
107       final PsiElement resolved = resolveResult.getElement();
108       return resolved instanceof PsiMethod && !resolveResult.isInvokedOnProperty() || resolved instanceof PsiClass;
109     }
110     if (expression instanceof GrApplicationStatement) {
111       return !PsiUtil.isExpressionStatement(expression);
112     }
113     if (expression instanceof GrClosableBlock && expression.getParent() instanceof GrStringInjection) return true;
114
115     return false;
116   }
117
118   public static int correctOffset(Editor editor, int offset) {
119     Document document = editor.getDocument();
120     CharSequence text = document.getCharsSequence();
121     int correctedOffset = offset;
122     int textLength = document.getTextLength();
123     if (offset >= textLength) {
124       correctedOffset = textLength - 1;
125     }
126     else if (!Character.isJavaIdentifierPart(text.charAt(offset))) {
127       correctedOffset--;
128     }
129
130     if (correctedOffset < 0) {
131       correctedOffset = offset;
132     }
133     else {
134       final char c = text.charAt(correctedOffset);
135       if (!Character.isJavaIdentifierPart(c)) {
136         if (c == ';') {//initially caret on the end of line
137           correctedOffset--;
138         }
139         if (c != ')' && c != ']' && c!='}' && c!='\'' && c!='"' && c!='/') {
140           correctedOffset = offset;
141         }
142       }
143     }
144     return correctedOffset;
145   }
146
147   @Nullable
148   public static GrVariable findVariableAtCaret(final PsiFile file, final Editor editor, final int offset) {
149     final int correctOffset = correctOffset(editor, offset);
150     final PsiElement elementAtCaret = file.findElementAt(correctOffset);
151     final GrVariable variable = PsiTreeUtil.getParentOfType(elementAtCaret, GrVariable.class);
152     if (variable != null && variable.getNameIdentifierGroovy().getTextRange().contains(correctOffset)) return variable;
153     return null;
154   }
155
156   public void invoke(final @NotNull Project project, final Editor editor, final PsiFile file, final @Nullable DataContext dataContext) {
157     final SelectionModel selectionModel = editor.getSelectionModel();
158     if (!selectionModel.hasSelection()) {
159       final int offset = editor.getCaretModel().getOffset();
160
161       final List<GrExpression> expressions = collectExpressions(file, editor, offset);
162       if (expressions.isEmpty()) {
163         final GrVariable variable = findVariableAtCaret(file, editor, offset);
164         if (variable == null || variable instanceof GrField || variable instanceof GrParameter) {
165           selectionModel.selectLineAtCaret();
166         }
167         else {
168           final TextRange textRange = variable.getTextRange();
169           selectionModel.setSelection(textRange.getStartOffset(), textRange.getEndOffset());
170         }
171       }
172       else if (expressions.size() == 1) {
173         final TextRange textRange = expressions.get(0).getTextRange();
174         selectionModel.setSelection(textRange.getStartOffset(), textRange.getEndOffset());
175       }
176       else {
177         IntroduceTargetChooser.showChooser(editor, expressions,
178                                            new Pass<GrExpression>() {
179                                              public void pass(final GrExpression selectedValue) {
180                                                invoke(project, editor, file, selectedValue.getTextRange().getStartOffset(),
181                                                       selectedValue.getTextRange().getEndOffset());
182                                              }
183                                            },
184                                            new Function<GrExpression, String>() {
185                                              @Override
186                                              public String fun(GrExpression grExpression) {
187                                                return grExpression.getText();
188                                              }
189                                            });
190         return;
191       }
192     }
193     invoke(project, editor, file, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
194   }
195
196   @Override
197   public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
198     // Does nothing
199   }
200
201   public GrIntroduceContext getContext(Project project, Editor editor, GrExpression expression, @Nullable GrVariable variable) {
202     final PsiElement scope = findScope(expression, variable);
203
204     if (variable == null) {
205       final PsiElement[] occurences = findOccurrences(expression, scope);
206       return new GrIntroduceContext(project, editor, expression, occurences, scope, variable);
207
208     }
209     else {
210       final List<PsiElement> list = Collections.synchronizedList(new ArrayList<PsiElement>());
211       ReferencesSearch.search(variable, new LocalSearchScope(scope)).forEach(new Processor<PsiReference>() {
212         @Override
213         public boolean process(PsiReference psiReference) {
214           final PsiElement element = psiReference.getElement();
215           if (element != null) {
216             list.add(element);
217           }
218           return true;
219         }
220       });
221       return new GrIntroduceContext(project, editor, variable.getInitializerGroovy(), list.toArray(new PsiElement[list.size()]), scope,
222                                     variable);
223     }
224   }
225
226   protected PsiElement[] findOccurrences(GrExpression expression, PsiElement scope) {
227     final PsiElement[] occurrences = GroovyRefactoringUtil.getExpressionOccurrences(PsiUtil.skipParentheses(expression, false), scope);
228     if (occurrences == null || occurrences.length == 0) {
229       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("no.occurrences.found"));
230     }
231     return occurrences;
232   }
233
234   private boolean invoke(final Project project, final Editor editor, PsiFile file, int startOffset, int endOffset) {
235     try {
236       PsiDocumentManager.getInstance(project).commitAllDocuments();
237       if (!(file instanceof GroovyFileBase)) {
238         throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("only.in.groovy.files"));
239       }
240       if (!CommonRefactoringUtil.checkReadOnlyStatus(project, file)) {
241         throw new GrIntroduceRefactoringError(RefactoringBundle.message("readonly.occurences.found"));
242       }
243
244       GrExpression selectedExpr = findExpression(file, startOffset, endOffset);
245       final GrVariable variable = findVariable(file, startOffset, endOffset);
246       if (variable != null) {
247         checkVariable(variable);
248       }
249       else if (selectedExpr != null) {
250         checkExpression(selectedExpr);
251       }
252       else {
253         throw new GrIntroduceRefactoringError(null);
254       }
255
256       final GrIntroduceContext context = getContext(project, editor, selectedExpr, variable);
257       checkOccurrences(context.occurrences);
258       final Settings settings = showDialog(context);
259       if (settings == null) return false;
260
261       CommandProcessor.getInstance().executeCommand(context.project, new Runnable() {
262       public void run() {
263         AccessToken accessToken = WriteAction.start();
264         try {
265           runRefactoring(context, settings);
266         }
267         finally {
268           accessToken.finish();
269         }
270       }
271     }, getRefactoringName(), null);
272
273       return true;
274     }
275     catch (GrIntroduceRefactoringError e) {
276       CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(e.getMessage()), getRefactoringName(), getHelpID());
277       return false;
278     }
279   }
280
281   @Nullable
282   public static GrVariable findVariable(PsiFile file, int startOffset, int endOffset) {
283     GrVariable var = GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrVariable.class);
284     if (var == null) {
285       final GrVariableDeclaration variableDeclaration =
286         GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrVariableDeclaration.class);
287       if (variableDeclaration == null) return null;
288       final GrVariable[] variables = variableDeclaration.getVariables();
289       if (variables.length == 1) {
290         var = variables[0];
291       }
292     }
293     if (var instanceof GrParameter || var instanceof GrField) {
294       return null;
295     }
296     return var;
297   }
298
299   @Nullable
300   public static GrExpression findExpression(PsiFile file, int startOffset, int endOffset) {
301     GrExpression selectedExpr = GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrExpression.class);
302     while (selectedExpr instanceof GrParenthesizedExpression) selectedExpr = ((GrParenthesizedExpression)selectedExpr).getOperand();
303     if (selectedExpr == null) return null;
304     PsiType type = selectedExpr.getType();
305     if (type != null) type = TypeConversionUtil.erasure(type);
306
307     if (PsiType.VOID.equals(type)) {
308       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("selected.expression.has.void.type"));
309     }
310
311     if (expressionIsNotCorrect(selectedExpr)) {
312       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("selected.block.should.represent.an.expression"));
313     }
314
315     return selectedExpr;
316   }
317
318   @Nullable
319   private Settings showDialog(GrIntroduceContext context) {
320
321     // Add occurences highlighting
322     ArrayList<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>();
323     HighlightManager highlightManager = null;
324     if (context.editor != null) {
325       highlightManager = HighlightManager.getInstance(context.project);
326       EditorColorsManager colorsManager = EditorColorsManager.getInstance();
327       TextAttributes attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
328       if (context.occurrences.length > 1) {
329         highlightManager.addOccurrenceHighlights(context.editor, context.occurrences, attributes, true, highlighters);
330       }
331     }
332
333     GrIntroduceDialog<Settings> dialog = getDialog(context);
334
335     dialog.show();
336     if (dialog.isOK()) {
337       if (context.editor != null) {
338         for (RangeHighlighter highlighter : highlighters) {
339           highlightManager.removeSegmentHighlighter(context.editor, highlighter);
340         }
341       }
342       return dialog.getSettings();
343     }
344     else {
345       if (context.occurrences.length > 1) {
346         WindowManager.getInstance().getStatusBar(context.project)
347           .setInfo(GroovyRefactoringBundle.message("press.escape.to.remove.the.highlighting"));
348       }
349     }
350     return null;
351   }
352
353   @Nullable
354   public static PsiElement findAnchor(GrIntroduceContext context,
355                                        GrIntroduceSettings settings,
356                                        PsiElement[] occurrences,
357                                        final PsiElement container) {
358     if (occurrences.length == 0) return null;
359     PsiElement candidate;
360     if (occurrences.length == 1 || !settings.replaceAllOccurrences()) {
361       candidate = context.expression;
362       candidate = findContainingStatement(candidate);
363     }
364     else {
365       GroovyRefactoringUtil.sortOccurrences(occurrences);
366       candidate = occurrences[0];
367       while (candidate != null && !container.equals(candidate.getParent())) {
368         candidate = candidate.getParent();
369       }
370     }
371
372     final GrStringInjection injection = PsiTreeUtil.getParentOfType(candidate, GrStringInjection.class);
373     if (injection != null && !injection.getText().contains("\n")) {
374       candidate = findContainingStatement(injection);
375     }
376
377     if (candidate == null) return null;
378     
379     if ((container instanceof GrWhileStatement) &&
380         candidate.equals(((GrWhileStatement)container).getCondition())) {
381       return container;
382     }
383     if ((container instanceof GrIfStatement) &&
384         candidate.equals(((GrIfStatement)container).getCondition())) {
385       return container;
386     }
387     if ((container instanceof GrForStatement) &&
388         candidate.equals(((GrForStatement)container).getClause())) {
389       return container;
390     }
391     return candidate;
392   }
393
394   @Nullable
395   private static PsiElement findContainingStatement(PsiElement candidate) {
396     while (candidate != null && !PsiUtil.isExpressionStatement(candidate)) candidate = candidate.getParent();
397     return candidate;
398   }
399
400   protected static void deleteLocalVar(GrIntroduceContext context) {
401     final GrVariable resolved = resolveLocalVar(context);
402
403     final PsiElement parent = resolved.getParent();
404     if (parent instanceof GrTupleDeclaration) {
405       if (((GrTupleDeclaration)parent).getVariables().length == 1) {
406         parent.getParent().delete();
407       }
408       else {
409         final GrExpression initializerGroovy = resolved.getInitializerGroovy();
410         if (initializerGroovy != null) initializerGroovy.delete();
411         resolved.delete();
412       }
413     }
414     else {
415       if (((GrVariableDeclaration)parent).getVariables().length == 1) {
416         parent.delete();
417       }
418       else {
419         resolved.delete();
420       }
421     }
422   }
423
424   protected static GrVariable resolveLocalVar(GrIntroduceContext context) {
425     if (context.var != null) return context.var;
426     return (GrVariable)((GrReferenceExpression)context.expression).resolve();
427   }
428
429   public static boolean hasLhs(final PsiElement[] occurrences) {
430     for (PsiElement element : occurrences) {
431       if (element instanceof GrReferenceExpression) {
432         if (PsiUtil.isLValue((GroovyPsiElement)element)) return true;
433         if (ControlFlowUtils.isIncOrDecOperand((GrReferenceExpression)element)) return true;
434       }
435     }
436     return false;
437   }
438
439
440   public interface Validator extends NameValidator {
441     boolean isOK(GrIntroduceDialog dialog);
442   }
443 }