fe3dcfdfb65211c31545b0fdcc045410c176dc02
[idea/community.git] / platform / lang-impl / src / com / intellij / refactoring / introduce / inplace / AbstractInplaceIntroducer.java
1 /*
2  * Copyright 2000-2011 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.intellij.refactoring.introduce.inplace;
17
18 import com.intellij.codeInsight.highlighting.HighlightManager;
19 import com.intellij.codeInsight.template.TextResult;
20 import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
21 import com.intellij.codeInsight.template.impl.TemplateState;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.application.Result;
24 import com.intellij.openapi.command.CommandProcessor;
25 import com.intellij.openapi.command.WriteCommandAction;
26 import com.intellij.openapi.command.impl.StartMarkAction;
27 import com.intellij.openapi.editor.*;
28 import com.intellij.openapi.editor.colors.EditorColors;
29 import com.intellij.openapi.editor.colors.EditorColorsManager;
30 import com.intellij.openapi.editor.event.DocumentAdapter;
31 import com.intellij.openapi.editor.event.DocumentEvent;
32 import com.intellij.openapi.editor.ex.EditorEx;
33 import com.intellij.openapi.editor.markup.RangeHighlighter;
34 import com.intellij.openapi.editor.markup.TextAttributes;
35 import com.intellij.openapi.fileTypes.FileType;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.ui.popup.Balloon;
38 import com.intellij.openapi.util.Key;
39 import com.intellij.openapi.util.Pair;
40 import com.intellij.openapi.util.Ref;
41 import com.intellij.openapi.util.TextRange;
42 import com.intellij.psi.*;
43 import com.intellij.psi.search.searches.ReferencesSearch;
44 import com.intellij.psi.util.PsiTreeUtil;
45 import com.intellij.refactoring.RefactoringBundle;
46 import com.intellij.refactoring.rename.inplace.InplaceRefactoring;
47 import com.intellij.refactoring.util.CommonRefactoringUtil;
48 import com.intellij.ui.DottedBorder;
49 import com.intellij.util.ui.PositionTracker;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import javax.swing.border.EmptyBorder;
55 import javax.swing.border.LineBorder;
56 import java.awt.*;
57 import java.util.*;
58 import java.util.List;
59
60 /**
61  * User: anna
62  * Date: 3/15/11
63  */
64 public abstract class AbstractInplaceIntroducer<V extends PsiNameIdentifierOwner, E extends PsiElement> extends
65                                                                                                         InplaceVariableIntroducer<E> {
66   protected V myLocalVariable;
67   protected RangeMarker myLocalMarker;
68
69   protected final String myExprText;
70   private final String myLocalName;
71
72   public static final Key<AbstractInplaceIntroducer> ACTIVE_INTRODUCE = Key.create("ACTIVE_INTRODUCE");
73
74   private EditorEx myPreview;
75   private final JComponent myPreviewComponent;
76
77   private DocumentAdapter myDocumentAdapter;
78   protected final JPanel myWholePanel;
79   private boolean myFinished = false;
80
81   public AbstractInplaceIntroducer(Project project,
82                                    Editor editor,
83                                    E expr,
84                                    @Nullable V localVariable,
85                                    E[] occurrences,
86                                    String title,
87                                    final FileType languageFileType) {
88     super(null, editor, project, title, occurrences, expr);
89     myLocalVariable = localVariable;
90     if (localVariable != null) {
91       final PsiElement nameIdentifier = localVariable.getNameIdentifier();
92       if (nameIdentifier != null) {
93         myLocalMarker = createMarker(nameIdentifier);
94       }
95     }
96     else {
97       myLocalMarker = null;
98     }
99     myExprText = getExpressionText(expr);
100     myLocalName = localVariable != null ? localVariable.getName() : null;
101
102     myPreview =
103       (EditorEx)EditorFactory.getInstance().createEditor(EditorFactory.getInstance().createDocument(""), project, languageFileType, true);
104     myPreview.setOneLineMode(true);
105     final EditorSettings settings = myPreview.getSettings();
106     settings.setAdditionalLinesCount(0);
107     settings.setAdditionalColumnsCount(1);
108     settings.setRightMarginShown(false);
109     settings.setFoldingOutlineShown(false);
110     settings.setLineNumbersShown(false);
111     settings.setLineMarkerAreaShown(false);
112     settings.setIndentGuidesShown(false);
113     settings.setVirtualSpace(false);
114     myPreview.setHorizontalScrollbarVisible(false);
115     myPreview.setVerticalScrollbarVisible(false);
116     myPreview.setCaretEnabled(false);
117     settings.setLineCursorWidth(1);
118
119     final Color bg = myPreview.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR);
120     myPreview.setBackgroundColor(bg);
121     myPreview.setBorder(BorderFactory.createCompoundBorder(new DottedBorder(Color.gray), new LineBorder(bg, 2)));
122
123     myPreviewComponent = new JPanel(new BorderLayout());
124     myPreviewComponent.add(myPreview.getComponent(), BorderLayout.CENTER);
125     myPreviewComponent.setBorder(new EmptyBorder(2, 2, 6, 2));
126
127     myWholePanel = new JPanel(new GridBagLayout());
128     myWholePanel.setBorder(null);
129
130     showDialogAdvertisement(getActionName());
131   }
132
133   @Nullable
134   protected String getExpressionText(E expr) {
135     return expr != null ? expr.getText() : null;
136   }
137
138   protected final void setPreviewText(final String text) {
139     if (myPreview == null) return; //already disposed
140     ApplicationManager.getApplication().runWriteAction(new Runnable() {
141       @Override
142       public void run() {
143         myPreview.getDocument().replaceString(0, myPreview.getDocument().getTextLength(), text);
144       }
145     });
146   }
147
148   protected final JComponent getPreviewComponent() {
149     return myPreviewComponent;
150   }
151
152   protected final Editor getPreviewEditor() {
153     return myPreview;
154   }
155
156
157   @Override
158   protected StartMarkAction startRename() throws StartMarkAction.AlreadyStartedException {
159     return StartMarkAction.start(myEditor, myProject, getCommandName());
160   }
161
162   /**
163    * Returns ID of the action the shortcut of which is used to show the non-in-place refactoring dialog.
164    *
165    * @return action ID
166    */
167   protected abstract String getActionName();
168
169   /**
170    * Creates an initial version of the declaration for the introduced element. Note that this method is not called in a write action
171    * and most likely needs to create one itself.
172    *
173    * @param replaceAll whether all occurrences are going to be replaced
174    * @param names      the suggested names for the declaration
175    * @return the declaration
176    */
177   @Nullable
178   protected abstract V createFieldToStartTemplateOn(boolean replaceAll, String[] names);
179
180   /**
181    * Returns the suggested names for the introduced element.
182    *
183    * @param replaceAll whether all occurrences are going to be replaced
184    * @param variable   introduced element declaration, if already created.
185    * @return the suggested names
186    */
187   protected abstract String[] suggestNames(boolean replaceAll, @Nullable V variable);
188
189   protected abstract void performIntroduce();
190   protected void performPostIntroduceTasks() {}
191
192   public abstract boolean isReplaceAllOccurrences();
193   public abstract void setReplaceAllOccurrences(boolean allOccurrences);
194   protected abstract JComponent getComponent();
195
196   protected abstract void saveSettings(@NotNull V variable);
197   protected abstract V getVariable();
198
199   public abstract E restoreExpression(PsiFile containingFile, V variable, RangeMarker marker, String exprText);
200
201   /**
202    * Begins the in-place refactoring operation.
203    *
204    * @return true if the in-place refactoring was successfully started, false if it failed to start and a dialog should be shown instead.
205    */
206   public boolean startInplaceIntroduceTemplate() {
207     final boolean replaceAllOccurrences = isReplaceAllOccurrences();
208     final Ref<Boolean> result = new Ref<Boolean>();
209     CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
210       public void run() {
211         final String[] names = suggestNames(replaceAllOccurrences, getLocalVariable());
212         final V variable = createFieldToStartTemplateOn(replaceAllOccurrences, names);
213         boolean started = false;
214         if (variable != null) {
215           int caretOffset = getCaretOffset();
216           myEditor.getCaretModel().moveToOffset(caretOffset);
217           myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
218
219           final LinkedHashSet<String> nameSuggestions = new LinkedHashSet<String>();
220           nameSuggestions.add(variable.getName());
221           nameSuggestions.addAll(Arrays.asList(names));
222           initOccurrencesMarkers();
223           setElementToRename(variable);
224           updateTitle(getVariable());
225           started = AbstractInplaceIntroducer.super.performInplaceRefactoring(nameSuggestions);
226           if (started) {
227             myDocumentAdapter = new DocumentAdapter() {
228               @Override
229               public void documentChanged(DocumentEvent e) {
230                 if (myPreview == null) return;
231                 final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor);
232                 if (templateState != null) {
233                   final TextResult value = templateState.getVariableValue(InplaceRefactoring.PRIMARY_VARIABLE_NAME);
234                   if (value != null) {
235                     updateTitle(getVariable(), value.getText());
236                   }
237                 }
238               }
239             };
240             myEditor.getDocument().addDocumentListener(myDocumentAdapter);
241             updateTitle(getVariable());
242             if (TemplateManagerImpl.getTemplateState(myEditor) != null) {
243               myEditor.putUserData(ACTIVE_INTRODUCE, AbstractInplaceIntroducer.this);
244             }
245           }
246         }
247         result.set(started);
248         if (!started) {
249           finish();
250         }
251       }
252
253     }, getCommandName(), getCommandName());
254     return result.get();
255   }
256
257   protected int getCaretOffset() {
258     RangeMarker r;
259     if (myLocalMarker != null) {
260       final PsiReference reference = myExpr != null ? myExpr.getReference() : null;
261       if (reference != null && reference.resolve() == myLocalVariable) {
262         r = myExprMarker;
263       } else {
264         r = myLocalMarker;
265       }
266     }
267     else {
268       r = myExprMarker;
269     }
270     return r != null ? r.getStartOffset() : 0;
271   }
272
273   protected void updateTitle(@Nullable V variable, String value) {
274     if (variable == null) return;
275
276     setPreviewText(variable.getText().replace(variable.getName(), value));
277     revalidate();
278   }
279
280   protected void updateTitle(@Nullable V variable) {
281     if (variable == null) return;
282     setPreviewText(variable.getText());
283     revalidate();
284   }
285
286   protected void revalidate() {
287     myWholePanel.revalidate();
288     if (myTarget != null) {
289       myBalloon.revalidate(new PositionTracker.Static<Balloon>(myTarget));
290     }
291   }
292
293   public void restartInplaceIntroduceTemplate() {
294     Runnable restartTemplateRunnable = new Runnable() {
295       public void run() {
296         final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor);
297         if (templateState != null) {
298           myEditor.putUserData(INTRODUCE_RESTART, true);
299           try {
300             templateState.gotoEnd(true);
301             startInplaceIntroduceTemplate();
302           }
303           finally {
304             myEditor.putUserData(INTRODUCE_RESTART, false);
305           }
306         }
307         updateTitle(getVariable());
308       }
309     };
310     CommandProcessor.getInstance().executeCommand(myProject, restartTemplateRunnable, getCommandName(), getCommandName());
311   }
312
313   public String getInputName() {
314     return myInsertedName;
315   }
316
317
318   @Override
319   public void finish() {
320     myFinished = true;
321     final TemplateState templateState = TemplateManagerImpl.getTemplateState(myEditor);
322     if (templateState != null) {
323       myEditor.putUserData(ACTIVE_INTRODUCE, null);
324     }
325     if (myDocumentAdapter != null) {
326       myEditor.getDocument().removeDocumentListener(myDocumentAdapter);
327     }
328     if (myBalloon == null) {
329       releaseIfNotRestart();
330     }
331     super.finish();
332     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
333     final V variable = getVariable();
334     if (variable == null) {
335       return;
336     }
337     restoreState(variable);
338   }
339
340   @Override
341   protected void releaseResources() {
342     super.releaseResources();
343     if (myPreview == null) return;
344
345     EditorFactory.getInstance().releaseEditor(myPreview);
346     myPreview = null;
347   }
348
349   @Override
350   protected void addReferenceAtCaret(Collection<PsiReference> refs) {
351     final V variable = getLocalVariable();
352     if (variable != null) {
353       for (PsiReference reference : ReferencesSearch.search(variable)) {
354         refs.add(reference);
355       }
356     } else {
357       refs.clear();
358     }
359   }
360
361   @Override
362   protected void collectAdditionalElementsToRename(List<Pair<PsiElement, TextRange>> stringUsages) {
363     if (isReplaceAllOccurrences()) {
364       for (E expression : getOccurrences()) {
365         stringUsages.add(Pair.<PsiElement, TextRange>create(expression, new TextRange(0, expression.getTextLength())));
366       }
367     }  else if (getExpr() != null) {
368       correctExpression();
369       stringUsages.add(Pair.<PsiElement, TextRange>create(getExpr(), new TextRange(0, getExpr().getTextLength())));
370     }
371
372     final V localVariable = getLocalVariable();
373     if (localVariable != null) {
374       final PsiElement nameIdentifier = localVariable.getNameIdentifier();
375       if (nameIdentifier != null) {
376         int length = nameIdentifier.getTextLength();
377         stringUsages.add(Pair.<PsiElement, TextRange>create(nameIdentifier, new TextRange(0, length)));
378       }
379     }
380   }
381
382   protected void correctExpression() {}
383
384   @Override
385   protected void addHighlights(@NotNull Map<TextRange, TextAttributes> ranges,
386                                @NotNull Editor editor,
387                                @NotNull Collection<RangeHighlighter> highlighters,
388                                @NotNull HighlightManager highlightManager) {
389     final TextAttributes attributes =
390       EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
391     final V variable = getVariable();
392     if (variable != null) {
393       final String name = variable.getName();
394       LOG.assertTrue(name != null, variable);
395       final int variableNameLength = name.length();
396       if (isReplaceAllOccurrences()) {
397         for (RangeMarker marker : getOccurrenceMarkers()) {
398           final int startOffset = marker.getStartOffset();
399           highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null);
400         }
401       }
402       else if (getExpr() != null) {
403         final int startOffset = getExprMarker().getStartOffset();
404         highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null);
405       }
406     }
407
408     for (RangeHighlighter highlighter : highlighters) {
409       highlighter.setGreedyToLeft(true);
410       highlighter.setGreedyToRight(true);
411     }
412   }
413
414   protected void restoreState(final V psiField) {
415     ApplicationManager.getApplication().runWriteAction(new Runnable() {
416       public void run() {
417         final PsiFile containingFile = psiField.getContainingFile();
418         final RangeMarker exprMarker = getExprMarker();
419         if (exprMarker != null) {
420           myExpr = restoreExpression(containingFile, psiField, exprMarker, myExprText);
421           if (myExpr != null && myExpr.isPhysical()) {
422             myExprMarker = createMarker(myExpr);
423           }
424         }
425         if (myLocalMarker != null) {
426           final PsiElement refVariableElement = containingFile.findElementAt(myLocalMarker.getStartOffset());
427           if (refVariableElement != null) {
428             final PsiElement parent = refVariableElement.getParent();
429             if (parent instanceof PsiNamedElement) {
430               ((PsiNamedElement)parent).setName(myLocalName);
431             }
432           }
433
434           final V localVariable = getLocalVariable();
435           if (localVariable != null && localVariable.isPhysical()) {
436             myLocalVariable = localVariable;
437             final PsiElement nameIdentifier = localVariable.getNameIdentifier();
438             if (nameIdentifier != null) {
439               myLocalMarker = createMarker(nameIdentifier);
440             }
441           }
442         }
443         final List<RangeMarker> occurrenceMarkers = getOccurrenceMarkers();
444         for (int i = 0, occurrenceMarkersSize = occurrenceMarkers.size(); i < occurrenceMarkersSize; i++) {
445           RangeMarker marker = occurrenceMarkers.get(i);
446           if (getExprMarker() != null && marker.getStartOffset() == getExprMarker().getStartOffset() && myExpr != null) {
447             myOccurrences[i] = myExpr;
448             continue;
449           }
450           final E psiExpression =
451              restoreExpression(containingFile, psiField, marker, getLocalVariable() != null ? myLocalName : myExprText);
452           if (psiExpression != null) {
453             myOccurrences[i] = psiExpression;
454           }
455         }
456
457         myOccurrenceMarkers = null;
458         deleteTemplateField(psiField);
459       }
460     });
461   }
462
463   protected void deleteTemplateField(V psiField) {
464     if (psiField.isValid()) {
465       psiField.delete();
466     }
467   }
468
469   @Override
470   protected boolean performRefactoring() {
471     if (getLocalVariable() == null && myExpr == null ||
472         getInputName() == null ||
473         getLocalVariable() != null && !getLocalVariable().isValid() ||
474         myExpr != null && !myExpr.isValid()) {
475       super.moveOffsetAfter(false);
476       return false;
477     }
478     if (getLocalVariable() != null) {
479       new WriteCommandAction(myProject, getCommandName(), getCommandName()) {
480         @Override
481         protected void run(Result result) throws Throwable {
482           getLocalVariable().setName(myLocalName);
483         }
484       }.execute();
485     }
486     performIntroduce();
487     V variable = getVariable();
488     if (variable != null) {
489       saveSettings(variable);
490     }
491     return false;
492   }
493
494   @Override
495   protected void moveOffsetAfter(boolean success) {
496     if (getLocalVariable() != null && getLocalVariable().isValid()) {
497       myEditor.getCaretModel().moveToOffset(getLocalVariable().getTextOffset());
498       myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
499     }
500     else if (getExprMarker() != null) {
501       final RangeMarker exprMarker = getExprMarker();
502       if (exprMarker.isValid()) {
503         myEditor.getCaretModel().moveToOffset(exprMarker.getStartOffset());
504         myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
505       }
506     }
507     super.moveOffsetAfter(success);
508     if (myLocalMarker != null && !isRestart()) {
509       myLocalMarker.dispose();
510     }
511     if (success) {
512       performPostIntroduceTasks();
513     }
514   }
515
516   public V getLocalVariable() {
517     if (myLocalVariable != null && myLocalVariable.isValid()) {
518       return myLocalVariable;
519     }
520     if (myLocalMarker != null) {
521       V variable = getVariable();
522       PsiFile containingFile;
523       if (variable != null) {
524         containingFile = variable.getContainingFile();
525       } else {
526         containingFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
527       }
528       PsiNameIdentifierOwner identifierOwner = PsiTreeUtil.getParentOfType(containingFile.findElementAt(myLocalMarker.getStartOffset()),
529                                                                            PsiNameIdentifierOwner.class, false);
530       return identifierOwner != null && identifierOwner.getClass() == myLocalVariable.getClass() ? (V)identifierOwner : null;
531
532     }
533     return myLocalVariable;
534   }
535
536   public void stopIntroduce(Editor editor) {
537     final TemplateState templateState = TemplateManagerImpl.getTemplateState(editor);
538     if (templateState != null) {
539       final Runnable runnable = new Runnable() {
540         public void run() {
541           templateState.gotoEnd(true);
542         }
543       };
544       CommandProcessor.getInstance().executeCommand(myProject, runnable, getCommandName(), getCommandName());
545     }
546   }
547
548   @Override
549   protected void navigateToAlreadyStarted(Document oldDocument, int exitCode) {
550     finish();
551     super.navigateToAlreadyStarted(oldDocument, exitCode);
552   }
553
554   @Override
555   protected void showBalloon() {
556     if (myFinished) return;
557     super.showBalloon();
558   }
559
560   public boolean startsOnTheSameElement(E expr, V localVariable) {
561     if (myExprMarker != null && myExprMarker.isValid() && expr != null && myExprMarker.getStartOffset() == expr.getTextOffset()) {
562       return true;
563     }
564
565     if (myLocalMarker != null &&
566         myLocalMarker.isValid() &&
567         localVariable != null &&
568         myLocalMarker.getStartOffset() == localVariable.getTextOffset()) {
569       return true;
570     }
571     return false;
572   }
573
574   public static void unableToStartWarning(Project project, Editor editor, AbstractInplaceIntroducer introducer) {
575     String message = RefactoringBundle
576       .getCannotRefactorMessage(introducer.getCommandName() + " is not finished yet. Unable to start a refactoring");
577     CommonRefactoringUtil.showErrorHint(project, editor, message, null, null);
578   }
579
580   @Nullable
581   public static AbstractInplaceIntroducer getActiveIntroducer(@Nullable Editor editor) {
582     if (editor == null) return null;
583     return editor.getUserData(ACTIVE_INTRODUCE);
584   }
585 }