inplace rename: no need to revert to continue editing non-identifier; no need to...
[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(true);
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(boolean success) {
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(success);
332     if (success) {
333       PsiDocumentManager.getInstance(myProject).commitAllDocuments();
334       final V variable = getVariable();
335       if (variable == null) {
336         return;
337       }
338       restoreState(variable);
339     }
340   }
341
342   @Override
343   protected void releaseResources() {
344     super.releaseResources();
345     if (myPreview == null) return;
346
347     EditorFactory.getInstance().releaseEditor(myPreview);
348     myPreview = null;
349   }
350
351   @Override
352   protected void addReferenceAtCaret(Collection<PsiReference> refs) {
353     final V variable = getLocalVariable();
354     if (variable != null) {
355       for (PsiReference reference : ReferencesSearch.search(variable)) {
356         refs.add(reference);
357       }
358     } else {
359       refs.clear();
360     }
361   }
362
363   @Override
364   protected void collectAdditionalElementsToRename(List<Pair<PsiElement, TextRange>> stringUsages) {
365     if (isReplaceAllOccurrences()) {
366       for (E expression : getOccurrences()) {
367         stringUsages.add(Pair.<PsiElement, TextRange>create(expression, new TextRange(0, expression.getTextLength())));
368       }
369     }  else if (getExpr() != null) {
370       correctExpression();
371       stringUsages.add(Pair.<PsiElement, TextRange>create(getExpr(), new TextRange(0, getExpr().getTextLength())));
372     }
373
374     final V localVariable = getLocalVariable();
375     if (localVariable != null) {
376       final PsiElement nameIdentifier = localVariable.getNameIdentifier();
377       if (nameIdentifier != null) {
378         int length = nameIdentifier.getTextLength();
379         stringUsages.add(Pair.<PsiElement, TextRange>create(nameIdentifier, new TextRange(0, length)));
380       }
381     }
382   }
383
384   protected void correctExpression() {}
385
386   @Override
387   protected void addHighlights(@NotNull Map<TextRange, TextAttributes> ranges,
388                                @NotNull Editor editor,
389                                @NotNull Collection<RangeHighlighter> highlighters,
390                                @NotNull HighlightManager highlightManager) {
391     final TextAttributes attributes =
392       EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
393     final V variable = getVariable();
394     if (variable != null) {
395       final String name = variable.getName();
396       LOG.assertTrue(name != null, variable);
397       final int variableNameLength = name.length();
398       if (isReplaceAllOccurrences()) {
399         for (RangeMarker marker : getOccurrenceMarkers()) {
400           final int startOffset = marker.getStartOffset();
401           highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null);
402         }
403       }
404       else if (getExpr() != null) {
405         final int startOffset = getExprMarker().getStartOffset();
406         highlightManager.addOccurrenceHighlight(editor, startOffset, startOffset + variableNameLength, attributes, 0, highlighters, null);
407       }
408     }
409
410     for (RangeHighlighter highlighter : highlighters) {
411       highlighter.setGreedyToLeft(true);
412       highlighter.setGreedyToRight(true);
413     }
414   }
415
416   protected void restoreState(final V psiField) {
417     ApplicationManager.getApplication().runWriteAction(new Runnable() {
418       public void run() {
419         final PsiFile containingFile = psiField.getContainingFile();
420         final RangeMarker exprMarker = getExprMarker();
421         if (exprMarker != null) {
422           myExpr = restoreExpression(containingFile, psiField, exprMarker, myExprText);
423           if (myExpr != null && myExpr.isPhysical()) {
424             myExprMarker = createMarker(myExpr);
425           }
426         }
427         if (myLocalMarker != null) {
428           final PsiElement refVariableElement = containingFile.findElementAt(myLocalMarker.getStartOffset());
429           if (refVariableElement != null) {
430             final PsiElement parent = refVariableElement.getParent();
431             if (parent instanceof PsiNamedElement) {
432               ((PsiNamedElement)parent).setName(myLocalName);
433             }
434           }
435
436           final V localVariable = getLocalVariable();
437           if (localVariable != null && localVariable.isPhysical()) {
438             myLocalVariable = localVariable;
439             final PsiElement nameIdentifier = localVariable.getNameIdentifier();
440             if (nameIdentifier != null) {
441               myLocalMarker = createMarker(nameIdentifier);
442             }
443           }
444         }
445         final List<RangeMarker> occurrenceMarkers = getOccurrenceMarkers();
446         for (int i = 0, occurrenceMarkersSize = occurrenceMarkers.size(); i < occurrenceMarkersSize; i++) {
447           RangeMarker marker = occurrenceMarkers.get(i);
448           if (getExprMarker() != null && marker.getStartOffset() == getExprMarker().getStartOffset() && myExpr != null) {
449             myOccurrences[i] = myExpr;
450             continue;
451           }
452           final E psiExpression =
453              restoreExpression(containingFile, psiField, marker, getLocalVariable() != null ? myLocalName : myExprText);
454           if (psiExpression != null) {
455             myOccurrences[i] = psiExpression;
456           }
457         }
458
459         myOccurrenceMarkers = null;
460         deleteTemplateField(psiField);
461       }
462     });
463   }
464
465   protected void deleteTemplateField(V psiField) {
466     if (psiField.isValid()) {
467       psiField.delete();
468     }
469   }
470
471   @Override
472   protected boolean performRefactoring() {
473     if (getLocalVariable() == null && myExpr == null ||
474         getInputName() == null ||
475         getLocalVariable() != null && !getLocalVariable().isValid() ||
476         myExpr != null && !myExpr.isValid()) {
477       super.moveOffsetAfter(false);
478       return false;
479     }
480     if (getLocalVariable() != null) {
481       new WriteCommandAction(myProject, getCommandName(), getCommandName()) {
482         @Override
483         protected void run(Result result) throws Throwable {
484           getLocalVariable().setName(myLocalName);
485         }
486       }.execute();
487     }
488     performIntroduce();
489     V variable = getVariable();
490     if (variable != null) {
491       saveSettings(variable);
492     }
493     return false;
494   }
495
496   @Override
497   protected void moveOffsetAfter(boolean success) {
498     if (getLocalVariable() != null && getLocalVariable().isValid()) {
499       myEditor.getCaretModel().moveToOffset(getLocalVariable().getTextOffset());
500       myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
501     }
502     else if (getExprMarker() != null) {
503       final RangeMarker exprMarker = getExprMarker();
504       if (exprMarker.isValid()) {
505         myEditor.getCaretModel().moveToOffset(exprMarker.getStartOffset());
506         myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
507       }
508     }
509     super.moveOffsetAfter(success);
510     if (myLocalMarker != null && !isRestart()) {
511       myLocalMarker.dispose();
512     }
513     if (success) {
514       performPostIntroduceTasks();
515     }
516   }
517
518   public V getLocalVariable() {
519     if (myLocalVariable != null && myLocalVariable.isValid()) {
520       return myLocalVariable;
521     }
522     if (myLocalMarker != null) {
523       V variable = getVariable();
524       PsiFile containingFile;
525       if (variable != null) {
526         containingFile = variable.getContainingFile();
527       } else {
528         containingFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
529       }
530       PsiNameIdentifierOwner identifierOwner = PsiTreeUtil.getParentOfType(containingFile.findElementAt(myLocalMarker.getStartOffset()),
531                                                                            PsiNameIdentifierOwner.class, false);
532       return identifierOwner != null && identifierOwner.getClass() == myLocalVariable.getClass() ? (V)identifierOwner : null;
533
534     }
535     return myLocalVariable;
536   }
537
538   public void stopIntroduce(Editor editor) {
539     final TemplateState templateState = TemplateManagerImpl.getTemplateState(editor);
540     if (templateState != null) {
541       final Runnable runnable = new Runnable() {
542         public void run() {
543           templateState.gotoEnd(true);
544         }
545       };
546       CommandProcessor.getInstance().executeCommand(myProject, runnable, getCommandName(), getCommandName());
547     }
548   }
549
550   @Override
551   protected void navigateToAlreadyStarted(Document oldDocument, int exitCode) {
552     finish(true);
553     super.navigateToAlreadyStarted(oldDocument, exitCode);
554   }
555
556   @Override
557   protected void showBalloon() {
558     if (myFinished) return;
559     super.showBalloon();
560   }
561
562   public boolean startsOnTheSameElement(E expr, V localVariable) {
563     if (myExprMarker != null && myExprMarker.isValid() && expr != null && myExprMarker.getStartOffset() == expr.getTextOffset()) {
564       return true;
565     }
566
567     if (myLocalMarker != null &&
568         myLocalMarker.isValid() &&
569         localVariable != null &&
570         myLocalMarker.getStartOffset() == localVariable.getTextOffset()) {
571       return true;
572     }
573     return false;
574   }
575
576   public static void unableToStartWarning(Project project, Editor editor, AbstractInplaceIntroducer introducer) {
577     String message = RefactoringBundle
578       .getCannotRefactorMessage(introducer.getCommandName() + " is not finished yet. Unable to start a refactoring");
579     CommonRefactoringUtil.showErrorHint(project, editor, message, null, null);
580   }
581
582   @Nullable
583   public static AbstractInplaceIntroducer getActiveIntroducer(@Nullable Editor editor) {
584     if (editor == null) return null;
585     return editor.getUserData(ACTIVE_INTRODUCE);
586   }
587 }