9c6abc69127e31aa7ea6b8ad888e651c4332a06f
[idea/community.git] / platform / lang-impl / src / com / intellij / lang / SmartEnterProcessorWithFixers.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.intellij.lang;
17
18 import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
19 import com.intellij.codeInsight.lookup.LookupManager;
20 import com.intellij.featureStatistics.FeatureUsageTracker;
21 import com.intellij.openapi.actionSystem.IdeActions;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Document;
24 import com.intellij.openapi.editor.Editor;
25 import com.intellij.openapi.editor.RangeMarker;
26 import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
27 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
28 import com.intellij.openapi.editor.ex.EditorEx;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.util.Key;
31 import com.intellij.psi.PsiElement;
32 import com.intellij.psi.PsiFile;
33 import com.intellij.util.IncorrectOperationException;
34 import com.intellij.util.containers.ContainerUtil;
35 import com.intellij.util.containers.OrderedSet;
36 import org.jetbrains.annotations.NotNull;
37
38 import java.util.ArrayList;
39 import java.util.List;
40
41 /**
42  * @author ignatov
43  */
44 public abstract class SmartEnterProcessorWithFixers extends SmartEnterProcessor {
45   protected static final Logger LOG = Logger.getInstance(SmartEnterProcessorWithFixers.class);
46   protected static final int MAX_ATTEMPTS = 20;
47   protected static final Key<Long> SMART_ENTER_TIMESTAMP = Key.create("smartEnterOriginalTimestamp");
48
49   protected int myFirstErrorOffset = Integer.MAX_VALUE;
50   protected int myAttempt = 0;
51
52   private final List<Fixer<? extends SmartEnterProcessorWithFixers>> myFixers = new ArrayList<Fixer<? extends SmartEnterProcessorWithFixers>>();
53   protected final List<FixEnterProcessor> myEnterProcessors = new ArrayList<FixEnterProcessor>();
54   private final List<FixEnterProcessor> myAfterEnterProcessors = new ArrayList<FixEnterProcessor>();
55
56   protected static void plainEnter(@NotNull final Editor editor) {
57     getEnterHandler().execute(editor, ((EditorEx)editor).getDataContext());
58   }
59
60   protected static EditorActionHandler getEnterHandler() {
61     return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_START_NEW_LINE);
62   }
63
64   protected static boolean isModified(@NotNull final Editor editor) {
65     final Long timestamp = editor.getUserData(SMART_ENTER_TIMESTAMP);
66     assert timestamp != null;
67     return editor.getDocument().getModificationStamp() != timestamp.longValue();
68   }
69
70   public boolean doNotStepInto(PsiElement element) {
71     return false;
72   }
73
74   @Override
75   public boolean process(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile psiFile) {
76     FeatureUsageTracker.getInstance().triggerFeatureUsed("codeassists.complete.statement");
77     return invokeProcessor(project, editor, psiFile, false);
78   }
79
80   @Override
81   public boolean processAfterCompletion(@NotNull Editor editor, @NotNull PsiFile psiFile) {
82     return invokeProcessor(psiFile.getProject(), editor, psiFile, true);
83   }
84
85   protected boolean invokeProcessor(@NotNull final Project project,
86                                     @NotNull final Editor editor,
87                                     @NotNull final PsiFile psiFile,
88                                     boolean afterCompletion) {
89     final Document document = editor.getDocument();
90     final CharSequence textForRollback = document.getImmutableCharSequence();
91     try {
92       editor.putUserData(SMART_ENTER_TIMESTAMP, editor.getDocument().getModificationStamp());
93       myFirstErrorOffset = Integer.MAX_VALUE;
94       process(project, editor, psiFile, 0, afterCompletion);
95     }
96     catch (TooManyAttemptsException e) {
97       document.replaceString(0, document.getTextLength(), textForRollback);
98     }
99     finally {
100       editor.putUserData(SMART_ENTER_TIMESTAMP, null);
101     }
102     return true;
103   }
104
105   protected void process(
106     @NotNull final Project project,
107     @NotNull final Editor editor,
108     @NotNull final PsiFile file,
109     final int attempt,
110     boolean afterCompletion) throws TooManyAttemptsException {
111
112     if (attempt > MAX_ATTEMPTS) throw new TooManyAttemptsException();
113     myAttempt = attempt;
114
115     try {
116       commit(editor);
117       if (myFirstErrorOffset != Integer.MAX_VALUE) {
118         editor.getCaretModel().moveToOffset(myFirstErrorOffset);
119       }
120
121       myFirstErrorOffset = Integer.MAX_VALUE;
122
123       PsiElement atCaret = getStatementAtCaret(editor, file);
124       if (atCaret == null) {
125         processDefaultEnter(project, editor, file);
126         return;
127       }
128
129       OrderedSet<PsiElement> queue = new OrderedSet<PsiElement>();
130       collectAllElements(atCaret, queue, true);
131       queue.add(atCaret);
132
133       for (PsiElement psiElement : queue) {
134         for (Fixer fixer : myFixers) {
135           fixer.apply(editor, this, psiElement);
136           if (LookupManager.getInstance(project).getActiveLookup() != null) {
137             return;
138           }
139           if (isUncommited(project) || !psiElement.isValid()) {
140             moveCaretInsideBracesIfAny(editor, file);
141             process(project, editor, file, attempt + 1, afterCompletion);
142             return;
143           }
144         }
145       }
146
147       doEnter(atCaret, file, editor, afterCompletion);
148     }
149     catch (IncorrectOperationException e) {
150       LOG.error(e);
151     }
152   }
153
154   protected void processDefaultEnter(@NotNull final Project project,
155                                      @NotNull final Editor editor,
156                                      @NotNull final PsiFile file) {}
157
158   protected void collectAllElements(@NotNull PsiElement element, @NotNull OrderedSet<PsiElement> result, boolean recursive) {
159     result.add(0, element);
160     if (doNotStepInto(element)) {
161       if (!recursive) return;
162       recursive = false;
163     }
164
165     collectAdditionalElements(element, result);
166
167     for (PsiElement child : element.getChildren()) {
168       collectAllElements(child, result, recursive);
169     }
170   }
171
172   protected void doEnter(@NotNull PsiElement atCaret, @NotNull PsiFile psiFile, @NotNull Editor editor, boolean afterCompletion)
173     throws IncorrectOperationException {
174     if (myFirstErrorOffset != Integer.MAX_VALUE) {
175       editor.getCaretModel().moveToOffset(myFirstErrorOffset);
176       reformat(atCaret);
177       return;
178     }
179
180     final RangeMarker rangeMarker = createRangeMarker(atCaret);
181     if (reformatBeforeEnter(atCaret)) {
182       reformat(atCaret);
183     }
184     commit(editor);
185
186     PsiElement actualAtCaret = atCaret.isValid()
187       ? atCaret
188       : restoreInvalidElementFromRange(psiFile, rangeMarker.getStartOffset(), rangeMarker.getEndOffset(), atCaret.getClass());
189
190     if (actualAtCaret != null) {
191       for (FixEnterProcessor enterProcessor : myEnterProcessors) {
192         if (enterProcessor.doEnter(actualAtCaret, psiFile, editor, isModified(editor))) {
193           return;
194         }
195       }
196     }
197
198     if (!isModified(editor) && !afterCompletion) {
199       plainEnter(editor);
200     }
201     else {
202       editor.getCaretModel().moveToOffset(myFirstErrorOffset == Integer.MAX_VALUE
203                                           ? (actualAtCaret != null
204                                              ? actualAtCaret.getTextRange().getEndOffset()
205                                              : rangeMarker.getEndOffset())
206                                           : myFirstErrorOffset);
207     }
208     rangeMarker.dispose();
209   }
210
211   protected PsiElement restoreInvalidElementFromRange(@NotNull PsiFile file,
212                                                       int startOffset,
213                                                       int endOffset,
214                                                       @NotNull Class<? extends PsiElement> klass) {
215     LOG.warn("Please, override com.intellij.lang.SmartEnterProcessorWithFixers.restoreInvalidElementFromRange for your language!");
216     return null;
217   }
218
219   protected boolean reformatBeforeEnter(@NotNull PsiElement atCaret) {return true;}
220
221   protected void addEnterProcessors(FixEnterProcessor... processors) {
222     ContainerUtil.addAllNotNull(myEnterProcessors, processors);
223   }
224
225   protected void addAfterEnterProcessors(FixEnterProcessor... processors) {
226     ContainerUtil.addAllNotNull(myAfterEnterProcessors, processors);
227   }
228
229   protected void addFixers(Fixer<? extends SmartEnterProcessorWithFixers>... fixers) {
230     ContainerUtil.addAllNotNull(myFixers, fixers);
231   }
232
233   protected void collectAdditionalElements(@NotNull PsiElement element, @NotNull List<PsiElement> result) {
234   }
235
236   protected void moveCaretInsideBracesIfAny(@NotNull Editor editor, @NotNull PsiFile file) throws IncorrectOperationException {
237   }
238
239   public static class TooManyAttemptsException extends Exception {
240   }
241
242   public abstract static class Fixer<P extends SmartEnterProcessorWithFixers> {
243     abstract public void apply(@NotNull Editor editor, @NotNull P processor, @NotNull PsiElement element) throws IncorrectOperationException;
244   }
245
246   public abstract static class FixEnterProcessor {
247     abstract public boolean doEnter(PsiElement atCaret, PsiFile file, @NotNull Editor editor, boolean modified);
248     
249     protected void plainEnter(@NotNull Editor editor) {
250       SmartEnterProcessorWithFixers.plainEnter(editor);
251     }
252   }
253
254   public void registerUnresolvedError(int offset) {
255     if (myFirstErrorOffset > offset) {
256       myFirstErrorOffset = offset;
257     }
258   }
259 }