CPP-4184 Generate multiple '}' on enter for code with several '{' in one line (commun...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / editorActions / enter / EnterAfterUnmatchedBraceHandler.java
1 /*
2  * Copyright 2000-2015 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
17 package com.intellij.codeInsight.editorActions.enter;
18
19 import com.intellij.codeInsight.CodeInsightSettings;
20 import com.intellij.codeInsight.highlighting.BraceMatcher;
21 import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
22 import com.intellij.lang.ASTNode;
23 import com.intellij.lang.Language;
24 import com.intellij.openapi.actionSystem.DataContext;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.editor.Editor;
28 import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
29 import com.intellij.openapi.editor.ex.EditorEx;
30 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
31 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
32 import com.intellij.openapi.fileTypes.FileType;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.util.Pair;
35 import com.intellij.openapi.util.Ref;
36 import com.intellij.openapi.util.TextRange;
37 import com.intellij.openapi.util.text.StringUtil;
38 import com.intellij.psi.PsiDocumentManager;
39 import com.intellij.psi.PsiElement;
40 import com.intellij.psi.PsiFile;
41 import com.intellij.psi.TokenType;
42 import com.intellij.psi.codeStyle.CodeStyleManager;
43 import com.intellij.psi.tree.IElementType;
44 import com.intellij.psi.util.PsiUtilCore;
45 import com.intellij.util.IncorrectOperationException;
46 import com.intellij.util.text.CharArrayUtil;
47 import org.jetbrains.annotations.NotNull;
48
49 public class EnterAfterUnmatchedBraceHandler extends EnterHandlerDelegateAdapter {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.enter.EnterAfterUnmatchedBraceHandler");
51
52   @Override
53   public Result preprocessEnter(@NotNull final PsiFile file,
54                                 @NotNull final Editor editor,
55                                 @NotNull final Ref<Integer> caretOffsetRef,
56                                 @NotNull final Ref<Integer> caretAdvance,
57                                 @NotNull final DataContext dataContext,
58                                 final EditorActionHandler originalHandler) {
59
60     int caretOffset = caretOffsetRef.get();
61     if (!isApplicable(file, caretOffset)) {
62       return Result.Continue;
63     }
64
65     int maxRBraceCount = getMaxRBraceCount(file, editor, caretOffset);
66     if (maxRBraceCount > 0) {
67       insertRBraces(file, editor,
68                     caretOffset,
69                     getRBraceOffset(file, editor, caretOffset),
70                     adjustRBraceCountForPosition(editor, caretOffset, maxRBraceCount));
71       return Result.DefaultForceIndent;
72     }
73     return Result.Continue;
74   }
75
76   /**
77    * Checks that the text context is in responsibility of the handler.
78    *
79    * @param file        target PSI file
80    * @param caretOffset target caret offset
81    * @return true, if handler is in charge
82    */
83   public boolean isApplicable(@NotNull PsiFile file, int caretOffset) {
84     return true;
85   }
86
87   /**
88    * Calculates the maximum number of '}' that can be inserted by handler.
89    * Can return <code>0</code> or less in custom implementation to skip '}' insertion in the <code>preprocessEnter</code> call
90    * and switch to default implementation.
91    *
92    * @param file        target PSI file
93    * @param editor      target editor
94    * @param caretOffset target caret offset
95    * @return maximum number of '}' that can be inserted by handler, <code>0</code> or less to switch to default implementation
96    */
97   protected int getMaxRBraceCount(@NotNull final PsiFile file, @NotNull final Editor editor, int caretOffset) {
98     if (!CodeInsightSettings.getInstance().INSERT_BRACE_ON_ENTER) {
99       return 0;
100     }
101     return Math.max(0, getUnmatchedLBracesNumberBefore(editor, caretOffset, file.getFileType()));
102   }
103
104   /**
105    * Calculates the string of '}' that have to be inserted by handler.
106    * Some languages can expand the string by additional characters (i.e. '\', ';')
107    *
108    * @param editor         target editor
109    * @param caretOffset    target caret offset
110    * @param maxRBraceCount the maximum number of '}' for insert at position, it always positive
111    * @return the string of '}' that has to be inserted by handler, it must have at least one '}'
112    */
113   protected String adjustRBraceCountForPosition(@NotNull final Editor editor, int caretOffset, int maxRBraceCount) {
114     assert maxRBraceCount > 0;
115
116     CharSequence text = editor.getDocument().getCharsSequence();
117     int bracesToInsert = 0;
118     for (int i = caretOffset - 1; i >= 0 && bracesToInsert < maxRBraceCount; --i) {
119       final char c = text.charAt(i);
120       if (c == '{') {
121         ++bracesToInsert;
122       }
123       else if (isStopChar(c)) {
124         break;
125       }
126     }
127     return StringUtil.repeatSymbol('}', Math.max(bracesToInsert, 1));
128   }
129
130   /**
131    * Checks the character before the inserted '}' to reduce the count of inserted '}'.
132    * The number of inserted '}' will increase for each found '{'.
133    *
134    * @param c character to check
135    * @return true, to stop back iteration
136    */
137   protected boolean isStopChar(char c) {
138     return " \n\t".indexOf(c) < 0;
139   }
140
141   /**
142    * Calculates the position for insertion of one or more '}'.
143    *
144    * @param file        target PSI file
145    * @param editor      target editor
146    * @param caretOffset target caret offset
147    * @return the position between <code>caretOffset</code> and the end of file
148    */
149   protected int getRBraceOffset(@NotNull final PsiFile file, @NotNull final Editor editor, int caretOffset) {
150     CharSequence text = editor.getDocument().getCharsSequence();
151     int offset = CharArrayUtil.shiftForward(text, caretOffset, " \t");
152     final int fileLength = text.length();
153     if (offset < fileLength && ")];,%<?".indexOf(text.charAt(offset)) < 0) {
154       offset = calculateOffsetToInsertClosingBrace(file, text, offset).second;
155       //offset = CharArrayUtil.shiftForwardUntil(text, caretOffset, "\n");
156     }
157     return Math.min(offset, fileLength);
158   }
159
160   /**
161    * Inserts the <code>generatedRBraces</code> at the <code>rBracesInsertOffset</code> position and formats the code block.
162    * @param file                target PSI file
163    * @param editor              target editor
164    * @param caretOffset         target caret offset
165    * @param rBracesInsertOffset target position to insert
166    * @param generatedRBraces    string of '}' to insert
167    */
168   protected void insertRBraces(@NotNull PsiFile file,
169                                @NotNull Editor editor,
170                                int caretOffset,
171                                int rBracesInsertOffset,
172                                String generatedRBraces) {
173     final Document document = editor.getDocument();
174     insertRBracesAtPosition(document, caretOffset, rBracesInsertOffset, generatedRBraces);
175     formatCodeFragmentBetweenBraces(file, document, caretOffset, rBracesInsertOffset, generatedRBraces);
176   }
177
178   /**
179    * Inserts the <code>rBracesCount</code> of '}' at the <code>rBracesInsertOffset</code> position.
180    *
181    * @param document            target document
182    * @param caretOffset         target caret offset
183    * @param rBracesInsertOffset target position to insert
184    * @param generatedRBraces    string of '}' to insert
185    */
186   protected void insertRBracesAtPosition(Document document, int caretOffset, int rBracesInsertOffset, String generatedRBraces) {
187     document.insertString(rBracesInsertOffset, "\n" + generatedRBraces);
188     // We need to adjust indents of the text that will be moved, hence, need to insert preliminary line feed.
189     // Example:
190     //     if (test1()) {
191     //     } else {<caret> if (test2()) {
192     //         foo();
193     //     }
194     // We insert here '\n}' after 'foo();' and have the following:
195     //     if (test1()) {
196     //     } else { if (test2()) {
197     //         foo();
198     //         }
199     //     }
200     // That is formatted incorrectly because line feed between 'else' and 'if' is not inserted yet (whole 'if' block is indent anchor
201     // to 'if' code block('{}')). So, we insert temporary line feed between 'if' and 'else', correct indent and remove that temporary
202     // line feed.
203     document.insertString(caretOffset, "\n");
204   }
205
206   /**
207    * Formats the code block between caret and inserted braces.
208    *
209    * @param file                target PSI file
210    * @param document            target document
211    * @param caretOffset         target caret offset
212    * @param rBracesInsertOffset target position to insert
213    * @param generatedRBraces    string of '}' to insert
214    */
215   protected void formatCodeFragmentBetweenBraces(@NotNull PsiFile file,
216                                                  @NotNull Document document,
217                                                  int caretOffset,
218                                                  int rBracesInsertOffset,
219                                                  String generatedRBraces) {
220     Project project = file.getProject();
221     long stamp = document.getModificationStamp();
222     boolean closingBraceIndentAdjusted;
223     try {
224       PsiDocumentManager.getInstance(project).commitDocument(document);
225       CodeStyleManager.getInstance(project).adjustLineIndent(file, new TextRange(caretOffset, rBracesInsertOffset + 2));
226     }
227     catch (IncorrectOperationException e) {
228       LOG.error(e);
229     }
230     finally {
231       closingBraceIndentAdjusted = stamp != document.getModificationStamp();
232       // do you remember that we insert the '\n'? here we take it back!
233       document.deleteString(caretOffset, caretOffset + 1);
234     }
235
236     // There is a possible case that formatter was unable to adjust line indent for the closing brace (that is the case for plain text
237     // document for example). Hence, we're trying to do the manually.
238     if (!closingBraceIndentAdjusted) {
239       int line = document.getLineNumber(rBracesInsertOffset);
240       StringBuilder buffer = new StringBuilder();
241       int start = document.getLineStartOffset(line);
242       int end = document.getLineEndOffset(line);
243       final CharSequence text = document.getCharsSequence();
244       for (int i = start; i < end; i++) {
245         char c = text.charAt(i);
246         if (c != ' ' && c != '\t') {
247           break;
248         }
249         else {
250           buffer.append(c);
251         }
252       }
253       if (buffer.length() > 0) {
254         document.insertString(rBracesInsertOffset + 1, buffer);
255       }
256     }
257   }
258
259   /**
260    * Current handler inserts closing curly brace (right brace) if necessary. There is a possible case that it should be located
261    * more than one line forward.
262    * <p/>
263    * <b>Example</b>
264    * <pre>
265    *     if (test1()) {
266    *     } else {<caret> if (test2()) {
267    *         foo();
268    *     }
269    * </pre>
270    * <p/>
271    * We want to get this after the processing:
272    * <pre>
273    *     if (test1()) {
274    *     } else {
275    *         if (test2()) {
276    *             foo();
277    *         }
278    *     }
279    * </pre>
280    * I.e. closing brace should be inserted two lines below current caret line. Hence, we need to calculate correct offset
281    * to use for brace inserting. This method is responsible for that.
282    * <p/>
283    * In essence it inspects PSI structure and finds PSE elements with the max length that starts at caret offset. End offset
284    * of that element is used as an insertion point.
285    *
286    * @param file   target PSI file
287    * @param text   text from the given file
288    * @param offset target offset where line feed will be inserted
289    * @return pair of (element, offset). The element is the '}' owner, if applicable; the offset is the position for inserting closing brace
290    */
291   protected Pair<PsiElement, Integer> calculateOffsetToInsertClosingBrace(@NotNull PsiFile file, @NotNull CharSequence text, final int offset) {
292     PsiElement element = PsiUtilCore.getElementAtOffset(file, offset);
293     ASTNode node = element.getNode();
294     if (node != null && node.getElementType() == TokenType.WHITE_SPACE) {
295       return Pair.create(null, CharArrayUtil.shiftForwardUntil(text, offset, "\n"));
296     }
297     for (PsiElement parent = element.getParent(); parent != null; parent = parent.getParent()) {
298       ASTNode parentNode = parent.getNode();
299       if (parentNode == null || parentNode.getStartOffset() != offset) {
300         break;
301       }
302       element = parent;
303     }
304     if (element.getTextOffset() != offset) {
305       return Pair.create(null, CharArrayUtil.shiftForwardUntil(text, offset, "\n"));
306     }
307     return Pair.create(element, element.getTextRange().getEndOffset());
308   }
309
310   public static boolean isAfterUnmatchedLBrace(Editor editor, int offset, FileType fileType) {
311     return getUnmatchedLBracesNumberBefore(editor, offset, fileType) > 0;
312   }
313
314   /**
315    * Calculates number of unmatched left braces before the given offset.
316    *
317    * @param editor   target editor
318    * @param offset   target offset
319    * @param fileType target file type
320    * @return number of unmatched braces before the given offset;
321    * negative value if it's not possible to perform the calculation or if there are no unmatched left braces before
322    * the given offset
323    */
324   protected static int getUnmatchedLBracesNumberBefore(Editor editor, int offset, FileType fileType) {
325     if (offset == 0) {
326       return -1;
327     }
328     CharSequence chars = editor.getDocument().getCharsSequence();
329     if (chars.charAt(offset - 1) != '{') {
330       return -1;
331     }
332
333     EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
334     HighlighterIterator iterator = highlighter.createIterator(offset - 1);
335     BraceMatcher braceMatcher = BraceMatchingUtil.getBraceMatcher(fileType, iterator);
336
337     if (!braceMatcher.isLBraceToken(iterator, chars, fileType) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
338       return -1;
339     }
340
341     Language language = iterator.getTokenType().getLanguage();
342
343     iterator = highlighter.createIterator(0);
344     int lBracesBeforeOffset = 0;
345     int lBracesAfterOffset = 0;
346     int rBracesBeforeOffset = 0;
347     int rBracesAfterOffset = 0;
348     for (; !iterator.atEnd(); iterator.advance()) {
349       IElementType tokenType = iterator.getTokenType();
350       if (!tokenType.getLanguage().equals(language) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
351         continue;
352       }
353
354       boolean beforeOffset = iterator.getStart() < offset;
355
356       if (braceMatcher.isLBraceToken(iterator, chars, fileType)) {
357         if (beforeOffset) {
358           lBracesBeforeOffset++;
359         }
360         else {
361           lBracesAfterOffset++;
362         }
363       }
364       else if (braceMatcher.isRBraceToken(iterator, chars, fileType)) {
365         if (beforeOffset) {
366           rBracesBeforeOffset++;
367         }
368         else {
369           rBracesAfterOffset++;
370         }
371       }
372     }
373
374     return lBracesBeforeOffset - rBracesBeforeOffset - (rBracesAfterOffset - lBracesAfterOffset);
375   }
376 }