6ed7c8e88f1bd6f8044f91cf9ef8256f3f4e4930
[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 import org.jetbrains.plugins.groovy.refactoring.introduce.field.GrIntroduceFieldHandler;
58
59 import java.util.ArrayList;
60 import java.util.Collections;
61 import java.util.List;
62
63 /**
64  * @author Maxim.Medvedev
65  */
66 public abstract class GrIntroduceHandlerBase<Settings extends GrIntroduceSettings> implements RefactoringActionHandler {
67   protected abstract String getRefactoringName();
68
69   protected abstract String getHelpID();
70
71   @NotNull
72   protected abstract PsiElement findScope(GrExpression expression, GrVariable variable);
73
74   protected abstract void checkExpression(GrExpression selectedExpr) throws GrIntroduceRefactoringError;
75
76   protected abstract void checkVariable(GrVariable variable) throws GrIntroduceRefactoringError;
77
78   protected abstract void checkOccurrences(PsiElement[] occurrences);
79
80   protected abstract GrIntroduceDialog<Settings> getDialog(GrIntroduceContext context);
81
82   @Nullable
83   public abstract GrVariable runRefactoring(GrIntroduceContext context, Settings settings);
84
85   public static List<GrExpression> collectExpressions(final PsiFile file, final Editor editor, final int offset) {
86     int correctedOffset = correctOffset(editor, offset);
87     final PsiElement elementAtCaret = file.findElementAt(correctedOffset);
88     final List<GrExpression> expressions = new ArrayList<GrExpression>();
89
90     for (GrExpression expression = PsiTreeUtil.getParentOfType(elementAtCaret, GrExpression.class);
91          expression != null;
92          expression = PsiTreeUtil.getParentOfType(expression, GrExpression.class)) {
93       if (expressions.contains(expression)) continue;
94       if (expressionIsNotCorrect(expression)) continue;
95
96       expressions.add(expression);
97     }
98     return expressions;
99   }
100
101   private static boolean expressionIsNotCorrect(GrExpression expression) {
102     if (expression instanceof GrParenthesizedExpression) return true;
103     if (expression instanceof GrSuperReferenceExpression) return true;
104     if (expression.getType() == PsiType.VOID) return true;
105     if (expression instanceof GrAssignmentExpression) return true;
106     if (expression instanceof GrReferenceExpression && expression.getParent() instanceof GrCall) {
107       final GroovyResolveResult resolveResult = ((GrReferenceExpression)expression).advancedResolve();
108       final PsiElement resolved = resolveResult.getElement();
109       return resolved instanceof PsiMethod && !resolveResult.isInvokedOnProperty() || resolved instanceof PsiClass;
110     }
111     if (expression instanceof GrApplicationStatement) {
112       return !PsiUtil.isExpressionStatement(expression);
113     }
114     if (expression instanceof GrClosableBlock && expression.getParent() instanceof GrStringInjection) return true;
115
116     return false;
117   }
118
119   public static int correctOffset(Editor editor, int offset) {
120     Document document = editor.getDocument();
121     CharSequence text = document.getCharsSequence();
122     int correctedOffset = offset;
123     int textLength = document.getTextLength();
124     if (offset >= textLength) {
125       correctedOffset = textLength - 1;
126     }
127     else if (!Character.isJavaIdentifierPart(text.charAt(offset))) {
128       correctedOffset--;
129     }
130
131     if (correctedOffset < 0) {
132       correctedOffset = offset;
133     }
134     else {
135       final char c = text.charAt(correctedOffset);
136       if (!Character.isJavaIdentifierPart(c)) {
137         if (c == ';') {//initially caret on the end of line
138           correctedOffset--;
139         }
140         if (c != ')' && c != ']' && c!='}' && c!='\'' && c!='"' && c!='/') {
141           correctedOffset = offset;
142         }
143       }
144     }
145     return correctedOffset;
146   }
147
148   @Nullable
149   public static GrVariable findVariableAtCaret(final PsiFile file, final Editor editor, final int offset) {
150     final int correctOffset = correctOffset(editor, offset);
151     final PsiElement elementAtCaret = file.findElementAt(correctOffset);
152     final GrVariable variable = PsiTreeUtil.getParentOfType(elementAtCaret, GrVariable.class);
153     if (variable != null && variable.getNameIdentifierGroovy().getTextRange().contains(correctOffset)) return variable;
154     return null;
155   }
156
157   public void invoke(final @NotNull Project project, final Editor editor, final PsiFile file, final @Nullable DataContext dataContext) {
158     final SelectionModel selectionModel = editor.getSelectionModel();
159     if (!selectionModel.hasSelection()) {
160       final int offset = editor.getCaretModel().getOffset();
161
162       final List<GrExpression> expressions = collectExpressions(file, editor, offset);
163       if (expressions.isEmpty()) {
164         final GrVariable variable = findVariableAtCaret(file, editor, offset);
165         if (variable == null || variable instanceof GrField || variable instanceof GrParameter) {
166           selectionModel.selectLineAtCaret();
167         }
168         else {
169           final TextRange textRange = variable.getTextRange();
170           selectionModel.setSelection(textRange.getStartOffset(), textRange.getEndOffset());
171         }
172       }
173       else if (expressions.size() == 1) {
174         final TextRange textRange = expressions.get(0).getTextRange();
175         selectionModel.setSelection(textRange.getStartOffset(), textRange.getEndOffset());
176       }
177       else {
178         IntroduceTargetChooser.showChooser(editor, expressions,
179                                            new Pass<GrExpression>() {
180                                              public void pass(final GrExpression selectedValue) {
181                                                invoke(project, editor, file, selectedValue.getTextRange().getStartOffset(),
182                                                       selectedValue.getTextRange().getEndOffset());
183                                              }
184                                            },
185                                            new Function<GrExpression, String>() {
186                                              @Override
187                                              public String fun(GrExpression grExpression) {
188                                                return grExpression.getText();
189                                              }
190                                            });
191         return;
192       }
193     }
194     invoke(project, editor, file, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
195   }
196
197   @Override
198   public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
199     // Does nothing
200   }
201
202   public GrIntroduceContext getContext(Project project, Editor editor, GrExpression expression, @Nullable GrVariable variable) {
203     final PsiElement scope = findScope(expression, variable);
204
205     if (variable == null) {
206       final PsiElement[] occurences = findOccurences(expression, scope);
207       return new GrIntroduceContext(project, editor, expression, occurences, scope, variable);
208
209     }
210     else {
211       final List<PsiElement> list = Collections.synchronizedList(new ArrayList<PsiElement>());
212       ReferencesSearch.search(variable, new LocalSearchScope(scope)).forEach(new Processor<PsiReference>() {
213         @Override
214         public boolean process(PsiReference psiReference) {
215           final PsiElement element = psiReference.getElement();
216           if (element != null) {
217             list.add(element);
218           }
219           return true;
220         }
221       });
222       return new GrIntroduceContext(project, editor, variable.getInitializerGroovy(), list.toArray(new PsiElement[list.size()]), scope,
223                                     variable);
224     }
225   }
226
227   protected PsiElement[] findOccurences(GrExpression expression, PsiElement scope) {
228     final PsiElement[] occurrences = GroovyRefactoringUtil.getExpressionOccurrences(PsiUtil.skipParentheses(expression, false), scope);
229     if (occurrences == null || occurrences.length == 0) {
230       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("no.occurences.found"));
231     }
232     return occurrences;
233   }
234
235   private boolean invoke(final Project project, final Editor editor, PsiFile file, int startOffset, int endOffset) {
236     try {
237       PsiDocumentManager.getInstance(project).commitAllDocuments();
238       if (!(file instanceof GroovyFileBase)) {
239         throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("only.in.groovy.files"));
240       }
241       if (!CommonRefactoringUtil.checkReadOnlyStatus(project, file)) {
242         throw new GrIntroduceRefactoringError(RefactoringBundle.message("readonly.occurences.found"));
243       }
244
245       GrExpression selectedExpr = findExpression(file, startOffset, endOffset);
246       final GrVariable variable = findVariable(file, startOffset, endOffset);
247       if (variable != null) {
248         checkVariable(variable);
249       }
250       else if (selectedExpr != null) {
251         checkExpression(selectedExpr);
252       }
253       else {
254         throw new GrIntroduceRefactoringError(null);
255       }
256
257       final GrIntroduceContext context = getContext(project, editor, selectedExpr, variable);
258       checkOccurrences(context.occurrences);
259       final Settings settings = showDialog(context);
260       if (settings == null) return false;
261
262       CommandProcessor.getInstance().executeCommand(context.project, new Runnable() {
263       public void run() {
264         AccessToken accessToken = WriteAction.start();
265         try {
266           runRefactoring(context, settings);
267         }
268         finally {
269           accessToken.finish();
270         }
271       }
272     }, getRefactoringName(), null);
273
274       return true;
275     }
276     catch (GrIntroduceRefactoringError e) {
277       CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(e.getMessage()), getRefactoringName(), getHelpID());
278       return false;
279     }
280   }
281
282   @Nullable
283   public static GrVariable findVariable(PsiFile file, int startOffset, int endOffset) {
284     GrVariable var = GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrVariable.class);
285     if (var == null) {
286       final GrVariableDeclaration variableDeclaration =
287         GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrVariableDeclaration.class);
288       if (variableDeclaration == null) return null;
289       final GrVariable[] variables = variableDeclaration.getVariables();
290       if (variables.length == 1) {
291         var = variables[0];
292       }
293     }
294     if (var instanceof GrParameter || var instanceof GrField) {
295       return null;
296     }
297     return var;
298   }
299
300   @Nullable
301   public static GrExpression findExpression(PsiFile file, int startOffset, int endOffset) {
302     GrExpression selectedExpr = GroovyRefactoringUtil.findElementInRange(file, startOffset, endOffset, GrExpression.class);
303     while (selectedExpr instanceof GrParenthesizedExpression) selectedExpr = ((GrParenthesizedExpression)selectedExpr).getOperand();
304     if (selectedExpr == null) return null;
305     PsiType type = selectedExpr.getType();
306     if (type != null) type = TypeConversionUtil.erasure(type);
307
308     if (PsiType.VOID.equals(type)) {
309       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("selected.expression.has.void.type"));
310     }
311
312     if (expressionIsNotCorrect(selectedExpr)) {
313       throw new GrIntroduceRefactoringError(GroovyRefactoringBundle.message("selected.block.should.represent.an.expression"));
314     }
315
316     return selectedExpr;
317   }
318
319   @Nullable
320   private Settings showDialog(GrIntroduceContext context) {
321
322     // Add occurences highlighting
323     ArrayList<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>();
324     HighlightManager highlightManager = null;
325     if (context.editor != null) {
326       highlightManager = HighlightManager.getInstance(context.project);
327       EditorColorsManager colorsManager = EditorColorsManager.getInstance();
328       TextAttributes attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
329       if (context.occurrences.length > 1) {
330         highlightManager.addOccurrenceHighlights(context.editor, context.occurrences, attributes, true, highlighters);
331       }
332     }
333
334     GrIntroduceDialog<Settings> dialog = getDialog(context);
335
336     dialog.show();
337     if (dialog.isOK()) {
338       if (context.editor != null) {
339         for (RangeHighlighter highlighter : highlighters) {
340           highlightManager.removeSegmentHighlighter(context.editor, highlighter);
341         }
342       }
343       return dialog.getSettings();
344     }
345     else {
346       if (context.occurrences.length > 1) {
347         WindowManager.getInstance().getStatusBar(context.project)
348           .setInfo(GroovyRefactoringBundle.message("press.escape.to.remove.the.highlighting"));
349       }
350     }
351     return null;
352   }
353
354   @Nullable
355   public static PsiElement findAnchor(GrIntroduceContext context,
356                                        GrIntroduceSettings settings,
357                                        PsiElement[] occurrences,
358                                        final PsiElement container) {
359     if (occurrences.length == 0) return null;
360     PsiElement candidate;
361     if (occurrences.length == 1 || !settings.replaceAllOccurrences()) {
362       candidate = context.expression;
363       candidate = findContainingStatement(candidate);
364     }
365     else {
366       GroovyRefactoringUtil.sortOccurrences(occurrences);
367       candidate = occurrences[0];
368       while (candidate != null && !container.equals(candidate.getParent())) {
369         candidate = candidate.getParent();
370       }
371     }
372
373     final GrStringInjection injection = PsiTreeUtil.getParentOfType(candidate, GrStringInjection.class);
374     if (injection != null && !injection.getText().contains("\n")) {
375       candidate = findContainingStatement(injection);
376     }
377
378     if (candidate == null) return null;
379     
380     if ((container instanceof GrWhileStatement) &&
381         candidate.equals(((GrWhileStatement)container).getCondition())) {
382       return container;
383     }
384     if ((container instanceof GrIfStatement) &&
385         candidate.equals(((GrIfStatement)container).getCondition())) {
386       return container;
387     }
388     if ((container instanceof GrForStatement) &&
389         candidate.equals(((GrForStatement)container).getClause())) {
390       return container;
391     }
392     return candidate;
393   }
394
395   @Nullable
396   private static PsiElement findContainingStatement(PsiElement candidate) {
397     while (candidate != null && !PsiUtil.isExpressionStatement(candidate)) candidate = candidate.getParent();
398     return candidate;
399   }
400
401   protected static void deleteLocalVar(GrIntroduceContext context) {
402     final GrVariable resolved = GrIntroduceFieldHandler.resolveLocalVar(context);
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 }